Add GlobalNotificationService (#21)

This commit is contained in:
Leendert de Borst
2024-06-17 14:58:11 +02:00
parent bc40f19052
commit 388f899249
12 changed files with 189 additions and 180 deletions

View File

@@ -6,7 +6,7 @@
return;
}
<div class="p-4 mb-4 text-sm text-green-800 rounded-lg bg-green-50 dark:bg-gray-800 dark:text-green-400" role="alert">
<div class="p-4 mb-4 text-sm text-green-800 rounded-lg bg-green-50 dark:bg-gray-800 dark:text-green-400 border-2" role="alert">
@Message
</div>

View File

@@ -0,0 +1,72 @@
@inject GlobalNotificationService GlobalNotificationService
@implements IDisposable
@foreach (var message in Messages)
{
if (message.Key == "success")
{
<AlertMessageSuccess Message="@message.Value" />
}
}
@foreach (var message in Messages)
{
if (message.Key == "error")
{
<AlertMessageError Message="@message.Value" />
}
}
@code {
private Dictionary<string, string> Messages { get; set; } = new();
private bool _onChangeSubscribed = false;
/// <inheritdoc />
protected override async Task OnAfterRenderAsync(bool firstRender)
{
await base.OnAfterRenderAsync(firstRender);
if (firstRender)
{
// We subscribe to the OnChange event of the PortalMessageService to update the UI when a new message is added
RefreshAddMessages();
GlobalNotificationService.OnChange += RefreshAddMessages;
_onChangeSubscribed = true;
}
}
/// <inheritdoc />
public void Dispose()
{
// We unsubscribe from the OnChange event of the PortalMessageService when the component is disposed
if (_onChangeSubscribed)
{
GlobalNotificationService.OnChange -= RefreshAddMessages;
_onChangeSubscribed = false;
}
}
/// <summary>
/// Refreshes the messages by adding any new messages from the PortalMessageService.
/// </summary>
public void RefreshAddMessages()
{
// We retrieve any additional messages from the GlobalNotificationService that we do not yet have.
var newMessages = GlobalNotificationService.GetMessagesForDisplay();
foreach (var message in newMessages)
{
if (!Messages.ContainsValue(message.Value))
{
Messages.Add(message.Key, message.Value);
}
}
// Remove messages that are no longer in the GlobalNotificationService and have already been displayed.
var keysToRemove = Messages.Keys.Except(newMessages.Keys).ToList();
foreach (var key in keysToRemove)
{
Messages.Remove(key);
}
StateHasChanged();
}
}

View File

@@ -1,13 +1,11 @@
@page "/add-alias"
@page "/alias/{id:guid}/edit"
@inherits PageBase
@inject NavigationManager Navigation
@inject AliasService AliasService
@inherits PageBase
@using AliasGenerators.Implementations
@using AliasGenerators.Password.Implementations
@using AliasVault.Shared.Models.WebApi
@using AliasVault.WebApp.Services
@if (EditMode)
{
@@ -296,11 +294,13 @@ else
if (Id is not null)
{
await AliasService.UpdateAliasAsync(Obj, Id.Value);
GlobalNotificationService.AddSuccessMessage("Alias updated successfully.");
}
}
else
{
Id = await AliasService.InsertAliasAsync(Obj);
GlobalNotificationService.AddSuccessMessage("Alias created successfully.");
}
IsSaving = false;

View File

@@ -1,8 +1,6 @@
@page "/alias/{id:guid}/delete"
@using AliasVault.Shared.Models.WebApi
@using AliasVault.WebApp.Services
@inherits PageBase
@using AliasVault.Shared.Models.WebApi
@inject AliasService AliasService
<LayoutPageTitle>Delete alias</LayoutPageTitle>

View File

