[FIX] web: View component support banner_route

The View component supports banner_route, fetches the HTML and passes
on the prop "bannerHTML" to the concrete component class.

The feature has been a little forgotten in 0134495ba5.
This commit just revives it.

closes odoo/odoo#76370

X-original-commit: 99d1da58392a66dea1187ef4f00e41e924382d61
Related: odoo/enterprise#20779
Signed-off-by: Aaron Bohy (aab) <aab@odoo.com>
This commit is contained in:
Lucas Perais (lpe)
2021-09-14 13:31:59 +00:00
parent ecf3099c8f
commit ced5b4568d
4 changed files with 378 additions and 6 deletions
@@ -31,4 +31,5 @@ export const standardViewProps = {
state: { type: Object, optional: 1 },
resId: { type: Number, optional: 1 },
resIds: { type: Array, optional: 1 },
bannerRoute: { type: String, optional: 1 },
};
@@ -0,0 +1,50 @@
/**@odoo-module */
import { loadAssets } from "@web/core/assets";
import { useService } from "@web/core/utils/hooks";
import { useActionLinks } from "@web/views/helpers/view_hook";
export class OnboardingBanner extends owl.Component {
setup() {
this.rpc = useService("rpc");
this.user = useService("user");
const resModel = "searchModel" in this.env ? this.env.searchModel.resModel : undefined;
useActionLinks({
resModel,
reload: async () => {
this.bannerHTML = await this.loadBanner(this.props.bannerRoute);
this.render();
},
});
}
async willStart() {
this.bannerHTML = await this.loadBanner(this.props.bannerRoute);
}
async loadBanner(bannerRoute) {
const response = await this.rpc(bannerRoute, { context: this.user.context });
if (!response.html) {
return;
}
const banner = new DOMParser().parseFromString(response.html, "text/html");
const assets = {
jsLibs: [],
cssLibs: [],
};
banner
.querySelectorAll(`link[rel="stylesheet"] , script[type="text/javascript"]`)
.forEach((elem) => {
if (elem.tagName === "SCRIPT") {
assets.jsLibs.push(elem.src);
} else if (elem.tagName === "LINK") {
assets.cssLibs.push(elem.href);
}
elem.remove();
});
await loadAssets(assets);
return new XMLSerializer().serializeToString(banner);
}
}
OnboardingBanner.template = owl.tags.xml`<div t-raw="bannerHTML" />`;
+3 -5
View File
@@ -201,16 +201,13 @@ export class View extends Component {
rootAttrs[attrName] = rootNode.getAttribute(attrName);
}
//////////////////////////////////////////////////////////////////
/** @todo take care of banner_route rootAttribute*/
//////////////////////////////////////////////////////////////////
// determine ViewClass to instantiate (if not already done)
if (rootAttrs.js_class) {
ViewClass = viewRegistry.get(rootAttrs.js_class);
}
const bannerRoute = rootAttrs.banner_route;
// prepare the view props
let viewProps = {
info: {
@@ -228,6 +225,7 @@ export class View extends Component {
fields,
resModel,
useSampleModel: false,
bannerRoute,
};
if ("useSampleModel" in this.props) {
+324 -1
View File
@@ -6,8 +6,10 @@ import { ormService } from "@web/core/orm_service";
import { registerCleanup } from "@web/../tests/helpers/cleanup";
import { registry } from "@web/core/registry";
import { View } from "@web/views/view";
import { OnboardingBanner } from "@web/views/onboarding_banner";
import { viewService } from "@web/views/view_service";
import { click } from "../helpers/utils";
import { click, makeDeferred } from "../helpers/utils";
import { actionService } from "@web/webclient/actions/action_service";
const { Component, mount, hooks, tags } = owl;
const { useState } = hooks;
@@ -95,6 +97,7 @@ QUnit.module("Views", (hooks) => {
}
ToyView.template = xml`<div t-att-class="class"><t t-call="{{ template }}"/></div>`;
ToyView.type = "toy";
ToyView.components = { Banner: OnboardingBanner };
class ToyViewImp extends ToyView {
setup() {
@@ -811,6 +814,326 @@ QUnit.module("Views", (hooks) => {
await click(toy.el.querySelector("a"));
});
QUnit.test("renders banner_route", async (assert) => {
assert.expect(3);
serverData.views["animal,1,toy"] = `
<toy banner_route="/mybody/isacage">
<Banner t-if="props.bannerRoute" bannerRoute="props.bannerRoute"/>
</toy>`;
const mockRPC = (route) => {
if (route === "/mybody/isacage") {
assert.step(route);
return { html: `<div class="setmybodyfree">myBanner</div>` };
}
};
const toy = await makeView({
serverData,
mockRPC,
resModel: "animal",
type: "toy",
views: [[1, "toy"]],
});
assert.verifySteps(["/mybody/isacage"]);
assert.containsOnce(toy, ".setmybodyfree");
});
QUnit.test("renders banner_route with js and css assets", async (assert) => {
assert.expect(7);
serverData.views["animal,1,toy"] = `
<toy banner_route="/mybody/isacage">
<Banner t-if="props.bannerRoute" bannerRoute="props.bannerRoute"/>
</toy>`;
const bannerArch = `
<div class="setmybodyfree">
<link rel="stylesheet" href="/mystyle" />
<script type="text/javascript" src="/myscript" />
myBanner
</div>`;
const mockRPC = (route) => {
if (route === "/mybody/isacage") {
assert.step(route);
return { html: bannerArch };
}
};
const docCreateElement = document.createElement.bind(document);
const createElement = (tagName) => {
const elem = docCreateElement(tagName);
if (tagName === "link") {
Object.defineProperty(elem, "href", {
set(href) {
if (href.includes("/mystyle")) {
assert.step("css loaded");
}
Promise.resolve().then(() => elem.dispatchEvent(new Event("load")));
},
});
} else if (tagName === "script") {
Object.defineProperty(elem, "src", {
set(src) {
if (src.includes("/myscript")) {
assert.step("js loaded");
}
Promise.resolve().then(() => elem.dispatchEvent(new Event("load")));
},
});
}
return elem;
};
patchWithCleanup(document, { createElement });
const toy = await makeView({
serverData,
mockRPC,
resModel: "animal",
type: "toy",
views: [[1, "toy"]],
});
assert.verifySteps(["/mybody/isacage", "js loaded", "css loaded"]);
assert.containsOnce(toy, ".setmybodyfree");
assert.containsNone(toy, "script");
assert.containsNone(toy, "link");
});
QUnit.test("banner can re-render with new HTML", async (assert) => {
assert.expect(10);
assert.expect(8);
serviceRegistry.add("action", actionService, { force: true });
serverData.views["animal,1,toy"] = `
<toy banner_route="/mybody/isacage">
<Banner t-if="props.bannerRoute" bannerRoute="props.bannerRoute"/>
</toy>`;
const banners = [
`<div class="banner1">
<a type="action" data-method="setTheControl" data-model="animal" data-reload-on-close="true" />
</div>`,
`<div class="banner2">
MyBanner
/div>`,
];
const mockRPC = async (route) => {
if (route === "/mybody/isacage") {
assert.step(route);
return { html: banners.shift() };
}
if (route.includes("setTheControl")) {
return {
type: "ir.actions.act_window_close",
};
}
};
const toy = await makeView({
serverData,
mockRPC,
resModel: "animal",
type: "toy",
views: [[1, "toy"]],
});
assert.verifySteps(["/mybody/isacage"]);
assert.containsOnce(toy, ".banner1");
assert.containsNone(toy, ".banner2");
await click(toy.el.querySelector("a"));
assert.verifySteps(["/mybody/isacage"]);
assert.containsNone(toy, ".banner1");
assert.containsOnce(toy, ".banner2");
});
QUnit.test("banner does not reload on render", async (assert) => {
assert.expect(5);
serverData.views["animal,1,toy"] = `
<toy banner_route="/mybody/isacage">
<Banner t-if="props.bannerRoute" bannerRoute="props.bannerRoute"/>
</toy>`;
const bannerArch = `
<div class="setmybodyfree">
myBanner
</div>`;
const mockRPC = (route) => {
if (route === "/mybody/isacage") {
assert.step(route);
return { html: bannerArch };
}
};
const toy = await makeView({
serverData,
mockRPC,
resModel: "animal",
type: "toy",
views: [[1, "toy"]],
});
assert.verifySteps(["/mybody/isacage"]);
assert.containsOnce(toy, ".setmybodyfree");
await toy.render();
await nextTick();
assert.verifySteps([]);
assert.containsOnce(toy, ".setmybodyfree");
});
QUnit.test("click on action-bound links in banner (concurrency)", async (assert) => {
assert.expect(1);
const prom = makeDeferred();
const expectedAction = {
type: "ir.actions.client",
tag: "gout",
};
const fakeActionService = {
name: "action",
start() {
return {
doAction(action) {
assert.deepEqual(action, expectedAction);
return Promise.resolve(true);
},
};
},
};
serviceRegistry.add("action", fakeActionService, { force: true });
serverData.views["animal,1,toy"] = `
<toy banner_route="/banner_route">
<Banner t-if="props.bannerRoute" bannerRoute="props.bannerRoute"/>
<a type="action" data-method="setTheControl" data-model="animal" />
</toy>`;
const mockRPC = async (route) => {
if (route.includes("banner_route")) {
return {
html: `<div><a type="action" data-method="heartOfTheSun" data-model="animal" /></div>`,
};
}
if (route.includes("setTheControl")) {
await prom;
return {
type: "ir.actions.client",
tag: "toug",
};
}
if (route.includes("heartOfTheSun")) {
return {
type: "ir.actions.client",
tag: "gout",
};
}
};
const toy = await makeView({
mockRPC,
serverData,
resModel: "animal",
type: "toy",
views: [[1, "toy"]],
});
await click(toy.el.querySelector("a[data-method='setTheControl']"));
click(toy.el.querySelector("a[data-method='heartOfTheSun']"));
prom.resolve();
await nextTick();
});
QUnit.test("real life banner", async (assert) => {
assert.expect(10);
serverData.views["animal,1,toy"] = `
<toy banner_route="/mybody/isacage">
<Banner t-if="props.bannerRoute" bannerRoute="props.bannerRoute"/>
</toy>`;
const bannerArch = `
<div class="modal o_onboarding_modal o_technical_modal" tabindex="-1" role="dialog">
<div class="modal-dialog" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Remove Configuration Tips</h5>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-label="Close">×</span>
</button>
</div>
<div class="modal-body">
<p>Do you want to remove this configuration panel?</p>
</div>
<div class="modal-footer">
<a type="action" class="btn btn-primary" data-dismiss="modal" data-toggle="collapse" href=".o_onboarding_container" data-model="mah.model" data-method="mah_method">Remove</a>
<button type="button" class="btn btn-secondary" data-dismiss="modal">Discard</button>
</div>
</div>
</div>
</div>
<div class="o_onboarding_container collapse show">
<div class="o_onboarding" />
<div class="o_onboarding_wrap" />
<a href="#" data-toggle="modal" data-target=".o_onboarding_modal" class="float-right o_onboarding_btn_close">
<i class="fa fa-times" title="Close the onboarding panel" id="closeOnboarding"></i>
</a>
<div class="bannerContent">Content</div>
</div>
</div>
</div>`;
const mockRPC = (route, args) => {
if (route === "/mybody/isacage") {
assert.step(route);
return { html: bannerArch };
}
if (args.method === "mah_method") {
assert.step(args.method);
return true;
}
};
const toy = await makeView({
serverData,
mockRPC,
resModel: "animal",
type: "toy",
views: [[1, "toy"]],
});
const prom = new Promise((resolve) => {
const complete = (ev) => {
if (ev.target.classList.contains("o_onboarding_container")) {
resolve();
}
};
// We need to handle both events, because the transition is not
// always executed
toy.el.addEventListener("transitionend", complete);
toy.el.addEventListener("transitioncancel", complete);
});
assert.verifySteps(["/mybody/isacage"]);
assert.isNotVisible(toy.el.querySelector(".modal"));
assert.hasClass(toy.el.querySelector(".o_onboarding_container"), "collapse show");
await click(toy.el.querySelector("#closeOnboarding"));
assert.isVisible(toy.el.querySelector(".modal"));
await click(toy.el.querySelector(".modal a[type='action']"));
assert.verifySteps(["mah_method"]);
await prom;
assert.doesNotHaveClass(toy.el.querySelector(".o_onboarding_container"), "show");
assert.hasClass(toy.el.querySelector(".o_onboarding_container"), "collapse");
assert.isNotVisible(toy.el.querySelector(".modal"));
});
////////////////////////////////////////////////////////////////////////////
// js_class
////////////////////////////////////////////////////////////////////////////