Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions backend/app/api/admin_menu.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
5 changes: 5 additions & 0 deletions backend/app/schemas/content.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
57 changes: 57 additions & 0 deletions backend/tests/integration/api/test_admin_menu.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 ──────────────────────────────────────────────────────────────


Expand Down
5 changes: 5 additions & 0 deletions frontend/src/api/menu.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<AdminMenuItemTree[]> {
const { data } = await apiClient.get<AdminMenuItemTree[]>('/admin/menu/tree')
return data
Expand Down
25 changes: 8 additions & 17 deletions frontend/src/components/admin/MenuListView.vue
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
<script setup lang="ts">
import { ref, watch, onMounted } from 'vue'
import { ref, computed, watch, onMounted } from 'vue'
import {
getAdminMenuItems,
getAdminMenuTypes,
createAdminMenuItem,
updateAdminMenuItem,
deleteAdminMenuItem,
Expand All @@ -15,22 +16,10 @@ import { useToast } from '@/composables/useToast'
const { confirm } = useConfirm()
const toast = useToast()

// Type constants
const typeOptions = [
{ value: 1, label: 'Menu' },
{ value: 2, label: 'Url personnalisée' },
{ value: 3, label: 'Url (redirection)' },
{ value: 4, label: 'Page' },
{ value: 5, label: 'Séparateur' },
{ value: 6, label: 'Bureau' },
{ value: 7, label: 'Serveurs' },
{ value: 8, label: 'Roster' },
{ value: 9, label: 'Calendrier' },
{ value: 10, label: 'Mission Maker' },
{ value: 11, label: 'Team Speak' },
]
// Type constants (loaded from backend)
const typeOptions = ref<{ value: number; label: string }[]>([])

const typeLabels: Record<number, string> = Object.fromEntries(typeOptions.map((t) => [t.value, t.label]))
const typeLabels = computed(() => Object.fromEntries(typeOptions.value.map((t) => [t.value, t.label])))

const restrictionOptions = [
{ value: 0, label: 'Tout le monde' },
Expand Down Expand Up @@ -115,14 +104,16 @@ async function loadItems() {

async function loadReferenceData() {
try {
const [menuResult, urlResult, pageResult] = await Promise.all([
const [menuResult, urlResult, pageResult, types] = await Promise.all([
getAdminMenuItems({ type: 1, limit: 100 }),
getAdminUrls({ limit: 100 }),
getAdminPages({ limit: 100 }),
getAdminMenuTypes(),
])
parentMenuItems.value = menuResult.items
availableUrls.value = urlResult.items
availablePages.value = pageResult.items
typeOptions.value = types
} catch (e) {
toast.error(e)
}
Expand Down
16 changes: 2 additions & 14 deletions frontend/src/components/admin/MenuTreeNode.vue
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<script setup lang="ts">
import { ref, onMounted, onUnmounted, nextTick } from 'vue'
import { ref, inject, onMounted, onUnmounted, nextTick } from 'vue'
import { dragAndDrop } from '@formkit/drag-and-drop/vue'
import { tearDown } from '@formkit/drag-and-drop'
import type { AdminMenuItemTree } from '@/types/api'
Expand All @@ -15,19 +15,7 @@ const emit = defineEmits<{

const TYPE_MENU = 1

const typeLabels: Record<number, string> = {
1: 'Menu',
2: 'Lien',
3: 'Url',
4: 'Page',
5: 'Séparateur',
6: 'Bureau',
7: 'Serveurs',
8: 'Roster',
9: 'Calendrier',
10: 'Mission Maker',
11: 'Team Speak',
}
const typeLabels = inject<import('vue').Ref<Record<number, string>>>('menuTypeLabels', ref({}))

const restrictionLabels: Record<number, string> = {
1: 'Invité+',
Expand Down
15 changes: 12 additions & 3 deletions frontend/src/components/admin/MenuTreeView.vue
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<script setup lang="ts">
import { ref, nextTick, onMounted } from 'vue'
import { ref, provide, nextTick, onMounted } from 'vue'
import { useDragAndDrop } from '@formkit/drag-and-drop/vue'
import { getAdminMenuTree, reorderAdminMenuItems } from '@/api/menu'
import { getAdminMenuTree, getAdminMenuTypes, reorderAdminMenuItems } from '@/api/menu'
import type { AdminMenuItemTree, MenuItemReorderEntry } from '@/types/api'
import { useToast } from '@/composables/useToast'
import { useMenuStore } from '@/stores/menu'
Expand All @@ -14,6 +14,9 @@ const loading = ref(false)
const saving = ref(false)
const hasChanges = ref(false)

const typeLabels = ref<Record<number, string>>({})
provide('menuTypeLabels', typeLabels)

function onChanged() {
hasChanges.value = true
}
Expand Down Expand Up @@ -82,7 +85,13 @@ async function saveOrder() {
}
}

onMounted(loadTree)
onMounted(async () => {
const [, types] = await Promise.all([
loadTree(),
getAdminMenuTypes(),
])
typeLabels.value = Object.fromEntries(types.map((t) => [t.value, t.label]))
})
</script>

<template>
Expand Down
Loading