mirror of
https://github.com/odoo/owl.git
synced 2025-10-06 19:59:41 +07:00
refactoring (action management is now applied in root widget)
This commit is contained in:
@@ -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());
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
@@ -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 () => {
|
||||
|
||||
Reference in New Issue
Block a user