From 5eecba7eec4d58d490b33bcda4aedfd87d0d1572 Mon Sep 17 00:00:00 2001 From: Brian Bicknell Date: Sat, 9 May 2026 00:51:10 -0400 Subject: [PATCH] Updated delete logic for voice works. --- JSMR.Api/Startup/WebApplicationExtensions.cs | 17 +++++++++ .../Delete/DeleteVoiceWorkResponse.cs | 2 +- .../Commands/Delete/DeleteVoiceWorkStatus.cs | 9 +++++ .../VoiceWorks/VoiceWorkWriter.cs | 21 ++++++++--- .../VoiceWorks/Delete_Voice_Work_Tests.cs | 12 +++--- JSMR.UI.Blazor/Components/JProduct.razor | 37 ++++++++++++++++++- .../Components/JProductCollection.razor | 10 ++++- JSMR.UI.Blazor/Exceptions/ApiException.cs | 9 +++++ JSMR.UI.Blazor/Pages/Home.razor | 7 +++- JSMR.UI.Blazor/Services/VoiceWorksClient.cs | 16 +++++++- 10 files changed, 124 insertions(+), 16 deletions(-) create mode 100644 JSMR.Application/VoiceWorks/Commands/Delete/DeleteVoiceWorkStatus.cs create mode 100644 JSMR.UI.Blazor/Exceptions/ApiException.cs diff --git a/JSMR.Api/Startup/WebApplicationExtensions.cs b/JSMR.Api/Startup/WebApplicationExtensions.cs index 356c35b..844a496 100644 --- a/JSMR.Api/Startup/WebApplicationExtensions.cs +++ b/JSMR.Api/Startup/WebApplicationExtensions.cs @@ -53,6 +53,23 @@ public static class WebApplicationExtensions } }); + app.UseExceptionHandler(errorApp => + { + errorApp.Run(async context => + { + context.Response.StatusCode = StatusCodes.Status500InternalServerError; + context.Response.ContentType = "application/problem+json"; + + await Results.Problem( + title: "An unexpected error occurred.", + detail: app.Environment.IsDevelopment() + ? "Check the API logs for details." + : null, + statusCode: StatusCodes.Status500InternalServerError + ).ExecuteAsync(context); + }); + }); + return app; } diff --git a/JSMR.Application/VoiceWorks/Commands/Delete/DeleteVoiceWorkResponse.cs b/JSMR.Application/VoiceWorks/Commands/Delete/DeleteVoiceWorkResponse.cs index a48b637..8a1bdce 100644 --- a/JSMR.Application/VoiceWorks/Commands/Delete/DeleteVoiceWorkResponse.cs +++ b/JSMR.Application/VoiceWorks/Commands/Delete/DeleteVoiceWorkResponse.cs @@ -1,3 +1,3 @@ namespace JSMR.Application.VoiceWorks.Commands.Delete; -public sealed record DeleteVoiceWorkResponse(Dictionary IsSuccess); \ No newline at end of file +public sealed record DeleteVoiceWorkResponse(Dictionary Results); \ No newline at end of file diff --git a/JSMR.Application/VoiceWorks/Commands/Delete/DeleteVoiceWorkStatus.cs b/JSMR.Application/VoiceWorks/Commands/Delete/DeleteVoiceWorkStatus.cs new file mode 100644 index 0000000..e584d39 --- /dev/null +++ b/JSMR.Application/VoiceWorks/Commands/Delete/DeleteVoiceWorkStatus.cs @@ -0,0 +1,9 @@ +namespace JSMR.Application.VoiceWorks.Commands.Delete; + +public enum DeleteVoiceWorkStatus +{ + Deleted, + NotFound, + NotAllowed, + Failed +} \ No newline at end of file diff --git a/JSMR.Infrastructure/Data/Repositories/VoiceWorks/VoiceWorkWriter.cs b/JSMR.Infrastructure/Data/Repositories/VoiceWorks/VoiceWorkWriter.cs index 8d25a69..bc85128 100644 --- a/JSMR.Infrastructure/Data/Repositories/VoiceWorks/VoiceWorkWriter.cs +++ b/JSMR.Infrastructure/Data/Repositories/VoiceWorks/VoiceWorkWriter.cs @@ -20,28 +20,39 @@ public class VoiceWorkWriter(AppDbContext dbContext) : IVoiceWorkWriter public async Task DeleteAsync(DeleteVoiceWorkRequest request, CancellationToken cancellationToken) { - Dictionary isSuccess = []; + Dictionary results = request.VoiceWorkIds.Select(x => x) + .ToDictionary(x => x, x => DeleteVoiceWorkStatus.NotFound); VoiceWork[] voiceWorks = [.. dbContext.VoiceWorks.Where(voiceWork => request.VoiceWorkIds.Contains(voiceWork.VoiceWorkId)) .Include(x => x.Circle)]; foreach (VoiceWork voiceWork in voiceWorks) { - isSuccess.Add(voiceWork.VoiceWorkId, false); + if (results.ContainsKey(voiceWork.VoiceWorkId) == false) + { + results[voiceWork.VoiceWorkId] = DeleteVoiceWorkStatus.NotFound; + continue; + } if (voiceWork.Circle is null) + { + results[voiceWork.VoiceWorkId] = DeleteVoiceWorkStatus.NotFound; continue; + } if (voiceWork.IsValid == true && voiceWork.Circle.Spam == false) + { + results[voiceWork.VoiceWorkId] = DeleteVoiceWorkStatus.NotAllowed; continue; + } dbContext.Remove(voiceWork); - isSuccess[voiceWork.VoiceWorkId] = true; + results[voiceWork.VoiceWorkId] = DeleteVoiceWorkStatus.Deleted; } - dbContext.SaveChanges(); + await dbContext.SaveChangesAsync(cancellationToken); - return new DeleteVoiceWorkResponse(isSuccess); + return new DeleteVoiceWorkResponse(results); } private async Task GetVoiceWorkAsync(int voiceWorkId, CancellationToken cancellationToken) diff --git a/JSMR.Tests/Data/Repositories/VoiceWorks/Delete_Voice_Work_Tests.cs b/JSMR.Tests/Data/Repositories/VoiceWorks/Delete_Voice_Work_Tests.cs index d7c7a5f..4401538 100644 --- a/JSMR.Tests/Data/Repositories/VoiceWorks/Delete_Voice_Work_Tests.cs +++ b/JSMR.Tests/Data/Repositories/VoiceWorks/Delete_Voice_Work_Tests.cs @@ -18,9 +18,9 @@ public class Delete_Voice_Work_Tests(MariaDbContainerFixture container) : VoiceW DeleteVoiceWorkRequest request = new([voiceWorkId]); DeleteVoiceWorkResponse response = await writer.DeleteAsync(request, TestContext.Current.CancellationToken); - response.IsSuccess.Count.ShouldBe(1); - response.IsSuccess.ShouldContainKey(voiceWorkId); - response.IsSuccess[voiceWorkId].ShouldBeTrue(); + response.Results.Count.ShouldBe(1); + response.Results.ShouldContainKey(voiceWorkId); + response.Results[voiceWorkId].ShouldBe(DeleteVoiceWorkStatus.Deleted); } [Fact] @@ -33,8 +33,8 @@ public class Delete_Voice_Work_Tests(MariaDbContainerFixture container) : VoiceW DeleteVoiceWorkRequest request = new([voiceWorkId]); DeleteVoiceWorkResponse response = await writer.DeleteAsync(request, TestContext.Current.CancellationToken); - response.IsSuccess.Count.ShouldBe(1); - response.IsSuccess.ShouldContainKey(voiceWorkId); - response.IsSuccess[voiceWorkId].ShouldBeFalse(); + response.Results.Count.ShouldBe(1); + response.Results.ShouldContainKey(voiceWorkId); + response.Results[voiceWorkId].ShouldBe(DeleteVoiceWorkStatus.NotAllowed); } } \ No newline at end of file diff --git a/JSMR.UI.Blazor/Components/JProduct.razor b/JSMR.UI.Blazor/Components/JProduct.razor index 8e5b586..46c6fb9 100644 --- a/JSMR.UI.Blazor/Components/JProduct.razor +++ b/JSMR.UI.Blazor/Components/JProduct.razor @@ -1,4 +1,5 @@ @using AntDesign +@using JSMR.Application.VoiceWorks.Commands.Delete @using JSMR.Application.VoiceWorks.Commands.SetFavorite @using JSMR.Application.VoiceWorks.Queries.Search @using JSMR.Domain.Enums @@ -136,6 +137,9 @@ [Parameter] public required VoiceWorkSearchResult Product { get; set; } + [Parameter] + public EventCallback ProductDeleted { get; set; } + private string GetCardClasses(VoiceWorkSearchResult voiceWork) { List classNames = ["j-card", "j-voice-work-card"]; @@ -240,7 +244,38 @@ { Title = "Are you sure you want to delete the following product?", Icon = icon, - Content = Product.ProductName + Content = Product.ProductName, + Centered = true, + OnOk = async (e) => + { + DeleteVoiceWorkRequest request = new( + VoiceWorkIds: [Product.VoiceWorkId] + ); + + try + { + DeleteVoiceWorkResponse? response = await Client.DeleteVoiceWorkAsync(request); + + if (response is null || response.Results[Product.VoiceWorkId] != DeleteVoiceWorkStatus.Deleted) + return; + + await ProductDeleted.InvokeAsync(); + } + catch (Exception ex) + { + AntDesign.ConfirmOptions errorOptions = new() + { + Title = "Unable to delete product", + Content = "Something went wrong while deleting this product. The product was not deleted. Check the API logs for details.", + Centered = true, + //Width = "70vw", + }; + + await ModalService.ErrorAsync(errorOptions); + + e.Cancel = true; + } + } }; await ModalService.ConfirmAsync(options); diff --git a/JSMR.UI.Blazor/Components/JProductCollection.razor b/JSMR.UI.Blazor/Components/JProductCollection.razor index 4d1944b..b0c27f6 100644 --- a/JSMR.UI.Blazor/Components/JProductCollection.razor +++ b/JSMR.UI.Blazor/Components/JProductCollection.razor @@ -13,7 +13,7 @@ else
@foreach (var product in Products) { - + }
} @@ -21,4 +21,12 @@ else @code { [Parameter] public VoiceWorkSearchResult[]? Products { get; set; } + + [Parameter] + public EventCallback ProductDeleted { get; set; } + + private async Task OnProductDeleted() + { + await ProductDeleted.InvokeAsync(); + } } diff --git a/JSMR.UI.Blazor/Exceptions/ApiException.cs b/JSMR.UI.Blazor/Exceptions/ApiException.cs new file mode 100644 index 0000000..f5f1105 --- /dev/null +++ b/JSMR.UI.Blazor/Exceptions/ApiException.cs @@ -0,0 +1,9 @@ +using System.Net; + +namespace JSMR.UI.Blazor.Exceptions; + +public sealed class ApiException(HttpStatusCode statusCode, string message, string? responseBody = null) : Exception(message) +{ + public HttpStatusCode StatusCode { get; } = statusCode; + public string? ResponseBody { get; } = responseBody; +} \ No newline at end of file diff --git a/JSMR.UI.Blazor/Pages/Home.razor b/JSMR.UI.Blazor/Pages/Home.razor index 8b21efe..e070e0e 100644 --- a/JSMR.UI.Blazor/Pages/Home.razor +++ b/JSMR.UI.Blazor/Pages/Home.razor @@ -14,7 +14,7 @@ - + @@ -105,4 +105,9 @@ await InvokeAsync(StateHasChanged); } + + private async Task OnAnnouncedProductDeleted() + { + _ = LoadAnnouncedVoiceWorksAsync(); + } } \ No newline at end of file diff --git a/JSMR.UI.Blazor/Services/VoiceWorksClient.cs b/JSMR.UI.Blazor/Services/VoiceWorksClient.cs index ca01d67..35425c7 100644 --- a/JSMR.UI.Blazor/Services/VoiceWorksClient.cs +++ b/JSMR.UI.Blazor/Services/VoiceWorksClient.cs @@ -7,6 +7,7 @@ using JSMR.Application.Tags.Queries.Search; using JSMR.Application.VoiceWorks.Commands.Delete; using JSMR.Application.VoiceWorks.Commands.SetFavorite; using JSMR.Application.VoiceWorks.Queries.Search; +using JSMR.UI.Blazor.Exceptions; using System.Net.Http.Json; using System.Text.Json; using System.Text.Json.Serialization; @@ -23,6 +24,18 @@ public class VoiceWorksClient(HttpClient http) } }; + private static async Task ReadJsonOrThrowAsync(HttpResponseMessage response, CancellationToken cancellationToken) + { + if (!response.IsSuccessStatusCode) + { + string body = await response.Content.ReadAsStringAsync(cancellationToken); + + throw new ApiException(response.StatusCode, $"Request failed: {(int)response.StatusCode}", body); + } + + return await response.Content.ReadFromJsonAsync(JsonOptions, cancellationToken: cancellationToken); + } + public async Task SearchAsync(SearchVoiceWorksRequest request, CancellationToken ct = default) { using var resp = await http.PostAsJsonAsync("/api/voiceworks/search", request, ct); @@ -56,7 +69,8 @@ public class VoiceWorksClient(HttpClient http) public async Task DeleteVoiceWorkAsync(DeleteVoiceWorkRequest request, CancellationToken ct = default) { using var resp = await http.PostAsJsonAsync("/api/voicework/delete", request, ct); - return await resp.Content.ReadFromJsonAsync(JsonOptions, cancellationToken: ct); + //return await resp.Content.ReadFromJsonAsync(JsonOptions, cancellationToken: ct); + return await ReadJsonOrThrowAsync(resp, ct); } public async Task UpdateTagStatusAsync(UpdateTagStatusRequest request, CancellationToken ct = default)