From 92de5421a1f2da1b94e2462534611b71f3ef5344 Mon Sep 17 00:00:00 2001 From: Scribe Date: Tue, 14 Apr 2026 20:11:27 -0700 Subject: [PATCH 1/3] feat: Add archive action to CategoriesPage (admin-only) - Add ArchiveAsync method to ICategoryApiClient interface - Implement ArchiveAsync in CategoryApiClient (DELETE /api/v1/categories/{id}) - Add Archive button to CategoriesPage visible only to admins - Add ConfirmDialog for archive confirmation - Implement optimistic UI update after successful archive - Use confirmation message: 'Archive '{CategoryName}'? It will no longer appear in issue forms.' Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Features/Categories/CategoriesPage.razor | 14 ++++++- .../Categories/CategoriesPage.razor.cs | 41 +++++++++++++++++++ .../Features/Categories/CategoryApiClient.cs | 17 ++++++++ 3 files changed, 71 insertions(+), 1 deletion(-) diff --git a/src/Web/Components/Features/Categories/CategoriesPage.razor b/src/Web/Components/Features/Categories/CategoriesPage.razor index 9f87d6a..1e4aabe 100644 --- a/src/Web/Components/Features/Categories/CategoriesPage.razor +++ b/src/Web/Components/Features/Categories/CategoriesPage.razor @@ -33,10 +33,17 @@ + TextAlign="TextAlign.Right" Width="180px" Title="Actions"> } + + + diff --git a/src/Web/Components/Features/Categories/CategoriesPage.razor.cs b/src/Web/Components/Features/Categories/CategoriesPage.razor.cs index f7383a1..9b4cb23 100644 --- a/src/Web/Components/Features/Categories/CategoriesPage.razor.cs +++ b/src/Web/Components/Features/Categories/CategoriesPage.razor.cs @@ -36,6 +36,11 @@ public partial class CategoriesPage : ComponentBase private bool _isLoading = true; + // Archive state + private bool _showArchiveDialog = false; + private string? _categoryToArchiveId = null; + private string _archiveConfirmMessage = ""; + protected override async Task OnInitializedAsync() { await LoadCategories(); @@ -121,4 +126,40 @@ private async Task OnUpdateRow(CategoryEditModel cat) await CategoryClient.UpdateAsync(cat.Id, command); } + /// + /// Shows the archive confirmation dialog. + /// + private void HandleArchive(string categoryId, string categoryName) + { + _categoryToArchiveId = categoryId; + _archiveConfirmMessage = $"Archive '{categoryName}'? It will no longer appear in issue forms."; + _showArchiveDialog = true; + } + + /// + /// Handles the archive confirmation. + /// + private async Task HandleArchiveConfirm() + { + _showArchiveDialog = false; + if (string.IsNullOrEmpty(_categoryToArchiveId)) return; + + var success = await CategoryClient.ArchiveAsync(_categoryToArchiveId); + if (success) + { + _categories = _categories.Where(c => c.Id != _categoryToArchiveId).ToList(); + await InvokeAsync(StateHasChanged); + } + _categoryToArchiveId = null; + } + + /// + /// Handles the archive cancellation. + /// + private void HandleArchiveCancel() + { + _showArchiveDialog = false; + _categoryToArchiveId = null; + } + } diff --git a/src/Web/Components/Features/Categories/CategoryApiClient.cs b/src/Web/Components/Features/Categories/CategoryApiClient.cs index b0f6d87..de688da 100644 --- a/src/Web/Components/Features/Categories/CategoryApiClient.cs +++ b/src/Web/Components/Features/Categories/CategoryApiClient.cs @@ -23,6 +23,9 @@ public interface ICategoryApiClient /// Updates an existing category. Task UpdateAsync(string id, UpdateCategoryCommand command, CancellationToken cancellationToken = default); + + /// Archives a category. + Task ArchiveAsync(string id, CancellationToken cancellationToken = default); } /// Typed HTTP client for the Categories API. @@ -70,4 +73,18 @@ public async Task> GetAllAsync(CancellationToken cancel : null; } + /// + public async Task ArchiveAsync(string id, CancellationToken cancellationToken = default) + { + try + { + var response = await _httpClient.DeleteAsync($"/api/v1/categories/{id}", cancellationToken).ConfigureAwait(false); + return response.IsSuccessStatusCode; + } + catch (HttpRequestException) + { + return false; + } + } + } From 609d47df0fdef8df9b1944d2b26583daf1379adf Mon Sep 17 00:00:00 2001 From: Scribe Date: Tue, 14 Apr 2026 20:12:37 -0700 Subject: [PATCH 2/3] feat: Add admin-only Archive action to StatusesPage - Add ArchiveAsync method to IStatusApiClient interface and implementation - Add archive button (admin-only) to StatusesPage with confirmation dialog - Display confirmation message warning about status reassignment - Optimistically remove archived status from local list on success - Follow existing UI patterns from IssueDetailPage and CategoriesPage Working as Legolas (Frontend Developer) Closes #123 Depends on #121 Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Features/Statuses/StatusApiClient.cs | 17 ++++ .../Features/Statuses/StatusesPage.razor | 98 +++++++++++-------- .../Features/Statuses/StatusesPage.razor.cs | 34 +++++++ 3 files changed, 106 insertions(+), 43 deletions(-) diff --git a/src/Web/Components/Features/Statuses/StatusApiClient.cs b/src/Web/Components/Features/Statuses/StatusApiClient.cs index 03409a6..dd5cc41 100644 --- a/src/Web/Components/Features/Statuses/StatusApiClient.cs +++ b/src/Web/Components/Features/Statuses/StatusApiClient.cs @@ -23,6 +23,9 @@ public interface IStatusApiClient /// Updates an existing status. Task UpdateAsync(string id, UpdateStatusCommand command, CancellationToken cancellationToken = default); + + /// Archives a status by its identifier. + Task ArchiveAsync(string id, CancellationToken cancellationToken = default); } /// Typed HTTP client for the Statuses API. @@ -70,4 +73,18 @@ public async Task> GetAllAsync(CancellationToken cancella : null; } + /// + public async Task ArchiveAsync(string id, CancellationToken cancellationToken = default) + { + try + { + var response = await _httpClient.DeleteAsync($"/api/v1/statuses/{id}", cancellationToken).ConfigureAwait(false); + return response.IsSuccessStatusCode; + } + catch (HttpRequestException) + { + return false; + } + } + } diff --git a/src/Web/Components/Features/Statuses/StatusesPage.razor b/src/Web/Components/Features/Statuses/StatusesPage.razor index 881aeea..0fb5683 100644 --- a/src/Web/Components/Features/Statuses/StatusesPage.razor +++ b/src/Web/Components/Features/Statuses/StatusesPage.razor @@ -5,48 +5,60 @@ Statuses — IssueManager
-
-

