Delay workspace render until session init completes

This commit is contained in:
2026-05-04 18:12:10 +02:00
parent 231b0ac9a0
commit da813583bd
2 changed files with 249 additions and 226 deletions

View File

@@ -1,231 +1,244 @@
@using RpgRoller.Components.Pages.HomeControls @using RpgRoller.Components.Pages.HomeControls
<div class="@State.AppCssClass"> <div class="@State.AppCssClass">
<p class="sr-only" aria-live="polite">@State.LiveAnnouncement</p> @if (!IsInitialized)
@if (State.HasHealthIssue)
{ {
<section class="health-banner" role="alert"> <main class="loading-shell" aria-busy="true" aria-live="polite">
<div> <h1>RpgRoller</h1>
<strong>API currently unavailable.</strong> <p>Loading workspace...</p>
<p>@State.HealthIssueMessage</p> </main>
</div>
<button type="button" @onclick="Session.RetryAfterHealthIssueAsync">Retry</button>
</section>
} }
else
{
<p class="sr-only" aria-live="polite">@State.LiveAnnouncement</p>
<div class="workspace-shell"> @if (State.HasHealthIssue)
<AppHeader
User="State.User"
ShowCampaign="true"
CampaignName="@State.SelectedCampaignName"
ShowConnectionState="true"
ConnectionStateLabel="@State.ConnectionStateLabel"
ConnectionStateCssClass="@State.ConnectionStateCssClass"
IsMenuOpen="State.IsScreenMenuOpen"
MenuButtonId="workspace-screen-menu-button"
MenuId="workspace-screen-menu"
MenuItems="HeaderMenuItems"
ToggleMenuRequested="ToggleScreenMenu"
LogoutRequested="Session.LogoutAsync"/>
@if (State.IsPlayScreen)
{ {
<main class="play-screen @(State.MobilePanel == "log" ? "mobile-log" : "mobile-character")"> <section class="health-banner" role="alert">
<CharacterPanel <div>
IsCampaignDataLoading="State.IsCampaignDataLoading" <strong>API currently unavailable.</strong>
SelectedCampaign="State.PlaySelectedCampaign" <p>@State.HealthIssueMessage</p>
SelectedCharacterId="State.PlaySelectedCharacterId" </div>
SelectedCharacter="State.PlaySelectedCharacter" <button type="button" @onclick="Session.RetryAfterHealthIssueAsync">Retry</button>
</section>
}
<div class="workspace-shell">
<AppHeader
User="State.User"
ShowCampaign="true"
CampaignName="@State.SelectedCampaignName"
ShowConnectionState="true"
ConnectionStateLabel="@State.ConnectionStateLabel"
ConnectionStateCssClass="@State.ConnectionStateCssClass"
IsMenuOpen="State.IsScreenMenuOpen"
MenuButtonId="workspace-screen-menu-button"
MenuId="workspace-screen-menu"
MenuItems="HeaderMenuItems"
ToggleMenuRequested="ToggleScreenMenu"
LogoutRequested="Session.LogoutAsync"/>
@if (State.IsPlayScreen)
{
<main class="play-screen @(State.MobilePanel == "log" ? "mobile-log" : "mobile-character")">
<CharacterPanel
IsCampaignDataLoading="State.IsCampaignDataLoading"
SelectedCampaign="State.PlaySelectedCampaign"
SelectedCharacterId="State.PlaySelectedCharacterId"
SelectedCharacter="State.PlaySelectedCharacter"
IsMutating="State.IsMutating"
SelectedCharacterSkills="State.PlaySelectedCharacterSkills"
SelectedCharacterSkillGroups="State.PlaySelectedCharacterSkillGroups"
SelectedCampaignRulesetId="@(State.PlaySelectedCampaign?.RulesetId ?? string.Empty)"
RollVisibility="State.RollVisibility"
RollVisibilityChanged="Session.OnRollVisibilityChangedAsync"
OwnerLabel="State.OwnerLabel"
SkillDefinitionLabel="State.SkillDefinitionLabel"
CanEditCharacter="Campaigns.CanEditCharacter"
CanEditSkill="Play.CanEditSkill"
CharacterSelected="Play.SelectCharacterAsync"
EditCharacterRequested="Campaigns.OpenEditCharacterModal"
SkillCreated="Play.OnSkillCreatedAsync"
SkillUpdated="Play.OnSkillUpdatedAsync"
SkillGroupCreated="Play.OnSkillGroupCreatedAsync"
SkillGroupUpdated="Play.OnSkillGroupUpdatedAsync"
SkillDeleted="Play.OnSkillDeletedAsync"
SkillGroupDeleted="Play.OnSkillGroupDeletedAsync"
ErrorOccurred="Play.OnCharacterPanelErrorAsync"
RollRequested="Play.RollSkillAsync"/>
<CampaignLogPanel
IsCampaignDataLoading="State.IsCampaignDataLoading"
CampaignLog="State.PlayVisibleCampaignLog"
ExpandedRollId="State.ExpandedCampaignLogRollId"
FreshRollId="State.FreshCampaignLogRollId"
SelectedCharacterId="State.PlaySelectedCharacterId"
SelectedCharacterName="@(State.PlaySelectedCharacter?.Name)"
SelectedCampaignRulesetId="@(State.PlaySelectedCampaign?.RulesetId ?? string.Empty)"
RollVisibility="State.RollVisibility"
IsMutating="State.IsMutating"
ToggleRollDetailRequested="Play.ToggleRollDetailAsync"
ResolveRollDetail="Play.ResolveRollDetail"
IsRollDetailLoading="Play.IsRollDetailLoading"
GetRollDetailError="Play.GetRollDetailError"
CustomRollCreated="Play.OnCustomRollCreatedAsync"
ErrorOccurred="Play.OnCampaignLogPanelErrorAsync"/>
</main>
<nav class="mobile-bottom-nav" aria-label="Play panel selector">
<button type="button" class="switch @(State.MobilePanel == "character" ? "active" : string.Empty)"
@onclick='() => Scope.SetMobilePanelAsync("character")'>
Character
</button>
<button type="button" class="switch @(State.MobilePanel == "log" ? "active" : string.Empty)"
@onclick='() => Scope.SetMobilePanelAsync("log")'>
Log
</button>
</nav>
}
else if (State.IsManagementScreen)
{
<CampaignManagementPanel
Campaigns="State.Campaigns"
SelectedCampaignId="State.SelectedCampaignId"
SelectedCampaign="State.SelectedCampaign"
Rulesets="State.Rulesets"
IsMutating="State.IsMutating" IsMutating="State.IsMutating"
SelectedCharacterSkills="State.PlaySelectedCharacterSkills"
SelectedCharacterSkillGroups="State.PlaySelectedCharacterSkillGroups"
SelectedCampaignRulesetId="@(State.PlaySelectedCampaign?.RulesetId ?? string.Empty)"
RollVisibility="State.RollVisibility"
RollVisibilityChanged="Session.OnRollVisibilityChangedAsync"
OwnerLabel="State.OwnerLabel" OwnerLabel="State.OwnerLabel"
SkillDefinitionLabel="State.SkillDefinitionLabel"
CanEditCharacter="Campaigns.CanEditCharacter" CanEditCharacter="Campaigns.CanEditCharacter"
CanEditSkill="Play.CanEditSkill" CanDeleteCharacter="Campaigns.CanDeleteCharacter"
CharacterSelected="Play.SelectCharacterAsync" CanDeleteCampaign="State.CanDeleteSelectedCampaign"
CampaignSelectionChanged="Campaigns.OnCampaignSelectionChangedAsync"
CampaignCreated="Campaigns.OnCampaignCreatedAsync"
DeleteCampaignRequested="Campaigns.DeleteSelectedCampaignAsync"
CreateCharacterRequested="Campaigns.OpenCreateCharacterModal"
EditCharacterRequested="Campaigns.OpenEditCharacterModal" EditCharacterRequested="Campaigns.OpenEditCharacterModal"
SkillCreated="Play.OnSkillCreatedAsync" DeleteCharacterRequested="Campaigns.DeleteCharacterAsync"/>
SkillUpdated="Play.OnSkillUpdatedAsync" }
SkillGroupCreated="Play.OnSkillGroupCreatedAsync" else if (State.IsAdminScreen)
SkillGroupUpdated="Play.OnSkillGroupUpdatedAsync" {
SkillDeleted="Play.OnSkillDeletedAsync" <main class="management-screen">
SkillGroupDeleted="Play.OnSkillGroupDeletedAsync" @if (State.IsCurrentUserAdmin)
ErrorOccurred="Play.OnCharacterPanelErrorAsync" {
RollRequested="Play.RollSkillAsync"/> <section class="card">
<div class="section-head">
<CampaignLogPanel <h2>Database</h2>
IsCampaignDataLoading="State.IsCampaignDataLoading" </div>
CampaignLog="State.PlayVisibleCampaignLog" <p class="muted">Download the current SQLite file for backup or offline inspection.</p>
ExpandedRollId="State.ExpandedCampaignLogRollId" <div class="management-actions">
FreshRollId="State.FreshCampaignLogRollId" <a class="action-link" href="@AdminDatabaseDownloadUrl" download>Download SQLite database</a>
SelectedCharacterId="State.PlaySelectedCharacterId" </div>
SelectedCharacterName="@(State.PlaySelectedCharacter?.Name)" </section>
SelectedCampaignRulesetId="@(State.PlaySelectedCampaign?.RulesetId ?? string.Empty)" }
RollVisibility="State.RollVisibility"
IsMutating="State.IsMutating"
ToggleRollDetailRequested="Play.ToggleRollDetailAsync"
ResolveRollDetail="Play.ResolveRollDetail"
IsRollDetailLoading="Play.IsRollDetailLoading"
GetRollDetailError="Play.GetRollDetailError"
CustomRollCreated="Play.OnCustomRollCreatedAsync"
ErrorOccurred="Play.OnCampaignLogPanelErrorAsync"/>
</main>
<nav class="mobile-bottom-nav" aria-label="Play panel selector">
<button type="button" class="switch @(State.MobilePanel == "character" ? "active" : string.Empty)"
@onclick='() => Scope.SetMobilePanelAsync("character")'>
Character
</button>
<button type="button" class="switch @(State.MobilePanel == "log" ? "active" : string.Empty)"
@onclick='() => Scope.SetMobilePanelAsync("log")'>
Log
</button>
</nav>
}
else if (State.IsManagementScreen)
{
<CampaignManagementPanel
Campaigns="State.Campaigns"
SelectedCampaignId="State.SelectedCampaignId"
SelectedCampaign="State.SelectedCampaign"
Rulesets="State.Rulesets"
IsMutating="State.IsMutating"
OwnerLabel="State.OwnerLabel"
CanEditCharacter="Campaigns.CanEditCharacter"
CanDeleteCharacter="Campaigns.CanDeleteCharacter"
CanDeleteCampaign="State.CanDeleteSelectedCampaign"
CampaignSelectionChanged="Campaigns.OnCampaignSelectionChangedAsync"
CampaignCreated="Campaigns.OnCampaignCreatedAsync"
DeleteCampaignRequested="Campaigns.DeleteSelectedCampaignAsync"
CreateCharacterRequested="Campaigns.OpenCreateCharacterModal"
EditCharacterRequested="Campaigns.OpenEditCharacterModal"
DeleteCharacterRequested="Campaigns.DeleteCharacterAsync"/>
}
else if (State.IsAdminScreen)
{
<main class="management-screen">
@if (State.IsCurrentUserAdmin)
{
<section class="card"> <section class="card">
<div class="section-head"> <div class="section-head">
<h2>Database</h2> <h2>User Management</h2>
</div>
<p class="muted">Download the current SQLite file for backup or offline inspection.</p>
<div class="management-actions">
<a class="action-link" href="@AdminDatabaseDownloadUrl" download>Download SQLite database</a>
</div> </div>
@if (State.IsAdminDataLoading)
{
<p class="empty">Loading users...</p>
}
else if (!State.IsCurrentUserAdmin)
{
<p class="empty">Admin role is required to manage users.</p>
}
else if (State.AdminUsers.Count == 0)
{
<p class="empty">No users found.</p>
}
else
{
<ul class="management-list">
@foreach (var user in State.AdminUsers)
{
<li>
<div>
<strong>@user.Username</strong>
<p class="muted">@user.DisplayName</p>
<p class="muted">Roles: @(user.Roles.Count == 0 ? "none" : string.Join(", ", user.Roles))</p>
</div>
<div class="skill-chip-actions">
<button type="button"
class="chip-button"
disabled="@(State.IsMutating || user.Id == State.User?.Id)"
@onclick="() => Admin.ToggleAdminRoleAsync(user)">
<span aria-hidden="true" class="emoji">🛡️</span>
<span class="sr-only">Toggle admin role for @user.Username</span>
</button>
<button type="button"
class="chip-button"
disabled="@(State.IsMutating || user.Id == State.User?.Id)"
@onclick="() => Admin.DeleteUserAsync(user)">
<span aria-hidden="true" class="emoji">🗑️</span>
<span class="sr-only">Delete user @user.Username</span>
</button>
</div>
</li>
}
</ul>
}
</section> </section>
} </main>
<section class="card">
<div class="section-head">
<h2>User Management</h2>
</div>
@if (State.IsAdminDataLoading)
{
<p class="empty">Loading users...</p>
}
else if (!State.IsCurrentUserAdmin)
{
<p class="empty">Admin role is required to manage users.</p>
}
else if (State.AdminUsers.Count == 0)
{
<p class="empty">No users found.</p>
}
else
{
<ul class="management-list">
@foreach (var user in State.AdminUsers)
{
<li>
<div>
<strong>@user.Username</strong>
<p class="muted">@user.DisplayName</p>
<p class="muted">Roles: @(user.Roles.Count == 0 ? "none" : string.Join(", ", user.Roles))</p>
</div>
<div class="skill-chip-actions">
<button type="button"
class="chip-button"
disabled="@(State.IsMutating || user.Id == State.User?.Id)"
@onclick="() => Admin.ToggleAdminRoleAsync(user)">
<span aria-hidden="true" class="emoji">🛡️</span>
<span class="sr-only">Toggle admin role for @user.Username</span>
</button>
<button type="button"
class="chip-button"
disabled="@(State.IsMutating || user.Id == State.User?.Id)"
@onclick="() => Admin.DeleteUserAsync(user)">
<span aria-hidden="true" class="emoji">🗑️</span>
<span class="sr-only">Delete user @user.Username</span>
</button>
</div>
</li>
}
</ul>
}
</section>
</main>
}
</div>
@if (State.Toasts.Count > 0)
{
<div class="toast-stack" aria-live="polite" aria-atomic="false">
@foreach (var toast in State.Toasts)
{
<div class="toast @(toast.IsError ? "error" : "success")" role="status">
<p>@toast.Message</p>
</div>
} }
</div> </div>
@if (State.Toasts.Count > 0)
{
<div class="toast-stack" aria-live="polite" aria-atomic="false">
@foreach (var toast in State.Toasts)
{
<div class="toast @(toast.IsError ? "error" : "success")" role="status">
<p>@toast.Message</p>
</div>
}
</div>
}
} }
</div> </div>
<CharacterFormModal @if (IsInitialized)
Visible="State.ShowCreateCharacterModal" {
Title="Create Character" <CharacterFormModal
SubmitLabel="Create Character" Visible="State.ShowCreateCharacterModal"
NameInputId="character-create-name" Title="Create Character"
CampaignInputId="character-create-campaign" SubmitLabel="Create Character"
OwnerUsernameInputId="character-create-owner" NameInputId="character-create-name"
InitialModel="State.CreateCharacterInitialModel" CampaignInputId="character-create-campaign"
FormVersion="State.CreateCharacterFormVersion" OwnerUsernameInputId="character-create-owner"
EditingCharacterId="null" InitialModel="State.CreateCharacterInitialModel"
CampaignOptions="State.CharacterCampaignOptions" FormVersion="State.CreateCharacterFormVersion"
IsMutating="State.IsMutating" EditingCharacterId="null"
AllowOwnerEdit="false" CampaignOptions="State.CharacterCampaignOptions"
AvailableUsernames="State.KnownUsernames" IsMutating="State.IsMutating"
CharacterSaved="Campaigns.OnCharacterCreatedAsync" AllowOwnerEdit="false"
CancelRequested="Campaigns.CloseCharacterModals"/> AvailableUsernames="State.KnownUsernames"
CharacterSaved="Campaigns.OnCharacterCreatedAsync"
CancelRequested="Campaigns.CloseCharacterModals"/>
<CharacterFormModal <CharacterFormModal
Visible="State.ShowEditCharacterModal" Visible="State.ShowEditCharacterModal"
Title="Edit Character" Title="Edit Character"
SubmitLabel="Save Character" SubmitLabel="Save Character"
NameInputId="character-edit-name" NameInputId="character-edit-name"
CampaignInputId="character-edit-campaign" CampaignInputId="character-edit-campaign"
OwnerUsernameInputId="character-edit-owner" OwnerUsernameInputId="character-edit-owner"
InitialModel="State.EditCharacterInitialModel" InitialModel="State.EditCharacterInitialModel"
FormVersion="State.EditCharacterFormVersion" FormVersion="State.EditCharacterFormVersion"
EditingCharacterId="State.EditingCharacterId" EditingCharacterId="State.EditingCharacterId"
CampaignOptions="State.CharacterCampaignOptions" CampaignOptions="State.CharacterCampaignOptions"
IsMutating="State.IsMutating" IsMutating="State.IsMutating"
AllowOwnerEdit="State.CanEditCharacterOwner" AllowOwnerEdit="State.CanEditCharacterOwner"
AvailableUsernames="State.KnownUsernames" AvailableUsernames="State.KnownUsernames"
CharacterSaved="Campaigns.OnCharacterUpdatedAsync" CharacterSaved="Campaigns.OnCharacterUpdatedAsync"
CancelRequested="Campaigns.CloseCharacterModals"/> CancelRequested="Campaigns.CloseCharacterModals"/>
<RolemasterSkillRollModal <RolemasterSkillRollModal
Visible="State.ShowRolemasterSkillRollModal" Visible="State.ShowRolemasterSkillRollModal"
SkillName="@(State.PendingRolemasterSkillRoll?.Name ?? string.Empty)" SkillName="@(State.PendingRolemasterSkillRoll?.Name ?? string.Empty)"
Expression="@(State.PendingRolemasterSkillRoll?.DiceRollDefinition ?? string.Empty)" Expression="@(State.PendingRolemasterSkillRoll?.DiceRollDefinition ?? string.Empty)"
ModifierText="@State.PendingRolemasterSituationalModifier" ModifierText="@State.PendingRolemasterSituationalModifier"
ModifierTextChanged="@(text => State.PendingRolemasterSituationalModifier = text)" ModifierTextChanged="@(text => State.PendingRolemasterSituationalModifier = text)"
ErrorMessage="@State.PendingRolemasterSkillRollError" ErrorMessage="@State.PendingRolemasterSkillRollError"
IsMutating="State.IsMutating" IsMutating="State.IsMutating"
IsSubmitting="State.IsSubmittingRolemasterSkillRoll" IsSubmitting="State.IsSubmittingRolemasterSkillRoll"
ConfirmRequested="Play.SubmitRolemasterSkillRollAsync" ConfirmRequested="Play.SubmitRolemasterSkillRollAsync"
CancelRequested="Play.CancelRolemasterSkillRollAsync"/> CancelRequested="Play.CancelRolemasterSkillRollAsync"/>
}

