diff --git a/web/static/src/ts/env.ts b/web/static/src/ts/env.ts new file mode 100644 index 00000000..982ace92 --- /dev/null +++ b/web/static/src/ts/env.ts @@ -0,0 +1,71 @@ +import { QWeb } from "./core/qweb_vdom"; +import { idGenerator, memoize } from "./core/utils"; +import { TemplateLoader } from "./loaders/templates"; +import { actionRegistry } from "./registries"; +import { RPC } from "./services/ajax"; +import { ActionManager } from "./store/action_manager"; +import { Ajax } from "./store/ajax"; +import { NotificationManager } from "./store/notifications"; +import { Router } from "./store/router"; +import { Env } from "./widgets/widget"; + +//------------------------------------------------------------------------------ +// Types +//------------------------------------------------------------------------------ + +interface Loaders { + loadTemplates: TemplateLoader; +} + +interface Services { + rpc: RPC; +} + +type EnvBuilder = (loaders: Loaders, services: Services) => Promise; +//------------------------------------------------------------------------------ +// 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 { + // services + 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", "
"); + qweb.loadTemplates(templates); + + const env: Env = { + // Base widget requirements + qweb, + getID: idGenerator(), + + actionManager, + ajax, + notifications, + actionRegistry, + router, + + rpc: ajax.rpc, + + debug: false, + isMobile: window.innerWidth <= 768 + }; + return env; +} diff --git a/web/static/src/ts/init.ts b/web/static/src/ts/init.ts deleted file mode 100644 index 3ddeeac7..00000000 --- a/web/static/src/ts/init.ts +++ /dev/null @@ -1,153 +0,0 @@ -import { Ajax } from "./store/ajax"; -import { NotificationManager } from "./core/notifications"; -import { QWeb } from "./core/qweb_vdom"; -import { Router } from "./core/router"; -import { findInTree, idGenerator, memoize } from "./core/utils"; -import { actionRegistry } from "./registries"; -import { ActionManager } from "./store/action_manager"; -import { MenuInfo, MenuItem } from "./widgets/root"; -import { Env } from "./widgets/widget"; - -//------------------------------------------------------------------------------ -// Types -//------------------------------------------------------------------------------ - -interface InitializedData { - env: Env; - menuInfo: MenuInfo; -} - -/** - * 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 init = memoize(async function(): Promise { - // services - const qweb = new QWeb(); - const router = new Router(); - const ajax = new Ajax(ajaxFetch); - const actionManager = new ActionManager(actionRegistry, ajax); - const notifications = new NotificationManager(); - - // templates - const templates = await loadTemplates(); - qweb.addTemplate("default", "
"); - qweb.loadTemplates(templates); - - const menuInfo = loadMenus(); - - const env: Env = { - // Base widget requirements - qweb, - getID: idGenerator(), - - actionManager, - ajax, - notifications, - actionRegistry, - router, - - rpc: ajax.rpc, - - debug: false, - isMobile: window.innerWidth <= 768 - }; - return { env, menuInfo }; -}); - -//------------------------------------------------------------------------------ -// Adapters -//------------------------------------------------------------------------------ - -function ajaxFetch(route: string, params: any): Promise { - console.log("RPC", route, params); - const delay = Math.random() * 150; - return new Promise(resolve => setTimeout(resolve, delay)); -} - -/** - * Load xml templates as a string. - */ -async function loadTemplates(): Promise { - const result = await fetch("templates.xml"); - if (!result.ok) { - throw new Error("Error while fetching xml templates"); - } - return result.text(); -} - -/** - * Load all menu items - */ -function loadMenus(): MenuInfo { - const menuItems: BaseMenuItem[] = (window).odoo.menus; - if (!menuItems) { - throw new Error("Cannot find menus description"); - } - const menuInfo = getMenuInfo(menuItems); - delete (window).odoo.menus; // overkill? - return menuInfo; -} - -interface BaseMenuItem { - id: number; - name: string; - parent_id: number | false; - action: string | false; - icon: string | false; - children: BaseMenuItem[]; -} - -/** - * Generate a valid MenuInfo object from a list of BaseMenuItems. This function - * is supposed to be called once at startup. - */ -export function getMenuInfo(items: BaseMenuItem[]): MenuInfo { - const menus: { [key: number]: MenuItem | undefined } = {}; - const actionMap: { [id: number]: MenuItem | undefined } = {}; - const roots: number[] = []; - - // build MenuItems - for (let root of items) { - roots.push(root.id); - addToMap(root); - } - - function addToMap(m: BaseMenuItem, app?: MenuItem): MenuItem { - const item: Partial = { - id: m.id, - name: m.name, - parentId: m.parent_id, - action: m.action, - icon: m.icon, - actionId: -1 // will be filled later on with correct id - }; - item.app = app || (item as MenuItem); - item.children = m.children.map(c => addToMap(c, item.app as MenuItem)); - menus[item.id!] = item as MenuItem; - return item as MenuItem; - } - - // add proper actionId to every menuitems - for (let menuId in menus) { - const menu = menus[menuId]!; - let menuWithAction = menu && findInTree(menu, m => Boolean(m.action)); - if (menuWithAction) { - menu.actionId = parseInt( - (menuWithAction.action).split(",")[1], - 10 - ); - if (menuWithAction === menu) { - actionMap[menu.actionId] = menu; - } - } - } - - return { menus, roots, actionMap }; -} diff --git a/web/static/src/ts/loaders/menus.ts b/web/static/src/ts/loaders/menus.ts new file mode 100644 index 00000000..919d39f7 --- /dev/null +++ b/web/static/src/ts/loaders/menus.ts @@ -0,0 +1,99 @@ +import { findInTree } from "../core/utils"; + +//------------------------------------------------------------------------------ +// 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[]; +} + +interface BaseMenuItem { + id: number; + name: string; + parent_id: number | false; + action: string | false; + icon: string | false; + children: BaseMenuItem[]; +} + +//------------------------------------------------------------------------------ +// Code +//------------------------------------------------------------------------------ + +/** + * Load all menu items + */ +export function loadMenus(): MenuInfo { + const menuItems: BaseMenuItem[] = (window).odoo.menus; + if (!menuItems) { + throw new Error("Cannot find menus description"); + } + const menuInfo = getMenuInfo(menuItems); + delete (window).odoo.menus; // overkill? + return menuInfo; +} + +/** + * Generate a valid MenuInfo object from a list of BaseMenuItems. This function + * is supposed to be called once at startup. + */ +export function getMenuInfo(items: BaseMenuItem[]): MenuInfo { + const menus: { [key: number]: MenuItem | undefined } = {}; + const actionMap: { [id: number]: MenuItem | undefined } = {}; + const roots: number[] = []; + + // build MenuItems + for (let root of items) { + roots.push(root.id); + addToMap(root); + } + + function addToMap(m: BaseMenuItem, app?: MenuItem): MenuItem { + const item: Partial = { + id: m.id, + name: m.name, + parentId: m.parent_id, + action: m.action, + icon: m.icon, + actionId: -1 // will be filled later on with correct id + }; + item.app = app || (item as MenuItem); + item.children = m.children.map(c => addToMap(c, item.app as MenuItem)); + menus[item.id!] = item as MenuItem; + return item as MenuItem; + } + + // add proper actionId to every menuitems + for (let menuId in menus) { + const menu = menus[menuId]!; + let menuWithAction = menu && findInTree(menu, m => Boolean(m.action)); + if (menuWithAction) { + menu.actionId = parseInt( + (menuWithAction.action).split(",")[1], + 10 + ); + if (menuWithAction === menu) { + actionMap[menu.actionId] = menu; + } + } + } + + return { menus, roots, actionMap }; +} diff --git a/web/static/src/ts/loaders/templates.ts b/web/static/src/ts/loaders/templates.ts new file mode 100644 index 00000000..9395226d --- /dev/null +++ b/web/static/src/ts/loaders/templates.ts @@ -0,0 +1,12 @@ +export type TemplateLoader = () => Promise; + +/** + * Load xml templates as a string. + */ +export const loadTemplates: TemplateLoader = async function(): Promise { + const result = await fetch("templates.xml"); + if (!result.ok) { + throw new Error("Error while fetching xml templates"); + } + return result.text(); +}; diff --git a/web/static/src/ts/main.ts b/web/static/src/ts/main.ts index 7b83ee1c..dd4b6cfb 100644 --- a/web/static/src/ts/main.ts +++ b/web/static/src/ts/main.ts @@ -1,6 +1,9 @@ /// -import { init } from "./init"; +import { makeEnv } from "./env"; +import { rpc } from "./services/ajax"; +import { loadMenus } from "./loaders/menus"; +import { loadTemplates } from "./loaders/templates"; import { Root } from "./widgets/root"; //------------------------------------------------------------------------------ @@ -8,7 +11,14 @@ import { Root } from "./widgets/root"; //------------------------------------------------------------------------------ document.addEventListener("DOMContentLoaded", async function() { - const { env, menuInfo } = await init(); + const services = { + rpc + }; + const loaders = { + loadTemplates + }; + const env = await makeEnv(loaders, services); + const menuInfo = loadMenus(); // Creating root widget const rootWidget = new Root(env, { menuInfo }); diff --git a/web/static/src/ts/registries.ts b/web/static/src/ts/registries.ts index 84d54888..e7cdee6a 100644 --- a/web/static/src/ts/registries.ts +++ b/web/static/src/ts/registries.ts @@ -1,5 +1,5 @@ import { Registry } from "./core/registry"; -import { ActionWidget } from "./services/action_manager"; +import { ActionWidget } from "./store/action_manager"; import { CRM } from "./widgets/crm"; import { Discuss } from "./widgets/discuss"; diff --git a/web/static/src/ts/services/ajax.ts b/web/static/src/ts/services/ajax.ts new file mode 100644 index 00000000..50804555 --- /dev/null +++ b/web/static/src/ts/services/ajax.ts @@ -0,0 +1,7 @@ +export type RPC = (route: string, params: any) => Promise; + +export const rpc: RPC = async function(route, params) { + console.log("RPC", route, params); + const delay = Math.random() * 150; + return new Promise(resolve => setTimeout(resolve, delay)); +}; diff --git a/web/static/src/ts/core/notifications.ts b/web/static/src/ts/store/notifications.ts similarity index 100% rename from web/static/src/ts/core/notifications.ts rename to web/static/src/ts/store/notifications.ts diff --git a/web/static/src/ts/core/router.ts b/web/static/src/ts/store/router.ts similarity index 100% rename from web/static/src/ts/core/router.ts rename to web/static/src/ts/store/router.ts diff --git a/web/static/src/ts/widgets/Navbar.ts b/web/static/src/ts/widgets/Navbar.ts index b3bedc88..9f40e8bb 100644 --- a/web/static/src/ts/widgets/Navbar.ts +++ b/web/static/src/ts/widgets/Navbar.ts @@ -1,4 +1,4 @@ -import { MenuItem } from "./root"; +import { MenuItem } from "../loaders/menus"; import { PureWidget } from "./widget"; //------------------------------------------------------------------------------ diff --git a/web/static/src/ts/widgets/action_container.ts b/web/static/src/ts/widgets/action_container.ts index 9ddba23b..e641617c 100644 --- a/web/static/src/ts/widgets/action_container.ts +++ b/web/static/src/ts/widgets/action_container.ts @@ -1,4 +1,4 @@ -import { ActionStack } from "../services/action_manager"; +import { ActionStack } from "../store/action_manager"; import { Widget } from "./widget"; //------------------------------------------------------------------------------ diff --git a/web/static/src/ts/widgets/home_menu.ts b/web/static/src/ts/widgets/home_menu.ts index c63bfbd0..f4c3e4aa 100644 --- a/web/static/src/ts/widgets/home_menu.ts +++ b/web/static/src/ts/widgets/home_menu.ts @@ -1,4 +1,4 @@ -import { MenuInfo, MenuItem } from "../widgets/root"; +import { MenuInfo, MenuItem } from "../loaders/menus"; import { Widget } from "./widget"; //------------------------------------------------------------------------------ diff --git a/web/static/src/ts/widgets/notification.ts b/web/static/src/ts/widgets/notification.ts index 141709bd..1ab18d70 100644 --- a/web/static/src/ts/widgets/notification.ts +++ b/web/static/src/ts/widgets/notification.ts @@ -1,4 +1,4 @@ -import { INotification } from "../core/notifications"; +import { INotification } from "../store/notifications"; import { Widget } from "./widget"; export class Notification extends Widget { diff --git a/web/static/src/ts/widgets/root.ts b/web/static/src/ts/widgets/root.ts index 2dc5e714..67f1de08 100644 --- a/web/static/src/ts/widgets/root.ts +++ b/web/static/src/ts/widgets/root.ts @@ -1,6 +1,7 @@ -import { Query } from "../core/router"; +import { Query } from "../store/router"; import { debounce } from "../core/utils"; -import { ActionStack } from "../services/action_manager"; +import { MenuInfo, MenuItem } from "../loaders/menus"; +import { ActionStack } from "../store/action_manager"; import { ActionContainer } from "./action_container"; import { HomeMenu } from "./home_menu"; import { Navbar } from "./navbar"; @@ -11,26 +12,6 @@ import { Env, Widget } from "./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 Props { menuInfo: MenuInfo; } diff --git a/web/static/src/ts/widgets/widget.ts b/web/static/src/ts/widgets/widget.ts index 2e112333..71a622f7 100644 --- a/web/static/src/ts/widgets/widget.ts +++ b/web/static/src/ts/widgets/widget.ts @@ -1,9 +1,9 @@ -import { IAjax } from "../services/ajax"; +import { IAjax } from "../store/ajax"; import { Component, PureComponent, WEnv } from "../core/component"; -import { INotificationManager } from "../core/notifications"; +import { INotificationManager } from "../store/notifications"; import { Registry } from "../core/registry"; -import { IRouter } from "../core/router"; -import { ActionWidget, IActionManager } from "../services/action_manager"; +import { IRouter } from "../store/router"; +import { ActionWidget, IActionManager } from "../store/action_manager"; //------------------------------------------------------------------------------ // Types diff --git a/web/static/tests/core/notifications.test.ts b/web/static/tests/core/notifications.test.ts index fce4741d..233878bd 100644 --- a/web/static/tests/core/notifications.test.ts +++ b/web/static/tests/core/notifications.test.ts @@ -1,4 +1,4 @@ -import { NotificationManager } from "../../src/ts/core/notifications"; +import { NotificationManager } from "../../src/ts/store/notifications"; test("can subscribe and add notification", () => { let n = 0; diff --git a/web/static/tests/helpers.ts b/web/static/tests/helpers.ts index cff32448..32e3bbce 100644 --- a/web/static/tests/helpers.ts +++ b/web/static/tests/helpers.ts @@ -1,15 +1,14 @@ import { readFile } from "fs"; import { WEnv } from "../src/ts/core/component"; import { Callback } from "../src/ts/core/event_bus"; -import { NotificationManager } from "../src/ts/core/notifications"; +import { NotificationManager } from "../src/ts/store/notifications"; import { QWeb } from "../src/ts/core/qweb_vdom"; -import { IRouter, Query, RouterEvent } from "../src/ts/core/router"; +import { IRouter, Query, RouterEvent } from "../src/ts/store/router"; import { idGenerator } from "../src/ts/core/utils"; -import { getMenuInfo } from "../src/ts/init"; +import { getMenuInfo, MenuInfo } from "../src/ts/loaders/menus"; import { actionRegistry } from "../src/ts/registries"; import { ActionManager } from "../src/ts/store/action_manager"; import { Ajax } from "../src/ts/store/ajax"; -import { MenuInfo } from "../src/ts/widgets/root"; import { Env } from "../src/ts/widgets/widget"; export function makeTestFixture() { diff --git a/web/static/tests/widgets/action_container.test.ts b/web/static/tests/widgets/action_container.test.ts index 9aa02db7..f98c9170 100644 --- a/web/static/tests/widgets/action_container.test.ts +++ b/web/static/tests/widgets/action_container.test.ts @@ -1,4 +1,4 @@ -import { ActionStack } from "../../src/ts/services/action_manager"; +import { ActionStack } from "../../src/ts/store/action_manager"; import { ActionContainer, Props } from "../../src/ts/widgets/action_container"; import { Widget } from "../../src/ts/widgets/widget"; import * as helpers from "../helpers"; diff --git a/web/static/tests/widgets/navbar.test.ts b/web/static/tests/widgets/navbar.test.ts index 8ce28336..8e742805 100644 --- a/web/static/tests/widgets/navbar.test.ts +++ b/web/static/tests/widgets/navbar.test.ts @@ -1,6 +1,6 @@ import { Navbar, Props } from "../../src/ts/widgets/navbar"; import * as helpers from "../helpers"; -import { MenuInfo } from "../../src/ts/widgets/root"; +import { MenuInfo } from "../../src/ts/loaders/menus"; //------------------------------------------------------------------------------ // Setup and helpers diff --git a/web/static/tests/widgets/notification.test.ts b/web/static/tests/widgets/notification.test.ts index 28f0bbe9..c57dc259 100644 --- a/web/static/tests/widgets/notification.test.ts +++ b/web/static/tests/widgets/notification.test.ts @@ -1,4 +1,4 @@ -import { INotification } from "../../src/ts/core/notifications"; +import { INotification } from "../../src/ts/store/notifications"; import { Notification } from "../../src/ts/widgets/notification"; import { makeTestEnv, makeTestFixture, loadTemplates } from "../helpers";