refactoring (action management is now applied in root widget)

This commit is contained in:
Géry Debongnie
2019-03-04 12:52:14 +01:00
parent 0983ff0d24
commit 3a761da764
12 changed files with 75 additions and 196 deletions
-13
View File
@@ -30,16 +30,3 @@ document.addEventListener("DOMContentLoaded", async function() {
// For debugging purpose, we keep a reference to the root widget in odoo
(<any>window).odoo.rootWidget = rootWidget;
});
let testMixin = Base =>
class extends Base {
calc() {
return 32;
}
};
class Foo {}
class Bar extends testMixin(Foo) {}
console.log(new Bar().calc());
+45 -33
View File
@@ -49,7 +49,13 @@ export type ActionDescription =
| ActWindowActionDescription;
export type ActionInfo = ClientActionInfo | ActWindowInfo;
export type ActionStack = ActionInfo[];
export interface Action {
id: number;
widget?: Widget<any, any>;
executor(parent: Widget<any, any>): Promise<Widget<any, any> | null>;
activate(): void;
}
//------------------------------------------------------------------------------
// Action Manager Mixin
@@ -60,55 +66,61 @@ export function actionManagerMixin<T extends ReturnType<typeof rpcMixin>>(
) {
return class extends Base {
actionCache: { [key: number]: Promise<ActionDescription> } = {};
currentAction?: Action;
lastAction?: Action;
async doAction(request: ActionRequest) {
const self = this;
const descr = await this.loadAction(request);
let executor;
switch (descr.type) {
case "ir.actions.client":
return this.doClientAction(descr);
executor = this.doClientAction(descr);
break;
case "ir.actions.act_window":
return this.doActWindowAction(descr);
executor = this.doActWindowAction(descr);
break;
default:
throw new Error("unhandled action");
}
const action: Action = {
id: this.generateID(),
executor,
activate() {
if (self.currentAction && self.currentAction.widget) {
self.currentAction.widget.destroy();
}
self.currentAction = action;
self.update({
inHome: false
});
document.title = descr.name + " - Odoo";
}
};
self.lastAction = action;
this.trigger("update_action", action);
}
doActWindowAction(descr: ActWindowActionDescription) {
let title = descr.name;
this.update({
inHome: false,
stack: [
{
id: 1,
context: {},
target: "current",
type: "act_window",
title,
Widget: View
}
]
});
document.title = descr.name + " - Odoo";
return async function executor(this: Action, parent: Widget<any, any>) {
const widget = new View(parent, {});
const div = document.createElement("div");
await widget.mount(div);
this.widget = widget;
return widget;
};
}
doClientAction(descr: ClientActionDescription) {
let key = descr.tag;
let title = descr.name;
let Widget = this.actionRegistry.get(key);
this.update({
inHome: false,
stack: [
{
id: 1,
context: {},
target: "current",
type: "client",
title,
Widget: Widget
}
]
});
document.title = descr.name + " - Odoo";
return async function executor(this: Action, parent: Widget<any, any>) {
const widget = new Widget(parent, {});
const div = document.createElement("div");
await widget.mount(div);
this.widget = widget;
return widget;
};
}
loadAction(id: number): Promise<ActionDescription> {
@@ -1,6 +1,5 @@
import { Type } from "../core/component";
import { BaseStore } from "./store";
import { idGenerator } from "../core/utils";
//------------------------------------------------------------------------------
// Notifications Mixin
@@ -15,8 +14,6 @@ export interface INotification {
export function notificationMixin<T extends Type<BaseStore>>(Base: T) {
return class extends Base {
generateID = idGenerator();
addNotification(notif: Partial<INotification>): number {
const id = this.generateID();
const defaultVals = {
+4 -9
View File
@@ -1,12 +1,9 @@
import { EventBus } from "../core/event_bus";
import { Registry } from "../core/registry";
import { idGenerator } from "../core/utils";
import { RPC } from "../services/ajax";
import { IRouter, Query } from "../services/router";
import {
actionManagerMixin,
ActionStack,
ActionWidget
} from "./action_manager_mixin";
import { actionManagerMixin, ActionWidget } from "./action_manager_mixin";
import { notificationMixin } from "./notification_mixin";
import { rpcMixin } from "./rpc_mixin";
import { MenuItem } from "./store";
@@ -15,7 +12,7 @@ import { MenuItem } from "./store";
// Types
//------------------------------------------------------------------------------
export { ActionStack, ActionWidget } from "./action_manager_mixin";
export { ActionWidget } from "./action_manager_mixin";
export { INotification } from "./notification_mixin";
export { RPC } from "./rpc_mixin";
@@ -40,7 +37,6 @@ export interface MenuInfo {
}
export interface State {
stack: ActionStack;
inHome: boolean;
currentApp: MenuItem | null;
}
@@ -55,7 +51,6 @@ export interface Services {
//------------------------------------------------------------------------------
export class BaseStore extends EventBus {
state: State = {
stack: [],
inHome: false,
currentApp: null
};
@@ -63,6 +58,7 @@ export class BaseStore extends EventBus {
services: Services;
actionRegistry: Registry<ActionWidget>;
currentQuery: Query;
generateID = idGenerator();
constructor(
services: Services,
@@ -115,7 +111,6 @@ export class Store extends actionManagerMixin(
this.state.inHome = true;
this.services.router.navigate({ home: true });
}
this.services.router.on("query_changed", this, this.updateAction);
this.updateAction(this.services.router.getQuery());
}
@@ -1,51 +0,0 @@
import { ActionStack } from "../store/store";
import { Widget } from "./widget";
//------------------------------------------------------------------------------
// Types
//------------------------------------------------------------------------------
export interface Props {
stack: ActionStack;
}
//------------------------------------------------------------------------------
// Action Container
//------------------------------------------------------------------------------
export class ActionContainer extends Widget<Props, {}> {
template = "web.action_container";
currentWidget: any;
willStart() {
return this.setContentWidget();
}
mounted() {
if (this.currentWidget && this.currentWidget.el) {
this.el!.appendChild(this.currentWidget.el);
this.currentWidget.__mount();
}
}
shouldUpdate(nextProps: Props) {
if (nextProps.stack !== this.props.stack) {
this.props = nextProps;
this.setContentWidget();
}
return false;
}
async setContentWidget() {
const info = this.props.stack[this.props.stack.length - 1];
if (info) {
const Widget = info.Widget;
let widget = new Widget(this, {});
await widget.mount(this.el || document.createElement("div"));
if (this.currentWidget) {
this.currentWidget.destroy();
}
this.currentWidget = widget;
}
}
}
+18 -2
View File
@@ -1,11 +1,11 @@
import { debounce } from "../core/utils";
import { Env } from "../env";
import { State, Store } from "../store/store";
import { ActionContainer } from "./action_container";
import { HomeMenu } from "./home_menu";
import { Navbar } from "./navbar";
import { Notification } from "./notification";
import { Widget } from "./widget";
import { Action } from "../store/action_manager_mixin";
//------------------------------------------------------------------------------
// Root Widget
@@ -13,7 +13,7 @@ import { Widget } from "./widget";
export class Root extends Widget<Store, State> {
template = "web.web_client";
widgets = { Navbar, HomeMenu, ActionContainer };
widgets = { Navbar, HomeMenu };
notifications: { [id: number]: Notification } = {};
store: Store;
@@ -53,5 +53,21 @@ export class Root extends Widget<Store, State> {
this.render();
}
}, 50));
// actions
this.store.on("update_action", this, this.applyAction);
if (this.store.lastAction) {
this.applyAction(this.store.lastAction);
}
}
async applyAction(action: Action) {
const widget = await action.executor(this);
if (widget) {
// to do: call some public method of widget instead...
(<HTMLElement>this.refs.content).appendChild(widget.el!);
widget.__mount();
action.activate();
}
}
}
+1 -3
View File
@@ -4,9 +4,7 @@
<div t-name="web.web_client" class="o_web_client">
<t t-widget="Navbar" t-props="{inHome:state.inHome,app:state.currentApp}" />
<t t-widget="HomeMenu" t-if="state.inHome" t-keep-alive="1" t-props="{menuInfo:props.menuInfo}" />
<t t-else="1">
<t t-widget="ActionContainer" t-props="{stack:state.stack}" t-keep-alive="1"/>
</t>
<div class="o_content" t-att-class="state.inHome ? 'o_hidden' : ''" t-ref="content"/>
<div class="o_notification_container" t-ref="notification_container"/>
<div class="o_loading d-none" t-ref="loading_indicator">Loading</div>
</div>
+3
View File
@@ -113,6 +113,7 @@ describe("state transitions", () => {
expect(store.services.router.getQuery()).toEqual({ home: true });
await promise;
store.lastAction!.activate();
expect(store.state.inHome).toBe(false);
expect(store.services.router.getQuery()).toEqual({
action_id: "131",
@@ -138,6 +139,8 @@ describe("state transitions", () => {
const promise = store.activateMenuItem(96);
expect(document.title).toBe("Odoo");
await promise;
store.lastAction!.activate();
expect(document.title).toBe("Discuss - Odoo");
store.toggleHomeMenu();
@@ -1,7 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`can be rendered with a non empty stack 1`] = `"<div class=\\"o_content\\"><div>some client action</div></div>"`;
exports[`can be rendered with an empty stack 1`] = `"<div class=\\"o_content\\"></div>"`;
exports[`content is updated properly when new props are given 1`] = `"<div class=\\"o_content\\"><div>some client action</div></div>"`;
@@ -26,6 +26,7 @@ exports[`can be rendered (in home menu) 1`] = `
</a>
</div>
</div>
<div class=\\"o_content o_hidden\\"></div>
<div class=\\"o_notification_container\\"></div>
<div class=\\"o_loading d-none\\">Loading</div>
</div>"
@@ -46,7 +47,7 @@ exports[`if url has action_id, will render action and navigate to proper menu_id
</div>
<div class=\\"o_content\\"><div class=\\"o_discuss\\">
<div class=\\"o_content\\"><div class=\\"o_discuss\\">
<span>DISCUSS!!</span>
<button>Reset first counter</button>
<button>Reset counter 2 in 3s</button>
@@ -73,7 +74,6 @@ exports[`if url has action_id, will render action and navigate to proper menu_id
<button>Add notif</button>
<button>Add sticky notif</button>
</div></div>
<div class=\\"o_notification_container\\"></div>
<div class=\\"o_loading d-none\\">Loading</div>
</div>"
@@ -94,7 +94,7 @@ exports[`start with no action => clicks on client action => discuss is rendered
</div>
<div class=\\"o_content\\"><div class=\\"o_discuss\\">
<div class=\\"o_content\\"><div class=\\"o_discuss\\">
<span>DISCUSS!!</span>
<button>Reset first counter</button>
<button>Reset counter 2 in 3s</button>
@@ -121,7 +121,6 @@ exports[`start with no action => clicks on client action => discuss is rendered
<button>Add notif</button>
<button>Add sticky notif</button>
</div></div>
<div class=\\"o_notification_container\\"></div>
<div class=\\"o_loading d-none\\">Loading</div>
</div>"
@@ -1,70 +0,0 @@
import { Env, makeEnv } from "../../src/ts/env";
import { ActionStack, Store } from "../../src/ts/store/store";
import { ActionContainer, Props } from "../../src/ts/widgets/action_container";
import { Widget } from "../../src/ts/widgets/widget";
import * as helpers from "../helpers";
//------------------------------------------------------------------------------
// Setup and helpers
//------------------------------------------------------------------------------
let fixture: HTMLElement;
let store: Store;
let env: Env;
let props: Props;
let templates: string;
beforeAll(async () => {
templates = await helpers.loadTemplates();
});
beforeEach(() => {
fixture = helpers.makeTestFixture();
store = helpers.makeTestStore();
env = makeEnv(store, templates);
props = { stack: [] };
});
afterEach(() => {
fixture.remove();
});
class ClientAction extends Widget<{}, {}> {
inlineTemplate = "<div>some client action</div>";
}
const demoStack: ActionStack = [
{
id: 33,
context: {},
title: "some title",
target: "new",
type: "client",
Widget: ClientAction
}
];
//------------------------------------------------------------------------------
// Tests
//------------------------------------------------------------------------------
test("can be rendered with an empty stack", async () => {
const container = new ActionContainer(env, props);
await container.mount(fixture);
expect(fixture.innerHTML).toMatchSnapshot();
});
test("can be rendered with a non empty stack", async () => {
props.stack = demoStack;
const container = new ActionContainer(env, props);
await container.mount(fixture);
expect(fixture.innerHTML).toMatchSnapshot();
});
test("content is updated properly when new props are given", async () => {
const container = new ActionContainer(env, props);
await container.mount(fixture);
await container.updateProps({ stack: demoStack });
await helpers.nextTick();
expect(fixture.innerHTML).toMatchSnapshot();
});
+1 -1
View File
@@ -45,12 +45,12 @@ test("if url has action_id, will render action and navigate to proper menu_id",
const root = new Root(env, store);
await root.mount(fixture);
await helpers.nextTick();
expect(env.services.router.getQuery()).toEqual({
action_id: "131",
menu_id: "96"
});
expect(fixture.innerHTML).toMatchSnapshot();
// we check here that the url was changed to set app id as menu_id
});
test("start with no action => clicks on client action => discuss is rendered", async () => {