@@ -1,9 +1,8 @@
@page "/alias/{id:guid}"
@inherits PageBase
@using AliasVault.Shared.Models.WebApi
@using AliasVault.WebApp.Services
@using AliasVault.WebApp.Components.Email
@inject AliasService AliasService
@inherits PageBase
<LayoutPageTitle>View alias</LayoutPageTitle>
@@ -13,9 +12,12 @@
}
else
{
<div class="grid grid-cols-2 px-4 pt-6 md:grid-cols-3 lg:gap-4 dark:bg-gray-900">
<div class="mb-4 col-span-full xl:mb-2">
<Breadcrumb BreadcrumbItems="BreadcrumbItems"/>
<GlobalNotificationDisplay />
<div class="flex items-center justify-between">
<h1 class="text-xl font-semibold text-gray-900 sm:text-2xl dark:text-white">View alias</h1>
<div class="flex">
@@ -28,6 +30,7 @@ else
</div>
</div>
</div>
<!-- Right Content -->
<div class="col-span-full lg:col-auto">
<div class="p-4 mb-4 bg-white border border-gray-200 rounded-lg shadow-sm 2xl:col-span-2 dark:border-gray-700 sm:p-6 dark:bg-gray-800">

View File

@@ -8,6 +8,7 @@
namespace AliasVault.WebApp.Pages.Base;
using AliasVault.WebApp.Components.Models;
using AliasVault.WebApp.Services;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.JSInterop;
@@ -33,6 +34,12 @@ public class PageBase : OwningComponentBase
[Inject]
public AuthenticationStateProvider AuthStateProvider { get; set; } = null!;
/// <summary>
/// Gets or sets the GlobalNotificationService.
/// </summary>
[Inject]
public GlobalNotificationService GlobalNotificationService { get; set; } = null!;
/// <summary>
/// Gets or sets the IJSRuntime.
/// </summary>

View File

@@ -1,9 +1,8 @@
@page "/"
@page "/aliases"
@using AliasVault.WebApp.Components.Alias
@using AliasVault.WebApp.Services
@inject AliasService AliasService
@inherits PageBase
@using AliasVault.WebApp.Components.Alias
@inject AliasService AliasService
<LayoutPageTitle>Home</LayoutPageTitle>
@@ -22,13 +21,13 @@
@if (IsLoading)
{
<LoadingIndicator />
<LoadingIndicator />
}
<div class="grid gap-4 px-4 mb-4 md:grid-cols-4 xl:grid-cols-6">
@foreach (var alias in Aliases)
{
<Alias Obj="@alias"/>
<Alias Obj="@alias"/>
}
</div>

View File

@@ -36,6 +36,7 @@ builder.Services.AddTransient<AliasVaultApiHandlerService>();
builder.Services.AddScoped<AuthService>();
builder.Services.AddScoped<AuthenticationStateProvider, AuthStateProvider>();
builder.Services.AddScoped<AliasService>();
builder.Services.AddScoped<GlobalNotificationService>();
builder.Services.AddSingleton<ClipboardCopyService>();
builder.Services.AddAuthorizationCore();
builder.Services.AddBlazoredLocalStorage();

View File

@@ -0,0 +1,63 @@
//-----------------------------------------------------------------------
// <copyright file="GlobalNotificationService.cs" company="lanedirt">
// Copyright (c) lanedirt. All rights reserved.
// Licensed under the MIT license. See LICENSE.md file in the project root for full license information.
// </copyright>
//-----------------------------------------------------------------------
namespace AliasVault.WebApp.Services;
/// <summary>
/// Handles global notifications that should be displayed to the user, such as success or error messages. These messages
/// are stored in this object which is scoped to the current session. This allows the messages to be cached until
/// they actually have been displayed. So they can survive redirects and page reloads.
/// </summary>
public class GlobalNotificationService
{
/// <summary>
/// Allow other components to subscribe to changes in the event object.
/// </summary>
public event Action? OnChange;
/// <summary>
/// Gets or sets success messages that should be displayed to the user. A default set of success messages is added in the parent OnInitialized method.
/// </summary>
protected List<string> SuccessMessages { get; set; } = new List<string>();
/// <summary>
/// Adds a success message to the list of messages that should be displayed to the user.
/// </summary>
/// <param name="message">The message to add.</param>
/// <param name="notifyStateChanged">Whether to notify state change to subscribers. Defaults to true.</param>
public void AddSuccessMessage(string message, bool notifyStateChanged = true)
{
SuccessMessages.Add(message);
// Notify subscribers that a message has been added.
if (notifyStateChanged)
{
NotifyStateChanged();
}
}
/// <summary>
/// Returns a dictionary with messages that should be displayed to the user. After this method is called,
/// the messages are automatically cleared.
/// </summary>
/// <returns>Dictionary with messages that are ready to be displayed on the next page load.</returns>
public Dictionary<string, string> GetMessagesForDisplay()
{
var messages = new Dictionary<string, string>();
foreach (var message in SuccessMessages)
{
messages.Add("success", message);
}
// Clear messages
SuccessMessages.Clear();
return messages;
}
private void NotifyStateChanged() => OnChange?.Invoke();
}

View File

@@ -16,6 +16,7 @@
@using AliasVault.WebApp.Components.Alerts
@using AliasVault.WebApp.Components.Loading
@using AliasVault.WebApp.Pages.Base
@using AliasVault.WebApp.Services
@using Microsoft.AspNetCore.Components.Authorization
@using Microsoft.AspNetCore.Components.WebAssembly.Authentication
@using Blazored.LocalStorage

View File

@@ -714,10 +714,6 @@ video {
margin-top: 2rem;
}
.me-2 {
margin-inline-end: 0.5rem;
}
.block {
display: block;
}
@@ -770,10 +766,6 @@ video {
height: 1.5rem;
}
.h-7 {
height: 1.75rem;
}
.h-8 {
height: 2rem;
}
@@ -786,10 +778,6 @@ video {
height: 100%;
}
.h-3 {
height: 0.75rem;
}
.w-1\/2 {
width: 50%;
}
@@ -822,10 +810,6 @@ video {
width: 16rem;
}
.w-7 {
width: 1.75rem;
}
.w-8 {
width: 2rem;
}
@@ -834,10 +818,6 @@ video {
width: 100%;
}
.w-3 {
width: 0.75rem;
}
.min-w-full {
min-width: 100%;
}
@@ -938,12 +918,6 @@ video {
margin-left: calc(0.25rem * calc(1 - var(--tw-space-x-reverse)));
}
.space-x-2 > :not([hidden]) ~ :not([hidden]) {
--tw-space-x-reverse: 0;
margin-right: calc(0.5rem * var(--tw-space-x-reverse));
margin-left: calc(0.5rem * calc(1 - var(--tw-space-x-reverse)));
}
.space-x-6 > :not([hidden]) ~ :not([hidden]) {
--tw-space-x-reverse: 0;
margin-right: calc(1.5rem * var(--tw-space-x-reverse));
@@ -1016,10 +990,6 @@ video {
border-radius: 0.5rem;
}
.rounded-md {
border-radius: 0.375rem;
}
.rounded-l-lg {
border-top-left-radius: 0.5rem;
border-bottom-left-radius: 0.5rem;
@@ -1046,6 +1016,11 @@ video {
border-top-width: 1px;
}
.border-blue-700 {
--tw-border-opacity: 1;
border-color: rgb(29 78 216 / var(--tw-border-opacity));
}
.border-gray-200 {
--tw-border-opacity: 1;
border-color: rgb(229 231 235 / var(--tw-border-opacity));
@@ -1056,31 +1031,11 @@ video {
border-color: rgb(209 213 219 / var(--tw-border-opacity));
}
.border-green-100 {
--tw-border-opacity: 1;
border-color: rgb(220 252 231 / var(--tw-border-opacity));
}
.border-green-500 {
--tw-border-opacity: 1;
border-color: rgb(34 197 94 / var(--tw-border-opacity));
}
.border-orange-100 {
--tw-border-opacity: 1;
border-color: rgb(255 237 213 / var(--tw-border-opacity));
}
.border-purple-100 {
--tw-border-opacity: 1;
border-color: rgb(243 232 255 / var(--tw-border-opacity));
}
.border-blue-700 {
--tw-border-opacity: 1;
border-color: rgb(29 78 216 / var(--tw-border-opacity));
}
.bg-blue-600 {
--tw-bg-opacity: 1;
background-color: rgb(37 99 235 / var(--tw-bg-opacity));
@@ -1111,11 +1066,6 @@ video {
background-color: rgb(17 24 39 / var(--tw-bg-opacity));
}
.bg-green-100 {
--tw-bg-opacity: 1;
background-color: rgb(220 252 231 / var(--tw-bg-opacity));
}
.bg-green-50 {
--tw-bg-opacity: 1;
background-color: rgb(240 253 244 / var(--tw-bg-opacity));
@@ -1126,11 +1076,6 @@ video {
background-color: rgb(22 163 74 / var(--tw-bg-opacity));
}
.bg-orange-100 {
--tw-bg-opacity: 1;
background-color: rgb(255 237 213 / var(--tw-bg-opacity));
}
.bg-primary-600 {
--tw-bg-opacity: 1;
background-color: rgb(214 131 56 / var(--tw-bg-opacity));
@@ -1141,11 +1086,6 @@ video {
background-color: rgb(184 112 47 / var(--tw-bg-opacity));
}
.bg-purple-100 {
--tw-bg-opacity: 1;
background-color: rgb(243 232 255 / var(--tw-bg-opacity));
}
.bg-red-50 {
--tw-bg-opacity: 1;
background-color: rgb(254 242 242 / var(--tw-bg-opacity));
@@ -1161,11 +1101,6 @@ video {
background-color: rgb(255 255 255 / var(--tw-bg-opacity));
}
.bg-blue-700 {
--tw-bg-opacity: 1;
background-color: rgb(29 78 216 / var(--tw-bg-opacity));
}
.bg-opacity-50 {
--tw-bg-opacity: 0.5;
}
@@ -1190,16 +1125,6 @@ video {
padding: 1.5rem;
}
.px-2 {
padding-left: 0.5rem;
padding-right: 0.5rem;
}
.px-2\.5 {
padding-left: 0.625rem;
padding-right: 0.625rem;
}
.px-3 {
padding-left: 0.75rem;
padding-right: 0.75rem;
@@ -1220,16 +1145,6 @@ video {
padding-right: 1.5rem;
}
.py-0 {
padding-top: 0px;
padding-bottom: 0px;
}
.py-0\.5 {
padding-top: 0.125rem;
padding-bottom: 0.125rem;
}
.py-1 {
padding-top: 0.25rem;
padding-bottom: 0.25rem;
@@ -1352,6 +1267,11 @@ video {
letter-spacing: 0.05em;
}
.text-blue-700 {
--tw-text-opacity: 1;
color: rgb(29 78 216 / var(--tw-text-opacity));
}
.text-gray-200 {
--tw-text-opacity: 1;
color: rgb(229 231 235 / var(--tw-text-opacity));
@@ -1387,21 +1307,11 @@ video {
color: rgb(22 101 52 / var(--tw-text-opacity));
}
.text-orange-800 {
--tw-text-opacity: 1;
color: rgb(154 52 18 / var(--tw-text-opacity));
}
.text-primary-700 {
--tw-text-opacity: 1;
color: rgb(184 112 47 / var(--tw-text-opacity));
}
.text-purple-800 {
--tw-text-opacity: 1;
color: rgb(107 33 168 / var(--tw-text-opacity));
}
.text-red-800 {
--tw-text-opacity: 1;
color: rgb(153 27 27 / var(--tw-text-opacity));
@@ -1412,11 +1322,6 @@ video {
color: rgb(255 255 255 / var(--tw-text-opacity));
}
.text-blue-700 {
--tw-text-opacity: 1;
color: rgb(29 78 216 / var(--tw-text-opacity));
}
.opacity-0 {
opacity: 0;
}
@@ -1478,6 +1383,11 @@ video {
background-color: rgb(229 231 235 / var(--tw-bg-opacity));
}
.hover\:bg-gray-50:hover {
--tw-bg-opacity: 1;
background-color: rgb(249 250 251 / var(--tw-bg-opacity));
}
.hover\:bg-gray-800:hover {
--tw-bg-opacity: 1;
background-color: rgb(31 41 55 / var(--tw-bg-opacity));
@@ -1508,16 +1418,6 @@ video {
background-color: rgb(153 27 27 / var(--tw-bg-opacity));
}
.hover\:bg-blue-800:hover {
--tw-bg-opacity: 1;
background-color: rgb(30 64 175 / var(--tw-bg-opacity));
}
.hover\:bg-gray-50:hover {
--tw-bg-opacity: 1;
background-color: rgb(249 250 251 / var(--tw-bg-opacity));
}
.hover\:text-gray-900:hover {
--tw-text-opacity: 1;
color: rgb(17 24 39 / var(--tw-text-opacity));
@@ -1598,6 +1498,11 @@ video {
border-color: rgb(75 85 99 / var(--tw-divide-opacity));
}
.dark\:border-blue-500:is(.dark *) {
--tw-border-opacity: 1;
border-color: rgb(59 130 246 / var(--tw-border-opacity));
}
.dark\:border-gray-600:is(.dark *) {
--tw-border-opacity: 1;
border-color: rgb(75 85 99 / var(--tw-border-opacity));
@@ -1608,26 +1513,6 @@ video {
border-color: rgb(55 65 81 / var(--tw-border-opacity));
}
.dark\:border-green-500:is(.dark *) {
--tw-border-opacity: 1;
border-color: rgb(34 197 94 / var(--tw-border-opacity));
}
.dark\:border-orange-300:is(.dark *) {
--tw-border-opacity: 1;
border-color: rgb(253 186 116 / var(--tw-border-opacity));
}
.dark\:border-purple-500:is(.dark *) {
--tw-border-opacity: 1;
border-color: rgb(168 85 247 / var(--tw-border-opacity));
}
.dark\:border-blue-500:is(.dark *) {
--tw-border-opacity: 1;
border-color: rgb(59 130 246 / var(--tw-border-opacity));
}
.dark\:bg-blue-500:is(.dark *) {
--tw-bg-opacity: 1;
background-color: rgb(59 130 246 / var(--tw-bg-opacity));
@@ -1673,15 +1558,15 @@ video {
background-color: rgb(239 68 68 / var(--tw-bg-opacity));
}
.dark\:bg-blue-600:is(.dark *) {
--tw-bg-opacity: 1;
background-color: rgb(37 99 235 / var(--tw-bg-opacity));
}
.dark\:bg-opacity-80:is(.dark *) {
--tw-bg-opacity: 0.8;
}
.dark\:text-blue-500:is(.dark *) {
--tw-text-opacity: 1;
color: rgb(59 130 246 / var(--tw-text-opacity));
}
.dark\:text-gray-300:is(.dark *) {
--tw-text-opacity: 1;
color: rgb(209 213 219 / var(--tw-text-opacity));
@@ -1702,11 +1587,6 @@ video {
color: rgb(74 222 128 / var(--tw-text-opacity));
}
.dark\:text-orange-300:is(.dark *) {
--tw-text-opacity: 1;
color: rgb(253 186 116 / var(--tw-text-opacity));
}
.dark\:text-primary-200:is(.dark *) {
--tw-text-opacity: 1;
color: rgb(251 203 116 / var(--tw-text-opacity));
@@ -1717,11 +1597,6 @@ video {
color: rgb(244 149 65 / var(--tw-text-opacity));
}
.dark\:text-purple-400:is(.dark *) {
--tw-text-opacity: 1;
color: rgb(192 132 252 / var(--tw-text-opacity));
}
.dark\:text-red-400:is(.dark *) {
--tw-text-opacity: 1;
color: rgb(248 113 113 / var(--tw-text-opacity));
@@ -1732,11 +1607,6 @@ video {
color: rgb(255 255 255 / var(--tw-text-opacity));
}
.dark\:text-blue-500:is(.dark *) {
--tw-text-opacity: 1;
color: rgb(59 130 246 / var(--tw-text-opacity));
}
.dark\:placeholder-gray-400:is(.dark *)::-moz-placeholder {
--tw-placeholder-opacity: 1;
color: rgb(156 163 175 / var(--tw-placeholder-opacity));
@@ -1751,6 +1621,11 @@ video {
--tw-ring-offset-color: #1f2937;
}
.dark\:hover\:bg-blue-500:hover:is(.dark *) {
--tw-bg-opacity: 1;
background-color: rgb(59 130 246 / var(--tw-bg-opacity));
}
.dark\:hover\:bg-blue-600:hover:is(.dark *) {
--tw-bg-opacity: 1;
background-color: rgb(37 99 235 / var(--tw-bg-opacity));
@@ -1786,16 +1661,6 @@ video {
background-color: rgb(220 38 38 / var(--tw-bg-opacity));
}
.dark\:hover\:bg-blue-700:hover:is(.dark *) {
--tw-bg-opacity: 1;
background-color: rgb(29 78 216 / var(--tw-bg-opacity));
}
.dark\:hover\:bg-blue-500:hover:is(.dark *) {
--tw-bg-opacity: 1;
background-color: rgb(59 130 246 / var(--tw-bg-opacity));
}
.dark\:hover\:text-primary-500:hover:is(.dark *) {
--tw-text-opacity: 1;
color: rgb(244 149 65 / var(--tw-text-opacity));

View File

@@ -86,8 +86,8 @@ public class AliasTests : PlaywrightTest
await submitButton.ClickAsync();
await WaitForURLAsync("**/alias/**", "View alias");
// Check if the alias was correctly updated.
pageContent = await Page.TextContentAsync("body");
Assert.That(pageContent, Does.Contain("Alias updated"), "Alias update confirmation message not shown.");
Assert.That(pageContent, Does.Contain(serviceNameAfter), "Alias not updated correctly.");
}