[IMP] web: introduce errorhandler and notupdatable component

This commit adds two utility components:

- NotUpdatable: a component meant to be used when a component does not
  depend on anything from its parent, so it should not be rerendered
  whenever the parent is updated.  This is an optimization.
- ErrorHandler: a component that provides a nice way to catch errors
  in the rendering for a sub component
This commit is contained in:
Géry Debongnie
2021-06-18 21:31:34 +02:00
committed by Aaron Bohy
parent 76c395d7db
commit 7ca9a2012b
8 changed files with 110 additions and 40 deletions
@@ -1,17 +1,8 @@
/** @odoo-module **/
const { Component, tags } = owl;
import { ErrorHandler, NotUpdatable } from "../utils/components";
class ErrorHandler extends Component {
catchError(error) {
this.props.callback();
throw error;
}
shouldUpdate() {
return false;
}
}
ErrorHandler.template = tags.xml`<t t-component="props.dialog.class" t-props="props.dialog.props" />`;
const { Component, tags } = owl;
export class DialogContainer extends Component {
setup() {
@@ -24,16 +15,24 @@ export class DialogContainer extends Component {
}
}
errorCallBack(id) {
return () => this.close(id);
handleError(error, dialogId) {
this.close(dialogId);
Promise.resolve().then(() => {
throw error;
});
}
}
DialogContainer.components = { ErrorHandler };
DialogContainer.components = { ErrorHandler, NotUpdatable };
DialogContainer.template = tags.xml`
<div class="o_dialog_container" t-att-class="{'modal-open': Object.keys(props.dialogs).length > 0}">
<t t-foreach="Object.values(props.dialogs)" t-as="dialog" t-key="dialog.id">
<ErrorHandler dialog="dialog" t-on-dialog-closed="dialog.props.close()" callback="errorCallBack(dialog.id)"
t-att-class="{o_inactive_modal: !dialog_last}"/>
<NotUpdatable>
<ErrorHandler onError="(error) => handleError(error, dialog.id)">
<t t-component="dialog.class" t-props="dialog.props"
t-on-dialog-closed="dialog.props.close()"
t-att-class="{o_inactive_modal: !dialog_last}"/>
</ErrorHandler>
</NotUpdatable>
</t>
</div>
`;
+12 -8
View File
@@ -2,14 +2,18 @@
const { Component, tags } = owl;
export function makeNonUpdatableComponent(Comp) {
class NoUpdate extends Component {
shouldUpdate() {
return false;
export class NotUpdatable extends Component {
shouldUpdate() {
return false;
}
}
NotUpdatable.template = tags.xml`<t t-slot="default" />`;
export class ErrorHandler extends Component {
catchError(error) {
if (this.props.onError) {
this.props.onError(error);
}
}
NoUpdate.template = tags.xml`<t t-component="Comp" t-props="props"/>`;
NoUpdate.components = { Comp };
return NoUpdate;
}
ErrorHandler.template = tags.xml`<t t-slot="default" />`;
@@ -6,6 +6,7 @@ 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";
const { Component, hooks } = owl;
const { useExternalListener, useRef } = hooks;
@@ -69,6 +70,14 @@ export class NavBar extends Component {
this.env.bus.off("MENUS:APP-CHANGED", this);
}
handleItemError(error, item) {
// remove the faulty component
item.isDisplayed = () => false;
Promise.resolve().then(() => {
throw error;
});
}
get currentApp() {
return this.menuService.getCurrentApp();
}
@@ -183,4 +192,4 @@ export class NavBar extends Component {
}
}
NavBar.template = "web.NavBar";
NavBar.components = { MenuDropdown, MenuItem };
NavBar.components = { MenuDropdown, MenuItem, NotUpdatable, ErrorHandler };
@@ -31,7 +31,11 @@
<!-- Systray -->
<ul class="o_menu_systray" role="menu">
<t t-foreach="systrayItems" t-as="item" t-key="item_index">
<t t-component="item.Component" t-props="item.props" />
<NotUpdatable>
<ErrorHandler onError="error => handleItemError(error, item)">
<t t-component="item.Component" t-props="item.props" />
</ErrorHandler>
</NotUpdatable>
</t>
</ul>
</nav>
+16 -7
View File
@@ -6,7 +6,7 @@ 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 { makeNonUpdatableComponent } from "../core/utils/components";
import { ErrorHandler, NotUpdatable } from "../core/utils/components";
const { Component, hooks } = owl;
const { useExternalListener } = hooks;
@@ -20,11 +20,7 @@ export class WebClient extends Component {
this.router = useService("router");
this.user = useService("user");
useService("legacy_service_provider");
this.Components = mainComponentRegistry.getEntries().map(([name, elem]) => {
const { Component, props } = elem;
const NonUpdatableComp = makeNonUpdatableComponent(Component);
return [name, { Component: NonUpdatableComp, props }];
});
this.Components = mainComponentRegistry.getEntries();
this.title.setParts({ zopenerp: "Odoo" }); // zopenerp is easy to grep
useBus(this.env.bus, "ROUTE_CHANGE", this.loadRouterState);
useBus(this.env.bus, "ACTION_MANAGER:UI-UPDATED", (mode) => {
@@ -47,6 +43,19 @@ export class WebClient extends Component {
this.env.bus.trigger("WEB_CLIENT_READY");
}
handleComponentError(error, C) {
// remove the faulty component
this.Components.splice(this.Components.indexOf(C), 1);
/**
* we rethrow the error to notify the user something bad happened.
* We do it after a tick to make sure owl can properly finish its
* rendering
*/
Promise.resolve().then(() => {
throw error;
});
}
async loadRouterState() {
let stateLoaded = await this.actionService.loadState();
let menuId = Number(this.router.current.hash.menu_id || 0);
@@ -106,5 +115,5 @@ export class WebClient extends Component {
}
}
}
WebClient.components = { ActionContainer, NavBar };
WebClient.components = { ActionContainer, NavBar, ErrorHandler, NotUpdatable };
WebClient.template = "web.WebClient";
+11 -3
View File
@@ -3,11 +3,19 @@
<t t-name="web.WebClient" owl="1">
<body class="o_web_client" t-att-class="{'o_is_superuser': user.userId === 1}">
<NavBar/>
<ActionContainer/>
<NotUpdatable>
<NavBar/>
</NotUpdatable>
<NotUpdatable>
<ActionContainer/>
</NotUpdatable>
<div>
<t t-foreach="Components" t-as="C" t-key="C[0]">
<t t-component="C[1].Component" t-props="C[1].props"/>
<NotUpdatable>
<ErrorHandler onError="error => handleComponentError(error, C)">
<t t-component="C[1].Component" t-props="C[1].props"/>
</ErrorHandler>
</NotUpdatable>
</t>
</div>
</body>
@@ -250,6 +250,7 @@ QUnit.test("dialog component crashes", async (assert) => {
env.services.dialog.add(FailingDialog);
await prom;
assert.verifySteps(["error"]);
assert.containsOnce(pseudoWebClient, ".modal");
assert.containsOnce(pseudoWebClient, ".modal .o_dialog_error");
@@ -1,6 +1,6 @@
/** @odoo-module **/
import { makeNonUpdatableComponent } from "@web/core/utils/components";
import { NotUpdatable, ErrorHandler } from "@web/core/utils/components";
import { makeTestEnv } from "../../helpers/mock_env";
import { getFixture } from "../../helpers/utils";
@@ -9,7 +9,7 @@ const { Component, mount } = owl;
QUnit.module("utils", () => {
QUnit.module("components");
QUnit.test("makeNonUpdatableComponent", async function (assert) {
QUnit.test("NotUpdatable component", async function (assert) {
class Child extends Component {
mounted() {
assert.step("mounted");
@@ -20,8 +20,12 @@ QUnit.module("utils", () => {
}
Child.template = owl.tags.xml`<div>hey</div>`;
class Parent extends Component {}
Parent.template = owl.tags.xml`<div><Child1/><Child2/></div>`;
Parent.components = { Child1: Child, Child2: makeNonUpdatableComponent(Child) };
Parent.template = owl.tags.xml`
<div>
<Child/>
<NotUpdatable><Child/></NotUpdatable>
</div>`;
Parent.components = { Child, NotUpdatable };
const target = getFixture();
const parent = await mount(Parent, { env: makeTestEnv(), target });
@@ -31,4 +35,36 @@ QUnit.module("utils", () => {
assert.verifySteps(["willupdateprops"]);
parent.destroy();
});
QUnit.test("ErrorHandler component", async function (assert) {
class Boom extends Component {}
Boom.template = owl.tags.xml`<div><t t-esc="this.will.throw"/></div>`;
class Parent extends Component {
setup() {
this.flag = true;
}
handleError() {
this.flag = false;
this.render();
}
}
Parent.template = owl.tags.xml`
<div>
<t t-if="flag">
<ErrorHandler onError="() => handleError()">
<Boom />
</ErrorHandler>
</t>
<t t-else="">
not boom
</t>
</div>`;
Parent.components = { Boom, ErrorHandler };
const target = getFixture();
const parent = await mount(Parent, { env: makeTestEnv(), target });
assert.strictEqual(target.innerHTML, "<div> not boom </div>");
parent.destroy();
});
});