reorganize store code

This commit is contained in:
Géry Debongnie
2019-02-27 13:48:36 +01:00
parent 385156ee72
commit 724181a5ba
21 changed files with 376 additions and 352 deletions
+1 -1
View File
@@ -22,7 +22,7 @@ export interface Subscription {
//------------------------------------------------------------------------------
export class EventBus {
private subscriptions: { [eventType: string]: Subscription[] } = {};
subscriptions: { [eventType: string]: Subscription[] } = {};
/**
* Add a listener for the 'eventType' events.
+1 -1
View File
@@ -1,7 +1,7 @@
import { WEnv } from "./core/component";
import { QWeb } from "./core/qweb_vdom";
import { idGenerator } from "./core/utils";
import { INotification, RPC, Services, Store } from "./store";
import { INotification, RPC, Services, Store } from "./store/store";
//------------------------------------------------------------------------------
// Types
+1 -1
View File
@@ -1,5 +1,5 @@
import { findInTree } from "./core/utils";
import { MenuInfo, MenuItem } from "./store";
import { MenuInfo, MenuItem } from "./store/store";
//------------------------------------------------------------------------------
// Templates
+1 -1
View File
@@ -5,7 +5,7 @@ import { loadMenus, loadTemplates } from "./loaders";
import { actionRegistry } from "./registries";
import { rpc } from "./services/ajax";
import { Router } from "./services/router";
import { Store } from "./store";
import { Store } from "./store/store";
import { Root } from "./widgets/root";
//------------------------------------------------------------------------------
+1 -1
View File
@@ -1,5 +1,5 @@
import { Registry } from "./core/registry";
import { ActionWidget } from "./store";
import { ActionWidget } from "./store/store";
import { CRM } from "./widgets/crm";
import { Discuss } from "./widgets/discuss";
-336
View File
@@ -1,336 +0,0 @@
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";
import { idGenerator } 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[];
}
export interface State {
stack: ActionStack;
inHome: boolean;
currentApp: MenuItem | null;
}
export interface Services {
rpc: RPC;
router: IRouter;
}
export type Context = { [key: string]: any };
export interface CommonActionInfo {
id: number;
context: Context;
title: string;
target: "current" | "new";
}
export type ActionRequest = string | number;
export type ActionWidget = Type<Widget<{}, {}>>;
export interface ClientActionInfo extends CommonActionInfo {
type: "client";
name: string;
Widget: ActionWidget;
}
export interface ActWindowInfo extends CommonActionInfo {
type: "act_window";
view: string;
}
export interface ActionDescription {
id: number;
type: "ir.actions.act_window" | "ir.actions.client";
target: "current";
}
export type ActionInfo = ClientActionInfo | ActWindowInfo;
export type ActionStack = ActionInfo[];
//------------------------------------------------------------------------------
// Store
//------------------------------------------------------------------------------
class BaseStore extends EventBus {
state: State = {
stack: [],
inHome: false,
currentApp: null
};
menuInfo: MenuInfo;
services: Services;
actionRegistry: Registry<ActionWidget>;
constructor(
services: Services,
menuInfo: MenuInfo,
actionRegistry: Registry<ActionWidget>
) {
super();
this.services = services;
this.menuInfo = menuInfo;
this.actionRegistry = actionRegistry;
}
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 });
}
}
export class Store extends actionManagerMixin(
rpcMixin(notificationMixin(BaseStore))
) {}
//------------------------------------------------------------------------------
// RPC Mixin
//------------------------------------------------------------------------------
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>;
interface RequestParameters {
route: string;
params: { [key: string]: any };
}
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.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 };
}
};
}
//------------------------------------------------------------------------------
// Notifications Mixin
//------------------------------------------------------------------------------
export interface INotification {
id: number;
title: string;
message: string;
type: "notification" | "warning";
sticky: boolean;
}
function notificationMixin<T extends Type<BaseStore>>(Base: T) {
return class extends Base {
private generateID = idGenerator();
addNotification(notif: Partial<INotification>): number {
const id = this.generateID();
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.closeNotification(id), 2500);
}
return id;
}
closeNotification(id: number) {
this.trigger("notification_closed", id);
}
};
}
//------------------------------------------------------------------------------
// Action Manager Mixin
//------------------------------------------------------------------------------
function actionManagerMixin<T extends ReturnType<typeof rpcMixin>>(Base: T) {
return class extends Base {
constructor(...args) {
super(...args);
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);
}
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 });
}
this.services.router.navigate(query);
return this.doAction(actionId);
} else {
this.update({ 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 };
}
async doAction(request: ActionRequest) {
if (typeof request === "number") {
await 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.update({
inHome: false,
stack: [
{
id: 1,
context: {},
target: "current",
type: "client",
name,
title,
Widget: Widget
}
]
});
}
}
private loadAction(id: number) {
return this.rpc({
route: "web/action/load",
params: {
action_id: id
}
});
}
};
}
@@ -0,0 +1,80 @@
import { Type } from "../core/component";
import { rpcMixin } from "./rpc_mixin";
import { Widget } from "../widgets/widget";
export type Context = { [key: string]: any };
export interface CommonActionInfo {
id: number;
context: Context;
title: string;
target: "current" | "new";
}
export type ActionRequest = string | number;
export type ActionWidget = Type<Widget<{}, {}>>;
export interface ClientActionInfo extends CommonActionInfo {
type: "client";
name: string;
Widget: ActionWidget;
}
export interface ActWindowInfo extends CommonActionInfo {
type: "act_window";
view: string;
}
export interface ActionDescription {
id: number;
type: "ir.actions.act_window" | "ir.actions.client";
target: "current";
}
export type ActionInfo = ClientActionInfo | ActWindowInfo;
export type ActionStack = ActionInfo[];
//------------------------------------------------------------------------------
// Action Manager Mixin
//------------------------------------------------------------------------------
export function actionManagerMixin<T extends ReturnType<typeof rpcMixin>>(
Base: T
) {
return class extends Base {
async doAction(request: ActionRequest) {
if (typeof request === "number") {
await 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.update({
inHome: false,
stack: [
{
id: 1,
context: {},
target: "current",
type: "client",
name,
title,
Widget: Widget
}
]
});
}
}
loadAction(id: number) {
return this.rpc({
route: "web/action/load",
params: {
action_id: id
}
});
}
};
}
@@ -0,0 +1,40 @@
import { Type } from "../core/component";
import { BaseStore } from "./store";
import { idGenerator } from "../core/utils";
//------------------------------------------------------------------------------
// Notifications Mixin
//------------------------------------------------------------------------------
export interface INotification {
id: number;
title: string;
message: string;
type: "notification" | "warning";
sticky: boolean;
}
export function notificationMixin<T extends Type<BaseStore>>(Base: T) {
return class extends Base {
generateID = idGenerator();
addNotification(notif: Partial<INotification>): number {
const id = this.generateID();
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.closeNotification(id), 2500);
}
return id;
}
closeNotification(id: number) {
this.trigger("notification_closed", id);
}
};
}
+74
View File
@@ -0,0 +1,74 @@
import { Type } from "../core/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.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 };
}
};
}
+166
View File
@@ -0,0 +1,166 @@
import { EventBus } from "../core/event_bus";
import { Registry } from "../core/registry";
import { RPC } from "../services/ajax";
import { IRouter, Query } from "../services/router";
import {
actionManagerMixin,
ActionStack,
ActionWidget
} from "./action_manager_mixin";
import { notificationMixin } from "./notification_mixin";
import { rpcMixin } from "./rpc_mixin";
import { MenuItem } from "./store";
//------------------------------------------------------------------------------
// Types
//------------------------------------------------------------------------------
export { ActionStack, ActionWidget } from "./action_manager_mixin";
export { INotification } from "./notification_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 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;
}
//------------------------------------------------------------------------------
// Store
//------------------------------------------------------------------------------
export class BaseStore extends EventBus {
state: State = {
stack: [],
inHome: false,
currentApp: null
};
menuInfo: MenuInfo;
services: Services;
actionRegistry: Registry<ActionWidget>;
constructor(
services: Services,
menuInfo: MenuInfo,
actionRegistry: Registry<ActionWidget>
) {
super();
this.services = services;
this.menuInfo = menuInfo;
this.actionRegistry = actionRegistry;
}
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 });
}
}
export class Store extends actionManagerMixin(
rpcMixin(notificationMixin(BaseStore))
) {
constructor(
services: Services,
menuInfo: MenuInfo,
actionRegistry: Registry<ActionWidget>
) {
super(services, menuInfo, actionRegistry);
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);
}
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 });
}
this.services.router.navigate(query);
return this.doAction(actionId);
} 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 ("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 };
}
}
+1 -1
View File
@@ -1,4 +1,4 @@
import { MenuItem } from "../store";
import { MenuItem } from "../store/store";
import { PureWidget } from "./widget";
//------------------------------------------------------------------------------
@@ -1,4 +1,4 @@
import { ActionStack } from "../store";
import { ActionStack } from "../store/store";
import { Widget } from "./widget";
//------------------------------------------------------------------------------
+1 -1
View File
@@ -1,4 +1,4 @@
import { MenuInfo, MenuItem } from "../store";
import { MenuInfo, MenuItem } from "../store/store";
import { Widget } from "./widget";
//------------------------------------------------------------------------------
+1 -1
View File
@@ -1,4 +1,4 @@
import { INotification } from "../store";
import { INotification } from "../store/store";
import { Widget } from "./widget";
export class Notification extends Widget<INotification, {}> {
+1 -1
View File
@@ -1,6 +1,6 @@
import { debounce } from "../core/utils";
import { Env } from "../env";
import { State, Store } from "../store";
import { State, Store } from "../store/store";
import { ActionContainer } from "./action_container";
import { HomeMenu } from "./home_menu";
import { Navbar } from "./navbar";
+1 -1
View File
@@ -6,7 +6,7 @@ import { idGenerator } from "../src/ts/core/utils";
import { getMenuInfo } from "../src/ts/loaders";
import { actionRegistry } from "../src/ts/registries";
import { IRouter, Query, RouterEvent } from "../src/ts/services/router";
import { MenuInfo, Services, Store } from "../src/ts/store";
import { MenuInfo, Services, Store } from "../src/ts/store/store";
export function makeTestFixture() {
let fixture = document.createElement("div");
@@ -1,5 +1,5 @@
import { Env, makeEnv } from "../../src/ts/env";
import { ActionStack, Store } from "../../src/ts/store";
import { ActionStack, Store } from "../../src/ts/store/store";
import { ActionContainer, Props } from "../../src/ts/widgets/action_container";
import { Widget } from "../../src/ts/widgets/widget";
import * as helpers from "../helpers";
+1 -1
View File
@@ -1,5 +1,5 @@
import { Env, makeEnv } from "../../src/ts/env";
import { Store } from "../../src/ts/store";
import { Store } from "../../src/ts/store/store";
import { HomeMenu, Props } from "../../src/ts/widgets/home_menu";
import * as helpers from "../helpers";
+1 -1
View File
@@ -1,5 +1,5 @@
import { Env, makeEnv } from "../../src/ts/env";
import { MenuInfo, Store } from "../../src/ts/store";
import { MenuInfo, Store } from "../../src/ts/store/store";
import { Navbar, Props } from "../../src/ts/widgets/navbar";
import * as helpers from "../helpers";
@@ -1,5 +1,5 @@
import { Env, makeEnv } from "../../src/ts/env";
import { INotification, Store } from "../../src/ts/store";
import { INotification, Store } from "../../src/ts/store/store";
import { Notification } from "../../src/ts/widgets/notification";
import * as helpers from "../helpers";
+1 -1
View File
@@ -1,5 +1,5 @@
import { Env, makeEnv } from "../../src/ts/env";
import { Store } from "../../src/ts/store";
import { Store } from "../../src/ts/store/store";
import { Root } from "../../src/ts/widgets/root";
import * as helpers from "../helpers";