diff --git a/backend/app/api/admin_menu.py b/backend/app/api/admin_menu.py index 93e1800..bc3e5e2 100644 --- a/backend/app/api/admin_menu.py +++ b/backend/app/api/admin_menu.py @@ -14,11 +14,17 @@ MenuItemCreate, MenuItemReorderRequest, MenuItemUpdate, + MenuTypeOut, ) router = APIRouter(prefix="/admin/menu", tags=["admin-menu"]) +@router.get("/types", response_model=list[MenuTypeOut]) +async def get_menu_types(user: User = Depends(require_admin)): + return [MenuTypeOut(value=k, label=v) for k, v in MenuItem.TYPES.items()] + + def _build_admin_item_out(item: MenuItem) -> AdminMenuItemOut: return AdminMenuItemOut( id=item.id, diff --git a/backend/app/schemas/content.py b/backend/app/schemas/content.py index 4999adc..e37b9f9 100644 --- a/backend/app/schemas/content.py +++ b/backend/app/schemas/content.py @@ -77,6 +77,11 @@ class PageBlockUpdate(BaseModel): enabled: bool +class MenuTypeOut(BaseModel): + value: int + label: str + + class MenuItemOut(BaseModel): id: int label: str | None = None diff --git a/backend/tests/integration/api/test_admin_menu.py b/backend/tests/integration/api/test_admin_menu.py index db7c74f..7d2d87e 100644 --- a/backend/tests/integration/api/test_admin_menu.py +++ b/backend/tests/integration/api/test_admin_menu.py @@ -33,6 +33,63 @@ async def _create_menu_item(db: AsyncSession, **overrides) -> MenuItem: return item +# ── Types ───────────────────────────────────────────────────────────── + + +@pytest.mark.asyncio +async def test_types_returns_all_menu_types(client: AsyncClient, db_session: AsyncSession): + # GIVEN + _, headers = await _create_admin(db_session) + + # WHEN + response = await client.get("/api/admin/menu/types", headers=headers) + + # THEN + assert response.status_code == 200 + data = response.json() + assert len(data) == len(MenuItem.TYPES) + values = {entry["value"] for entry in data} + assert values == set(MenuItem.TYPES.keys()) + for entry in data: + assert entry["label"] == MenuItem.TYPES[entry["value"]] + + +@pytest.mark.asyncio +async def test_types_includes_discord_voice(client: AsyncClient, db_session: AsyncSession): + # GIVEN + _, headers = await _create_admin(db_session) + + # WHEN + response = await client.get("/api/admin/menu/types", headers=headers) + + # THEN + data = response.json() + discord_entries = [e for e in data if e["value"] == MenuItem.TYPE_DISCORD_VOICE] + assert len(discord_entries) == 1 + assert discord_entries[0]["label"] == "Discord (vocal)" + + +@pytest.mark.asyncio +async def test_types_unauthenticated(client: AsyncClient): + # WHEN + response = await client.get("/api/admin/menu/types") + + # THEN + assert response.status_code == 401 + + +@pytest.mark.asyncio +async def test_types_unauthorized(client: AsyncClient, db_session: AsyncSession): + # GIVEN + _, headers = await _create_user(db_session) + + # WHEN + response = await client.get("/api/admin/menu/types", headers=headers) + + # THEN + assert response.status_code == 403 + + # ── List ────────────────────────────────────────────────────────────── diff --git a/frontend/src/api/menu.ts b/frontend/src/api/menu.ts index f8a4852..d2da3d6 100644 --- a/frontend/src/api/menu.ts +++ b/frontend/src/api/menu.ts @@ -21,6 +21,11 @@ export async function getAdminMenuItems(params?: { return data } +export async function getAdminMenuTypes(): Promise<{ value: number; label: string }[]> { + const { data } = await apiClient.get<{ value: number; label: string }[]>('/admin/menu/types') + return data +} + export async function getAdminMenuTree(): Promise { const { data } = await apiClient.get('/admin/menu/tree') return data diff --git a/frontend/src/components/admin/MenuListView.vue b/frontend/src/components/admin/MenuListView.vue index a2330af..ce4263d 100644 --- a/frontend/src/components/admin/MenuListView.vue +++ b/frontend/src/components/admin/MenuListView.vue @@ -1,7 +1,8 @@