add notification widget/full system

This commit is contained in:
Géry Debongnie
2019-01-29 15:19:02 +01:00
parent ec90663dd7
commit d029f87a0f
11 changed files with 149 additions and 15 deletions
+2
View File
@@ -185,3 +185,5 @@ We have 3 main folders and 3 main files:
- **Rendering** think about batching all patching updates in a - **Rendering** think about batching all patching updates in a
nextanimationframe nextanimationframe
- when t-widget/t-on directives are compiled, the widget/eval context is actually available. Should we use that info to determine if bound methods exists on the widget?
+37
View File
@@ -1,5 +1,7 @@
$navbar-height: 40px; $navbar-height: 40px;
$main-color: #875a7b; $main-color: #875a7b;
$o-notification-info-bg-color: #fcfbea;
$o-main-text-color: #666666;
html { html {
height: 100%; height: 100%;
@@ -53,6 +55,41 @@ body {
} }
} }
/* Notifications */
.o_notification_container {
position: absolute;
width: 300px;
right: 5px;
top: $navbar-height;
bottom: 0;
.o_notification {
padding: 0;
margin: 5px 0 0 0;
background-color: $o-notification-info-bg-color;
box-shadow: 0px 0px 5px 1px $o-main-text-color;
.o_notification_title {
display: flex;
align-items: center;
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
padding: 10px 10px 10px 20px;
font-weight: bold;
.o_icon {
display: inline-block;
margin-right: 20px;
color: rgba(0, 0, 0, 0.3);
}
}
.o_notification_content {
padding: 10px;
}
}
}
/* Discuss */ /* Discuss */
.o_discuss { .o_discuss {
padding: 20px; padding: 20px;
+11 -4
View File
@@ -29,6 +29,7 @@ export class Context {
caller: Element | undefined; caller: Element | undefined;
shouldDefineOwner: boolean = false; shouldDefineOwner: boolean = false;
shouldProtectContext: boolean = false; shouldProtectContext: boolean = false;
inLoop: boolean = false;
constructor() { constructor() {
this.rootContext = this; this.rootContext = this;
@@ -134,7 +135,7 @@ export class QWeb {
if (!doc.firstChild) { if (!doc.firstChild) {
throw new Error("Invalid template (should not be empty)"); throw new Error("Invalid template (should not be empty)");
} }
if (doc.firstChild.nodeName === "parsererror") { if (doc.getElementsByTagName("parsererror").length) {
throw new Error("Invalid XML in template"); throw new Error("Invalid XML in template");
} }
let tbranch = doc.querySelectorAll("[t-elif], [t-else]"); let tbranch = doc.querySelectorAll("[t-elif], [t-else]");
@@ -331,13 +332,17 @@ export class QWeb {
const attrs: string[] = []; const attrs: string[] = [];
const tattrs: number[] = []; const tattrs: number[] = [];
for (let i = 0; i < attributes.length; i++) { for (let i = 0; i < attributes.length; i++) {
const name = attributes[i].name; let name = attributes[i].name;
const value = attributes[i].textContent!; const value = attributes[i].textContent!;
// regular attributes // regular attributes
if (!name.startsWith("t-")) { if (!name.startsWith("t-")) {
const attID = ctx.generateID(); const attID = ctx.generateID();
ctx.addLine(`let _${attID} = '${value}';`); ctx.addLine(`let _${attID} = '${value}';`);
if (!name.match(/^[a-zA-Z]+$/)) {
// attribute contains 'non letters' => we want to quote it
name = '"' + name + '"';
}
attrs.push(`${name}: _${attID}`); attrs.push(`${name}: _${attID}`);
} }
@@ -624,6 +629,7 @@ const forEachDirective: Directive = {
priority: 10, priority: 10,
atNodeEncounter({ node, qweb, ctx }): boolean { atNodeEncounter({ node, qweb, ctx }): boolean {
ctx.rootContext.shouldProtectContext = true; ctx.rootContext.shouldProtectContext = true;
ctx.inLoop = true;
const elems = node.getAttribute("t-foreach")!; const elems = node.getAttribute("t-foreach")!;
const name = node.getAttribute("t-as")!; const name = node.getAttribute("t-as")!;
let arrayID = ctx.generateID(); let arrayID = ctx.generateID();
@@ -715,8 +721,9 @@ const widgetDirective: Directive = {
ctx.addLine(`let _${dummyID}_index = c${ctx.parentNode}.length;`); ctx.addLine(`let _${dummyID}_index = c${ctx.parentNode}.length;`);
ctx.addLine(`c${ctx.parentNode}.push(_${dummyID});`); ctx.addLine(`c${ctx.parentNode}.push(_${dummyID});`);
ctx.addLine(`let def${defID};`); ctx.addLine(`let def${defID};`);
let templateID = ctx.inLoop ? `(${widgetID} + i)` : String(widgetID);
ctx.addLine( ctx.addLine(
`let w${widgetID} = ${widgetID} in context.__widget__.cmap ? context.__widget__.children[context.__widget__.cmap[${widgetID}]] : false;` `let w${widgetID} = ${templateID} in context.__widget__.cmap ? context.__widget__.children[context.__widget__.cmap[${templateID}]] : false;`
); );
ctx.addLine(`if (w${widgetID}) {`); ctx.addLine(`if (w${widgetID}) {`);
@@ -734,7 +741,7 @@ const widgetDirective: Directive = {
`let _${widgetID} = new context.widgets['${value}'](owner, ${props});` `let _${widgetID} = new context.widgets['${value}'](owner, ${props});`
); );
ctx.addLine( ctx.addLine(
`context.__widget__.cmap[${widgetID}] = _${widgetID}.__widget__.id;` `context.__widget__.cmap[${templateID}] = _${widgetID}.__widget__.id;`
); );
ctx.addLine( ctx.addLine(
`def${defID} = _${widgetID}._start().then(() => _${widgetID}._render()).then(vnode=>{c${ `def${defID} = _${widgetID}._start().then(() => _${widgetID}._render()).then(vnode=>{c${
+8
View File
@@ -4,6 +4,10 @@ import { WEnv } from "./core/widget";
import { registry } from "./registry"; import { registry } from "./registry";
import { ActionManager, IActionManager } from "./services/action_manager"; import { ActionManager, IActionManager } from "./services/action_manager";
import { Ajax, IAjax } from "./services/ajax"; import { Ajax, IAjax } from "./services/ajax";
import {
INotificationManager,
NotificationManager
} from "./services/notifications";
import { IRouter, Router } from "./services/router"; import { IRouter, Router } from "./services/router";
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
@@ -21,6 +25,7 @@ export interface Env extends WEnv {
ajax: IAjax; ajax: IAjax;
router: IRouter; router: IRouter;
menus: Menu[]; menus: Menu[];
notifications: INotificationManager;
// helpers // helpers
rpc: IAjax["rpc"]; rpc: IAjax["rpc"];
@@ -48,6 +53,8 @@ export const makeEnvironment = memoize(function(): Env {
const router = new Router(); const router = new Router();
const ajax = new Ajax(); const ajax = new Ajax();
const actionManager = new ActionManager(router, registry); const actionManager = new ActionManager(router, registry);
const notifications = new NotificationManager();
const menus = [ const menus = [
{ title: "Discuss", actionID: 1 }, { title: "Discuss", actionID: 1 },
{ title: "CRM", actionID: 2 } { title: "CRM", actionID: 2 }
@@ -62,6 +69,7 @@ export const makeEnvironment = memoize(function(): Env {
router, router,
actionManager, actionManager,
menus, menus,
notifications,
rpc: ajax.rpc, rpc: ajax.rpc,
debug: false debug: false
+16 -2
View File
@@ -1,12 +1,18 @@
import { Widget } from "./core/widget"; import { Widget } from "./core/widget";
import { Navbar } from "./widgets/navbar"; import { Navbar } from "./widgets/navbar";
import { ActionWidget } from "./services/action_manager"; import { ActionWidget } from "./services/action_manager";
import { INotification } from "./services/notifications";
import { Notification } from "./widgets/notification";
import { Env } from "./env"; import { Env } from "./env";
const template = ` const template = `
<div class="o_web_client"> <div class="o_web_client">
<t t-widget="Navbar"/> <t t-widget="Navbar"/>
<div class="o_content" t-ref="content"> <div class="o_content" t-ref="content"></div>
<div class="o_notification_container">
<t t-foreach="state.notifications" t-as="notif">
<t t-widget="Notification" t-props="notif"/>
</t>
</div> </div>
</div> </div>
`; `;
@@ -14,11 +20,14 @@ const template = `
export class Root extends Widget<Env, {}> { export class Root extends Widget<Env, {}> {
name = "root"; name = "root";
template = template; template = template;
widgets = { Navbar }; widgets = { Navbar, Notification };
content: Widget<Env, {}> | null = null; content: Widget<Env, {}> | null = null;
state: { notifications: INotification[] } = { notifications: [] };
mounted() { mounted() {
this.env.actionManager.on("action_ready", this, this.setContentWidget); this.env.actionManager.on("action_ready", this, this.setContentWidget);
this.env.notifications.on("notification_added", this, this.addNotification);
const actionWidget = this.env.actionManager.getCurrentAction(); const actionWidget = this.env.actionManager.getCurrentAction();
if (actionWidget) { if (actionWidget) {
this.setContentWidget(actionWidget); this.setContentWidget(actionWidget);
@@ -34,4 +43,9 @@ export class Root extends Widget<Env, {}> {
} }
this.content = newWidget; this.content = newWidget;
} }
addNotification(notif: INotification) {
const notifications = this.state.notifications.concat(notif);
this.updateState({ notifications });
}
} }
+6 -6
View File
@@ -4,7 +4,7 @@ import { EventBus as Bus } from "../core/event_bus";
// Types // Types
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
export interface Notification { export interface INotification {
id: number; id: number;
title: string; title: string;
message: string; message: string;
@@ -14,10 +14,10 @@ export interface Notification {
export type NotificationEvent = "notification_added" | "notification_closed"; export type NotificationEvent = "notification_added" | "notification_closed";
export type Callback = (notif: Notification) => void; export type Callback = (notif: INotification) => void;
export interface INotificationManager { export interface INotificationManager {
add(notif: Partial<Notification>): number; add(notif: Partial<INotification>): number;
close(id: number): void; close(id: number): void;
on(event: NotificationEvent, owner: any, callback: Callback): void; on(event: NotificationEvent, owner: any, callback: Callback): void;
} }
@@ -26,10 +26,10 @@ export interface INotificationManager {
// Notification Manager // Notification Manager
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
export class NotificationManager extends Bus implements INotificationManager { export class NotificationManager extends Bus implements INotificationManager {
nextID = 0; nextID = 1;
notifications: { [key: number]: Notification } = {}; notifications: { [key: number]: INotification } = {};
add(notif: Partial<Notification>): number { add(notif: Partial<INotification>): number {
const id = this.nextID++; const id = this.nextID++;
const defaultVals = { const defaultVals = {
title: "", title: "",
+8 -1
View File
@@ -11,7 +11,7 @@ const template = `
<button t-on-click="toggle">Toggle Clock/counters</button> <button t-on-click="toggle">Toggle Clock/counters</button>
<button t-on-click="toggleColor">Toggle Color</button> <button t-on-click="toggleColor">Toggle Color</button>
<button t-on-click="updateState({})">Rerender this widget</button> <button t-on-click="updateState({})">Rerender this widget</button>
<input/> <input t-ref="textinput"/>
<t t-if="state.validcounter"> <t t-if="state.validcounter">
<t t-widget="Counter" t-ref="counter" t-props="{initialState:4}"/> <t t-widget="Counter" t-ref="counter" t-props="{initialState:4}"/>
<t t-widget="Counter" t-ref="counter2" t-props="{initialState:400}"/> <t t-widget="Counter" t-ref="counter2" t-props="{initialState:400}"/>
@@ -20,6 +20,7 @@ const template = `
<t t-widget="Clock"/> <t t-widget="Clock"/>
</t> </t>
<t t-widget="ColorWidget" t-props="{color: state.color}"/> <t t-widget="ColorWidget" t-props="{color: state.color}"/>
<button t-on-click="addNotif">Add Notification</button>
</div> </div>
`; `;
@@ -52,6 +53,12 @@ export class Discuss extends Widget<Env, {}> {
const newColor = this.state.color === "red" ? "blue" : "red"; const newColor = this.state.color === "red" ? "blue" : "red";
this.updateState({ color: newColor }); this.updateState({ color: newColor });
} }
addNotif() {
const text = (<any>this.refs.textinput).value;
const message = `It is now ${new Date().toLocaleTimeString()}.<br/> Msg: ${text}`;
this.env.notifications.add({ title: "hey", message: message });
}
} }
class ColorWidget extends Widget<Env, { color: "red" | "blue" }> { class ColorWidget extends Widget<Env, { color: "red" | "blue" }> {
+20
View File
@@ -0,0 +1,20 @@
import { Widget } from "../core/widget";
import { Env } from "../env";
import { INotification } from "../services/notifications";
const template = `
<div class="o_notification">
<a t-if="props.sticky" class="fa fa-times o_close" href="#" title="Close" aria-label="Close"/>
<div class="o_notification_title">
<t t-raw="props.title"/>
</div>
<div class="o_notification_content" t-if="props.message.length">
<t t-raw="props.message"/>
</div>
</div>`;
export class Notification extends Widget<Env, INotification> {
name = "notification";
template = template;
}
+7
View File
@@ -280,6 +280,13 @@ describe("attributes", () => {
expect(result).toBe(expected); expect(result).toBe(expected);
}); });
test("static attributes with dashes", () => {
qweb.addTemplate("test", `<div aria-label="Close"/>`);
const result = renderToString(qweb, "test");
const expected = `<div aria-label="Close"></div>`;
expect(result).toBe(expected);
});
test("static attributes on void elements", () => { test("static attributes on void elements", () => {
qweb.addTemplate("test", `<img src="/test.jpg" alt="Test"/>`); qweb.addTemplate("test", `<img src="/test.jpg" alt="Test"/>`);
const result = renderToString(qweb, "test"); const result = renderToString(qweb, "test");
+32
View File
@@ -1,5 +1,6 @@
import { WEnv, Widget } from "../../src/ts/core/widget"; import { WEnv, Widget } from "../../src/ts/core/widget";
import { makeTestWEnv, makeTestFixture } from "../helpers"; import { makeTestWEnv, makeTestFixture } from "../helpers";
import { normalize } from "../helpers";
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
// Setup and helpers // Setup and helpers
@@ -573,6 +574,37 @@ describe("composition", () => {
"<div><div>0<button>Inc</button></div></div>" "<div><div>0<button>Inc</button></div></div>"
); );
}); });
test("sub widgets rendered in a loop", async () => {
class ChildWidget extends Widget<WEnv, { n: number }> {
name = "c";
template = `<span><t t-esc="props.n"/></span>`;
}
class Parent extends Widget<WEnv, {}> {
name = "p";
template = `
<div>
<t t-foreach="state.numbers" t-as="number">
<t t-widget="ChildWidget" t-props="{n: number}"/>
</t>
</div>`;
state = {
numbers: [1, 2, 3]
};
widgets = { ChildWidget };
}
const parent = new Parent(env);
await parent.mount(fixture);
expect(normalize(fixture.innerHTML)).toBe(
normalize(`
<div>
<span>1</span>
<span>2</span>
<span>3</span>
</div>
`)
);
});
}); });
describe("props evaluation (with t-props directive)", () => { describe("props evaluation (with t-props directive)", () => {
@@ -1,6 +1,6 @@
import { import {
NotificationManager, NotificationManager,
Notification, INotification,
INotificationManager INotificationManager
} from "../../src/ts/services/notifications"; } from "../../src/ts/services/notifications";
@@ -8,7 +8,7 @@ import {
// Setup and helpers // Setup and helpers
//------------------------------------------------------------------------------ //------------------------------------------------------------------------------
function makeNotification(notif: Partial<Notification> = {}): Notification { function makeNotification(notif: Partial<INotification> = {}): INotification {
const defaultNotif = { const defaultNotif = {
id: 1, id: 1,
title: "title", title: "title",