[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:
committed by
Aaron Bohy
parent
76c395d7db
commit
7ca9a2012b
@@ -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>
|
||||
`;
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user