Add custom sidebar theme with menu and navbar integration
This commit is contained in:
parent
b3d19b3e3b
commit
fb0594957c
|
|
@ -0,0 +1 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
|
@ -0,0 +1,32 @@
|
||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
{
|
||||||
|
'name': 'Custom sidebar',
|
||||||
|
'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_theme/static/src/scss/sidebar_menu.scss',
|
||||||
|
|
||||||
|
# 'odex_sidebar_backend_theme/static/src/xml/sidebar_menu_template.xml',
|
||||||
|
# 'odex_sidebar_backend_theme/static/src/xml/menu_item_template.xml',
|
||||||
|
|
||||||
|
'odex_sidebar_backend_theme/static/src/js/sidebar_menu.js',
|
||||||
|
'odex_sidebar_backend_theme/static/src/js/menu_item.js',
|
||||||
|
|
||||||
|
'odex_sidebar_backend_theme/static/src/xml/navbar_patch.xml',
|
||||||
|
'odex_sidebar_backend_theme/static/src/js/navbar_patch.js',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
'installable': True,
|
||||||
|
'application': False,
|
||||||
|
'auto_install': False,
|
||||||
|
'license': 'LGPL-3',
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,56 @@
|
||||||
|
/** @odoo-module **/
|
||||||
|
|
||||||
|
import { Component, xml } from "@odoo/owl";
|
||||||
|
import { useService } from "@web/core/utils/hooks";
|
||||||
|
|
||||||
|
export class MenuItem extends Component {
|
||||||
|
static template = xml`
|
||||||
|
<li t-att-class="{ 'has-children': props.menu.children.length > 0, 'open': props.menu.isOpen }">
|
||||||
|
<div class="menu-item-container">
|
||||||
|
<a href="#" class="menu-item-link" 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)"/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ul t-if="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"/>
|
||||||
|
</t>
|
||||||
|
</ul>
|
||||||
|
</li>`
|
||||||
|
|
||||||
|
static components = { MenuItem };
|
||||||
|
|
||||||
|
// 2. نحدد الـ props التي يستقبلها المكون
|
||||||
|
static props = {
|
||||||
|
menu: { type: Object },
|
||||||
|
siblings: { type: Array },
|
||||||
|
};
|
||||||
|
|
||||||
|
setup() {
|
||||||
|
this.menuService = useService("menu");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. تعديل دالة toggleMenu لتتعامل مع الأشقاء
|
||||||
|
toggleMenu(menu, siblings) {
|
||||||
|
const wasOpen = menu.isOpen; // نحفظ الحالة القديمة
|
||||||
|
|
||||||
|
// الخطوة أ: إغلاق جميع القوائم الشقيقة
|
||||||
|
for (const sibling of siblings) {
|
||||||
|
sibling.isOpen = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// الخطوة ب: فتح القائمة الحالية فقط إذا كانت مغلقة في الأصل
|
||||||
|
// هذا يمنعها من إعادة الفتح فورًا ويسمح بسلوك الإغلاق عند النقر عليها مرة أخرى
|
||||||
|
if (!wasOpen) {
|
||||||
|
menu.isOpen = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
selectMenu(menu) {
|
||||||
|
this.menuService.selectMenu(menu);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,25 @@
|
||||||
|
/** @odoo-module **/
|
||||||
|
|
||||||
|
// **العودة إلى استيراد NavBar**
|
||||||
|
import { NavBar } from "@web/webclient/navbar/navbar";
|
||||||
|
import { patch } from "@web/core/utils/patch";
|
||||||
|
import { useService } from "@web/core/utils/hooks";
|
||||||
|
|
||||||
|
|
||||||
|
// **العودة إلى عمل patch لمكون NavBar**
|
||||||
|
patch(NavBar.prototype, {
|
||||||
|
setup() {
|
||||||
|
super.setup(...arguments); // مهم جدًا
|
||||||
|
// استدعاء خدمة dialog
|
||||||
|
this.busService = useService("bus_service");
|
||||||
|
},
|
||||||
|
|
||||||
|
// 3. إضافة دالة لإرسال الحدث
|
||||||
|
toggleSidebar() {
|
||||||
|
this.busService.trigger("toggle-sidebar");
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -0,0 +1,103 @@
|
||||||
|
/** @odoo-module **/
|
||||||
|
|
||||||
|
import { Component, onMounted, useEffect, useState, xml } 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 = xml`
|
||||||
|
<div class="custom_sidebar" t-att-class="{ 'is-open': state.isOpen }">
|
||||||
|
<div class="custom_icon_close" t-on-click="toggleSidebar">
|
||||||
|
<i class="fa fa-close"/>
|
||||||
|
</div>
|
||||||
|
<ul class="sidebar_menu_list">
|
||||||
|
<t t-foreach="state.menus" t-as="app" t-key="app.id">
|
||||||
|
<MenuItem menu="app" siblings="state.menus"/>
|
||||||
|
</t>
|
||||||
|
</ul>
|
||||||
|
</div>`
|
||||||
|
;
|
||||||
|
|
||||||
|
static components = { MenuItem };
|
||||||
|
|
||||||
|
setup() {
|
||||||
|
this.menuService = useService("menu");
|
||||||
|
this.busService = useService("bus_service");
|
||||||
|
|
||||||
|
this.state = useState({
|
||||||
|
menus: [],
|
||||||
|
isOpen: false
|
||||||
|
});
|
||||||
|
|
||||||
|
// =================== التحكم عبر JavaScript يبدأ هنا ===================
|
||||||
|
|
||||||
|
// const applyLayoutChanges = (isOpen) => {
|
||||||
|
// const actionManager = document.querySelector('.o_action_manager');
|
||||||
|
// const mainNavbar = document.querySelector('.o_navbar');
|
||||||
|
// const sidebarWidth = "250px";
|
||||||
|
|
||||||
|
// if (isOpen) {
|
||||||
|
// if (actionManager) actionManager.style.marginLeft = sidebarWidth;
|
||||||
|
// if (mainNavbar) mainNavbar.style.marginLeft = sidebarWidth;
|
||||||
|
// } else {
|
||||||
|
// if (actionManager) actionManager.style.marginLeft = '0';
|
||||||
|
// if (mainNavbar) mainNavbar.style.marginLeft = '0';
|
||||||
|
// }
|
||||||
|
// };
|
||||||
|
|
||||||
|
// // 2. استخدام useEffect لمراقبة التغييرات في state.isOpen
|
||||||
|
// useEffect(
|
||||||
|
// (isOpen) => {
|
||||||
|
// // هذا الكود سيعمل في كل مرة تتغير فيها قيمة isOpen
|
||||||
|
// applyLayoutChanges(isOpen);
|
||||||
|
// },
|
||||||
|
// () => [this.state.isOpen] // <-- هذا هو الجزء الحاسم: نطلب من useEffect مراقبة هذه القيمة
|
||||||
|
// );
|
||||||
|
|
||||||
|
// ====================================================================
|
||||||
|
|
||||||
|
// 1. عرّف الدالة كمتغير داخل setup
|
||||||
|
const toggleSidebar = () => {
|
||||||
|
// الآن `this` مضمون أنه يشير إلى نسخة المكون التي تم إنشاؤها بواسطة setup
|
||||||
|
this.state.isOpen = !this.state.isOpen;
|
||||||
|
// this.busService.trigger("sidebar-state-changed", this.state.isOpen);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 2. اربطها بـ this حتى يمكن الوصول إليها من أماكن أخرى إذا لزم الأمر
|
||||||
|
this.toggleSidebar = toggleSidebar;
|
||||||
|
// ===============================================
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
this.loadMenus();
|
||||||
|
this.busService.addEventListener("toggle-sidebar", this.toggleSidebar);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
loadMenus() {
|
||||||
|
const allMenus = this.menuService.getAll();
|
||||||
|
const clonedMenus = structuredClone(allMenus);
|
||||||
|
const menuMap = new Map(clonedMenus.map(menu => [menu.id, menu]));
|
||||||
|
let finalMenuTree = [];
|
||||||
|
|
||||||
|
for (const menu of clonedMenus) {
|
||||||
|
if (menu.children) {
|
||||||
|
menu.children = menu.children.map(childId => 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.state.menus.splice(0, this.state.menus.length, ...finalMenuTree);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
registry.category("main_components").add("sidebar_menu_component", {
|
||||||
|
Component: SidebarMenu,
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,173 @@
|
||||||
|
/* ===================================
|
||||||
|
1. تصميم الـ Sidebar الرئيسي
|
||||||
|
=================================== */
|
||||||
|
.custom_sidebar {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 250px;
|
||||||
|
height: 100%;
|
||||||
|
background-color: #2f3542 !important;
|
||||||
|
overflow-y: auto;
|
||||||
|
z-index: 999;
|
||||||
|
padding-top: 60px;
|
||||||
|
transform: translateX(-100%);
|
||||||
|
transition: transform 0.3s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom_sidebar .custom_icon_close {
|
||||||
|
color: #fff;
|
||||||
|
position: absolute;
|
||||||
|
top: 8px;
|
||||||
|
right: 30px;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* عند إضافة كلاس is-open، تظهر القائمة */
|
||||||
|
.custom_sidebar.is-open {
|
||||||
|
transform: translateX(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// اخقاء الايقون الافتراضية للابلكيشن من الشريط العلوي
|
||||||
|
.o_navbar .o_main_navbar .o_navbar_apps_menu .o-dropdown {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===================================
|
||||||
|
2. إخفاء الشريط العلوي وتعديل المحتوى الرئيسي
|
||||||
|
=================================== */
|
||||||
|
.o_web_client {
|
||||||
|
margin-left: 0 !important;
|
||||||
|
transition: all 0.3s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* تعديل عرض الشريط العلوي والمحتوى الرئيسي */
|
||||||
|
.o_navbar,
|
||||||
|
.o_action_manager {
|
||||||
|
transition: all 0.3s ease-in-out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* إخفاء القوائم داخل الشريط العلوي */
|
||||||
|
.o_main_navbar .o_menu_sections {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===================================
|
||||||
|
3. تصميم عناصر القائمة (Accordion)
|
||||||
|
=================================== */
|
||||||
|
|
||||||
|
/* تصميم حاوية عنصر القائمة الرئيسي */
|
||||||
|
.custom_sidebar .menu-item-container {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
cursor: pointer;
|
||||||
|
border-bottom: 1px solid rgba(255, 255, 255, 0.18);
|
||||||
|
transition: background-color 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom_sidebar .sidebar_menu_list {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* تصميم رابط القائمة الرئيسي */
|
||||||
|
.custom_sidebar .menu-item-container .menu-item-link {
|
||||||
|
flex-grow: 1;
|
||||||
|
padding: 13px 9px;
|
||||||
|
color: #b9d0ec;
|
||||||
|
font-size: 15px;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* تغيير اللون عند الفتح (عندما يكون للعنصر li كلاس 'open') */
|
||||||
|
.custom_sidebar li.open > .menu-item-container {
|
||||||
|
background: #45ae54;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom_sidebar li.open > .menu-item-container .menu-item-link {
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* تصميم أيقونة الفتح/الإغلاق (السهم) */
|
||||||
|
.custom_sidebar .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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* تغيير لون السهم عند الفتح */
|
||||||
|
.custom_sidebar li.open > .menu-item-container .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" );
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===================================
|
||||||
|
4. تصميم القوائم الفرعية
|
||||||
|
=================================== */
|
||||||
|
.custom_sidebar .submenu_list {
|
||||||
|
list-style: none;
|
||||||
|
padding: 5px 0;
|
||||||
|
margin: 0;
|
||||||
|
background-color: rgba(0, 0, 0, 0.2); /* خلفية داكنة قليلاً للقوائم الفرعية */
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom_sidebar .submenu_list li .menu-item-link {
|
||||||
|
font-size: 14px;
|
||||||
|
padding-left: 25px; /* مسافة بادئة أكبر لتمييز المستوى */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* تصميم خاص بعناصر المستوى الأخير (التي لا تفتح قوائم أخرى) */
|
||||||
|
.custom_sidebar .submenu_list li:not(.has-children) .menu-item-link {
|
||||||
|
color: #fff;
|
||||||
|
transition: all 0.3s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom_sidebar .submenu_list li:not(.has-children) .menu-item-link:hover {
|
||||||
|
background-color: #193658;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===================================
|
||||||
|
5. تصميم شريط التمرير (Scrollbar)
|
||||||
|
=================================== */
|
||||||
|
.custom_sidebar::-webkit-scrollbar {
|
||||||
|
width: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom_sidebar::-webkit-scrollbar-track {
|
||||||
|
background: #2f3542; /* خلفية شريط التمرير بنفس لون الـ Sidebar */
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom_sidebar::-webkit-scrollbar-thumb {
|
||||||
|
background-color: #45ae54;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.custom_sidebar::-webkit-scrollbar-thumb:hover {
|
||||||
|
background-color: #5bc569; /* لون أفتح قليلاً عند المرور */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ===================================
|
||||||
|
6. تصميم أيقونة الفتح (جديد)
|
||||||
|
=================================== */
|
||||||
|
.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,23 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<templates xml:space="preserve">
|
||||||
|
|
||||||
|
<t t-name="odex_sidebar_backend_theme.MenuItem" owl="1">
|
||||||
|
<li t-att-class="{ 'has-children': props.menu.children.length > 0, 'open': props.menu.isOpen }">
|
||||||
|
<div class="menu-item-container">
|
||||||
|
<a href="#" class="menu-item-link" 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)"/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ul t-if="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"/>
|
||||||
|
</t>
|
||||||
|
</ul>
|
||||||
|
</li>
|
||||||
|
</t>
|
||||||
|
|
||||||
|
</templates>
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<templates id="template" xml:space="preserve">
|
||||||
|
|
||||||
|
<t t-name="odex_sidebar_backend_theme.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>
|
||||||
|
</t>
|
||||||
|
|
||||||
|
</templates>
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<templates xml:space="preserve">
|
||||||
|
<t t-name="odex_sidebar_backend_theme.SidebarMenu" owl="1">
|
||||||
|
<!-- تأكد من أن هذا الكلاس الديناميكي موجود -->
|
||||||
|
<div class="custom_sidebar" t-att-class="{ 'is-open': state.isOpen }">
|
||||||
|
<ul class="sidebar_menu_list">
|
||||||
|
<t t-foreach="state.menus" t-as="app" t-key="app.id">
|
||||||
|
<MenuItem menu="app" siblings="state.menus"/>
|
||||||
|
</t>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</t>
|
||||||
|
</templates>
|
||||||
Loading…
Reference in New Issue