Extract workspace admin coordinator
This commit is contained in:
@@ -35,7 +35,7 @@ Frontend:
|
|||||||
- `RpgRoller/Components/Pages/Home.razor`: minimal gateway page (loading/auth/workspace switch)
|
- `RpgRoller/Components/Pages/Home.razor`: minimal gateway page (loading/auth/workspace switch)
|
||||||
- `RpgRoller/Components/Pages/Home.razor.cs`: single `Home` code-behind with only gateway/session-view orchestration
|
- `RpgRoller/Components/Pages/Home.razor.cs`: single `Home` code-behind with only gateway/session-view orchestration
|
||||||
- `RpgRoller/Components/Pages/Workspace.razor`: authenticated workspace UI and workspace-specific state/logic
|
- `RpgRoller/Components/Pages/Workspace.razor`: authenticated workspace UI and workspace-specific state/logic
|
||||||
- `RpgRoller/Components/Pages/WorkspaceState.cs`, `WorkspaceToast.cs`, `WorkspaceFeedbackService.cs`, and `WorkspaceSessionCoordinator.cs`: extracted workspace UI state, toast records, feedback/status handling, and session/bootstrap orchestration while `Workspace` remains the behavior owner
|
- `RpgRoller/Components/Pages/WorkspaceState.cs`, `WorkspaceToast.cs`, `WorkspaceFeedbackService.cs`, `WorkspaceSessionCoordinator.cs`, and `WorkspaceAdminCoordinator.cs`: extracted workspace UI state, toast records, feedback/status handling, session/bootstrap orchestration, and admin user actions while `Workspace` remains the behavior owner
|
||||||
- `RpgRoller/Components/**/*.razor.cs`: component code-behind classes (state, handlers, parameters, injected dependencies); `.razor` files remain markup-focused
|
- `RpgRoller/Components/**/*.razor.cs`: component code-behind classes (state, handlers, parameters, injected dependencies); `.razor` files remain markup-focused
|
||||||
- `RpgRoller/Components/Pages/Home.Models.cs`: reusable `FormState<TModel>` + page form models
|
- `RpgRoller/Components/Pages/Home.Models.cs`: reusable `FormState<TModel>` + page form models
|
||||||
- `RpgRoller/Components/Pages/HomeControls/`: auth, campaign management, play-screen, and modal controls extracted from `Home.razor`
|
- `RpgRoller/Components/Pages/HomeControls/`: auth, campaign management, play-screen, and modal controls extracted from `Home.razor`
|
||||||
|
|||||||
@@ -186,93 +186,11 @@ public partial class Workspace : IAsyncDisposable
|
|||||||
return SwitchScreenAsync(ScreenAdmin);
|
return SwitchScreenAsync(ScreenAdmin);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task EnsureAdminUsersLoadedAsync()
|
private Task EnsureAdminUsersLoadedAsync() => Admin.EnsureAdminUsersLoadedAsync();
|
||||||
{
|
|
||||||
if (!IsCurrentUserAdmin || HasLoadedAdminUsers || IsAdminDataLoading)
|
|
||||||
return;
|
|
||||||
|
|
||||||
IsAdminDataLoading = true;
|
private Task ToggleAdminRoleAsync(AdminUserSummary user) => Admin.ToggleAdminRoleAsync(user);
|
||||||
try
|
|
||||||
{
|
|
||||||
await ReloadAdminUsersAsync();
|
|
||||||
}
|
|
||||||
catch (ApiRequestException ex) when (ex.StatusCode == 401)
|
|
||||||
{
|
|
||||||
ClearAuthenticatedState();
|
|
||||||
await StopStateEventsAsync();
|
|
||||||
await LoggedOut.InvokeAsync("Session expired. Please log in again.");
|
|
||||||
}
|
|
||||||
catch (ApiRequestException ex)
|
|
||||||
{
|
|
||||||
SetStatus(ex.Message, true);
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
IsAdminDataLoading = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task ReloadAdminUsersAsync()
|
private Task DeleteUserAsync(AdminUserSummary user) => Admin.DeleteUserAsync(user);
|
||||||
{
|
|
||||||
AdminUsers = (await WorkspaceQuery.GetAdminUsersAsync())
|
|
||||||
.OrderBy(user => user.Username, StringComparer.OrdinalIgnoreCase)
|
|
||||||
.ToList();
|
|
||||||
|
|
||||||
HasLoadedAdminUsers = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task ToggleAdminRoleAsync(AdminUserSummary user)
|
|
||||||
{
|
|
||||||
if (IsMutating || User is null || !IsCurrentUserAdmin || user.Id == User.Id)
|
|
||||||
return;
|
|
||||||
|
|
||||||
IsMutating = true;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
IReadOnlyList<string> roles = HasAdminRole(user) ? Array.Empty<string>() : [UserRoles.Admin];
|
|
||||||
_ = await ApiClient.RequestAsync<AdminUserSummary>(
|
|
||||||
"PUT",
|
|
||||||
$"/api/admin/users/{user.Id}/roles",
|
|
||||||
new UpdateUserRolesRequest(roles));
|
|
||||||
|
|
||||||
await ReloadAdminUsersAsync();
|
|
||||||
SetStatus("User roles updated.", false);
|
|
||||||
}
|
|
||||||
catch (ApiRequestException ex)
|
|
||||||
{
|
|
||||||
SetStatus(ex.Message, true);
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
IsMutating = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task DeleteUserAsync(AdminUserSummary user)
|
|
||||||
{
|
|
||||||
if (IsMutating || User is null || !IsCurrentUserAdmin || user.Id == User.Id)
|
|
||||||
return;
|
|
||||||
|
|
||||||
var confirmed = await JS.InvokeAsync<bool>("confirm", $"Delete user '{user.Username}'?");
|
|
||||||
if (!confirmed)
|
|
||||||
return;
|
|
||||||
|
|
||||||
IsMutating = true;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
_ = await ApiClient.RequestAsync<bool>("DELETE", $"/api/admin/users/{user.Id}");
|
|
||||||
await ReloadAdminUsersAsync();
|
|
||||||
SetStatus("User deleted.", false);
|
|
||||||
}
|
|
||||||
catch (ApiRequestException ex)
|
|
||||||
{
|
|
||||||
SetStatus(ex.Message, true);
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
IsMutating = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task SetMobilePanelAsync(string panel)
|
private async Task SetMobilePanelAsync(string panel)
|
||||||
{
|
{
|
||||||
@@ -970,14 +888,18 @@ public partial class Workspace : IAsyncDisposable
|
|||||||
private bool CanDeleteSelectedCampaign => State.CanDeleteSelectedCampaign;
|
private bool CanDeleteSelectedCampaign => State.CanDeleteSelectedCampaign;
|
||||||
private bool IsSelectedCampaignD6 => State.IsSelectedCampaignD6;
|
private bool IsSelectedCampaignD6 => State.IsSelectedCampaignD6;
|
||||||
|
|
||||||
private static bool HasAdminRole(AdminUserSummary user)
|
|
||||||
{
|
|
||||||
return user.Roles.Contains(UserRoles.Admin, StringComparer.OrdinalIgnoreCase);
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool IsPlayScreen => State.IsPlayScreen;
|
private bool IsPlayScreen => State.IsPlayScreen;
|
||||||
private bool IsManagementScreen => State.IsManagementScreen;
|
private bool IsManagementScreen => State.IsManagementScreen;
|
||||||
private bool IsAdminScreen => State.IsAdminScreen;
|
private bool IsAdminScreen => State.IsAdminScreen;
|
||||||
|
private WorkspaceAdminCoordinator Admin => m_Admin ??= new(
|
||||||
|
State,
|
||||||
|
Feedback,
|
||||||
|
JS,
|
||||||
|
ApiClient,
|
||||||
|
WorkspaceQuery,
|
||||||
|
ClearAuthenticatedState,
|
||||||
|
StopStateEventsAsync,
|
||||||
|
message => LoggedOut.InvokeAsync(message));
|
||||||
private WorkspaceFeedbackService Feedback => m_Feedback ??= new(State, () => InvokeAsync(StateHasChanged));
|
private WorkspaceFeedbackService Feedback => m_Feedback ??= new(State, () => InvokeAsync(StateHasChanged));
|
||||||
private WorkspaceSessionCoordinator Session => m_Session ??= new(
|
private WorkspaceSessionCoordinator Session => m_Session ??= new(
|
||||||
State,
|
State,
|
||||||
@@ -1021,6 +943,7 @@ public partial class Workspace : IAsyncDisposable
|
|||||||
private const string MobilePanelSessionKey = "play-panel";
|
private const string MobilePanelSessionKey = "play-panel";
|
||||||
private const int CampaignLogWindowSize = 25;
|
private const int CampaignLogWindowSize = 25;
|
||||||
|
|
||||||
|
private WorkspaceAdminCoordinator? m_Admin;
|
||||||
private WorkspaceFeedbackService? m_Feedback;
|
private WorkspaceFeedbackService? m_Feedback;
|
||||||
private WorkspaceSessionCoordinator? m_Session;
|
private WorkspaceSessionCoordinator? m_Session;
|
||||||
}
|
}
|
||||||
|
|||||||
132
RpgRoller/Components/Pages/WorkspaceAdminCoordinator.cs
Normal file
132
RpgRoller/Components/Pages/WorkspaceAdminCoordinator.cs
Normal file
@@ -0,0 +1,132 @@
|
|||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
using Microsoft.JSInterop;
|
||||||
|
using RpgRoller.Contracts;
|
||||||
|
using RpgRoller.Domain;
|
||||||
|
|
||||||
|
namespace RpgRoller.Components.Pages;
|
||||||
|
|
||||||
|
[ExcludeFromCodeCoverage]
|
||||||
|
public sealed class WorkspaceAdminCoordinator
|
||||||
|
{
|
||||||
|
public WorkspaceAdminCoordinator(
|
||||||
|
WorkspaceState state,
|
||||||
|
WorkspaceFeedbackService feedback,
|
||||||
|
IJSRuntime js,
|
||||||
|
RpgRollerApiClient apiClient,
|
||||||
|
WorkspaceQueryService workspaceQuery,
|
||||||
|
Action clearAuthenticatedState,
|
||||||
|
Func<Task> stopStateEventsAsync,
|
||||||
|
Func<string?, Task> onLoggedOutAsync)
|
||||||
|
{
|
||||||
|
m_State = state;
|
||||||
|
m_Feedback = feedback;
|
||||||
|
m_JS = js;
|
||||||
|
m_ApiClient = apiClient;
|
||||||
|
m_WorkspaceQuery = workspaceQuery;
|
||||||
|
m_ClearAuthenticatedState = clearAuthenticatedState;
|
||||||
|
m_StopStateEventsAsync = stopStateEventsAsync;
|
||||||
|
m_OnLoggedOutAsync = onLoggedOutAsync;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task EnsureAdminUsersLoadedAsync()
|
||||||
|
{
|
||||||
|
if (!m_State.IsCurrentUserAdmin || m_State.HasLoadedAdminUsers || m_State.IsAdminDataLoading)
|
||||||
|
return;
|
||||||
|
|
||||||
|
m_State.IsAdminDataLoading = true;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await ReloadAdminUsersAsync();
|
||||||
|
}
|
||||||
|
catch (ApiRequestException ex) when (ex.StatusCode == 401)
|
||||||
|
{
|
||||||
|
m_ClearAuthenticatedState();
|
||||||
|
await m_StopStateEventsAsync();
|
||||||
|
await m_OnLoggedOutAsync("Session expired. Please log in again.");
|
||||||
|
}
|
||||||
|
catch (ApiRequestException ex)
|
||||||
|
{
|
||||||
|
m_Feedback.SetStatus(ex.Message, true);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
m_State.IsAdminDataLoading = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task ToggleAdminRoleAsync(AdminUserSummary user)
|
||||||
|
{
|
||||||
|
if (m_State.IsMutating || m_State.User is null || !m_State.IsCurrentUserAdmin || user.Id == m_State.User.Id)
|
||||||
|
return;
|
||||||
|
|
||||||
|
m_State.IsMutating = true;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
IReadOnlyList<string> roles = HasAdminRole(user) ? Array.Empty<string>() : [UserRoles.Admin];
|
||||||
|
_ = await m_ApiClient.RequestAsync<AdminUserSummary>(
|
||||||
|
"PUT",
|
||||||
|
$"/api/admin/users/{user.Id}/roles",
|
||||||
|
new UpdateUserRolesRequest(roles));
|
||||||
|
|
||||||
|
await ReloadAdminUsersAsync();
|
||||||
|
m_Feedback.SetStatus("User roles updated.", false);
|
||||||
|
}
|
||||||
|
catch (ApiRequestException ex)
|
||||||
|
{
|
||||||
|
m_Feedback.SetStatus(ex.Message, true);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
m_State.IsMutating = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task DeleteUserAsync(AdminUserSummary user)
|
||||||
|
{
|
||||||
|
if (m_State.IsMutating || m_State.User is null || !m_State.IsCurrentUserAdmin || user.Id == m_State.User.Id)
|
||||||
|
return;
|
||||||
|
|
||||||
|
var confirmed = await m_JS.InvokeAsync<bool>("confirm", $"Delete user '{user.Username}'?");
|
||||||
|
if (!confirmed)
|
||||||
|
return;
|
||||||
|
|
||||||
|
m_State.IsMutating = true;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_ = await m_ApiClient.RequestAsync<bool>("DELETE", $"/api/admin/users/{user.Id}");
|
||||||
|
await ReloadAdminUsersAsync();
|
||||||
|
m_Feedback.SetStatus("User deleted.", false);
|
||||||
|
}
|
||||||
|
catch (ApiRequestException ex)
|
||||||
|
{
|
||||||
|
m_Feedback.SetStatus(ex.Message, true);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
m_State.IsMutating = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task ReloadAdminUsersAsync()
|
||||||
|
{
|
||||||
|
m_State.AdminUsers = (await m_WorkspaceQuery.GetAdminUsersAsync())
|
||||||
|
.OrderBy(user => user.Username, StringComparer.OrdinalIgnoreCase)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
m_State.HasLoadedAdminUsers = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool HasAdminRole(AdminUserSummary user)
|
||||||
|
{
|
||||||
|
return user.Roles.Contains(UserRoles.Admin, StringComparer.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly RpgRollerApiClient m_ApiClient;
|
||||||
|
private readonly Action m_ClearAuthenticatedState;
|
||||||
|
private readonly WorkspaceFeedbackService m_Feedback;
|
||||||
|
private readonly IJSRuntime m_JS;
|
||||||
|
private readonly Func<string?, Task> m_OnLoggedOutAsync;
|
||||||
|
private readonly WorkspaceState m_State;
|
||||||
|
private readonly Func<Task> m_StopStateEventsAsync;
|
||||||
|
private readonly WorkspaceQueryService m_WorkspaceQuery;
|
||||||
|
}
|
||||||
@@ -58,7 +58,7 @@ test("Rolemaster open-ended roll detail renders specialized dice chips", async (
|
|||||||
|
|
||||||
await page.goto("/");
|
await page.goto("/");
|
||||||
await expect(page.getByText("Campaign Log")).toBeVisible();
|
await expect(page.getByText("Campaign Log")).toBeVisible();
|
||||||
await expect(page.locator(".log-panel .log-event-badge")).toHaveText("Fumble");
|
await expect(page.locator(".log-panel .log-event-badge")).toContainText(["Fumble"]);
|
||||||
|
|
||||||
const logEntry = page.locator(".log-panel .log-entry-toggle").first();
|
const logEntry = page.locator(".log-panel .log-entry-toggle").first();
|
||||||
await expect(logEntry).toBeVisible();
|
await expect(logEntry).toBeVisible();
|
||||||
|
|||||||
Reference in New Issue
Block a user