mirror of
https://github.com/odoo/owl.git
synced 2025-10-06 19:59:41 +07:00
store refactoring
This commit is contained in:
@@ -10,10 +10,13 @@ import { INotification, RPC, Services, Store } from "./store";
|
|||||||
export interface Env extends WEnv {
|
export interface Env extends WEnv {
|
||||||
services: Services;
|
services: Services;
|
||||||
|
|
||||||
// helpers
|
// commands
|
||||||
dispatch(action: string, param?: any): void;
|
activateMenuItem(menuId: number): void;
|
||||||
addNotification(notif: Partial<INotification>): number;
|
addNotification(notif: Partial<INotification>): number;
|
||||||
closeNotification(id: number);
|
closeNotification(id: number): void;
|
||||||
|
toggleHomeMenu(): void;
|
||||||
|
|
||||||
|
// helpers
|
||||||
rpc: RPC;
|
rpc: RPC;
|
||||||
|
|
||||||
// configuration
|
// configuration
|
||||||
@@ -33,11 +36,16 @@ export function makeEnv(store: Store, templates: string): Env {
|
|||||||
const env: Env = {
|
const env: Env = {
|
||||||
qweb,
|
qweb,
|
||||||
getID: idGenerator(),
|
getID: idGenerator(),
|
||||||
|
|
||||||
services: store.services,
|
services: store.services,
|
||||||
dispatch: store.dispatch.bind(store),
|
|
||||||
|
activateMenuItem: store.activateMenuItem.bind(store),
|
||||||
addNotification: store.addNotification.bind(store),
|
addNotification: store.addNotification.bind(store),
|
||||||
closeNotification: store.closeNotification.bind(store),
|
closeNotification: store.closeNotification.bind(store),
|
||||||
|
toggleHomeMenu: store.toggleHomeMenu.bind(store),
|
||||||
|
|
||||||
rpc: store.rpc.bind(store),
|
rpc: store.rpc.bind(store),
|
||||||
|
|
||||||
debug: false,
|
debug: false,
|
||||||
isMobile: window.innerWidth <= 768
|
isMobile: window.innerWidth <= 768
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -30,3 +30,16 @@ document.addEventListener("DOMContentLoaded", async function() {
|
|||||||
// For debugging purpose, we keep a reference to the root widget in odoo
|
// For debugging purpose, we keep a reference to the root widget in odoo
|
||||||
(<any>window).odoo.rootWidget = rootWidget;
|
(<any>window).odoo.rootWidget = rootWidget;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let testMixin = Base =>
|
||||||
|
class extends Base {
|
||||||
|
calc() {
|
||||||
|
return 32;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
class Foo {}
|
||||||
|
|
||||||
|
class Bar extends testMixin(Foo) {}
|
||||||
|
|
||||||
|
console.log(new Bar().calc());
|
||||||
|
|||||||
+218
-189
@@ -4,6 +4,7 @@ import { Registry } from "./core/registry";
|
|||||||
import { RPC } from "./services/ajax";
|
import { RPC } from "./services/ajax";
|
||||||
import { IRouter, Query } from "./services/router";
|
import { IRouter, Query } from "./services/router";
|
||||||
import { Widget } from "./widgets/widget";
|
import { Widget } from "./widgets/widget";
|
||||||
|
import { idGenerator } from "./core/utils";
|
||||||
|
|
||||||
//------------------------------------------------------------------------------
|
//------------------------------------------------------------------------------
|
||||||
// Types
|
// Types
|
||||||
@@ -73,6 +74,51 @@ export interface ActionDescription {
|
|||||||
export type ActionInfo = ClientActionInfo | ActWindowInfo;
|
export type ActionInfo = ClientActionInfo | ActWindowInfo;
|
||||||
export type ActionStack = ActionInfo[];
|
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 {
|
export interface RPCModelQuery {
|
||||||
model: string;
|
model: string;
|
||||||
method: string;
|
method: string;
|
||||||
@@ -90,6 +136,56 @@ export type RPCQuery = RPCModelQuery | RPCControllerQuery;
|
|||||||
|
|
||||||
export type RPC = (rpc: RPCQuery) => Promise<any>;
|
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 {
|
export interface INotification {
|
||||||
id: number;
|
id: number;
|
||||||
title: string;
|
title: string;
|
||||||
@@ -98,207 +194,140 @@ export interface INotification {
|
|||||||
sticky: boolean;
|
sticky: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface RequestParameters {
|
function notificationMixin<T extends Type<BaseStore>>(Base: T) {
|
||||||
route: string;
|
return class extends Base {
|
||||||
params: { [key: string]: any };
|
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);
|
||||||
|
}
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
//------------------------------------------------------------------------------
|
//------------------------------------------------------------------------------
|
||||||
// Store
|
// Action Manager Mixin
|
||||||
//------------------------------------------------------------------------------
|
//------------------------------------------------------------------------------
|
||||||
|
|
||||||
export class Store extends EventBus {
|
function actionManagerMixin<T extends ReturnType<typeof rpcMixin>>(Base: T) {
|
||||||
state: State;
|
return class extends Base {
|
||||||
menuInfo: MenuInfo;
|
constructor(...args) {
|
||||||
services: Services;
|
super(...args);
|
||||||
actionRegistry: Registry<ActionWidget>;
|
const query = this.services.router.getQuery();
|
||||||
|
let { app, actionId } = this.getAppAndAction(query);
|
||||||
constructor(
|
this.state.currentApp = app;
|
||||||
services: Services,
|
if (!actionId) {
|
||||||
menuInfo: MenuInfo,
|
this.state.inHome = true;
|
||||||
actionRegistry: Registry<ActionWidget>
|
|
||||||
) {
|
|
||||||
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.addNotification(params);
|
|
||||||
break;
|
|
||||||
case "close_notification":
|
|
||||||
this.closeNotification(params);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
updateState(nextState: Partial<State>) {
|
|
||||||
Object.assign(this.state, nextState);
|
|
||||||
this.trigger("state_updated", this.state);
|
|
||||||
}
|
|
||||||
|
|
||||||
nextID = 1;
|
|
||||||
|
|
||||||
addNotification(notif: Partial<INotification>): 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.closeNotification(id), 2500);
|
|
||||||
}
|
|
||||||
return id;
|
|
||||||
}
|
|
||||||
closeNotification(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(
|
this.services.router.on("query_changed", this, this.updateAction);
|
||||||
query: Query
|
this.updateAction(this.services.router.getQuery());
|
||||||
): { app: MenuItem | null; actionId: number | null } {
|
}
|
||||||
const menuInfo = this.menuInfo;
|
|
||||||
let app: MenuItem | null = null;
|
private updateAction(query: Query) {
|
||||||
let actionId: number | null = null;
|
let { app, actionId } = this.getAppAndAction(query);
|
||||||
if ("action_id" in query) {
|
this.updateAppState(app, actionId);
|
||||||
actionId = parseInt(query.action_id, 10);
|
}
|
||||||
if (menuInfo.actionMap[actionId]) {
|
|
||||||
const menu = menuInfo.actionMap[actionId]!;
|
activateMenuItem(menuId: number) {
|
||||||
app = menu.app;
|
const menu = this.menuInfo.menus[menuId];
|
||||||
|
if (!menu) {
|
||||||
|
throw new Error("Invalid menu id");
|
||||||
|
}
|
||||||
|
this.updateAppState(menu.app, menu.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.update({ currentApp: app });
|
||||||
|
}
|
||||||
|
this.services.router.navigate(query);
|
||||||
|
this.doAction(actionId);
|
||||||
|
} else {
|
||||||
|
this.update({ inHome: true, currentApp: newApp });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if ("menu_id" in query) {
|
|
||||||
const menuId = parseInt(query.menu_id, 10);
|
private getAppAndAction(
|
||||||
const menu = menuInfo.menus[menuId];
|
query: Query
|
||||||
if (menu) {
|
): { app: MenuItem | null; actionId: number | null } {
|
||||||
app = menu.app;
|
const menuInfo = this.menuInfo;
|
||||||
if (!actionId) {
|
let app: MenuItem | null = null;
|
||||||
actionId = menu.actionId;
|
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) {
|
||||||
return { app, actionId };
|
const menuId = parseInt(query.menu_id, 10);
|
||||||
}
|
const menu = menuInfo.menus[menuId];
|
||||||
|
if (menu) {
|
||||||
doAction(request: ActionRequest) {
|
app = menu.app;
|
||||||
if (typeof request === "number") {
|
if (!actionId) {
|
||||||
this.loadAction(request);
|
actionId = menu.actionId;
|
||||||
// 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
|
|
||||||
}
|
}
|
||||||
]
|
}
|
||||||
|
}
|
||||||
|
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.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
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
private loadAction(id: number) {
|
|
||||||
this.rpc({
|
|
||||||
route: "web/action/load",
|
|
||||||
params: {
|
|
||||||
action_id: id
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
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 };
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,10 +25,10 @@ export class Navbar extends PureWidget<Props, {}> {
|
|||||||
|
|
||||||
toggleHome(ev: MouseEvent) {
|
toggleHome(ev: MouseEvent) {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
this.env.dispatch("toggle_home_menu");
|
this.env.toggleHomeMenu();
|
||||||
}
|
}
|
||||||
|
|
||||||
openMenu(menu: MenuItem) {
|
openMenu(menu: MenuItem) {
|
||||||
this.env.dispatch("open_menu", menu);
|
this.env.activateMenuItem(menu.id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,6 +23,6 @@ export class HomeMenu extends Widget<Props, {}> {
|
|||||||
|
|
||||||
openMenu(app: MenuItem, event: MouseEvent) {
|
openMenu(app: MenuItem, event: MouseEvent) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
this.env.dispatch("open_menu", app);
|
this.env.activateMenuItem(app.id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,93 +0,0 @@
|
|||||||
import { makeTestStore, nextMicroTick } from "./helpers";
|
|
||||||
|
|
||||||
//------------------------------------------------------------------------------
|
|
||||||
// Setup and helpers
|
|
||||||
//------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
function mockFetch(route: string, params: any): Promise<any> {
|
|
||||||
return Promise.resolve(`${route}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
//------------------------------------------------------------------------------
|
|
||||||
// Tests
|
|
||||||
//------------------------------------------------------------------------------
|
|
||||||
|
|
||||||
describe("rpc", () => {
|
|
||||||
test("properly translate query in route", async () => {
|
|
||||||
const store = makeTestStore({ rpc: mockFetch });
|
|
||||||
const result = await store.rpc({ model: "test", method: "hey" });
|
|
||||||
expect(result).toBe("/web/dataset/call_kw/test/hey");
|
|
||||||
});
|
|
||||||
|
|
||||||
test("trigger proper events", async () => {
|
|
||||||
const store = makeTestStore({ rpc: mockFetch });
|
|
||||||
const events: string[] = [];
|
|
||||||
store.on("rpc_status", null, s => {
|
|
||||||
events.push(s);
|
|
||||||
});
|
|
||||||
expect(events).toEqual([]);
|
|
||||||
store.rpc({ model: "test", method: "hey" });
|
|
||||||
expect(events).toEqual(["loading"]);
|
|
||||||
await nextMicroTick();
|
|
||||||
expect(events).toEqual(["loading", "notloading"]);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("notifications", () => {
|
|
||||||
test("can subscribe and add notification", () => {
|
|
||||||
let n = 0;
|
|
||||||
const store = makeTestStore();
|
|
||||||
store.on("notification_added", null, () => n++);
|
|
||||||
store.on("notification_closed", null, () => n--);
|
|
||||||
expect(n).toBe(0);
|
|
||||||
const id = store.addNotification({
|
|
||||||
title: "test",
|
|
||||||
message: "message"
|
|
||||||
});
|
|
||||||
expect(n).toBe(1);
|
|
||||||
expect(id).toBeDefined();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test("can close a notification", () => {
|
|
||||||
let n = 0;
|
|
||||||
const store = makeTestStore();
|
|
||||||
store.on("notification_added", null, () => n++);
|
|
||||||
store.on("notification_closed", null, () => n--);
|
|
||||||
|
|
||||||
const id = store.addNotification({ title: "test", message: "message" });
|
|
||||||
expect(n).toBe(1);
|
|
||||||
|
|
||||||
store.closeNotification(id);
|
|
||||||
expect(n).toBe(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("notifications closes themselves after a while", () => {
|
|
||||||
jest.useFakeTimers();
|
|
||||||
let n = 0;
|
|
||||||
const store = makeTestStore();
|
|
||||||
store.on("notification_added", null, () => n++);
|
|
||||||
store.on("notification_closed", null, () => n--);
|
|
||||||
|
|
||||||
store.addNotification({ title: "test", message: "message" });
|
|
||||||
|
|
||||||
expect(setTimeout).toHaveBeenCalledTimes(1);
|
|
||||||
expect(n).toBe(1);
|
|
||||||
jest.runAllTimers();
|
|
||||||
expect(n).toBe(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("sticky notifications do not close themselves after a while", () => {
|
|
||||||
jest.useFakeTimers();
|
|
||||||
let n = 0;
|
|
||||||
const store = makeTestStore();
|
|
||||||
store.on("notification_added", null, () => n++);
|
|
||||||
store.on("notification_closed", null, () => n--);
|
|
||||||
|
|
||||||
store.addNotification({ title: "test", message: "message", sticky: true });
|
|
||||||
|
|
||||||
expect(setTimeout).toHaveBeenCalledTimes(0);
|
|
||||||
expect(n).toBe(1);
|
|
||||||
jest.runAllTimers();
|
|
||||||
expect(n).toBe(1);
|
|
||||||
});
|
|
||||||
@@ -0,0 +1,119 @@
|
|||||||
|
import { makeTestStore, nextMicroTick } from "../helpers";
|
||||||
|
|
||||||
|
//------------------------------------------------------------------------------
|
||||||
|
// Setup and helpers
|
||||||
|
//------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
function mockFetch(route: string, params: any): Promise<any> {
|
||||||
|
return Promise.resolve(`${route}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
//------------------------------------------------------------------------------
|
||||||
|
// Tests
|
||||||
|
//------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
describe("rpc", () => {
|
||||||
|
test("properly translate query in route", async () => {
|
||||||
|
const store = makeTestStore({ rpc: mockFetch });
|
||||||
|
const result = await store.rpc({ model: "test", method: "hey" });
|
||||||
|
expect(result).toBe("/web/dataset/call_kw/test/hey");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("trigger proper events", async () => {
|
||||||
|
const store = makeTestStore({ rpc: mockFetch });
|
||||||
|
const events: string[] = [];
|
||||||
|
store.on("rpc_status", null, s => {
|
||||||
|
events.push(s);
|
||||||
|
});
|
||||||
|
expect(events).toEqual([]);
|
||||||
|
store.rpc({ model: "test", method: "hey" });
|
||||||
|
expect(events).toEqual(["loading"]);
|
||||||
|
await nextMicroTick();
|
||||||
|
expect(events).toEqual(["loading", "notloading"]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("notifications", () => {
|
||||||
|
test("can subscribe and add notification", () => {
|
||||||
|
let n = 0;
|
||||||
|
const store = makeTestStore();
|
||||||
|
store.on("notification_added", null, () => n++);
|
||||||
|
store.on("notification_closed", null, () => n--);
|
||||||
|
expect(n).toBe(0);
|
||||||
|
const id = store.addNotification({
|
||||||
|
title: "test",
|
||||||
|
message: "message"
|
||||||
|
});
|
||||||
|
expect(n).toBe(1);
|
||||||
|
expect(id).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("can close a notification", () => {
|
||||||
|
let n = 0;
|
||||||
|
const store = makeTestStore();
|
||||||
|
store.on("notification_added", null, () => n++);
|
||||||
|
store.on("notification_closed", null, () => n--);
|
||||||
|
|
||||||
|
const id = store.addNotification({
|
||||||
|
title: "test",
|
||||||
|
message: "message"
|
||||||
|
});
|
||||||
|
expect(n).toBe(1);
|
||||||
|
|
||||||
|
store.closeNotification(id);
|
||||||
|
expect(n).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("notifications closes themselves after a while", () => {
|
||||||
|
jest.useFakeTimers();
|
||||||
|
let n = 0;
|
||||||
|
const store = makeTestStore();
|
||||||
|
store.on("notification_added", null, () => n++);
|
||||||
|
store.on("notification_closed", null, () => n--);
|
||||||
|
|
||||||
|
store.addNotification({ title: "test", message: "message" });
|
||||||
|
|
||||||
|
expect(setTimeout).toHaveBeenCalledTimes(1);
|
||||||
|
expect(n).toBe(1);
|
||||||
|
jest.runAllTimers();
|
||||||
|
expect(n).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("sticky notifications do not close themselves after a while", () => {
|
||||||
|
jest.useFakeTimers();
|
||||||
|
let n = 0;
|
||||||
|
const store = makeTestStore();
|
||||||
|
store.on("notification_added", null, () => n++);
|
||||||
|
store.on("notification_closed", null, () => n--);
|
||||||
|
|
||||||
|
store.addNotification({
|
||||||
|
title: "test",
|
||||||
|
message: "message",
|
||||||
|
sticky: true
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(setTimeout).toHaveBeenCalledTimes(0);
|
||||||
|
expect(n).toBe(1);
|
||||||
|
jest.runAllTimers();
|
||||||
|
expect(n).toBe(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("state transitions", () => {
|
||||||
|
test("toggle menu", async () => {
|
||||||
|
const store = makeTestStore();
|
||||||
|
expect(store.state.inHome).toBe(true);
|
||||||
|
store.toggleHomeMenu();
|
||||||
|
|
||||||
|
// should still be in home menu since no app is currently active
|
||||||
|
expect(store.state.inHome).toBe(true);
|
||||||
|
|
||||||
|
store.activateMenuItem(96);
|
||||||
|
expect(store.state.inHome).toBe(false);
|
||||||
|
|
||||||
|
store.toggleHomeMenu();
|
||||||
|
expect(store.state.inHome).toBe(true);
|
||||||
|
store.toggleHomeMenu();
|
||||||
|
expect(store.state.inHome).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -62,3 +62,13 @@ test("mobile mode: navbar is different", async () => {
|
|||||||
await navbar.mount(fixture);
|
await navbar.mount(fixture);
|
||||||
expect(fixture.innerHTML).toMatchSnapshot();
|
expect(fixture.innerHTML).toMatchSnapshot();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("clicking on left icon toggle home menu ", async () => {
|
||||||
|
props.app = menuInfo.menus[96]!;
|
||||||
|
store.state.inHome = false;
|
||||||
|
const navbar = new Navbar(env, props);
|
||||||
|
await navbar.mount(fixture);
|
||||||
|
expect(store.state.inHome).toBe(false);
|
||||||
|
(<any>fixture).getElementsByClassName("o_title")[0].click();
|
||||||
|
expect(store.state.inHome).toBe(true);
|
||||||
|
});
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ test("if url has action_id, will render action and navigate to proper menu_id",
|
|||||||
const router = new helpers.MockRouter({ action_id: "595" });
|
const router = new helpers.MockRouter({ action_id: "595" });
|
||||||
store = helpers.makeTestStore({ router });
|
store = helpers.makeTestStore({ router });
|
||||||
env = makeEnv(store, templates);
|
env = makeEnv(store, templates);
|
||||||
|
await helpers.nextTick();
|
||||||
|
|
||||||
const root = new Root(env, store);
|
const root = new Root(env, store);
|
||||||
await root.mount(fixture);
|
await root.mount(fixture);
|
||||||
|
|||||||
Reference in New Issue
Block a user