Unify play management and admin screens in workspace
This commit is contained in:
@@ -60,6 +60,5 @@ public enum HomeViewMode
|
||||
{
|
||||
Loading,
|
||||
Anonymous,
|
||||
Workspace,
|
||||
Admin
|
||||
Workspace
|
||||
}
|
||||
|
||||
@@ -22,10 +22,6 @@
|
||||
break;
|
||||
|
||||
case HomeViewMode.Workspace:
|
||||
<Workspace LoggedOut="OnLoggedOutAsync" AdminRequested="OnAdminRequestedAsync" RequestedScreen="WorkspaceScreenOverride"/>
|
||||
break;
|
||||
|
||||
case HomeViewMode.Admin:
|
||||
<AdminHome LoggedOut="OnLoggedOutAsync" WorkspaceRequested="OnWorkspaceRequestedAsync"/>
|
||||
<Workspace LoggedOut="OnLoggedOutAsync"/>
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -22,19 +22,16 @@ public partial class Home
|
||||
try
|
||||
{
|
||||
_ = await ApiClient.RequestAsync<MeResponse>("GET", "/api/me");
|
||||
WorkspaceScreenOverride = null;
|
||||
CurrentView = HomeViewMode.Workspace;
|
||||
ClearStatus();
|
||||
}
|
||||
catch (ApiRequestException ex) when (ex.StatusCode == 401)
|
||||
{
|
||||
WorkspaceScreenOverride = null;
|
||||
CurrentView = HomeViewMode.Anonymous;
|
||||
ClearStatus();
|
||||
}
|
||||
catch (ApiRequestException ex)
|
||||
{
|
||||
WorkspaceScreenOverride = null;
|
||||
CurrentView = HomeViewMode.Anonymous;
|
||||
SetStatus(ex.Message, true);
|
||||
}
|
||||
@@ -42,22 +39,6 @@ public partial class Home
|
||||
|
||||
private Task OnLoggedInAsync()
|
||||
{
|
||||
WorkspaceScreenOverride = null;
|
||||
CurrentView = HomeViewMode.Workspace;
|
||||
ClearStatus();
|
||||
return InvokeAsync(StateHasChanged);
|
||||
}
|
||||
|
||||
private Task OnAdminRequestedAsync()
|
||||
{
|
||||
CurrentView = HomeViewMode.Admin;
|
||||
ClearStatus();
|
||||
return InvokeAsync(StateHasChanged);
|
||||
}
|
||||
|
||||
private Task OnWorkspaceRequestedAsync(string screen)
|
||||
{
|
||||
WorkspaceScreenOverride = NormalizeWorkspaceScreen(screen) ?? "play";
|
||||
CurrentView = HomeViewMode.Workspace;
|
||||
ClearStatus();
|
||||
return InvokeAsync(StateHasChanged);
|
||||
@@ -65,7 +46,6 @@ public partial class Home
|
||||
|
||||
private Task OnLoggedOutAsync(string? message)
|
||||
{
|
||||
WorkspaceScreenOverride = null;
|
||||
CurrentView = HomeViewMode.Anonymous;
|
||||
if (string.IsNullOrWhiteSpace(message))
|
||||
{
|
||||
@@ -90,19 +70,7 @@ public partial class Home
|
||||
StatusIsError = false;
|
||||
}
|
||||
|
||||
private static string? NormalizeWorkspaceScreen(string? screen)
|
||||
{
|
||||
if (string.Equals(screen, "management", StringComparison.OrdinalIgnoreCase))
|
||||
return "management";
|
||||
|
||||
if (string.Equals(screen, "play", StringComparison.OrdinalIgnoreCase))
|
||||
return "play";
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private HomeViewMode CurrentView { get; set; } = HomeViewMode.Loading;
|
||||
private string? WorkspaceScreenOverride { get; set; }
|
||||
private string? StatusMessage { get; set; }
|
||||
private bool StatusIsError { get; set; }
|
||||
private bool HasInitialized { get; set; }
|
||||
|
||||
@@ -76,8 +76,7 @@
|
||||
</button>
|
||||
</nav>
|
||||
}
|
||||
|
||||
@if (IsManagementScreen)
|
||||
else if (IsManagementScreen)
|
||||
{
|
||||
<CampaignManagementPanel
|
||||
Campaigns="Campaigns"
|
||||
@@ -96,6 +95,59 @@
|
||||
EditCharacterRequested="OpenEditCharacterModal"
|
||||
DeleteCharacterRequested="DeleteCharacterAsync"/>
|
||||
}
|
||||
else if (IsAdminScreen)
|
||||
{
|
||||
<main class="management-screen">
|
||||
<section class="card">
|
||||
<div class="section-head">
|
||||
<h2>User Management</h2>
|
||||
</div>
|
||||
@if (IsAdminDataLoading)
|
||||
{
|
||||
<p class="empty">Loading users...</p>
|
||||
}
|
||||
else if (!IsCurrentUserAdmin)
|
||||
{
|
||||
<p class="empty">Admin role is required to manage users.</p>
|
||||
}
|
||||
else if (AdminUsers.Count == 0)
|
||||
{
|
||||
<p class="empty">No users found.</p>
|
||||
}
|
||||
else
|
||||
{
|
||||
<ul class="management-list">
|
||||
@foreach (var user in AdminUsers)
|
||||
{
|
||||
<li>
|
||||
<div>
|
||||
<strong>@user.Username</strong>
|
||||
<p class="muted">@user.DisplayName</p>
|
||||
<p class="muted">Roles: @(user.Roles.Count == 0 ? "none" : string.Join(", ", user.Roles))</p>
|
||||
</div>
|
||||
<div class="skill-chip-actions">
|
||||
<button type="button"
|
||||
class="chip-button"
|
||||
disabled="@(IsMutating || user.Id == User?.Id)"
|
||||
@onclick="() => ToggleAdminRoleAsync(user)">
|
||||
<span aria-hidden="true" class="emoji">🛡️</span>
|
||||
<span class="sr-only">Toggle admin role for @user.Username</span>
|
||||
</button>
|
||||
<button type="button"
|
||||
class="chip-button"
|
||||
disabled="@(IsMutating || user.Id == User?.Id)"
|
||||
@onclick="() => DeleteUserAsync(user)">
|
||||
<span aria-hidden="true" class="emoji">🗑️</span>
|
||||
<span class="sr-only">Delete user @user.Username</span>
|
||||
</button>
|
||||
</div>
|
||||
</li>
|
||||
}
|
||||
</ul>
|
||||
}
|
||||
</section>
|
||||
</main>
|
||||
}
|
||||
</div>
|
||||
|
||||
@if (Toasts.Count > 0)
|
||||
|
||||
@@ -22,18 +22,8 @@ public partial class Workspace : IAsyncDisposable
|
||||
|
||||
private async Task InitializeAsync()
|
||||
{
|
||||
var requestedScreen = NormalizeRequestedScreen(RequestedScreen);
|
||||
if (requestedScreen is not null)
|
||||
{
|
||||
CurrentScreen = requestedScreen;
|
||||
await JS.InvokeVoidAsync("rpgRollerApi.setSessionValue", ScreenSessionKey, CurrentScreen);
|
||||
}
|
||||
else
|
||||
{
|
||||
var storedScreen = await JS.InvokeAsync<string?>("rpgRollerApi.getSessionValue", ScreenSessionKey);
|
||||
if (string.Equals(storedScreen, "management", StringComparison.OrdinalIgnoreCase))
|
||||
CurrentScreen = "management";
|
||||
}
|
||||
var storedScreen = await JS.InvokeAsync<string?>("rpgRollerApi.getSessionValue", ScreenSessionKey);
|
||||
CurrentScreen = NormalizeRequestedScreen(storedScreen) ?? ScreenPlay;
|
||||
|
||||
var storedPanel = await JS.InvokeAsync<string?>("rpgRollerApi.getSessionValue", MobilePanelSessionKey);
|
||||
if (string.Equals(storedPanel, "log", StringComparison.OrdinalIgnoreCase))
|
||||
@@ -126,11 +116,16 @@ public partial class Workspace : IAsyncDisposable
|
||||
|
||||
User = me.User;
|
||||
ActiveCharacterId = me.ActiveCharacterId;
|
||||
await EnsureScreenAccessAsync();
|
||||
|
||||
await ReloadCampaignsAsync(preferredCampaignId ?? me.CurrentCampaignId);
|
||||
await ReloadCharacterCampaignOptionsAsync();
|
||||
await RefreshCampaignScopeAsync();
|
||||
await SyncStateEventsAsync();
|
||||
|
||||
if (IsAdminScreen)
|
||||
await EnsureAdminUsersLoadedAsync();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -234,9 +229,16 @@ public partial class Workspace : IAsyncDisposable
|
||||
|
||||
private async Task SwitchScreenAsync(string screen)
|
||||
{
|
||||
CurrentScreen = NormalizeRequestedScreen(screen) ?? "play";
|
||||
var targetScreen = NormalizeRequestedScreen(screen) ?? ScreenPlay;
|
||||
if (string.Equals(targetScreen, ScreenAdmin, StringComparison.OrdinalIgnoreCase) && !IsCurrentUserAdmin)
|
||||
targetScreen = ScreenPlay;
|
||||
|
||||
CurrentScreen = targetScreen;
|
||||
IsScreenMenuOpen = false;
|
||||
await JS.InvokeVoidAsync("rpgRollerApi.setSessionValue", ScreenSessionKey, CurrentScreen);
|
||||
await PersistScreenPreferenceAsync(CurrentScreen);
|
||||
|
||||
if (IsAdminScreen)
|
||||
await EnsureAdminUsersLoadedAsync();
|
||||
}
|
||||
|
||||
private Task SwitchToPlayAsync()
|
||||
@@ -249,10 +251,112 @@ public partial class Workspace : IAsyncDisposable
|
||||
return SwitchScreenAsync("management");
|
||||
}
|
||||
|
||||
private async Task OpenAdminAsync()
|
||||
private Task SwitchToAdminAsync()
|
||||
{
|
||||
IsScreenMenuOpen = false;
|
||||
await AdminRequested.InvokeAsync();
|
||||
return SwitchScreenAsync(ScreenAdmin);
|
||||
}
|
||||
|
||||
private async Task EnsureScreenAccessAsync()
|
||||
{
|
||||
if (IsCurrentUserAdmin)
|
||||
return;
|
||||
|
||||
AdminUsers = [];
|
||||
HasLoadedAdminUsers = false;
|
||||
|
||||
if (!IsAdminScreen)
|
||||
return;
|
||||
|
||||
CurrentScreen = ScreenPlay;
|
||||
await PersistScreenPreferenceAsync(CurrentScreen);
|
||||
}
|
||||
|
||||
private async Task EnsureAdminUsersLoadedAsync()
|
||||
{
|
||||
if (!IsCurrentUserAdmin || HasLoadedAdminUsers || IsAdminDataLoading)
|
||||
return;
|
||||
|
||||
IsAdminDataLoading = true;
|
||||
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()
|
||||
{
|
||||
AdminUsers = (await ApiClient.RequestAsync<IReadOnlyList<AdminUserSummary>>("GET", "/api/admin/users"))
|
||||
.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)
|
||||
@@ -533,15 +637,32 @@ public partial class Workspace : IAsyncDisposable
|
||||
|
||||
private static string? NormalizeRequestedScreen(string? screen)
|
||||
{
|
||||
if (string.Equals(screen, ScreenAdmin, StringComparison.OrdinalIgnoreCase))
|
||||
return ScreenAdmin;
|
||||
|
||||
if (string.Equals(screen, "management", StringComparison.OrdinalIgnoreCase))
|
||||
return "management";
|
||||
|
||||
if (string.Equals(screen, "play", StringComparison.OrdinalIgnoreCase))
|
||||
return "play";
|
||||
if (string.Equals(screen, ScreenPlay, StringComparison.OrdinalIgnoreCase))
|
||||
return ScreenPlay;
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private async Task PersistScreenPreferenceAsync(string screen)
|
||||
{
|
||||
try
|
||||
{
|
||||
await JS.InvokeVoidAsync("rpgRollerApi.setSessionValue", ScreenSessionKey, screen);
|
||||
}
|
||||
catch (JSDisconnectedException)
|
||||
{
|
||||
}
|
||||
catch (InvalidOperationException ex) when (IsStaticRenderInteropException(ex))
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
private bool CanEditSkill(SkillSummary skill)
|
||||
{
|
||||
if (SelectedCampaign is null)
|
||||
@@ -752,6 +873,9 @@ public partial class Workspace : IAsyncDisposable
|
||||
EditCharacterInitialModel = new();
|
||||
CreateCharacterFormVersion = 0;
|
||||
EditCharacterFormVersion = 0;
|
||||
AdminUsers = [];
|
||||
HasLoadedAdminUsers = false;
|
||||
IsAdminDataLoading = false;
|
||||
Toasts.Clear();
|
||||
}
|
||||
|
||||
@@ -808,6 +932,7 @@ public partial class Workspace : IAsyncDisposable
|
||||
private List<CampaignOption> CharacterCampaignOptions { get; set; } = [];
|
||||
private List<CampaignLogEntry> CampaignLog { get; set; } = [];
|
||||
private List<RulesetDefinition> Rulesets { get; set; } = [];
|
||||
private List<AdminUserSummary> AdminUsers { get; set; } = [];
|
||||
private Guid? SelectedCharacterId { get; set; }
|
||||
private RollResult? LastRoll { get; set; }
|
||||
private List<string> KnownUsernames { get; set; } = [];
|
||||
@@ -815,6 +940,8 @@ public partial class Workspace : IAsyncDisposable
|
||||
|
||||
private bool IsMutating { get; set; }
|
||||
private bool IsCampaignDataLoading { get; set; }
|
||||
private bool IsAdminDataLoading { get; set; }
|
||||
private bool HasLoadedAdminUsers { get; set; }
|
||||
private bool HasHealthIssue { get; set; }
|
||||
private string HealthIssueMessage { get; set; } = "Retry to restore the API connection.";
|
||||
private List<WorkspaceToast> Toasts { get; } = [];
|
||||
@@ -839,12 +966,6 @@ public partial class Workspace : IAsyncDisposable
|
||||
[Parameter]
|
||||
public EventCallback<string?> LoggedOut { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public EventCallback AdminRequested { get; set; }
|
||||
|
||||
[Parameter]
|
||||
public string? RequestedScreen { get; set; }
|
||||
|
||||
private string? SelectedCampaignName => SelectedCampaign?.Name;
|
||||
|
||||
private CharacterSummary? SelectedCharacter =>
|
||||
@@ -862,14 +983,20 @@ public partial class Workspace : IAsyncDisposable
|
||||
private bool IsSelectedCampaignD6 =>
|
||||
string.Equals(SelectedCampaign?.RulesetId, "d6", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
private static bool HasAdminRole(AdminUserSummary user)
|
||||
{
|
||||
return user.Roles.Contains(UserRoles.Admin, StringComparer.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
private List<SkillSummary> SelectedCharacterSkills =>
|
||||
SelectedCampaign is null || !SelectedCharacterId.HasValue ? [] : SelectedCampaign.Skills.Where(skill => skill.CharacterId == SelectedCharacterId.Value).OrderBy(skill => skill.Name, StringComparer.OrdinalIgnoreCase).ToList();
|
||||
|
||||
private List<SkillGroupSummary> SelectedCharacterSkillGroups =>
|
||||
SelectedCampaign is null || !SelectedCharacterId.HasValue ? [] : SelectedCampaign.SkillGroups.Where(group => group.CharacterId == SelectedCharacterId.Value).OrderBy(group => group.Name, StringComparer.OrdinalIgnoreCase).ToList();
|
||||
|
||||
private bool IsPlayScreen => string.Equals(CurrentScreen, "play", StringComparison.OrdinalIgnoreCase);
|
||||
private bool IsManagementScreen => !IsPlayScreen;
|
||||
private bool IsPlayScreen => string.Equals(CurrentScreen, ScreenPlay, StringComparison.OrdinalIgnoreCase);
|
||||
private bool IsManagementScreen => string.Equals(CurrentScreen, ScreenManagement, StringComparison.OrdinalIgnoreCase);
|
||||
private bool IsAdminScreen => string.Equals(CurrentScreen, ScreenAdmin, StringComparison.OrdinalIgnoreCase);
|
||||
private IReadOnlyList<AppHeaderMenuItem> HeaderMenuItems
|
||||
{
|
||||
get
|
||||
@@ -881,7 +1008,7 @@ public partial class Workspace : IAsyncDisposable
|
||||
};
|
||||
|
||||
if (IsCurrentUserAdmin)
|
||||
items.Add(new AppHeaderMenuItem { Label = "Admin", IsActive = false, OnSelected = OpenAdminAsync });
|
||||
items.Add(new AppHeaderMenuItem { Label = "Admin", IsActive = IsAdminScreen, OnSelected = SwitchToAdminAsync });
|
||||
|
||||
return items;
|
||||
}
|
||||
@@ -903,6 +1030,9 @@ public partial class Workspace : IAsyncDisposable
|
||||
|
||||
private string AppCssClass => IsPlayScreen ? "rr-app app-play" : "rr-app";
|
||||
|
||||
private const string ScreenPlay = "play";
|
||||
private const string ScreenManagement = "management";
|
||||
private const string ScreenAdmin = "admin";
|
||||
private const string ScreenSessionKey = "screen";
|
||||
private const string CampaignSessionKey = "campaign";
|
||||
private const string MobilePanelSessionKey = "play-panel";
|
||||
|
||||
Reference in New Issue
Block a user