Merge pull request #54 from expsa/youssef_new_sidebar2
feat: Add a new custom sidebar backend theme with menu, navbar, and s…
This commit is contained in:
commit
329ba55823
|
|
@ -0,0 +1 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
{
|
||||||
|
'name': 'Odex Sidebar Backend Theme2',
|
||||||
|
'version': '18.0.1.0.0',
|
||||||
|
'category': 'Web',
|
||||||
|
'summary': 'Custom menu for navigating all Odoo apps and menus smoothly',
|
||||||
|
'description': """
|
||||||
|
Replace the default Odoo app menu bar with a collapsible sidebar menu.
|
||||||
|
""",
|
||||||
|
'author': 'Your Company',
|
||||||
|
'website': 'https://www.yourcompany.com',
|
||||||
|
'depends': ['web'],
|
||||||
|
'data': [],
|
||||||
|
'assets': {
|
||||||
|
'web.assets_backend': [
|
||||||
|
'odex_sidebar_backend_theme2/static/src/scss/sidebar_menu.scss',
|
||||||
|
|
||||||
|
'odex_sidebar_backend_theme2/static/src/xml/sidebar_menu_template.xml',
|
||||||
|
'odex_sidebar_backend_theme2/static/src/xml/menu_item_template.xml',
|
||||||
|
|
||||||
|
'odex_sidebar_backend_theme2/static/src/js/sidebar_menu.js',
|
||||||
|
'odex_sidebar_backend_theme2/static/src/js/menu_item.js',
|
||||||
|
|
||||||
|
'odex_sidebar_backend_theme2/static/src/xml/navbar_patch.xml',
|
||||||
|
'odex_sidebar_backend_theme2/static/src/js/navbar_patch.js',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
'installable': True,
|
||||||
|
'application': False,
|
||||||
|
'auto_install': False,
|
||||||
|
'license': 'LGPL-3',
|
||||||
|
}
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 2.9 KiB |
|
|
@ -0,0 +1,49 @@
|
||||||
|
/** @odoo-module **/
|
||||||
|
|
||||||
|
import { Component } from "@odoo/owl";
|
||||||
|
import { useService } from "@web/core/utils/hooks";
|
||||||
|
|
||||||
|
export class MenuItem extends Component {
|
||||||
|
static template = "odex_sidebar_backend_theme2.MenuItem"
|
||||||
|
|
||||||
|
static components = { MenuItem };
|
||||||
|
|
||||||
|
// 2. Define props received by the component
|
||||||
|
static props = {
|
||||||
|
menu: { type: Object },
|
||||||
|
siblings: { type: Array },
|
||||||
|
isCollapsed: { type: Boolean, optional: true },
|
||||||
|
};
|
||||||
|
|
||||||
|
setup() {
|
||||||
|
this.menuService = useService("menu");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. Modify toggleMenu function to handle siblings
|
||||||
|
toggleMenu(menu, siblings) {
|
||||||
|
const wasOpen = menu.isOpen; // Save old state
|
||||||
|
|
||||||
|
// Step A: Close all sibling menus
|
||||||
|
for (const sibling of siblings) {
|
||||||
|
sibling.isOpen = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step B: Open current menu only if it was originally closed
|
||||||
|
// This prevents immediate reopening and allows closing behavior when clicking again
|
||||||
|
if (!wasOpen) {
|
||||||
|
menu.isOpen = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
selectMenu(menu) {
|
||||||
|
this.menuService.selectMenu(menu);
|
||||||
|
try {
|
||||||
|
// Use xmlid if available, otherwise use name as a stable identifier
|
||||||
|
const uniqueId = menu.xmlid || menu.name || menu.id;
|
||||||
|
localStorage.setItem('odex_sidebar_active_menu', uniqueId);
|
||||||
|
console.log('Saved menu:', uniqueId, menu.name);
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Storage error:', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
/** @odoo-module **/
|
||||||
|
|
||||||
|
// **Re-import NavBar**
|
||||||
|
import { NavBar } from "@web/webclient/navbar/navbar";
|
||||||
|
import { patch } from "@web/core/utils/patch";
|
||||||
|
import { useService } from "@web/core/utils/hooks";
|
||||||
|
|
||||||
|
|
||||||
|
// **Patch NavBar component**
|
||||||
|
patch(NavBar.prototype, {
|
||||||
|
setup() {
|
||||||
|
super.setup(...arguments); // Very important
|
||||||
|
// Initialize bus service
|
||||||
|
this.busService = useService("bus_service");
|
||||||
|
},
|
||||||
|
|
||||||
|
// 3. Add function to trigger event
|
||||||
|
toggleSidebar() {
|
||||||
|
this.busService.trigger("toggle-sidebar");
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -0,0 +1,152 @@
|
||||||
|
/** @odoo-module **/
|
||||||
|
|
||||||
|
import { Component, onMounted, useEffect, useState } from "@odoo/owl";
|
||||||
|
import { registry } from "@web/core/registry";
|
||||||
|
import { useService } from "@web/core/utils/hooks";
|
||||||
|
import { MenuItem } from "./menu_item";
|
||||||
|
|
||||||
|
export class SidebarMenu extends Component {
|
||||||
|
static template = "odex_sidebar_backend_theme2.SidebarMenu"
|
||||||
|
|
||||||
|
static components = { MenuItem };
|
||||||
|
|
||||||
|
setup() {
|
||||||
|
this.menuService = useService("menu");
|
||||||
|
this.busService = useService("bus_service");
|
||||||
|
this.actionService = useService("action");
|
||||||
|
|
||||||
|
this.state = useState({
|
||||||
|
menus: [],
|
||||||
|
isOpen: true,
|
||||||
|
isCollapsed: false
|
||||||
|
});
|
||||||
|
|
||||||
|
// =================== JavaScript Control Starts Here ===================
|
||||||
|
|
||||||
|
const applyLayoutChanges = (isOpen) => {
|
||||||
|
const actionManager = document.querySelector('.o_action_manager');
|
||||||
|
const mainNavbar = document.querySelector('.o_navbar');
|
||||||
|
|
||||||
|
// Determine sidebar width based on collapse state
|
||||||
|
const collapsedWidth = "90px";
|
||||||
|
|
||||||
|
if (isOpen) {
|
||||||
|
if (actionManager) actionManager.style.marginInlineStart = collapsedWidth;
|
||||||
|
if (mainNavbar) mainNavbar.style.marginInlineStart = collapsedWidth;
|
||||||
|
} else {
|
||||||
|
// If sidebar is hidden, remove margin
|
||||||
|
if (actionManager) actionManager.style.marginInlineStart = '0';
|
||||||
|
if (mainNavbar) mainNavbar.style.marginInlineStart = '0';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Use useEffect to monitor changes in open and collapse states
|
||||||
|
useEffect(
|
||||||
|
() => {
|
||||||
|
// This code runs every time isOpen or isCollapsed changes
|
||||||
|
applyLayoutChanges(this.state.isOpen, this.state.isCollapsed);
|
||||||
|
},
|
||||||
|
() => [this.state.isOpen, this.state.isCollapsed] // <-- Monitor both values
|
||||||
|
);
|
||||||
|
|
||||||
|
// ====================================================================
|
||||||
|
|
||||||
|
const toggleSidebar = () => {
|
||||||
|
this.state.isOpen = !this.state.isOpen;
|
||||||
|
};
|
||||||
|
|
||||||
|
const toggleCollapse = () => {
|
||||||
|
this.state.isCollapsed = !this.state.isCollapsed;
|
||||||
|
try {
|
||||||
|
localStorage.setItem('odex_sidebar_collapsed', this.state.isCollapsed);
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Storage error:', e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleLogout = async () => {
|
||||||
|
await this.actionService.doAction({
|
||||||
|
type: 'ir.actions.act_url',
|
||||||
|
url: '/web/session/logout',
|
||||||
|
target: 'self',
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
this.toggleSidebar = toggleSidebar;
|
||||||
|
this.toggleCollapse = toggleCollapse;
|
||||||
|
this.handleLogout = handleLogout;
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
this.loadMenus();
|
||||||
|
this.busService.addEventListener("toggle-sidebar", this.toggleSidebar);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const isCollapsed = localStorage.getItem('odex_sidebar_collapsed') === 'true';
|
||||||
|
this.state.isCollapsed = isCollapsed;
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Storage error:', e);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call function on initial load to ensure correct state application
|
||||||
|
applyLayoutChanges(this.state.isOpen, this.state.isCollapsed);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
loadMenus() {
|
||||||
|
const allMenus = this.menuService.getAll();
|
||||||
|
const clonedMenus = structuredClone(allMenus);
|
||||||
|
|
||||||
|
const menuMap = new Map(clonedMenus.map(menu => [menu.id, menu]));
|
||||||
|
const menuMapByXmlId = new Map(clonedMenus.map(menu => [menu.xmlid || menu.name, menu]));
|
||||||
|
const parentMap = new Map();
|
||||||
|
let finalMenuTree = [];
|
||||||
|
|
||||||
|
for (const menu of clonedMenus) {
|
||||||
|
if (menu.children) {
|
||||||
|
menu.children = menu.children.map(childId => {
|
||||||
|
parentMap.set(childId, menu.id);
|
||||||
|
return menuMap.get(childId);
|
||||||
|
}).filter(Boolean);
|
||||||
|
} else {
|
||||||
|
menu.children = [];
|
||||||
|
}
|
||||||
|
menu.isOpen = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const rootMenu = clonedMenus.find(menu => menu.id === "root");
|
||||||
|
if (rootMenu && rootMenu.children) {
|
||||||
|
finalMenuTree = rootMenu.children;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const activeId = localStorage.getItem('odex_sidebar_active_menu');
|
||||||
|
console.log('Restored menu ID:', activeId);
|
||||||
|
|
||||||
|
let targetMenu = menuMapByXmlId.get(activeId) || menuMap.get(activeId);
|
||||||
|
|
||||||
|
if (targetMenu) {
|
||||||
|
let currentId = targetMenu.id;
|
||||||
|
while (parentMap.has(currentId)) {
|
||||||
|
const parentId = parentMap.get(currentId);
|
||||||
|
const parentMenu = menuMap.get(parentId);
|
||||||
|
if (parentMenu) {
|
||||||
|
parentMenu.isOpen = true;
|
||||||
|
currentId = parentId;
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.warn('Active menu not found in map:', activeId);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.error('Storage error:', e);
|
||||||
|
}
|
||||||
|
|
||||||
|
this.state.menus.splice(0, this.state.menus.length, ...finalMenuTree);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
registry.category("main_components").add("sidebar_menu_component", {
|
||||||
|
Component: SidebarMenu,
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,494 @@
|
||||||
|
/* ===================================
|
||||||
|
1. Main Sidebar Design
|
||||||
|
=================================== */
|
||||||
|
.custom_sidebar {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 270px;
|
||||||
|
height: 100vh;
|
||||||
|
overflow: auto;
|
||||||
|
background-color: #151a2d;
|
||||||
|
z-index: 999;
|
||||||
|
transform: translateX(0%);
|
||||||
|
transition: all 0.4s ease-in-out;
|
||||||
|
|
||||||
|
&:not(.is-open) {
|
||||||
|
transform: translateX(-100%) !important;
|
||||||
|
/* Use !important to force hiding */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* =============== Add New Flyout =============== */
|
||||||
|
|
||||||
|
&.is-collapsed {
|
||||||
|
overflow: visible;
|
||||||
|
|
||||||
|
li {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flyout-panel {
|
||||||
|
display: none;
|
||||||
|
position: absolute;
|
||||||
|
left: 100%;
|
||||||
|
top: 0;
|
||||||
|
min-width: 220px;
|
||||||
|
background: #151a2d;
|
||||||
|
color: #fff;
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 10px;
|
||||||
|
box-shadow: 0 12px 30px rgba(8, 10, 20, 0.6);
|
||||||
|
transform-origin: left top;
|
||||||
|
transition: opacity 180ms ease, transform 180ms ease;
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateX(-6px) scale(0.98);
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
li:hover > .flyout-panel {
|
||||||
|
display: block;
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateX(0) scale(1);
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flyout-list {
|
||||||
|
list-style: none;
|
||||||
|
padding: 6px 4px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flyout-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 8px 10px;
|
||||||
|
border-radius: 8px;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 150ms ease;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flyout-item:hover {
|
||||||
|
background: rgba(255, 255, 255, 0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.flyout-item .icon {
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flyout-item .label {
|
||||||
|
color: #fff;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Submenu flyout */
|
||||||
|
.flyout-item.has-children {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flyout-subpanel {
|
||||||
|
display: none;
|
||||||
|
position: absolute;
|
||||||
|
left: 100%;
|
||||||
|
top: 0;
|
||||||
|
min-width: 200px;
|
||||||
|
background: #151a2d;
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 10px;
|
||||||
|
box-shadow: 0 12px 30px rgba(8, 10, 20, 0.6);
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateX(-6px) scale(0.98);
|
||||||
|
transition: opacity 180ms ease, transform 180ms ease;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 1200;
|
||||||
|
}
|
||||||
|
|
||||||
|
.flyout-item.has-children:hover > .flyout-subpanel {
|
||||||
|
display: block;
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateX(0) scale(1);
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.is-collapsed {
|
||||||
|
width: 90px;
|
||||||
|
transform: translateX(0);
|
||||||
|
|
||||||
|
.sidebar-header {
|
||||||
|
padding: 25px 10px;
|
||||||
|
justify-content: center;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
.header-logo {
|
||||||
|
img {
|
||||||
|
width: 46px;
|
||||||
|
height: 46px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-toggler {
|
||||||
|
position: static;
|
||||||
|
width: 50%;
|
||||||
|
margin-top: 25px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-nav {
|
||||||
|
.sidebar_menu_list {
|
||||||
|
padding: 0 8px;
|
||||||
|
|
||||||
|
&.primary-nav {
|
||||||
|
|
||||||
|
.has-children,
|
||||||
|
li:not(.has-children) {
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
.menu-item-container {
|
||||||
|
padding: 11px 8px;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
.menu-item-icon {
|
||||||
|
margin: 0;
|
||||||
|
width: 30px;
|
||||||
|
height: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-item-link {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-icon {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: #eef2ff;
|
||||||
|
color: #151a2d;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
background-color: #d9e1fd;
|
||||||
|
transform: scale(0.95);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.submenu_list {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.secondary-nav {
|
||||||
|
width: auto;
|
||||||
|
bottom: 20px;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
padding: 0 8px;
|
||||||
|
|
||||||
|
.nav-item {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1000;
|
||||||
|
|
||||||
|
.nav-link {
|
||||||
|
padding: 11px 8px;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
.nav-label {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav_icon {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown_menu {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Flyout support for secondary-nav
|
||||||
|
&:hover > .flyout-panel {
|
||||||
|
display: block;
|
||||||
|
opacity: 1;
|
||||||
|
transform: translateX(0) scale(1);
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
li.open {
|
||||||
|
> .menu-item-container {
|
||||||
|
background-color: #fff;
|
||||||
|
|
||||||
|
.menu-item-link {
|
||||||
|
color: #151a2d !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-icon {
|
||||||
|
transform: rotate(90deg);
|
||||||
|
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23ffffff' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M5 14l6-6-6-6'/%3e%3c/svg%3e");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&::-webkit-scrollbar {
|
||||||
|
width: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::-webkit-scrollbar-track {
|
||||||
|
background: #2f3542;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::-webkit-scrollbar-thumb {
|
||||||
|
background-color: #151a2d;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::-webkit-scrollbar-thumb:hover {
|
||||||
|
background-color: #151a2d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-header {
|
||||||
|
display: flex;
|
||||||
|
position: relative;
|
||||||
|
padding: 25px 20px;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
|
||||||
|
.header-logo {
|
||||||
|
img {
|
||||||
|
width: 46px;
|
||||||
|
height: 46px;
|
||||||
|
display: block;
|
||||||
|
object-fit: contain;
|
||||||
|
border-radius: 50%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-toggler {
|
||||||
|
position: absolute;
|
||||||
|
right: 20px;
|
||||||
|
height: 35px;
|
||||||
|
width: 35px;
|
||||||
|
color: #151a2d;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
background: #eef2ff;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: 8px;
|
||||||
|
transition: 0.4s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: #d9e1fd;
|
||||||
|
}
|
||||||
|
|
||||||
|
span {
|
||||||
|
transition: 0.4s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
i {
|
||||||
|
font-size: 18px;
|
||||||
|
transition: transform 0.4s ease;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar-nav {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
.sidebar_menu_list {
|
||||||
|
list-style: none;
|
||||||
|
display: flex;
|
||||||
|
gap: 4px;
|
||||||
|
padding: 0 15px;
|
||||||
|
flex-direction: column;
|
||||||
|
transform: translateY(15px);
|
||||||
|
transition: 0.4s ease;
|
||||||
|
|
||||||
|
&.primary-nav {
|
||||||
|
.has-children,
|
||||||
|
li:not(.has-children) {
|
||||||
|
&:hover {
|
||||||
|
> .menu-item-container {
|
||||||
|
background-color: #eef2ff;
|
||||||
|
|
||||||
|
.menu-item-link {
|
||||||
|
color: #151a2d;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.menu-item-container {
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.3s;
|
||||||
|
border: 1px solid #151a2d;
|
||||||
|
text-decoration: none;
|
||||||
|
padding: 11px 15px;
|
||||||
|
border-radius: 8px;
|
||||||
|
white-space: nowrap;
|
||||||
|
|
||||||
|
// image icon style
|
||||||
|
.menu-item-icon {
|
||||||
|
width: 25px;
|
||||||
|
height: 25px;
|
||||||
|
margin: 0 10px;
|
||||||
|
object-fit: contain;
|
||||||
|
}
|
||||||
|
|
||||||
|
// name style
|
||||||
|
.menu-item-link {
|
||||||
|
color: #fff;
|
||||||
|
flex-grow: 1;
|
||||||
|
font-size: 1rem;
|
||||||
|
text-decoration: none;
|
||||||
|
transition: opacity 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
// toggle icon style
|
||||||
|
.toggle-icon {
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
margin-right: 10px;
|
||||||
|
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23b9d0ec' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='M5 14l6-6-6-6'/%3e%3c/svg%3e");
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position: center;
|
||||||
|
transition: transform 0.2s ease-in-out;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.submenu_list {
|
||||||
|
list-style: none;
|
||||||
|
padding: 5px 0;
|
||||||
|
|
||||||
|
li {
|
||||||
|
.menu-item-container {
|
||||||
|
margin-left: 8px;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: #eef2ff;
|
||||||
|
border-color: #151a2d;
|
||||||
|
|
||||||
|
.menu-item-link {
|
||||||
|
color: #151a2d;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.secondary-nav {
|
||||||
|
margin-top: auto;
|
||||||
|
// width: calc(100% - 30px);
|
||||||
|
background: #151a2d;
|
||||||
|
padding: 20px 15px;
|
||||||
|
|
||||||
|
.nav-item {
|
||||||
|
position: relative;
|
||||||
|
// margin: 0 15px;
|
||||||
|
|
||||||
|
&:hover > .nav-link {
|
||||||
|
color: #151a2d !important;
|
||||||
|
background-color: #eef2ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-link {
|
||||||
|
color: #fff !important;
|
||||||
|
display: flex;
|
||||||
|
gap: 12px;
|
||||||
|
white-space: nowrap;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 11px 15px;
|
||||||
|
align-items: center;
|
||||||
|
text-decoration: none;
|
||||||
|
border: 1px solid #151a2d;
|
||||||
|
transition: 0.4s ease;
|
||||||
|
|
||||||
|
.nav_icon {
|
||||||
|
font-size: 24px;
|
||||||
|
margin: 0 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.nav-label {
|
||||||
|
font-size: 1rem;
|
||||||
|
transition: opacity 0.3s ease;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown_menu {
|
||||||
|
height: 0;
|
||||||
|
overflow-y: hidden;
|
||||||
|
list-style: none;
|
||||||
|
padding-left: 15px;
|
||||||
|
transition: height 0.4s ease;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===================================
|
||||||
|
2. Hide Top Bar and Adjust Main Content
|
||||||
|
=================================== */
|
||||||
|
// Hide default application icon from top bar
|
||||||
|
.o_navbar .o_main_navbar .o_navbar_apps_menu .o-dropdown {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Hide submenus inside the top bar */
|
||||||
|
.o_main_navbar .o_menu_sections {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.o_web_client {
|
||||||
|
margin-left: 0 !important;
|
||||||
|
transition: all 0.3s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Adjust Top Bar and Main Content */
|
||||||
|
.o_navbar,
|
||||||
|
.o_action_manager {
|
||||||
|
transition: all 0.3s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Open Icon Design (New) */
|
||||||
|
.sidebar_toggle_icon {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
width: auto;
|
||||||
|
height: calc(var(--o-navbar-height) - 0px);
|
||||||
|
border-radius: 0;
|
||||||
|
user-select: none;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--NavBar-entry-color, rgba(255, 255, 255, 0.9));
|
||||||
|
font-size: 1.2em;
|
||||||
|
padding: 0 15px;
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,93 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<templates xml:space="preserve">
|
||||||
|
|
||||||
|
<t t-name="odex_sidebar_backend_theme2.MenuItem" owl="1">
|
||||||
|
<li t-att-class="{ 'has-children': props.menu.children.length > 0, 'open': props.menu.isOpen }">
|
||||||
|
|
||||||
|
<a href="#" class="menu-item-container" t-att-title="props.isCollapsed ? props.menu.name : ''">
|
||||||
|
|
||||||
|
<!-- Icon -->
|
||||||
|
<t t-if="props.menu.webIconData">
|
||||||
|
<img class="menu-item-icon" t-att-src="props.menu.webIconData" />
|
||||||
|
</t>
|
||||||
|
<t t-elif="props.menu.icon">
|
||||||
|
<i t-att-class="'menu-item-icon ' + props.menu.icon"></i>
|
||||||
|
</t>
|
||||||
|
|
||||||
|
<!-- Label (hidden when collapsed) -->
|
||||||
|
<a href="#" class="menu-item-link" t-if="!props.isCollapsed" t-on-click.prevent="() => this.selectMenu(props.menu)">
|
||||||
|
<t t-esc="props.menu.name" />
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<span t-if="props.menu.children.length > 0"
|
||||||
|
class="toggle-icon"
|
||||||
|
t-on-click="() => this.toggleMenu(props.menu, props.siblings)"/>
|
||||||
|
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<!-- FLYOUT (only when collapsed) -->
|
||||||
|
<t t-if="props.isCollapsed and props.menu.children.length > 0">
|
||||||
|
<div class="flyout-panel" role="menu">
|
||||||
|
<ul class="flyout-list">
|
||||||
|
<t t-foreach="props.menu.children" t-as="child" t-key="child.id">
|
||||||
|
|
||||||
|
<li class="flyout-item" t-att-class="{'has-children': child.children.length > 0}" role="menuitem">
|
||||||
|
|
||||||
|
<!-- child icon -->
|
||||||
|
<div class="icon">
|
||||||
|
<t t-if="child.webIconData">
|
||||||
|
<img t-att-src="child.webIconData" />
|
||||||
|
</t>
|
||||||
|
<t t-elif="child.icon">
|
||||||
|
<i t-att-class="'menu-item-icon ' + child.icon"></i>
|
||||||
|
</t>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- child label -->
|
||||||
|
<a href="#" class="label" t-on-click.prevent="() => this.selectMenu(child)">
|
||||||
|
<t t-esc="child.name" />
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<!-- SUBPANEL (submenu flyout) -->
|
||||||
|
<t t-if="child.children.length > 0">
|
||||||
|
<div class="flyout-subpanel" role="menu">
|
||||||
|
<ul class="flyout-list">
|
||||||
|
<t t-foreach="child.children" t-as="sub" t-key="sub.id">
|
||||||
|
|
||||||
|
<li class="flyout-item" role="menuitem">
|
||||||
|
<div class="icon">
|
||||||
|
<t t-if="sub.webIconData">
|
||||||
|
<img t-att-src="sub.webIconData" />
|
||||||
|
</t>
|
||||||
|
<t t-elif="sub.icon">
|
||||||
|
<i t-att-class="'menu-item-icon ' + sub.icon"></i>
|
||||||
|
</t>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<a href="#" class="label" t-on-click.prevent="() => this.selectMenu(sub)">
|
||||||
|
<t t-esc="sub.name" />
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
</t>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</t>
|
||||||
|
|
||||||
|
</li>
|
||||||
|
|
||||||
|
</t>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</t>
|
||||||
|
|
||||||
|
<!-- Normal submenu (when not collapsed) -->
|
||||||
|
<ul t-if="!props.isCollapsed and props.menu.isOpen and props.menu.children.length > 0" class="submenu_list">
|
||||||
|
<t t-foreach="props.menu.children" t-as="childMenu" t-key="childMenu.id">
|
||||||
|
<MenuItem menu="childMenu" siblings="props.menu.children" isCollapsed="props.isCollapsed" />
|
||||||
|
</t>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
</li>
|
||||||
|
</t>
|
||||||
|
</templates>
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<templates id="template" xml:space="preserve">
|
||||||
|
|
||||||
|
<t t-name="odex_sidebar_backend_theme2.NavBar" t-inherit="web.NavBar.AppsMenu" t-inherit-mode="extension" owl="1">
|
||||||
|
<xpath expr="//div[hasclass('o_navbar_apps_menu')]" position="inside">
|
||||||
|
<button class="sidebar_toggle_icon" title="Toggle Sidebar" t-on-click="toggleSidebar">
|
||||||
|
<i class="fa fa-bars"/>
|
||||||
|
</button>
|
||||||
|
</xpath>
|
||||||
|
|
||||||
|
<xpath expr="//a[hasclass('o_menu_toggle')]" position="replace">
|
||||||
|
<button class="sidebar_toggle_icon" title="Toggle Sidebar" t-on-click="toggleSidebar">
|
||||||
|
<i class="fa fa-bars"/>
|
||||||
|
</button>
|
||||||
|
</xpath>
|
||||||
|
</t>
|
||||||
|
|
||||||
|
</templates>
|
||||||
|
|
@ -0,0 +1,84 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<templates xml:space="preserve">
|
||||||
|
<t t-name="odex_sidebar_backend_theme2.SidebarMenu" owl="1">
|
||||||
|
<div class="custom_sidebar" t-att-class="{ 'is-open': state.isOpen, 'is-collapsed': state.isCollapsed }">
|
||||||
|
<!-- Sidebar Header -->
|
||||||
|
<header class="sidebar-header">
|
||||||
|
<a href="/" class="header-logo" t-att-title="state.isCollapsed ? 'Home' : ''">
|
||||||
|
<img src="/odex_sidebar_backend_theme2/static/src/img/logo1.png" alt="CodingNepal" />
|
||||||
|
</a>
|
||||||
|
<button class="sidebar-toggler" t-on-click="() => this.toggleCollapse()">
|
||||||
|
<span class="material-symbols-rounded">
|
||||||
|
<i t-att-class="state.isCollapsed ? 'fa fa-chevron-right' : 'fa fa-chevron-left'"></i>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
</header>
|
||||||
|
<nav class="sidebar-nav">
|
||||||
|
<!-- Primary Top Nav -->
|
||||||
|
<ul class="sidebar_menu_list primary-nav">
|
||||||
|
<t t-foreach="state.menus" t-as="app" t-key="app.id">
|
||||||
|
<MenuItem menu="app" siblings="state.menus" isCollapsed="state.isCollapsed"/>
|
||||||
|
</t>
|
||||||
|
</ul>
|
||||||
|
<!-- Secondary Bottom Nav -->
|
||||||
|
<ul class="sidebar_menu_list secondary-nav">
|
||||||
|
<li class="nav-item">
|
||||||
|
<a href="#" class="nav-link" t-att-title="state.isCollapsed ? 'Support' : ''">
|
||||||
|
<span class="nav_icon"><i class="fa fa-question-circle"></i></span>
|
||||||
|
<span class="nav-label">Support</span>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<!-- FLYOUT for Support (only when collapsed) -->
|
||||||
|
<t t-if="state.isCollapsed">
|
||||||
|
<div class="flyout-panel" role="menu">
|
||||||
|
<ul class="flyout-list">
|
||||||
|
<li class="flyout-item" role="menuitem">
|
||||||
|
<div class="icon">
|
||||||
|
<i class="fa fa-question-circle"></i>
|
||||||
|
</div>
|
||||||
|
<a href="https://www.odoo.com/buy" class="label">Support</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</t>
|
||||||
|
|
||||||
|
<!-- Normal dropdown (when not collapsed) -->
|
||||||
|
<ul class="dropdown_menu" t-if="!state.isCollapsed">
|
||||||
|
<li class="nav-item">
|
||||||
|
<a href="https://www.odoo.com/buy" class="nav-link dropdown-title">Support</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="nav-item">
|
||||||
|
<a href="#" class="nav-link" t-att-title="state.isCollapsed ? 'Sign Out' : ''" t-on-click.prevent="() => this.handleLogout()">
|
||||||
|
<span class="nav_icon"><i class="fa fa-sign-out"></i></span>
|
||||||
|
<span class="nav-label">Sign Out</span>
|
||||||
|
</a>
|
||||||
|
|
||||||
|
<!-- FLYOUT for Sign Out (only when collapsed) -->
|
||||||
|
<t t-if="state.isCollapsed">
|
||||||
|
<div class="flyout-panel" role="menu">
|
||||||
|
<ul class="flyout-list">
|
||||||
|
<li class="flyout-item" role="menuitem">
|
||||||
|
<div class="icon">
|
||||||
|
<i class="fa fa-sign-out"></i>
|
||||||
|
</div>
|
||||||
|
<a href="#" class="label" t-on-click.prevent="() => this.handleLogout()">Sign Out</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</t>
|
||||||
|
|
||||||
|
<!-- Normal dropdown (when not collapsed) -->
|
||||||
|
<ul class="dropdown_menu" t-if="!state.isCollapsed">
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link dropdown-title" href="#" t-on-click.prevent="() => this.handleLogout()">Sign Out</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
</t>
|
||||||
|
</templates>
|
||||||
Loading…
Reference in New Issue