feat: navigation restructure + drag-and-drop menu ordering

Navigation:
- Remove Plugins header from sidebar
- Merge static items and plugin items into unified sorted list
- Group related menu items (Dateien, E-Mail, Kalender, Automation)
- German labels for all menu items
- Sort by order field, alphabetical fallback for ties

Drag-and-Drop Menu:
- New backend endpoints: GET/PUT /api/v1/users/me/menu-order
- Store menu order in user preferences JSONB field
- New SettingsMenuOrder page with @dnd-kit drag-and-drop
- New Settings tab: Menu ordering
- Sidebar reads saved menu order and sorts accordingly
- Reset to default option
This commit is contained in:
Agent Zero
2026-07-24 21:46:27 +02:00
parent 4b9cb5a098
commit e017da9d12
17 changed files with 608 additions and 142 deletions
+59 -1
View File
@@ -13,7 +13,7 @@ from app.core.auth import get_redis
from app.core.db import get_db
from app.core.notifications import create_notification
from app.core.permissions import invalidate_permission_cache
from app.deps import require_permission
from app.deps import get_current_user, require_permission
from app.schemas.user import UserCreate, UserUpdate, UserResponse, PaginatedUsers
from app.services.user_service import user_service, _UNSET
@@ -276,3 +276,61 @@ async def delete_user(
)
return Response(status_code=status.HTTP_204_NO_CONTENT)
@router.get("/me/menu-order")
async def get_menu_order(
db: AsyncSession = Depends(get_db),
current_user: dict = Depends(get_current_user),
):
"""Get the current user's menu order preference."""
tenant_id = uuid.UUID(current_user["tenant_id"])
user_id = uuid.UUID(current_user["user_id"])
from sqlalchemy import select
from app.models.user import User
result = await db.execute(
select(User).where(User.id == user_id, User.tenant_id == tenant_id)
)
user = result.scalar_one_or_none()
if user is None:
raise HTTPException(404, detail={"detail": "User not found", "code": "not_found"})
menu_order = user.preferences.get("menu_order", [])
return {"menu_order": menu_order}
@router.put("/me/menu-order")
async def update_menu_order(
body: dict,
db: AsyncSession = Depends(get_db),
current_user: dict = Depends(get_current_user),
):
"""Update the current user's menu order preference."""
tenant_id = uuid.UUID(current_user["tenant_id"])
user_id = uuid.UUID(current_user["user_id"])
from sqlalchemy import select
from app.models.user import User
menu_order = body.get("menu_order")
if not isinstance(menu_order, list) or not all(isinstance(x, str) for x in menu_order):
raise HTTPException(
400,
detail={"detail": "menu_order must be a list of strings", "code": "invalid_format"},
)
result = await db.execute(
select(User).where(User.id == user_id, User.tenant_id == tenant_id)
)
user = result.scalar_one_or_none()
if user is None:
raise HTTPException(404, detail={"detail": "User not found", "code": "not_found"})
prefs = dict(user.preferences) if user.preferences else {}
prefs["menu_order"] = menu_order
user.preferences = prefs
await db.commit()
return {"menu_order": menu_order}