mirror of
https://github.com/odoo/owl.git
synced 2025-10-06 19:59:41 +07:00
add notification widget/full system
This commit is contained in:
@@ -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?
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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${
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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: "",
|
||||
|
||||
@@ -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" }> {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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");
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user