diff --git a/src/ScadaLink.CentralUI/Components/Pages/Notifications/NotificationLists.razor b/src/ScadaLink.CentralUI/Components/Pages/Notifications/NotificationLists.razor new file mode 100644 index 0000000..371e156 --- /dev/null +++ b/src/ScadaLink.CentralUI/Components/Pages/Notifications/NotificationLists.razor @@ -0,0 +1,137 @@ +@page "/notifications/lists" +@using ScadaLink.Security +@using ScadaLink.Commons.Entities.Notifications +@using ScadaLink.Commons.Interfaces.Repositories +@attribute [Authorize(Policy = AuthorizationPolicies.RequireDesign)] +@inject INotificationRepository NotificationRepository +@inject NavigationManager NavigationManager +@inject IDialogService Dialog + +
+ + +
+

Notification Lists

+ +
+ + @if (_loading) + { + + } + else if (_errorMessage != null) + { +
@_errorMessage
+ } + else if (_lists.Count == 0) + { +
+
+
No notification lists
+ +
+
+ } + else + { +
+ + + + + + + + + + @foreach (var list in _lists) + { + var recipients = _recipients.GetValueOrDefault(list.Id) + ?? (IReadOnlyList)Array.Empty(); + + + + + + } + +
NameRecipientsActions
@list.Name + @if (recipients.Count == 0) + { + No recipients + } + else + { + @foreach (var r in recipients) + { + @r.Name <@r.EmailAddress> + } + } + + + +
+
+ } +
+ +@code { + private bool _loading = true; + private string? _errorMessage; + private List _lists = new(); + private readonly Dictionary> _recipients = new(); + private ToastNotification _toast = default!; + + protected override async Task OnInitializedAsync() => await LoadAsync(); + + private async Task LoadAsync() + { + _loading = true; + _errorMessage = null; + try + { + _lists = (await NotificationRepository.GetAllNotificationListsAsync()).ToList(); + _recipients.Clear(); + foreach (var list in _lists) + { + _recipients[list.Id] = await NotificationRepository.GetRecipientsByListIdAsync(list.Id); + } + } + catch (Exception ex) + { + _errorMessage = ex.Message; + } + _loading = false; + } + + private async Task DeleteList(NotificationList list) + { + if (!await Dialog.ConfirmAsync("Delete", $"Delete notification list '{list.Name}'?", danger: true)) + { + return; + } + try + { + await NotificationRepository.DeleteNotificationListAsync(list.Id); + await NotificationRepository.SaveChangesAsync(); + _toast.ShowSuccess("Deleted."); + await LoadAsync(); + } + catch (Exception ex) + { + _toast.ShowError(ex.Message); + } + } +} diff --git a/tests/ScadaLink.CentralUI.Tests/Pages/NotificationListsPageTests.cs b/tests/ScadaLink.CentralUI.Tests/Pages/NotificationListsPageTests.cs new file mode 100644 index 0000000..c998130 --- /dev/null +++ b/tests/ScadaLink.CentralUI.Tests/Pages/NotificationListsPageTests.cs @@ -0,0 +1,76 @@ +using System.Security.Claims; +using Bunit; +using Microsoft.AspNetCore.Components.Authorization; +using Microsoft.Extensions.DependencyInjection; +using NSubstitute; +using ScadaLink.CentralUI.Components.Shared; +using ScadaLink.Commons.Entities.Notifications; +using ScadaLink.Commons.Interfaces.Repositories; +using NotificationListsPage = ScadaLink.CentralUI.Components.Pages.Notifications.NotificationLists; + +namespace ScadaLink.CentralUI.Tests.Pages; + +/// +/// bUnit rendering tests for the standalone Notification Lists page (Task 7). +/// +public class NotificationListsPageTests : BunitContext +{ + private void WireAuthAndDialog() + { + Services.AddSingleton(new AlwaysConfirmDialogService()); + + var claims = new[] + { + new Claim("Username", "tester"), + new Claim(ClaimTypes.Role, "Design"), + }; + var user = new ClaimsPrincipal(new ClaimsIdentity(claims, "TestAuth")); + Services.AddSingleton(new TestAuthStateProvider(user)); + Services.AddAuthorizationCore(); + } + + [Fact] + public void RendersNotificationListRows() + { + var repo = Substitute.For(); + repo.GetAllNotificationListsAsync() + .Returns(Task.FromResult>( + new List { new("Ops On-Call") { Id = 1 } })); + repo.GetRecipientsByListIdAsync(1) + .Returns(Task.FromResult>( + new List { new("Jane", "jane@example.com") })); + Services.AddSingleton(repo); + WireAuthAndDialog(); + + var cut = Render(); + + Assert.Contains("Ops On-Call", cut.Markup); + Assert.Contains("jane@example.com", cut.Markup); + } + + [Fact] + public void ShowsEmptyState_WhenNoLists() + { + var repo = Substitute.For(); + repo.GetAllNotificationListsAsync() + .Returns(Task.FromResult>( + new List())); + Services.AddSingleton(repo); + WireAuthAndDialog(); + + var cut = Render(); + + Assert.Contains("No notification lists", cut.Markup); + } + + /// A dialog service that auto-confirms, so action paths run end-to-end. + private sealed class AlwaysConfirmDialogService : IDialogService + { + public Task ConfirmAsync(string title, string message, bool danger = false) + => Task.FromResult(true); + + public Task PromptAsync( + string title, string label, string initialValue = "", string? placeholder = null) + => Task.FromResult(null); + } +}