diff --git a/addons/web/static/src/core/effects/effect_container.js b/addons/web/static/src/core/effects/effect_container.js index e57753bec98e1..e4b40d4dcd450 100644 --- a/addons/web/static/src/core/effects/effect_container.js +++ b/addons/web/static/src/core/effects/effect_container.js @@ -1,25 +1,24 @@ /** @odoo-module **/ -import { RainbowMan } from "./rainbow_man"; - const { Component, tags } = owl; export class EffectContainer extends Component { setup() { - this.rainbowProps = {}; + this.effect = null; this.props.bus.on("UPDATE", this, (effect) => { - this.rainbowProps = effect; + this.effect = effect; this.render(); }); } - closeRainbowMan() { - this.rainbowProps = {}; + removeEffect() { + this.effect = null; this.render(); } } EffectContainer.template = tags.xml`
- + + +
`; -EffectContainer.components = { RainbowMan }; diff --git a/addons/web/static/src/core/effects/effect_service.js b/addons/web/static/src/core/effects/effect_service.js index ba8d8352bb6b3..5f2401b7884f1 100644 --- a/addons/web/static/src/core/effects/effect_service.js +++ b/addons/web/static/src/core/effects/effect_service.js @@ -2,28 +2,78 @@ import { registry } from "../registry"; import { EffectContainer } from "./effect_container"; +import { RainbowMan } from "./rainbow_man"; const { EventBus } = owl.core; -export function convertRainBowMessage(message) { +const effectRegistry = registry.category("effects"); + +// ----------------------------------------------------------------------------- +// RainbowMan effect +// ----------------------------------------------------------------------------- + +/** + * Handles effect of type "rainbow_man". If the effects aren't disabled, returns + * the RainbowMan component to instantiate and its props. If the effects are + * disabled, displays the message in a notification. + * + * @param {Object} env + * @param {Object} [params={}] + * @param {string} [params.message="Well Done"] + * The message in the notice the rainbowman holds or the content of the notification if effects are disabled + * Can be a simple a string + * Can be a string representation of html (prefer component if you want interactions in the DOM) + * @param {boolean} [params.img_url="/web/static/img/smile.svg"] + * The url of the image to display inside the rainbow + * @param {boolean} [params.messageIsHtml] + * Set to true if the message encodes html, s.t. it will be correctly inserted into the DOM. + * @param {"slow"|"medium"|"fast"|"no"} [params.fadeout="medium"] + * Delay for rainbowman to disappear + * 'fast' will make rainbowman dissapear quickly + * 'medium' and 'slow' will wait little longer before disappearing (can be used when options.message is longer) + * 'no' will keep rainbowman on screen until user clicks anywhere outside rainbowman + * @param {owl.Component} [params.Component=RainbowMan] + * Component class to instantiate (if effects aren't disabled) + * @param {Object} [params.props] + * If params.Component is given, its props can be passed with this argument + */ +function rainbowMan(env, params = {}) { + let message = params.message; if (message instanceof jQuery) { console.log( - "Providing jQuery to an effect is deprecated. Note that all event handlers will be lost." + "Providing a jQuery element to an effect is deprecated. Note that all event handlers will be lost." ); - return message.html(); + message = message.html(); } else if (message instanceof Element) { console.log( - "Providing HTML to an effect is deprecated. Note that all event handlers will be lost." + "Providing an HTML element to an effect is deprecated. Note that all event handlers will be lost." ); - return message.outerHTML; - } else if (typeof message === "string") { - return message; + message = message.outerHTML; + } else if (!message) { + message = env._t("well Done!"); + } + if (env.services.user.showEffect) { + return { + Component: params.Component || RainbowMan, + props: { + imgUrl: params.img_url || "/web/static/img/smile.svg", + fadeout: params.fadeout, + message, + messageIsHtml: params.messageIsHtml || false, + ...params.props, + }, + }; } + env.services.notification.add(message); } +effectRegistry.add("rainbow_man", rainbowMan); + +// ----------------------------------------------------------------------------- +// Effect service +// ----------------------------------------------------------------------------- export const effectService = { - dependencies: ["notification", "user"], - start(env, { notification, user }) { + start(env) { const bus = new EventBus(); registry.category("main_components").add("EffectContainer", { Component: EffectContainer, @@ -32,74 +82,15 @@ export const effectService = { let effectId = 0; /** - * This private method checks if the effects are disabled. - * If so, it makes a notification from the message attribute of an effect and - * doesn't trigger the effect at all. - * @param {Object} effect The effect to display - */ - function applyEffect(effect) { - if (!user.showEffect) { - notification.add(effect.message, { sticky: false }); - } else { - bus.trigger("UPDATE", effect); - } - } - - /** - * Display a rainbowman effect - * - * @param {Object} [params={}] - * @param {string|Function} [params.message="Well Done"] - * The message in the notice the rainbowman holds or the content of the notification if effects are disabled - * Can be a simple a string - * Can be a string representation of html (prefer component if you want interactions in the DOM) - * Can be a function returning a string (like _t) - * @param {boolean} [params.messageIsHtml] - * The message can be a string representation of html but it needs to be marked as well - * @param {"slow"|"medium"|"fast"|"no"} [params.fadeout="medium"] - * Delay for rainbowman to disappear - * 'fast' will make rainbowman dissapear quickly - * 'medium' and 'slow' will wait little longer before disappearing (can be used when options.message is longer) - * 'no' will keep rainbowman on screen until user clicks anywhere outside rainbowman - * @param {owl.Component} [params.Component] - * Component class to instantiate - * It this option is set, the message option is ignored unless effect are disbled - * Then, the message param is used to display a notification - * @param {Object} [params.props] - * If a component is used, its props can be passed with this argument - */ - function rainbowMan(params = {}) { - const effect = Object.assign({ - imgUrl: "/web/static/img/smile.svg", - fadeout: params.fadeout, - id: ++effectId, - message: convertRainBowMessage(params.message) || env._t("Well Done!"), - messageIsHtml: params.messageIsHtml || false, - Component: params.Component, - props: params.props, - }); - applyEffect(effect); - } - - /** - * Display an effect - * This is a dispatcher for the effect. Usefull if the request for effect comes - * from the server. - * In the weblient, use the more specific effect functions. - * - * @param {string} type - * What effect to create - * - rainbowman - * @param {Object} [params={}] - * All the options for the effect. - * The options get passed to the more specific effect methods. + * @param {Object} params various params depending on the type of effect + * @param {string} [params.type="rainbow_man"] the effect to display */ - function add(type, params = {}) { - switch (type.replace("_", "").toLowerCase()) { - case "rainbowman": - return rainbowMan(params); - default: - throw new Error("NON_IMPLEMENTED_EFFECT"); + function add(params) { + const type = params.type || "rainbow_man"; + const effect = effectRegistry.get(type); + const { Component, props } = effect(env, params) || {}; + if (Component) { + bus.trigger("UPDATE", { Component, props, id: effectId++ }); } } diff --git a/addons/web/static/src/core/effects/rainbow_man.js b/addons/web/static/src/core/effects/rainbow_man.js index d082e8345d4b5..a8aae22ac0947 100644 --- a/addons/web/static/src/core/effects/rainbow_man.js +++ b/addons/web/static/src/core/effects/rainbow_man.js @@ -50,7 +50,7 @@ export class RainbowMan extends Component { } closeRainbowMan() { - this.trigger("close-rainbowman"); + this.props.close(); } } RainbowMan.template = "web.RainbowMan"; diff --git a/addons/web/static/src/legacy/legacy_service_provider.js b/addons/web/static/src/legacy/legacy_service_provider.js index b8efe0125b13f..a776135a7b87e 100644 --- a/addons/web/static/src/legacy/legacy_service_provider.js +++ b/addons/web/static/src/legacy/legacy_service_provider.js @@ -11,10 +11,10 @@ export const legacyServiceProvider = { dependencies: ["effect", "action"], start({ services }) { browser.addEventListener("show-effect", (ev) => { - services.effect.add(ev.detail.type, ev.detail); + services.effect.add(ev.detail); }); bus.on("show-effect", this, (payload) => { - services.effect.add(payload.type, payload); + services.effect.add(payload); }); browser.addEventListener("do-action", (ev) => { diff --git a/addons/web/static/src/legacy/utils.js b/addons/web/static/src/legacy/utils.js index bc5b548aa0292..75b0c3303051d 100644 --- a/addons/web/static/src/legacy/utils.js +++ b/addons/web/static/src/legacy/utils.js @@ -296,7 +296,7 @@ export function makeLegacyRainbowManService(legacyEnv) { dependencies: ["effect"], start(env, { effect }) { legacyEnv.bus.on("show-effect", null, (payload) => { - effect.add(payload.type, payload); + effect.add(payload); }); }, }; diff --git a/addons/web/static/src/webclient/actions/action_service.js b/addons/web/static/src/webclient/actions/action_service.js index 0fb7454d12da3..a6bf736bfbe22 100644 --- a/addons/web/static/src/webclient/actions/action_service.js +++ b/addons/web/static/src/webclient/actions/action_service.js @@ -1153,7 +1153,7 @@ function makeActionManager(env) { await _executeCloseAction(); } if (effect) { - env.services.effect.add(effect.type, effect); + env.services.effect.add(effect); } } diff --git a/addons/web/static/tests/core/effects/effect_service_tests.js b/addons/web/static/tests/core/effects/effect_service_tests.js new file mode 100644 index 0000000000000..ae673191e8554 --- /dev/null +++ b/addons/web/static/tests/core/effects/effect_service_tests.js @@ -0,0 +1,126 @@ +/** @odoo-module **/ + +import { notificationService } from "@web/core/notifications/notification_service"; +import { registry } from "@web/core/registry"; +import { effectService } from "@web/core/effects/effect_service"; +import { RainbowMan } from "@web/core/effects/rainbow_man"; +import { userService } from "@web/core/user_service"; +import { session } from "@web/session"; +import { makeTestEnv } from "../../helpers/mock_env"; +import { makeFakeLocalizationService } from "../../helpers/mock_services"; +import { click, getFixture, nextTick, patchWithCleanup } from "../../helpers/utils"; +import { registerCleanup } from "../../helpers/cleanup"; + +const { Component, mount, tags } = owl; +const serviceRegistry = registry.category("services"); +const mainComponentRegistry = registry.category("main_components"); + +class Parent extends Component { + setup() { + this.EffectContainer = mainComponentRegistry.get("EffectContainer"); + this.NotificationContainer = mainComponentRegistry.get("NotificationContainer"); + } +} +Parent.template = tags.xml` +
+ + +
+ `; + +async function makeParent() { + const env = await makeTestEnv({ serviceRegistry }); + const target = getFixture(); + const parent = await mount(Parent, { env, target }); + registerCleanup(() => parent.destroy()); + return parent; +} + +QUnit.module("Effect Service", (hooks) => { + let effectParams; + hooks.beforeEach(() => { + effectParams = { + message: "
Congrats!
", + messageIsHtml: true, + fadeout: "nextTick", + }; + + patchWithCleanup(session, { show_effect: true }); // enable effects + + serviceRegistry.add("user", userService); + serviceRegistry.add("effect", effectService); + serviceRegistry.add("notification", notificationService); + serviceRegistry.add("localization", makeFakeLocalizationService()); + }); + + QUnit.test("effect service displays a rainbowman by default", async function (assert) { + const parent = await makeParent(); + + parent.env.services.effect.add({ message: "Hello", fadeout: "no" }); + await nextTick(); + + assert.containsOnce(parent.el, ".o_reward"); + assert.strictEqual(parent.el.querySelector(".o_reward").innerText, "Hello"); + }); + + QUnit.test("rainbowman effect with show_effect: false", async function (assert) { + patchWithCleanup(session, { show_effect: false }); + + const parent = await makeParent(); + + parent.env.services.effect.add({ type: "rainbow_man", message: "", fadeout: "no" }); + await nextTick(); + + assert.containsNone(parent.el, ".o_reward"); + assert.containsOnce(parent.el, ".o_notification"); + }); + + QUnit.test("rendering a rainbowman destroy after animation", async function (assert) { + patchWithCleanup(RainbowMan, { + rainbowFadeouts: { nextTick: 0 }, + }); + + const parent = await makeParent(); + parent.env.services.effect.add(effectParams); + await nextTick(); + + assert.containsOnce(parent, ".o_reward"); + assert.containsOnce(parent, ".o_reward_rainbow"); + assert.strictEqual( + parent.el.querySelector(".o_reward_msg_content").innerHTML, + "
Congrats!
" + ); + + const ev = new AnimationEvent("animationend", { animationName: "reward-fading-reverse" }); + parent.el.querySelector(".o_reward").dispatchEvent(ev); + await nextTick(); + assert.containsNone(parent, ".o_reward"); + }); + + QUnit.test("rendering a rainbowman destroy on click", async function (assert) { + const parent = await makeParent(); + + parent.env.services.effect.add({ ...effectParams, fadeout: "no" }); + await nextTick(); + + assert.containsOnce(parent.el, ".o_reward"); + assert.containsOnce(parent.el, ".o_reward_rainbow"); + + await click(parent.el); + assert.containsNone(parent, ".o_reward"); + }); + + QUnit.test("rendering a rainbowman with an escaped message", async function (assert) { + const parent = await makeParent(); + + parent.env.services.effect.add({ ...effectParams, messageIsHtml: false }); + await nextTick(); + + assert.containsOnce(parent.el, ".o_reward"); + assert.containsOnce(parent.el, ".o_reward_rainbow"); + assert.strictEqual( + parent.el.querySelector(".o_reward_msg_content").textContent, + "
Congrats!
" + ); + }); +}); diff --git a/addons/web/static/tests/webclient/actions/effects_tests.js b/addons/web/static/tests/webclient/actions/effects_tests.js index 607131d144177..cba1f3ad67881 100644 --- a/addons/web/static/tests/webclient/actions/effects_tests.js +++ b/addons/web/static/tests/webclient/actions/effects_tests.js @@ -27,7 +27,7 @@ QUnit.module("ActionManager", (hooks) => { await doAction(webClient, 1); assert.containsOnce(webClient.el, ".o_kanban_view"); assert.containsNone(webClient.el, ".o_reward"); - webClient.env.services.effect.add("rainbowman", { message: "", fadeout: "no" }); + webClient.env.services.effect.add({ type: "rainbow_man", message: "", fadeout: "no" }); await nextTick(); await legacyExtraNextTick(); assert.containsOnce(webClient.el, ".o_reward"); @@ -36,7 +36,7 @@ QUnit.module("ActionManager", (hooks) => { await legacyExtraNextTick(); assert.containsNone(webClient.el, ".o_reward"); assert.containsOnce(webClient.el, ".o_kanban_view"); - webClient.env.services.effect.add("rainbowman", { message: "", fadeout: "no" }); + webClient.env.services.effect.add({ type: "rainbow_man", message: "", fadeout: "no" }); await nextTick(); await legacyExtraNextTick(); assert.containsOnce(webClient.el, ".o_reward"); @@ -48,22 +48,6 @@ QUnit.module("ActionManager", (hooks) => { assert.containsOnce(webClient.el, ".o_list_view"); }); - QUnit.test("show effect notification instead of rainbow man", async function (assert) { - assert.expect(6); - - const webClient = await createWebClient({ serverData }); - await doAction(webClient, 1); - assert.containsOnce(webClient.el, ".o_kanban_view"); - assert.containsNone(webClient.el, ".o_reward"); - assert.containsNone(webClient.el, ".o_notification"); - webClient.env.services.effect.add("rainbowman", { message: "", fadeout: "no" }); - await nextTick(); - await legacyExtraNextTick(); - assert.containsOnce(webClient.el, ".o_kanban_view"); - assert.containsNone(webClient.el, ".o_reward"); - assert.containsOnce(webClient.el, ".o_notification"); - }); - QUnit.test("on close with effect from server", async function (assert) { assert.expect(1); patchWithCleanup(session, { show_effect: true }); @@ -89,14 +73,14 @@ QUnit.module("ActionManager", (hooks) => { QUnit.test("on close with effect in xml", async function (assert) { assert.expect(2); serverData.views["partner,false,form"] = ` -
-
-
- - `; +
+
+
+ + `; patchWithCleanup(session, { show_effect: true }); const mockRPC = async (route) => { if (route === "/web/dataset/call_button") { diff --git a/addons/web/static/tests/webclient/effects/rainbow_man_tests.js b/addons/web/static/tests/webclient/effects/rainbow_man_tests.js deleted file mode 100644 index 91283070d3f8e..0000000000000 --- a/addons/web/static/tests/webclient/effects/rainbow_man_tests.js +++ /dev/null @@ -1,92 +0,0 @@ -/** @odoo-module **/ - -import { notificationService } from "@web/core/notifications/notification_service"; -import { registry } from "@web/core/registry"; -import { effectService } from "@web/core/effects/effect_service"; -import { RainbowMan } from "@web/core/effects/rainbow_man"; -import { userService } from "@web/core/user_service"; -import { makeTestEnv } from "../../helpers/mock_env"; -import { click, getFixture, nextTick, patchWithCleanup } from "../../helpers/utils"; -import { session } from "@web/session"; - -const { Component, mount, tags } = owl; -const serviceRegistry = registry.category("services"); - -class Parent extends Component { - setup() { - this.RainbowMgr = registry.category("main_components").get("EffectContainer"); - } -} -Parent.template = tags.xml` -
- -
- `; - -QUnit.module("RainbowMan", (hooks) => { - let rainbowManDefault, target; - hooks.beforeEach(async () => { - rainbowManDefault = { - message: "
Congrats!
", - messageIsHtml: true, - fadeout: "nextTick", - }; - target = getFixture(); - patchWithCleanup(session, { show_effect: true }); - serviceRegistry.add("user", userService); - serviceRegistry.add("effect", effectService); - serviceRegistry.add("notification", notificationService); - }); - - QUnit.test("rendering a rainbowman destroy after animation", async function (assert) { - assert.expect(4); - const _delays = RainbowMan.rainbowFadeouts; - RainbowMan.rainbowFadeouts = { nextTick: 0 }; - const env = await makeTestEnv({ serviceRegistry }); - const parent = await mount(Parent, { env, target }); - env.services.effect.add("rainbowman", rainbowManDefault); - await nextTick(); - assert.containsOnce(target, ".o_reward"); - assert.containsOnce(parent.el, ".o_reward_rainbow"); - assert.strictEqual( - parent.el.querySelector(".o_reward_msg_content").innerHTML, - "
Congrats!
" - ); - - const ev = new AnimationEvent("animationend", { animationName: "reward-fading-reverse" }); - target.querySelector(".o_reward").dispatchEvent(ev); - await nextTick(); - assert.containsNone(target, ".o_reward"); - RainbowMan.rainbowFadeouts = _delays; - parent.destroy(); - }); - - QUnit.test("rendering a rainbowman destroy on click", async function (assert) { - assert.expect(3); - rainbowManDefault.fadeout = "no"; - const env = await makeTestEnv({ serviceRegistry }); - const parent = await mount(Parent, { env, target }); - env.services.effect.add("rainbowman", rainbowManDefault); - await nextTick(); - assert.containsOnce(parent.el, ".o_reward"); - assert.containsOnce(parent.el, ".o_reward_rainbow"); - await click(target); - assert.containsNone(target, ".o_reward"); - parent.destroy(); - }); - - QUnit.test("rendering a rainbowman with an escaped message", async function (assert) { - assert.expect(3); - const env = await makeTestEnv({ serviceRegistry }); - const parent = await mount(Parent, { env, target }); - env.services.effect.add("rainbowman", { ...rainbowManDefault, messageIsHtml: false }); - await nextTick(); - assert.containsOnce(parent.el, ".o_reward"); - assert.containsOnce(parent.el, ".o_reward_rainbow"); - assert.strictEqual( - parent.el.querySelector(".o_reward_msg_content").textContent, - "
Congrats!
" - ); - parent.destroy(); - }); -});