From ec40baa1073f85cfd29bd8165198617f266b3bb4 Mon Sep 17 00:00:00 2001 From: Frank Tovar Date: Sun, 5 Apr 2026 00:12:19 +0200 Subject: [PATCH] Extract workspace campaign coordinator --- README.md | 2 +- RpgRoller/Components/Pages/Workspace.razor.cs | 160 ++------------- .../Pages/WorkspaceCampaignCoordinator.cs | 193 ++++++++++++++++++ 3 files changed, 216 insertions(+), 139 deletions(-) create mode 100644 RpgRoller/Components/Pages/WorkspaceCampaignCoordinator.cs diff --git a/README.md b/README.md index ee0b553..3306579 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,7 @@ Frontend: - `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/Workspace.razor`: authenticated workspace UI and workspace-specific state/logic -- `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/Pages/WorkspaceState.cs`, `WorkspaceToast.cs`, `WorkspaceFeedbackService.cs`, `WorkspaceSessionCoordinator.cs`, `WorkspaceAdminCoordinator.cs`, and `WorkspaceCampaignCoordinator.cs`: extracted workspace UI state, toast records, feedback/status handling, session/bootstrap orchestration, admin user actions, and campaign-management/modal flows 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/Pages/Home.Models.cs`: reusable `FormState` + page form models - `RpgRoller/Components/Pages/HomeControls/`: auth, campaign management, play-screen, and modal controls extracted from `Home.razor` diff --git a/RpgRoller/Components/Pages/Workspace.razor.cs b/RpgRoller/Components/Pages/Workspace.razor.cs index 9a2c285..d9416af 100644 --- a/RpgRoller/Components/Pages/Workspace.razor.cs +++ b/RpgRoller/Components/Pages/Workspace.razor.cs @@ -208,144 +208,23 @@ public partial class Workspace : IAsyncDisposable return SetMobilePanelAsync("log"); } - private async Task OnCampaignSelectionChangedAsync(ChangeEventArgs args) - { - if (!Guid.TryParse(args.Value?.ToString(), out var campaignId)) - return; + private Task OnCampaignSelectionChangedAsync(ChangeEventArgs args) => CampaignsFlow.OnCampaignSelectionChangedAsync(args); - SelectedCampaignId = campaignId; - await JS.InvokeVoidAsync("rpgRollerApi.setSessionValue", CampaignSessionKey, campaignId.ToString()); - await RefreshCampaignScopeAsync(); - await SyncStateEventsAsync(); - IsScreenMenuOpen = false; - } + private Task OnCampaignCreatedAsync(Guid campaignId) => CampaignsFlow.OnCampaignCreatedAsync(campaignId); - private async Task OnCampaignCreatedAsync(Guid campaignId) - { - await ReloadCampaignsAsync(campaignId); - await ReloadCharacterCampaignOptionsAsync(); - await RefreshCampaignScopeAsync(); - await SyncStateEventsAsync(); - SetStatus("Campaign created.", false); - } + private void OpenCreateCharacterModal() => CampaignsFlow.OpenCreateCharacterModal(); - private void OpenCreateCharacterModal() - { - CreateCharacterInitialModel = new() - { - Name = string.Empty, - CampaignId = SelectedCampaignId?.ToString() ?? CharacterCampaignOptions.FirstOrDefault()?.Id.ToString() ?? string.Empty, - OwnerUsername = string.Empty - }; + private Task OpenEditCharacterModal(CharacterSummary character) => CampaignsFlow.OpenEditCharacterModal(character); - CreateCharacterFormVersion++; - CanEditCharacterOwner = false; - ShowCreateCharacterModal = true; - } + private void CloseCharacterModals() => CampaignsFlow.CloseCharacterModals(); - private async Task OpenEditCharacterModal(CharacterSummary character) - { - if (IsCurrentUserGm || IsCurrentUserAdmin) - await LoadKnownUsernamesAsync(); + private Task OnCharacterCreatedAsync(Guid? campaignId) => CampaignsFlow.OnCharacterCreatedAsync(campaignId); - EditingCharacterId = character.Id; - EditCharacterInitialModel = new() - { - Name = character.Name, - CampaignId = character.CampaignId?.ToString() ?? string.Empty, - OwnerUsername = string.Empty - }; + private Task OnCharacterUpdatedAsync(Guid? campaignId) => CampaignsFlow.OnCharacterUpdatedAsync(campaignId); - EditCharacterFormVersion++; - CanEditCharacterOwner = IsCurrentUserGm || IsCurrentUserAdmin; - ShowEditCharacterModal = true; - } + private Task DeleteSelectedCampaignAsync() => CampaignsFlow.DeleteSelectedCampaignAsync(); - private void CloseCharacterModals() - { - ShowCreateCharacterModal = false; - ShowEditCharacterModal = false; - CanEditCharacterOwner = false; - EditingCharacterId = null; - } - - private async Task OnCharacterCreatedAsync(Guid? campaignId) - { - CloseCharacterModals(); - await ReloadCampaignsAsync(campaignId); - await ReloadCharacterCampaignOptionsAsync(); - await RefreshCampaignScopeAsync(); - await SyncStateEventsAsync(); - SetStatus("Character created.", false); - } - - private async Task OnCharacterUpdatedAsync(Guid? campaignId) - { - CloseCharacterModals(); - await ReloadCampaignsAsync(campaignId); - await ReloadCharacterCampaignOptionsAsync(); - await RefreshCampaignScopeAsync(); - await SyncStateEventsAsync(); - SetStatus(campaignId.HasValue ? "Character updated." : "Character unlinked from campaign.", false); - } - - private async Task DeleteSelectedCampaignAsync() - { - if (SelectedCampaign is null || IsMutating || !CanDeleteSelectedCampaign) - return; - - var confirmed = await JS.InvokeAsync("confirm", $"Delete campaign '{SelectedCampaign.Name}'?"); - if (!confirmed) - return; - - IsMutating = true; - try - { - _ = await ApiClient.RequestAsync("DELETE", $"/api/campaigns/{SelectedCampaign.Id}"); - await ReloadCampaignsAsync(null); - await ReloadCharacterCampaignOptionsAsync(); - await RefreshCampaignScopeAsync(); - await SyncStateEventsAsync(); - SetStatus("Campaign deleted.", false); - } - catch (ApiRequestException ex) - { - SetStatus(ex.Message, true); - } - finally - { - IsMutating = false; - } - } - - private async Task DeleteCharacterAsync(CharacterSummary character) - { - if (IsMutating || !CanDeleteCharacter(character)) - return; - - var confirmed = await JS.InvokeAsync("confirm", $"Delete character '{character.Name}'?"); - if (!confirmed) - return; - - IsMutating = true; - try - { - _ = await ApiClient.RequestAsync("DELETE", $"/api/characters/{character.Id}"); - await ReloadCampaignsAsync(SelectedCampaignId); - await ReloadCharacterCampaignOptionsAsync(); - await RefreshCampaignScopeAsync(); - await SyncStateEventsAsync(); - SetStatus("Character deleted.", false); - } - catch (ApiRequestException ex) - { - SetStatus(ex.Message, true); - } - finally - { - IsMutating = false; - } - } + private Task DeleteCharacterAsync(CharacterSummary character) => CampaignsFlow.DeleteCharacterAsync(character); private async Task SelectCharacterAsync(Guid characterId) { @@ -354,15 +233,9 @@ public partial class Workspace : IAsyncDisposable await EnsureSelectedCharacterActiveAsync(); } - private bool CanEditCharacter(CharacterSummary character) - { - return User is not null && (character.OwnerUserId == User.Id || IsCurrentUserGm || IsCurrentUserAdmin); - } + private bool CanEditCharacter(CharacterSummary character) => CampaignsFlow.CanEditCharacter(character); - private bool CanDeleteCharacter(CharacterSummary character) - { - return User is not null && (character.OwnerUserId == User.Id || IsCurrentUserAdmin); - } + private bool CanDeleteCharacter(CharacterSummary character) => CampaignsFlow.CanDeleteCharacter(character); private static bool CanActivateCharacter(CharacterSummary character, UserSummary? user) { @@ -891,6 +764,16 @@ public partial class Workspace : IAsyncDisposable private bool IsPlayScreen => State.IsPlayScreen; private bool IsManagementScreen => State.IsManagementScreen; private bool IsAdminScreen => State.IsAdminScreen; + private WorkspaceCampaignCoordinator CampaignsFlow => m_CampaignsFlow ??= new( + State, + Feedback, + JS, + ApiClient, + LoadKnownUsernamesAsync, + ReloadCampaignsAsync, + ReloadCharacterCampaignOptionsAsync, + RefreshCampaignScopeAsync, + SyncStateEventsAsync); private WorkspaceAdminCoordinator Admin => m_Admin ??= new( State, Feedback, @@ -943,6 +826,7 @@ public partial class Workspace : IAsyncDisposable private const string MobilePanelSessionKey = "play-panel"; private const int CampaignLogWindowSize = 25; + private WorkspaceCampaignCoordinator? m_CampaignsFlow; private WorkspaceAdminCoordinator? m_Admin; private WorkspaceFeedbackService? m_Feedback; private WorkspaceSessionCoordinator? m_Session; diff --git a/RpgRoller/Components/Pages/WorkspaceCampaignCoordinator.cs b/RpgRoller/Components/Pages/WorkspaceCampaignCoordinator.cs new file mode 100644 index 0000000..0548d45 --- /dev/null +++ b/RpgRoller/Components/Pages/WorkspaceCampaignCoordinator.cs @@ -0,0 +1,193 @@ +using System.Diagnostics.CodeAnalysis; +using Microsoft.AspNetCore.Components; +using Microsoft.JSInterop; +using RpgRoller.Contracts; + +namespace RpgRoller.Components.Pages; + +[ExcludeFromCodeCoverage] +public sealed class WorkspaceCampaignCoordinator +{ + public WorkspaceCampaignCoordinator( + WorkspaceState state, + WorkspaceFeedbackService feedback, + IJSRuntime js, + RpgRollerApiClient apiClient, + Func loadKnownUsernamesAsync, + Func reloadCampaignsAsync, + Func reloadCharacterCampaignOptionsAsync, + Func refreshCampaignScopeAsync, + Func syncStateEventsAsync) + { + m_State = state; + m_Feedback = feedback; + m_JS = js; + m_ApiClient = apiClient; + m_LoadKnownUsernamesAsync = loadKnownUsernamesAsync; + m_ReloadCampaignsAsync = reloadCampaignsAsync; + m_ReloadCharacterCampaignOptionsAsync = reloadCharacterCampaignOptionsAsync; + m_RefreshCampaignScopeAsync = refreshCampaignScopeAsync; + m_SyncStateEventsAsync = syncStateEventsAsync; + } + + public async Task OnCampaignSelectionChangedAsync(ChangeEventArgs args) + { + if (!Guid.TryParse(args.Value?.ToString(), out var campaignId)) + return; + + m_State.SelectedCampaignId = campaignId; + await m_JS.InvokeVoidAsync("rpgRollerApi.setSessionValue", CampaignSessionKey, campaignId.ToString()); + await m_RefreshCampaignScopeAsync(); + await m_SyncStateEventsAsync(); + m_State.IsScreenMenuOpen = false; + } + + public async Task OnCampaignCreatedAsync(Guid campaignId) + { + await m_ReloadCampaignsAsync(campaignId); + await m_ReloadCharacterCampaignOptionsAsync(); + await m_RefreshCampaignScopeAsync(); + await m_SyncStateEventsAsync(); + m_Feedback.SetStatus("Campaign created.", false); + } + + public void OpenCreateCharacterModal() + { + m_State.CreateCharacterInitialModel = new() + { + Name = string.Empty, + CampaignId = m_State.SelectedCampaignId?.ToString() ?? m_State.CharacterCampaignOptions.FirstOrDefault()?.Id.ToString() ?? string.Empty, + OwnerUsername = string.Empty + }; + + m_State.CreateCharacterFormVersion += 1; + m_State.CanEditCharacterOwner = false; + m_State.ShowCreateCharacterModal = true; + } + + public async Task OpenEditCharacterModal(CharacterSummary character) + { + if (m_State.IsCurrentUserGm || m_State.IsCurrentUserAdmin) + await m_LoadKnownUsernamesAsync(); + + m_State.EditingCharacterId = character.Id; + m_State.EditCharacterInitialModel = new() + { + Name = character.Name, + CampaignId = character.CampaignId?.ToString() ?? string.Empty, + OwnerUsername = string.Empty + }; + + m_State.EditCharacterFormVersion += 1; + m_State.CanEditCharacterOwner = m_State.IsCurrentUserGm || m_State.IsCurrentUserAdmin; + m_State.ShowEditCharacterModal = true; + } + + public void CloseCharacterModals() + { + m_State.ShowCreateCharacterModal = false; + m_State.ShowEditCharacterModal = false; + m_State.CanEditCharacterOwner = false; + m_State.EditingCharacterId = null; + } + + public async Task OnCharacterCreatedAsync(Guid? campaignId) + { + CloseCharacterModals(); + await m_ReloadCampaignsAsync(campaignId); + await m_ReloadCharacterCampaignOptionsAsync(); + await m_RefreshCampaignScopeAsync(); + await m_SyncStateEventsAsync(); + m_Feedback.SetStatus("Character created.", false); + } + + public async Task OnCharacterUpdatedAsync(Guid? campaignId) + { + CloseCharacterModals(); + await m_ReloadCampaignsAsync(campaignId); + await m_ReloadCharacterCampaignOptionsAsync(); + await m_RefreshCampaignScopeAsync(); + await m_SyncStateEventsAsync(); + m_Feedback.SetStatus(campaignId.HasValue ? "Character updated." : "Character unlinked from campaign.", false); + } + + public async Task DeleteSelectedCampaignAsync() + { + if (m_State.SelectedCampaign is null || m_State.IsMutating || !m_State.CanDeleteSelectedCampaign) + return; + + var confirmed = await m_JS.InvokeAsync("confirm", $"Delete campaign '{m_State.SelectedCampaign.Name}'?"); + if (!confirmed) + return; + + m_State.IsMutating = true; + try + { + _ = await m_ApiClient.RequestAsync("DELETE", $"/api/campaigns/{m_State.SelectedCampaign.Id}"); + await m_ReloadCampaignsAsync(null); + await m_ReloadCharacterCampaignOptionsAsync(); + await m_RefreshCampaignScopeAsync(); + await m_SyncStateEventsAsync(); + m_Feedback.SetStatus("Campaign deleted.", false); + } + catch (ApiRequestException ex) + { + m_Feedback.SetStatus(ex.Message, true); + } + finally + { + m_State.IsMutating = false; + } + } + + public async Task DeleteCharacterAsync(CharacterSummary character) + { + if (m_State.IsMutating || !CanDeleteCharacter(character)) + return; + + var confirmed = await m_JS.InvokeAsync("confirm", $"Delete character '{character.Name}'?"); + if (!confirmed) + return; + + m_State.IsMutating = true; + try + { + _ = await m_ApiClient.RequestAsync("DELETE", $"/api/characters/{character.Id}"); + await m_ReloadCampaignsAsync(m_State.SelectedCampaignId); + await m_ReloadCharacterCampaignOptionsAsync(); + await m_RefreshCampaignScopeAsync(); + await m_SyncStateEventsAsync(); + m_Feedback.SetStatus("Character deleted.", false); + } + catch (ApiRequestException ex) + { + m_Feedback.SetStatus(ex.Message, true); + } + finally + { + m_State.IsMutating = false; + } + } + + public bool CanEditCharacter(CharacterSummary character) + { + return m_State.User is not null && (character.OwnerUserId == m_State.User.Id || m_State.IsCurrentUserGm || m_State.IsCurrentUserAdmin); + } + + public bool CanDeleteCharacter(CharacterSummary character) + { + return m_State.User is not null && (character.OwnerUserId == m_State.User.Id || m_State.IsCurrentUserAdmin); + } + + private readonly RpgRollerApiClient m_ApiClient; + private readonly WorkspaceFeedbackService m_Feedback; + private readonly IJSRuntime m_JS; + private readonly Func m_LoadKnownUsernamesAsync; + private readonly Func m_ReloadCharacterCampaignOptionsAsync; + private readonly Func m_ReloadCampaignsAsync; + private readonly Func m_RefreshCampaignScopeAsync; + private readonly WorkspaceState m_State; + private readonly Func m_SyncStateEventsAsync; + + private const string CampaignSessionKey = "campaign"; +}