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:
@@ -41,7 +41,7 @@ class AIAssistantPlugin(BasePlugin):
|
||||
],
|
||||
is_core=True,
|
||||
menu_items=[
|
||||
FrontendMenuItem(label_key='nav.aiAssistant', label='AI Assistant', path='/ai-assistant', icon='Bot', order=90),
|
||||
FrontendMenuItem(label_key='nav.aiAssistant', label='KI Assistent', path='/ai-assistant', icon='Bot', order=90),
|
||||
],
|
||||
page_routes=[
|
||||
FrontendPageRoute(path='/ai-assistant', component='@/pages/AIAssistant', protected=True),
|
||||
|
||||
@@ -75,13 +75,15 @@ class AutomationPlugin(BasePlugin):
|
||||
label="Automation",
|
||||
path="/automation",
|
||||
icon="Zap",
|
||||
group="Automation",
|
||||
order=50,
|
||||
),
|
||||
FrontendMenuItem(
|
||||
label_key="nav.agents",
|
||||
label="Agents",
|
||||
label="Agenten",
|
||||
path="/agents",
|
||||
icon="Bot",
|
||||
group="Automation",
|
||||
order=51,
|
||||
),
|
||||
],
|
||||
|
||||
@@ -42,8 +42,8 @@ class CalendarPlugin(BasePlugin):
|
||||
"calendar:admin",
|
||||
],
|
||||
menu_items=[
|
||||
FrontendMenuItem(label_key='nav.calendar', label='Calendar', path='/calendar', icon='Calendar', order=20),
|
||||
FrontendMenuItem(label_key='nav.calendarKanban', label='Calendar Kanban', path='/calendar/kanban', icon='KanbanSquare', group='nav.calendar', order=21),
|
||||
FrontendMenuItem(label_key='nav.calendar', label='Kalender', path='/calendar', icon='Calendar', group='Kalender', order=20),
|
||||
FrontendMenuItem(label_key='nav.calendar.kanban', label='Kanban', path='/calendar/kanban', icon='KanbanSquare', group='Kalender', order=21),
|
||||
],
|
||||
page_routes=[
|
||||
FrontendPageRoute(path='/calendar', component='@/pages/Calendar', protected=True),
|
||||
|
||||
@@ -32,8 +32,8 @@ class DmsPlugin(BasePlugin):
|
||||
"dms:admin",
|
||||
],
|
||||
menu_items=[
|
||||
FrontendMenuItem(label_key='nav.files', label='Files', path='/dms', icon='FolderOpen', order=40),
|
||||
FrontendMenuItem(label_key='nav.trash', label='Trash', path='/dms/trash', icon='Trash2', group='nav.files', order=41),
|
||||
FrontendMenuItem(label_key='nav.dms', label='Dateien', path='/dms', icon='FolderOpen', group='Dateien', order=40),
|
||||
FrontendMenuItem(label_key='nav.dms.trash', label='Papierkorb', path='/dms/trash', icon='Trash2', group='Dateien', order=41),
|
||||
],
|
||||
page_routes=[
|
||||
FrontendPageRoute(path='/dms', component='@/pages/Dms', protected=True),
|
||||
|
||||
@@ -50,7 +50,7 @@ class KommunikationPlugin(BasePlugin):
|
||||
],
|
||||
is_core=True,
|
||||
menu_items=[
|
||||
FrontendMenuItem(label_key='nav.communication', label='Communication', path='/communication', icon='MessageSquare', order=80),
|
||||
FrontendMenuItem(label_key='nav.communication', label='Kommunikation', path='/communication', icon='MessageSquare', order=80),
|
||||
],
|
||||
page_routes=[
|
||||
FrontendPageRoute(path='/communication', component='@/pages/Communication', protected=True),
|
||||
|
||||
@@ -57,8 +57,8 @@ class MailPlugin(BasePlugin):
|
||||
migrations=["0001_initial.sql", "0006_flag_type.sql", "0007_sync_queue.sql", "0008_sync_queue_deleted_at.sql", "0009_remove_mail_soft_delete.sql", "0010_add_deleted_at.sql"],
|
||||
permissions=["mail:read", "mail:send", "mail:config", "mail:share", "mail:write", "mail:delete"],
|
||||
menu_items=[
|
||||
FrontendMenuItem(label_key='nav.email', label='Mail', path='/mail', icon='Mail', order=30),
|
||||
FrontendMenuItem(label_key='nav.emailSettings', label='Mail Settings', path='/mail/settings', icon='Settings', group='nav.email', order=31),
|
||||
FrontendMenuItem(label_key='nav.mail', label='E-Mail', path='/mail', icon='Mail', group='E-Mail', order=30),
|
||||
FrontendMenuItem(label_key='nav.mail.settings', label='Einstellungen', path='/mail/settings', icon='Settings', group='E-Mail', order=31),
|
||||
],
|
||||
page_routes=[
|
||||
FrontendPageRoute(path='/mail', component='@/pages/Mail', protected=True),
|
||||
|
||||
@@ -27,7 +27,7 @@ class ReportGeneratorPlugin(BasePlugin):
|
||||
migrations=["0001_initial.sql"],
|
||||
permissions=["reports:read", "reports:generate", "reports:manage_templates"],
|
||||
menu_items=[
|
||||
FrontendMenuItem(label_key='nav.reports', label='Reports', path='/reports', icon='BarChart3', order=70),
|
||||
FrontendMenuItem(label_key='nav.reports', label='Berichte', path='/reports', icon='BarChart3', order=70),
|
||||
],
|
||||
page_routes=[
|
||||
FrontendPageRoute(path='/reports', component='@/pages/Reports', protected=True),
|
||||
|
||||
@@ -39,7 +39,7 @@ class TasksPlugin(BasePlugin):
|
||||
menu_items=[
|
||||
FrontendMenuItem(
|
||||
label_key="nav.tasks",
|
||||
label="Tasks",
|
||||
label="Aufgaben",
|
||||
path="/tasks",
|
||||
icon="CheckSquare",
|
||||
order=30,
|
||||
|
||||
+59
-1
@@ -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}
|
||||
|
||||
Reference in New Issue
Block a user