diff --git a/src/AliasVault.WebApp/Components/Alerts/AlertMessageSuccess.razor b/src/AliasVault.WebApp/Components/Alerts/AlertMessageSuccess.razor
index fe91b43a8..cfb289137 100644
--- a/src/AliasVault.WebApp/Components/Alerts/AlertMessageSuccess.razor
+++ b/src/AliasVault.WebApp/Components/Alerts/AlertMessageSuccess.razor
@@ -6,7 +6,7 @@
return;
}
-
+
@Message
diff --git a/src/AliasVault.WebApp/Components/Alerts/GlobalNotificationDisplay.razor b/src/AliasVault.WebApp/Components/Alerts/GlobalNotificationDisplay.razor
new file mode 100644
index 000000000..d3632b64c
--- /dev/null
+++ b/src/AliasVault.WebApp/Components/Alerts/GlobalNotificationDisplay.razor
@@ -0,0 +1,72 @@
+@inject GlobalNotificationService GlobalNotificationService
+@implements IDisposable
+
+@foreach (var message in Messages)
+{
+ if (message.Key == "success")
+ {
+
+ }
+}
+@foreach (var message in Messages)
+{
+ if (message.Key == "error")
+ {
+
+ }
+}
+
+@code {
+ private Dictionary
Messages { get; set; } = new();
+ private bool _onChangeSubscribed = false;
+
+ ///
+ 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;
+ }
+ }
+
+ ///
+ public void Dispose()
+ {
+ // We unsubscribe from the OnChange event of the PortalMessageService when the component is disposed
+ if (_onChangeSubscribed)
+ {
+ GlobalNotificationService.OnChange -= RefreshAddMessages;
+ _onChangeSubscribed = false;
+ }
+ }
+
+ ///
+ /// Refreshes the messages by adding any new messages from the PortalMessageService.
+ ///
+ 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();
+ }
+}
diff --git a/src/AliasVault.WebApp/Pages/Aliases/AddEdit.razor b/src/AliasVault.WebApp/Pages/Aliases/AddEdit.razor
index 0a28987cc..8536f45c0 100644
--- a/src/AliasVault.WebApp/Pages/Aliases/AddEdit.razor
+++ b/src/AliasVault.WebApp/Pages/Aliases/AddEdit.razor
@@ -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;
diff --git a/src/AliasVault.WebApp/Pages/Aliases/Delete.razor b/src/AliasVault.WebApp/Pages/Aliases/Delete.razor
index 66be0d5b5..7af6464a2 100644
--- a/src/AliasVault.WebApp/Pages/Aliases/Delete.razor
+++ b/src/AliasVault.WebApp/Pages/Aliases/Delete.razor
@@ -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
Delete alias
diff --git a/src/AliasVault.WebApp/Pages/Aliases/View.razor b/src/AliasVault.WebApp/Pages/Aliases/View.razor
index b1fa9007e..b1ace6222 100644
--- a/src/AliasVault.WebApp/Pages/Aliases/View.razor
+++ b/src/AliasVault.WebApp/Pages/Aliases/View.razor
@@ -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
View alias
@@ -13,9 +12,12 @@
}
else
{
+
+
+
View alias
@@ -28,6 +30,7 @@ else
+
diff --git a/src/AliasVault.WebApp/Pages/Base/PageBase.cs b/src/AliasVault.WebApp/Pages/Base/PageBase.cs
index fe2f2baaa..7ed2e099c 100644
--- a/src/AliasVault.WebApp/Pages/Base/PageBase.cs
+++ b/src/AliasVault.WebApp/Pages/Base/PageBase.cs
@@ -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!;
+ ///
+ /// Gets or sets the GlobalNotificationService.
+ ///
+ [Inject]
+ public GlobalNotificationService GlobalNotificationService { get; set; } = null!;
+
///
/// Gets or sets the IJSRuntime.
///
diff --git a/src/AliasVault.WebApp/Pages/Home.razor b/src/AliasVault.WebApp/Pages/Home.razor
index 2b86a9826..5a8c0ae00 100644
--- a/src/AliasVault.WebApp/Pages/Home.razor
+++ b/src/AliasVault.WebApp/Pages/Home.razor
@@ -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
Home
@@ -22,13 +21,13 @@
@if (IsLoading)
{
-
+
}
@foreach (var alias in Aliases)
{
-
+
}
diff --git a/src/AliasVault.WebApp/Program.cs b/src/AliasVault.WebApp/Program.cs
index 698dc46d4..1e63afc28 100644
--- a/src/AliasVault.WebApp/Program.cs
+++ b/src/AliasVault.WebApp/Program.cs
@@ -36,6 +36,7 @@ builder.Services.AddTransient
();
builder.Services.AddScoped();
builder.Services.AddScoped();
builder.Services.AddScoped();
+builder.Services.AddScoped();
builder.Services.AddSingleton();
builder.Services.AddAuthorizationCore();
builder.Services.AddBlazoredLocalStorage();
diff --git a/src/AliasVault.WebApp/Services/GlobalNotificationService.cs b/src/AliasVault.WebApp/Services/GlobalNotificationService.cs
new file mode 100644
index 000000000..eb81ac32d
--- /dev/null
+++ b/src/AliasVault.WebApp/Services/GlobalNotificationService.cs
@@ -0,0 +1,63 @@
+//-----------------------------------------------------------------------
+//
+// Copyright (c) lanedirt. All rights reserved.
+// Licensed under the MIT license. See LICENSE.md file in the project root for full license information.
+//
+//-----------------------------------------------------------------------
+
+namespace AliasVault.WebApp.Services;
+
+///
+/// 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.
+///
+public class GlobalNotificationService
+{
+ ///
+ /// Allow other components to subscribe to changes in the event object.
+ ///
+ public event Action? OnChange;
+
+ ///
+ /// 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.
+ ///
+ protected List SuccessMessages { get; set; } = new List();
+
+ ///
+ /// Adds a success message to the list of messages that should be displayed to the user.
+ ///
+ /// The message to add.
+ /// Whether to notify state change to subscribers. Defaults to true.
+ public void AddSuccessMessage(string message, bool notifyStateChanged = true)
+ {
+ SuccessMessages.Add(message);
+
+ // Notify subscribers that a message has been added.
+ if (notifyStateChanged)
+ {
+ NotifyStateChanged();
+ }
+ }
+
+ ///
+ /// Returns a dictionary with messages that should be displayed to the user. After this method is called,
+ /// the messages are automatically cleared.
+ ///
+ /// Dictionary with messages that are ready to be displayed on the next page load.
+ public Dictionary GetMessagesForDisplay()
+ {
+ var messages = new Dictionary();
+ foreach (var message in SuccessMessages)
+ {
+ messages.Add("success", message);
+ }
+
+ // Clear messages
+ SuccessMessages.Clear();
+
+ return messages;
+ }
+
+ private void NotifyStateChanged() => OnChange?.Invoke();
+}
diff --git a/src/AliasVault.WebApp/_Imports.razor b/src/AliasVault.WebApp/_Imports.razor
index 98b53de48..710bff927 100644
--- a/src/AliasVault.WebApp/_Imports.razor
+++ b/src/AliasVault.WebApp/_Imports.razor
@@ -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
diff --git a/src/AliasVault.WebApp/wwwroot/css/tailwind.css b/src/AliasVault.WebApp/wwwroot/css/tailwind.css
index 1e3fcd9c7..814fc57f5 100644
--- a/src/AliasVault.WebApp/wwwroot/css/tailwind.css
+++ b/src/AliasVault.WebApp/wwwroot/css/tailwind.css
@@ -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));
diff --git a/src/Tests/AliasVault.E2ETests/AliasTests.cs b/src/Tests/AliasVault.E2ETests/AliasTests.cs
index 6c539f968..dd587cad5 100644
--- a/src/Tests/AliasVault.E2ETests/AliasTests.cs
+++ b/src/Tests/AliasVault.E2ETests/AliasTests.cs
@@ -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.");
}