Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion src/Web/Components/Features/Categories/CategoriesPage.razor
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,17 @@
</EditTemplate>
</RadzenDataGridColumn>
<RadzenDataGridColumn TItem="CategoryEditModel" Context="cat" Filterable="false" Sortable="false"
TextAlign="TextAlign.Right" Width="140px" Title="Actions">
TextAlign="TextAlign.Right" Width="180px" Title="Actions">
<Template Context="cat">
<RadzenButton Icon="edit" ButtonStyle="ButtonStyle.Light" Size="ButtonSize.Small"
Click="@(() => EditRow(cat))" @onclick:stopPropagation="true" />
<AuthorizeView Policy="Admin">
<Authorized>
<RadzenButton Icon="archive" ButtonStyle="ButtonStyle.Danger" Size="ButtonSize.Small"
Click="@(() => HandleArchive(cat.Id, cat.CategoryName))" @onclick:stopPropagation="true"
Class="ms-1" />
</Authorized>
</AuthorizeView>
Comment on lines +40 to +46
Copy link

Copilot AI Apr 15, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AuthorizeView is using Policy="Admin", but the Web app appears to use role-based checks ([Authorize(Roles = "Admin")], <AuthorizeView Roles="Admin">) and no "Admin" policy is configured. This can hide the Archive button for admins or throw at runtime when the policy can't be found. Use Roles="Admin" here (or define an Admin authorization policy in AuthExtensions).

Copilot uses AI. Check for mistakes.
</Template>
<EditTemplate Context="cat">
<RadzenButton Icon="check" ButtonStyle="ButtonStyle.Success" Size="ButtonSize.Small"
Expand All @@ -50,3 +57,8 @@
</div>
}
</div>

<!-- Archive Confirmation Dialog -->
<ConfirmDialog IsVisible="@_showArchiveDialog" Title="Archive Category"
Message="@_archiveConfirmMessage" ConfirmText="Archive"
CancelText="Cancel" OnConfirm="HandleArchiveConfirm" OnCancel="HandleArchiveCancel" />
41 changes: 41 additions & 0 deletions src/Web/Components/Features/Categories/CategoriesPage.razor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -121,4 +126,40 @@ private async Task OnUpdateRow(CategoryEditModel cat)
await CategoryClient.UpdateAsync(cat.Id, command);
}

/// <summary>
/// Shows the archive confirmation dialog.
/// </summary>
private void HandleArchive(string categoryId, string categoryName)
{
_categoryToArchiveId = categoryId;
_archiveConfirmMessage = $"Archive '{categoryName}'? It will no longer appear in issue forms.";
_showArchiveDialog = true;
}

/// <summary>
/// Handles the archive confirmation.
/// </summary>
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;
}

/// <summary>
/// Handles the archive cancellation.
/// </summary>
private void HandleArchiveCancel()
{
_showArchiveDialog = false;
_categoryToArchiveId = null;
}

}
17 changes: 17 additions & 0 deletions src/Web/Components/Features/Categories/CategoryApiClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ public interface ICategoryApiClient

/// <summary>Updates an existing category.</summary>
Task<CategoryDto?> UpdateAsync(string id, UpdateCategoryCommand command, CancellationToken cancellationToken = default);

/// <summary>Archives a category.</summary>
Task<bool> ArchiveAsync(string id, CancellationToken cancellationToken = default);
}

/// <summary>Typed HTTP client for the Categories API.</summary>
Expand Down Expand Up @@ -70,4 +73,18 @@ public async Task<IEnumerable<CategoryDto>> GetAllAsync(CancellationToken cancel
: null;
}

/// <inheritdoc/>
public async Task<bool> 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;
}
}

}
17 changes: 17 additions & 0 deletions src/Web/Components/Features/Statuses/StatusApiClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ public interface IStatusApiClient

/// <summary>Updates an existing status.</summary>
Task<StatusDto?> UpdateAsync(string id, UpdateStatusCommand command, CancellationToken cancellationToken = default);

/// <summary>Archives a status by its identifier.</summary>
Task<bool> ArchiveAsync(string id, CancellationToken cancellationToken = default);
}

/// <summary>Typed HTTP client for the Statuses API.</summary>
Expand Down Expand Up @@ -70,4 +73,18 @@ public async Task<IEnumerable<StatusDto>> GetAllAsync(CancellationToken cancella
: null;
}

/// <inheritdoc/>
public async Task<bool> 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;
}
}

}
98 changes: 55 additions & 43 deletions src/Web/Components/Features/Statuses/StatusesPage.razor
Original file line number Diff line number Diff line change
Expand Up @@ -5,48 +5,60 @@
<PageTitle>Statuses — IssueManager</PageTitle>

<div class="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<div class="flex items-center justify-between mb-6">
<h1 class="page-title">Statuses</h1>
<RadzenButton ButtonStyle="ButtonStyle.Success" Icon="add_circle_outline" Text="Add Status" Click="@InsertRow"
Disabled="@(_insertingStatus != null || _editingStatus != null)" class="px-4 py-2 text-sm" />
</div>
<div class="flex items-center justify-between mb-6">
<h1 class="page-title">Statuses</h1>
<RadzenButton ButtonStyle="ButtonStyle.Success" Icon="add_circle_outline" Text="Add Status" Click="@InsertRow"
Disabled="@(_insertingStatus != null || _editingStatus != null)" class="px-4 py-2 text-sm" />
</div>