View File

@@ -16,6 +16,7 @@ public partial class Workspace : IAsyncDisposable
return; return;
await Session.InitializeAsync(); await Session.InitializeAsync();
IsInitialized = true;
await InvokeAsync(StateHasChanged); await InvokeAsync(StateHasChanged);
} }
@@ -87,36 +88,45 @@ public partial class Workspace : IAsyncDisposable
return exception.Message.Contains("statically rendered", StringComparison.OrdinalIgnoreCase); return exception.Message.Contains("statically rendered", StringComparison.OrdinalIgnoreCase);
} }
[Inject] [Inject] private IJSRuntime JS { get; set; } = null!;
private IJSRuntime JS { get; set; } = null!;
[Inject] [Inject] private RpgRollerApiClient ApiClient { get; set; } = null!;
private RpgRollerApiClient ApiClient { get; set; } = null!;
[Inject] [Inject] private WorkspaceQueryService WorkspaceQuery { get; set; } = null!;
private WorkspaceQueryService WorkspaceQuery { get; set; } = null!;
[Inject] [Inject] private NavigationManager Navigation { get; set; } = null!;
private NavigationManager Navigation { get; set; } = null!;
[Parameter] [Parameter] public EventCallback<string?> LoggedOut { get; set; }
public EventCallback<string?> LoggedOut { get; set; }
private bool IsInitialized { get; set; }
private WorkspaceState State { get; } = new(); private WorkspaceState State { get; } = new();
private WorkspaceCampaignScopeCoordinator Scope => m_Scope ??= new(State, Feedback, JS, WorkspaceQuery, Play.EnsureSelectedCharacterActiveAsync, Play.RefreshSelectedCharacterSheetAsync, Play.RefreshCampaignLogAsync, Play.ResetCampaignLogDetailState, Play.ResetCampaignStateTracking, ClearAuthenticatedState, StopStateEventsAsync, message => LoggedOut.InvokeAsync(message)); private WorkspaceCampaignScopeCoordinator Scope => m_Scope ??= new(State, Feedback, JS, WorkspaceQuery,
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, StopStateEventsCoreAsync, Scope.RefreshCampaignRosterAsync, Play.RefreshSelectedCharacterSheetAsync, Play.RefreshCampaignLogAsync, () => InvokeAsync(StateHasChanged)); private WorkspaceLiveStateController Live => m_Live ??= new(State, Feedback, StartStateEventsCoreAsync,
StopStateEventsCoreAsync, Scope.RefreshCampaignRosterAsync, Play.RefreshSelectedCharacterSheetAsync,
Play.RefreshCampaignLogAsync, () => InvokeAsync(StateHasChanged));
private WorkspacePlayCoordinator Play => m_Play ??= new(State, Feedback, ApiClient, WorkspaceQuery, CanEditCharacter, () => InvokeAsync(StateHasChanged)); private WorkspacePlayCoordinator Play => m_Play ??= new(State, Feedback, ApiClient, WorkspaceQuery,
CanEditCharacter, () => InvokeAsync(StateHasChanged));
private WorkspaceCampaignCoordinator Campaigns => m_Campaigns ??= new(State, Feedback, JS, ApiClient, Session.LoadKnownUsernamesAsync, Scope.ReloadCampaignsAsync, Scope.ReloadCharacterCampaignOptionsAsync, Scope.RefreshCampaignScopeAsync, Live.SyncStateEventsAsync); private WorkspaceCampaignCoordinator Campaigns => m_Campaigns ??= new(State, Feedback, JS, ApiClient,
Session.LoadKnownUsernamesAsync, Scope.ReloadCampaignsAsync, Scope.ReloadCharacterCampaignOptionsAsync,
Scope.RefreshCampaignScopeAsync, Live.SyncStateEventsAsync);
private WorkspaceAdminCoordinator Admin => m_Admin ??= new(State, Feedback, JS, ApiClient, WorkspaceQuery, ClearAuthenticatedState, StopStateEventsAsync, message => LoggedOut.InvokeAsync(message)); private WorkspaceAdminCoordinator Admin => m_Admin ??= new(State, Feedback, JS, ApiClient, WorkspaceQuery,
ClearAuthenticatedState, StopStateEventsAsync, message => LoggedOut.InvokeAsync(message));
private WorkspaceFeedbackService Feedback => m_Feedback ??= new(State, () => InvokeAsync(StateHasChanged)); private WorkspaceFeedbackService Feedback => m_Feedback ??= new(State, () => InvokeAsync(StateHasChanged));
private WorkspaceSessionCoordinator Session => m_Session ??= new(State, Feedback, JS, ApiClient, WorkspaceQuery, Scope.ReloadCampaignsAsync, Scope.ReloadCharacterCampaignOptionsAsync, Scope.RefreshCampaignScopeAsync, Live.SyncStateEventsAsync, Live.StopStateEventsAsync, EnsureAdminUsersLoadedAsync, Play.ResetCampaignLogDetailState, () => InvokeAsync(StateHasChanged), message => LoggedOut.InvokeAsync(message)); private WorkspaceSessionCoordinator Session => m_Session ??= new(State, Feedback, JS, ApiClient, WorkspaceQuery,
Scope.ReloadCampaignsAsync, Scope.ReloadCharacterCampaignOptionsAsync, Scope.RefreshCampaignScopeAsync,
Live.SyncStateEventsAsync, Live.StopStateEventsAsync, EnsureAdminUsersLoadedAsync,
Play.ResetCampaignLogDetailState, () => InvokeAsync(StateHasChanged),
message => LoggedOut.InvokeAsync(message));
private IReadOnlyList<AppHeaderMenuItem> HeaderMenuItems private IReadOnlyList<AppHeaderMenuItem> HeaderMenuItems
{ {