Statuses

- -
+
+

Statuses

+ +
- @if (_isLoading) - { -
Loading...
- } - else - { -
- - - - - - - - - - - - - - - - - - - - - - -
- } +@if (_isLoading) +{ +
Loading...
+} +else +{ +
+ + + + + + + + + + + + + + + + + + + + + + +
+}
+ + + diff --git a/src/Web/Components/Features/Statuses/StatusesPage.razor.cs b/src/Web/Components/Features/Statuses/StatusesPage.razor.cs index 50806ce..363d0e3 100644 --- a/src/Web/Components/Features/Statuses/StatusesPage.razor.cs +++ b/src/Web/Components/Features/Statuses/StatusesPage.razor.cs @@ -27,6 +27,11 @@ public partial class StatusesPage : ComponentBase private StatusEditModel? _editingStatus; private bool _isLoading = true; + // Archive dialog state + private bool _showArchiveDialog = false; + private string? _statusToArchiveId = null; + private string? _statusToArchiveName = null; + protected override async Task OnInitializedAsync() { await LoadStatuses(); @@ -102,4 +107,33 @@ private async Task OnUpdateRow(StatusEditModel status) }; await StatusClient.UpdateAsync(status.Id, command); } + + private void ShowArchiveDialog(string id, string name) + { + _statusToArchiveId = id; + _statusToArchiveName = name; + _showArchiveDialog = true; + } + + private async Task HandleArchiveConfirm() + { + _showArchiveDialog = false; + if (string.IsNullOrEmpty(_statusToArchiveId)) return; + + var success = await StatusClient.ArchiveAsync(_statusToArchiveId); + if (success) + { + _statuses = _statuses.Where(s => s.Id != _statusToArchiveId).ToList(); + await InvokeAsync(StateHasChanged); + } + _statusToArchiveId = null; + _statusToArchiveName = null; + } + + private void HandleArchiveCancel() + { + _showArchiveDialog = false; + _statusToArchiveId = null; + _statusToArchiveName = null; + } } From d69bd9514a370e5444619314632bbc236fb479ce Mon Sep 17 00:00:00 2001 From: Scribe Date: Wed, 15 Apr 2026 08:27:14 -0700 Subject: [PATCH 3/3] fix: register authorization services in StatusesPage and CategoriesPage bunit tests AuthorizeView in CategoriesPage and StatusesPage requires IAuthorizationPolicyProvider. Add _ctx.AddAuthorization() to test constructors so row templates render correctly when the grid has data. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../Components/Features/Categories/CategoriesPageTests.cs | 1 + .../Components/Features/Statuses/StatusesPageTests.cs | 1 + 2 files changed, 2 insertions(+) diff --git a/tests/Web.Tests.Bunit/Components/Features/Categories/CategoriesPageTests.cs b/tests/Web.Tests.Bunit/Components/Features/Categories/CategoriesPageTests.cs index e156cfb..4d723f9 100644 --- a/tests/Web.Tests.Bunit/Components/Features/Categories/CategoriesPageTests.cs +++ b/tests/Web.Tests.Bunit/Components/Features/Categories/CategoriesPageTests.cs @@ -27,6 +27,7 @@ public CategoriesPageTests() { _ctx = new BunitContext(); _ctx.JSInterop.Mode = JSRuntimeMode.Loose; + _ctx.AddAuthorization(); _mockCategoryClient = Substitute.For(); _mockCategoryClient.GetAllAsync(Arg.Any()) .Returns(Task.FromResult>([])); diff --git a/tests/Web.Tests.Bunit/Components/Features/Statuses/StatusesPageTests.cs b/tests/Web.Tests.Bunit/Components/Features/Statuses/StatusesPageTests.cs index 17f6a21..8841e9d 100644 --- a/tests/Web.Tests.Bunit/Components/Features/Statuses/StatusesPageTests.cs +++ b/tests/Web.Tests.Bunit/Components/Features/Statuses/StatusesPageTests.cs @@ -27,6 +27,7 @@ public StatusesPageTests() { _ctx = new BunitContext(); _ctx.JSInterop.Mode = JSRuntimeMode.Loose; + _ctx.AddAuthorization(); _mockStatusClient = Substitute.For(); _mockStatusClient.GetAllAsync(Arg.Any()) .Returns(Task.FromResult>([]));