From ff6e89052d6e76057ce5b06332ce23592de25621 Mon Sep 17 00:00:00 2001 From: Aaron Bohy Date: Mon, 2 Aug 2021 09:14:29 +0000 Subject: [PATCH] [REF] web,*: regroup custom hooks in core/utils/hooks *base_automation,iap,im_livechat,point_of_sale This commit introduces a new file in core/utils: hooks.js This file contains custom hooks (useEffect, useService, useBus...). The useHotkey hook is tightly related to hotkeys, so it has been moved to core/hotkeys. closes odoo/odoo#74535 Related: odoo/enterprise#20003 Signed-off-by: Lucas Perais (lpe) --- .../src/js/base_automation_error_dialog.js | 2 +- .../js/insufficient_credit_error_handler.js | 2 +- addons/im_livechat/__manifest__.py | 4 +- .../static/src/js/chrome_adapter.js | 4 +- addons/web/static/src/core/autofocus_hook.js | 45 --- addons/web/static/src/core/bus_hook.js | 40 --- .../static/src/core/debug/debug_context.js | 2 +- .../web/static/src/core/debug/debug_menu.js | 2 + .../core/debug/profiling/profiling_item.js | 3 +- addons/web/static/src/core/dialog/dialog.js | 3 +- .../web/static/src/core/dropdown/dropdown.js | 4 +- addons/web/static/src/core/effect_hook.js | 47 --- .../static/src/core/effects/rainbow_man.js | 4 +- .../static/src/core/errors/error_dialogs.js | 6 +- .../static/src/core/file_input/file_input.js | 2 +- .../src/core/{ => hotkeys}/hotkey_hook.js | 5 +- .../static/src/core/popover/popover_hook.js | 2 +- addons/web/static/src/core/service_hook.js | 43 --- addons/web/static/src/core/ui/ui_service.js | 3 +- addons/web/static/src/core/utils/hooks.js | 173 +++++++++++ .../web/static/src/legacy/action_adapters.js | 3 +- .../static/src/legacy/js/core/custom_hooks.js | 2 +- .../legacy/js/fields/abstract_field_owl.js | 2 +- .../legacy/js/views/pivot/pivot_renderer.js | 2 +- addons/web/static/src/legacy/legacy_dialog.js | 4 +- addons/web/static/src/legacy/legacy_views.js | 2 +- .../src/webclient/actions/action_dialog.js | 8 +- .../src/webclient/actions/action_hook.js | 4 +- .../src/webclient/actions/action_service.js | 16 +- .../src/webclient/actions/client_actions.js | 2 +- .../src/webclient/commands/command_hook.js | 3 +- .../src/webclient/commands/command_palette.js | 5 +- .../webclient/commands/default_providers.js | 2 +- .../loading_indicator/loading_indicator.js | 6 +- .../web/static/src/webclient/navbar/navbar.js | 13 +- .../switch_company_menu.js | 8 +- .../src/webclient/user_menu/user_menu.js | 9 +- addons/web/static/src/webclient/webclient.js | 4 +- .../web/static/tests/core/bus_hook_tests.js | 35 --- addons/web/static/tests/core/dialog_tests.js | 2 +- .../static/tests/core/effect_hook_tests.js | 183 ------------ .../core/hotkeys/hotkey_service_tests.js | 2 +- .../tests/core/network/rpc_service_tests.js | 2 +- .../static/tests/core/orm_service_tests.js | 2 +- .../static/tests/core/service_hook_tests.js | 51 ---- .../static/tests/core/utils/hooks_tests.js | 275 ++++++++++++++++++ .../webclient/actions/push_state_tests.js | 2 +- .../tests/webclient/actions/target_tests.js | 9 +- .../static/src/debug/tour_dialog_component.js | 2 +- 49 files changed, 520 insertions(+), 536 deletions(-) delete mode 100644 addons/web/static/src/core/autofocus_hook.js delete mode 100644 addons/web/static/src/core/bus_hook.js delete mode 100644 addons/web/static/src/core/effect_hook.js rename addons/web/static/src/core/{ => hotkeys}/hotkey_hook.js (86%) delete mode 100644 addons/web/static/src/core/service_hook.js create mode 100644 addons/web/static/src/core/utils/hooks.js delete mode 100644 addons/web/static/tests/core/bus_hook_tests.js delete mode 100644 addons/web/static/tests/core/effect_hook_tests.js delete mode 100644 addons/web/static/tests/core/service_hook_tests.js create mode 100644 addons/web/static/tests/core/utils/hooks_tests.js diff --git a/addons/base_automation/static/src/js/base_automation_error_dialog.js b/addons/base_automation/static/src/js/base_automation_error_dialog.js index 4004b0d1673..718c6505a4c 100644 --- a/addons/base_automation/static/src/js/base_automation_error_dialog.js +++ b/addons/base_automation/static/src/js/base_automation_error_dialog.js @@ -3,7 +3,7 @@ import { ErrorDialog } from "@web/core/errors/error_dialogs"; import session from "web.session"; import { registry } from "@web/core/registry"; -import { useService } from "@web/core/service_hook"; +import { useService } from "@web/core/utils/hooks"; export class BaseAutomationErrorDialog extends ErrorDialog { setup() { diff --git a/addons/iap/static/src/js/insufficient_credit_error_handler.js b/addons/iap/static/src/js/insufficient_credit_error_handler.js index e450d413aae..24c76affbf6 100644 --- a/addons/iap/static/src/js/insufficient_credit_error_handler.js +++ b/addons/iap/static/src/js/insufficient_credit_error_handler.js @@ -1,7 +1,7 @@ /** @odoo-module */ import { Dialog } from "@web/core/dialog/dialog"; import { registry } from "@web/core/registry"; -import { useService } from "@web/core/service_hook"; +import { useService } from "@web/core/utils/hooks"; class InsufficientCreditDialog extends Dialog { setup() { diff --git a/addons/im_livechat/__manifest__.py b/addons/im_livechat/__manifest__.py index 8e1e40cc515..d4692bb49f2 100644 --- a/addons/im_livechat/__manifest__.py +++ b/addons/im_livechat/__manifest__.py @@ -99,9 +99,8 @@ Help your customers with this chat, and analyse their feedback. 'web/static/src/core/browser/browser.js', 'web/static/src/core/browser/feature_detection.js', 'web/static/src/core/dialog/dialog.js', - 'web/static/src/core/effect_hook.js', 'web/static/src/core/errors/error_dialogs.js', - 'web/static/src/core/hotkey_hook.js', + 'web/static/src/core/utils/hooks.js', 'web/static/src/core/hotkeys/hotkey_service.js', 'web/static/src/core/l10n/dates.js', 'web/static/src/core/l10n/localization.js', @@ -113,7 +112,6 @@ Help your customers with this chat, and analyse their feedback. 'web/static/src/core/notifications/notification_container.js', 'web/static/src/core/notifications/notification_service.js', 'web/static/src/core/registry.js', - 'web/static/src/core/service_hook.js', 'web/static/src/core/ui/block_ui.js', 'web/static/src/core/ui/ui_service.js', 'web/static/src/core/user_service.js', diff --git a/addons/point_of_sale/static/src/js/chrome_adapter.js b/addons/point_of_sale/static/src/js/chrome_adapter.js index 0e50ed0a24d..9448d11e9aa 100644 --- a/addons/point_of_sale/static/src/js/chrome_adapter.js +++ b/addons/point_of_sale/static/src/js/chrome_adapter.js @@ -1,11 +1,11 @@ /** @odoo-module */ -import { useService } from "@web/core/service_hook"; +import { useService } from "@web/core/utils/hooks"; import Chrome from "point_of_sale.Chrome"; import Registries from "point_of_sale.Registries"; import { configureGui } from "point_of_sale.Gui"; -import { useBus } from "@web/core/bus_hook"; +import { useBus } from "@web/core/utils/hooks"; import PosComponent from "point_of_sale.PosComponent"; import PopupControllerMixin from "point_of_sale.PopupControllerMixin"; import { registry } from "@web/core/registry"; diff --git a/addons/web/static/src/core/autofocus_hook.js b/addons/web/static/src/core/autofocus_hook.js deleted file mode 100644 index c061a1074a7..00000000000 --- a/addons/web/static/src/core/autofocus_hook.js +++ /dev/null @@ -1,45 +0,0 @@ -/** @odoo-module **/ - -import { useEffect } from "./effect_hook"; - -const { useComponent } = owl.hooks; - -// ----------------------------------------------------------------------------- -// Hook functions -// ----------------------------------------------------------------------------- - -/** - * Focus a given selector as soon as it appears in the DOM and if it was not - * displayed before. If the selected target is an input|textarea, set the selection - * at the end. - * - * @param {Object} [params] - * @param {string} [params.selector='autofocus'] default: select the first element - * with an `autofocus` attribute. - * @returns {Function} function that forces the focus on the next update if visible. - */ -export function useAutofocus(params = {}) { - const comp = useComponent(); - // Prevent autofocus in mobile - if (comp.env.isSmall) { - return () => {}; - } - const selector = params.selector || "[autofocus]"; - let forceFocusCount = 0; - useEffect( - function autofocus(target) { - if (target) { - target.focus(); - if (["INPUT", "TEXTAREA"].includes(target.tagName)) { - const inputEl = target; - inputEl.selectionStart = inputEl.selectionEnd = inputEl.value.length; - } - } - }, - () => [comp.el.querySelector(selector), forceFocusCount] - ); - - return function focusOnUpdate() { - forceFocusCount++; // force the effect to rerun on next patch - }; -} diff --git a/addons/web/static/src/core/bus_hook.js b/addons/web/static/src/core/bus_hook.js deleted file mode 100644 index c3f28169a57..00000000000 --- a/addons/web/static/src/core/bus_hook.js +++ /dev/null @@ -1,40 +0,0 @@ -/** @odoo-module **/ -import { useEffect } from "./effect_hook"; -const { useComponent } = owl.hooks; - -/** - * This file contains various custom hooks. - * Their inner working is rather simple: - * Each custom hook simply hook itself to any number of owl lifecycle hooks. - * You can then use them just like an owl hook in any Component - * e.g.: - * import { useBus } from "@web/core/bus_hook"; - * ... - * setup() { - * ... - * useBus(someBus, someEvent, callback) - * ... - * } - */ - -// ----------------------------------------------------------------------------- -// Hook functions -// ----------------------------------------------------------------------------- - -/** - * Ensures a bus event listener is attached and cleared the proper way. - * - * @param {EventBus} bus - * @param {string} eventName - * @param {Callback} callback - */ -export function useBus(bus, eventName, callback) { - const component = useComponent(); - useEffect( - () => { - bus.on(eventName, component, callback); - return () => bus.off(eventName, component); - }, - () => [] - ); -} diff --git a/addons/web/static/src/core/debug/debug_context.js b/addons/web/static/src/core/debug/debug_context.js index a972f76da4a..70d94f29281 100644 --- a/addons/web/static/src/core/debug/debug_context.js +++ b/addons/web/static/src/core/debug/debug_context.js @@ -1,7 +1,7 @@ /** @odoo-module **/ import { registry } from "../registry"; import { memoize } from "../utils/functions"; -import { useEffect } from "../effect_hook"; +import { useEffect } from "@web/core/utils/hooks"; const { useEnv, useSubEnv } = owl.hooks; const debugRegistry = registry.category("debug"); diff --git a/addons/web/static/src/core/debug/debug_menu.js b/addons/web/static/src/core/debug/debug_menu.js index 78b61a8e83c..21b796956ba 100644 --- a/addons/web/static/src/core/debug/debug_menu.js +++ b/addons/web/static/src/core/debug/debug_menu.js @@ -1,5 +1,7 @@ /** @odoo-module **/ + import { useCurrentDebugContext } from "./debug_context"; + const { Component } = owl; export class DebugMenu extends Component { diff --git a/addons/web/static/src/core/debug/profiling/profiling_item.js b/addons/web/static/src/core/debug/profiling/profiling_item.js index 64eb4f890d1..949bde29eea 100644 --- a/addons/web/static/src/core/debug/profiling/profiling_item.js +++ b/addons/web/static/src/core/debug/profiling/profiling_item.js @@ -1,8 +1,7 @@ /** @odoo-module **/ import { DropdownItem } from "@web/core/dropdown/dropdown_item"; -import { useBus } from "@web/core/bus_hook"; -import { useService } from "@web/core/service_hook"; +import { useBus, useService } from "@web/core/utils/hooks"; const { Component } = owl; diff --git a/addons/web/static/src/core/dialog/dialog.js b/addons/web/static/src/core/dialog/dialog.js index 1e2d1f66498..a3aa9700f17 100644 --- a/addons/web/static/src/core/dialog/dialog.js +++ b/addons/web/static/src/core/dialog/dialog.js @@ -1,7 +1,6 @@ /** @odoo-module **/ -import { useHotkey } from "../hotkey_hook"; -import { useService } from "../service_hook"; +import { useHotkey } from "@web/core/hotkeys/hotkey_hook"; import { useActiveElement } from "../ui/ui_service"; const { Component, hooks } = owl; diff --git a/addons/web/static/src/core/dropdown/dropdown.js b/addons/web/static/src/core/dropdown/dropdown.js index 9d37aed99ba..f989a2387b4 100644 --- a/addons/web/static/src/core/dropdown/dropdown.js +++ b/addons/web/static/src/core/dropdown/dropdown.js @@ -1,8 +1,6 @@ /** @odoo-module **/ -import { useBus } from "../bus_hook"; -import { useService } from "../service_hook"; -import { useEffect } from "../effect_hook"; +import { useBus, useEffect, useService } from "@web/core/utils/hooks"; import { scrollTo } from "../utils/scrolling"; import { ParentClosingMode } from "./dropdown_item"; diff --git a/addons/web/static/src/core/effect_hook.js b/addons/web/static/src/core/effect_hook.js deleted file mode 100644 index 7e2b298e97d..00000000000 --- a/addons/web/static/src/core/effect_hook.js +++ /dev/null @@ -1,47 +0,0 @@ -/** @odoo-module */ -const { onMounted, onWillPatch, onPatched, onWillUnmount } = owl.hooks; - -const NO_OP = () => {}; -/** - * @callback Effect - * @param {...any} dependencies the dependencies computed by computeDependencies - * @returns {void|(()=>void)} a cleanup function that reverses the side - * effects of the effect callback. - */ - -/** - * This hook will run a callback when a component is mounted and patched, and - * will run a cleanup function before patching and before unmounting the - * the component. - * - * @param {Effect} effect the effect to run on component mount and/or patch - * @param {()=>any[]} [computeDependencies=()=>[NaN]] a callback to compute - * dependencies that will decide if the effect needs to be cleaned up and - * run again. If the dependencies did not change, the effect will not run - * again. The default value returns an array containing only NaN because - * NaN !== NaN, which will cause the effect to rerun on every patch. - */ -export function useEffect(effect, computeDependencies = () => [NaN]) { - let cleanup, dependencies; - onMounted(() => { - dependencies = computeDependencies(); - cleanup = effect(...dependencies) || NO_OP; - }); - - let shouldReapplyOnPatch = false; - onWillPatch(() => { - const newDeps = computeDependencies(); - shouldReapplyOnPatch = newDeps.some((val, i) => val !== dependencies[i]); - if (shouldReapplyOnPatch) { - cleanup(); - dependencies = newDeps; - } - }); - onPatched(() => { - if (shouldReapplyOnPatch) { - cleanup = effect(...dependencies) || NO_OP; - } - }); - - onWillUnmount(() => cleanup()); -} diff --git a/addons/web/static/src/core/effects/rainbow_man.js b/addons/web/static/src/core/effects/rainbow_man.js index 5b4ca128794..d082e8345d4 100644 --- a/addons/web/static/src/core/effects/rainbow_man.js +++ b/addons/web/static/src/core/effects/rainbow_man.js @@ -1,7 +1,7 @@ /** @odoo-module **/ -import { browser } from "../browser/browser"; -import { useEffect } from "../effect_hook"; +import { browser } from "@web/core/browser/browser"; +import { useEffect } from "@web/core/utils/hooks"; const { Component, hooks } = owl; diff --git a/addons/web/static/src/core/errors/error_dialogs.js b/addons/web/static/src/core/errors/error_dialogs.js index 6bfa3e52a5c..2f45a8a4e15 100644 --- a/addons/web/static/src/core/errors/error_dialogs.js +++ b/addons/web/static/src/core/errors/error_dialogs.js @@ -4,7 +4,7 @@ import { browser } from "../browser/browser"; import { Dialog } from "../dialog/dialog"; import { _lt } from "../l10n/translation"; import { registry } from "../registry"; -import { useService } from "../service_hook"; +import { useService } from "@web/core/utils/hooks"; import { capitalize } from "../utils/strings"; const { hooks } = owl; @@ -20,7 +20,7 @@ export const odooExceptionTitleMap = new Map( "odoo.exceptions.UserError": _lt("User Error"), "odoo.exceptions.ValidationError": _lt("Validation Error"), "odoo.exceptions.AccessError": _lt("Access Error"), - "odoo.exceptions.Warning": _lt("Warning") + "odoo.exceptions.Warning": _lt("Warning"), }) ); @@ -31,7 +31,7 @@ export class ErrorDialog extends Dialog { setup() { super.setup(); this.state = useState({ - showTraceback: false + showTraceback: false, }); } onClickClipboard() { diff --git a/addons/web/static/src/core/file_input/file_input.js b/addons/web/static/src/core/file_input/file_input.js index 7b86febcdf4..6a55f26fd6e 100644 --- a/addons/web/static/src/core/file_input/file_input.js +++ b/addons/web/static/src/core/file_input/file_input.js @@ -1,6 +1,6 @@ /** @odoo-module **/ -import { useService } from "../service_hook"; +import { useService } from "@web/core/utils/hooks"; const { Component, hooks, QWeb } = owl; const { useRef } = hooks; diff --git a/addons/web/static/src/core/hotkey_hook.js b/addons/web/static/src/core/hotkeys/hotkey_hook.js similarity index 86% rename from addons/web/static/src/core/hotkey_hook.js rename to addons/web/static/src/core/hotkeys/hotkey_hook.js index b9cbd554408..96eca0ef296 100644 --- a/addons/web/static/src/core/hotkey_hook.js +++ b/addons/web/static/src/core/hotkeys/hotkey_hook.js @@ -1,7 +1,6 @@ -/** @odoo-module */ +/** @odoo-module **/ -import { useService } from "./service_hook"; -import { useEffect } from "./effect_hook"; +import { useEffect, useService } from "@web/core/utils/hooks"; /** * This hook will register/unregister the given registration diff --git a/addons/web/static/src/core/popover/popover_hook.js b/addons/web/static/src/core/popover/popover_hook.js index 26e1623253a..1b3660cc455 100644 --- a/addons/web/static/src/core/popover/popover_hook.js +++ b/addons/web/static/src/core/popover/popover_hook.js @@ -1,6 +1,6 @@ /** @odoo-module **/ -import { useService } from "../service_hook"; +import { useService } from "@web/core/utils/hooks"; const { onWillUnmount, useComponent } = owl.hooks; diff --git a/addons/web/static/src/core/service_hook.js b/addons/web/static/src/core/service_hook.js deleted file mode 100644 index 5fd2f4ef321..00000000000 --- a/addons/web/static/src/core/service_hook.js +++ /dev/null @@ -1,43 +0,0 @@ -/** @odoo-module **/ - -import { SERVICES_METADATA } from "../env"; - -const { useComponent } = owl.hooks; - -/** - * Import a service into a component - * - * @param {string} serviceName - * @returns {any} - */ -export function useService(serviceName) { - const component = useComponent(); - const { services } = component.env; - if (!(serviceName in services)) { - throw new Error(`Service ${serviceName} is not available`); - } - const service = services[serviceName]; - if (serviceName in SERVICES_METADATA) { - if (service instanceof Function) { - return protectMethod(component, null, service); - } else { - const methods = SERVICES_METADATA[serviceName]; - const result = Object.create(service); - for (let method of methods) { - result[method] = protectMethod(component, service, service[method]); - } - return result; - } - } - return service; -} - -function protectMethod(component, caller, fn) { - return async (...args) => { - if (component.__owl__.status === 5 /* DESTROYED */) { - throw new Error("Component is destroyed"); - } - const result = await fn.call(caller, ...args); - return component.__owl__.status === 5 ? new Promise(() => {}) : result; - }; -} diff --git a/addons/web/static/src/core/ui/ui_service.js b/addons/web/static/src/core/ui/ui_service.js index 44828d16e2e..0c14a8077cf 100644 --- a/addons/web/static/src/core/ui/ui_service.js +++ b/addons/web/static/src/core/ui/ui_service.js @@ -1,9 +1,8 @@ /** @odoo-module **/ -import { useService } from "@web/core/service_hook"; +import { useEffect, useService } from "@web/core/utils/hooks"; import { registry } from "@web/core/registry"; import { debounce } from "@web/core/utils/timing"; -import { useEffect } from "@web/core/effect_hook"; import { BlockUI } from "./block_ui"; const { Component, core, hooks } = owl; diff --git a/addons/web/static/src/core/utils/hooks.js b/addons/web/static/src/core/utils/hooks.js new file mode 100644 index 00000000000..fabcf18aeeb --- /dev/null +++ b/addons/web/static/src/core/utils/hooks.js @@ -0,0 +1,173 @@ +/** @odoo-module **/ + +import { SERVICES_METADATA } from "@web/env"; + +const { onMounted, onWillPatch, onPatched, onWillUnmount, useComponent } = owl.hooks; + +/** + * This file contains various custom hooks. + * Their inner working is rather simple: + * Each custom hook simply hooks itself to any number of owl lifecycle hooks. + * You can then use them just like an owl hook in any Component + * e.g.: + * import { useBus } from "@web/core/utils/hooks"; + * ... + * setup() { + * ... + * useBus(someBus, someEvent, callback) + * ... + * } + */ + +// ----------------------------------------------------------------------------- +// useAutofocus +// ----------------------------------------------------------------------------- + +/** + * Focus a given selector as soon as it appears in the DOM and if it was not + * displayed before. If the selected target is an input|textarea, set the selection + * at the end. + * + * @param {Object} [params] + * @param {string} [params.selector='autofocus'] default: select the first element + * with an `autofocus` attribute. + * @returns {Function} function that forces the focus on the next update if visible. + */ +export function useAutofocus(params = {}) { + const comp = useComponent(); + // Prevent autofocus in mobile + if (comp.env.isSmall) { + return () => {}; + } + const selector = params.selector || "[autofocus]"; + let forceFocusCount = 0; + useEffect( + function autofocus(target) { + if (target) { + target.focus(); + if (["INPUT", "TEXTAREA"].includes(target.tagName)) { + const inputEl = target; + inputEl.selectionStart = inputEl.selectionEnd = inputEl.value.length; + } + } + }, + () => [comp.el.querySelector(selector), forceFocusCount] + ); + + return function focusOnUpdate() { + forceFocusCount++; // force the effect to rerun on next patch + }; +} + +// ----------------------------------------------------------------------------- +// useBus +// ----------------------------------------------------------------------------- + +/** + * Ensures a bus event listener is attached and cleared the proper way. + * + * @param {EventBus} bus + * @param {string} eventName + * @param {Callback} callback + */ +export function useBus(bus, eventName, callback) { + const component = useComponent(); + useEffect( + () => { + bus.on(eventName, component, callback); + return () => bus.off(eventName, component); + }, + () => [] + ); +} + +// ----------------------------------------------------------------------------- +// useEffect +// ----------------------------------------------------------------------------- + +const NO_OP = () => {}; +/** + * @callback Effect + * @param {...any} dependencies the dependencies computed by computeDependencies + * @returns {void|(()=>void)} a cleanup function that reverses the side + * effects of the effect callback. + */ + +/** + * This hook will run a callback when a component is mounted and patched, and + * will run a cleanup function before patching and before unmounting the + * the component. + * + * @param {Effect} effect the effect to run on component mount and/or patch + * @param {()=>any[]} [computeDependencies=()=>[NaN]] a callback to compute + * dependencies that will decide if the effect needs to be cleaned up and + * run again. If the dependencies did not change, the effect will not run + * again. The default value returns an array containing only NaN because + * NaN !== NaN, which will cause the effect to rerun on every patch. + */ +export function useEffect(effect, computeDependencies = () => [NaN]) { + let cleanup, dependencies; + onMounted(() => { + dependencies = computeDependencies(); + cleanup = effect(...dependencies) || NO_OP; + }); + + let shouldReapplyOnPatch = false; + onWillPatch(() => { + const newDeps = computeDependencies(); + shouldReapplyOnPatch = newDeps.some((val, i) => val !== dependencies[i]); + if (shouldReapplyOnPatch) { + cleanup(); + dependencies = newDeps; + } + }); + onPatched(() => { + if (shouldReapplyOnPatch) { + cleanup = effect(...dependencies) || NO_OP; + } + }); + + onWillUnmount(() => cleanup()); +} + +// ----------------------------------------------------------------------------- +// useService +// ----------------------------------------------------------------------------- + +function _protectMethod(component, caller, fn) { + return async (...args) => { + if (component.__owl__.status === 5 /* DESTROYED */) { + throw new Error("Component is destroyed"); + } + const result = await fn.call(caller, ...args); + return component.__owl__.status === 5 ? new Promise(() => {}) : result; + }; +} + +/** + * Import a service into a component + * + * @param {string} serviceName + * @returns {any} + */ +export function useService(serviceName) { + const component = useComponent(); + const { services } = component.env; + if (!(serviceName in services)) { + throw new Error(`Service ${serviceName} is not available`); + } + const service = services[serviceName]; + if (serviceName in SERVICES_METADATA) { + if (service instanceof Function) { + return _protectMethod(component, null, service); + } else { + const methods = SERVICES_METADATA[serviceName]; + const result = Object.create(service); + for (let method of methods) { + result[method] = _protectMethod(component, service, service[method]); + } + return result; + } + } + return service; +} diff --git a/addons/web/static/src/legacy/action_adapters.js b/addons/web/static/src/legacy/action_adapters.js index 1f4d159e278..333e22fbe2d 100644 --- a/addons/web/static/src/legacy/action_adapters.js +++ b/addons/web/static/src/legacy/action_adapters.js @@ -6,8 +6,7 @@ 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 { useEffect } from "../core/effect_hook"; -import { useService } from "../core/service_hook"; +import { useEffect, useService } from "@web/core/utils/hooks"; import { ViewNotFoundError } from "../webclient/actions/action_service"; import { cleanDomFromBootstrap, wrapSuccessOrFail, mapDoActionOptionAPI } from "./utils"; diff --git a/addons/web/static/src/legacy/js/core/custom_hooks.js b/addons/web/static/src/legacy/js/core/custom_hooks.js index 44515ee3cb5..8b5d9834d0a 100644 --- a/addons/web/static/src/legacy/js/core/custom_hooks.js +++ b/addons/web/static/src/legacy/js/core/custom_hooks.js @@ -2,7 +2,7 @@ odoo.define('web.custom_hooks', function (require) { "use strict"; const { Component } = owl; - const { useEffect } = require("@web/core/effect_hook"); + const { useEffect } = require("@web/core/utils/hooks"); /** * Focus a given selector as soon as it appears in the DOM and if it was not diff --git a/addons/web/static/src/legacy/js/fields/abstract_field_owl.js b/addons/web/static/src/legacy/js/fields/abstract_field_owl.js index afce40c9ebb..4f63fdbacc1 100644 --- a/addons/web/static/src/legacy/js/fields/abstract_field_owl.js +++ b/addons/web/static/src/legacy/js/fields/abstract_field_owl.js @@ -3,7 +3,7 @@ odoo.define('web.AbstractFieldOwl', function (require) { const field_utils = require('web.field_utils'); const { useListener } = require('web.custom_hooks'); - const { useEffect } = require('@web/core/effect_hook'); + const { useEffect } = require("@web/core/utils/hooks"); /** * This file defines the Owl version of the AbstractField. Specific fields diff --git a/addons/web/static/src/legacy/js/views/pivot/pivot_renderer.js b/addons/web/static/src/legacy/js/views/pivot/pivot_renderer.js index 97a1eb2b5e3..09e2e44085c 100644 --- a/addons/web/static/src/legacy/js/views/pivot/pivot_renderer.js +++ b/addons/web/static/src/legacy/js/views/pivot/pivot_renderer.js @@ -1,6 +1,6 @@ /** @odoo-module alias=web.PivotRenderer **/ - import { useEffect } from "@web/core/effect_hook"; + import { useEffect } from "@web/core/utils/hooks"; import DropdownMenu from 'web.DropdownMenu'; import DropdownMenuItem from 'web.DropdownMenuItem'; import OwlAbstractRenderer from '../abstract_renderer_owl'; diff --git a/addons/web/static/src/legacy/legacy_dialog.js b/addons/web/static/src/legacy/legacy_dialog.js index 192a204323b..ffa7c9283b7 100644 --- a/addons/web/static/src/legacy/legacy_dialog.js +++ b/addons/web/static/src/legacy/legacy_dialog.js @@ -2,11 +2,9 @@ import { Dialog } from "../core/dialog/dialog"; import { patch } from "@web/core/utils/patch"; -import { useEffect } from "@web/core/effect_hook"; +import { useEffect } from "@web/core/utils/hooks"; import OwlDialog from "web.OwlDialog"; -const { hooks } = owl; - /** * This is a patch of the new Dialog class. * Its purpose is to inform the old "active/inactive" mechanism. diff --git a/addons/web/static/src/legacy/legacy_views.js b/addons/web/static/src/legacy/legacy_views.js index e5268ae32df..7079ead2ce7 100644 --- a/addons/web/static/src/legacy/legacy_views.js +++ b/addons/web/static/src/legacy/legacy_views.js @@ -1,7 +1,7 @@ /** @odoo-module **/ const { Component, hooks, tags } = owl; -import { useService } from "../core/service_hook"; +import { useService } from "@web/core/utils/hooks"; import { useSetupAction } from "../webclient/actions/action_hook"; import legacyViewRegistry from "web.view_registry"; import { ViewAdapter } from "./action_adapters"; diff --git a/addons/web/static/src/webclient/actions/action_dialog.js b/addons/web/static/src/webclient/actions/action_dialog.js index cfba24d6149..caa40f95c84 100644 --- a/addons/web/static/src/webclient/actions/action_dialog.js +++ b/addons/web/static/src/webclient/actions/action_dialog.js @@ -1,9 +1,9 @@ /** @odoo-module **/ -import { Dialog } from "../../core/dialog/dialog"; -import { DebugMenu } from "../../core/debug/debug_menu"; -import { useOwnDebugContext } from "../../core/debug/debug_context"; -import { useEffect } from "../../core/effect_hook"; +import { Dialog } from "@web/core/dialog/dialog"; +import { DebugMenu } from "@web/core/debug/debug_menu"; +import { useOwnDebugContext } from "@web/core/debug/debug_context"; +import { useEffect } from "@web/core/utils/hooks"; const { hooks } = owl; diff --git a/addons/web/static/src/webclient/actions/action_hook.js b/addons/web/static/src/webclient/actions/action_hook.js index c592f2daf4a..f64fedf2f4c 100644 --- a/addons/web/static/src/webclient/actions/action_hook.js +++ b/addons/web/static/src/webclient/actions/action_hook.js @@ -1,7 +1,7 @@ /** @odoo-module **/ -import { getScrollPosition, setScrollPosition } from "../../core/utils/scrolling"; -import { useEffect } from "../../core/effect_hook"; +import { getScrollPosition, setScrollPosition } from "@web/core/utils/scrolling"; +import { useEffect } from "@web/core/utils/hooks"; const { useComponent } = owl.hooks; diff --git a/addons/web/static/src/webclient/actions/action_service.js b/addons/web/static/src/webclient/actions/action_service.js index 0de69ce8c82..98460c64c08 100644 --- a/addons/web/static/src/webclient/actions/action_service.js +++ b/addons/web/static/src/webclient/actions/action_service.js @@ -1,14 +1,14 @@ /** @odoo-module **/ import { cleanDomFromBootstrap } from "@web/legacy/utils"; -import { browser } from "../../core/browser/browser"; -import { useBus } from "../../core/bus_hook"; -import { makeContext } from "../../core/context"; -import { download } from "../../core/network/download"; -import { evaluateExpr } from "../../core/py_js/py"; -import { registry } from "../../core/registry"; -import { KeepLast } from "../../core/utils/concurrency"; -import { sprintf } from "../../core/utils/strings"; +import { browser } from "@web/core/browser/browser"; +import { useBus } from "@web/core/utils/hooks"; +import { makeContext } from "@web/core/context"; +import { download } from "@web/core/network/download"; +import { evaluateExpr } from "@web/core/py_js/py"; +import { registry } from "@web/core/registry"; +import { KeepLast } from "@web/core/utils/concurrency"; +import { sprintf } from "@web/core/utils/strings"; import { ActionDialog } from "./action_dialog"; /** @typedef {number|false} ActionId */ diff --git a/addons/web/static/src/webclient/actions/client_actions.js b/addons/web/static/src/webclient/actions/client_actions.js index e70527b7509..d056e854695 100644 --- a/addons/web/static/src/webclient/actions/client_actions.js +++ b/addons/web/static/src/webclient/actions/client_actions.js @@ -1,7 +1,7 @@ /** @odoo-module **/ import { registry } from "@web/core/registry"; -import { useService } from "@web/core/service_hook"; +import { useService } from "@web/core/utils/hooks"; import { sprintf } from "@web/core/utils/strings"; const { utils, Component } = owl; diff --git a/addons/web/static/src/webclient/commands/command_hook.js b/addons/web/static/src/webclient/commands/command_hook.js index 3dbd8800aa1..13fff8ad997 100644 --- a/addons/web/static/src/webclient/commands/command_hook.js +++ b/addons/web/static/src/webclient/commands/command_hook.js @@ -1,7 +1,6 @@ /** @odoo-module **/ -import { useEffect } from "@web/core/effect_hook"; -import { useService } from "@web/core/service_hook"; +import { useEffect, useService } from "@web/core/utils/hooks"; /** * @typedef {import("./command_service").CommandServiceAddOptions} CommandServiceAddOptions diff --git a/addons/web/static/src/webclient/commands/command_palette.js b/addons/web/static/src/webclient/commands/command_palette.js index 2a3e09c37ff..0db99be24a3 100644 --- a/addons/web/static/src/webclient/commands/command_palette.js +++ b/addons/web/static/src/webclient/commands/command_palette.js @@ -1,9 +1,8 @@ /** @odoo-module **/ -import { useAutofocus } from "@web/core/autofocus_hook"; -import { useHotkey } from "@web/core/hotkey_hook"; +import { useAutofocus, useService } from "@web/core/utils/hooks"; +import { useHotkey } from "@web/core/hotkeys/hotkey_hook"; import { registry } from "@web/core/registry"; -import { useService } from "@web/core/service_hook"; import { KeepLast } from "@web/core/utils/concurrency"; import { scrollTo } from "@web/core/utils/scrolling"; import { fuzzyLookup } from "@web/core/utils/search"; diff --git a/addons/web/static/src/webclient/commands/default_providers.js b/addons/web/static/src/webclient/commands/default_providers.js index 55216c8ff0a..9fb14f3e614 100644 --- a/addons/web/static/src/webclient/commands/default_providers.js +++ b/addons/web/static/src/webclient/commands/default_providers.js @@ -1,7 +1,7 @@ /** @odoo-module **/ import { isMacOS } from "@web/core/browser/feature_detection"; -import { useHotkey } from "@web/core/hotkey_hook"; +import { useHotkey } from "@web/core/hotkeys/hotkey_hook"; import { registry } from "@web/core/registry"; import { capitalize } from "@web/core/utils/strings"; import { getVisibleElements } from "@web/core/utils/ui"; diff --git a/addons/web/static/src/webclient/loading_indicator/loading_indicator.js b/addons/web/static/src/webclient/loading_indicator/loading_indicator.js index ce4dea56bfb..77750083d59 100644 --- a/addons/web/static/src/webclient/loading_indicator/loading_indicator.js +++ b/addons/web/static/src/webclient/loading_indicator/loading_indicator.js @@ -1,8 +1,8 @@ /** @odoo-module **/ -import { browser } from "../../core/browser/browser"; -import { registry } from "../../core/registry"; -import { useService } from "../../core/service_hook"; +import { browser } from "@web/core/browser/browser"; +import { registry } from "@web/core/registry"; +import { useService } from "@web/core/utils/hooks"; const { Component, useState } = owl; diff --git a/addons/web/static/src/webclient/navbar/navbar.js b/addons/web/static/src/webclient/navbar/navbar.js index e635feef055..7cc04cf90a3 100644 --- a/addons/web/static/src/webclient/navbar/navbar.js +++ b/addons/web/static/src/webclient/navbar/navbar.js @@ -1,12 +1,11 @@ /** @odoo-module **/ -import { Dropdown } from "../../core/dropdown/dropdown"; -import { DropdownItem } from "../../core/dropdown/dropdown_item"; -import { useService } from "../../core/service_hook"; -import { useEffect } from "../../core/effect_hook"; -import { registry } from "../../core/registry"; -import { debounce } from "../../core/utils/timing"; -import { ErrorHandler, NotUpdatable } from "../../core/utils/components"; +import { Dropdown } from "@web/core/dropdown/dropdown"; +import { DropdownItem } from "@web/core/dropdown/dropdown_item"; +import { useEffect, useService } from "@web/core/utils/hooks"; +import { registry } from "@web/core/registry"; +import { debounce } from "@web/core/utils/timing"; +import { ErrorHandler, NotUpdatable } from "@web/core/utils/components"; const { Component, hooks } = owl; const { useExternalListener, useRef } = hooks; diff --git a/addons/web/static/src/webclient/switch_company_menu/switch_company_menu.js b/addons/web/static/src/webclient/switch_company_menu/switch_company_menu.js index 0688c2617e3..690a29b6dcf 100644 --- a/addons/web/static/src/webclient/switch_company_menu/switch_company_menu.js +++ b/addons/web/static/src/webclient/switch_company_menu/switch_company_menu.js @@ -1,9 +1,9 @@ /** @odoo-module **/ -import { useService } from "../../core/service_hook"; -import { registry } from "../../core/registry"; -import { browser } from "../../core/browser/browser"; -import { symmetricalDifference } from "../../core/utils/arrays"; +import { useService } from "@web/core/utils/hooks"; +import { registry } from "@web/core/registry"; +import { browser } from "@web/core/browser/browser"; +import { symmetricalDifference } from "@web/core/utils/arrays"; const { Component, hooks } = owl; const { useState } = hooks; diff --git a/addons/web/static/src/webclient/user_menu/user_menu.js b/addons/web/static/src/webclient/user_menu/user_menu.js index e2caac2fa7e..abc94a7367f 100644 --- a/addons/web/static/src/webclient/user_menu/user_menu.js +++ b/addons/web/static/src/webclient/user_menu/user_menu.js @@ -1,10 +1,9 @@ /** @odoo-module **/ -import { browser } from "../../core/browser/browser"; -import { DropdownItem } from "../../core/dropdown/dropdown_item"; -import { useService } from "../../core/service_hook"; -import { registry } from "../../core/registry"; -import { useEffect } from "../../core/effect_hook"; +import { browser } from "@web/core/browser/browser"; +import { DropdownItem } from "@web/core/dropdown/dropdown_item"; +import { registry } from "@web/core/registry"; +import { useEffect, useService } from "@web/core/utils/hooks"; const { Component } = owl; diff --git a/addons/web/static/src/webclient/webclient.js b/addons/web/static/src/webclient/webclient.js index e81a058205f..3bcd54e6fdb 100644 --- a/addons/web/static/src/webclient/webclient.js +++ b/addons/web/static/src/webclient/webclient.js @@ -1,10 +1,8 @@ /** @odoo-module **/ -import { useService } from "../core/service_hook"; -import { useBus } from "../core/bus_hook"; import { ActionContainer } from "./actions/action_container"; import { NavBar } from "./navbar/navbar"; -import { useEffect } from "@web/core/effect_hook"; +import { useBus, useEffect, useService } from "@web/core/utils/hooks"; import { NotUpdatable } from "../core/utils/components"; import { MainComponentsContainer } from "../core/main_components_container"; import { useOwnDebugContext } from "../core/debug/debug_context"; diff --git a/addons/web/static/tests/core/bus_hook_tests.js b/addons/web/static/tests/core/bus_hook_tests.js deleted file mode 100644 index dd1985fad57..00000000000 --- a/addons/web/static/tests/core/bus_hook_tests.js +++ /dev/null @@ -1,35 +0,0 @@ -/** @odoo-module **/ - -import { useBus } from "@web/core/bus_hook"; -import { makeTestEnv } from "../helpers/mock_env"; -import { getFixture, nextTick } from "../helpers/utils"; - -const { Component, mount, tags } = owl; - -QUnit.module("useBus"); - -QUnit.test("useBus hook: simple usecase", async function (assert) { - class MyComponent extends Component { - setup() { - useBus(this.env.bus, "test-event", this.myCallback); - } - myCallback() { - assert.step("callback"); - } - } - MyComponent.template = tags.xml`
`; - - const env = await makeTestEnv(); - const target = getFixture(); - const comp = await mount(MyComponent, { env, target }); - env.bus.trigger("test-event"); - await nextTick(); - assert.verifySteps(["callback"]); - - comp.unmount(); - env.bus.trigger("test-event"); - await nextTick(); - assert.verifySteps([]); - - comp.destroy(); -}); diff --git a/addons/web/static/tests/core/dialog_tests.js b/addons/web/static/tests/core/dialog_tests.js index 8165bd9f096..3e7a82fc390 100644 --- a/addons/web/static/tests/core/dialog_tests.js +++ b/addons/web/static/tests/core/dialog_tests.js @@ -1,7 +1,7 @@ /** @odoo-module **/ import { registry } from "@web/core/registry"; -import { useService } from "@web/core/service_hook"; +import { useService } from "@web/core/utils/hooks"; import { uiService } from "@web/core/ui/ui_service"; import { hotkeyService } from "@web/core/hotkeys/hotkey_service"; import { Dialog } from "@web/core/dialog/dialog"; diff --git a/addons/web/static/tests/core/effect_hook_tests.js b/addons/web/static/tests/core/effect_hook_tests.js deleted file mode 100644 index 55f827f17ef..00000000000 --- a/addons/web/static/tests/core/effect_hook_tests.js +++ /dev/null @@ -1,183 +0,0 @@ -/** @odoo-module **/ -import { useEffect } from "@web/core/effect_hook"; -import { makeTestEnv } from "../helpers/mock_env"; -import { getFixture } from "../helpers/utils"; - -const { Component, tags, hooks, mount } = owl; -const { useState } = hooks; - -QUnit.module("useEffect"); - -QUnit.test("useEffect: effect runs on mount, is reapplied on patch, and is cleaned up on unmount and before reapplying", async function (assert) { - assert.expect(7); - - let cleanupRun = 0; - class MyComponent extends Component { - setup() { - this.state = useState({ - value: 0, - }); - useEffect( - () => { - assert.step(`value is ${this.state.value}`); - return () => assert.step(`cleaning up for value = ${this.state.value} (cleanup ${cleanupRun++})`); - } - ); - } - } - MyComponent.template = tags.xml`
`; - - const env = await makeTestEnv(); - const target = getFixture(); - const component = await mount(MyComponent, { env, target }); - - assert.step("before state mutation"); - component.state.value++; - // Wait for an owl render - await new Promise(resolve => requestAnimationFrame(resolve)); - assert.step("after state mutation"); - await component.unmount(); - - assert.verifySteps([ - "value is 0", - "before state mutation", - // While one might expect value to be 0 at cleanup, because the value is - // read during cleanup from the state rather than captured by a dependency - // it already has the new value. Having this in business code is a symptom - // of a missing dependency and can lead to bugs. - "cleaning up for value = 1 (cleanup 0)", - "value is 1", - "after state mutation", - "cleaning up for value = 1 (cleanup 1)", - ]); -}); - -QUnit.test("useEffect: dependencies prevent effects from rerunning when unchanged", async function (assert) { - assert.expect(21); - - class MyComponent extends Component { - setup() { - this.state = useState({ - a: 0, - b: 0, - }); - useEffect( - (a) => { - assert.step(`Effect a: ${a}`); - return () => assert.step(`cleaning up for a: ${a}`); - }, - () => [this.state.a] - ); - useEffect( - (b) => { - assert.step(`Effect b: ${b}`); - return () => assert.step(`cleaning up for b: ${b}`); - }, - () => [this.state.b] - ); - useEffect( - (a, b) => { - assert.step(`Effect ab: {a: ${a}, b: ${b}}`); - return () => assert.step(`cleaning up for ab: {a: ${a}, b: ${b}}`); - }, - () => [this.state.a, this.state.b] - ); - } - } - MyComponent.template = tags.xml`
`; - - const env = await makeTestEnv(); - const target = getFixture(); - assert.step("before mount"); - const component = await mount(MyComponent, { env, target }); - assert.step("after mount"); - - assert.step("before state mutation: a"); - component.state.a++; - // Wait for an owl render - await new Promise(resolve => requestAnimationFrame(resolve)); - assert.step("after state mutation: a"); - - assert.step("before state mutation: b"); - component.state.b++; - // Wait for an owl render - await new Promise(resolve => requestAnimationFrame(resolve)); - assert.step("after state mutation: b"); - await component.unmount(); - - assert.verifySteps([ - // All effects run on mount - "before mount", - "Effect a: 0", - "Effect b: 0", - "Effect ab: {a: 0, b: 0}", - "after mount", - - "before state mutation: a", - // Cleanups run in reverse order - "cleaning up for ab: {a: 0, b: 0}", - // Cleanup for b is not run - "cleaning up for a: 0", - - "Effect a: 1", - // Effect b is not run - "Effect ab: {a: 1, b: 0}", - "after state mutation: a", - - "before state mutation: b", - "cleaning up for ab: {a: 1, b: 0}", - "cleaning up for b: 0", - // Cleanup for a is not run - - // Effect a is not run - "Effect b: 1", - "Effect ab: {a: 1, b: 1}", - "after state mutation: b", - - // All cleanups run on unmount - "cleaning up for ab: {a: 1, b: 1}", - "cleaning up for b: 1", - "cleaning up for a: 1", - ]); -}); - -QUnit.test("useEffect: effect with empty dependency list never reruns", async function (assert) { - assert.expect(6); - - class MyComponent extends Component { - setup() { - this.state = useState({ - value: 0, - }); - useEffect( - () => { - assert.step(`value is ${this.state.value}`); - return () => assert.step(`cleaning up for ${this.state.value}`); - }, - () => [] - ); - } - } - MyComponent.template = tags.xml`
`; - - const env = await makeTestEnv(); - const target = getFixture(); - const component = await mount(MyComponent, { env, target }); - - assert.step("before state mutation"); - component.state.value++; - // Wait for an owl render - await new Promise(resolve => requestAnimationFrame(resolve)); - assert.equal(component.el.textContent, 1, "Value was correctly changed inside the component"); - assert.step("after state mutation"); - await component.unmount(); - - assert.verifySteps([ - "value is 0", - "before state mutation", - // no cleanup or effect caused by mutation - "after state mutation", - // Value being clean - "cleaning up for 1", - ]); -}); diff --git a/addons/web/static/tests/core/hotkeys/hotkey_service_tests.js b/addons/web/static/tests/core/hotkeys/hotkey_service_tests.js index efedd7b8271..743a117bbfa 100644 --- a/addons/web/static/tests/core/hotkeys/hotkey_service_tests.js +++ b/addons/web/static/tests/core/hotkeys/hotkey_service_tests.js @@ -1,7 +1,7 @@ /** @odoo-module **/ import { browser } from "@web/core/browser/browser"; -import { useHotkey } from "@web/core/hotkey_hook"; +import { useHotkey } from "@web/core/hotkeys/hotkey_hook"; import { registry } from "@web/core/registry"; import { uiService, useActiveElement } from "@web/core/ui/ui_service"; import { hotkeyService } from "@web/core/hotkeys/hotkey_service"; diff --git a/addons/web/static/tests/core/network/rpc_service_tests.js b/addons/web/static/tests/core/network/rpc_service_tests.js index 4031b3c9983..ef5363c8a13 100644 --- a/addons/web/static/tests/core/network/rpc_service_tests.js +++ b/addons/web/static/tests/core/network/rpc_service_tests.js @@ -4,7 +4,7 @@ import { browser } from "@web/core/browser/browser"; import { ConnectionAbortedError, rpcService } from "@web/core/network/rpc_service"; import { notificationService } from "@web/core/notifications/notification_service"; import { registry } from "@web/core/registry"; -import { useService } from "@web/core/service_hook"; +import { useService } from "@web/core/utils/hooks"; import { patch, unpatch } from "@web/core/utils/patch"; import { clearRegistryWithCleanup, makeTestEnv } from "../../helpers/mock_env"; import { makeMockXHR } from "../../helpers/mock_services"; diff --git a/addons/web/static/tests/core/orm_service_tests.js b/addons/web/static/tests/core/orm_service_tests.js index 0557d6e832b..c8e7d11fcc2 100644 --- a/addons/web/static/tests/core/orm_service_tests.js +++ b/addons/web/static/tests/core/orm_service_tests.js @@ -2,7 +2,7 @@ import { ormService } from "@web/core/orm_service"; import { registry } from "@web/core/registry"; -import { useService } from "@web/core/service_hook"; +import { useService } from "@web/core/utils/hooks"; import { makeTestEnv } from "../helpers/mock_env"; import { getFixture } from "../helpers/utils"; diff --git a/addons/web/static/tests/core/service_hook_tests.js b/addons/web/static/tests/core/service_hook_tests.js deleted file mode 100644 index ff29ea930d7..00000000000 --- a/addons/web/static/tests/core/service_hook_tests.js +++ /dev/null @@ -1,51 +0,0 @@ -/** @odoo-module **/ - -import { useService } from "@web/core/service_hook"; -import { registry } from "@web/core/registry"; -import { makeTestEnv } from "../helpers/mock_env"; -import { getFixture } from "../helpers/utils"; - -const { Component, mount, tags } = owl; -const serviceRegistry = registry.category("services"); - -QUnit.module("useService"); - -QUnit.test("useService: unavailable service", async function (assert) { - class MyComponent extends Component { - setup() { - useService("toy_service"); - } - } - MyComponent.template = tags.xml`
`; - - const env = await makeTestEnv(); - const target = getFixture(); - try { - await mount(MyComponent, { env, target }); - } catch (e) { - assert.strictEqual(e.message, "Service toy_service is not available"); - } -}); - -QUnit.test("useService: service that returns null", async function (assert) { - class MyComponent extends Component { - setup() { - this.toyService = useService("toy_service"); - } - } - MyComponent.template = tags.xml`
`; - - serviceRegistry.add("toy_service", { - name: "toy_service", - start: () => { - return null; - }, - }); - - const env = await makeTestEnv(); - const target = getFixture(); - - const comp = await mount(MyComponent, { env, target }); - assert.strictEqual(comp.toyService, null); - comp.unmount(); -}); diff --git a/addons/web/static/tests/core/utils/hooks_tests.js b/addons/web/static/tests/core/utils/hooks_tests.js new file mode 100644 index 00000000000..901ea18fa80 --- /dev/null +++ b/addons/web/static/tests/core/utils/hooks_tests.js @@ -0,0 +1,275 @@ +/** @odoo-module **/ + +import { useBus, useEffect, useService } from "@web/core/utils/hooks"; +import { registry } from "@web/core/registry"; +import { makeTestEnv } from "../../helpers/mock_env"; +import { getFixture, nextTick } from "../../helpers/utils"; + +const { Component, mount, tags, useState } = owl; +const serviceRegistry = registry.category("services"); + +QUnit.module("utils", () => { + QUnit.module("Hooks", () => { + QUnit.module("useBus"); + + QUnit.test("useBus hook: simple usecase", async function (assert) { + class MyComponent extends Component { + setup() { + useBus(this.env.bus, "test-event", this.myCallback); + } + myCallback() { + assert.step("callback"); + } + } + MyComponent.template = tags.xml`
`; + + const env = await makeTestEnv(); + const target = getFixture(); + const comp = await mount(MyComponent, { env, target }); + env.bus.trigger("test-event"); + await nextTick(); + assert.verifySteps(["callback"]); + + comp.unmount(); + env.bus.trigger("test-event"); + await nextTick(); + assert.verifySteps([]); + + comp.destroy(); + }); + + QUnit.module("useEffect"); + + QUnit.test( + "useEffect: effect runs on mount, is reapplied on patch, and is cleaned up on unmount and before reapplying", + async function (assert) { + assert.expect(7); + + let cleanupRun = 0; + class MyComponent extends Component { + setup() { + this.state = useState({ + value: 0, + }); + useEffect(() => { + assert.step(`value is ${this.state.value}`); + return () => + assert.step( + `cleaning up for value = ${ + this.state.value + } (cleanup ${cleanupRun++})` + ); + }); + } + } + MyComponent.template = tags.xml`
`; + + const env = await makeTestEnv(); + const target = getFixture(); + const component = await mount(MyComponent, { env, target }); + + assert.step("before state mutation"); + component.state.value++; + // Wait for an owl render + await new Promise((resolve) => requestAnimationFrame(resolve)); + assert.step("after state mutation"); + await component.unmount(); + + assert.verifySteps([ + "value is 0", + "before state mutation", + // While one might expect value to be 0 at cleanup, because the value is + // read during cleanup from the state rather than captured by a dependency + // it already has the new value. Having this in business code is a symptom + // of a missing dependency and can lead to bugs. + "cleaning up for value = 1 (cleanup 0)", + "value is 1", + "after state mutation", + "cleaning up for value = 1 (cleanup 1)", + ]); + } + ); + + QUnit.test( + "useEffect: dependencies prevent effects from rerunning when unchanged", + async function (assert) { + assert.expect(21); + + class MyComponent extends Component { + setup() { + this.state = useState({ + a: 0, + b: 0, + }); + useEffect( + (a) => { + assert.step(`Effect a: ${a}`); + return () => assert.step(`cleaning up for a: ${a}`); + }, + () => [this.state.a] + ); + useEffect( + (b) => { + assert.step(`Effect b: ${b}`); + return () => assert.step(`cleaning up for b: ${b}`); + }, + () => [this.state.b] + ); + useEffect( + (a, b) => { + assert.step(`Effect ab: {a: ${a}, b: ${b}}`); + return () => assert.step(`cleaning up for ab: {a: ${a}, b: ${b}}`); + }, + () => [this.state.a, this.state.b] + ); + } + } + MyComponent.template = tags.xml`
`; + + const env = await makeTestEnv(); + const target = getFixture(); + assert.step("before mount"); + const component = await mount(MyComponent, { env, target }); + assert.step("after mount"); + + assert.step("before state mutation: a"); + component.state.a++; + // Wait for an owl render + await new Promise((resolve) => requestAnimationFrame(resolve)); + assert.step("after state mutation: a"); + + assert.step("before state mutation: b"); + component.state.b++; + // Wait for an owl render + await new Promise((resolve) => requestAnimationFrame(resolve)); + assert.step("after state mutation: b"); + await component.unmount(); + + assert.verifySteps([ + // All effects run on mount + "before mount", + "Effect a: 0", + "Effect b: 0", + "Effect ab: {a: 0, b: 0}", + "after mount", + + "before state mutation: a", + // Cleanups run in reverse order + "cleaning up for ab: {a: 0, b: 0}", + // Cleanup for b is not run + "cleaning up for a: 0", + + "Effect a: 1", + // Effect b is not run + "Effect ab: {a: 1, b: 0}", + "after state mutation: a", + + "before state mutation: b", + "cleaning up for ab: {a: 1, b: 0}", + "cleaning up for b: 0", + // Cleanup for a is not run + + // Effect a is not run + "Effect b: 1", + "Effect ab: {a: 1, b: 1}", + "after state mutation: b", + + // All cleanups run on unmount + "cleaning up for ab: {a: 1, b: 1}", + "cleaning up for b: 1", + "cleaning up for a: 1", + ]); + } + ); + + QUnit.test( + "useEffect: effect with empty dependency list never reruns", + async function (assert) { + assert.expect(6); + + class MyComponent extends Component { + setup() { + this.state = useState({ + value: 0, + }); + useEffect( + () => { + assert.step(`value is ${this.state.value}`); + return () => assert.step(`cleaning up for ${this.state.value}`); + }, + () => [] + ); + } + } + MyComponent.template = tags.xml`
`; + + const env = await makeTestEnv(); + const target = getFixture(); + const component = await mount(MyComponent, { env, target }); + + assert.step("before state mutation"); + component.state.value++; + // Wait for an owl render + await new Promise((resolve) => requestAnimationFrame(resolve)); + assert.equal( + component.el.textContent, + 1, + "Value was correctly changed inside the component" + ); + assert.step("after state mutation"); + await component.unmount(); + + assert.verifySteps([ + "value is 0", + "before state mutation", + // no cleanup or effect caused by mutation + "after state mutation", + // Value being clean + "cleaning up for 1", + ]); + } + ); + + QUnit.module("useService"); + + QUnit.test("useService: unavailable service", async function (assert) { + class MyComponent extends Component { + setup() { + useService("toy_service"); + } + } + MyComponent.template = tags.xml`
`; + + const env = await makeTestEnv(); + const target = getFixture(); + try { + await mount(MyComponent, { env, target }); + } catch (e) { + assert.strictEqual(e.message, "Service toy_service is not available"); + } + }); + + QUnit.test("useService: service that returns null", async function (assert) { + class MyComponent extends Component { + setup() { + this.toyService = useService("toy_service"); + } + } + MyComponent.template = tags.xml`
`; + + serviceRegistry.add("toy_service", { + name: "toy_service", + start: () => { + return null; + }, + }); + + const env = await makeTestEnv(); + const target = getFixture(); + + const comp = await mount(MyComponent, { env, target }); + assert.strictEqual(comp.toyService, null); + comp.unmount(); + }); + }); +}); diff --git a/addons/web/static/tests/webclient/actions/push_state_tests.js b/addons/web/static/tests/webclient/actions/push_state_tests.js index 17c5303c312..e9176dc6cb5 100644 --- a/addons/web/static/tests/webclient/actions/push_state_tests.js +++ b/addons/web/static/tests/webclient/actions/push_state_tests.js @@ -2,7 +2,7 @@ import { browser } from "@web/core/browser/browser"; import { registry } from "@web/core/registry"; -import { useService } from "@web/core/service_hook"; +import { useService } from "@web/core/utils/hooks"; import testUtils from "web.test_utils"; import { click, legacyExtraNextTick, nextTick, patchWithCleanup } from "../../helpers/utils"; import { createWebClient, doAction, getActionManagerServerData } from "./../helpers"; diff --git a/addons/web/static/tests/webclient/actions/target_tests.js b/addons/web/static/tests/webclient/actions/target_tests.js index cb9cc612784..7a0f9878564 100644 --- a/addons/web/static/tests/webclient/actions/target_tests.js +++ b/addons/web/static/tests/webclient/actions/target_tests.js @@ -4,16 +4,11 @@ import testUtils from "web.test_utils"; import core from "web.core"; import AbstractAction from "web.AbstractAction"; import { registry } from "@web/core/registry"; -import { - click, - legacyExtraNextTick, - patchWithCleanup, - makeDeferred, -} from "../../helpers/utils"; +import { click, legacyExtraNextTick, patchWithCleanup, makeDeferred } from "../../helpers/utils"; import { createWebClient, doAction, getActionManagerServerData } from "./../helpers"; import { registerCleanup } from "../../helpers/cleanup"; import { errorService } from "@web/core/errors/error_service"; -import { useService } from "@web/core/service_hook"; +import { useService } from "@web/core/utils/hooks"; import { ClientErrorDialog } from "@web/core/errors/error_dialogs"; let serverData; diff --git a/addons/web_tour/static/src/debug/tour_dialog_component.js b/addons/web_tour/static/src/debug/tour_dialog_component.js index e57079e00f0..ca7c7d68161 100644 --- a/addons/web_tour/static/src/debug/tour_dialog_component.js +++ b/addons/web_tour/static/src/debug/tour_dialog_component.js @@ -1,6 +1,6 @@ /** @odoo-module **/ -import { useService } from "@web/core/service_hook"; +import { useService } from "@web/core/utils/hooks"; import { Dialog } from "@web/core/dialog/dialog"; import { _lt } from "@web/core/l10n/translation";