From d029f87a0f82c0ab1b55224adc65ca30ac24f102 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?G=C3=A9ry=20Debongnie?= Date: Tue, 29 Jan 2019 15:19:02 +0100 Subject: [PATCH] add notification widget/full system --- README.md | 2 + web/static/src/scss/app.scss | 37 +++++++++++++++++++ web/static/src/ts/core/qweb_vdom.ts | 15 ++++++-- web/static/src/ts/env.ts | 8 ++++ web/static/src/ts/root.ts | 18 ++++++++- web/static/src/ts/services/notifications.ts | 12 +++--- web/static/src/ts/widgets/Discuss.ts | 9 ++++- web/static/src/ts/widgets/notification.ts | 20 ++++++++++ web/static/tests/core/qweb_vdom.test.ts | 7 ++++ web/static/tests/core/widget.test.ts | 32 ++++++++++++++++ .../tests/services/notifications.test.ts | 4 +- 11 files changed, 149 insertions(+), 15 deletions(-) create mode 100644 web/static/src/ts/widgets/notification.ts diff --git a/README.md b/README.md index f622f6be..526352d5 100644 --- a/README.md +++ b/README.md @@ -185,3 +185,5 @@ We have 3 main folders and 3 main files: - **Rendering** think about batching all patching updates in a 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? diff --git a/web/static/src/scss/app.scss b/web/static/src/scss/app.scss index ec5514ee..5dcd58b0 100644 --- a/web/static/src/scss/app.scss +++ b/web/static/src/scss/app.scss @@ -1,5 +1,7 @@ $navbar-height: 40px; $main-color: #875a7b; +$o-notification-info-bg-color: #fcfbea; +$o-main-text-color: #666666; html { 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 */ .o_discuss { padding: 20px; diff --git a/web/static/src/ts/core/qweb_vdom.ts b/web/static/src/ts/core/qweb_vdom.ts index 917de500..1d45729b 100644 --- a/web/static/src/ts/core/qweb_vdom.ts +++ b/web/static/src/ts/core/qweb_vdom.ts @@ -29,6 +29,7 @@ export class Context { caller: Element | undefined; shouldDefineOwner: boolean = false; shouldProtectContext: boolean = false; + inLoop: boolean = false; constructor() { this.rootContext = this; @@ -134,7 +135,7 @@ export class QWeb { if (!doc.firstChild) { 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"); } let tbranch = doc.querySelectorAll("[t-elif], [t-else]"); @@ -331,13 +332,17 @@ export class QWeb { const attrs: string[] = []; const tattrs: number[] = []; for (let i = 0; i < attributes.length; i++) { - const name = attributes[i].name; + let name = attributes[i].name; const value = attributes[i].textContent!; // regular attributes if (!name.startsWith("t-")) { const attID = ctx.generateID(); 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}`); } @@ -624,6 +629,7 @@ const forEachDirective: Directive = { priority: 10, atNodeEncounter({ node, qweb, ctx }): boolean { ctx.rootContext.shouldProtectContext = true; + ctx.inLoop = true; const elems = node.getAttribute("t-foreach")!; const name = node.getAttribute("t-as")!; let arrayID = ctx.generateID(); @@ -715,8 +721,9 @@ const widgetDirective: Directive = { ctx.addLine(`let _${dummyID}_index = c${ctx.parentNode}.length;`); ctx.addLine(`c${ctx.parentNode}.push(_${dummyID});`); ctx.addLine(`let def${defID};`); + let templateID = ctx.inLoop ? `(${widgetID} + i)` : String(widgetID); 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}) {`); @@ -734,7 +741,7 @@ const widgetDirective: Directive = { `let _${widgetID} = new context.widgets['${value}'](owner, ${props});` ); ctx.addLine( - `context.__widget__.cmap[${widgetID}] = _${widgetID}.__widget__.id;` + `context.__widget__.cmap[${templateID}] = _${widgetID}.__widget__.id;` ); ctx.addLine( `def${defID} = _${widgetID}._start().then(() => _${widgetID}._render()).then(vnode=>{c${ diff --git a/web/static/src/ts/env.ts b/web/static/src/ts/env.ts index e952c00b..e8d3a7d3 100644 --- a/web/static/src/ts/env.ts +++ b/web/static/src/ts/env.ts @@ -4,6 +4,10 @@ import { WEnv } from "./core/widget"; import { registry } from "./registry"; import { ActionManager, IActionManager } from "./services/action_manager"; import { Ajax, IAjax } from "./services/ajax"; +import { + INotificationManager, + NotificationManager +} from "./services/notifications"; import { IRouter, Router } from "./services/router"; //------------------------------------------------------------------------------ @@ -21,6 +25,7 @@ export interface Env extends WEnv { ajax: IAjax; router: IRouter; menus: Menu[]; + notifications: INotificationManager; // helpers rpc: IAjax["rpc"]; @@ -48,6 +53,8 @@ export const makeEnvironment = memoize(function(): Env { const router = new Router(); const ajax = new Ajax(); const actionManager = new ActionManager(router, registry); + const notifications = new NotificationManager(); + const menus = [ { title: "Discuss", actionID: 1 }, { title: "CRM", actionID: 2 } @@ -62,6 +69,7 @@ export const makeEnvironment = memoize(function(): Env { router, actionManager, menus, + notifications, rpc: ajax.rpc, debug: false diff --git a/web/static/src/ts/root.ts b/web/static/src/ts/root.ts index c75fe253..d5154057 100644 --- a/web/static/src/ts/root.ts +++ b/web/static/src/ts/root.ts @@ -1,12 +1,18 @@ import { Widget } from "./core/widget"; import { Navbar } from "./widgets/navbar"; import { ActionWidget } from "./services/action_manager"; +import { INotification } from "./services/notifications"; +import { Notification } from "./widgets/notification"; import { Env } from "./env"; const template = `
-
+
+
+ + +
`; @@ -14,11 +20,14 @@ const template = ` export class Root extends Widget { name = "root"; template = template; - widgets = { Navbar }; + widgets = { Navbar, Notification }; content: Widget | null = null; + state: { notifications: INotification[] } = { notifications: [] }; + mounted() { this.env.actionManager.on("action_ready", this, this.setContentWidget); + this.env.notifications.on("notification_added", this, this.addNotification); const actionWidget = this.env.actionManager.getCurrentAction(); if (actionWidget) { this.setContentWidget(actionWidget); @@ -34,4 +43,9 @@ export class Root extends Widget { } this.content = newWidget; } + + addNotification(notif: INotification) { + const notifications = this.state.notifications.concat(notif); + this.updateState({ notifications }); + } } diff --git a/web/static/src/ts/services/notifications.ts b/web/static/src/ts/services/notifications.ts index ec8bfab2..44a52c53 100644 --- a/web/static/src/ts/services/notifications.ts +++ b/web/static/src/ts/services/notifications.ts @@ -4,7 +4,7 @@ import { EventBus as Bus } from "../core/event_bus"; // Types //------------------------------------------------------------------------------ -export interface Notification { +export interface INotification { id: number; title: string; message: string; @@ -14,10 +14,10 @@ export interface Notification { export type NotificationEvent = "notification_added" | "notification_closed"; -export type Callback = (notif: Notification) => void; +export type Callback = (notif: INotification) => void; export interface INotificationManager { - add(notif: Partial): number; + add(notif: Partial): number; close(id: number): void; on(event: NotificationEvent, owner: any, callback: Callback): void; } @@ -26,10 +26,10 @@ export interface INotificationManager { // Notification Manager //------------------------------------------------------------------------------ export class NotificationManager extends Bus implements INotificationManager { - nextID = 0; - notifications: { [key: number]: Notification } = {}; + nextID = 1; + notifications: { [key: number]: INotification } = {}; - add(notif: Partial): number { + add(notif: Partial): number { const id = this.nextID++; const defaultVals = { title: "", diff --git a/web/static/src/ts/widgets/Discuss.ts b/web/static/src/ts/widgets/Discuss.ts index cd5fae28..c435af51 100644 --- a/web/static/src/ts/widgets/Discuss.ts +++ b/web/static/src/ts/widgets/Discuss.ts @@ -11,7 +11,7 @@ const template = ` - + @@ -20,6 +20,7 @@ const template = ` +
`; @@ -52,6 +53,12 @@ export class Discuss extends Widget { const newColor = this.state.color === "red" ? "blue" : "red"; this.updateState({ color: newColor }); } + + addNotif() { + const text = (this.refs.textinput).value; + const message = `It is now ${new Date().toLocaleTimeString()}.
Msg: ${text}`; + this.env.notifications.add({ title: "hey", message: message }); + } } class ColorWidget extends Widget { diff --git a/web/static/src/ts/widgets/notification.ts b/web/static/src/ts/widgets/notification.ts new file mode 100644 index 00000000..3d1d3870 --- /dev/null +++ b/web/static/src/ts/widgets/notification.ts @@ -0,0 +1,20 @@ +import { Widget } from "../core/widget"; +import { Env } from "../env"; +import { INotification } from "../services/notifications"; + +const template = ` + `; + +export class Notification extends Widget { + name = "notification"; + template = template; +} diff --git a/web/static/tests/core/qweb_vdom.test.ts b/web/static/tests/core/qweb_vdom.test.ts index 735b81dd..352c13ed 100644 --- a/web/static/tests/core/qweb_vdom.test.ts +++ b/web/static/tests/core/qweb_vdom.test.ts @@ -280,6 +280,13 @@ describe("attributes", () => { expect(result).toBe(expected); }); + test("static attributes with dashes", () => { + qweb.addTemplate("test", `
`); + const result = renderToString(qweb, "test"); + const expected = `
`; + expect(result).toBe(expected); + }); + test("static attributes on void elements", () => { qweb.addTemplate("test", `Test`); const result = renderToString(qweb, "test"); diff --git a/web/static/tests/core/widget.test.ts b/web/static/tests/core/widget.test.ts index 7c016f19..c932966f 100644 --- a/web/static/tests/core/widget.test.ts +++ b/web/static/tests/core/widget.test.ts @@ -1,5 +1,6 @@ import { WEnv, Widget } from "../../src/ts/core/widget"; import { makeTestWEnv, makeTestFixture } from "../helpers"; +import { normalize } from "../helpers"; //------------------------------------------------------------------------------ // Setup and helpers @@ -573,6 +574,37 @@ describe("composition", () => { "
0
" ); }); + + test("sub widgets rendered in a loop", async () => { + class ChildWidget extends Widget { + name = "c"; + template = ``; + } + class Parent extends Widget { + name = "p"; + template = ` +
+ + + +
`; + state = { + numbers: [1, 2, 3] + }; + widgets = { ChildWidget }; + } + const parent = new Parent(env); + await parent.mount(fixture); + expect(normalize(fixture.innerHTML)).toBe( + normalize(` +
+ 1 + 2 + 3 +
+ `) + ); + }); }); describe("props evaluation (with t-props directive)", () => { diff --git a/web/static/tests/services/notifications.test.ts b/web/static/tests/services/notifications.test.ts index 75f91d07..f53d13bd 100644 --- a/web/static/tests/services/notifications.test.ts +++ b/web/static/tests/services/notifications.test.ts @@ -1,6 +1,6 @@ import { NotificationManager, - Notification, + INotification, INotificationManager } from "../../src/ts/services/notifications"; @@ -8,7 +8,7 @@ import { // Setup and helpers //------------------------------------------------------------------------------ -function makeNotification(notif: Partial = {}): Notification { +function makeNotification(notif: Partial = {}): INotification { const defaultNotif = { id: 1, title: "title",