mirror of
https://github.com/odoo/owl.git
synced 2025-10-06 19:59:41 +07:00
work on navbar
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
$navbar-height: 40px;
|
$navbar-height: 46px;
|
||||||
$main-color: #875a7b;
|
$main-color: #875a7b;
|
||||||
$o-notification-info-bg-color: #fcfbea;
|
$o-notification-info-bg-color: #fcfbea;
|
||||||
$o-main-text-color: #666666;
|
$o-main-text-color: #666666;
|
||||||
@@ -12,6 +12,25 @@ body {
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/***** Generic stuff *****/
|
||||||
|
.dropdown-toggle.o-no-caret::before,
|
||||||
|
.dropdown-toggle.o-no-caret::after {
|
||||||
|
content: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown-item {
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 20px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: #e9ecef;
|
||||||
|
}
|
||||||
|
&:active {
|
||||||
|
color: #212529;
|
||||||
|
background-color: #dee2e6;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/***** Web Client *****/
|
/***** Web Client *****/
|
||||||
.o_web_client {
|
.o_web_client {
|
||||||
font-family: sans-serif;
|
font-family: sans-serif;
|
||||||
@@ -26,17 +45,28 @@ body {
|
|||||||
color: white;
|
color: white;
|
||||||
display: flex;
|
display: flex;
|
||||||
line-height: $navbar-height;
|
line-height: $navbar-height;
|
||||||
font-size: 18px;
|
font-size: 13px;
|
||||||
|
|
||||||
&.o_in_home {
|
&.o_in_home {
|
||||||
background-color: #bba2a5;
|
background-color: #bba2a5;
|
||||||
}
|
}
|
||||||
|
|
||||||
a.o_title {
|
a.o_title {
|
||||||
padding: 10px 16px 16px 20px;
|
padding: 13px 16px 16px 20px;
|
||||||
&:hover {
|
color: white;
|
||||||
background-color: #68465f;
|
font-size: 18px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
a.o_title:hover,
|
||||||
|
a.o_menu_brand:hover {
|
||||||
|
background-color: darken($main-color, 10);
|
||||||
|
}
|
||||||
|
|
||||||
|
a.o_menu_brand {
|
||||||
|
font-size: 20px;
|
||||||
|
color: white;
|
||||||
|
text-decoration: none;
|
||||||
|
padding: 1px 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
ul {
|
ul {
|
||||||
@@ -46,18 +76,20 @@ body {
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
li {
|
.o_menu_sections > li {
|
||||||
margin: 0 4px;
|
margin: 0 4px;
|
||||||
font-size: 20px;
|
|
||||||
&:hover {
|
&:hover {
|
||||||
background-color: darken($main-color, 10);
|
background-color: darken($main-color, 10);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
a {
|
> a {
|
||||||
color: white;
|
display: block;
|
||||||
text-decoration: none;
|
height: 40px;
|
||||||
padding: 0 5px;
|
font-size: 15px;
|
||||||
|
color: white;
|
||||||
|
text-decoration: none;
|
||||||
|
padding: 0 10px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -40,7 +40,11 @@ export interface Type<T> extends Function {
|
|||||||
// Widget
|
// Widget
|
||||||
//------------------------------------------------------------------------------
|
//------------------------------------------------------------------------------
|
||||||
|
|
||||||
export class BaseWidget<T extends WEnv, Props, State> extends EventBus {
|
export class BaseWidget<
|
||||||
|
T extends WEnv,
|
||||||
|
Props,
|
||||||
|
State extends {}
|
||||||
|
> extends EventBus {
|
||||||
__widget__: Meta<WEnv>;
|
__widget__: Meta<WEnv>;
|
||||||
template: string = "default";
|
template: string = "default";
|
||||||
inlineTemplate: string | null = null;
|
inlineTemplate: string | null = null;
|
||||||
@@ -50,7 +54,7 @@ export class BaseWidget<T extends WEnv, Props, State> extends EventBus {
|
|||||||
}
|
}
|
||||||
|
|
||||||
env: T;
|
env: T;
|
||||||
state: Object = {};
|
state: State = <State>{};
|
||||||
props: Props;
|
props: Props;
|
||||||
refs: {
|
refs: {
|
||||||
[key: string]: BaseWidget<T, any, any> | HTMLElement | undefined;
|
[key: string]: BaseWidget<T, any, any> | HTMLElement | undefined;
|
||||||
|
|||||||
@@ -36,9 +36,6 @@ export interface Env extends WEnv {
|
|||||||
// registries
|
// registries
|
||||||
actionRegistry: Registry<ActionWidget>;
|
actionRegistry: Registry<ActionWidget>;
|
||||||
|
|
||||||
// data
|
|
||||||
menus: Menu[];
|
|
||||||
|
|
||||||
// helpers
|
// helpers
|
||||||
rpc: IAjax["rpc"];
|
rpc: IAjax["rpc"];
|
||||||
|
|
||||||
@@ -72,12 +69,6 @@ export const makeEnvironment = memoize(async function(): Promise<Env> {
|
|||||||
const actionManager = new ActionManager(actionRegistry);
|
const actionManager = new ActionManager(actionRegistry);
|
||||||
const notifications = new NotificationManager();
|
const notifications = new NotificationManager();
|
||||||
|
|
||||||
// demo data
|
|
||||||
const menus = [
|
|
||||||
{ title: "Discuss", actionID: 1 },
|
|
||||||
{ title: "CRM", actionID: 2 }
|
|
||||||
];
|
|
||||||
|
|
||||||
// templates
|
// templates
|
||||||
const result = await fetch("templates.xml");
|
const result = await fetch("templates.xml");
|
||||||
if (!result.ok) {
|
if (!result.ok) {
|
||||||
@@ -98,8 +89,6 @@ export const makeEnvironment = memoize(async function(): Promise<Env> {
|
|||||||
actionRegistry,
|
actionRegistry,
|
||||||
router,
|
router,
|
||||||
|
|
||||||
menus,
|
|
||||||
|
|
||||||
rpc: ajax.rpc,
|
rpc: ajax.rpc,
|
||||||
|
|
||||||
debug: false,
|
debug: false,
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ export interface BaseMenuItem {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface MenuItem extends BaseMenuItem {
|
export interface MenuItem extends BaseMenuItem {
|
||||||
|
// root menu id
|
||||||
menuId: number;
|
menuId: number;
|
||||||
actionId: number;
|
actionId: number;
|
||||||
children: MenuItem[];
|
children: MenuItem[];
|
||||||
@@ -28,6 +29,10 @@ export interface MenuInfo {
|
|||||||
// Helpers
|
// Helpers
|
||||||
//------------------------------------------------------------------------------
|
//------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate a valid MenuInfo object from a list of BaseMenuItems. This function
|
||||||
|
* is supposed to be called once at startup.
|
||||||
|
*/
|
||||||
export function processMenuItems(items: BaseMenuItem[]): MenuInfo {
|
export function processMenuItems(items: BaseMenuItem[]): MenuInfo {
|
||||||
const menuMap: MenuInfo["menuMap"] = {};
|
const menuMap: MenuInfo["menuMap"] = {};
|
||||||
const roots: number[] = [];
|
const roots: number[] = [];
|
||||||
@@ -38,10 +43,11 @@ export function processMenuItems(items: BaseMenuItem[]): MenuInfo {
|
|||||||
addToMap(root, root.id);
|
addToMap(root, root.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
function addToMap(m: BaseMenuItem, menuId: number) {
|
function addToMap(m: BaseMenuItem, menuId: number): MenuItem {
|
||||||
let item: MenuItem = Object.assign({ menuId, actionId: -1 }, <MenuItem>m);
|
let item: MenuItem = Object.assign({ menuId, actionId: -1 }, <MenuItem>m);
|
||||||
menuMap[item.id] = item;
|
menuMap[item.id] = item;
|
||||||
m.children.forEach(c => addToMap(c, menuId));
|
item.children = m.children.map(c => addToMap(c, menuId));
|
||||||
|
return item;
|
||||||
}
|
}
|
||||||
|
|
||||||
// add proper actionId to every menuitems
|
// add proper actionId to every menuitems
|
||||||
|
|||||||
@@ -59,10 +59,12 @@ export class ActionManager extends EventBus implements IActionManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
doAction(request: ActionRequest) {
|
doAction(request: ActionRequest) {
|
||||||
|
console.log("doaction", request);
|
||||||
if (typeof request === "number") {
|
if (typeof request === "number") {
|
||||||
// this is an action ID
|
// this is an action ID
|
||||||
let name = request === 1 ? "discuss" : "crm";
|
let name = request === 131 ? "discuss" : "crm";
|
||||||
let title = request === 1 ? "Discuss" : "CRM";
|
let title =
|
||||||
|
request === 131 ? "Discuss" : request === 250 ? "Notes" : "CRM";
|
||||||
let Widget = this.registry.get(name);
|
let Widget = this.registry.get(name);
|
||||||
this.stack = [
|
this.stack = [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ interface State {
|
|||||||
export class Discuss extends Widget<{}, State> {
|
export class Discuss extends Widget<{}, State> {
|
||||||
template = "web.discuss";
|
template = "web.discuss";
|
||||||
widgets = { Clock, Counter, ColorWidget };
|
widgets = { Clock, Counter, ColorWidget };
|
||||||
state = { validcounter: true, color: "red" };
|
state: State = { validcounter: true, color: "red" };
|
||||||
|
|
||||||
resetCounter(ev: MouseEvent) {
|
resetCounter(ev: MouseEvent) {
|
||||||
if (this.refs.counter instanceof Counter) {
|
if (this.refs.counter instanceof Counter) {
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { Menu } from "../env";
|
|
||||||
import { MenuItem } from "../misc/menu_helpers";
|
import { MenuItem } from "../misc/menu_helpers";
|
||||||
import { Widget } from "./widget";
|
import { PureWidget } from "./widget";
|
||||||
|
|
||||||
//------------------------------------------------------------------------------
|
//------------------------------------------------------------------------------
|
||||||
// Types
|
// Types
|
||||||
@@ -15,16 +14,21 @@ export interface Props {
|
|||||||
// Navbar
|
// Navbar
|
||||||
//------------------------------------------------------------------------------
|
//------------------------------------------------------------------------------
|
||||||
|
|
||||||
export class Navbar extends Widget<Props, {}> {
|
export class Navbar extends PureWidget<Props, {}> {
|
||||||
template = "web.navbar";
|
template = "web.navbar";
|
||||||
|
|
||||||
getUrl(menu: Menu) {
|
getUrl(menu: MenuItem) {
|
||||||
const action_id = String(menu.actionID);
|
const action_id = String(menu.actionId);
|
||||||
return this.env.router.formatURL("", { action_id });
|
const menu_id = String(menu.menuId);
|
||||||
|
return this.env.router.formatURL("", { action_id, menu_id });
|
||||||
}
|
}
|
||||||
|
|
||||||
toggleHome(ev: MouseEvent) {
|
toggleHome(ev: MouseEvent) {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
this.trigger("toggle_home_menu");
|
this.trigger("toggle_home_menu");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
openMenu(menu: MenuItem) {
|
||||||
|
this.trigger("open_menu", menu);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,8 +21,8 @@ export class HomeMenu extends Widget<Props, {}> {
|
|||||||
return info.roots.map(root => info.menuMap[root]!);
|
return info.roots.map(root => info.menuMap[root]!);
|
||||||
}
|
}
|
||||||
|
|
||||||
openApp(app: MenuItem, event: MouseEvent) {
|
openMenu(app: MenuItem, event: MouseEvent) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
this.trigger("app_opened", app);
|
this.trigger("open_menu", app);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,9 @@ import { Widget } from "./widget";
|
|||||||
export class Notification extends Widget<INotification, {}> {
|
export class Notification extends Widget<INotification, {}> {
|
||||||
template = "web.notification";
|
template = "web.notification";
|
||||||
|
|
||||||
close() {
|
close(ev: MouseEvent) {
|
||||||
|
// we do not want the url to change
|
||||||
|
ev.preventDefault();
|
||||||
this.env.notifications.close(this.props.id);
|
this.env.notifications.close(this.props.id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -89,7 +89,8 @@ export class Root extends Widget<Props, State> {
|
|||||||
this.updateState({ inHome: !this.state.inHome });
|
this.updateState({ inHome: !this.state.inHome });
|
||||||
}
|
}
|
||||||
|
|
||||||
openApp(app: MenuItem) {
|
openMenu(menu: MenuItem) {
|
||||||
this.updateAppState(app, app.actionId);
|
const app = this.props.menuInfo.menuMap[menu.menuId]!;
|
||||||
|
this.updateAppState(app, menu.actionId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,3 +2,23 @@ import { BaseWidget } from "../core/base_widget";
|
|||||||
import { Env } from "../env";
|
import { Env } from "../env";
|
||||||
|
|
||||||
export class Widget<Props, State> extends BaseWidget<Env, Props, State> {}
|
export class Widget<Props, State> extends BaseWidget<Env, Props, State> {}
|
||||||
|
|
||||||
|
// TODO: move this to PureComponent in core
|
||||||
|
export class PureWidget<Props, State> extends Widget<Props, State> {
|
||||||
|
shouldUpdate(nextProps: Props): boolean {
|
||||||
|
for (let k in nextProps) {
|
||||||
|
if (nextProps[k] !== this.props[k]) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
async updateState(nextState: Partial<State>) {
|
||||||
|
for (let k in nextState) {
|
||||||
|
if (nextState[k] !== this.state[k]) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return super.updateState(nextState);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,9 +2,9 @@
|
|||||||
<templates id="template" xml:space="preserve">
|
<templates id="template" xml:space="preserve">
|
||||||
|
|
||||||
<div t-name="web.web_client" class="o_web_client">
|
<div t-name="web.web_client" class="o_web_client">
|
||||||
<t t-widget="Navbar" t-props="{inHome:state.inHome,app:state.currentApp}" t-on-toggle_home_menu="toggleHome"/>
|
<t t-widget="Navbar" t-props="{inHome:state.inHome,app:state.currentApp}" t-on-toggle_home_menu="toggleHome" t-on-open_menu="openMenu"/>
|
||||||
<t t-if="state.inHome">
|
<t t-if="state.inHome">
|
||||||
<t t-widget="HomeMenu" t-keep-alive="1" t-props="{menuInfo:props.menuInfo}" t-on-app_opened="openApp"/>
|
<t t-widget="HomeMenu" t-keep-alive="1" t-props="{menuInfo:props.menuInfo}" t-on-open_menu="openMenu"/>
|
||||||
</t>
|
</t>
|
||||||
<t t-else="1">
|
<t t-else="1">
|
||||||
<t t-widget="ActionContainer" t-props="{stack:state.stack}" t-keep-alive="1"/>
|
<t t-widget="ActionContainer" t-props="{stack:state.stack}" t-keep-alive="1"/>
|
||||||
@@ -18,22 +18,44 @@
|
|||||||
|
|
||||||
<div t-name="web.navbar" class="o_navbar" t-att-class="props.inHome ? 'o_navbar o_in_home' : 'o_navbar'">
|
<div t-name="web.navbar" class="o_navbar" t-att-class="props.inHome ? 'o_navbar o_in_home' : 'o_navbar'">
|
||||||
<a aria-label="Applications" class="o_title fa fa-th" href="#" title="Applications" accesskey="h" t-on-click="toggleHome"/>
|
<a aria-label="Applications" class="o_title fa fa-th" href="#" title="Applications" accesskey="h" t-on-click="toggleHome"/>
|
||||||
<ul t-if="!props.inHome && props.app">
|
<t t-if="!props.inHome && props.app">
|
||||||
<a class="o_menu_brand" href="#" role="button">
|
<a class="o_menu_brand" t-att-href="getUrl(props.app)" role="button" t-on-click="openMenu(props.app)">
|
||||||
<t t-esc="props.app.name"/>
|
<t t-esc="props.app.name"/>
|
||||||
</a>
|
</a>
|
||||||
<li t-foreach="env.menus" t-as="menu">
|
<ul class="o_menu_sections">
|
||||||
<a t-att-href="getUrl(menu)">
|
<li t-foreach="props.app.children" t-as="menu">
|
||||||
<t t-esc="menu.title"/>
|
<t t-if="menu.children.length === 0">
|
||||||
</a>
|
<a t-att-href="getUrl(menu)" role="menuitem" t-on-click="openMenu(menu)">
|
||||||
</li>
|
<span>
|
||||||
<li t-if="env.isMobile">MOBILEMODE</li>
|
<t t-esc="menu.name"/>
|
||||||
</ul>
|
</span>
|
||||||
|
</a>
|
||||||
|
</t>
|
||||||
|
<t t-else="1">
|
||||||
|
<a href="#" role="button" class="dropdown-toggle o-no-caret" data-toggle="dropdown">
|
||||||
|
<span>
|
||||||
|
<t t-esc="menu.name"/>
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
<div class="dropdown-menu" role="menu">
|
||||||
|
<t t-foreach="menu.children" t-as="submenu">
|
||||||
|
<a t-att-href="getUrl(submenu)" data-toggle="collapse" data-target="#someid" role="menuitem" class="dropdown-item" t-on-click="openMenu(submenu)">
|
||||||
|
<span>
|
||||||
|
<t t-esc="submenu.name"/>
|
||||||
|
</span>
|
||||||
|
</a>
|
||||||
|
</t>
|
||||||
|
</div>
|
||||||
|
</t>
|
||||||
|
</li>
|
||||||
|
<li t-if="env.isMobile">MOBILEMODE</li>
|
||||||
|
</ul>
|
||||||
|
</t>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div t-name="web.home_menu" class="o_home_menu">
|
<div t-name="web.home_menu" class="o_home_menu">
|
||||||
<div class="o_apps">
|
<div class="o_apps">
|
||||||
<a t-foreach="apps" t-as="app" t-att-href="'#menu_id=' + app.menuId + '&action_id=' + app.actionId" class="o_app" t-on-click="openApp(app)">
|
<a t-foreach="apps" t-as="app" t-att-href="'#menu_id=' + app.menuId + '&action_id=' + app.actionId" class="o_app" t-on-click="openMenu(app)">
|
||||||
<i t-att-class="app.icon + ' o_app_icon fa-3x fa-fw'"/>
|
<i t-att-class="app.icon + ' o_app_icon fa-3x fa-fw'"/>
|
||||||
<div class="o_caption">
|
<div class="o_caption">
|
||||||
<t t-esc="app.name"/>
|
<t t-esc="app.name"/>
|
||||||
|
|||||||
@@ -40,8 +40,7 @@ export function makeTestEnv(): Env {
|
|||||||
router,
|
router,
|
||||||
rpc: ajax.rpc,
|
rpc: ajax.rpc,
|
||||||
debug: false,
|
debug: false,
|
||||||
isMobile: false,
|
isMobile: false
|
||||||
menus: []
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { Env } from "../../src/ts/env";
|
|
||||||
import { Navbar, Props } from "../../src/ts/widgets/navbar";
|
import { Navbar, Props } from "../../src/ts/widgets/navbar";
|
||||||
import { makeTestEnv, makeTestFixture, loadTemplates } from "../helpers";
|
import { makeTestEnv, makeTestFixture, loadTemplates } from "../helpers";
|
||||||
|
|
||||||
@@ -7,7 +6,7 @@ import { makeTestEnv, makeTestFixture, loadTemplates } from "../helpers";
|
|||||||
//------------------------------------------------------------------------------
|
//------------------------------------------------------------------------------
|
||||||
|
|
||||||
let fixture: HTMLElement;
|
let fixture: HTMLElement;
|
||||||
let env: Env;
|
let env: ReturnType<typeof makeTestEnv>;
|
||||||
let props: Props;
|
let props: Props;
|
||||||
let templates: string;
|
let templates: string;
|
||||||
|
|
||||||
@@ -37,7 +36,6 @@ test("can be rendered", async () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("can render one menu item", async () => {
|
test("can render one menu item", async () => {
|
||||||
env.menus.push({ title: "menu", actionID: 4 });
|
|
||||||
const navbar = new Navbar(env, props);
|
const navbar = new Navbar(env, props);
|
||||||
await navbar.mount(fixture);
|
await navbar.mount(fixture);
|
||||||
expect(fixture.innerHTML).toMatchSnapshot();
|
expect(fixture.innerHTML).toMatchSnapshot();
|
||||||
|
|||||||
Reference in New Issue
Block a user