Implement milestone 2 route navigation
This commit is contained in:
3
RpgRoller/Components/Pages/AdminPage.razor
Normal file
3
RpgRoller/Components/Pages/AdminPage.razor
Normal file
@@ -0,0 +1,3 @@
|
||||
@page "/admin"
|
||||
@inherits AuthenticatedPageBase
|
||||
<Workspace Route="WorkspaceRoute.Admin" LoggedOut="OnLoggedOutAsync"/>
|
||||
8
RpgRoller/Components/Pages/AdminPage.razor.cs
Normal file
8
RpgRoller/Components/Pages/AdminPage.razor.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace RpgRoller.Components.Pages;
|
||||
|
||||
[ExcludeFromCodeCoverage]
|
||||
public partial class AdminPage
|
||||
{
|
||||
}
|
||||
29
RpgRoller/Components/Pages/AuthenticatedPageBase.cs
Normal file
29
RpgRoller/Components/Pages/AuthenticatedPageBase.cs
Normal file
@@ -0,0 +1,29 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using Microsoft.AspNetCore.WebUtilities;
|
||||
|
||||
namespace RpgRoller.Components.Pages;
|
||||
|
||||
[ExcludeFromCodeCoverage]
|
||||
public abstract class AuthenticatedPageBase : ComponentBase
|
||||
{
|
||||
protected Task OnLoggedOutAsync(string? message)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(message))
|
||||
{
|
||||
Navigation.NavigateTo("/login", forceLoad: true);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
var query = new Dictionary<string, string?>
|
||||
{
|
||||
["message"] = message,
|
||||
["kind"] = message.Contains("expired", StringComparison.OrdinalIgnoreCase) ? "error" : "success"
|
||||
};
|
||||
|
||||
Navigation.NavigateTo(QueryHelpers.AddQueryString("/login", query), forceLoad: true);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
[Inject] protected NavigationManager Navigation { get; set; } = null!;
|
||||
}
|
||||
3
RpgRoller/Components/Pages/CampaignsPage.razor
Normal file
3
RpgRoller/Components/Pages/CampaignsPage.razor
Normal file
@@ -0,0 +1,3 @@
|
||||
@page "/campaigns"
|
||||
@inherits AuthenticatedPageBase
|
||||
<Workspace Route="WorkspaceRoute.Campaigns" LoggedOut="OnLoggedOutAsync"/>
|
||||
8
RpgRoller/Components/Pages/CampaignsPage.razor.cs
Normal file
8
RpgRoller/Components/Pages/CampaignsPage.razor.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace RpgRoller.Components.Pages;
|
||||
|
||||
[ExcludeFromCodeCoverage]
|
||||
public partial class CampaignsPage
|
||||
{
|
||||
}
|
||||
@@ -1,2 +1,3 @@
|
||||
@page "/play"
|
||||
<Workspace LoggedOut="OnLoggedOutAsync"/>
|
||||
@inherits AuthenticatedPageBase
|
||||
<Workspace Route="WorkspaceRoute.Play" LoggedOut="OnLoggedOutAsync"/>
|
||||
|
||||
@@ -1,29 +1,8 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
using Microsoft.AspNetCore.WebUtilities;
|
||||
|
||||
namespace RpgRoller.Components.Pages;
|
||||
|
||||
[ExcludeFromCodeCoverage]
|
||||
public partial class PlayPage
|
||||
{
|
||||
private Task OnLoggedOutAsync(string? message)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(message))
|
||||
{
|
||||
Navigation.NavigateTo("/login", forceLoad: true);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
var query = new Dictionary<string, string?>
|
||||
{
|
||||
["message"] = message,
|
||||
["kind"] = message.Contains("expired", StringComparison.OrdinalIgnoreCase) ? "error" : "success"
|
||||
};
|
||||
|
||||
Navigation.NavigateTo(QueryHelpers.AddQueryString("/login", query), forceLoad: true);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
[Inject] private NavigationManager Navigation { get; set; } = null!;
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
@using RpgRoller.Components.Pages.HomeControls
|
||||
<div class="@State.AppCssClass">
|
||||
<div class="@AppCssClass">
|
||||
<p class="sr-only" aria-live="polite">@State.LiveAnnouncement</p>
|
||||
|
||||
@if (State.HasHealthIssue)
|
||||
@@ -28,7 +28,7 @@
|
||||
ToggleMenuRequested="ToggleScreenMenu"
|
||||
LogoutRequested="Session.LogoutAsync"/>
|
||||
|
||||
@if (State.IsPlayScreen)
|
||||
@if (IsPlayRoute)
|
||||
{
|
||||
<main class="play-screen @(State.MobilePanel == "log" ? "mobile-log" : "mobile-character")">
|
||||
<CharacterPanel
|
||||
@@ -87,7 +87,7 @@
|
||||
</button>
|
||||
</nav>
|
||||
}
|
||||
else if (State.IsManagementScreen)
|
||||
else if (IsCampaignsRoute)
|
||||
{
|
||||
<CampaignManagementPanel
|
||||
Campaigns="State.Campaigns"
|
||||
@@ -106,7 +106,7 @@
|
||||
EditCharacterRequested="Campaigns.OpenEditCharacterModal"
|
||||
DeleteCharacterRequested="Campaigns.DeleteCharacterAsync"/>
|
||||
}
|
||||
else if (State.IsAdminScreen)
|
||||
else if (IsAdminRoute)
|
||||
{
|
||||
<main class="management-screen">
|
||||
@if (State.IsCurrentUserAdmin)
|
||||
|
||||
@@ -9,6 +9,11 @@ namespace RpgRoller.Components.Pages;
|
||||
[ExcludeFromCodeCoverage]
|
||||
public partial class Workspace : IAsyncDisposable
|
||||
{
|
||||
protected override void OnParametersSet()
|
||||
{
|
||||
State.IsScreenMenuOpen = false;
|
||||
}
|
||||
|
||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||
{
|
||||
State.HasInteractiveRenderStarted = true;
|
||||
@@ -100,6 +105,22 @@ public partial class Workspace : IAsyncDisposable
|
||||
State.IsScreenMenuOpen = !State.IsScreenMenuOpen;
|
||||
}
|
||||
|
||||
private Task NavigateToRouteAsync(string route)
|
||||
{
|
||||
State.IsScreenMenuOpen = false;
|
||||
Navigation.NavigateTo(route);
|
||||
return InvokeAsync(StateHasChanged);
|
||||
}
|
||||
|
||||
private Task RedirectToPlayAsync()
|
||||
{
|
||||
if (IsPlayRoute)
|
||||
return Task.CompletedTask;
|
||||
|
||||
Navigation.NavigateTo("/play");
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private static bool IsStaticRenderInteropException(InvalidOperationException exception)
|
||||
{
|
||||
return exception.Message.Contains("statically rendered", StringComparison.OrdinalIgnoreCase);
|
||||
@@ -114,22 +135,30 @@ public partial class Workspace : IAsyncDisposable
|
||||
[Inject] private NavigationManager Navigation { get; set; } = null!;
|
||||
|
||||
[Parameter] public EventCallback<string?> LoggedOut { get; set; }
|
||||
[Parameter] public WorkspaceRoute Route { get; set; } = WorkspaceRoute.Play;
|
||||
|
||||
private WorkspaceState State { get; } = new();
|
||||
private bool HasSessionInitialized { get; set; }
|
||||
private bool EnableCharacterControls { get; set; }
|
||||
private bool EnableCustomRollComposer { get; set; }
|
||||
private bool IsPlayRoute => Route == WorkspaceRoute.Play;
|
||||
private bool IsCampaignsRoute => Route == WorkspaceRoute.Campaigns;
|
||||
private bool IsAdminRoute => Route == WorkspaceRoute.Admin;
|
||||
private string AppCssClass => IsPlayRoute ? "rr-app app-play" : "rr-app";
|
||||
|
||||
private WorkspaceCampaignScopeCoordinator Scope => m_Scope ??= new(State, Feedback, JS, WorkspaceQuery,
|
||||
Play.EnsureSelectedCharacterActiveAsync, Play.RefreshSelectedCharacterSheetAsync, Play.RefreshCampaignLogAsync,
|
||||
Play.ResetCampaignLogDetailState, Play.ResetCampaignStateTracking, ClearAuthenticatedState,
|
||||
() => IsPlayRoute, Play.EnsureSelectedCharacterActiveAsync, Play.RefreshSelectedCharacterSheetAsync,
|
||||
Play.RefreshCampaignLogAsync, Play.ResetCampaignLogDetailState, Play.ResetCampaignStateTracking,
|
||||
ClearAuthenticatedState,
|
||||
StopStateEventsAsync, message => LoggedOut.InvokeAsync(message));
|
||||
|
||||
private WorkspaceLiveStateController Live => m_Live ??= new(State, Feedback, StartStateEventsCoreAsync,
|
||||
private WorkspaceLiveStateController Live => m_Live ??= new(State, Feedback, () => IsPlayRoute, () => IsAdminRoute,
|
||||
StartStateEventsCoreAsync,
|
||||
StopStateEventsCoreAsync, Scope.RefreshCampaignRosterAsync, Play.RefreshSelectedCharacterSheetAsync,
|
||||
Play.RefreshCampaignLogAsync, () => InvokeAsync(StateHasChanged));
|
||||
|
||||
private WorkspacePlayCoordinator Play => m_Play ??= new(State, Feedback, ApiClient, WorkspaceQuery,
|
||||
private WorkspacePlayCoordinator Play => m_Play ??= new(State, Feedback, () => IsPlayRoute, ApiClient,
|
||||
WorkspaceQuery,
|
||||
CanEditCharacter, () => InvokeAsync(StateHasChanged));
|
||||
|
||||
private WorkspaceCampaignCoordinator Campaigns => m_Campaigns ??= new(State, Feedback, JS, ApiClient,
|
||||
@@ -142,10 +171,10 @@ public partial class Workspace : IAsyncDisposable
|
||||
private WorkspaceFeedbackService Feedback => m_Feedback ??= new(State, () => InvokeAsync(StateHasChanged));
|
||||
|
||||
private WorkspaceSessionCoordinator Session => m_Session ??= new(State, Feedback, JS, ApiClient, WorkspaceQuery,
|
||||
() => IsAdminRoute, RedirectToPlayAsync,
|
||||
Scope.ReloadCampaignsAsync, Scope.ReloadCharacterCampaignOptionsAsync, Scope.RefreshCampaignScopeAsync,
|
||||
Live.SyncStateEventsAsync, Live.StopStateEventsAsync, EnsureAdminUsersLoadedAsync,
|
||||
Play.ResetCampaignLogDetailState, () => InvokeAsync(StateHasChanged),
|
||||
message => LoggedOut.InvokeAsync(message));
|
||||
Play.ResetCampaignLogDetailState, message => LoggedOut.InvokeAsync(message));
|
||||
|
||||
private IReadOnlyList<AppHeaderMenuItem> HeaderMenuItems
|
||||
{
|
||||
@@ -156,14 +185,14 @@ public partial class Workspace : IAsyncDisposable
|
||||
new()
|
||||
{
|
||||
Label = "Play",
|
||||
IsActive = State.IsPlayScreen,
|
||||
OnSelected = () => Session.SwitchScreenAsync("play")
|
||||
IsActive = IsPlayRoute,
|
||||
OnSelected = () => NavigateToRouteAsync("/play")
|
||||
},
|
||||
new()
|
||||
{
|
||||
Label = "Campaign Management",
|
||||
IsActive = State.IsManagementScreen,
|
||||
OnSelected = () => Session.SwitchScreenAsync("management")
|
||||
IsActive = IsCampaignsRoute,
|
||||
OnSelected = () => NavigateToRouteAsync("/campaigns")
|
||||
}
|
||||
};
|
||||
|
||||
@@ -172,8 +201,8 @@ public partial class Workspace : IAsyncDisposable
|
||||
items.Add(new()
|
||||
{
|
||||
Label = "Admin",
|
||||
IsActive = State.IsAdminScreen,
|
||||
OnSelected = () => Session.SwitchScreenAsync(ScreenAdmin)
|
||||
IsActive = IsAdminRoute,
|
||||
OnSelected = () => NavigateToRouteAsync("/admin")
|
||||
});
|
||||
}
|
||||
|
||||
@@ -184,7 +213,6 @@ public partial class Workspace : IAsyncDisposable
|
||||
private string AdminDatabaseDownloadUrl => Navigation.ToAbsoluteUri("api/admin/database").ToString();
|
||||
private DotNetObjectReference<Workspace>? DotNetRef { get; set; }
|
||||
|
||||
private const string ScreenAdmin = "admin";
|
||||
private WorkspaceAdminCoordinator? m_Admin;
|
||||
private WorkspaceCampaignCoordinator? m_Campaigns;
|
||||
private WorkspaceFeedbackService? m_Feedback;
|
||||
|
||||
@@ -4,7 +4,20 @@ using Microsoft.JSInterop;
|
||||
namespace RpgRoller.Components.Pages;
|
||||
|
||||
[ExcludeFromCodeCoverage]
|
||||
public sealed class WorkspaceCampaignScopeCoordinator(WorkspaceState state, WorkspaceFeedbackService feedback, IJSRuntime js, WorkspaceQueryService workspaceQuery, Func<Task> ensureSelectedCharacterActiveAsync, Func<Task> refreshSelectedCharacterSheetAsync, Func<Guid?, Task> refreshCampaignLogAsync, Action resetCampaignLogDetailState, Action resetCampaignStateTracking, Action clearAuthenticatedState, Func<Task> stopStateEventsAsync, Func<string?, Task> onLoggedOutAsync)
|
||||
public sealed class WorkspaceCampaignScopeCoordinator(
|
||||
WorkspaceState state,
|
||||
WorkspaceFeedbackService feedback,
|
||||
IJSRuntime js,
|
||||
WorkspaceQueryService workspaceQuery,
|
||||
Func<bool> isPlayRoute,
|
||||
Func<Task> ensureSelectedCharacterActiveAsync,
|
||||
Func<Task> refreshSelectedCharacterSheetAsync,
|
||||
Func<Guid?, Task> refreshCampaignLogAsync,
|
||||
Action resetCampaignLogDetailState,
|
||||
Action resetCampaignStateTracking,
|
||||
Action clearAuthenticatedState,
|
||||
Func<Task> stopStateEventsAsync,
|
||||
Func<string?, Task> onLoggedOutAsync)
|
||||
{
|
||||
public async Task ReloadCampaignsAsync(Guid? preferredCampaignId)
|
||||
{
|
||||
@@ -24,13 +37,15 @@ public sealed class WorkspaceCampaignScopeCoordinator(WorkspaceState state, Work
|
||||
else if (!state.SelectedCampaignId.HasValue || !campaignIds.Contains(state.SelectedCampaignId.Value))
|
||||
state.SelectedCampaignId = state.Campaigns[0].Id;
|
||||
|
||||
await js.InvokeVoidAsync("rpgRollerApi.setSessionValue", CampaignSessionKey, state.SelectedCampaignId?.ToString());
|
||||
await js.InvokeVoidAsync("rpgRollerApi.setSessionValue", CampaignSessionKey,
|
||||
state.SelectedCampaignId?.ToString());
|
||||
}
|
||||
|
||||
public async Task ReloadCharacterCampaignOptionsAsync()
|
||||
{
|
||||
var campaignOptions = await workspaceQuery.GetCharacterCampaignOptionsAsync();
|
||||
state.CharacterCampaignOptions = campaignOptions.OrderBy(campaign => campaign.Name, StringComparer.OrdinalIgnoreCase).ToList();
|
||||
state.CharacterCampaignOptions =
|
||||
campaignOptions.OrderBy(campaign => campaign.Name, StringComparer.OrdinalIgnoreCase).ToList();
|
||||
}
|
||||
|
||||
public async Task RefreshCampaignRosterAsync()
|
||||
@@ -45,7 +60,8 @@ public sealed class WorkspaceCampaignScopeCoordinator(WorkspaceState state, Work
|
||||
state.SelectedCampaign = await workspaceQuery.GetCampaignAsync(state.SelectedCampaignId.Value);
|
||||
SyncSelectedCharacter();
|
||||
|
||||
if (state.IsPlayScreen && state.PlaySelectedCharacterId.HasValue && state.SelectedCharacterId != state.PlaySelectedCharacterId)
|
||||
if (isPlayRoute() && state.PlaySelectedCharacterId.HasValue &&
|
||||
state.SelectedCharacterId != state.PlaySelectedCharacterId)
|
||||
state.SelectedCharacterId = state.PlaySelectedCharacterId;
|
||||
|
||||
await ensureSelectedCharacterActiveAsync();
|
||||
|
||||
@@ -4,7 +4,17 @@ using RpgRoller.Contracts;
|
||||
namespace RpgRoller.Components.Pages;
|
||||
|
||||
[ExcludeFromCodeCoverage]
|
||||
public sealed class WorkspaceLiveStateController(WorkspaceState state, WorkspaceFeedbackService feedback, Func<Guid, Task> startStateEventsAsync, Func<Task> stopStateEventsCoreAsync, Func<Task> refreshCampaignRosterAsync, Func<Task> refreshSelectedCharacterSheetAsync, Func<Guid?, Task> refreshCampaignLogAsync, Func<Task> requestRefreshAsync)
|
||||
public sealed class WorkspaceLiveStateController(
|
||||
WorkspaceState state,
|
||||
WorkspaceFeedbackService feedback,
|
||||
Func<bool> isPlayRoute,
|
||||
Func<bool> isAdminRoute,
|
||||
Func<Guid, Task> startStateEventsAsync,
|
||||
Func<Task> stopStateEventsCoreAsync,
|
||||
Func<Task> refreshCampaignRosterAsync,
|
||||
Func<Task> refreshSelectedCharacterSheetAsync,
|
||||
Func<Guid?, Task> refreshCampaignLogAsync,
|
||||
Func<Task> requestRefreshAsync)
|
||||
{
|
||||
public async Task OnStateEventReceivedAsync(CampaignStateSnapshot state1)
|
||||
{
|
||||
@@ -27,15 +37,17 @@ public sealed class WorkspaceLiveStateController(WorkspaceState state, Workspace
|
||||
var previousSelectedCharacterId = state.SelectedCharacterId;
|
||||
var previousSelectedCharacterVersion = GetCharacterVersion(previousState, previousSelectedCharacterId);
|
||||
var rosterChanged = state1.RosterVersion != previousState.RosterVersion;
|
||||
var logChanged = state.IsPlayScreen && state1.LogVersion != previousState.LogVersion;
|
||||
var logChanged = isPlayRoute() && state1.LogVersion != previousState.LogVersion;
|
||||
|
||||
if (rosterChanged)
|
||||
await refreshCampaignRosterAsync();
|
||||
|
||||
var selectedCharacterChanged = previousSelectedCharacterId != state.SelectedCharacterId;
|
||||
var selectedCharacterVersionChanged = state.IsPlayScreen && !selectedCharacterChanged && GetCharacterVersion(state1, state.SelectedCharacterId) != previousSelectedCharacterVersion;
|
||||
var selectedCharacterVersionChanged = isPlayRoute() && !selectedCharacterChanged &&
|
||||
GetCharacterVersion(state1, state.SelectedCharacterId) !=
|
||||
previousSelectedCharacterVersion;
|
||||
|
||||
if (state.IsPlayScreen && (selectedCharacterChanged || selectedCharacterVersionChanged))
|
||||
if (isPlayRoute() && (selectedCharacterChanged || selectedCharacterVersionChanged))
|
||||
await refreshSelectedCharacterSheetAsync();
|
||||
|
||||
if (logChanged)
|
||||
@@ -54,9 +66,9 @@ public sealed class WorkspaceLiveStateController(WorkspaceState state, Workspace
|
||||
{
|
||||
state.ConnectionState = state1 switch
|
||||
{
|
||||
"connected" => "connected",
|
||||
"connected" => "connected",
|
||||
"reconnecting" => "reconnecting",
|
||||
_ => "offline"
|
||||
_ => "offline"
|
||||
};
|
||||
|
||||
if (state.ConnectionState == "reconnecting")
|
||||
@@ -70,7 +82,7 @@ public sealed class WorkspaceLiveStateController(WorkspaceState state, Workspace
|
||||
|
||||
public async Task SyncStateEventsAsync()
|
||||
{
|
||||
if (state.User is null || !state.SelectedCampaignId.HasValue || state.IsAdminScreen)
|
||||
if (state.User is null || !state.SelectedCampaignId.HasValue || isAdminRoute())
|
||||
{
|
||||
await StopStateEventsAsync();
|
||||
state.ConnectionState = "offline";
|
||||
@@ -94,6 +106,7 @@ public sealed class WorkspaceLiveStateController(WorkspaceState state, Workspace
|
||||
if (!characterId.HasValue)
|
||||
return 0;
|
||||
|
||||
return snapshot.CharacterVersions.FirstOrDefault(version => version.CharacterId == characterId.Value)?.Version ?? 0;
|
||||
return snapshot.CharacterVersions.FirstOrDefault(version => version.CharacterId == characterId.Value)
|
||||
?.Version ?? 0;
|
||||
}
|
||||
}
|
||||
@@ -5,11 +5,18 @@ using RpgRoller.Contracts;
|
||||
namespace RpgRoller.Components.Pages;
|
||||
|
||||
[ExcludeFromCodeCoverage]
|
||||
public sealed class WorkspacePlayCoordinator(WorkspaceState state, WorkspaceFeedbackService feedback, RpgRollerApiClient apiClient, WorkspaceQueryService workspaceQuery, Func<CharacterSummary, bool> canEditCharacter, Func<Task> requestRefreshAsync)
|
||||
public sealed class WorkspacePlayCoordinator(
|
||||
WorkspaceState state,
|
||||
WorkspaceFeedbackService feedback,
|
||||
Func<bool> isPlayRoute,
|
||||
RpgRollerApiClient apiClient,
|
||||
WorkspaceQueryService workspaceQuery,
|
||||
Func<CharacterSummary, bool> canEditCharacter,
|
||||
Func<Task> requestRefreshAsync)
|
||||
{
|
||||
public async Task RefreshCampaignLogAsync(Guid? afterRollId = null)
|
||||
{
|
||||
if (!state.SelectedCampaignId.HasValue || !state.IsPlayScreen)
|
||||
if (!state.SelectedCampaignId.HasValue || !isPlayRoute())
|
||||
{
|
||||
state.CampaignLog = [];
|
||||
state.CampaignLogCursor = null;
|
||||
@@ -18,7 +25,8 @@ public sealed class WorkspacePlayCoordinator(WorkspaceState state, WorkspaceFeed
|
||||
}
|
||||
|
||||
var previousLogCount = state.CampaignLog.Count;
|
||||
var page = await workspaceQuery.GetCampaignLogPageAsync(state.SelectedCampaignId.Value, afterRollId, CampaignLogWindowSize);
|
||||
var page = await workspaceQuery.GetCampaignLogPageAsync(state.SelectedCampaignId.Value, afterRollId,
|
||||
CampaignLogWindowSize);
|
||||
Guid? newestRollId = null;
|
||||
if (!afterRollId.HasValue || page.ResetRequired)
|
||||
state.CampaignLog = page.Entries.ToList();
|
||||
@@ -30,7 +38,8 @@ public sealed class WorkspacePlayCoordinator(WorkspaceState state, WorkspaceFeed
|
||||
}
|
||||
|
||||
var shouldAutoExpandNewest = afterRollId.HasValue && page.Entries.Length > 0;
|
||||
if (!shouldAutoExpandNewest && !afterRollId.HasValue && state.CurrentCampaignState is not null && previousLogCount == 0 && page.Entries.Length > 0)
|
||||
if (!shouldAutoExpandNewest && !afterRollId.HasValue && state.CurrentCampaignState is not null &&
|
||||
previousLogCount == 0 && page.Entries.Length > 0)
|
||||
shouldAutoExpandNewest = true;
|
||||
|
||||
if (shouldAutoExpandNewest)
|
||||
@@ -58,7 +67,7 @@ public sealed class WorkspacePlayCoordinator(WorkspaceState state, WorkspaceFeed
|
||||
|
||||
public async Task RefreshSelectedCharacterSheetAsync()
|
||||
{
|
||||
if (!state.SelectedCharacterId.HasValue || state.SelectedCampaign is null || !state.IsPlayScreen)
|
||||
if (!state.SelectedCharacterId.HasValue || state.SelectedCampaign is null || !isPlayRoute())
|
||||
{
|
||||
state.SelectedCharacterSkills = [];
|
||||
state.SelectedCharacterSkillGroups = [];
|
||||
@@ -66,8 +75,10 @@ public sealed class WorkspacePlayCoordinator(WorkspaceState state, WorkspaceFeed
|
||||
}
|
||||
|
||||
var sheet = await workspaceQuery.GetCharacterSheetAsync(state.SelectedCharacterId.Value);
|
||||
state.SelectedCharacterSkillGroups = sheet.SkillGroups.OrderBy(group => group.Name, StringComparer.OrdinalIgnoreCase).ToList();
|
||||
state.SelectedCharacterSkills = sheet.Skills.OrderBy(skill => skill.Name, StringComparer.OrdinalIgnoreCase).ToList();
|
||||
state.SelectedCharacterSkillGroups =
|
||||
sheet.SkillGroups.OrderBy(group => group.Name, StringComparer.OrdinalIgnoreCase).ToList();
|
||||
state.SelectedCharacterSkills =
|
||||
sheet.Skills.OrderBy(skill => skill.Name, StringComparer.OrdinalIgnoreCase).ToList();
|
||||
}
|
||||
|
||||
public Task EnsureSelectedCharacterActiveAsync()
|
||||
@@ -152,7 +163,8 @@ public sealed class WorkspacePlayCoordinator(WorkspaceState state, WorkspaceFeed
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
if (string.Equals(state.SelectedCampaign.RulesetId, RulesetFormHelpers.RulesetIds.Rolemaster, StringComparison.OrdinalIgnoreCase))
|
||||
if (string.Equals(state.SelectedCampaign.RulesetId, RulesetFormHelpers.RulesetIds.Rolemaster,
|
||||
StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
OpenRolemasterSkillRollModal(skill);
|
||||
return Task.CompletedTask;
|
||||
@@ -177,7 +189,8 @@ public sealed class WorkspacePlayCoordinator(WorkspaceState state, WorkspaceFeed
|
||||
state.IsSubmittingRolemasterSkillRoll = true;
|
||||
try
|
||||
{
|
||||
await ExecuteSkillRollAsync(state.PendingRolemasterSkillRoll.Id, situationalModifier, keepModalOpenOnError: true);
|
||||
await ExecuteSkillRollAsync(state.PendingRolemasterSkillRoll.Id, situationalModifier,
|
||||
keepModalOpenOnError: true);
|
||||
}
|
||||
finally
|
||||
{
|
||||
@@ -199,7 +212,8 @@ public sealed class WorkspacePlayCoordinator(WorkspaceState state, WorkspaceFeed
|
||||
state.IsMutating = true;
|
||||
try
|
||||
{
|
||||
var roll = await apiClient.RequestAsync<RollResult>("POST", $"/api/skills/{skillId}/roll", new RollSkillRequest(state.RollVisibility, situationalModifier));
|
||||
var roll = await apiClient.RequestAsync<RollResult>("POST", $"/api/skills/{skillId}/roll",
|
||||
new RollSkillRequest(state.RollVisibility, situationalModifier));
|
||||
CloseRolemasterSkillRollModal();
|
||||
await HandleRecordedRollAsync(roll);
|
||||
}
|
||||
@@ -314,7 +328,8 @@ public sealed class WorkspacePlayCoordinator(WorkspaceState state, WorkspaceFeed
|
||||
return;
|
||||
|
||||
var character = state.SelectedCampaign.Characters.FirstOrDefault(c => c.Id == state.SelectedCharacterId.Value);
|
||||
if (character is null || !CanActivateCharacter(character, state.User) || state.ActiveCharacterId == character.Id)
|
||||
if (character is null || !CanActivateCharacter(character, state.User) ||
|
||||
state.ActiveCharacterId == character.Id)
|
||||
return;
|
||||
|
||||
try
|
||||
@@ -345,13 +360,16 @@ public sealed class WorkspacePlayCoordinator(WorkspaceState state, WorkspaceFeed
|
||||
{
|
||||
var visibleRollIds = state.CampaignLog.Select(entry => entry.RollId).ToHashSet();
|
||||
|
||||
foreach (var rollId in state.CampaignLogDetails.Keys.Where(rollId => !visibleRollIds.Contains(rollId)).ToArray())
|
||||
foreach (var rollId in state.CampaignLogDetails.Keys.Where(rollId => !visibleRollIds.Contains(rollId))
|
||||
.ToArray())
|
||||
state.CampaignLogDetails.Remove(rollId);
|
||||
|
||||
foreach (var rollId in state.CampaignLogDetailsLoading.Where(rollId => !visibleRollIds.Contains(rollId)).ToArray())
|
||||
foreach (var rollId in state.CampaignLogDetailsLoading.Where(rollId => !visibleRollIds.Contains(rollId))
|
||||
.ToArray())
|
||||
state.CampaignLogDetailsLoading.Remove(rollId);
|
||||
|
||||
foreach (var rollId in state.CampaignLogDetailErrors.Keys.Where(rollId => !visibleRollIds.Contains(rollId)).ToArray())
|
||||
foreach (var rollId in state.CampaignLogDetailErrors.Keys.Where(rollId => !visibleRollIds.Contains(rollId))
|
||||
.ToArray())
|
||||
state.CampaignLogDetailErrors.Remove(rollId);
|
||||
|
||||
if (state.ExpandedCampaignLogRollId.HasValue && !visibleRollIds.Contains(state.ExpandedCampaignLogRollId.Value))
|
||||
|
||||
8
RpgRoller/Components/Pages/WorkspaceRoute.cs
Normal file
8
RpgRoller/Components/Pages/WorkspaceRoute.cs
Normal file
@@ -0,0 +1,8 @@
|
||||
namespace RpgRoller.Components.Pages;
|
||||
|
||||
public enum WorkspaceRoute
|
||||
{
|
||||
Play,
|
||||
Campaigns,
|
||||
Admin
|
||||
}
|
||||
@@ -3,18 +3,31 @@ using RpgRoller.Contracts;
|
||||
|
||||
namespace RpgRoller.Components.Pages;
|
||||
|
||||
public sealed class WorkspaceSessionCoordinator(WorkspaceState state, WorkspaceFeedbackService feedback, IJSRuntime js, RpgRollerApiClient apiClient, WorkspaceQueryService workspaceQuery, Func<Guid?, Task> reloadCampaignsAsync, Func<Task> reloadCharacterCampaignOptionsAsync, Func<Task> refreshCampaignScopeAsync, Func<Task> syncStateEventsAsync, Func<Task> stopStateEventsAsync, Func<Task> ensureAdminUsersLoadedAsync, Action resetCampaignLogDetailState, Func<Task> requestRefreshAsync, Func<string?, Task> onLoggedOutAsync)
|
||||
public sealed class WorkspaceSessionCoordinator(
|
||||
WorkspaceState state,
|
||||
WorkspaceFeedbackService feedback,
|
||||
IJSRuntime js,
|
||||
RpgRollerApiClient apiClient,
|
||||
WorkspaceQueryService workspaceQuery,
|
||||
Func<bool> isAdminRoute,
|
||||
Func<Task> redirectToPlayAsync,
|
||||
Func<Guid?, Task> reloadCampaignsAsync,
|
||||
Func<Task> reloadCharacterCampaignOptionsAsync,
|
||||
Func<Task> refreshCampaignScopeAsync,
|
||||
Func<Task> syncStateEventsAsync,
|
||||
Func<Task> stopStateEventsAsync,
|
||||
Func<Task> ensureAdminUsersLoadedAsync,
|
||||
Action resetCampaignLogDetailState,
|
||||
Func<string?, Task> onLoggedOutAsync)
|
||||
{
|
||||
public async Task InitializeAsync()
|
||||
{
|
||||
var storedScreen = await js.InvokeAsync<string?>("rpgRollerApi.getSessionValue", ScreenSessionKey);
|
||||
state.CurrentScreen = NormalizeRequestedScreen(storedScreen) ?? ScreenPlay;
|
||||
|
||||
var storedPanel = await js.InvokeAsync<string?>("rpgRollerApi.getSessionValue", MobilePanelSessionKey);
|
||||
if (string.Equals(storedPanel, "log", StringComparison.OrdinalIgnoreCase))
|
||||
state.MobilePanel = "log";
|
||||
|
||||
var storedRollVisibility = await js.InvokeAsync<string?>("rpgRollerApi.getSessionValue", RollVisibilitySessionKey);
|
||||
var storedRollVisibility =
|
||||
await js.InvokeAsync<string?>("rpgRollerApi.getSessionValue", RollVisibilitySessionKey);
|
||||
state.RollVisibility = NormalizeRollVisibility(storedRollVisibility);
|
||||
|
||||
Guid? preferredCampaignId = null;
|
||||
@@ -78,30 +91,6 @@ public sealed class WorkspaceSessionCoordinator(WorkspaceState state, WorkspaceF
|
||||
await onLoggedOutAsync("Logged out.");
|
||||
}
|
||||
|
||||
public async Task SwitchScreenAsync(string screen)
|
||||
{
|
||||
var targetScreen = NormalizeRequestedScreen(screen) ?? ScreenPlay;
|
||||
if (string.Equals(targetScreen, ScreenAdmin, StringComparison.OrdinalIgnoreCase) && !state.IsCurrentUserAdmin)
|
||||
targetScreen = ScreenPlay;
|
||||
|
||||
state.CurrentScreen = targetScreen;
|
||||
state.IsScreenMenuOpen = false;
|
||||
await PersistScreenPreferenceAsync(state.CurrentScreen);
|
||||
await requestRefreshAsync();
|
||||
|
||||
if (state.User is not null)
|
||||
{
|
||||
await refreshCampaignScopeAsync();
|
||||
await syncStateEventsAsync();
|
||||
}
|
||||
|
||||
if (state.IsAdminScreen)
|
||||
{
|
||||
await ensureAdminUsersLoadedAsync();
|
||||
await requestRefreshAsync();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task OnRollVisibilityChangedAsync(string visibility)
|
||||
{
|
||||
state.RollVisibility = NormalizeRollVisibility(visibility);
|
||||
@@ -168,14 +157,15 @@ public sealed class WorkspaceSessionCoordinator(WorkspaceState state, WorkspaceF
|
||||
|
||||
state.User = me.User;
|
||||
state.ActiveCharacterId = me.ActiveCharacterId;
|
||||
await EnsureScreenAccessAsync();
|
||||
if (!await EnsureRouteAccessAsync())
|
||||
return true;
|
||||
|
||||
await reloadCampaignsAsync(preferredCampaignId ?? me.CurrentCampaignId);
|
||||
await reloadCharacterCampaignOptionsAsync();
|
||||
await refreshCampaignScopeAsync();
|
||||
await syncStateEventsAsync();
|
||||
|
||||
if (state.IsAdminScreen)
|
||||
if (isAdminRoute())
|
||||
await ensureAdminUsersLoadedAsync();
|
||||
|
||||
return true;
|
||||
@@ -193,33 +183,17 @@ public sealed class WorkspaceSessionCoordinator(WorkspaceState state, WorkspaceF
|
||||
}
|
||||
}
|
||||
|
||||
private async Task EnsureScreenAccessAsync()
|
||||
private async Task<bool> EnsureRouteAccessAsync()
|
||||
{
|
||||
if (state.IsCurrentUserAdmin)
|
||||
return;
|
||||
if (state.IsCurrentUserAdmin || !isAdminRoute())
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
state.AdminUsers = [];
|
||||
state.HasLoadedAdminUsers = false;
|
||||
|
||||
if (!state.IsAdminScreen)
|
||||
return;
|
||||
|
||||
state.CurrentScreen = ScreenPlay;
|
||||
await PersistScreenPreferenceAsync(state.CurrentScreen);
|
||||
}
|
||||
|
||||
private async Task PersistScreenPreferenceAsync(string screen)
|
||||
{
|
||||
try
|
||||
{
|
||||
await js.InvokeVoidAsync("rpgRollerApi.setSessionValue", ScreenSessionKey, screen);
|
||||
}
|
||||
catch (JSDisconnectedException)
|
||||
{
|
||||
}
|
||||
catch (InvalidOperationException ex) when (IsStaticRenderInteropException(ex))
|
||||
{
|
||||
}
|
||||
await redirectToPlayAsync();
|
||||
return false;
|
||||
}
|
||||
|
||||
private static string NormalizeRollVisibility(string? visibility)
|
||||
@@ -227,29 +201,6 @@ public sealed class WorkspaceSessionCoordinator(WorkspaceState state, WorkspaceF
|
||||
return string.Equals(visibility, "private", StringComparison.OrdinalIgnoreCase) ? "private" : "public";
|
||||
}
|
||||
|
||||
private static string? NormalizeRequestedScreen(string? screen)
|
||||
{
|
||||
if (string.Equals(screen, ScreenAdmin, StringComparison.OrdinalIgnoreCase))
|
||||
return ScreenAdmin;
|
||||
|
||||
if (string.Equals(screen, ScreenManagement, StringComparison.OrdinalIgnoreCase))
|
||||
return ScreenManagement;
|
||||
|
||||
if (string.Equals(screen, ScreenPlay, StringComparison.OrdinalIgnoreCase))
|
||||
return ScreenPlay;
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static bool IsStaticRenderInteropException(InvalidOperationException exception)
|
||||
{
|
||||
return exception.Message.Contains("JavaScript interop calls cannot be issued", StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
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";
|
||||
private const string RollVisibilitySessionKey = "roll-visibility";
|
||||
|
||||
@@ -17,7 +17,9 @@ public sealed class WorkspaceState
|
||||
if (ownerUserId == SelectedCampaign.Gm.Id)
|
||||
return $"{SelectedCampaign.Gm.DisplayName} (GM)";
|
||||
|
||||
var ownerDisplayName = SelectedCampaign.Characters.Where(character => character.OwnerUserId == ownerUserId).Select(character => character.OwnerDisplayName).FirstOrDefault(displayName => !string.IsNullOrWhiteSpace(displayName));
|
||||
var ownerDisplayName = SelectedCampaign.Characters.Where(character => character.OwnerUserId == ownerUserId)
|
||||
.Select(character => character.OwnerDisplayName)
|
||||
.FirstOrDefault(displayName => !string.IsNullOrWhiteSpace(displayName));
|
||||
|
||||
return string.IsNullOrWhiteSpace(ownerDisplayName) ? "Unknown owner" : ownerDisplayName;
|
||||
}
|
||||
@@ -26,8 +28,10 @@ public sealed class WorkspaceState
|
||||
{
|
||||
if (!string.Equals(SelectedCampaign?.RulesetId, "d6", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
if (string.Equals(SelectedCampaign?.RulesetId, RulesetFormHelpers.RulesetIds.Rolemaster, StringComparison.OrdinalIgnoreCase))
|
||||
return RulesetFormHelpers.DescribeRolemasterExpression(skill.DiceRollDefinition, skill.FumbleRange, skill.RolemasterAutoRetry);
|
||||
if (string.Equals(SelectedCampaign?.RulesetId, RulesetFormHelpers.RulesetIds.Rolemaster,
|
||||
StringComparison.OrdinalIgnoreCase))
|
||||
return RulesetFormHelpers.DescribeRolemasterExpression(skill.DiceRollDefinition, skill.FumbleRange,
|
||||
skill.RolemasterAutoRetry);
|
||||
|
||||
return skill.DiceRollDefinition;
|
||||
}
|
||||
@@ -59,7 +63,6 @@ public sealed class WorkspaceState
|
||||
public bool HasHealthIssue { get; set; }
|
||||
public string HealthIssueMessage { get; set; } = "Retry to restore the API connection.";
|
||||
public List<WorkspaceToast> Toasts { get; } = [];
|
||||
public string CurrentScreen { get; set; } = "play";
|
||||
public string MobilePanel { get; set; } = "character";
|
||||
public string ConnectionState { get; set; } = "offline";
|
||||
public string LiveAnnouncement { get; set; } = string.Empty;
|
||||
@@ -88,7 +91,9 @@ public sealed class WorkspaceState
|
||||
public HashSet<Guid> CampaignLogDetailsLoading { get; } = [];
|
||||
public Dictionary<Guid, string> CampaignLogDetailErrors { get; } = [];
|
||||
|
||||
public string? SelectedCampaignName => SelectedCampaign?.Name ?? Campaigns.FirstOrDefault(campaign => campaign.Id == SelectedCampaignId)?.Name;
|
||||
public string? SelectedCampaignName => SelectedCampaign?.Name ??
|
||||
Campaigns.FirstOrDefault(campaign => campaign.Id == SelectedCampaignId)
|
||||
?.Name;
|
||||
|
||||
public CharacterSummary? SelectedCharacter =>
|
||||
SelectedCampaign?.Characters.FirstOrDefault(character => character.Id == SelectedCharacterId);
|
||||
@@ -101,11 +106,14 @@ public sealed class WorkspaceState
|
||||
return null;
|
||||
|
||||
if (User is null)
|
||||
return new(SelectedCampaign.Id, SelectedCampaign.Name, SelectedCampaign.RulesetId, SelectedCampaign.Gm, []);
|
||||
return new(SelectedCampaign.Id, SelectedCampaign.Name, SelectedCampaign.RulesetId, SelectedCampaign.Gm,
|
||||
[]);
|
||||
|
||||
var ownedCharacters = SelectedCampaign.Characters.Where(character => character.OwnerUserId == User.Id).ToArray();
|
||||
var ownedCharacters = SelectedCampaign.Characters.Where(character => character.OwnerUserId == User.Id)
|
||||
.ToArray();
|
||||
|
||||
return new(SelectedCampaign.Id, SelectedCampaign.Name, SelectedCampaign.RulesetId, SelectedCampaign.Gm, ownedCharacters);
|
||||
return new(SelectedCampaign.Id, SelectedCampaign.Name, SelectedCampaign.RulesetId, SelectedCampaign.Gm,
|
||||
ownedCharacters);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -119,14 +127,18 @@ public sealed class WorkspaceState
|
||||
|
||||
if (SelectedCharacterId.HasValue)
|
||||
{
|
||||
var selectedCharacter = playSelectedCampaign.Characters.FirstOrDefault(character => character.Id == SelectedCharacterId.Value);
|
||||
var selectedCharacter =
|
||||
playSelectedCampaign.Characters.FirstOrDefault(character =>
|
||||
character.Id == SelectedCharacterId.Value);
|
||||
if (selectedCharacter is not null)
|
||||
return selectedCharacter;
|
||||
}
|
||||
|
||||
if (ActiveCharacterId.HasValue)
|
||||
{
|
||||
var activeCharacter = playSelectedCampaign.Characters.FirstOrDefault(character => character.Id == ActiveCharacterId.Value);
|
||||
var activeCharacter =
|
||||
playSelectedCampaign.Characters.FirstOrDefault(character =>
|
||||
character.Id == ActiveCharacterId.Value);
|
||||
if (activeCharacter is not null)
|
||||
return activeCharacter;
|
||||
}
|
||||
@@ -157,23 +169,17 @@ public sealed class WorkspaceState
|
||||
public bool IsSelectedCampaignD6 =>
|
||||
string.Equals(SelectedCampaign?.RulesetId, "d6", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
public bool IsPlayScreen => string.Equals(CurrentScreen, "play", StringComparison.OrdinalIgnoreCase);
|
||||
public bool IsManagementScreen => string.Equals(CurrentScreen, "management", StringComparison.OrdinalIgnoreCase);
|
||||
public bool IsAdminScreen => string.Equals(CurrentScreen, "admin", StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
public string ConnectionStateLabel => ConnectionState switch
|
||||
{
|
||||
"connected" => "Connected",
|
||||
"connected" => "Connected",
|
||||
"reconnecting" => "Reconnecting",
|
||||
_ => "Offline fallback"
|
||||
_ => "Offline fallback"
|
||||
};
|
||||
|
||||
public string ConnectionStateCssClass => ConnectionState switch
|
||||
{
|
||||
"connected" => "ok",
|
||||
"connected" => "ok",
|
||||
"reconnecting" => "warn",
|
||||
_ => "offline"
|
||||
_ => "offline"
|
||||
};
|
||||
|
||||
public string AppCssClass => IsPlayScreen ? "rr-app app-play" : "rr-app";
|
||||
}
|
||||
Reference in New Issue
Block a user