@if (_isLoading)
{
<div class="loading-state">Loading...</div>
}
else
{
<div class="card-flush">
<RadzenDataGrid @ref="_grid" Data="@_statuses" TItem="StatusEditModel" EditMode="DataGridEditMode.Single"
RowUpdate="@OnUpdateRow" RowCreate="@OnCreateRow" AllowSorting="true" AllowPaging="true" PageSize="10">
<Columns>
<RadzenDataGridColumn TItem="StatusEditModel" Property="StatusName" Title="Status Name" Width="200px">
<EditTemplate Context="status">
<RadzenTextBox @bind-Value="status.StatusName" Style="width:100%" Name="StatusName" />
<RadzenRequiredValidator Component="StatusName" Text="Name is required" Popup="true" />
</EditTemplate>
</RadzenDataGridColumn>
<RadzenDataGridColumn TItem="StatusEditModel" Property="StatusDescription" Title="Description">
<EditTemplate Context="status">
<RadzenTextBox @bind-Value="status.StatusDescription" Style="width:100%" Name="StatusDesc" />
</EditTemplate>
</RadzenDataGridColumn>
<RadzenDataGridColumn TItem="StatusEditModel" Context="status" Filterable="false" Sortable="false"
TextAlign="TextAlign.Right" Width="140px" Title="Actions">
<Template Context="status">
<RadzenButton Icon="edit" ButtonStyle="ButtonStyle.Light" Size="ButtonSize.Small"
Click="@(() => EditRow(status))" @onclick:stopPropagation="true" />
</Template>
<EditTemplate Context="status">
<RadzenButton Icon="check" ButtonStyle="ButtonStyle.Success" Size="ButtonSize.Small"
Click="@(() => SaveRow(status))" />
<RadzenButton Icon="close" ButtonStyle="ButtonStyle.Light" Size="ButtonSize.Small"
Click="@(() => CancelEdit(status))" Class="ms-1" />
</EditTemplate>
</RadzenDataGridColumn>
</Columns>
</RadzenDataGrid>
</div>
}
@if (_isLoading)
{
<div class="loading-state">Loading...</div>
}
else
{
<div class="card-flush">
<RadzenDataGrid @ref="_grid" Data="@_statuses" TItem="StatusEditModel" EditMode="DataGridEditMode.Single"
RowUpdate="@OnUpdateRow" RowCreate="@OnCreateRow" AllowSorting="true" AllowPaging="true" PageSize="10">
<Columns>
<RadzenDataGridColumn TItem="StatusEditModel" Property="StatusName" Title="Status Name" Width="200px">
<EditTemplate Context="status">
<RadzenTextBox @bind-Value="status.StatusName" Style="width:100%" Name="StatusName" />
<RadzenRequiredValidator Component="StatusName" Text="Name is required" Popup="true" />
</EditTemplate>
</RadzenDataGridColumn>
<RadzenDataGridColumn TItem="StatusEditModel" Property="StatusDescription" Title="Description">
<EditTemplate Context="status">
<RadzenTextBox @bind-Value="status.StatusDescription" Style="width:100%" Name="StatusDesc" />
</EditTemplate>
</RadzenDataGridColumn>
<RadzenDataGridColumn TItem="StatusEditModel" Context="status" Filterable="false" Sortable="false"
TextAlign="TextAlign.Right" Width="180px" Title="Actions">
<Template Context="status">
<RadzenButton Icon="edit" ButtonStyle="ButtonStyle.Light" Size="ButtonSize.Small"
Click="@(() => EditRow(status))" @onclick:stopPropagation="true" />
<AuthorizeView Policy="Admin">
<Authorized>
<RadzenButton Icon="archive" ButtonStyle="ButtonStyle.Danger" Size="ButtonSize.Small"
Click="@(() => ShowArchiveDialog(status.Id, status.StatusName))" @onclick:stopPropagation="true"
Class="ms-1" />
</Authorized>
</AuthorizeView>
</Template>
<EditTemplate Context="status">
<RadzenButton Icon="check" ButtonStyle="ButtonStyle.Success" Size="ButtonSize.Small"
Click="@(() => SaveRow(status))" />
<RadzenButton Icon="close" ButtonStyle="ButtonStyle.Light" Size="ButtonSize.Small"
Click="@(() => CancelEdit(status))" Class="ms-1" />
</EditTemplate>
</RadzenDataGridColumn>
</Columns>
</RadzenDataGrid>
</div>
}
</div>

<!-- Archive Confirmation Dialog -->
<ConfirmDialog IsVisible="@_showArchiveDialog" Title="Archive Status"
Message="@($"Archive '{_statusToArchiveName}'? Issues with this status will need to be reassigned.")"
ConfirmText="Archive" CancelText="Cancel" OnConfirm="HandleArchiveConfirm" OnCancel="HandleArchiveCancel" />
34 changes: 34 additions & 0 deletions src/Web/Components/Features/Statuses/StatusesPage.razor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ public CategoriesPageTests()
{
_ctx = new BunitContext();
_ctx.JSInterop.Mode = JSRuntimeMode.Loose;
_ctx.AddAuthorization();
_mockCategoryClient = Substitute.For<ICategoryApiClient>();
_mockCategoryClient.GetAllAsync(Arg.Any<CancellationToken>())
.Returns(Task.FromResult<IEnumerable<CategoryDto>>([]));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ public StatusesPageTests()
{
_ctx = new BunitContext();
_ctx.JSInterop.Mode = JSRuntimeMode.Loose;
_ctx.AddAuthorization();
_mockStatusClient = Substitute.For<IStatusApiClient>();
_mockStatusClient.GetAllAsync(Arg.Any<CancellationToken>())
.Returns(Task.FromResult<IEnumerable<StatusDto>>([]));
Expand Down
Loading