Skip to content

Commit

Permalink
[FIX] web: effect: display rainbowman by default
Browse files Browse the repository at this point in the history
In [1], we rewrote the effect feature in the wowl infrastructure.
However, we lost something in the process: the "type" of effect
(basically "rainbow_man" as it is the only implemented effect)
became a mandatory param, whereas before it simply defaulted to
"rainbow_man" when not set. As a consequence, previously defined
effects in views could crash.

This commit fixes that issue, and reworks a bit the feature by
introducing a registry of effects, which easily allows to add new
effects from the outside. An effect is a function that may return
a Component (and its props) to display.

Most of the diff in the tests is about moving files where they
should be (the effect feature being in core/, the tests must be
in core/ as well, and not depend on the webclient).

[1] 0573aca

Task 2652927

closes odoo#77109

X-original-commit: 74dc3f6
Signed-off-by: Géry Debongnie (ged) <ged@openerp.com>
  • Loading branch information
aab-odoo committed Sep 23, 2021
1 parent cb69d7a commit 35ac826
Show file tree
Hide file tree
Showing 9 changed files with 215 additions and 207 deletions.
15 changes: 7 additions & 8 deletions 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`
<div class="o_effects_manager">
<RainbowMan t-if="rainbowProps.id" t-props="rainbowProps" t-key="rainbowProps.id" t-on-close-rainbowman="closeRainbowMan"/>
<t t-if="effect">
<t t-component="effect.Component" t-props="effect.props" t-key="effect.id" close="() => removeEffect()"/>
</t>
</div>`;
EffectContainer.components = { RainbowMan };
143 changes: 67 additions & 76 deletions addons/web/static/src/core/effects/effect_service.js
Expand Up @@ -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,
Expand All @@ -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++ });
}
}

Expand Down
2 changes: 1 addition & 1 deletion addons/web/static/src/core/effects/rainbow_man.js
Expand Up @@ -50,7 +50,7 @@ export class RainbowMan extends Component {
}

closeRainbowMan() {
this.trigger("close-rainbowman");
this.props.close();
}
}
RainbowMan.template = "web.RainbowMan";
Expand Down
4 changes: 2 additions & 2 deletions addons/web/static/src/legacy/legacy_service_provider.js
Expand Up @@ -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) => {
Expand Down
2 changes: 1 addition & 1 deletion addons/web/static/src/legacy/utils.js
Expand Up @@ -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);
});
},
};
Expand Down
2 changes: 1 addition & 1 deletion addons/web/static/src/webclient/actions/action_service.js
Expand Up @@ -1153,7 +1153,7 @@ function makeActionManager(env) {
await _executeCloseAction();
}
if (effect) {
env.services.effect.add(effect.type, effect);
env.services.effect.add(effect);
}
}

Expand Down
126 changes: 126 additions & 0 deletions 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`
<div>
<t t-component="EffectContainer.Component" t-props="EffectContainer.props" />
<t t-component="NotificationContainer.Component" t-props="NotificationContainer.props" />
</div>
`;

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: "<div>Congrats!</div>",
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,
"<div>Congrats!</div>"
);

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,
"<div>Congrats!</div>"
);
});
});

0 comments on commit 35ac826

Please sign in to comment.