mirror of
https://github.com/odoo/owl.git
synced 2025-10-06 19:59:41 +07:00
move demo code to examples/
This commit is contained in:
@@ -0,0 +1,47 @@
|
||||
import { Widget } from "../widget";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Types
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
interface State {
|
||||
currentTime: string;
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Clock
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
export class Clock extends Widget<{}, State> {
|
||||
inlineTemplate = `<div class="o_clock"><t t-esc="state.currentTime"/></div>`;
|
||||
timeout: any | undefined;
|
||||
|
||||
state = {
|
||||
currentTime: ""
|
||||
};
|
||||
|
||||
willStart() {
|
||||
return this.env.rpc({ model: "res.partner", method: "fetch" });
|
||||
}
|
||||
mounted() {
|
||||
this.updateTime();
|
||||
this.startClock();
|
||||
}
|
||||
|
||||
willUnmount() {
|
||||
clearTimeout(this.timeout);
|
||||
}
|
||||
|
||||
updateTime() {
|
||||
this.setState({ currentTime: new Date().toLocaleTimeString() });
|
||||
}
|
||||
|
||||
startClock() {
|
||||
const now = Date.now();
|
||||
const offset = 1000 - (now % 1000);
|
||||
this.timeout = setTimeout(() => {
|
||||
this.updateTime();
|
||||
this.startClock();
|
||||
}, offset);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
import { Widget } from "../widget";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Types
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
interface Props {
|
||||
initialState?: number;
|
||||
}
|
||||
|
||||
interface State {
|
||||
counter: number;
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Counter
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
export class Counter extends Widget<Props, State> {
|
||||
inlineTemplate = `
|
||||
<div t-name="counter">
|
||||
<button t-on-click="increment(-1)">-</button>
|
||||
<span style="font-weight:bold">Value: <t t-esc="state.counter"/></span>
|
||||
<button t-on-click="increment(1)">+</button>
|
||||
</div>`;
|
||||
|
||||
state = {
|
||||
counter: 0
|
||||
};
|
||||
|
||||
constructor(parent: Widget<any, any>, props: Props) {
|
||||
super(parent, props);
|
||||
this.state.counter = props.initialState || 0;
|
||||
}
|
||||
|
||||
increment(delta: number) {
|
||||
this.setState({ counter: this.state.counter + delta });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
import { Clock } from "./clock";
|
||||
import { Counter } from "./counter";
|
||||
import { Widget } from "../widget";
|
||||
import { ControlPanel } from "../ui/control_panel";
|
||||
//------------------------------------------------------------------------------
|
||||
// Types
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
interface State {
|
||||
validcounter: boolean;
|
||||
color: "red" | "blue";
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Discuss
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
export class Discuss extends Widget<{}, State> {
|
||||
template = "web.discuss";
|
||||
widgets = { Clock, Counter, ColorWidget, ControlPanel };
|
||||
state: State = { validcounter: true, color: "red" };
|
||||
|
||||
resetCounter(ev: MouseEvent) {
|
||||
if (this.refs.counter instanceof Counter) {
|
||||
this.refs.counter.setState({ counter: 3 });
|
||||
}
|
||||
}
|
||||
|
||||
resetCounterAsync(ev: MouseEvent) {
|
||||
setTimeout(() => {
|
||||
if (this.refs.counter2 instanceof Counter) {
|
||||
this.refs.counter2.setState({ counter: 300 });
|
||||
}
|
||||
}, 3000);
|
||||
}
|
||||
|
||||
toggle() {
|
||||
this.setState({ validcounter: !this.state.validcounter });
|
||||
}
|
||||
|
||||
toggleColor() {
|
||||
const newColor = this.state.color === "red" ? "blue" : "red";
|
||||
this.setState({ color: newColor });
|
||||
}
|
||||
|
||||
addNotif(sticky: boolean) {
|
||||
const text = (<any>this.refs.textinput).value;
|
||||
const message = `It is now ${new Date().toLocaleTimeString()}.<br/> Msg: ${text}`;
|
||||
this.env.addNotification({
|
||||
title: "hey",
|
||||
message: message,
|
||||
sticky
|
||||
});
|
||||
}
|
||||
|
||||
addWarning() {
|
||||
this.env.addNotification({
|
||||
title: "This is a warning",
|
||||
message: "All your bases are belong to us",
|
||||
type: "warning",
|
||||
sticky: true
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
class ColorWidget extends Widget<{ color: "red" | "blue" }, {}> {
|
||||
inlineTemplate = `
|
||||
<div t-name="colorwidget">
|
||||
<span>Current Color: </span>
|
||||
<t t-esc="props.color"/>
|
||||
</div>`;
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
import { Env as WEnv } from "../../../../../src/component";
|
||||
import { QWeb } from "../../../../../src/qweb";
|
||||
import { Registry } from "../../../../../src/registry";
|
||||
import { RPC as RPCService } from "./services/ajax";
|
||||
import { RPC } from "./store/store";
|
||||
import { IRouter } from "./services/router";
|
||||
import { ControllerWidget, Store, Notification } from "./store/store";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Types
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
export interface Services {
|
||||
rpc: RPCService;
|
||||
router: IRouter;
|
||||
}
|
||||
|
||||
export interface Env extends WEnv {
|
||||
services: Services;
|
||||
|
||||
// registries
|
||||
actionRegistry: Registry<ControllerWidget>;
|
||||
viewRegistry: Registry<ControllerWidget>;
|
||||
|
||||
// commands
|
||||
activateMenuItem(menuId: number): void;
|
||||
addNotification(notif: Partial<Notification>): number;
|
||||
closeNotification(id: number): void;
|
||||
toggleHomeMenu(): void;
|
||||
|
||||
// helpers
|
||||
rpc: RPC;
|
||||
|
||||
// configuration
|
||||
debug: boolean;
|
||||
isMobile: boolean;
|
||||
}
|
||||
|
||||
export interface InitData {
|
||||
services: Services;
|
||||
templates: string;
|
||||
actionRegistry: Registry<ControllerWidget>;
|
||||
viewRegistry: Registry<ControllerWidget>;
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Environment
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
export function makeEnv(data: InitData): Env {
|
||||
const qweb = new QWeb();
|
||||
qweb.addTemplate("default", "<div/>");
|
||||
qweb.loadTemplates(data.templates);
|
||||
|
||||
function throwNonImplementedError(): any {
|
||||
throw new Error("Not implemented. This feature requires a store.");
|
||||
}
|
||||
|
||||
const env: Env = {
|
||||
qweb,
|
||||
|
||||
services: data.services,
|
||||
actionRegistry: data.actionRegistry,
|
||||
viewRegistry: data.viewRegistry,
|
||||
|
||||
activateMenuItem: throwNonImplementedError,
|
||||
addNotification: throwNonImplementedError,
|
||||
closeNotification: throwNonImplementedError,
|
||||
toggleHomeMenu: throwNonImplementedError,
|
||||
|
||||
rpc: throwNonImplementedError,
|
||||
|
||||
debug: false,
|
||||
isMobile: window.innerWidth <= 768
|
||||
};
|
||||
return env;
|
||||
}
|
||||
|
||||
export function linkStoreToEnv(env: Env, store: Store) {
|
||||
env.activateMenuItem = store.activateMenuItem.bind(store);
|
||||
env.addNotification = store.addNotification.bind(store);
|
||||
env.closeNotification = store.closeNotification.bind(store);
|
||||
env.toggleHomeMenu = store.toggleHomeMenu.bind(store);
|
||||
env.rpc = store.rpc.bind(store);
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
import { findInTree } from "../../../../../src/utils";
|
||||
import { MenuInfo, MenuItem } from "./store/store";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Templates
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* 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();
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Menus
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
export interface BaseMenuItem {
|
||||
id: number;
|
||||
name: string;
|
||||
parent_id: number | false;
|
||||
action: string | false;
|
||||
icon: string | false;
|
||||
children: BaseMenuItem[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Load all menu items
|
||||
*/
|
||||
export function loadMenus(): MenuInfo {
|
||||
const menuItems: BaseMenuItem[] = (<any>window).odoo.menus;
|
||||
if (!menuItems) {
|
||||
throw new Error("Cannot find menus description");
|
||||
}
|
||||
const menuInfo = getMenuInfo(menuItems);
|
||||
delete (<any>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<MenuItem> = {
|
||||
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(
|
||||
(<string>menuWithAction.action).split(",")[1],
|
||||
10
|
||||
);
|
||||
if (menuWithAction === menu) {
|
||||
actionMap[menu.actionId] = menu;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { menus, roots, actionMap };
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
///<amd-module name="main" />
|
||||
|
||||
import { makeEnv, linkStoreToEnv } from "./env";
|
||||
import { loadMenus, loadTemplates } from "./loaders";
|
||||
import { actionRegistry, viewRegistry } from "./registries";
|
||||
import { rpc } from "./services/ajax";
|
||||
import { Router } from "./services/router";
|
||||
import { Store } from "./store/store";
|
||||
import { Root } from "./ui/root";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Web Client Bootstrapping
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
document.addEventListener("DOMContentLoaded", async function() {
|
||||
const services = {
|
||||
rpc,
|
||||
router: new Router()
|
||||
};
|
||||
|
||||
const templates = await loadTemplates();
|
||||
const menuInfo = loadMenus();
|
||||
const env = makeEnv({ services, templates, actionRegistry, viewRegistry });
|
||||
const store = new Store(env, menuInfo);
|
||||
linkStoreToEnv(env, store);
|
||||
|
||||
// Creating root widget
|
||||
const rootWidget = new Root(env, store);
|
||||
await rootWidget.mount(document.body);
|
||||
|
||||
// For debugging purpose, we keep a reference to the root widget in odoo
|
||||
(<any>window).odoo.rootWidget = rootWidget;
|
||||
});
|
||||
@@ -0,0 +1,25 @@
|
||||
import { Registry } from "../../../../../src/registry";
|
||||
import { ControllerWidget } from "./store/store";
|
||||
import { Discuss } from "./discuss/discuss";
|
||||
import { ListView } from "./views/list_view";
|
||||
import { KanbanView } from "./views/kanban_view";
|
||||
import { FormView } from "./views/form_view";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Views
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
export const viewRegistry: Registry<ControllerWidget> = new Registry();
|
||||
|
||||
viewRegistry
|
||||
.add("list", ListView)
|
||||
.add("kanban", KanbanView)
|
||||
.add("form", FormView);
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Client Actions
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
export const actionRegistry: Registry<ControllerWidget> = new Registry();
|
||||
|
||||
actionRegistry.add("mail.discuss", Discuss);
|
||||
@@ -0,0 +1,5 @@
|
||||
export type RPC = (route: string, params: any) => Promise<any>;
|
||||
|
||||
export const rpc: RPC = async function(route, params) {
|
||||
return (<any>window).demoData.mockAjax(route, params);
|
||||
};
|
||||
@@ -0,0 +1,71 @@
|
||||
import { EventBus, Callback } from "../../../../../../src/event_bus";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Types and helpers
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
export type Query = { [key: string]: string | true };
|
||||
|
||||
export type RouterEvent = "query_changed";
|
||||
|
||||
export interface IRouter {
|
||||
navigate(query: Query): void;
|
||||
on(event: RouterEvent, owner: any, callback: Callback): void;
|
||||
getQuery(): Query;
|
||||
formatURL(path: string, query: Query): string;
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Router
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
function clearSlashes(s: string): string {
|
||||
return s.replace(/\/$/, "").replace(/^\//, "");
|
||||
}
|
||||
|
||||
export class Router extends EventBus implements IRouter {
|
||||
currentQuery: Query;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.currentQuery = this.getQuery();
|
||||
window.onhashchange = () => {
|
||||
this.currentQuery = this.getQuery();
|
||||
this.trigger("query_changed", this.currentQuery);
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Query} query
|
||||
*/
|
||||
navigate(query: Query) {
|
||||
const url = this.formatURL("", query);
|
||||
window.history.replaceState(null, document.title, url);
|
||||
}
|
||||
|
||||
formatQuery(query: Query): string {
|
||||
let parts: string[] = [];
|
||||
for (let key in query) {
|
||||
if (query[key] === true) {
|
||||
parts.push(key);
|
||||
} else {
|
||||
parts.push(`${key}=${query[key]}`);
|
||||
}
|
||||
}
|
||||
return parts.join("&");
|
||||
}
|
||||
|
||||
formatURL(path: string, query: Query): string {
|
||||
let url = clearSlashes(path);
|
||||
return `/${url}#${this.formatQuery(query)}`;
|
||||
}
|
||||
|
||||
getQuery(): Query {
|
||||
const query = {};
|
||||
for (let part of window.location.hash.slice(1).split("&")) {
|
||||
let [key, value] = part.split("=");
|
||||
query[key] = value;
|
||||
}
|
||||
return query;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,155 @@
|
||||
import { Type } from "../../../../../../src/component";
|
||||
import { rpcMixin } from "./rpc_mixin";
|
||||
import { Widget } from "../widget";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Types
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
// Miscellaneous
|
||||
export type ActionRequest = number;
|
||||
|
||||
export type ControllerWidget = Type<Widget<{}, {}>>;
|
||||
|
||||
// Action Description
|
||||
interface BaseActionDescription {
|
||||
id: number;
|
||||
target: "current";
|
||||
name: string;
|
||||
}
|
||||
export interface ClientActionDescription extends BaseActionDescription {
|
||||
type: "ir.actions.client";
|
||||
tag: string;
|
||||
}
|
||||
|
||||
export interface ActWindowActionDescription extends BaseActionDescription {
|
||||
type: "ir.actions.act_window";
|
||||
views: [false | number, string][];
|
||||
domain: false | string;
|
||||
res_id: number;
|
||||
res_model: string;
|
||||
context: Object | string;
|
||||
}
|
||||
|
||||
export type ActionDescription =
|
||||
| ClientActionDescription
|
||||
| ActWindowActionDescription;
|
||||
|
||||
// Controller
|
||||
export interface Controller {
|
||||
id: number;
|
||||
actionId: number;
|
||||
widget?: Widget<any, any>;
|
||||
create(parent: Widget<any, any>): Promise<Widget<any, any> | null>;
|
||||
title: string;
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Action Manager Mixin
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
export function actionManagerMixin<T extends ReturnType<typeof rpcMixin>>(
|
||||
Base: T
|
||||
) {
|
||||
return class extends Base {
|
||||
actionCache: { [key: number]: Promise<ActionDescription> } = {};
|
||||
currentController?: Controller;
|
||||
lastController?: Controller;
|
||||
|
||||
async doAction(request: ActionRequest) {
|
||||
const self = this;
|
||||
const descr = await this.loadAction(request);
|
||||
let executor;
|
||||
switch (descr.type) {
|
||||
case "ir.actions.client":
|
||||
executor = this.doClientAction(descr);
|
||||
break;
|
||||
case "ir.actions.act_window":
|
||||
executor = this.doActWindowAction(descr);
|
||||
break;
|
||||
default:
|
||||
throw new Error("unhandled action");
|
||||
}
|
||||
const action: Controller = {
|
||||
id: this.generateID(),
|
||||
actionId: descr.id,
|
||||
create: executor,
|
||||
title: descr.name
|
||||
};
|
||||
self.lastController = action;
|
||||
this.trigger("update_action", action);
|
||||
}
|
||||
|
||||
doActWindowAction(descr: ActWindowActionDescription) {
|
||||
const tag = descr.views[0][1];
|
||||
let View = this.env.viewRegistry.get(tag);
|
||||
if (!View) {
|
||||
this.addNotification({
|
||||
title: "Invalid View type",
|
||||
type: "warning",
|
||||
message: `Cannot find view of type '${tag}' in the view registry`
|
||||
});
|
||||
View = Widget;
|
||||
}
|
||||
return async function executor(
|
||||
this: Controller,
|
||||
parent: Widget<any, any>
|
||||
) {
|
||||
const widget = new View!(parent, {
|
||||
info: descr.views[0][1],
|
||||
title: descr.name
|
||||
});
|
||||
const div = document.createElement("div");
|
||||
await widget.mount(div);
|
||||
this.widget = widget;
|
||||
return widget;
|
||||
};
|
||||
}
|
||||
|
||||
doClientAction(descr: ClientActionDescription): Controller["create"] {
|
||||
let key = descr.tag;
|
||||
let ActionWidget = this.env.actionRegistry.get(key);
|
||||
if (!ActionWidget) {
|
||||
this.addNotification({
|
||||
title: "Invalid Client Action",
|
||||
type: "warning",
|
||||
message: `Cannot find widget '${key}' in the action registry`
|
||||
});
|
||||
ActionWidget = Widget;
|
||||
}
|
||||
return async function executor(
|
||||
this: Controller,
|
||||
parent: Widget<any, any>
|
||||
) {
|
||||
const widget = new ActionWidget!(parent, {});
|
||||
const div = document.createElement("div");
|
||||
await widget.mount(div);
|
||||
this.widget = widget;
|
||||
return widget;
|
||||
};
|
||||
}
|
||||
|
||||
loadAction(id: number): Promise<ActionDescription> {
|
||||
if (id in this.actionCache) {
|
||||
return this.actionCache[id];
|
||||
}
|
||||
return (this.actionCache[id] = this.rpc({
|
||||
route: "web/action/load",
|
||||
params: {
|
||||
action_id: id
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
activateController(controller: Controller) {
|
||||
if (this.currentController && this.currentController.widget) {
|
||||
this.currentController.widget.destroy();
|
||||
}
|
||||
this.currentController = controller;
|
||||
this.update({
|
||||
inHome: false
|
||||
});
|
||||
document.title = controller.title + " - Odoo";
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,74 @@
|
||||
import { Type } from "../../../../../../src/component";
|
||||
import { BaseStore } from "./store";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// 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;
|
||||
|
||||
export type RPC = (rpc: RPCQuery) => Promise<any>;
|
||||
|
||||
export interface RequestParameters {
|
||||
route: string;
|
||||
params: { [key: string]: any };
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// rpc Mixin
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
export function rpcMixin<T extends Type<BaseStore>>(Base: T) {
|
||||
return class extends Base {
|
||||
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.env.services.rpc(request.route, request.params);
|
||||
this.counter--;
|
||||
if (this.counter === 0) {
|
||||
this.trigger("rpc_status", "notloading");
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
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 };
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
import { EventBus } from "../../../../../../src/event_bus";
|
||||
import { idGenerator } from "../../../../../../src/utils";
|
||||
import { Env } from "../env";
|
||||
import { Query } from "../services/router";
|
||||
import { actionManagerMixin } from "./action_manager_mixin";
|
||||
import { rpcMixin } from "./rpc_mixin";
|
||||
import { MenuItem } from "./store";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Types
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
export { ControllerWidget } from "./action_manager_mixin";
|
||||
export { RPC } from "./rpc_mixin";
|
||||
|
||||
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 Notification {
|
||||
id: number;
|
||||
title: string;
|
||||
message: string;
|
||||
type: "notification" | "warning";
|
||||
sticky: boolean;
|
||||
}
|
||||
|
||||
export interface MenuInfo {
|
||||
menus: { [key: number]: MenuItem | undefined };
|
||||
|
||||
actionMap: { [id: number]: MenuItem | undefined };
|
||||
roots: number[];
|
||||
}
|
||||
|
||||
export interface State {
|
||||
inHome: boolean;
|
||||
currentApp: MenuItem | null;
|
||||
notifications: Notification[];
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Store
|
||||
//------------------------------------------------------------------------------
|
||||
export class BaseStore extends EventBus {
|
||||
state: State = {
|
||||
inHome: false,
|
||||
currentApp: null,
|
||||
notifications: []
|
||||
};
|
||||
menuInfo: MenuInfo;
|
||||
env: Env;
|
||||
currentQuery: Query;
|
||||
generateID = idGenerator();
|
||||
|
||||
constructor(env: Env, menuInfo: MenuInfo) {
|
||||
super();
|
||||
this.env = env;
|
||||
this.menuInfo = menuInfo;
|
||||
this.currentQuery = {};
|
||||
}
|
||||
|
||||
update(nextState: Partial<State>) {
|
||||
Object.assign(this.state, nextState);
|
||||
this.trigger("state_updated", this.state);
|
||||
}
|
||||
|
||||
toggleHomeMenu() {
|
||||
if (this.state.inHome && !this.state.currentApp) {
|
||||
return;
|
||||
}
|
||||
this.update({ inHome: !this.state.inHome });
|
||||
if (this.state.inHome) {
|
||||
this.env.services.router.navigate({ home: true });
|
||||
} else {
|
||||
this.updateQuery(this.currentQuery);
|
||||
}
|
||||
}
|
||||
|
||||
updateQuery(query: Query) {
|
||||
this.currentQuery = query;
|
||||
this.env.services.router.navigate(query);
|
||||
}
|
||||
|
||||
addNotification(notif: Partial<Notification>): number {
|
||||
const id = this.generateID();
|
||||
const defaultVals = {
|
||||
title: "",
|
||||
message: "",
|
||||
type: "notification",
|
||||
sticky: false
|
||||
};
|
||||
const notification = Object.assign(defaultVals, notif, { id });
|
||||
const notifs = this.state.notifications.concat(notification);
|
||||
this.update({ notifications: notifs });
|
||||
if (!notification.sticky) {
|
||||
setTimeout(() => this.closeNotification(id), 2500);
|
||||
}
|
||||
return id;
|
||||
}
|
||||
|
||||
closeNotification(id: number) {
|
||||
const notifs = this.state.notifications.filter(n => n.id !== id);
|
||||
this.update({ notifications: notifs });
|
||||
}
|
||||
}
|
||||
|
||||
export class Store extends actionManagerMixin(rpcMixin(BaseStore)) {
|
||||
constructor(env: Env, menuInfo: MenuInfo) {
|
||||
super(env, menuInfo);
|
||||
const query = this.env.services.router.getQuery();
|
||||
let { app, actionId } = this.getAppAndAction(query);
|
||||
this.state.currentApp = app;
|
||||
if (!actionId) {
|
||||
this.state.inHome = true;
|
||||
this.env.services.router.navigate({ home: true });
|
||||
}
|
||||
this.env.services.router.on("query_changed", this, this.updateAction);
|
||||
this.updateAction(this.env.services.router.getQuery());
|
||||
}
|
||||
|
||||
private updateAction(query: Query) {
|
||||
let { app, actionId } = this.getAppAndAction(query);
|
||||
this.updateAppState(app, actionId);
|
||||
}
|
||||
|
||||
activateMenuItem(menuId: number) {
|
||||
const menu = this.menuInfo.menus[menuId];
|
||||
if (!menu) {
|
||||
throw new Error("Invalid menu id");
|
||||
}
|
||||
return this.updateAppState(menu.app, menu.actionId);
|
||||
}
|
||||
|
||||
private async 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.update({ currentApp: app });
|
||||
}
|
||||
await this.doAction(actionId);
|
||||
this.updateQuery(query);
|
||||
} else {
|
||||
this.update({ inHome: true, currentApp: newApp });
|
||||
}
|
||||
}
|
||||
|
||||
getAppAndAction(
|
||||
query: Query
|
||||
): { app: MenuItem | null; actionId: number | null } {
|
||||
const menuInfo = this.menuInfo;
|
||||
let app: MenuItem | null = null;
|
||||
let actionId: number | null = null;
|
||||
if (typeof query.action_id === "string") {
|
||||
actionId = parseInt(query.action_id, 10);
|
||||
if (menuInfo.actionMap[actionId]) {
|
||||
const menu = menuInfo.actionMap[actionId]!;
|
||||
app = menu.app;
|
||||
}
|
||||
}
|
||||
if (typeof query.menu_id === "string") {
|
||||
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 };
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { Widget } from "../widget";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Types
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
export interface Props {
|
||||
title: string;
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Navbar
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
export class ControlPanel extends Widget<Props, {}> {
|
||||
template = "web.control_panel";
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import { MenuInfo, MenuItem } from "../store/store";
|
||||
import { Widget } from "../widget";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Types
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
export interface Props {
|
||||
menuInfo: MenuInfo;
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Home Menu
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
export class HomeMenu extends Widget<Props, {}> {
|
||||
template = "web.home_menu";
|
||||
|
||||
get apps(): MenuItem[] {
|
||||
const info = this.props.menuInfo;
|
||||
return info.roots.map(root => info.menus[root]!);
|
||||
}
|
||||
|
||||
openMenu(app: MenuItem, event: MouseEvent) {
|
||||
event.preventDefault();
|
||||
this.env.activateMenuItem(app.id);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import { MenuItem } from "../store/store";
|
||||
import { PureWidget } from "../widget";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Types
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
export interface Props {
|
||||
inHome: boolean;
|
||||
app: MenuItem | null;
|
||||
}
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Navbar
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
export class Navbar extends PureWidget<Props, {}> {
|
||||
template = "web.navbar";
|
||||
|
||||
getUrl(menu: MenuItem) {
|
||||
const action_id = String(menu.actionId);
|
||||
const menu_id = String(menu.app.id);
|
||||
return this.env.services.router.formatURL("", { action_id, menu_id });
|
||||
}
|
||||
|
||||
toggleHome(ev: MouseEvent) {
|
||||
ev.preventDefault();
|
||||
this.env.toggleHomeMenu();
|
||||
}
|
||||
|
||||
openMenu(menu: MenuItem) {
|
||||
this.env.activateMenuItem(menu.id);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import { Notification as INotification } from "../store/store";
|
||||
import { Widget } from "../widget";
|
||||
|
||||
export class Notification extends Widget<INotification, {}> {
|
||||
template = "web.notification";
|
||||
|
||||
close(ev: MouseEvent) {
|
||||
// we do not want the url to change
|
||||
ev.preventDefault();
|
||||
this.env.closeNotification(this.props.id);
|
||||
}
|
||||
|
||||
get icon() {
|
||||
return this.props.type === "warning" ? "fa-exclamation" : "fa-lightbulb";
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
import { debounce } from "../../../../../../src/utils";
|
||||
import { Env } from "../env";
|
||||
import { Controller } from "../store/action_manager_mixin";
|
||||
import { State, Store } from "../store/store";
|
||||
import { Widget } from "../widget";
|
||||
import { HomeMenu } from "./home_menu";
|
||||
import { Navbar } from "./navbar";
|
||||
import { Notification } from "./notification";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Root Widget
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
export class Root extends Widget<Store, State> {
|
||||
template = "web.web_client";
|
||||
widgets = { Navbar, HomeMenu, Notification };
|
||||
|
||||
store: Store;
|
||||
|
||||
constructor(env: Env, store: Store) {
|
||||
super(env, store);
|
||||
this.store = store;
|
||||
this.state = store.state;
|
||||
}
|
||||
|
||||
mounted() {
|
||||
this.store.on("state_updated", this, this.setState);
|
||||
this.store.on("rpc_status", this, this.toggleLoadingIndicator);
|
||||
this.store.on("update_action", this, this.applyController);
|
||||
if (this.store.lastController) {
|
||||
this.applyController(this.store.lastController);
|
||||
}
|
||||
|
||||
// adding reactiveness to mobile/non mobile
|
||||
window.addEventListener("resize", <any>(
|
||||
debounce(this.applyMobileInterface.bind(this), 50)
|
||||
));
|
||||
}
|
||||
|
||||
applyMobileInterface() {
|
||||
const isMobile = window.innerWidth <= 768;
|
||||
if (isMobile !== this.env.isMobile) {
|
||||
this.env.isMobile = isMobile;
|
||||
this.render();
|
||||
}
|
||||
}
|
||||
|
||||
toggleLoadingIndicator(status: "loading" | "notloading") {
|
||||
const method = status === "loading" ? "remove" : "add";
|
||||
(<any>this.refs.loading_indicator).classList[method]("d-none");
|
||||
}
|
||||
|
||||
async applyController(controller: Controller) {
|
||||
const widget = await controller.create(this);
|
||||
if (widget) {
|
||||
this.attachChild(widget, <HTMLElement>this.refs.content);
|
||||
widget.el!.classList.add("o_action_controller");
|
||||
this.store.activateController(controller);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
import { Widget } from "../widget";
|
||||
import { ControlPanel } from "../ui/control_panel";
|
||||
|
||||
export class BaseView extends Widget<{ info: any }, {}> {
|
||||
template = "web.base_view";
|
||||
widgets = { ControlPanel };
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { BaseView } from "./base_view";
|
||||
|
||||
export class FormView extends BaseView {
|
||||
viewType = "form";
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { BaseView } from "./base_view";
|
||||
|
||||
export class KanbanView extends BaseView {
|
||||
viewType = "kanban";
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { BaseView } from "./base_view";
|
||||
|
||||
export class ListView extends BaseView {
|
||||
viewType = "list";
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import { Component } from "../../../../../src/component";
|
||||
import { Env } from "./env";
|
||||
|
||||
//------------------------------------------------------------------------------
|
||||
// Widget classes
|
||||
//------------------------------------------------------------------------------
|
||||
|
||||
export class Widget<P, S> extends Component<Env, P, S> {
|
||||
constructor(parent, props) {
|
||||
super(parent, props);
|
||||
(<any>this.__widget__).isMobile = this.env.isMobile;
|
||||
}
|
||||
async updateProps(nextProps: P): Promise<void> {
|
||||
if ((<any>this.__widget__).isMobile !== this.env.isMobile) {
|
||||
(<any>this.__widget__).isMobile = this.env.isMobile;
|
||||
return this._updateProps(nextProps);
|
||||
}
|
||||
return super.updateProps(nextProps);
|
||||
}
|
||||
}
|
||||
|
||||
export class PureWidget<P, S> extends Widget<P, S> {
|
||||
shouldUpdate(nextProps: P): boolean {
|
||||
for (let k in nextProps) {
|
||||
if (nextProps[k] !== this.props[k]) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
async setState(nextState: Partial<S>) {
|
||||
for (let k in nextState) {
|
||||
if (nextState[k] !== this.state[k]) {
|
||||
return super.setState(nextState);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user