diff --git a/web/static/src/ts/core/Widget.ts b/web/static/src/ts/core/Widget.ts index 624468c3..c07997c4 100644 --- a/web/static/src/ts/core/Widget.ts +++ b/web/static/src/ts/core/Widget.ts @@ -20,7 +20,7 @@ export class Widget { env: T; el: HTMLElement | null = null; state: Object = {}; - refs: { [key: string]: Widget } = {}; + refs: { [key: string]: any } = {}; // either HTMLElement or Widget //-------------------------------------------------------------------------- // Lifecycle diff --git a/web/static/src/ts/core/bus.ts b/web/static/src/ts/core/bus.ts index ab5a8065..0e41c0ec 100644 --- a/web/static/src/ts/core/bus.ts +++ b/web/static/src/ts/core/bus.ts @@ -5,9 +5,22 @@ interface Subscription { callback: Callback; } +/** + * Simple event bus: it can emit events, and add/remove listeners. + */ export class Bus { private subscriptions: { [eventType: string]: Subscription[] } = {}; + /** + * Add a listener for the 'eventType' events. + * + * Note that the 'owner' of this event can be anything, but will more likely + * be a widget or a class. The idea is that the callback will be called with + * the proper owner bound. + * + * Also, the owner should be kind of unique. This will be used to remove the + * listener. + */ on(eventType: string, owner: any, callback: Callback) { if (!this.subscriptions[eventType]) { this.subscriptions[eventType] = []; @@ -17,16 +30,25 @@ export class Bus { callback }); } + + /** + * Remove a listener + */ off(eventType: string, owner: any) { const subs = this.subscriptions[eventType]; if (subs) { this.subscriptions[eventType] = subs.filter(s => s.owner !== owner); } } + + /** + * Emit an event of type 'eventType'. Any extra arguments will be passed to + * the listeners callback. + */ trigger(eventType: string, ...args: any[]) { const subs = this.subscriptions[eventType] || []; for (let sub of subs) { - sub.callback(...args); + sub.callback.call(sub.owner, ...args); } } } diff --git a/web/static/src/ts/env.ts b/web/static/src/ts/env.ts index 49a8fb5d..f1900bc3 100644 --- a/web/static/src/ts/env.ts +++ b/web/static/src/ts/env.ts @@ -1,28 +1,36 @@ import { QWeb } from "./core/qweb_vdom"; import { WEnv } from "./core/widget"; -import { actions, Action } from "./services/actions"; import { ActionManager } from "./services/action_manager"; -import Ajax from "./services/ajax"; -import Router from "./services/router"; +import { Ajax } from "./services/ajax"; +import { Router } from "./services/router"; + +export interface Menu { + title: string; + actionID: number; +} export interface Env extends WEnv { actionManager: ActionManager; - actions: Action[]; ajax: Ajax; router: Router; + menus: Menu[]; } export function makeEnvironment(): Env { const qweb = new QWeb(); const router = new Router(); const ajax = new Ajax(); - const actionManager = new ActionManager(); + const actionManager = new ActionManager(router); + const menus = [ + { title: "Discuss", actionID: 1 }, + { title: "CRM", actionID: 2 } + ]; return { qweb, ajax, router, actionManager, - actions + menus }; } diff --git a/web/static/src/ts/services/action_manager.ts b/web/static/src/ts/services/action_manager.ts index 92db3e89..c33260ea 100644 --- a/web/static/src/ts/services/action_manager.ts +++ b/web/static/src/ts/services/action_manager.ts @@ -1,22 +1,81 @@ -import { Action } from "./actions"; +import { Bus } from "../core/bus"; +import { Widget } from "../core/widget"; +import { Env } from "../env"; +import { Router, Query } from "./router"; +import { Discuss } from "../widgets/discuss"; +import { CRM } from "../widgets/crm"; -// export interface Action { -// id: number; -// title: string; -// Widget: Type>; -// default?: boolean; -// } +interface Type extends Function { + new (...args: any[]): T; +} -// const actions: Action[] = [ -// { id: 1, title: "Discuss", Widget: Discuss, default: true }, -// { id: 2, title: "CRM", Widget: CRM } -// ]; +interface ClientAction { + type: "client"; + name: string; +} -export class ActionManager { - doAction(action: Action) { - console.log(action); - // load data (??) - // trigger action somewhere - // upload url with router +interface ActWindowAction { + type: "act_window"; + views: string[]; +} + +export type Action = ClientAction | ActWindowAction; + +export interface ActionWidget { + id: number; + Widget: Type>; + props: any; +} + +const actions: any[] = [ + { id: 1, title: "Discuss", Widget: Discuss, default: true }, + { id: 2, title: "CRM", Widget: CRM } +]; + +export class ActionManager extends Bus { + router: Router; + currentAction: ActionWidget | null = null; + + constructor(router: Router) { + super(); + this.router = router; + const query = this.router.getQuery(); + this.update(query); + this.router.on("query_changed", this, this.update); + if (!this.currentAction) { + const action = actions.find(a => a.default); + if (action) { + this.doAction(action.id); + } + } + } + + update(query: Query) { + const initialAction = this.currentAction; + if ("action_id" in query) { + const actionID = parseInt(query.action_id); + this.doAction(actionID); + } + if (this.currentAction && initialAction !== this.currentAction) { + } + } + + doAction(actionID: number) { + const action = actions.find(a => a.id === actionID); + if (action) { + this.currentAction = { + id: action.id, + Widget: action.Widget, + props: {} + }; + } + this.trigger("action_ready", this.currentAction); + this.router.navigate({ action_id: String(actionID) }); + } + + registerAction(action: Action) {} + + getCurrentAction(): ActionWidget | null { + return this.currentAction; } } diff --git a/web/static/src/ts/services/actions.ts b/web/static/src/ts/services/actions.ts deleted file mode 100644 index 856a2984..00000000 --- a/web/static/src/ts/services/actions.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { CRM } from "../widgets/crm"; -import { Discuss } from "../widgets/discuss"; -import { Widget } from "../core/widget"; -import { Env } from "../env"; - -interface Type extends Function { - new (...args: any[]): T; -} - -export interface Action { - id: number; - title: string; - Widget: Type>; - default?: boolean; -} - -export const actions: Action[] = [ - { id: 1, title: "Discuss", Widget: Discuss, default: true }, - { id: 2, title: "CRM", Widget: CRM } -]; - -// class ActionManager { - -// doAction(action: Action) { - -// } -// } diff --git a/web/static/src/ts/services/ajax.ts b/web/static/src/ts/services/ajax.ts index df8c9ee0..a7d48ab9 100644 --- a/web/static/src/ts/services/ajax.ts +++ b/web/static/src/ts/services/ajax.ts @@ -1 +1 @@ -export default class Ajax {} +export class Ajax {} diff --git a/web/static/src/ts/services/router.ts b/web/static/src/ts/services/router.ts index 12b11445..3d4ca998 100644 --- a/web/static/src/ts/services/router.ts +++ b/web/static/src/ts/services/router.ts @@ -2,76 +2,49 @@ import { Bus } from "../core/bus"; export type Query = { [key: string]: string }; -export interface Route { - // this is the part before the hash: www.something.com/web#action=1 => web - path: string; - query: Query; - title: string; -} - -function clearSlaches(s: string): string { +function clearSlashes(s: string): string { return s.replace(/\/$/, "").replace(/^\//, ""); } -export default class Router extends Bus { - listeners: { owner: any; callback: (info: Route) => void }[] = []; +export class Router extends Bus { + currentQuery: Query; constructor() { super(); - window.addEventListener("popstate", this.onUrlChange.bind(this)); - window.onhashchange = function() { - console.log("aaaa"); - debugger; + this.currentQuery = this.getQuery(); + window.onhashchange = () => { + this.currentQuery = this.getQuery(); + this.trigger("query_changed", this.currentQuery); }; } - onUrlChange(event: PopStateEvent) { - debugger; - event.preventDefault(); - const info = this.getRoute(); - for (let listener of this.listeners) { - listener.callback.call(listener.owner, info); - } - } - /** - * @param {Route} route relative route: for example, /web/ * @param {Query} query */ - navigate(info: Partial) { - const currentRoute = this.getRoute(); - const route = info.path || currentRoute.path; - const query = info.query || {}; - const title = info.title || currentRoute.title; - const url = this.formatURL(route, query); - window.history.pushState(null, title, url); - } - register(owner: any, callback: (info: Route) => void) { - this.listeners.push({ owner, callback }); + navigate(query: Query) { + const url = this.formatURL("", query); + window.history.replaceState(null, document.title, url); } - unregister(owner: any) { - this.listeners = this.listeners.filter(l => l.owner !== owner); + formatQuery(query: Query): string { + let parts: string[] = []; + for (let key in query) { + parts.push(`${key}=${query[key]}`); + } + return parts.join("&"); } formatURL(path: string, query: Query): string { - let url = clearSlaches(path); - let hasHash = false; - for (let key in query) { - url = url + (hasHash ? "&" : "#"); - url = url + `${key}=${query[key]}`; - hasHash = true; - } - return "/" + url; + let url = clearSlashes(path); + return `/${url}#${this.formatQuery(query)}`; } - getRoute(): Route { - const path = clearSlaches(window.location.pathname); + getQuery(): Query { const query = {}; for (let part of window.location.hash.slice(1).split("?")) { let [key, value] = part.split("="); query[key] = value; } - return { path, query, title: document.title }; + return query; } } diff --git a/web/static/src/ts/widgets/Navbar.ts b/web/static/src/ts/widgets/Navbar.ts index 90f72e1d..6db49336 100644 --- a/web/static/src/ts/widgets/Navbar.ts +++ b/web/static/src/ts/widgets/Navbar.ts @@ -1,14 +1,13 @@ import { Widget } from "../core/widget"; -import { Action } from "../services/actions"; -import { Env } from "../env"; +import { Env, Menu } from "../env"; const template = `
Odoo @@ -19,13 +18,13 @@ export class Navbar extends Widget { name = "navbar"; template = template; - getUrl(action: Action) { - const action_id = String(action.id); - return this.env.router.formatURL("web", { action_id }); + getUrl(menu: Menu) { + const action_id = String(menu.actionID); + return this.env.router.formatURL("", { action_id }); } - activateAction(action: Action, event: MouseEvent) { + activateMenu(menu: Menu, event: MouseEvent) { event.preventDefault(); - this.env.actionManager.doAction(action); + this.env.actionManager.doAction(menu.actionID); } } diff --git a/web/static/src/ts/widgets/root_widget.ts b/web/static/src/ts/widgets/root_widget.ts index 8e5ed9d4..19e05cef 100644 --- a/web/static/src/ts/widgets/root_widget.ts +++ b/web/static/src/ts/widgets/root_widget.ts @@ -1,13 +1,13 @@ import { Widget } from "../core/widget"; import { Navbar } from "./navbar"; -import { Action } from "../services/actions"; +import { ActionWidget } from "../services/action_manager"; +// import { Action } from "../services/actions"; import { Env } from "../env"; const template = `
-
- +
`; @@ -16,39 +16,44 @@ export class RootWidget extends Widget { name = "root"; template = template; widgets = { Navbar }; - - constructor(env: Env) { - super(env); - this.setMainWidget(); - } + content: Widget | null = null; mounted() { - this.env.router.register(this, this.onUrlChange); - } - - setMainWidget() { - const action = this.getAction(); - (this.widgets).Content = action.Widget; - } - - onUrlChange() { - this.setMainWidget(); - // notice that this can only be safely done because the root widget is - // mounted now. - this.render(); - } - - getAction(): Action { - const routeInfo = this.env.router.getRoute(); - const actionID = parseInt(routeInfo.query.action_id); - let actions: Action[] = this.env.actions; - let action = actions.find(a => a.id === actionID); - if (!action) { - action = actions.find(a => a.default === true); - if (!action) { - throw new Error("No valid action!"); - } + this.env.actionManager.on("action_ready", this, this.setContentWidget); + const actionWidget = this.env.actionManager.getCurrentAction(); + if (actionWidget) { + this.setContentWidget(actionWidget); } - return action; } + + async setContentWidget(actionWidget: ActionWidget) { + const currentWidget = this.content; + const newWidget = new actionWidget.Widget(this, actionWidget.props); + await newWidget.mount(this.refs.content); + if (currentWidget) { + currentWidget.destroy(); + } + this.content = newWidget; + } + + // onUrlChange() { + // this.setMainWidget(); + // // notice that this can only be safely done because the root widget is + // // mounted now. + // this.render(); + // } + + // getAction(): Action { + // const routeInfo = this.env.router.getRoute(); + // const actionID = parseInt(routeInfo.query.action_id); + // let actions: Action[] = this.env.actions; + // let action = actions.find(a => a.id === actionID); + // if (!action) { + // action = actions.find(a => a.default === true); + // if (!action) { + // throw new Error("No valid action!"); + // } + // } + // return action; + // } } diff --git a/web/static/tests/bus_test.ts b/web/static/tests/bus_test.ts index 0744495d..c08d714f 100644 --- a/web/static/tests/bus_test.ts +++ b/web/static/tests/bus_test.ts @@ -10,6 +10,16 @@ describe("event bus behaviour", () => { expect(notified).toBe(true); }); + test("callbacks are called with proper 'this'", () => { + expect.assertions(1); + const bus = new Bus(); + const owner = {}; + bus.on("event", owner, function(this: any) { + expect(this).toBe(owner); + }); + bus.trigger("event"); + }); + test("can unsubscribe", () => { const bus = new Bus(); let notified = false;