mirror of
https://github.com/odoo/owl.git
synced 2025-10-06 19:59:41 +07:00
implement store, refactoring
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
@@ -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();
|
||||
};
|
||||
@@ -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,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";
|
||||
|
||||
|
||||
@@ -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));
|
||||
};
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
}
|
||||
@@ -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 [];
|
||||
}
|
||||
}
|
||||
@@ -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 };
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,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"/>
|
||||
|
||||
Reference in New Issue
Block a user