@page "/admin/menu-structure"
@using Microsoft.AspNetCore.Authorization
@using TrafagSalesExporter.Models
@using TrafagSalesExporter.Security
@using TrafagSalesExporter.Services
@attribute [Authorize(Policy = SecurityPolicies.AdminOnly)]
@inject INavigationMenuService NavigationMenuService
@inject IUiTextService UiText
@inject ISnackbar Snackbar
@T("Menuestruktur", "Menu structure")
@T("Menuestruktur", "Menu structure")
@T("Speichern", "Save")
@T("Standard wiederherstellen", "Restore default")
@T("Bestehende Menuepunkte koennen in andere Gruppen gehaengt, sortiert, aus- oder eingeblendet und umbenannt werden. Die Zielseite bleibt unveraendert.",
"Existing menu entries can be moved into other groups, sorted, hidden/shown and renamed. The target page stays unchanged.")
@if (_loading)
{
}
else
{
@T("Drag & Drop", "Drag & drop")
@foreach (var item in OrderedItems)
{
}
@T("Details", "Details")
@T("Reihenfolge", "Order")
@T("Titel", "Title")
@T("Typ", "Type")
@T("Untermenue von", "Parent")
@T("Sichtbar", "Visible")
@T("Aktion", "Action")
@if (!string.IsNullOrWhiteSpace(context.Href))
{
@context.Href
}
@context.ItemType
@T("Hauptmenue", "Root menu")
@foreach (var group in ParentOptions(context))
{
@GroupLabel(group)
}
@context.Key
}
@code {
private const string RootParentValue = "__root__";
private List _items = [];
private NavigationMenuItem? _draggedItem;
private bool _loading = true;
private IEnumerable OrderedItems => _items
.OrderBy(x => x.ParentKey ?? string.Empty)
.ThenBy(x => x.SortOrder)
.ThenBy(x => x.TitleDe);
protected override async Task OnInitializedAsync()
{
await LoadAsync();
}
private async Task LoadAsync()
{
_loading = true;
_items = await NavigationMenuService.GetItemsAsync();
_loading = false;
}
private async Task SaveAsync()
{
NormalizeSortOrders();
await NavigationMenuService.SaveItemsAsync(_items);
Snackbar.Add(T("Menuestruktur gespeichert.", "Menu structure saved."), Severity.Success);
}
private async Task ResetAsync()
{
await NavigationMenuService.ResetToDefaultsAsync();
await LoadAsync();
Snackbar.Add(T("Standard-Menuestruktur wiederhergestellt.", "Default menu structure restored."), Severity.Info);
}
private IEnumerable ParentOptions(NavigationMenuItem item)
=> _items
.Where(x => x.ItemType == NavigationMenuItemTypes.Group)
.Where(x => x.Key != item.Key)
.Where(x => !WouldCreateCycle(item.Key, x.Key))
.OrderBy(x => x.TitleDe);
private void ChangeParent(NavigationMenuItem item, string parentKey)
{
item.ParentKey = parentKey == RootParentValue ? null : parentKey;
item.SortOrder = NextSortOrder(item.ParentKey);
}
private void Move(NavigationMenuItem item, int direction)
{
var siblings = _items
.Where(x => string.Equals(x.ParentKey, item.ParentKey, StringComparison.OrdinalIgnoreCase))
.OrderBy(x => x.SortOrder)
.ThenBy(x => x.TitleDe)
.ToList();
var index = siblings.FindIndex(x => x.Key == item.Key);
var targetIndex = index + direction;
if (index < 0 || targetIndex < 0 || targetIndex >= siblings.Count)
return;
(siblings[index].SortOrder, siblings[targetIndex].SortOrder) = (siblings[targetIndex].SortOrder, siblings[index].SortOrder);
}
private void StartDrag(NavigationMenuItem item)
{
_draggedItem = item;
}
private void DropOnRoot()
{
if (_draggedItem is null)
return;
_draggedItem.ParentKey = null;
_draggedItem.SortOrder = NextSortOrder(null);
_draggedItem = null;
}
private void DropOn(NavigationMenuItem target)
{
if (_draggedItem is null || _draggedItem.Key == target.Key)
return;
if (target.ItemType == NavigationMenuItemTypes.Group && !WouldCreateCycle(_draggedItem.Key, target.Key))
{
_draggedItem.ParentKey = target.Key;
_draggedItem.SortOrder = NextSortOrder(target.Key);
}
else
{
_draggedItem.ParentKey = target.ParentKey;
_draggedItem.SortOrder = target.SortOrder - 1;
NormalizeSortOrders();
}
_draggedItem = null;
}
private bool WouldCreateCycle(string itemKey, string candidateParentKey)
{
var current = _items.FirstOrDefault(x => x.Key == candidateParentKey);
while (current is not null)
{
if (current.Key == itemKey)
return true;
current = string.IsNullOrWhiteSpace(current.ParentKey)
? null
: _items.FirstOrDefault(x => x.Key == current.ParentKey);
}
return false;
}
private int NextSortOrder(string? parentKey)
=> _items
.Where(x => string.Equals(x.ParentKey, parentKey, StringComparison.OrdinalIgnoreCase))
.Select(x => x.SortOrder)
.DefaultIfEmpty(0)
.Max() + 10;
private void NormalizeSortOrders()
{
foreach (var group in _items.GroupBy(x => x.ParentKey ?? string.Empty))
{
var sortOrder = 10;
foreach (var item in group.OrderBy(x => x.SortOrder).ThenBy(x => x.TitleDe))
{
item.SortOrder = sortOrder;
sortOrder += 10;
}
}
}
private static string NormalizeParent(string? parentKey)
=> string.IsNullOrWhiteSpace(parentKey) ? RootParentValue : parentKey;
private string GroupLabel(NavigationMenuItem item)
=> UiText.Text(item.TitleDe, item.TitleEn);
private string T(string german, string english) => UiText.Text(german, english);
private string Indent(NavigationMenuItem item)
{
var depth = 0;
var current = item;
while (!string.IsNullOrWhiteSpace(current.ParentKey))
{
depth++;
var next = _items.FirstOrDefault(x => x.Key == current.ParentKey);
if (next is null || next.Key == current.Key)
break;
current = next;
}
return new string(' ', depth * 4);
}
}