User
Deploy sidebar navigation fix
1600631
Raw
History Blame Contribute Delete
10.4 kB
@using Microsoft.AspNetCore.Components
@using Microsoft.AspNetCore.Components.Authorization
@using Microsoft.AspNetCore.Components.Routing
@using TaskTrackingSystem.Shared.Models.Menu
@using Microsoft.JSInterop
@implements IDisposable
@inject TaskTrackingSystem.WebApp.Localization.UiLanguageService Language
@inject AuthenticationStateProvider AuthStateProvider
@inject MenuAuthorizationService MenuAuthorization
@inject NavigationManager Navigation
@inject IJSRuntime JS
<nav class="flex-1 space-y-1 overflow-y-auto px-3 pb-5 pt-2 text-slate-300"
data-enhance-nav="false"
onclick="if (event.target.closest('a')) document.body.classList.remove('tts-sidebar-open');">
@if (menus.Any())
{
@foreach (var menu in menus)
{
if (menu.SubMenus != null && menu.SubMenus.Count > 0)
{
<button class="nav-dropdown-toggle flex w-full items-center rounded-lg px-3 py-2.5 text-sm font-semibold text-slate-300 transition-colors hover:bg-white/10 hover:text-white @(IsMenuGroupExpanded(menu.MenuCode) ? "expanded bg-white/10 text-white" : "")"
@onclick="() => ToggleMenuGroup(menu.MenuCode)">
@RenderIcon(menu.Icon)
<span class="flex-1 text-left">@AppLocalization.Menu(menu.MenuCode, menu.MenuName)</span>
<i data-lucide="chevron-down" class="nav-chevron h-4 w-4 text-slate-400 shrink-0"></i>
</button>
@if (IsMenuGroupExpanded(menu.MenuCode))
{
<div class="nav-dropdown-children">
@foreach (var sub in menu.SubMenus)
{
<NavLink class="nav-link-item nav-child-item flex items-center rounded-lg px-3 py-2 text-sm font-semibold text-slate-300 transition-colors hover:bg-white/10 hover:text-white"
href="@MenuAuthorizationService.NormalizeMenuHref(sub.MenuUrl)"
Match="@GetNavLinkMatch(sub.MenuUrl)">
@RenderSmallIcon(sub.Icon)
<span>@AppLocalization.Menu(sub.MenuCode, sub.MenuName)</span>
</NavLink>
}
</div>
}
}
else
{
<NavLink class="nav-link-item flex items-center rounded-lg px-3 py-2.5 text-sm font-semibold text-slate-300 transition-colors hover:bg-white/10 hover:text-white"
href="@MenuAuthorizationService.NormalizeMenuHref(menu.MenuUrl)"
Match="@(string.IsNullOrWhiteSpace(menu.MenuUrl) || menu.MenuUrl.Trim('/') is "dashboard" or "" ? NavLinkMatch.All : NavLinkMatch.Prefix)">
@RenderIcon(menu.Icon)
<span>@AppLocalization.Menu(menu.MenuCode, menu.MenuName)</span>
</NavLink>
}
}
}
else
{
<div class="px-4 py-6 text-xs text-slate-400">
@Language.Texts.NoAccessItems
</div>
}
</nav>
@code {
private List<MenuDto> menus = new();
private string currentPath = string.Empty;
private HashSet<string> expandedMenuGroups = new();
protected override async Task OnInitializedAsync()
{
Language.Changed += OnLanguageChanged;
currentPath = Navigation.ToBaseRelativePath(Navigation.Uri).Trim('/');
Navigation.LocationChanged += OnLocationChanged;
try
{
var authState = await AuthStateProvider.GetAuthenticationStateAsync();
var user = authState.User;
if (user.Identity?.IsAuthenticated == true)
{
var rawMenus = await MenuAuthorization.GetUserMenusAsync(user);
menus = ProcessMenus(rawMenus);
BuildUrlHierarchy(menus);
foreach (var menu in menus)
{
if (menu.SubMenus != null && menu.SubMenus.Count > 0 && IsMenuExpanded(menu.MenuCode, menu.SubMenus))
{
expandedMenuGroups.Add(menu.MenuCode);
}
}
}
}
catch (Exception ex)
{
Console.WriteLine($"Error loading dynamic menus: {ex.Message}");
}
}
private void OnLocationChanged(object? sender, LocationChangedEventArgs e)
{
currentPath = Navigation.ToBaseRelativePath(e.Location).Trim('/');
RefreshExpandedMenuGroups();
_ = InvokeAsync(StateHasChanged);
}
private void OnLanguageChanged()
{
_ = InvokeAsync(StateHasChanged);
}
private List<MenuDto> ProcessMenus(List<MenuDto> rawMenus)
{
return rawMenus
.Where(menu => !string.Equals(menu.MenuCode, "ROLE_LAYOUTS", StringComparison.OrdinalIgnoreCase) &&
!string.Equals(menu.MenuCode, "DASHBOARD_WIDGETS", StringComparison.OrdinalIgnoreCase))
.OrderBy(menu => string.Equals(menu.MenuCode, "DASHBOARD", StringComparison.OrdinalIgnoreCase) ? 0 : 1)
.ThenBy(menu => menu.OrderNo)
.ThenBy(menu => AppLocalization.Menu(menu.MenuCode, menu.MenuName))
.ToList();
}
private async Task RefreshMenusAsync()
{
try
{
var authState = await AuthStateProvider.GetAuthenticationStateAsync();
var user = authState.User;
if (user.Identity?.IsAuthenticated == true)
{
var freshMenus = await MenuAuthorization.GetUserMenusAsync(user);
var processedMenus = ProcessMenus(freshMenus);
// Only update and re-render if the menu list actually changed.
if (processedMenus.Count != menus.Count ||
processedMenus.Select(m => m.MenuCode).SequenceEqual(menus.Select(m => m.MenuCode)) == false)
{
menus = processedMenus;
BuildUrlHierarchy(menus);
expandedMenuGroups.Clear();
foreach (var menu in menus)
{
if (menu.SubMenus != null && menu.SubMenus.Count > 0 && IsMenuExpanded(menu.MenuCode, menu.SubMenus))
{
expandedMenuGroups.Add(menu.MenuCode);
}
}
}
else
{
// Access items are the same - just refresh expanded state for the new route.
foreach (var menu in menus)
{
if (menu.SubMenus != null && menu.SubMenus.Count > 0 && IsMenuExpanded(menu.MenuCode, menu.SubMenus))
{
expandedMenuGroups.Add(menu.MenuCode);
}
}
}
}
}
catch (Exception ex)
{
Console.WriteLine($"Error refreshing menus on navigation: {ex.Message}");
}
StateHasChanged();
}
private void RefreshExpandedMenuGroups()
{
foreach (var menu in menus)
{
if (menu.SubMenus != null && menu.SubMenus.Count > 0 && IsMenuExpanded(menu.MenuCode, menu.SubMenus))
{
expandedMenuGroups.Add(menu.MenuCode);
}
}
}
private void ToggleMenuGroup(string menuCode)
{
if (!expandedMenuGroups.Remove(menuCode))
{
expandedMenuGroups.Add(menuCode);
}
}
private bool IsMenuGroupExpanded(string menuCode) => expandedMenuGroups.Contains(menuCode);
private bool IsMenuExpanded(string menuCode, IEnumerable<MenuDto> subMenus)
{
return subMenus.Any(s =>
!string.IsNullOrWhiteSpace(s.MenuUrl) &&
currentPath.StartsWith(s.MenuUrl.Trim('/'), StringComparison.OrdinalIgnoreCase));
}
private static NavLinkMatch GetNavLinkMatch(string? menuUrl)
{
return string.IsNullOrWhiteSpace(menuUrl) || menuUrl.Trim('/') is "dashboard" or ""
? NavLinkMatch.All
: NavLinkMatch.Prefix;
}
private static void BuildUrlHierarchy(List<MenuDto> menuList)
{
var toRemove = new List<MenuDto>();
foreach (var parent in menuList)
{
if (string.IsNullOrWhiteSpace(parent.MenuUrl))
{
continue;
}
if (parent.SubMenus != null && parent.SubMenus.Count > 0)
{
continue;
}
var parentPath = parent.MenuUrl.Trim('/');
if (string.IsNullOrEmpty(parentPath))
{
continue;
}
var children = menuList
.Where(m => m != parent
&& !string.IsNullOrWhiteSpace(m.MenuUrl)
&& m.MenuUrl.Trim('/').StartsWith(parentPath + "/", StringComparison.OrdinalIgnoreCase))
.OrderBy(m => m.OrderNo)
.ToList();
if (children.Count > 0)
{
parent.SubMenus = children;
toRemove.AddRange(children);
}
}
foreach (var item in toRemove)
{
menuList.Remove(item);
}
}
private static RenderFragment RenderIcon(string? icon) => builder =>
{
builder.OpenElement(0, "i");
builder.AddAttribute(1, "data-lucide", string.IsNullOrWhiteSpace(icon) ? "circle" : icon);
builder.AddAttribute(2, "class", "h-4 w-4 mr-3 text-slate-400 shrink-0");
builder.CloseElement();
};
private static RenderFragment RenderSmallIcon(string? icon) => builder =>
{
builder.OpenElement(0, "i");
builder.AddAttribute(1, "data-lucide", string.IsNullOrWhiteSpace(icon) ? "circle" : icon);
builder.AddAttribute(2, "class", "h-3.5 w-3.5 mr-3 text-slate-400 shrink-0");
builder.CloseElement();
};
protected override async Task OnAfterRenderAsync(bool firstRender)
{
try
{
await JS.InvokeVoidAsync("initIcons");
}
catch
{
}
}
public void Dispose()
{
Navigation.LocationChanged -= OnLocationChanged;
Language.Changed -= OnLanguageChanged;
}
}