feat: add admin menu configuration and sidebar improvements
- Add AdminMenuConfig model for per-platform menu customization - Add menu registry for centralized menu configuration - Add my-menu-config and platform-menu-config admin pages - Update sidebar with improved layout and Alpine.js interactions - Add FrontendType enum for admin/vendor menu separation - Document self-contained module patterns in session note Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
128
alembic/versions/za0k1l2m3n4o5_add_admin_menu_config.py
Normal file
128
alembic/versions/za0k1l2m3n4o5_add_admin_menu_config.py
Normal file
@@ -0,0 +1,128 @@
|
||||
"""Add admin menu configuration table
|
||||
|
||||
Revision ID: za0k1l2m3n4o5
|
||||
Revises: z9j0k1l2m3n4
|
||||
Create Date: 2026-01-25
|
||||
|
||||
Adds configurable admin sidebar menus:
|
||||
- Platform-level config: Controls which menu items platform admins see
|
||||
- User-level config: Controls which menu items super admins see
|
||||
- Opt-out model: All items visible by default
|
||||
- Mandatory items enforced at application level (companies, vendors, users, settings)
|
||||
"""
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "za0k1l2m3n4o5"
|
||||
down_revision = "z9j0k1l2m3n4"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# Create admin_menu_configs table
|
||||
op.create_table(
|
||||
"admin_menu_configs",
|
||||
sa.Column("id", sa.Integer(), nullable=False),
|
||||
sa.Column(
|
||||
"platform_id",
|
||||
sa.Integer(),
|
||||
nullable=True,
|
||||
comment="Platform scope - applies to all platform admins of this platform",
|
||||
),
|
||||
sa.Column(
|
||||
"user_id",
|
||||
sa.Integer(),
|
||||
nullable=True,
|
||||
comment="User scope - applies to this specific super admin",
|
||||
),
|
||||
sa.Column(
|
||||
"menu_item_id",
|
||||
sa.String(50),
|
||||
nullable=False,
|
||||
comment="Menu item identifier from registry (e.g., 'products', 'inventory')",
|
||||
),
|
||||
sa.Column(
|
||||
"is_visible",
|
||||
sa.Boolean(),
|
||||
nullable=False,
|
||||
server_default="true",
|
||||
comment="Whether this menu item is visible (False = hidden)",
|
||||
),
|
||||
sa.Column(
|
||||
"created_at",
|
||||
sa.DateTime(timezone=True),
|
||||
nullable=False,
|
||||
server_default=sa.func.now(),
|
||||
),
|
||||
sa.Column(
|
||||
"updated_at",
|
||||
sa.DateTime(timezone=True),
|
||||
nullable=False,
|
||||
server_default=sa.func.now(),
|
||||
onupdate=sa.func.now(),
|
||||
),
|
||||
# Foreign keys
|
||||
sa.ForeignKeyConstraint(
|
||||
["platform_id"],
|
||||
["platforms.id"],
|
||||
ondelete="CASCADE",
|
||||
),
|
||||
sa.ForeignKeyConstraint(
|
||||
["user_id"],
|
||||
["users.id"],
|
||||
ondelete="CASCADE",
|
||||
),
|
||||
sa.PrimaryKeyConstraint("id"),
|
||||
# Unique constraints
|
||||
sa.UniqueConstraint("platform_id", "menu_item_id", name="uq_platform_menu_config"),
|
||||
sa.UniqueConstraint("user_id", "menu_item_id", name="uq_user_menu_config"),
|
||||
# Check constraint: exactly one scope must be set
|
||||
sa.CheckConstraint(
|
||||
"(platform_id IS NOT NULL AND user_id IS NULL) OR "
|
||||
"(platform_id IS NULL AND user_id IS NOT NULL)",
|
||||
name="ck_admin_menu_config_scope",
|
||||
),
|
||||
)
|
||||
|
||||
# Create indexes for performance
|
||||
op.create_index(
|
||||
"idx_admin_menu_configs_platform_id",
|
||||
"admin_menu_configs",
|
||||
["platform_id"],
|
||||
)
|
||||
op.create_index(
|
||||
"idx_admin_menu_configs_user_id",
|
||||
"admin_menu_configs",
|
||||
["user_id"],
|
||||
)
|
||||
op.create_index(
|
||||
"idx_admin_menu_configs_menu_item_id",
|
||||
"admin_menu_configs",
|
||||
["menu_item_id"],
|
||||
)
|
||||
op.create_index(
|
||||
"idx_admin_menu_config_platform_visible",
|
||||
"admin_menu_configs",
|
||||
["platform_id", "is_visible"],
|
||||
)
|
||||
op.create_index(
|
||||
"idx_admin_menu_config_user_visible",
|
||||
"admin_menu_configs",
|
||||
["user_id", "is_visible"],
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
# Drop indexes
|
||||
op.drop_index("idx_admin_menu_config_user_visible", table_name="admin_menu_configs")
|
||||
op.drop_index("idx_admin_menu_config_platform_visible", table_name="admin_menu_configs")
|
||||
op.drop_index("idx_admin_menu_configs_menu_item_id", table_name="admin_menu_configs")
|
||||
op.drop_index("idx_admin_menu_configs_user_id", table_name="admin_menu_configs")
|
||||
op.drop_index("idx_admin_menu_configs_platform_id", table_name="admin_menu_configs")
|
||||
|
||||
# Drop table
|
||||
op.drop_table("admin_menu_configs")
|
||||
@@ -0,0 +1,117 @@
|
||||
"""Add frontend_type to admin_menu_configs
|
||||
|
||||
Revision ID: zb1l2m3n4o5p6
|
||||
Revises: za0k1l2m3n4o5
|
||||
Create Date: 2026-01-25
|
||||
|
||||
Adds frontend_type column to support both admin and vendor menu configuration:
|
||||
- 'admin': Admin panel menus (super admins, platform admins)
|
||||
- 'vendor': Vendor dashboard menus (configured per platform)
|
||||
|
||||
Also updates unique constraints to include frontend_type and adds
|
||||
a check constraint ensuring user_id scope is only used for admin frontend.
|
||||
"""
|
||||
|
||||
from alembic import op
|
||||
import sqlalchemy as sa
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = "zb1l2m3n4o5p6"
|
||||
down_revision = "za0k1l2m3n4o5"
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade() -> None:
|
||||
# 1. Create the enum type for frontend_type
|
||||
frontend_type_enum = sa.Enum('admin', 'vendor', name='frontendtype')
|
||||
frontend_type_enum.create(op.get_bind(), checkfirst=True)
|
||||
|
||||
# 2. Add frontend_type column with default value
|
||||
op.add_column(
|
||||
"admin_menu_configs",
|
||||
sa.Column(
|
||||
"frontend_type",
|
||||
sa.Enum('admin', 'vendor', name='frontendtype'),
|
||||
nullable=False,
|
||||
server_default="admin",
|
||||
comment="Which frontend this config applies to (admin or vendor)",
|
||||
),
|
||||
)
|
||||
|
||||
# 3. Create index on frontend_type
|
||||
op.create_index(
|
||||
"idx_admin_menu_configs_frontend_type",
|
||||
"admin_menu_configs",
|
||||
["frontend_type"],
|
||||
)
|
||||
|
||||
# 4. Drop old unique constraints
|
||||
op.drop_constraint("uq_platform_menu_config", "admin_menu_configs", type_="unique")
|
||||
op.drop_constraint("uq_user_menu_config", "admin_menu_configs", type_="unique")
|
||||
|
||||
# 5. Create new unique constraints that include frontend_type
|
||||
op.create_unique_constraint(
|
||||
"uq_frontend_platform_menu_config",
|
||||
"admin_menu_configs",
|
||||
["frontend_type", "platform_id", "menu_item_id"],
|
||||
)
|
||||
op.create_unique_constraint(
|
||||
"uq_frontend_user_menu_config",
|
||||
"admin_menu_configs",
|
||||
["frontend_type", "user_id", "menu_item_id"],
|
||||
)
|
||||
|
||||
# 6. Add check constraint: user_id scope only allowed for admin frontend
|
||||
op.create_check_constraint(
|
||||
"ck_user_scope_admin_only",
|
||||
"admin_menu_configs",
|
||||
"(user_id IS NULL) OR (frontend_type = 'admin')",
|
||||
)
|
||||
|
||||
# 7. Create composite indexes for common queries
|
||||
op.create_index(
|
||||
"idx_admin_menu_config_frontend_platform",
|
||||
"admin_menu_configs",
|
||||
["frontend_type", "platform_id"],
|
||||
)
|
||||
op.create_index(
|
||||
"idx_admin_menu_config_frontend_user",
|
||||
"admin_menu_configs",
|
||||
["frontend_type", "user_id"],
|
||||
)
|
||||
|
||||
|
||||
def downgrade() -> None:
|
||||
# Drop new indexes
|
||||
op.drop_index("idx_admin_menu_config_frontend_user", table_name="admin_menu_configs")
|
||||
op.drop_index("idx_admin_menu_config_frontend_platform", table_name="admin_menu_configs")
|
||||
|
||||
# Drop check constraint
|
||||
op.drop_constraint("ck_user_scope_admin_only", "admin_menu_configs", type_="check")
|
||||
|
||||
# Drop new unique constraints
|
||||
op.drop_constraint("uq_frontend_user_menu_config", "admin_menu_configs", type_="unique")
|
||||
op.drop_constraint("uq_frontend_platform_menu_config", "admin_menu_configs", type_="unique")
|
||||
|
||||
# Restore old unique constraints
|
||||
op.create_unique_constraint(
|
||||
"uq_platform_menu_config",
|
||||
"admin_menu_configs",
|
||||
["platform_id", "menu_item_id"],
|
||||
)
|
||||
op.create_unique_constraint(
|
||||
"uq_user_menu_config",
|
||||
"admin_menu_configs",
|
||||
["user_id", "menu_item_id"],
|
||||
)
|
||||
|
||||
# Drop frontend_type index
|
||||
op.drop_index("idx_admin_menu_configs_frontend_type", table_name="admin_menu_configs")
|
||||
|
||||
# Drop frontend_type column
|
||||
op.drop_column("admin_menu_configs", "frontend_type")
|
||||
|
||||
# Drop the enum type
|
||||
sa.Enum('admin', 'vendor', name='frontendtype').drop(op.get_bind(), checkfirst=True)
|
||||
Reference in New Issue
Block a user