Extract workspace state holder

This commit is contained in:
2026-04-04 23:55:19 +02:00
parent 17b049d2ca
commit 4f77d4a702
4 changed files with 218 additions and 139 deletions

View File

@@ -35,6 +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` 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/**/*.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`

View File

@@ -1146,141 +1146,80 @@ public partial class Workspace : IAsyncDisposable
[Inject] [Inject]
private NavigationManager Navigation { get; set; } = null!; private NavigationManager Navigation { get; set; } = null!;
private UserSummary? User { get; set; } private WorkspaceState State { get; } = new();
private Guid? ActiveCharacterId { get; set; }
private Guid? SelectedCampaignId { get; set; }
private CampaignRoster? SelectedCampaign { get; set; }
private List<CampaignSummary> Campaigns { get; set; } = [];
private List<CampaignOption> CharacterCampaignOptions { get; set; } = [];
private List<CharacterSheetSkill> SelectedCharacterSkills { get; set; } = [];
private List<CharacterSheetSkillGroup> SelectedCharacterSkillGroups { get; set; } = [];
private List<CampaignLogListEntry> 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; } = [];
private string RollVisibility { get; set; } = "public";
private bool IsMutating { get; set; } private UserSummary? User { get => State.User; set => State.User = value; }
private bool IsCampaignDataLoading { get; set; } private Guid? ActiveCharacterId { get => State.ActiveCharacterId; set => State.ActiveCharacterId = value; }
private bool IsAdminDataLoading { get; set; } private Guid? SelectedCampaignId { get => State.SelectedCampaignId; set => State.SelectedCampaignId = value; }
private bool HasLoadedAdminUsers { get; set; } private CampaignRoster? SelectedCampaign { get => State.SelectedCampaign; set => State.SelectedCampaign = value; }
private bool HasHealthIssue { get; set; } private List<CampaignSummary> Campaigns { get => State.Campaigns; set => State.Campaigns = value; }
private string HealthIssueMessage { get; set; } = "Retry to restore the API connection."; private List<CampaignOption> CharacterCampaignOptions { get => State.CharacterCampaignOptions; set => State.CharacterCampaignOptions = value; }
private List<WorkspaceToast> Toasts { get; } = []; private List<CharacterSheetSkill> SelectedCharacterSkills { get => State.SelectedCharacterSkills; set => State.SelectedCharacterSkills = value; }
private string CurrentScreen { get; set; } = "play"; private List<CharacterSheetSkillGroup> SelectedCharacterSkillGroups { get => State.SelectedCharacterSkillGroups; set => State.SelectedCharacterSkillGroups = value; }
private string MobilePanel { get; set; } = "character"; private List<CampaignLogListEntry> CampaignLog { get => State.CampaignLog; set => State.CampaignLog = value; }
private string ConnectionState { get; set; } = "offline"; private List<RulesetDefinition> Rulesets { get => State.Rulesets; set => State.Rulesets = value; }
private string LiveAnnouncement { get; set; } = string.Empty; private List<AdminUserSummary> AdminUsers { get => State.AdminUsers; set => State.AdminUsers = value; }
private bool IsScreenMenuOpen { get; set; } private Guid? SelectedCharacterId { get => State.SelectedCharacterId; set => State.SelectedCharacterId = value; }
private RollResult? LastRoll { get => State.LastRoll; set => State.LastRoll = value; }
private List<string> 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 IsMutating { get => State.IsMutating; set => State.IsMutating = value; }
private bool ShowEditCharacterModal { get; set; } private bool IsCampaignDataLoading { get => State.IsCampaignDataLoading; set => State.IsCampaignDataLoading = value; }
private bool CanEditCharacterOwner { get; set; } private bool IsAdminDataLoading { get => State.IsAdminDataLoading; set => State.IsAdminDataLoading = value; }
private Guid? EditingCharacterId { get; set; } private bool HasLoadedAdminUsers { get => State.HasLoadedAdminUsers; set => State.HasLoadedAdminUsers = value; }
private CharacterFormModel CreateCharacterInitialModel { get; set; } = new(); private bool HasHealthIssue { get => State.HasHealthIssue; set => State.HasHealthIssue = value; }
private CharacterFormModel EditCharacterInitialModel { get; set; } = new(); private string HealthIssueMessage { get => State.HealthIssueMessage; set => State.HealthIssueMessage = value; }
private int CreateCharacterFormVersion { get; set; } private List<WorkspaceToast> Toasts => State.Toasts;
private int EditCharacterFormVersion { get; set; } private string CurrentScreen { get => State.CurrentScreen; set => State.CurrentScreen = value; }
private bool StateRefreshInProgress { get; set; } private string MobilePanel { get => State.MobilePanel; set => State.MobilePanel = value; }
private bool HasInteractiveRenderStarted { get; set; } 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<Workspace>? DotNetRef { get; set; } private DotNetObjectReference<Workspace>? DotNetRef { get; set; }
private CampaignStateSnapshot? CurrentCampaignState { get; set; } private CampaignStateSnapshot? CurrentCampaignState { get => State.CurrentCampaignState; set => State.CurrentCampaignState = value; }
private Guid? CampaignLogCursor { get; set; } private Guid? CampaignLogCursor { get => State.CampaignLogCursor; set => State.CampaignLogCursor = value; }
private Guid? ExpandedCampaignLogRollId { get; set; } private Guid? ExpandedCampaignLogRollId { get => State.ExpandedCampaignLogRollId; set => State.ExpandedCampaignLogRollId = value; }
private Guid? FreshCampaignLogRollId { get; set; } private Guid? FreshCampaignLogRollId { get => State.FreshCampaignLogRollId; set => State.FreshCampaignLogRollId = value; }
private Dictionary<Guid, CampaignRollDetail> CampaignLogDetails { get; } = []; private Dictionary<Guid, CampaignRollDetail> CampaignLogDetails => State.CampaignLogDetails;
private HashSet<Guid> CampaignLogDetailsLoading { get; } = []; private HashSet<Guid> CampaignLogDetailsLoading => State.CampaignLogDetailsLoading;
private Dictionary<Guid, string> CampaignLogDetailErrors { get; } = []; private Dictionary<Guid, string> CampaignLogDetailErrors => State.CampaignLogDetailErrors;
[Parameter] [Parameter]
public EventCallback<string?> LoggedOut { get; set; } public EventCallback<string?> LoggedOut { get; set; }
private string? SelectedCampaignName => SelectedCampaign?.Name ?? Campaigns.FirstOrDefault(campaign => campaign.Id == SelectedCampaignId)?.Name; private string? SelectedCampaignName => State.SelectedCampaignName;
private CharacterSummary? SelectedCharacter => State.SelectedCharacter;
private CharacterSummary? SelectedCharacter => private CampaignRoster? PlaySelectedCampaign => State.PlaySelectedCampaign;
SelectedCampaign?.Characters.FirstOrDefault(c => c.Id == SelectedCharacterId); private CharacterSummary? PlaySelectedCharacter => State.PlaySelectedCharacter;
private Guid? PlaySelectedCharacterId => State.PlaySelectedCharacterId;
private CampaignRoster? PlaySelectedCampaign private List<CharacterSheetSkill> PlaySelectedCharacterSkills => State.PlaySelectedCharacterSkills;
{ private List<CharacterSheetSkillGroup> PlaySelectedCharacterSkillGroups => State.PlaySelectedCharacterSkillGroups;
get private List<CampaignLogListEntry> PlayVisibleCampaignLog => State.PlayVisibleCampaignLog;
{ private bool IsCurrentUserGm => State.IsCurrentUserGm;
if (SelectedCampaign is null) private bool IsCurrentUserAdmin => State.IsCurrentUserAdmin;
return null; private bool CanDeleteSelectedCampaign => State.CanDeleteSelectedCampaign;
private bool IsSelectedCampaignD6 => State.IsSelectedCampaignD6;
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<CharacterSheetSkill> PlaySelectedCharacterSkills =>
PlaySelectedCampaign is null || !PlaySelectedCharacterId.HasValue ? [] : SelectedCharacterSkills;
private List<CharacterSheetSkillGroup> PlaySelectedCharacterSkillGroups =>
PlaySelectedCampaign is null || !PlaySelectedCharacterId.HasValue ? [] : SelectedCharacterSkillGroups;
private List<CampaignLogListEntry> 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 static bool HasAdminRole(AdminUserSummary user) private static bool HasAdminRole(AdminUserSummary user)
{ {
return user.Roles.Contains(UserRoles.Admin, StringComparer.OrdinalIgnoreCase); return user.Roles.Contains(UserRoles.Admin, StringComparer.OrdinalIgnoreCase);
} }
private bool IsPlayScreen => string.Equals(CurrentScreen, ScreenPlay, StringComparison.OrdinalIgnoreCase); private bool IsPlayScreen => State.IsPlayScreen;
private bool IsManagementScreen => string.Equals(CurrentScreen, ScreenManagement, StringComparison.OrdinalIgnoreCase); private bool IsManagementScreen => State.IsManagementScreen;
private bool IsAdminScreen => string.Equals(CurrentScreen, ScreenAdmin, StringComparison.OrdinalIgnoreCase); private bool IsAdminScreen => State.IsAdminScreen;
private IReadOnlyList<AppHeaderMenuItem> HeaderMenuItems private IReadOnlyList<AppHeaderMenuItem> HeaderMenuItems
{ {
get get
@@ -1298,21 +1237,9 @@ public partial class Workspace : IAsyncDisposable
} }
} }
private string ConnectionStateLabel => ConnectionState switch private string ConnectionStateLabel => State.ConnectionStateLabel;
{ private string ConnectionStateCssClass => State.ConnectionStateCssClass;
"connected" => "Connected", private string AppCssClass => State.AppCssClass;
"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 AdminDatabaseDownloadUrl => Navigation.ToAbsoluteUri("api/admin/database").ToString(); private string AdminDatabaseDownloadUrl => Navigation.ToAbsoluteUri("api/admin/database").ToString();
private const string ScreenPlay = "play"; private const string ScreenPlay = "play";
@@ -1324,6 +1251,4 @@ public partial class Workspace : IAsyncDisposable
private const string RollVisibilitySessionKey = "roll-visibility"; private const string RollVisibilitySessionKey = "roll-visibility";
private const int CampaignLogWindowSize = 25; private const int CampaignLogWindowSize = 25;
private const int ToastDurationMs = 3200; private const int ToastDurationMs = 3200;
private sealed record WorkspaceToast(Guid Id, string Message, bool IsError);
} }

