Extract workspace campaign scope 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`, `WorkspaceSessionCoordinator.cs`, `WorkspaceAdminCoordinator.cs`, `WorkspaceCampaignCoordinator.cs`, `WorkspacePlayCoordinator.cs`, and `WorkspaceLiveStateController.cs`: extracted workspace UI state, toast records, feedback/status handling, session/bootstrap orchestration, admin user actions, campaign-management/modal flows, play/log coordination, and live-state reconciliation while `Workspace` remains the behavior owner
|
- `RpgRoller/Components/Pages/WorkspaceState.cs`, `WorkspaceToast.cs`, `WorkspaceFeedbackService.cs`, `WorkspaceSessionCoordinator.cs`, `WorkspaceAdminCoordinator.cs`, `WorkspaceCampaignCoordinator.cs`, `WorkspaceCampaignScopeCoordinator.cs`, `WorkspacePlayCoordinator.cs`, and `WorkspaceLiveStateController.cs`: extracted workspace UI state, toast records, feedback/status handling, session/bootstrap orchestration, admin user actions, campaign-management/modal flows, campaign-scope loading, play/log coordination, and live-state reconciliation 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`
|
||||||
|
|||||||
@@ -30,92 +30,15 @@ public partial class Workspace : IAsyncDisposable
|
|||||||
return Session.LoadKnownUsernamesAsync();
|
return Session.LoadKnownUsernamesAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task ReloadCampaignsAsync(Guid? preferredCampaignId)
|
private Task ReloadCampaignsAsync(Guid? preferredCampaignId) => Scope.ReloadCampaignsAsync(preferredCampaignId);
|
||||||
{
|
|
||||||
var campaigns = await WorkspaceQuery.GetCampaignsAsync();
|
|
||||||
Campaigns = campaigns.OrderBy(c => c.Name, StringComparer.OrdinalIgnoreCase).ToList();
|
|
||||||
|
|
||||||
if (Campaigns.Count == 0)
|
private Task ReloadCharacterCampaignOptionsAsync() => Scope.ReloadCharacterCampaignOptionsAsync();
|
||||||
{
|
|
||||||
SelectedCampaignId = null;
|
|
||||||
await JS.InvokeVoidAsync("rpgRollerApi.setSessionValue", CampaignSessionKey, null);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var campaignIds = Campaigns.Select(c => c.Id).ToHashSet();
|
private Task RefreshCampaignRosterAsync() => Scope.RefreshCampaignRosterAsync();
|
||||||
if (preferredCampaignId.HasValue && campaignIds.Contains(preferredCampaignId.Value))
|
|
||||||
SelectedCampaignId = preferredCampaignId.Value;
|
|
||||||
else if (!SelectedCampaignId.HasValue || !campaignIds.Contains(SelectedCampaignId.Value))
|
|
||||||
SelectedCampaignId = Campaigns[0].Id;
|
|
||||||
|
|
||||||
await JS.InvokeVoidAsync("rpgRollerApi.setSessionValue", CampaignSessionKey, SelectedCampaignId?.ToString());
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task ReloadCharacterCampaignOptionsAsync()
|
|
||||||
{
|
|
||||||
var campaignOptions = await WorkspaceQuery.GetCharacterCampaignOptionsAsync();
|
|
||||||
CharacterCampaignOptions = campaignOptions.OrderBy(campaign => campaign.Name, StringComparer.OrdinalIgnoreCase).ToList();
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task RefreshCampaignRosterAsync()
|
|
||||||
{
|
|
||||||
if (!SelectedCampaignId.HasValue)
|
|
||||||
{
|
|
||||||
SelectedCampaign = null;
|
|
||||||
SelectedCharacterId = null;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
SelectedCampaign = await WorkspaceQuery.GetCampaignAsync(SelectedCampaignId.Value);
|
|
||||||
SyncSelectedCharacter();
|
|
||||||
|
|
||||||
if (IsPlayScreen && PlaySelectedCharacterId.HasValue && SelectedCharacterId != PlaySelectedCharacterId)
|
|
||||||
SelectedCharacterId = PlaySelectedCharacterId;
|
|
||||||
|
|
||||||
await EnsureSelectedCharacterActiveAsync();
|
|
||||||
}
|
|
||||||
|
|
||||||
private Task RefreshCampaignLogAsync(Guid? afterRollId = null) => Play.RefreshCampaignLogAsync(afterRollId);
|
private Task RefreshCampaignLogAsync(Guid? afterRollId = null) => Play.RefreshCampaignLogAsync(afterRollId);
|
||||||
|
|
||||||
private async Task RefreshCampaignScopeAsync()
|
private Task RefreshCampaignScopeAsync() => Scope.RefreshCampaignScopeAsync();
|
||||||
{
|
|
||||||
if (!SelectedCampaignId.HasValue)
|
|
||||||
{
|
|
||||||
SelectedCampaign = null;
|
|
||||||
SelectedCharacterSkills = [];
|
|
||||||
SelectedCharacterSkillGroups = [];
|
|
||||||
CampaignLog = [];
|
|
||||||
SelectedCharacterId = null;
|
|
||||||
ConnectionState = "offline";
|
|
||||||
CurrentCampaignState = null;
|
|
||||||
CampaignLogCursor = null;
|
|
||||||
ResetCampaignLogDetailState();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
IsCampaignDataLoading = true;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
await RefreshCampaignRosterAsync();
|
|
||||||
await RefreshSelectedCharacterSheetAsync();
|
|
||||||
await RefreshCampaignLogAsync();
|
|
||||||
ResetCampaignStateTracking();
|
|
||||||
}
|
|
||||||
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
|
|
||||||
{
|
|
||||||
IsCampaignDataLoading = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private Task LogoutAsync() => Session.LogoutAsync();
|
private Task LogoutAsync() => Session.LogoutAsync();
|
||||||
|
|
||||||
@@ -142,11 +65,7 @@ public partial class Workspace : IAsyncDisposable
|
|||||||
|
|
||||||
private Task DeleteUserAsync(AdminUserSummary user) => Admin.DeleteUserAsync(user);
|
private Task DeleteUserAsync(AdminUserSummary user) => Admin.DeleteUserAsync(user);
|
||||||
|
|
||||||
private async Task SetMobilePanelAsync(string panel)
|
private Task SetMobilePanelAsync(string panel) => Scope.SetMobilePanelAsync(panel);
|
||||||
{
|
|
||||||
MobilePanel = string.Equals(panel, "log", StringComparison.OrdinalIgnoreCase) ? "log" : "character";
|
|
||||||
await JS.InvokeVoidAsync("rpgRollerApi.setSessionValue", MobilePanelSessionKey, MobilePanel);
|
|
||||||
}
|
|
||||||
|
|
||||||
private Task SetMobilePanelCharacterAsync()
|
private Task SetMobilePanelCharacterAsync()
|
||||||
{
|
{
|
||||||
@@ -253,27 +172,6 @@ public partial class Workspace : IAsyncDisposable
|
|||||||
return exception.Message.Contains("statically rendered", StringComparison.OrdinalIgnoreCase);
|
return exception.Message.Contains("statically rendered", StringComparison.OrdinalIgnoreCase);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void SyncSelectedCharacter()
|
|
||||||
{
|
|
||||||
if (SelectedCampaign is null || SelectedCampaign.Characters.Length == 0)
|
|
||||||
{
|
|
||||||
SelectedCharacterId = null;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var candidateIds = SelectedCampaign.Characters.Select(c => c.Id).ToHashSet();
|
|
||||||
if (SelectedCharacterId.HasValue && candidateIds.Contains(SelectedCharacterId.Value))
|
|
||||||
return;
|
|
||||||
|
|
||||||
if (ActiveCharacterId.HasValue && candidateIds.Contains(ActiveCharacterId.Value))
|
|
||||||
{
|
|
||||||
SelectedCharacterId = ActiveCharacterId;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
SelectedCharacterId = SelectedCampaign.Characters[0].Id;
|
|
||||||
}
|
|
||||||
|
|
||||||
private string OwnerLabel(Guid ownerUserId)
|
private string OwnerLabel(Guid ownerUserId)
|
||||||
{
|
{
|
||||||
if (User is not null && ownerUserId == User.Id)
|
if (User is not null && ownerUserId == User.Id)
|
||||||
@@ -415,6 +313,19 @@ public partial class Workspace : IAsyncDisposable
|
|||||||
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 WorkspaceCampaignScopeCoordinator Scope => m_Scope ??= new(
|
||||||
|
State,
|
||||||
|
Feedback,
|
||||||
|
JS,
|
||||||
|
WorkspaceQuery,
|
||||||
|
EnsureSelectedCharacterActiveAsync,
|
||||||
|
RefreshSelectedCharacterSheetAsync,
|
||||||
|
RefreshCampaignLogAsync,
|
||||||
|
ResetCampaignLogDetailState,
|
||||||
|
ResetCampaignStateTracking,
|
||||||
|
ClearAuthenticatedState,
|
||||||
|
StopStateEventsAsync,
|
||||||
|
message => LoggedOut.InvokeAsync(message));
|
||||||
private WorkspaceLiveStateController Live => m_Live ??= new(
|
private WorkspaceLiveStateController Live => m_Live ??= new(
|
||||||
State,
|
State,
|
||||||
Feedback,
|
Feedback,
|
||||||
@@ -489,10 +400,8 @@ public partial class Workspace : IAsyncDisposable
|
|||||||
private string AdminDatabaseDownloadUrl => Navigation.ToAbsoluteUri("api/admin/database").ToString();
|
private string AdminDatabaseDownloadUrl => Navigation.ToAbsoluteUri("api/admin/database").ToString();
|
||||||
|
|
||||||
private const string ScreenAdmin = "admin";
|
private const string ScreenAdmin = "admin";
|
||||||
private const string CampaignSessionKey = "campaign";
|
|
||||||
private const string MobilePanelSessionKey = "play-panel";
|
|
||||||
private const int CampaignLogWindowSize = 25;
|
|
||||||
|
|
||||||
|
private WorkspaceCampaignScopeCoordinator? m_Scope;
|
||||||
private WorkspaceLiveStateController? m_Live;
|
private WorkspaceLiveStateController? m_Live;
|
||||||
private WorkspacePlayCoordinator? m_Play;
|
private WorkspacePlayCoordinator? m_Play;
|
||||||
private WorkspaceCampaignCoordinator? m_CampaignsFlow;
|
private WorkspaceCampaignCoordinator? m_CampaignsFlow;
|
||||||
|
|||||||
165
RpgRoller/Components/Pages/WorkspaceCampaignScopeCoordinator.cs
Normal file
165
RpgRoller/Components/Pages/WorkspaceCampaignScopeCoordinator.cs
Normal file
@@ -0,0 +1,165 @@
|
|||||||
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
using Microsoft.JSInterop;
|
||||||
|
using RpgRoller.Contracts;
|
||||||
|
|
||||||
|
namespace RpgRoller.Components.Pages;
|
||||||
|
|
||||||
|
[ExcludeFromCodeCoverage]
|
||||||
|
public sealed class WorkspaceCampaignScopeCoordinator
|
||||||
|
{
|
||||||
|
public 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)
|
||||||
|
{
|
||||||
|
m_State = state;
|
||||||
|
m_Feedback = feedback;
|
||||||
|
m_JS = js;
|
||||||
|
m_WorkspaceQuery = workspaceQuery;
|
||||||
|
m_EnsureSelectedCharacterActiveAsync = ensureSelectedCharacterActiveAsync;
|
||||||
|
m_RefreshSelectedCharacterSheetAsync = refreshSelectedCharacterSheetAsync;
|
||||||
|
m_RefreshCampaignLogAsync = refreshCampaignLogAsync;
|
||||||
|
m_ResetCampaignLogDetailState = resetCampaignLogDetailState;
|
||||||
|
m_ResetCampaignStateTracking = resetCampaignStateTracking;
|
||||||
|
m_ClearAuthenticatedState = clearAuthenticatedState;
|
||||||
|
m_StopStateEventsAsync = stopStateEventsAsync;
|
||||||
|
m_OnLoggedOutAsync = onLoggedOutAsync;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task ReloadCampaignsAsync(Guid? preferredCampaignId)
|
||||||
|
{
|
||||||
|
var campaigns = await m_WorkspaceQuery.GetCampaignsAsync();
|
||||||
|
m_State.Campaigns = campaigns.OrderBy(campaign => campaign.Name, StringComparer.OrdinalIgnoreCase).ToList();
|
||||||
|
|
||||||
|
if (m_State.Campaigns.Count == 0)
|
||||||
|
{
|
||||||
|
m_State.SelectedCampaignId = null;
|
||||||
|
await m_JS.InvokeVoidAsync("rpgRollerApi.setSessionValue", CampaignSessionKey, null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var campaignIds = m_State.Campaigns.Select(campaign => campaign.Id).ToHashSet();
|
||||||
|
if (preferredCampaignId.HasValue && campaignIds.Contains(preferredCampaignId.Value))
|
||||||
|
m_State.SelectedCampaignId = preferredCampaignId.Value;
|
||||||
|
else if (!m_State.SelectedCampaignId.HasValue || !campaignIds.Contains(m_State.SelectedCampaignId.Value))
|
||||||
|
m_State.SelectedCampaignId = m_State.Campaigns[0].Id;
|
||||||
|
|
||||||
|
await m_JS.InvokeVoidAsync("rpgRollerApi.setSessionValue", CampaignSessionKey, m_State.SelectedCampaignId?.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task ReloadCharacterCampaignOptionsAsync()
|
||||||
|
{
|
||||||
|
var campaignOptions = await m_WorkspaceQuery.GetCharacterCampaignOptionsAsync();
|
||||||
|
m_State.CharacterCampaignOptions = campaignOptions.OrderBy(campaign => campaign.Name, StringComparer.OrdinalIgnoreCase).ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task RefreshCampaignRosterAsync()
|
||||||
|
{
|
||||||
|
if (!m_State.SelectedCampaignId.HasValue)
|
||||||
|
{
|
||||||
|
m_State.SelectedCampaign = null;
|
||||||
|
m_State.SelectedCharacterId = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_State.SelectedCampaign = await m_WorkspaceQuery.GetCampaignAsync(m_State.SelectedCampaignId.Value);
|
||||||
|
SyncSelectedCharacter();
|
||||||
|
|
||||||
|
if (m_State.IsPlayScreen && m_State.PlaySelectedCharacterId.HasValue && m_State.SelectedCharacterId != m_State.PlaySelectedCharacterId)
|
||||||
|
m_State.SelectedCharacterId = m_State.PlaySelectedCharacterId;
|
||||||
|
|
||||||
|
await m_EnsureSelectedCharacterActiveAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task RefreshCampaignScopeAsync()
|
||||||
|
{
|
||||||
|
if (!m_State.SelectedCampaignId.HasValue)
|
||||||
|
{
|
||||||
|
m_State.SelectedCampaign = null;
|
||||||
|
m_State.SelectedCharacterSkills = [];
|
||||||
|
m_State.SelectedCharacterSkillGroups = [];
|
||||||
|
m_State.CampaignLog = [];
|
||||||
|
m_State.SelectedCharacterId = null;
|
||||||
|
m_State.ConnectionState = "offline";
|
||||||
|
m_State.CurrentCampaignState = null;
|
||||||
|
m_State.CampaignLogCursor = null;
|
||||||
|
m_ResetCampaignLogDetailState();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_State.IsCampaignDataLoading = true;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await RefreshCampaignRosterAsync();
|
||||||
|
await m_RefreshSelectedCharacterSheetAsync();
|
||||||
|
await m_RefreshCampaignLogAsync(null);
|
||||||
|
m_ResetCampaignStateTracking();
|
||||||
|
}
|
||||||
|
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.IsCampaignDataLoading = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task SetMobilePanelAsync(string panel)
|
||||||
|
{
|
||||||
|
m_State.MobilePanel = string.Equals(panel, "log", StringComparison.OrdinalIgnoreCase) ? "log" : "character";
|
||||||
|
await m_JS.InvokeVoidAsync("rpgRollerApi.setSessionValue", MobilePanelSessionKey, m_State.MobilePanel);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SyncSelectedCharacter()
|
||||||
|
{
|
||||||
|
if (m_State.SelectedCampaign is null || m_State.SelectedCampaign.Characters.Length == 0)
|
||||||
|
{
|
||||||
|
m_State.SelectedCharacterId = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var candidateIds = m_State.SelectedCampaign.Characters.Select(character => character.Id).ToHashSet();
|
||||||
|
if (m_State.SelectedCharacterId.HasValue && candidateIds.Contains(m_State.SelectedCharacterId.Value))
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (m_State.ActiveCharacterId.HasValue && candidateIds.Contains(m_State.ActiveCharacterId.Value))
|
||||||
|
{
|
||||||
|
m_State.SelectedCharacterId = m_State.ActiveCharacterId;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
m_State.SelectedCharacterId = m_State.SelectedCampaign.Characters[0].Id;
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly Action m_ClearAuthenticatedState;
|
||||||
|
private readonly Func<Task> m_EnsureSelectedCharacterActiveAsync;
|
||||||
|
private readonly WorkspaceFeedbackService m_Feedback;
|
||||||
|
private readonly IJSRuntime m_JS;
|
||||||
|
private readonly Func<string?, Task> m_OnLoggedOutAsync;
|
||||||
|
private readonly Func<Guid?, Task> m_RefreshCampaignLogAsync;
|
||||||
|
private readonly Func<Task> m_RefreshSelectedCharacterSheetAsync;
|
||||||
|
private readonly Action m_ResetCampaignLogDetailState;
|
||||||
|
private readonly Action m_ResetCampaignStateTracking;
|
||||||
|
private readonly WorkspaceState m_State;
|
||||||
|
private readonly Func<Task> m_StopStateEventsAsync;
|
||||||
|
private readonly WorkspaceQueryService m_WorkspaceQuery;
|
||||||
|
|
||||||
|
private const string CampaignSessionKey = "campaign";
|
||||||
|
private const string MobilePanelSessionKey = "play-panel";
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user