[FIX] web: effect: display rainbowman by default

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] 0573acae23

Task 2652927

closes odoo/odoo#77109

X-original-commit: 74dc3f654ee5849982b494e46eefb1fc2b4e0c74
Signed-off-by: Géry Debongnie (ged) <ged@openerp.com>
This commit is contained in:
Aaron Bohy
2021-09-23 20:18:47 +00:00
parent cb69d7a93e
commit 35ac826f94
9 changed files with 215 additions and 207 deletions
@@ -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 };
@@ -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
* @param {Object} params various params depending on the type of effect
* @param {string} [params.type="rainbow_man"] 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.
*/
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++ });
}
}
@@ -50,7 +50,7 @@ export class RainbowMan extends Component {
}
closeRainbowMan() {
this.trigger("close-rainbowman");
this.props.close();
}
}
RainbowMan.template = "web.RainbowMan";
@@ -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) => {
+1 -1
View File
@@ -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);
});
},
};
@@ -1153,7 +1153,7 @@ function makeActionManager(env) {
await _executeCloseAction();
}
if (effect) {
env.services.effect.add(effect.type, effect);
env.services.effect.add(effect);
}
}
@@ -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>"
);
});
});
@@ -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"] = `
<form>
<header>
<button string="Call method" name="object" type="object"
effect="{'type': 'rainbow_man', 'message': 'rainBowInXML'}"
/>
</header>
<field name="display_name"/>
</form>`;
<form>
<header>
<button string="Call method" name="object" type="object"
effect="{'type': 'rainbow_man', 'message': 'rainBowInXML'}"
/>
</header>
<field name="display_name"/>
</form>`;
patchWithCleanup(session, { show_effect: true });
const mockRPC = async (route) => {
if (route === "/web/dataset/call_button") {
@@ -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`
<div>
<t t-component="RainbowMgr.Component" t-props="RainbowMgr.props" />
</div>
`;
QUnit.module("RainbowMan", (hooks) => {
let rainbowManDefault, target;
hooks.beforeEach(async () => {
rainbowManDefault = {
message: "<div>Congrats!</div>",
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,
"<div>Congrats!</div>"
);
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,
"<div>Congrats!</div>"
);
parent.destroy();
});
});