View File

@@ -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<CampaignSummary> Campaigns { get; set; } = [];
public List<CampaignOption> CharacterCampaignOptions { get; set; } = [];
public List<CharacterSheetSkill> SelectedCharacterSkills { get; set; } = [];
public List<CharacterSheetSkillGroup> SelectedCharacterSkillGroups { get; set; } = [];
public List<CampaignLogListEntry> CampaignLog { get; set; } = [];
public List<RulesetDefinition> Rulesets { get; set; } = [];
public List<AdminUserSummary> AdminUsers { get; set; } = [];
public Guid? SelectedCharacterId { get; set; }
public RollResult? LastRoll { get; set; }
public List<string> 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<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;
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<Guid, CampaignRollDetail> CampaignLogDetails { get; } = [];
public HashSet<Guid> CampaignLogDetailsLoading { get; } = [];
public Dictionary<Guid, string> 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<CharacterSheetSkill> PlaySelectedCharacterSkills =>
PlaySelectedCampaign is null || !PlaySelectedCharacterId.HasValue ? [] : SelectedCharacterSkills;
public List<CharacterSheetSkillGroup> PlaySelectedCharacterSkillGroups =>
PlaySelectedCampaign is null || !PlaySelectedCharacterId.HasValue ? [] : SelectedCharacterSkillGroups;
public List<CampaignLogListEntry> 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";
}

View File

@@ -0,0 +1,3 @@
namespace RpgRoller.Components.Pages;
public sealed record WorkspaceToast(Guid Id, string Message, bool IsError);