Replace all ~1,086 occurrences of Wizamart/wizamart/WIZAMART/WizaMart with Orion/orion/ORION across 184 files. This includes database identifiers, email addresses, domain references, R2 bucket names, DNS prefixes, encryption salt, Celery app name, config defaults, Docker configs, CI configs, documentation, seed data, and templates. Renames homepage-wizamart.html template to homepage-orion.html. Fixes duplicate file_pattern key in api.yaml architecture rule. 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=1,
|
|
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=1,
|
|
)
|
|
```
|
|
|
|
### 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=1,
|
|
menu_item_id="inventory",
|
|
is_visible=False
|
|
)
|
|
|
|
# Platform "OMS" hides letzshop from store dashboard
|
|
AdminMenuConfig(
|
|
frontend_type=FrontendType.STORE,
|
|
platform_id=1,
|
|
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
|