148 lines
6.1 KiB
PHP
148 lines
6.1 KiB
PHP
<?php
|
|
|
|
namespace App\Controller;
|
|
|
|
use App\Content\MenuRepository;
|
|
use App\Content\PageRepository;
|
|
use App\Domain\InvalidMenu;
|
|
use App\Domain\StaleWrite;
|
|
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
|
use Symfony\Component\HttpFoundation\Request;
|
|
use Symfony\Component\HttpFoundation\Response;
|
|
use Symfony\Component\Routing\Attribute\Route;
|
|
|
|
final class MenuController extends AbstractController
|
|
{
|
|
public function __construct(private MenuRepository $menus, private PageRepository $pages)
|
|
{
|
|
}
|
|
|
|
#[Route('/%kant.admin_path%/menus', name: 'admin_menus', methods: ['GET'])]
|
|
public function index(Request $request): Response
|
|
{
|
|
$slot = $request->query->getString('slot', 'header');
|
|
$slot = isset(MenuRepository::SLOTS[$slot]) ? $slot : 'header';
|
|
$menu = $this->menus->all()[$slot] ?? null;
|
|
$rows = $this->flatten($menu['items'] ?? []);
|
|
$selected = [];
|
|
foreach ($rows as &$row) {
|
|
if ($row['pageId']) {
|
|
$selected[$row['pageId']] ??= $this->pages->choices(id: $row['pageId'])[0] ?? ['id' => $row['pageId'], 'title' => 'Nicht mehr verfügbar', 'path' => ''];
|
|
}
|
|
$row['page'] = $selected[$row['pageId']] ?? null;
|
|
}
|
|
unset($row);
|
|
|
|
return $this->render('admin/menus.html.twig', [
|
|
'slots' => MenuRepository::SLOTS, 'slot' => $slot, 'slotName' => MenuRepository::SLOTS[$slot],
|
|
'menu' => $menu, 'rows' => $rows,
|
|
'menuIcons' => \App\Content\DocumentValidator::ICONS,
|
|
]);
|
|
}
|
|
|
|
#[Route('/%kant.admin_path%/menus/{slot}', name: 'admin_menu_save', methods: ['POST'])]
|
|
public function save(Request $request, string $slot): Response
|
|
{
|
|
$this->denyAccessUnlessGranted('ROLE_EDITOR');
|
|
if (!$this->isCsrfTokenValid('menus', $request->request->getString('_csrf_token'))) {
|
|
return 'json' === $request->getPreferredFormat() ? $this->json(['error' => 'Deine Sitzung ist abgelaufen. Bitte erneut anmelden.'], 403) : $this->back('Deine Sitzung ist abgelaufen. Bitte melde dich erneut an.', slot: $slot);
|
|
}
|
|
try {
|
|
$this->menus->save(
|
|
$slot,
|
|
$request->request->getString('name'),
|
|
$this->tree($request->request->all('items')),
|
|
$request->request->getInt('expectedVersion', -1),
|
|
);
|
|
} catch (InvalidMenu|StaleWrite $e) {
|
|
return 'json' === $request->getPreferredFormat() ? $this->json(['error' => $e->getMessage()], $e instanceof StaleWrite ? 409 : 422) : $this->back($e->getMessage(), slot: $slot);
|
|
}
|
|
|
|
if ('json' === $request->getPreferredFormat()) {
|
|
return $this->json(['message' => 'Menü gespeichert.', 'version' => $this->menus->all()[$slot]['version']]);
|
|
}
|
|
|
|
return $this->back('Das Menü wurde gespeichert.', 'success', $slot);
|
|
}
|
|
|
|
/**
|
|
* Turns the stored tree back into the flat rows the form edits.
|
|
*
|
|
* @return list<array{label: string, type: string, pageId: string, anchor: string, url: string, level: int}>
|
|
*/
|
|
private function flatten(array $items, int $level = 1): array
|
|
{
|
|
$rows = [];
|
|
foreach ($items as $item) {
|
|
$rows[] = [
|
|
'label' => $item['label'], 'type' => $item['type'], 'level' => $level,
|
|
'pageId' => $item['pageId'] ?? '', 'anchor' => $item['anchor'] ?? '', 'url' => $item['url'] ?? '',
|
|
'newTab' => $item['newTab'] ?? false,
|
|
'icon' => $item['icon'] ?? '', 'iconPosition' => $item['iconPosition'] ?? 'before',
|
|
];
|
|
array_push($rows, ...$this->flatten($item['children'] ?? [], $level + 1));
|
|
}
|
|
|
|
return $rows;
|
|
}
|
|
|
|
/**
|
|
* The form posts a flat, numbered list so it works without JavaScript. Order comes from the
|
|
* position field, nesting from the level field: a level-2 row joins the last level-1 row.
|
|
* Rows without a label are treated as removed, which is what the empty "add" row is for.
|
|
*/
|
|
private function tree(array $rows): array
|
|
{
|
|
$flat = [];
|
|
foreach ($rows as $row) {
|
|
if (!is_array($row) || '' === trim((string) ($row['label'] ?? ''))) {
|
|
continue;
|
|
}
|
|
$flat[] = [
|
|
'label' => trim((string) $row['label']),
|
|
'type' => (string) ($row['type'] ?? 'page'),
|
|
'pageId' => (string) ($row['pageId'] ?? ''),
|
|
'anchor' => ltrim(trim((string) ($row['anchor'] ?? '')), '#'),
|
|
'url' => trim((string) ($row['url'] ?? '')),
|
|
'newTab' => '1' === (string) ($row['newTab'] ?? ''),
|
|
'icon' => (string) ($row['icon'] ?? ''), 'iconPosition' => (string) ($row['iconPosition'] ?? 'before'),
|
|
'level' => max(1, min(MenuRepository::MAX_DEPTH, (int) ($row['level'] ?? 1))),
|
|
'position' => (int) ($row['position'] ?? 0),
|
|
];
|
|
}
|
|
usort($flat, static fn (array $a, array $b): int => $a['position'] <=> $b['position']);
|
|
|
|
$items = [];
|
|
foreach ($flat as $row) {
|
|
$item = ['label' => $row['label'], 'type' => $row['type'], 'children' => []];
|
|
if ('' !== $row['icon']) {
|
|
$item['icon'] = $row['icon'];
|
|
$item['iconPosition'] = $row['iconPosition'];
|
|
}
|
|
if ($row['newTab']) {
|
|
$item['newTab'] = true;
|
|
}
|
|
if ('url' === $row['type']) {
|
|
$item['url'] = $row['url'];
|
|
} else {
|
|
$item['pageId'] = $row['pageId'];
|
|
$item['anchor'] = $row['anchor'];
|
|
}
|
|
if (1 === $row['level'] || [] === $items) {
|
|
$items[] = $item;
|
|
continue;
|
|
}
|
|
$items[array_key_last($items)]['children'][] = $item;
|
|
}
|
|
|
|
return $items;
|
|
}
|
|
|
|
private function back(string $message, string $type = 'error', string $slot = 'header'): Response
|
|
{
|
|
$this->addFlash($type, $message);
|
|
|
|
return $this->redirectToRoute('admin_menus', 'header' === $slot ? [] : ['slot' => $slot]);
|
|
}
|
|
}
|