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
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;
$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;
+11 -4
View File
@@ -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${
+8
View File
@@ -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
+16 -2
View File
@@ -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 = `
<div class="o_web_client">
<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>
`;
@@ -14,11 +20,14 @@ const template = `
export class Root extends Widget<Env, {}> {
name = "root";
template = template;
widgets = { Navbar };
widgets = { Navbar, Notification };
content: Widget<Env, {}> | 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<Env, {}> {
}
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
//------------------------------------------------------------------------------
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<Notification>): number;
add(notif: Partial<INotification>): 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<Notification>): number {
add(notif: Partial<INotification>): number {
const id = this.nextID++;
const defaultVals = {
title: "",
+8 -1
View File
@@ -11,7 +11,7 @@ const template = `
<button t-on-click="toggle">Toggle Clock/counters</button>
<button t-on-click="toggleColor">Toggle Color</button>
<button t-on-click="updateState({})">Rerender this widget</button>
<input/>
<input t-ref="textinput"/>
<t t-if="state.validcounter">
<t t-widget="Counter" t-ref="counter" t-props="{initialState:4}"/>
<t t-widget="Counter" t-ref="counter2" t-props="{initialState:400}"/>
@@ -20,6 +20,7 @@ const template = `
<t t-widget="Clock"/>
</t>
<t t-widget="ColorWidget" t-props="{color: state.color}"/>
<button t-on-click="addNotif">Add Notification</button>
</div>
`;
@@ -52,6 +53,12 @@ export class Discuss extends Widget<Env, {}> {
const newColor = this.state.color === "red" ? "blue" : "red";
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" }> {
+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);
});
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", () => {
qweb.addTemplate("test", `<img src="/test.jpg" alt="Test"/>`);
const result = renderToString(qweb, "test");
+32
View File
@@ -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", () => {
"<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)", () => {
@@ -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> = {}): Notification {
function makeNotification(notif: Partial<INotification> = {}): INotification {
const defaultNotif = {
id: 1,
title: "title",