From 4f77d4a70218305c41db60e13664adc525798a57 Mon Sep 17 00:00:00 2001 From: Frank Tovar Date: Sat, 4 Apr 2026 23:55:19 +0200 Subject: [PATCH] Extract workspace state holder --- README.md | 1 + RpgRoller/Components/Pages/Workspace.razor.cs | 203 ++++++------------ RpgRoller/Components/Pages/WorkspaceState.cs | 150 +++++++++++++ RpgRoller/Components/Pages/WorkspaceToast.cs | 3 + 4 files changed, 218 insertions(+), 139 deletions(-) create mode 100644 RpgRoller/Components/Pages/WorkspaceState.cs create mode 100644 RpgRoller/Components/Pages/WorkspaceToast.cs diff --git a/README.md b/README.md index 7e6323e..04f0af7 100644 --- a/README.md +++ b/README.md @@ -35,6 +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` and `WorkspaceToast.cs`: extracted workspace UI state, toast records, and pure computed projections 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 9bf84b5..2c53753 100644 --- a/RpgRoller/Components/Pages/Workspace.razor.cs +++ b/RpgRoller/Components/Pages/Workspace.razor.cs @@ -1146,141 +1146,80 @@ public partial class Workspace : IAsyncDisposable [Inject] private NavigationManager Navigation { get; set; } = null!; - private UserSummary? User { get; set; } - private Guid? ActiveCharacterId { get; set; } - private Guid? SelectedCampaignId { get; set; } - private CampaignRoster? SelectedCampaign { get; set; } - private List Campaigns { get; set; } = []; - private List CharacterCampaignOptions { get; set; } = []; - private List SelectedCharacterSkills { get; set; } = []; - private List SelectedCharacterSkillGroups { get; set; } = []; - private List CampaignLog { get; set; } = []; - private List Rulesets { get; set; } = []; - private List AdminUsers { get; set; } = []; - private Guid? SelectedCharacterId { get; set; } - private RollResult? LastRoll { get; set; } - private List KnownUsernames { get; set; } = []; - private string RollVisibility { get; set; } = "public"; + private WorkspaceState State { get; } = new(); - 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 Toasts { get; } = []; - private string CurrentScreen { get; set; } = "play"; - private string MobilePanel { get; set; } = "character"; - private string ConnectionState { get; set; } = "offline"; - private string LiveAnnouncement { get; set; } = string.Empty; - private bool IsScreenMenuOpen { get; set; } + private UserSummary? User { get => State.User; set => State.User = value; } + private Guid? ActiveCharacterId { get => State.ActiveCharacterId; set => State.ActiveCharacterId = value; } + private Guid? SelectedCampaignId { get => State.SelectedCampaignId; set => State.SelectedCampaignId = value; } + private CampaignRoster? SelectedCampaign { get => State.SelectedCampaign; set => State.SelectedCampaign = value; } + private List Campaigns { get => State.Campaigns; set => State.Campaigns = value; } + private List CharacterCampaignOptions { get => State.CharacterCampaignOptions; set => State.CharacterCampaignOptions = value; } + private List SelectedCharacterSkills { get => State.SelectedCharacterSkills; set => State.SelectedCharacterSkills = value; } + private List SelectedCharacterSkillGroups { get => State.SelectedCharacterSkillGroups; set => State.SelectedCharacterSkillGroups = value; } + private List CampaignLog { get => State.CampaignLog; set => State.CampaignLog = value; } + private List Rulesets { get => State.Rulesets; set => State.Rulesets = value; } + private List AdminUsers { get => State.AdminUsers; set => State.AdminUsers = value; } + private Guid? SelectedCharacterId { get => State.SelectedCharacterId; set => State.SelectedCharacterId = value; } + private RollResult? LastRoll { get => State.LastRoll; set => State.LastRoll = value; } + private List KnownUsernames { get => State.KnownUsernames; set => State.KnownUsernames = value; } + private string RollVisibility { get => State.RollVisibility; set => State.RollVisibility = value; } - private bool ShowCreateCharacterModal { get; set; } - private bool ShowEditCharacterModal { get; set; } - private bool CanEditCharacterOwner { get; set; } - private Guid? EditingCharacterId { get; set; } - private CharacterFormModel CreateCharacterInitialModel { get; set; } = new(); - private CharacterFormModel EditCharacterInitialModel { get; set; } = new(); - private int CreateCharacterFormVersion { get; set; } - private int EditCharacterFormVersion { get; set; } - private bool StateRefreshInProgress { get; set; } - private bool HasInteractiveRenderStarted { get; set; } + private bool IsMutating { get => State.IsMutating; set => State.IsMutating = value; } + private bool IsCampaignDataLoading { get => State.IsCampaignDataLoading; set => State.IsCampaignDataLoading = value; } + private bool IsAdminDataLoading { get => State.IsAdminDataLoading; set => State.IsAdminDataLoading = value; } + private bool HasLoadedAdminUsers { get => State.HasLoadedAdminUsers; set => State.HasLoadedAdminUsers = value; } + private bool HasHealthIssue { get => State.HasHealthIssue; set => State.HasHealthIssue = value; } + private string HealthIssueMessage { get => State.HealthIssueMessage; set => State.HealthIssueMessage = value; } + private List Toasts => State.Toasts; + private string CurrentScreen { get => State.CurrentScreen; set => State.CurrentScreen = value; } + private string MobilePanel { get => State.MobilePanel; set => State.MobilePanel = value; } + private string ConnectionState { get => State.ConnectionState; set => State.ConnectionState = value; } + private string LiveAnnouncement { get => State.LiveAnnouncement; set => State.LiveAnnouncement = value; } + private bool IsScreenMenuOpen { get => State.IsScreenMenuOpen; set => State.IsScreenMenuOpen = value; } + + private bool ShowCreateCharacterModal { get => State.ShowCreateCharacterModal; set => State.ShowCreateCharacterModal = value; } + private bool ShowEditCharacterModal { get => State.ShowEditCharacterModal; set => State.ShowEditCharacterModal = value; } + private bool CanEditCharacterOwner { get => State.CanEditCharacterOwner; set => State.CanEditCharacterOwner = value; } + private Guid? EditingCharacterId { get => State.EditingCharacterId; set => State.EditingCharacterId = value; } + private CharacterFormModel CreateCharacterInitialModel { get => State.CreateCharacterInitialModel; set => State.CreateCharacterInitialModel = value; } + private CharacterFormModel EditCharacterInitialModel { get => State.EditCharacterInitialModel; set => State.EditCharacterInitialModel = value; } + private int CreateCharacterFormVersion { get => State.CreateCharacterFormVersion; set => State.CreateCharacterFormVersion = value; } + private int EditCharacterFormVersion { get => State.EditCharacterFormVersion; set => State.EditCharacterFormVersion = value; } + private bool StateRefreshInProgress { get => State.StateRefreshInProgress; set => State.StateRefreshInProgress = value; } + private bool HasInteractiveRenderStarted { get => State.HasInteractiveRenderStarted; set => State.HasInteractiveRenderStarted = value; } private DotNetObjectReference? DotNetRef { get; set; } - private CampaignStateSnapshot? CurrentCampaignState { get; set; } - private Guid? CampaignLogCursor { get; set; } - private Guid? ExpandedCampaignLogRollId { get; set; } - private Guid? FreshCampaignLogRollId { get; set; } - private Dictionary CampaignLogDetails { get; } = []; - private HashSet CampaignLogDetailsLoading { get; } = []; - private Dictionary CampaignLogDetailErrors { get; } = []; + private CampaignStateSnapshot? CurrentCampaignState { get => State.CurrentCampaignState; set => State.CurrentCampaignState = value; } + private Guid? CampaignLogCursor { get => State.CampaignLogCursor; set => State.CampaignLogCursor = value; } + private Guid? ExpandedCampaignLogRollId { get => State.ExpandedCampaignLogRollId; set => State.ExpandedCampaignLogRollId = value; } + private Guid? FreshCampaignLogRollId { get => State.FreshCampaignLogRollId; set => State.FreshCampaignLogRollId = value; } + private Dictionary CampaignLogDetails => State.CampaignLogDetails; + private HashSet CampaignLogDetailsLoading => State.CampaignLogDetailsLoading; + private Dictionary CampaignLogDetailErrors => State.CampaignLogDetailErrors; [Parameter] public EventCallback LoggedOut { get; set; } - private string? SelectedCampaignName => SelectedCampaign?.Name ?? Campaigns.FirstOrDefault(campaign => campaign.Id == SelectedCampaignId)?.Name; - - private CharacterSummary? SelectedCharacter => - SelectedCampaign?.Characters.FirstOrDefault(c => c.Id == SelectedCharacterId); - - private CampaignRoster? PlaySelectedCampaign - { - get - { - if (SelectedCampaign is null) - return null; - - if (User is null) - return new CampaignRoster(SelectedCampaign.Id, SelectedCampaign.Name, SelectedCampaign.RulesetId, SelectedCampaign.Gm, []); - - var ownedCharacters = SelectedCampaign.Characters - .Where(character => character.OwnerUserId == User.Id) - .ToList(); - - return new CampaignRoster( - SelectedCampaign.Id, - SelectedCampaign.Name, - SelectedCampaign.RulesetId, - SelectedCampaign.Gm, - ownedCharacters.ToArray()); - } - } - - private CharacterSummary? PlaySelectedCharacter - { - get - { - var playSelectedCampaign = PlaySelectedCampaign; - if (playSelectedCampaign is null || playSelectedCampaign.Characters.Length == 0) - return null; - - if (SelectedCharacterId.HasValue) - { - 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); - if (activeCharacter is not null) - return activeCharacter; - } - - return playSelectedCampaign.Characters[0]; - } - } - - private Guid? PlaySelectedCharacterId => PlaySelectedCharacter?.Id; - - private List PlaySelectedCharacterSkills => - PlaySelectedCampaign is null || !PlaySelectedCharacterId.HasValue ? [] : SelectedCharacterSkills; - - private List PlaySelectedCharacterSkillGroups => - PlaySelectedCampaign is null || !PlaySelectedCharacterId.HasValue ? [] : SelectedCharacterSkillGroups; - - private List PlayVisibleCampaignLog => CampaignLog; - - private bool IsCurrentUserGm => - SelectedCampaign is not null && User is not null && SelectedCampaign.Gm.Id == User.Id; - - private bool IsCurrentUserAdmin => - User is not null && User.Roles.Contains(UserRoles.Admin, StringComparer.OrdinalIgnoreCase); - - private bool CanDeleteSelectedCampaign => - SelectedCampaign is not null && User is not null && (SelectedCampaign.Gm.Id == User.Id || IsCurrentUserAdmin); - - private bool IsSelectedCampaignD6 => - string.Equals(SelectedCampaign?.RulesetId, "d6", StringComparison.OrdinalIgnoreCase); + private string? SelectedCampaignName => State.SelectedCampaignName; + private CharacterSummary? SelectedCharacter => State.SelectedCharacter; + private CampaignRoster? PlaySelectedCampaign => State.PlaySelectedCampaign; + private CharacterSummary? PlaySelectedCharacter => State.PlaySelectedCharacter; + private Guid? PlaySelectedCharacterId => State.PlaySelectedCharacterId; + private List PlaySelectedCharacterSkills => State.PlaySelectedCharacterSkills; + private List PlaySelectedCharacterSkillGroups => State.PlaySelectedCharacterSkillGroups; + private List PlayVisibleCampaignLog => State.PlayVisibleCampaignLog; + private bool IsCurrentUserGm => State.IsCurrentUserGm; + private bool IsCurrentUserAdmin => State.IsCurrentUserAdmin; + private bool CanDeleteSelectedCampaign => State.CanDeleteSelectedCampaign; + private bool IsSelectedCampaignD6 => State.IsSelectedCampaignD6; private static bool HasAdminRole(AdminUserSummary user) { return user.Roles.Contains(UserRoles.Admin, StringComparer.OrdinalIgnoreCase); } - 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 bool IsPlayScreen => State.IsPlayScreen; + private bool IsManagementScreen => State.IsManagementScreen; + private bool IsAdminScreen => State.IsAdminScreen; private IReadOnlyList HeaderMenuItems { get @@ -1298,21 +1237,9 @@ public partial class Workspace : IAsyncDisposable } } - private string ConnectionStateLabel => ConnectionState switch - { - "connected" => "Connected", - "reconnecting" => "Reconnecting", - _ => "Offline fallback" - }; - - private string ConnectionStateCssClass => ConnectionState switch - { - "connected" => "ok", - "reconnecting" => "warn", - _ => "offline" - }; - - private string AppCssClass => IsPlayScreen ? "rr-app app-play" : "rr-app"; + private string ConnectionStateLabel => State.ConnectionStateLabel; + private string ConnectionStateCssClass => State.ConnectionStateCssClass; + private string AppCssClass => State.AppCssClass; private string AdminDatabaseDownloadUrl => Navigation.ToAbsoluteUri("api/admin/database").ToString(); private const string ScreenPlay = "play"; @@ -1324,6 +1251,4 @@ public partial class Workspace : IAsyncDisposable private const string RollVisibilitySessionKey = "roll-visibility"; private const int CampaignLogWindowSize = 25; private const int ToastDurationMs = 3200; - - private sealed record WorkspaceToast(Guid Id, string Message, bool IsError); } diff --git a/RpgRoller/Components/Pages/WorkspaceState.cs b/RpgRoller/Components/Pages/WorkspaceState.cs new file mode 100644 index 0000000..fca7a3f --- /dev/null +++ b/RpgRoller/Components/Pages/WorkspaceState.cs @@ -0,0 +1,150 @@ +using RpgRoller.Contracts; +using RpgRoller.Domain; + +namespace RpgRoller.Components.Pages; + +public sealed class WorkspaceState +{ + public UserSummary? User { get; set; } + public Guid? ActiveCharacterId { get; set; } + public Guid? SelectedCampaignId { get; set; } + public CampaignRoster? SelectedCampaign { get; set; } + public List Campaigns { get; set; } = []; + public List CharacterCampaignOptions { get; set; } = []; + public List SelectedCharacterSkills { get; set; } = []; + public List SelectedCharacterSkillGroups { get; set; } = []; + public List CampaignLog { get; set; } = []; + public List Rulesets { get; set; } = []; + public List AdminUsers { get; set; } = []; + public Guid? SelectedCharacterId { get; set; } + public RollResult? LastRoll { get; set; } + public List KnownUsernames { get; set; } = []; + public string RollVisibility { get; set; } = "public"; + + public bool IsMutating { get; set; } + public bool IsCampaignDataLoading { get; set; } + public bool IsAdminDataLoading { get; set; } + public bool HasLoadedAdminUsers { get; set; } + public bool HasHealthIssue { get; set; } + public string HealthIssueMessage { get; set; } = "Retry to restore the API connection."; + public List 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; + public bool IsScreenMenuOpen { get; set; } + + public bool ShowCreateCharacterModal { get; set; } + public bool ShowEditCharacterModal { get; set; } + public bool CanEditCharacterOwner { get; set; } + public Guid? EditingCharacterId { get; set; } + public CharacterFormModel CreateCharacterInitialModel { get; set; } = new(); + public CharacterFormModel EditCharacterInitialModel { get; set; } = new(); + public int CreateCharacterFormVersion { get; set; } + public int EditCharacterFormVersion { get; set; } + public bool StateRefreshInProgress { get; set; } + public bool HasInteractiveRenderStarted { get; set; } + public CampaignStateSnapshot? CurrentCampaignState { get; set; } + public Guid? CampaignLogCursor { get; set; } + public Guid? ExpandedCampaignLogRollId { get; set; } + public Guid? FreshCampaignLogRollId { get; set; } + public Dictionary CampaignLogDetails { get; } = []; + public HashSet CampaignLogDetailsLoading { get; } = []; + public Dictionary CampaignLogDetailErrors { get; } = []; + + public string? SelectedCampaignName => SelectedCampaign?.Name ?? Campaigns.FirstOrDefault(campaign => campaign.Id == SelectedCampaignId)?.Name; + + public CharacterSummary? SelectedCharacter => + SelectedCampaign?.Characters.FirstOrDefault(character => character.Id == SelectedCharacterId); + + public CampaignRoster? PlaySelectedCampaign + { + get + { + if (SelectedCampaign is null) + return null; + + if (User is null) + return new CampaignRoster(SelectedCampaign.Id, SelectedCampaign.Name, SelectedCampaign.RulesetId, SelectedCampaign.Gm, []); + + var ownedCharacters = SelectedCampaign.Characters + .Where(character => character.OwnerUserId == User.Id) + .ToArray(); + + return new CampaignRoster( + SelectedCampaign.Id, + SelectedCampaign.Name, + SelectedCampaign.RulesetId, + SelectedCampaign.Gm, + ownedCharacters); + } + } + + public CharacterSummary? PlaySelectedCharacter + { + get + { + var playSelectedCampaign = PlaySelectedCampaign; + if (playSelectedCampaign is null || playSelectedCampaign.Characters.Length == 0) + return null; + + if (SelectedCharacterId.HasValue) + { + 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); + if (activeCharacter is not null) + return activeCharacter; + } + + return playSelectedCampaign.Characters[0]; + } + } + + public Guid? PlaySelectedCharacterId => PlaySelectedCharacter?.Id; + + public List PlaySelectedCharacterSkills => + PlaySelectedCampaign is null || !PlaySelectedCharacterId.HasValue ? [] : SelectedCharacterSkills; + + public List PlaySelectedCharacterSkillGroups => + PlaySelectedCampaign is null || !PlaySelectedCharacterId.HasValue ? [] : SelectedCharacterSkillGroups; + + public List PlayVisibleCampaignLog => CampaignLog; + + public bool IsCurrentUserGm => + SelectedCampaign is not null && User is not null && SelectedCampaign.Gm.Id == User.Id; + + public bool IsCurrentUserAdmin => + User is not null && User.Roles.Contains(UserRoles.Admin, StringComparer.OrdinalIgnoreCase); + + public bool CanDeleteSelectedCampaign => + SelectedCampaign is not null && User is not null && (SelectedCampaign.Gm.Id == User.Id || IsCurrentUserAdmin); + + 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", + "reconnecting" => "Reconnecting", + _ => "Offline fallback" + }; + + public string ConnectionStateCssClass => ConnectionState switch + { + "connected" => "ok", + "reconnecting" => "warn", + _ => "offline" + }; + + public string AppCssClass => IsPlayScreen ? "rr-app app-play" : "rr-app"; +} diff --git a/RpgRoller/Components/Pages/WorkspaceToast.cs b/RpgRoller/Components/Pages/WorkspaceToast.cs new file mode 100644 index 0000000..1af114b --- /dev/null +++ b/RpgRoller/Components/Pages/WorkspaceToast.cs @@ -0,0 +1,3 @@ +namespace RpgRoller.Components.Pages; + +public sealed record WorkspaceToast(Guid Id, string Message, bool IsError);