implement store, refactoring

This commit is contained in:
Géry Debongnie
2019-02-19 20:47:21 +01:00
parent 6ce1af636d
commit 7f3886ebba
20 changed files with 376 additions and 467 deletions
+1 -1
View File
@@ -3,8 +3,8 @@ import sdAttrs from "../../../libs/snabbdom/src/modules/attributes";
import sdListeners from "../../../libs/snabbdom/src/modules/eventlisteners";
import { init } from "../../../libs/snabbdom/src/snabbdom";
import { VNode } from "../../../libs/snabbdom/src/vnode";
import { QWeb } from "./qweb_vdom";
import { EventBus } from "./event_bus";
import { QWeb } from "./qweb_vdom";
//------------------------------------------------------------------------------
// Types/helpers
+9 -65
View File
@@ -1,95 +1,39 @@
import { WEnv } from "./core/component";
import { QWeb } from "./core/qweb_vdom";
import { Registry } from "./core/registry";
import { idGenerator, memoize } from "./core/utils";
import { TemplateLoader } from "./loaders/templates";
import { actionRegistry } from "./registries";
import { RPC } from "./services/ajax";
import {
ActionManager,
ActionWidget,
IActionManager
} from "./store/action_manager";
import { Ajax, IAjax } from "./store/ajax";
import {
INotificationManager,
NotificationManager
} from "./store/notifications";
import { IRouter, Router } from "./store/router";
import { idGenerator } from "./core/utils";
import { Store, Services, RPC } from "./store";
//------------------------------------------------------------------------------
// Types
//------------------------------------------------------------------------------
export interface Env extends WEnv {
// services
actionManager: IActionManager;
ajax: IAjax;
notifications: INotificationManager;
router: IRouter;
// registries
actionRegistry: Registry<ActionWidget>;
services: Services;
// helpers
rpc: IAjax["rpc"];
dispatch(action: string, param?: any): void;
rpc: RPC;
// configuration
debug: boolean;
isMobile: boolean;
}
interface Loaders {
loadTemplates: TemplateLoader;
}
interface Services {
rpc: RPC;
}
type EnvBuilder = (loaders: Loaders, services: Services) => Promise<Env>;
//------------------------------------------------------------------------------
// Environment
//------------------------------------------------------------------------------
/**
* init returns the main environment for the application.
*
* Note that it does not make much sense (except for tests) to have more than
* one environment. For example, with two environment, the router code in one
* environment will probably interfere with the code from the other environment.
*
* For this reason, the result of init is memoized: every call to
* this function will actually return the same environment.
*/
export const makeEnv: EnvBuilder = memoize(_makeEnv);
async function _makeEnv(loaders: Loaders, services: Services): Promise<Env> {
// services
export function makeEnv(store: Store, templates: string): Env {
const qweb = new QWeb();
const router = new Router();
const ajax = new Ajax(services.rpc);
const actionManager = new ActionManager(actionRegistry, ajax);
const notifications = new NotificationManager();
// templates
const templates = await loaders.loadTemplates();
qweb.addTemplate("default", "<div/>");
qweb.loadTemplates(templates);
const env: Env = {
// Base widget requirements
qweb,
getID: idGenerator(),
actionManager,
ajax,
notifications,
actionRegistry,
router,
rpc: ajax.rpc,
services: store.services,
dispatch: store.dispatch.bind(store),
rpc: store.rpc.bind(store),
debug: false,
isMobile: window.innerWidth <= 768
};
@@ -1,28 +1,24 @@
import { findInTree } from "../core/utils";
import { findInTree } from "./core/utils";
import { MenuItem, MenuInfo } from "./store";
//------------------------------------------------------------------------------
// Types
// Templates
//------------------------------------------------------------------------------
export interface MenuItem {
id: number;
name: string;
parentId: number | false;
action: string | false;
icon: string | false;
// root menu id
app: MenuItem;
actionId: number;
children: MenuItem[];
/**
* Load xml templates as a string.
*/
export async function loadTemplates(): Promise<string> {
const result = await fetch("templates.xml");
if (!result.ok) {
throw new Error("Error while fetching xml templates");
}
return result.text();
}
export interface MenuInfo {
menus: { [key: number]: MenuItem | undefined };
actionMap: { [id: number]: MenuItem | undefined };
roots: number[];
}
//------------------------------------------------------------------------------
// Menus
//------------------------------------------------------------------------------
interface BaseMenuItem {
id: number;
@@ -33,10 +29,6 @@ interface BaseMenuItem {
children: BaseMenuItem[];
}
//------------------------------------------------------------------------------
// Code
//------------------------------------------------------------------------------
/**
* Load all menu items
*/
-12
View File
@@ -1,12 +0,0 @@
export type TemplateLoader = () => Promise<string>;
/**
* Load xml templates as a string.
*/
export const loadTemplates: TemplateLoader = async function(): Promise<string> {
const result = await fetch("templates.xml");
if (!result.ok) {
throw new Error("Error while fetching xml templates");
}
return result.text();
};
+11 -8
View File
@@ -1,9 +1,11 @@
///<amd-module name="main" />
import { makeEnv } from "./env";
import { loadMenus, loadTemplates } from "./loaders";
import { actionRegistry } from "./registries";
import { rpc } from "./services/ajax";
import { loadMenus } from "./loaders/menus";
import { loadTemplates } from "./loaders/templates";
import { Router } from "./services/router";
import { Store } from "./store";
import { Root } from "./widgets/root";
//------------------------------------------------------------------------------
@@ -12,16 +14,17 @@ import { Root } from "./widgets/root";
document.addEventListener("DOMContentLoaded", async function() {
const services = {
rpc
rpc,
router: new Router()
};
const loaders = {
loadTemplates
};
const env = await makeEnv(loaders, services);
const templates = await loadTemplates();
const menuInfo = loadMenus();
const store = new Store(services, menuInfo, actionRegistry);
const env = makeEnv(store, templates);
// Creating root widget
const rootWidget = new Root(env, { menuInfo });
const rootWidget = new Root(env, store);
await rootWidget.mount(document.body);
// For debugging purpose, we keep a reference to the root widget in odoo
+1 -1
View File
@@ -1,5 +1,5 @@
import { Registry } from "./core/registry";
import { ActionWidget } from "./store/action_manager";
import { ActionWidget } from "./store";
import { CRM } from "./widgets/crm";
import { Discuss } from "./widgets/discuss";
+1 -1
View File
@@ -2,6 +2,6 @@ export type RPC = (route: string, params: any) => Promise<any>;
export const rpc: RPC = async function(route, params) {
console.log("RPC", route, params);
const delay = Math.random() * 150;
const delay = Math.random() * 1000;
return new Promise(resolve => setTimeout(resolve, delay));
};
+304
View File
@@ -0,0 +1,304 @@
import { Type } from "./core/component";
import { EventBus } from "./core/event_bus";
import { Registry } from "./core/registry";
import { RPC } from "./services/ajax";
import { IRouter, Query } from "./services/router";
import { Widget } from "./widgets/widget";
//------------------------------------------------------------------------------
// Types
//------------------------------------------------------------------------------
export interface MenuItem {
id: number;
name: string;
parentId: number | false;
action: string | false;
icon: string | false;
// root menu id
app: MenuItem;
actionId: number;
children: MenuItem[];
}
export interface MenuInfo {
menus: { [key: number]: MenuItem | undefined };
actionMap: { [id: number]: MenuItem | undefined };
roots: number[];
}
export interface State {
stack: ActionStack;
inHome: boolean;
currentApp: MenuItem | null;
}
export interface Services {
rpc: RPC;
router: IRouter;
}
export type Context = { [key: string]: any };
export interface CommonActionInfo {
id: number;
context: Context;
title: string;
target: "current" | "new";
}
export type ActionRequest = string | number;
export type ActionWidget = Type<Widget<{}, {}>>;
export interface ClientActionInfo extends CommonActionInfo {
type: "client";
name: string;
Widget: ActionWidget;
}
export interface ActWindowInfo extends CommonActionInfo {
type: "act_window";
view: string;
}
export interface ActionDescription {
id: number;
type: "ir.actions.act_window" | "ir.actions.client";
target: "current";
}
export type ActionInfo = ClientActionInfo | ActWindowInfo;
export type ActionStack = ActionInfo[];
export interface RPCModelQuery {
model: string;
method: string;
args?: any[];
kwargs?: { [key: string]: any };
context?: { [key: string]: any };
}
export interface RPCControllerQuery {
route: string;
params: { [key: string]: any };
}
export type RPCQuery = RPCModelQuery | RPCControllerQuery;
export type RPC = (rpc: RPCQuery) => Promise<any>;
export interface INotification {
id: number;
title: string;
message: string;
type: "notification" | "warning";
sticky: boolean;
}
interface RequestParameters {
route: string;
params: { [key: string]: any };
}
//------------------------------------------------------------------------------
// Store
//------------------------------------------------------------------------------
export class Store extends EventBus {
state: State;
menuInfo: MenuInfo;
services: Services;
actionRegistry: Registry<ActionWidget>;
constructor(
services: Services,
menuInfo: MenuInfo,
actionRegistry: Registry<ActionWidget>
) {
super();
this.services = services;
this.menuInfo = menuInfo;
this.actionRegistry = actionRegistry;
this.state = {
stack: [],
inHome: false,
currentApp: null
};
const query = this.services.router.getQuery();
let { app, actionId } = this.getAppAndAction(query);
this.state.currentApp = app;
if (!actionId) {
this.state.inHome = true;
}
this.services.router.on("query_changed", this, this.updateAction);
this.updateAction(this.services.router.getQuery());
}
private updateAction(query: Query) {
let { app, actionId } = this.getAppAndAction(query);
this.updateAppState(app, actionId);
}
dispatch(action: string, params?: any) {
switch (action) {
case "open_menu":
this.updateAppState(params.app, params.actionId);
break;
case "toggle_home_menu":
this.updateState({ inHome: !this.state.inHome });
break;
case "add_notification":
this.add(params);
break;
case "close_notification":
this.close(params);
break;
}
}
updateState(nextState: Partial<State>) {
Object.assign(this.state, nextState);
this.trigger("state_updated", this.state);
}
nextID = 1;
add(notif: Partial<INotification>): number {
const id = this.nextID++;
const defaultVals = {
title: "",
message: "",
type: "notification",
sticky: false
};
const notification = Object.assign(defaultVals, notif, { id });
this.trigger("notification_added", notification);
if (!notification.sticky) {
setTimeout(() => this.close(id), 2500);
}
return id;
}
close(id: number) {
this.trigger("notification_closed", id);
}
private updateAppState(app: MenuItem | null, actionId: number | null) {
const newApp = app || this.state.currentApp;
if (actionId) {
const query: Query = { action_id: String(actionId) };
const menuId = newApp ? newApp.app.id : false;
if (menuId) {
query.menu_id = String(menuId);
}
if (app) {
this.updateState({ currentApp: app });
}
this.services.router.navigate(query);
this.doAction(actionId);
} else {
this.updateState({ inHome: true, currentApp: newApp });
}
}
private getAppAndAction(
query: Query
): { app: MenuItem | null; actionId: number | null } {
const menuInfo = this.menuInfo;
let app: MenuItem | null = null;
let actionId: number | null = null;
if ("action_id" in query) {
actionId = parseInt(query.action_id, 10);
if (menuInfo.actionMap[actionId]) {
const menu = menuInfo.actionMap[actionId]!;
app = menu.app;
}
}
if ("menu_id" in query) {
const menuId = parseInt(query.menu_id, 10);
const menu = menuInfo.menus[menuId];
if (menu) {
app = menu.app;
if (!actionId) {
actionId = menu.actionId;
}
}
}
return { app, actionId };
}
doAction(request: ActionRequest) {
if (typeof request === "number") {
this.loadAction(request);
// this is an action ID
let name = request === 131 ? "discuss" : "crm";
let title =
request === 131 ? "Discuss" : request === 250 ? "Notes" : "CRM";
let Widget = this.actionRegistry.get(name);
this.updateState({
inHome: false,
stack: [
{
id: 1,
context: {},
target: "current",
type: "client",
name,
title,
Widget: Widget
}
]
});
}
}
private loadAction(id: number) {
this.rpc({
route: "web/action/load",
params: {
action_id: id
}
});
}
counter: number = 0;
async rpc(rpc: RPCQuery): Promise<any> {
const request = this.prepareRequest(rpc);
if (this.counter === 0) {
this.trigger("rpc_status", "loading");
}
this.counter++;
const result = await this.services.rpc(request.route, request.params);
this.counter--;
if (this.counter === 0) {
this.trigger("rpc_status", "notloading");
}
return result;
}
private prepareRequest(query: RPCQuery): RequestParameters {
let route: string;
let params = "params" in query ? query.params : {};
if ("route" in query) {
route = query.route;
} else if ("model" in query && "method" in query) {
route = `/web/dataset/call_kw/${query.model}/${query.method}`;
params.args = query.args || [];
params.model = query.model;
params.method = query.method;
params.kwargs = Object.assign(params.kwargs || {}, query.kwargs);
params.kwargs.context =
query.context || params.context || params.kwargs.context;
} else {
throw new Error("Invalid Query");
}
// doing this remove empty keys, and undefined stuff
const sanitizedParams = JSON.parse(JSON.stringify(params));
return { route, params: sanitizedParams };
}
}
-105
View File
@@ -1,105 +0,0 @@
import { IAjax } from "./ajax";
import { Type } from "../core/component";
import { EventBus } from "../core/event_bus";
import { Registry } from "../core/registry";
import { Widget } from "../widgets/widget";
//------------------------------------------------------------------------------
// Types
//------------------------------------------------------------------------------
export type ActionRequest = string | number;
export type Context = { [key: string]: any };
export interface CommonActionInfo {
id: number;
context: Context;
title: string;
target: "current" | "new";
}
export type ActionWidget = Type<Widget<{}, {}>>;
export interface ClientActionInfo extends CommonActionInfo {
type: "client";
name: string;
Widget: ActionWidget;
}
export interface ActWindowInfo extends CommonActionInfo {
type: "act_window";
view: string;
}
export interface ActionDescription {
id: number;
type: "ir.actions.act_window" | "ir.actions.client";
target: "current";
}
export type ActionInfo = ClientActionInfo | ActWindowInfo;
export type ActionStack = ActionInfo[];
export type ActionEvent = "action_stack_updated";
type Callback = (stack: ActionStack) => void;
export interface IActionManager {
doAction(request: ActionRequest): void;
on(event: ActionEvent, owner: any, callback: Callback): void;
getStack(): ActionStack;
}
//------------------------------------------------------------------------------
// Action Manager
//------------------------------------------------------------------------------
export class ActionManager extends EventBus implements IActionManager {
registry: Registry<ActionWidget>;
ajax: IAjax;
stack: ActionStack;
constructor(registry: Registry<ActionWidget>, ajax: IAjax) {
super();
this.registry = registry;
this.ajax = ajax;
this.stack = [];
}
doAction(request: ActionRequest) {
if (typeof request === "number") {
this.loadAction(request);
// this is an action ID
let name = request === 131 ? "discuss" : "crm";
let title =
request === 131 ? "Discuss" : request === 250 ? "Notes" : "CRM";
let Widget = this.registry.get(name);
this.stack = [
{
id: 1,
context: {},
target: "current",
type: "client",
name,
title,
Widget: Widget
}
];
this.trigger("action_stack_updated", this.stack);
}
}
private loadAction(id: number) {
this.ajax.rpc({
route: "web/action/load",
params: {
action_id: id
}
});
}
getStack(): ActionStack {
return [];
}
}
-84
View File
@@ -1,84 +0,0 @@
import { EventBus } from "../core/event_bus";
//------------------------------------------------------------------------------
// Types
//------------------------------------------------------------------------------
export interface RPCModelQuery {
model: string;
method: string;
args?: any[];
kwargs?: { [key: string]: any };
context?: { [key: string]: any };
}
export interface RPCControllerQuery {
route: string;
params: { [key: string]: any };
}
export type RPCQuery = RPCModelQuery | RPCControllerQuery;
type AjaxStatus = "loading" | "notloading";
export interface IAjax {
rpc(rpc: RPCQuery): Promise<any>;
on(event: "rpc_status", owner: any, callback: (status: AjaxStatus) => void);
}
interface RequestParameters {
route: string;
params: { [key: string]: any };
}
export type FetchMethod = (route: string, params: any) => Promise<any>;
//------------------------------------------------------------------------------
// Ajax
//------------------------------------------------------------------------------
export class Ajax extends EventBus implements IAjax {
fetch: FetchMethod;
counter: number = 0;
constructor(fetch: FetchMethod) {
super();
this.fetch = fetch;
}
async rpc(rpc: RPCQuery): Promise<any> {
const request = this.prepareRequest(rpc);
if (this.counter === 0) {
this.trigger("rpc_status", "loading");
}
this.counter++;
const result = await this.fetch(request.route, request.params);
this.counter--;
if (this.counter === 0) {
this.trigger("rpc_status", "notloading");
}
return result;
}
private prepareRequest(query: RPCQuery): RequestParameters {
let route: string;
let params = "params" in query ? query.params : {};
if ("route" in query) {
route = query.route;
} else if ("model" in query && "method" in query) {
route = `/web/dataset/call_kw/${query.model}/${query.method}`;
params.args = query.args || [];
params.model = query.model;
params.method = query.method;
params.kwargs = Object.assign(params.kwargs || {}, query.kwargs);
params.kwargs.context =
query.context || params.context || params.kwargs.context;
} else {
throw new Error("Invalid Query");
}
// doing this remove empty keys, and undefined stuff
const sanitizedParams = JSON.parse(JSON.stringify(params));
return { route, params: sanitizedParams };
}
}
-55
View File
@@ -1,55 +0,0 @@
import { EventBus as Bus } from "../core/event_bus";
//------------------------------------------------------------------------------
// Types
//------------------------------------------------------------------------------
export interface INotification {
id: number;
title: string;
message: string;
type: "notification" | "warning";
sticky: boolean;
}
export interface INotificationManager {
add(notif: Partial<INotification>): number;
close(id: number): void;
on(
event: "notification_added",
owner: any,
callback: (notif: INotification) => void
): void;
on(
event: "notification_closed",
owner: any,
callback: (id: number) => void
): void;
}
//------------------------------------------------------------------------------
// Notification Manager
//------------------------------------------------------------------------------
export class NotificationManager extends Bus implements INotificationManager {
nextID = 1;
add(notif: Partial<INotification>): number {
const id = this.nextID++;
const defaultVals = {
title: "",
message: "",
type: "notification",
sticky: false
};
const notification = Object.assign(defaultVals, notif, { id });
this.trigger("notification_added", notification);
if (!notification.sticky) {
setTimeout(() => this.close(id), 2500);
}
return id;
}
close(id: number) {
this.trigger("notification_closed", id);
}
}
+5 -1
View File
@@ -46,7 +46,11 @@ export class Discuss extends Widget<{}, State> {
addNotif(sticky: boolean) {
const text = (<any>this.refs.textinput).value;
const message = `It is now ${new Date().toLocaleTimeString()}.<br/> Msg: ${text}`;
this.env.notifications.add({ title: "hey", message: message, sticky });
this.env.dispatch("add_notification", {
title: "hey",
message: message,
sticky
});
}
}
+4 -4
View File
@@ -1,4 +1,4 @@
import { MenuItem } from "../loaders/menus";
import { MenuItem } from "../store";
import { PureWidget } from "./widget";
//------------------------------------------------------------------------------
@@ -20,15 +20,15 @@ export class Navbar extends PureWidget<Props, {}> {
getUrl(menu: MenuItem) {
const action_id = String(menu.actionId);
const menu_id = String(menu.app.id);
return this.env.router.formatURL("", { action_id, menu_id });
return this.env.services.router.formatURL("", { action_id, menu_id });
}
toggleHome(ev: MouseEvent) {
ev.preventDefault();
this.trigger("toggle_home_menu");
this.env.dispatch("toggle_home_menu");
}
openMenu(menu: MenuItem) {
this.trigger("open_menu", menu);
this.env.dispatch("open_menu", menu);
}
}
@@ -1,4 +1,4 @@
import { ActionStack } from "../store/action_manager";
import { ActionStack } from "../store";
import { Widget } from "./widget";
//------------------------------------------------------------------------------
+3
View File
@@ -20,6 +20,9 @@ export class Clock extends Widget<{}, State> {
currentTime: ""
};
willStart() {
return this.env.rpc({ model: "res.partner", method: "fetch" });
}
mounted() {
this.updateTime();
this.startClock();
+2 -2
View File
@@ -1,4 +1,4 @@
import { MenuInfo, MenuItem } from "../loaders/menus";
import { MenuInfo, MenuItem } from "../store";
import { Widget } from "./widget";
//------------------------------------------------------------------------------
@@ -23,6 +23,6 @@ export class HomeMenu extends Widget<Props, {}> {
openMenu(app: MenuItem, event: MouseEvent) {
event.preventDefault();
this.trigger("open_menu", app);
this.env.dispatch("open_menu", app);
}
}
+2 -2
View File
@@ -1,4 +1,4 @@
import { INotification } from "../store/notifications";
import { INotification } from "../store";
import { Widget } from "./widget";
export class Notification extends Widget<INotification, {}> {
@@ -7,6 +7,6 @@ export class Notification extends Widget<INotification, {}> {
close(ev: MouseEvent) {
// we do not want the url to change
ev.preventDefault();
this.env.notifications.close(this.props.id);
this.env.dispatch("close_notification", this.props.id);
}
}
+15 -100
View File
@@ -1,78 +1,50 @@
import { Query } from "../store/router";
import { debounce } from "../core/utils";
import { MenuInfo, MenuItem } from "../loaders/menus";
import { ActionStack } from "../store/action_manager";
import { Env } from "../env";
import { State, Store } from "../store";
import { ActionContainer } from "./action_container";
import { HomeMenu } from "./home_menu";
import { Navbar } from "./navbar";
import { Notification } from "./notification";
import { Widget } from "./widget";
import { Env } from "../env";
//------------------------------------------------------------------------------
// Types
//------------------------------------------------------------------------------
export interface Props {
menuInfo: MenuInfo;
}
interface State {
stack: ActionStack;
inHome: boolean;
currentApp: MenuItem | null;
}
//------------------------------------------------------------------------------
// Root Widget
//------------------------------------------------------------------------------
export class Root extends Widget<Props, State> {
export class Root extends Widget<Store, State> {
template = "web.web_client";
widgets = { Navbar, HomeMenu, ActionContainer };
state: State = {
stack: [],
inHome: false,
currentApp: null
};
notifications: { [id: number]: Notification } = {};
store: Store;
constructor(env: Env, props: Props) {
super(env, props);
const query = this.env.router.getQuery();
let { app, actionId } = this.getAppAndAction(query);
this.state.currentApp = app;
if (!actionId) {
this.state.inHome = true;
}
constructor(env: Env, store: Store) {
super(env, store);
this.store = store;
this.state = store.state;
}
mounted() {
this.store.on("state_updated", this, newState => {
this.updateState(newState);
});
// notifications
this.env.notifications.on("notification_added", this, notif => {
this.store.on("notification_added", this, notif => {
const notification = new Notification(this, notif);
this.notifications[notif.id] = notification;
notification.mount(<any>this.refs.notification_container);
});
this.env.notifications.on("notification_closed", this, id => {
this.store.on("notification_closed", this, id => {
this.notifications[id].destroy();
delete this.notifications[id];
});
// loading indicator
this.env.ajax.on("rpc_status", this, status => {
this.store.on("rpc_status", this, status => {
const method = status === "loading" ? "remove" : "add";
(<any>this.refs.loading_indicator).classList[method]("d-none");
});
// actions
this.env.actionManager.on("action_stack_updated", this, stack =>
this.updateState({ stack, inHome: false })
);
this.env.router.on("query_changed", this, this.updateAction);
this.updateAction(this.env.router.getQuery());
// adding reactiveness to mobile/non mobile
window.addEventListener("resize", <any>debounce(() => {
const isMobile = window.innerWidth <= 768;
@@ -82,61 +54,4 @@ export class Root extends Widget<Props, State> {
}
}, 50));
}
private updateAction(query: Query) {
let { app, actionId } = this.getAppAndAction(query);
this.updateAppState(app, actionId);
}
private updateAppState(app: MenuItem | null, actionId: number | null) {
const newApp = app || this.state.currentApp;
if (actionId) {
const query: Query = { action_id: String(actionId) };
const menuId = newApp ? newApp.app.id : false;
if (menuId) {
query.menu_id = String(menuId);
}
if (app) {
this.updateState({ currentApp: app });
}
this.env.router.navigate(query);
this.env.actionManager.doAction(actionId);
} else {
this.updateState({ inHome: true, currentApp: newApp });
}
}
toggleHome() {
this.updateState({ inHome: !this.state.inHome });
}
openMenu(menu: MenuItem) {
this.updateAppState(menu.app, menu.actionId);
}
private getAppAndAction(
query: Query
): { app: MenuItem | null; actionId: number | null } {
const menuInfo = this.props.menuInfo;
let app: MenuItem | null = null;
let actionId: number | null = null;
if ("action_id" in query) {
actionId = parseInt(query.action_id, 10);
if (menuInfo.actionMap[actionId]) {
const menu = menuInfo.actionMap[actionId]!;
app = menu.app;
}
}
if ("menu_id" in query) {
const menuId = parseInt(query.menu_id, 10);
const menu = menuInfo.menus[menuId];
if (menu) {
app = menu.app;
if (!actionId) {
actionId = menu.actionId;
}
}
}
return { app, actionId };
}
}
+2 -2
View File
@@ -2,9 +2,9 @@
<templates id="template" xml:space="preserve">
<div t-name="web.web_client" class="o_web_client">
<t t-widget="Navbar" t-props="{inHome:state.inHome,app:state.currentApp}" t-on-toggle_home_menu="toggleHome" t-on-open_menu="openMenu"/>
<t t-widget="Navbar" t-props="{inHome:state.inHome,app:state.currentApp}" />
<t t-if="state.inHome">
<t t-widget="HomeMenu" t-keep-alive="1" t-props="{menuInfo:props.menuInfo}" t-on-open_menu="openMenu"/>
<t t-widget="HomeMenu" t-keep-alive="1" t-props="{menuInfo:props.menuInfo}" />
</t>
<t t-else="1">
<t t-widget="ActionContainer" t-props="{stack:state.stack}" t-keep-alive="1"/>