refactor: complete module-driven architecture migration
This commit completes the migration to a fully module-driven architecture: ## Models Migration - Moved all domain models from models/database/ to their respective modules: - tenancy: User, Admin, Vendor, Company, Platform, VendorDomain, etc. - cms: MediaFile, VendorTheme - messaging: Email, VendorEmailSettings, VendorEmailTemplate - core: AdminMenuConfig - models/database/ now only contains Base and TimestampMixin (infrastructure) ## Schemas Migration - Moved all domain schemas from models/schema/ to their respective modules: - tenancy: company, vendor, admin, team, vendor_domain - cms: media, image, vendor_theme - messaging: email - models/schema/ now only contains base.py and auth.py (infrastructure) ## Routes Migration - Moved admin routes from app/api/v1/admin/ to modules: - menu_config.py -> core module - modules.py -> tenancy module - module_config.py -> tenancy module - app/api/v1/admin/ now only aggregates auto-discovered module routes ## Menu System - Implemented module-driven menu system with MenuDiscoveryService - Extended FrontendType enum: PLATFORM, ADMIN, VENDOR, STOREFRONT - Added MenuItemDefinition and MenuSectionDefinition dataclasses - Each module now defines its own menu items in definition.py - MenuService integrates with MenuDiscoveryService for template rendering ## Documentation - Updated docs/architecture/models-structure.md - Updated docs/architecture/menu-management.md - Updated architecture validation rules for new exceptions ## Architecture Validation - Updated MOD-019 rule to allow base.py in models/schema/ - Created core module exceptions.py and schemas/ directory - All validation errors resolved (only warnings remain) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -47,6 +47,93 @@ if TYPE_CHECKING:
|
||||
from app.modules.enums import FrontendType
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# Menu Item Definitions
|
||||
# =============================================================================
|
||||
|
||||
|
||||
@dataclass
|
||||
class MenuItemDefinition:
|
||||
"""
|
||||
Definition of a single menu item within a section.
|
||||
|
||||
Attributes:
|
||||
id: Unique identifier (e.g., "catalog.products", "orders.list")
|
||||
label_key: i18n key for the menu item label
|
||||
icon: Lucide icon name (e.g., "box", "shopping-cart")
|
||||
route: URL path (can include placeholders like {vendor_code})
|
||||
order: Sort order within section (lower = higher priority)
|
||||
is_mandatory: If True, cannot be hidden by user preferences
|
||||
requires_permission: Permission code required to see this item
|
||||
badge_source: Key for dynamic badge count (e.g., "pending_orders_count")
|
||||
is_super_admin_only: Only visible to super admins (admin frontend only)
|
||||
|
||||
Example:
|
||||
MenuItemDefinition(
|
||||
id="catalog.products",
|
||||
label_key="catalog.menu.products",
|
||||
icon="box",
|
||||
route="/admin/catalog/products",
|
||||
order=10,
|
||||
is_mandatory=True
|
||||
)
|
||||
"""
|
||||
|
||||
id: str
|
||||
label_key: str
|
||||
icon: str
|
||||
route: str
|
||||
order: int = 100
|
||||
is_mandatory: bool = False
|
||||
requires_permission: str | None = None
|
||||
badge_source: str | None = None
|
||||
is_super_admin_only: bool = False
|
||||
|
||||
|
||||
@dataclass
|
||||
class MenuSectionDefinition:
|
||||
"""
|
||||
Definition of a menu section containing related menu items.
|
||||
|
||||
Sections group related menu items together in the sidebar.
|
||||
A section can be collapsed/expanded by the user.
|
||||
|
||||
Attributes:
|
||||
id: Unique section identifier (e.g., "catalog", "orders")
|
||||
label_key: i18n key for section header (None for headerless sections)
|
||||
icon: Lucide icon name for section (optional)
|
||||
order: Sort order among sections (lower = higher priority)
|
||||
items: List of menu items in this section
|
||||
is_super_admin_only: Only visible to super admins
|
||||
is_collapsible: Whether section can be collapsed
|
||||
|
||||
Example:
|
||||
MenuSectionDefinition(
|
||||
id="catalog",
|
||||
label_key="catalog.menu.section",
|
||||
icon="package",
|
||||
order=20,
|
||||
items=[
|
||||
MenuItemDefinition(
|
||||
id="catalog.products",
|
||||
label_key="catalog.menu.products",
|
||||
icon="box",
|
||||
route="/admin/catalog/products",
|
||||
order=10
|
||||
),
|
||||
]
|
||||
)
|
||||
"""
|
||||
|
||||
id: str
|
||||
label_key: str | None
|
||||
icon: str | None = None
|
||||
order: int = 100
|
||||
items: list[MenuItemDefinition] = field(default_factory=list)
|
||||
is_super_admin_only: bool = False
|
||||
is_collapsible: bool = True
|
||||
|
||||
|
||||
@dataclass
|
||||
class ScheduledTask:
|
||||
"""
|
||||
@@ -190,6 +277,14 @@ class ModuleDefinition:
|
||||
menu_items: dict[FrontendType, list[str]] = field(default_factory=dict)
|
||||
permissions: list[str] = field(default_factory=list)
|
||||
|
||||
# =========================================================================
|
||||
# Menu Definitions (Module-Driven Menus)
|
||||
# =========================================================================
|
||||
# NEW: Full menu definitions per frontend type. When set, these take
|
||||
# precedence over menu_items for menu rendering. This enables modules
|
||||
# to fully define their own menu structure with icons, routes, and labels.
|
||||
menus: dict[FrontendType, list[MenuSectionDefinition]] = field(default_factory=dict)
|
||||
|
||||
# =========================================================================
|
||||
# Classification
|
||||
# =========================================================================
|
||||
@@ -235,15 +330,15 @@ class ModuleDefinition:
|
||||
scheduled_tasks: list[ScheduledTask] = field(default_factory=list)
|
||||
|
||||
# =========================================================================
|
||||
# Menu Item Methods
|
||||
# Menu Item Methods (Legacy - uses menu_items dict of IDs)
|
||||
# =========================================================================
|
||||
|
||||
def get_menu_items(self, frontend_type: FrontendType) -> list[str]:
|
||||
"""Get menu item IDs for a specific frontend type."""
|
||||
"""Get menu item IDs for a specific frontend type (legacy)."""
|
||||
return self.menu_items.get(frontend_type, [])
|
||||
|
||||
def get_all_menu_items(self) -> set[str]:
|
||||
"""Get all menu item IDs across all frontend types."""
|
||||
"""Get all menu item IDs across all frontend types (legacy)."""
|
||||
all_items = set()
|
||||
for items in self.menu_items.values():
|
||||
all_items.update(items)
|
||||
@@ -253,6 +348,50 @@ class ModuleDefinition:
|
||||
"""Check if this module provides a specific menu item."""
|
||||
return menu_item_id in self.get_all_menu_items()
|
||||
|
||||
# =========================================================================
|
||||
# Menu Definition Methods (New - uses menus dict of full definitions)
|
||||
# =========================================================================
|
||||
|
||||
def get_menu_sections(self, frontend_type: FrontendType) -> list[MenuSectionDefinition]:
|
||||
"""
|
||||
Get menu section definitions for a specific frontend type.
|
||||
|
||||
Args:
|
||||
frontend_type: The frontend type to get menus for
|
||||
|
||||
Returns:
|
||||
List of MenuSectionDefinition objects, sorted by order
|
||||
"""
|
||||
sections = self.menus.get(frontend_type, [])
|
||||
return sorted(sections, key=lambda s: s.order)
|
||||
|
||||
def get_all_menu_definitions(self) -> dict[FrontendType, list[MenuSectionDefinition]]:
|
||||
"""
|
||||
Get all menu definitions for all frontend types.
|
||||
|
||||
Returns:
|
||||
Dict mapping FrontendType to list of MenuSectionDefinition
|
||||
"""
|
||||
return self.menus
|
||||
|
||||
def has_menus_for_frontend(self, frontend_type: FrontendType) -> bool:
|
||||
"""Check if this module has menu definitions for a frontend type."""
|
||||
return frontend_type in self.menus and len(self.menus[frontend_type]) > 0
|
||||
|
||||
def get_mandatory_menu_item_ids(self, frontend_type: FrontendType) -> set[str]:
|
||||
"""
|
||||
Get IDs of all mandatory menu items for a frontend type.
|
||||
|
||||
Returns:
|
||||
Set of menu item IDs that are marked as is_mandatory=True
|
||||
"""
|
||||
mandatory_ids = set()
|
||||
for section in self.menus.get(frontend_type, []):
|
||||
for item in section.items:
|
||||
if item.is_mandatory:
|
||||
mandatory_ids.add(item.id)
|
||||
return mandatory_ids
|
||||
|
||||
# =========================================================================
|
||||
# Feature Methods
|
||||
# =========================================================================
|
||||
|
||||
Reference in New Issue
Block a user