diff --git a/web/static/src/ts/core/component.ts b/web/static/src/ts/core/component.ts index 1f54dc94..3f1732bd 100644 --- a/web/static/src/ts/core/component.ts +++ b/web/static/src/ts/core/component.ts @@ -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 diff --git a/web/static/src/ts/env.ts b/web/static/src/ts/env.ts index 8403d524..bd81297c 100644 --- a/web/static/src/ts/env.ts +++ b/web/static/src/ts/env.ts @@ -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; + 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; //------------------------------------------------------------------------------ // 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 +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", "
"); 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 }; diff --git a/web/static/src/ts/loaders/menus.ts b/web/static/src/ts/loaders.ts similarity index 83% rename from web/static/src/ts/loaders/menus.ts rename to web/static/src/ts/loaders.ts index 919d39f7..4e3e7f82 100644 --- a/web/static/src/ts/loaders/menus.ts +++ b/web/static/src/ts/loaders.ts @@ -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 { + 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 */ diff --git a/web/static/src/ts/loaders/templates.ts b/web/static/src/ts/loaders/templates.ts deleted file mode 100644 index 9395226d..00000000 --- a/web/static/src/ts/loaders/templates.ts +++ /dev/null @@ -1,12 +0,0 @@ -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 dd4b6cfb..77b58247 100644 --- a/web/static/src/ts/main.ts +++ b/web/static/src/ts/main.ts @@ -1,9 +1,11 @@ /// 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 diff --git a/web/static/src/ts/registries.ts b/web/static/src/ts/registries.ts index e7cdee6a..774f7545 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 "./store/action_manager"; +import { ActionWidget } from "./store"; 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 index 50804555..eba8e968 100644 --- a/web/static/src/ts/services/ajax.ts +++ b/web/static/src/ts/services/ajax.ts @@ -2,6 +2,6 @@ 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; + const delay = Math.random() * 1000; return new Promise(resolve => setTimeout(resolve, delay)); }; diff --git a/web/static/src/ts/store/router.ts b/web/static/src/ts/services/router.ts similarity index 100% rename from web/static/src/ts/store/router.ts rename to web/static/src/ts/services/router.ts diff --git a/web/static/src/ts/store.ts b/web/static/src/ts/store.ts new file mode 100644 index 00000000..784d15df --- /dev/null +++ b/web/static/src/ts/store.ts @@ -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>; + +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; + +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; + + constructor( + services: Services, + menuInfo: MenuInfo, + actionRegistry: Registry + ) { + 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) { + Object.assign(this.state, nextState); + this.trigger("state_updated", this.state); + } + + nextID = 1; + + add(notif: Partial): 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 { + 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 }; + } +} diff --git a/web/static/src/ts/store/action_manager.ts b/web/static/src/ts/store/action_manager.ts deleted file mode 100644 index c847aa9c..00000000 --- a/web/static/src/ts/store/action_manager.ts +++ /dev/null @@ -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>; - -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; - ajax: IAjax; - stack: ActionStack; - - constructor(registry: Registry, 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 []; - } -} diff --git a/web/static/src/ts/store/ajax.ts b/web/static/src/ts/store/ajax.ts deleted file mode 100644 index 49ddf485..00000000 --- a/web/static/src/ts/store/ajax.ts +++ /dev/null @@ -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; - 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; - -//------------------------------------------------------------------------------ -// 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 { - 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 }; - } -} diff --git a/web/static/src/ts/store/notifications.ts b/web/static/src/ts/store/notifications.ts deleted file mode 100644 index 68ba766e..00000000 --- a/web/static/src/ts/store/notifications.ts +++ /dev/null @@ -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): 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): 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); - } -} diff --git a/web/static/src/ts/widgets/Discuss.ts b/web/static/src/ts/widgets/Discuss.ts index bc34cab9..60408c41 100644 --- a/web/static/src/ts/widgets/Discuss.ts +++ b/web/static/src/ts/widgets/Discuss.ts @@ -46,7 +46,11 @@ export class Discuss extends Widget<{}, State> { addNotif(sticky: boolean) { const text = (this.refs.textinput).value; const message = `It is now ${new Date().toLocaleTimeString()}.
Msg: ${text}`; - this.env.notifications.add({ title: "hey", message: message, sticky }); + this.env.dispatch("add_notification", { + title: "hey", + message: message, + sticky + }); } } diff --git a/web/static/src/ts/widgets/Navbar.ts b/web/static/src/ts/widgets/Navbar.ts index 9f40e8bb..bc0af8d7 100644 --- a/web/static/src/ts/widgets/Navbar.ts +++ b/web/static/src/ts/widgets/Navbar.ts @@ -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 { 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); } } diff --git a/web/static/src/ts/widgets/action_container.ts b/web/static/src/ts/widgets/action_container.ts index e641617c..7a0aa589 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 "../store/action_manager"; +import { ActionStack } from "../store"; import { Widget } from "./widget"; //------------------------------------------------------------------------------ diff --git a/web/static/src/ts/widgets/clock.ts b/web/static/src/ts/widgets/clock.ts index f08b2305..443c0600 100644 --- a/web/static/src/ts/widgets/clock.ts +++ b/web/static/src/ts/widgets/clock.ts @@ -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(); diff --git a/web/static/src/ts/widgets/home_menu.ts b/web/static/src/ts/widgets/home_menu.ts index f4c3e4aa..31cf3ea5 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 "../loaders/menus"; +import { MenuInfo, MenuItem } from "../store"; import { Widget } from "./widget"; //------------------------------------------------------------------------------ @@ -23,6 +23,6 @@ export class HomeMenu extends Widget { openMenu(app: MenuItem, event: MouseEvent) { event.preventDefault(); - this.trigger("open_menu", app); + this.env.dispatch("open_menu", app); } } diff --git a/web/static/src/ts/widgets/notification.ts b/web/static/src/ts/widgets/notification.ts index 1ab18d70..b9bc8560 100644 --- a/web/static/src/ts/widgets/notification.ts +++ b/web/static/src/ts/widgets/notification.ts @@ -1,4 +1,4 @@ -import { INotification } from "../store/notifications"; +import { INotification } from "../store"; import { Widget } from "./widget"; export class Notification extends Widget { @@ -7,6 +7,6 @@ export class Notification extends Widget { 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); } } diff --git a/web/static/src/ts/widgets/root.ts b/web/static/src/ts/widgets/root.ts index 958d26ff..c12d9f92 100644 --- a/web/static/src/ts/widgets/root.ts +++ b/web/static/src/ts/widgets/root.ts @@ -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 { +export class Root extends Widget { 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(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"; (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", debounce(() => { const isMobile = window.innerWidth <= 768; @@ -82,61 +54,4 @@ export class Root extends Widget { } }, 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 }; - } } diff --git a/web/static/src/xml/templates.xml b/web/static/src/xml/templates.xml index cc762f22..07158f4e 100644 --- a/web/static/src/xml/templates.xml +++ b/web/static/src/xml/templates.xml @@ -2,9 +2,9 @@
- + - +