diff --git a/odex30_base/odex_sidebar_backend_theme2/ __init__.py b/odex30_base/odex_sidebar_backend_theme2/ __init__.py new file mode 100644 index 0000000..7c68785 --- /dev/null +++ b/odex30_base/odex_sidebar_backend_theme2/ __init__.py @@ -0,0 +1 @@ +# -*- coding: utf-8 -*- \ No newline at end of file diff --git a/odex30_base/odex_sidebar_backend_theme2/__manifest__.py b/odex30_base/odex_sidebar_backend_theme2/__manifest__.py new file mode 100644 index 0000000..ba61d13 --- /dev/null +++ b/odex30_base/odex_sidebar_backend_theme2/__manifest__.py @@ -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', +} diff --git a/odex30_base/odex_sidebar_backend_theme2/static/src/img/logo1.png b/odex30_base/odex_sidebar_backend_theme2/static/src/img/logo1.png new file mode 100644 index 0000000..cd74056 Binary files /dev/null and b/odex30_base/odex_sidebar_backend_theme2/static/src/img/logo1.png differ diff --git a/odex30_base/odex_sidebar_backend_theme2/static/src/js/menu_item.js b/odex30_base/odex_sidebar_backend_theme2/static/src/js/menu_item.js new file mode 100644 index 0000000..017214d --- /dev/null +++ b/odex30_base/odex_sidebar_backend_theme2/static/src/js/menu_item.js @@ -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); + } + } +} diff --git a/odex30_base/odex_sidebar_backend_theme2/static/src/js/navbar_patch.js b/odex30_base/odex_sidebar_backend_theme2/static/src/js/navbar_patch.js new file mode 100644 index 0000000..b9fef6a --- /dev/null +++ b/odex30_base/odex_sidebar_backend_theme2/static/src/js/navbar_patch.js @@ -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"); + }, +}); + + + + diff --git a/odex30_base/odex_sidebar_backend_theme2/static/src/js/sidebar_menu.js b/odex30_base/odex_sidebar_backend_theme2/static/src/js/sidebar_menu.js new file mode 100644 index 0000000..4e785b1 --- /dev/null +++ b/odex30_base/odex_sidebar_backend_theme2/static/src/js/sidebar_menu.js @@ -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, +}); diff --git a/odex30_base/odex_sidebar_backend_theme2/static/src/scss/sidebar_menu.scss b/odex30_base/odex_sidebar_backend_theme2/static/src/scss/sidebar_menu.scss new file mode 100644 index 0000000..365569d --- /dev/null +++ b/odex30_base/odex_sidebar_backend_theme2/static/src/scss/sidebar_menu.scss @@ -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; +} \ No newline at end of file diff --git a/odex30_base/odex_sidebar_backend_theme2/static/src/xml/menu_item_template.xml b/odex30_base/odex_sidebar_backend_theme2/static/src/xml/menu_item_template.xml new file mode 100644 index 0000000..f993b67 --- /dev/null +++ b/odex30_base/odex_sidebar_backend_theme2/static/src/xml/menu_item_template.xml @@ -0,0 +1,93 @@ + + + + +
  • + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
  • +
    +
    diff --git a/odex30_base/odex_sidebar_backend_theme2/static/src/xml/navbar_patch.xml b/odex30_base/odex_sidebar_backend_theme2/static/src/xml/navbar_patch.xml new file mode 100644 index 0000000..055aeb6 --- /dev/null +++ b/odex30_base/odex_sidebar_backend_theme2/static/src/xml/navbar_patch.xml @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + diff --git a/odex30_base/odex_sidebar_backend_theme2/static/src/xml/sidebar_menu_template.xml b/odex30_base/odex_sidebar_backend_theme2/static/src/xml/sidebar_menu_template.xml new file mode 100644 index 0000000..18ec6b1 --- /dev/null +++ b/odex30_base/odex_sidebar_backend_theme2/static/src/xml/sidebar_menu_template.xml @@ -0,0 +1,84 @@ + + + +
    + + + +
    +
    +