Spaces:
Running
Running
| @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; | |
| } | |
| } | |