# Menu Management Architecture The Orion platform provides a **module-driven menu system** where each module defines its own menu items. The `MenuDiscoveryService` aggregates menus from all enabled modules, applying visibility configuration and permission filtering. ## Overview ``` ┌─────────────────────────────────────────────────────────────────────────┐ │ MODULE DEFINITIONS (Source of Truth) │ │ app/modules/*/definition.py │ │ │ │ Each module defines its menu items per FrontendType: │ │ ┌─────────────────────────────┐ ┌─────────────────────────────┐ │ │ │ catalog.definition.py │ │ orders.definition.py │ │ │ │ menus={ADMIN: [...], │ │ menus={ADMIN: [...], │ │ │ │ STORE: [...]} │ │ STORE: [...]} │ │ │ └─────────────────────────────┘ └─────────────────────────────┘ │ └─────────────────────────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────────────────┐ │ MENU DISCOVERY SERVICE │ │ app/modules/core/services/menu_discovery_service.py │ │ │ │ ┌─────────────────────────────────────────────────────────────────┐ │ │ │ 1. Collect menu items from all enabled modules │ │ │ │ 2. Filter by user permissions (super_admin_only) │ │ │ │ 3. Apply visibility overrides (AdminMenuConfig) │ │ │ │ 4. Sort by section/item order │ │ │ └─────────────────────────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────────────────┐ │ VISIBILITY CONFIGURATION │ │ app/modules/core/models/admin_menu_config.py │ │ │ │ ┌─────────────────────────────────────────────────────────────────┐ │ │ │ AdminMenuConfig Table │ │ │ │ Stores visibility overrides (hidden items only) │ │ │ │ - Platform scope: applies to platform admins/stores │ │ │ │ - User scope: applies to specific super admin │ │ │ └─────────────────────────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────────────────────────┐ │ FILTERED MENU OUTPUT │ │ │ │ Module Menus - Disabled Modules - Hidden Items = Visible Menu │ └─────────────────────────────────────────────────────────────────────────┘ ``` ## Frontend Types The system supports four distinct frontend types: | Frontend | Description | Users | |----------|-------------|-------| | `PLATFORM` | Public marketing pages | Unauthenticated visitors | | `ADMIN` | Admin panel | Super admins, platform admins | | `STORE` | Store dashboard | Stores on a platform | | `STOREFRONT` | Customer-facing shop | Shop customers | ```python from app.modules.enums import FrontendType # Use in code FrontendType.PLATFORM # "platform" FrontendType.ADMIN # "admin" FrontendType.STORE # "store" FrontendType.STOREFRONT # "storefront" ``` ## Module-Driven Menus Each module defines its menu items in `definition.py` using dataclasses: ### Menu Item Definition ```python # app/modules/base.py @dataclass class MenuItemDefinition: """Single menu item definition.""" id: str # Unique identifier (e.g., "catalog.products") label_key: str # i18n key for label icon: str # Lucide icon name route: str # URL path order: int = 100 # Sort order within section is_mandatory: bool = False # Cannot be hidden by user is_super_admin_only: bool = False # Only visible to super admins @dataclass class MenuSectionDefinition: """Section containing menu items.""" id: str # Section identifier label_key: str # i18n key for section label icon: str # Section icon order: int = 100 # Sort order items: list[MenuItemDefinition] = field(default_factory=list) is_super_admin_only: bool = False ``` ### Example Module Definition ```python # app/modules/catalog/definition.py from app.modules.base import ModuleDefinition, MenuSectionDefinition, MenuItemDefinition from app.modules.enums import FrontendType catalog_module = ModuleDefinition( code="catalog", name="Product Catalog", description="Product and category management", version="1.0.0", is_core=True, menus={ FrontendType.ADMIN: [ MenuSectionDefinition( id="catalog", label_key="menu.catalog", icon="package", order=30, items=[ MenuItemDefinition( id="products", label_key="menu.products", icon="box", route="/admin/products", order=10, is_mandatory=True ), MenuItemDefinition( id="categories", label_key="menu.categories", icon="folder-tree", route="/admin/categories", order=20 ), ] ) ], FrontendType.STORE: [ MenuSectionDefinition( id="products", label_key="menu.my_products", icon="package", order=10, items=[ MenuItemDefinition( id="products", label_key="menu.products", icon="box", route="/store/{store_code}/products", order=10, is_mandatory=True ), ] ) ], } ) ``` ## Menu Discovery Service The `MenuDiscoveryService` aggregates menus from all enabled modules: ```python from app.modules.core.services.menu_discovery_service import menu_discovery_service # Get menu for a frontend type sections = menu_discovery_service.get_menu_for_frontend( db=db, frontend_type=FrontendType.ADMIN, platform_id=platform.id, user_id=current_user.id, is_super_admin=current_user.is_super_admin, ) # Get all menu items for configuration UI all_items = menu_discovery_service.get_all_menu_items( db=db, frontend_type=FrontendType.ADMIN, platform_id=platform.id, ) ``` ### Discovery Flow 1. **Collect**: Get menu definitions from all modules in `MODULES` registry 2. **Filter by Module**: Only include menus from enabled modules for the platform 3. **Filter by Permissions**: Remove `super_admin_only` items for non-super admins 4. **Apply Visibility**: Check `AdminMenuConfig` for hidden items 5. **Sort**: Order sections and items by their `order` field 6. **Return**: List of `MenuSectionDefinition` with filtered items ## Visibility Configuration ### Opt-Out Model The system uses an **opt-out model**: - All menu items are **visible by default** - Only **hidden** items are stored in the database - This keeps the database small and makes the default state explicit ### Mandatory Items Certain menu items cannot be hidden. These are marked with `is_mandatory=True` in their definition: ```python MenuItemDefinition( id="dashboard", label_key="menu.dashboard", icon="home", route="/admin/dashboard", is_mandatory=True, # Cannot be hidden ) ``` ### Scope Types Menu configuration supports two scopes: | Scope | Field | Description | Use Case | |-------|-------|-------------|----------| | Platform | `platform_id` | Applies to all users on platform | Hide features not used by platform | | User | `user_id` | Applies to specific super admin | Personal preference customization | **Important Rules:** - Exactly one scope must be set (platform XOR user) - User scope is only allowed for admin frontend (super admins only) - Store frontend only supports platform scope ### Resolution Order **Admin Frontend:** ``` Platform admin → Check platform config → Fall back to default (all visible) Super admin → Check user config → Fall back to default (all visible) ``` **Store Frontend:** ``` Store → Check platform config → Fall back to default (all visible) ``` ## Database Model ### AdminMenuConfig Table ```sql CREATE TABLE admin_menu_configs ( id SERIAL PRIMARY KEY, frontend_type VARCHAR(10) NOT NULL, -- 'admin' or 'store' platform_id INTEGER REFERENCES platforms(id), user_id INTEGER REFERENCES users(id), menu_item_id VARCHAR(50) NOT NULL, is_visible BOOLEAN NOT NULL DEFAULT TRUE, created_at TIMESTAMP, updated_at TIMESTAMP, -- Constraints CONSTRAINT uq_frontend_platform_menu_config UNIQUE (frontend_type, platform_id, menu_item_id), CONSTRAINT uq_frontend_user_menu_config UNIQUE (frontend_type, user_id, menu_item_id), CONSTRAINT ck_admin_menu_config_scope CHECK ((platform_id IS NOT NULL AND user_id IS NULL) OR (platform_id IS NULL AND user_id IS NOT NULL)), CONSTRAINT ck_user_scope_admin_only CHECK ((user_id IS NULL) OR (frontend_type = 'admin')) ); ``` ### Examples ```python from app.modules.core.models import AdminMenuConfig from app.modules.enums import FrontendType # Platform "OMS" hides inventory from admin panel AdminMenuConfig( frontend_type=FrontendType.ADMIN, platform_id=oms_platform.id, # OMS platform menu_item_id="inventory", is_visible=False ) # Platform "OMS" hides letzshop from store dashboard AdminMenuConfig( frontend_type=FrontendType.STORE, platform_id=oms_platform.id, # OMS platform menu_item_id="letzshop", is_visible=False ) # Super admin "john" hides code-quality from their admin panel AdminMenuConfig( frontend_type=FrontendType.ADMIN, user_id=5, menu_item_id="code-quality", is_visible=False ) ``` ## Module Integration ### Automatic Menu Discovery When a module is enabled/disabled for a platform, the menu discovery service automatically includes/excludes its menu items: ```python # Module enablement check happens automatically in MenuDiscoveryService enabled_modules = module_service.get_enabled_module_codes(db, platform_id) # Only modules in enabled_modules have their menus included for module_code, module_def in MODULES.items(): if module_code in enabled_modules: # Include this module's menus pass ``` ### Three-Layer Filtering The final visible menu is computed by three layers: 1. **Module Definitions**: All possible menu items from modules 2. **Module Enablement**: Items from disabled modules are hidden 3. **Visibility Config**: Explicitly hidden items are removed ``` Final Menu = Module Menu Items - Items from Disabled Modules - Items with is_visible=False in config - Items not matching user role (super_admin_only) ``` ## Menu Service Integration The `MenuService` provides the interface used by templates: ```python from app.modules.core.services import menu_service # Get menu for rendering in templates menu_data = menu_service.get_menu_for_rendering( db=db, frontend_type=FrontendType.ADMIN, platform_id=platform_id, user_id=user_id, is_super_admin=is_super_admin, store_code=store_code, # For store frontend ) # Returns legacy format for template compatibility: # { # "frontend_type": "admin", # "sections": [ # { # "id": "main", # "label": None, # "items": [{"id": "dashboard", "label": "Dashboard", ...}] # }, # ... # ] # } ``` ## Access Control ### Route Protection ```python from app.api.deps import require_menu_access @router.get("/admin/inventory") async def inventory_page( _access: bool = Depends(require_menu_access("inventory", FrontendType.ADMIN)) ): # Only accessible if menu item is visible for user's context pass ``` ### Sidebar Rendering The sidebar template filters items based on: 1. Module enablement 2. Visibility configuration 3. User role (super admin check) ```jinja2 {% for section in menu_sections %} {% if not section.super_admin_only or current_user.is_super_admin %} {% for item in section.items %} {% if item.id in visible_menu_items %} {{ item.label }} {% endif %} {% endfor %} {% endif %} {% endfor %} ``` ## UI for Menu Configuration ### Platform Admin Menu Config Located at `/admin/platform-menu-config` (accessible by super admins): - Configure which menu items are visible for platform admins - Configure which menu items are visible for stores on this platform - Mandatory items cannot be unchecked ### Personal Menu Config (Super Admins) Located at `/admin/my-menu`: - Super admins can customize their own admin panel menu - Personal preferences that don't affect other users - Useful for hiding rarely-used features ## Adding Menu Items to a Module 1. **Define menu sections and items** in your module's `definition.py`: ```python # app/modules/mymodule/definition.py from app.modules.base import ModuleDefinition, MenuSectionDefinition, MenuItemDefinition from app.modules.enums import FrontendType mymodule = ModuleDefinition( code="mymodule", name="My Module", description="Description of my module", version="1.0.0", menus={ FrontendType.ADMIN: [ MenuSectionDefinition( id="mymodule", label_key="mymodule.menu.section", icon="star", order=50, # Position in sidebar items=[ MenuItemDefinition( id="mymodule-main", label_key="mymodule.menu.main", icon="star", route="/admin/mymodule", order=10, ), ] ) ], } ) ``` 2. **Add translation keys** in your module's locales: ```json // app/modules/mymodule/locales/en.json { "mymodule.menu.section": "My Module", "mymodule.menu.main": "Main Page" } ``` 3. **The menu is automatically discovered** - no registration needed. ## Best Practices ### Do - Define menus in module `definition.py` using dataclasses - Use translation keys (`label_key`) for labels - Set appropriate `order` values for positioning - Mark essential items as `is_mandatory=True` - Use `is_super_admin_only=True` for admin-only features ### Don't - Hardcode menu item labels (use i18n keys) - Store `is_visible=True` in database (default state, wastes space) - Allow hiding mandatory items via API - Create menu items outside of module definitions ## Related Documentation - [Module System](module-system.md) - Module architecture and definitions - [Multi-Tenant System](multi-tenant.md) - Platform isolation - [Feature Gating](../implementation/feature-gating-system.md) - Tier-based access