Update 8 documentation files to reflect new URL scheme:
- Dev: /platforms/{code}/storefront/{store_code}/
- Prod: subdomain.platform.lu/ (root path = storefront)
- Rename DEFAULT_PLATFORM_CODE to MAIN_PLATFORM_CODE
- Replace hardcoded platform_id=1 with dynamic values
- Update route examples, middleware descriptions, code samples
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
494 lines
18 KiB
Markdown
494 lines
18 KiB
Markdown
# 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 %}
|
|
<a href="{{ item.url }}">{{ item.label }}</a>
|
|
{% 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
|