fix: stabilize route startup render
This commit is contained in:
@@ -17,7 +17,7 @@
|
|||||||
<div class="section-head">
|
<div class="section-head">
|
||||||
<h2>User Management</h2>
|
<h2>User Management</h2>
|
||||||
</div>
|
</div>
|
||||||
@if (Workspace.State.IsAdminDataLoading)
|
@if (IsAdminDataLoading)
|
||||||
{
|
{
|
||||||
<p class="empty">Loading users...</p>
|
<p class="empty">Loading users...</p>
|
||||||
}
|
}
|
||||||
@@ -65,4 +65,6 @@
|
|||||||
|
|
||||||
@code {
|
@code {
|
||||||
[Parameter, EditorRequired] public WorkspacePageContext Workspace { get; set; } = null!;
|
[Parameter, EditorRequired] public WorkspacePageContext Workspace { get; set; } = null!;
|
||||||
|
|
||||||
|
private bool IsAdminDataLoading => !Workspace.HasSessionInitialized || Workspace.State.IsAdminDataLoading;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
<main class="play-screen @(Workspace.State.MobilePanel == "log" ? "mobile-log" : "mobile-character")">
|
<main class="play-screen @(Workspace.State.MobilePanel == "log" ? "mobile-log" : "mobile-character")">
|
||||||
<CharacterPanel
|
<CharacterPanel
|
||||||
IsCampaignDataLoading="Workspace.State.IsCampaignDataLoading"
|
IsCampaignDataLoading="@IsCampaignDataLoading"
|
||||||
SelectedCampaign="Workspace.State.PlaySelectedCampaign"
|
SelectedCampaign="Workspace.State.PlaySelectedCampaign"
|
||||||
SelectedCharacterId="Workspace.State.PlaySelectedCharacterId"
|
SelectedCharacterId="Workspace.State.PlaySelectedCharacterId"
|
||||||
SelectedCharacter="Workspace.State.PlaySelectedCharacter"
|
SelectedCharacter="Workspace.State.PlaySelectedCharacter"
|
||||||
@@ -29,7 +29,7 @@
|
|||||||
RollRequested="Workspace.Play.RollSkillAsync"/>
|
RollRequested="Workspace.Play.RollSkillAsync"/>
|
||||||
|
|
||||||
<CampaignLogPanel
|
<CampaignLogPanel
|
||||||
IsCampaignDataLoading="Workspace.State.IsCampaignDataLoading"
|
IsCampaignDataLoading="@IsCampaignDataLoading"
|
||||||
CampaignLog="Workspace.State.PlayVisibleCampaignLog"
|
CampaignLog="Workspace.State.PlayVisibleCampaignLog"
|
||||||
ExpandedRollId="Workspace.State.ExpandedCampaignLogRollId"
|
ExpandedRollId="Workspace.State.ExpandedCampaignLogRollId"
|
||||||
FreshRollId="Workspace.State.FreshCampaignLogRollId"
|
FreshRollId="Workspace.State.FreshCampaignLogRollId"
|
||||||
@@ -72,4 +72,6 @@
|
|||||||
|
|
||||||
@code {
|
@code {
|
||||||
[Parameter, EditorRequired] public WorkspacePageContext Workspace { get; set; } = null!;
|
[Parameter, EditorRequired] public WorkspacePageContext Workspace { get; set; } = null!;
|
||||||
|
|
||||||
|
private bool IsCampaignDataLoading => !Workspace.HasSessionInitialized || Workspace.State.IsCampaignDataLoading;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,30 +14,19 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
<div class="workspace-shell">
|
<div class="workspace-shell">
|
||||||
@if (HasSessionInitialized)
|
<AppHeader
|
||||||
{
|
User="State.User"
|
||||||
<AppHeader
|
ShowCampaign="@ShowCampaignInHeader"
|
||||||
User="State.User"
|
CampaignName="@State.SelectedCampaignName"
|
||||||
ShowCampaign="@ShowCampaignInHeader"
|
ShowConnectionState="@ShowConnectionStateInHeader"
|
||||||
CampaignName="@State.SelectedCampaignName"
|
ConnectionStateLabel="@State.ConnectionStateLabel"
|
||||||
ShowConnectionState="@ShowConnectionStateInHeader"
|
ConnectionStateCssClass="@State.ConnectionStateCssClass"
|
||||||
ConnectionStateLabel="@State.ConnectionStateLabel"
|
IsMenuOpen="State.IsScreenMenuOpen"
|
||||||
ConnectionStateCssClass="@State.ConnectionStateCssClass"
|
MenuButtonId="workspace-screen-menu-button"
|
||||||
IsMenuOpen="State.IsScreenMenuOpen"
|
MenuId="workspace-screen-menu"
|
||||||
MenuButtonId="workspace-screen-menu-button"
|
MenuItems="HeaderMenuItems"
|
||||||
MenuId="workspace-screen-menu"
|
ToggleMenuRequested="ToggleScreenMenu"
|
||||||
MenuItems="HeaderMenuItems"
|
LogoutRequested="Session.LogoutAsync"/>
|
||||||
ToggleMenuRequested="ToggleScreenMenu"
|
|
||||||
LogoutRequested="Session.LogoutAsync"/>
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
<main class="management-screen">
|
|
||||||
<section class="card">
|
|
||||||
<p class="empty">Loading workspace...</p>
|
|
||||||
</section>
|
|
||||||
</main>
|
|
||||||
}
|
|
||||||
|
|
||||||
@if (ChildContent is not null)
|
@if (ChildContent is not null)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -12,6 +12,10 @@ public partial class Workspace : IAsyncDisposable
|
|||||||
protected override void OnParametersSet()
|
protected override void OnParametersSet()
|
||||||
{
|
{
|
||||||
State.IsScreenMenuOpen = false;
|
State.IsScreenMenuOpen = false;
|
||||||
|
if (PreviousRoute.HasValue && PreviousRoute.Value != Route && HasSessionInitialized)
|
||||||
|
_ = InvokeAsync(HandleRouteChangedAsync);
|
||||||
|
|
||||||
|
PreviousRoute = Route;
|
||||||
}
|
}
|
||||||
|
|
||||||
[JSInvokable]
|
[JSInvokable]
|
||||||
@@ -114,6 +118,24 @@ public partial class Workspace : IAsyncDisposable
|
|||||||
await RequestRefreshAsync();
|
await RequestRefreshAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task HandleRouteChangedAsync()
|
||||||
|
{
|
||||||
|
if (!HasSessionInitialized)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (IsAdminRoute)
|
||||||
|
{
|
||||||
|
await Live.SyncStateEventsAsync();
|
||||||
|
await EnsureAdminUsersLoadedAsync();
|
||||||
|
await RequestRefreshAsync();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await Scope.RefreshCampaignScopeAsync();
|
||||||
|
await Live.SyncStateEventsAsync();
|
||||||
|
await RequestRefreshAsync();
|
||||||
|
}
|
||||||
|
|
||||||
private static bool IsStaticRenderInteropException(InvalidOperationException exception)
|
private static bool IsStaticRenderInteropException(InvalidOperationException exception)
|
||||||
{
|
{
|
||||||
return exception.Message.Contains("statically rendered", StringComparison.OrdinalIgnoreCase);
|
return exception.Message.Contains("statically rendered", StringComparison.OrdinalIgnoreCase);
|
||||||
@@ -220,4 +242,5 @@ public partial class Workspace : IAsyncDisposable
|
|||||||
private WorkspaceCampaignScopeCoordinator? m_Scope;
|
private WorkspaceCampaignScopeCoordinator? m_Scope;
|
||||||
private WorkspaceSessionCoordinator? m_Session;
|
private WorkspaceSessionCoordinator? m_Session;
|
||||||
private Task? InitializationTask { get; set; }
|
private Task? InitializationTask { get; set; }
|
||||||
}
|
private WorkspaceRoute? PreviousRoute { get; set; }
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,9 +1,6 @@
|
|||||||
@using Microsoft.AspNetCore.Components
|
@using Microsoft.AspNetCore.Components
|
||||||
|
|
||||||
@if (Workspace.HasSessionInitialized)
|
@ChildContent(Workspace)
|
||||||
{
|
|
||||||
@ChildContent(Workspace)
|
|
||||||
}
|
|
||||||
|
|
||||||
@code {
|
@code {
|
||||||
protected override async Task OnAfterRenderAsync(bool firstRender)
|
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||||
|
|||||||
2
TASKS.md
2
TASKS.md
@@ -92,6 +92,8 @@ After Milestone 4, authenticated startup is now triggered by a route-owned wrapp
|
|||||||
|
|
||||||
Follow-up: the first pass at Milestone 4 removed shell-level `OnAfterRenderAsync`, but did not yet split `Session.InitializeAsync()` by route. The final follow-up fix made startup genuinely route-scoped by keeping `/admin` off play-only campaign scope and SSE startup, gating the full shell behind authenticated initialization, and adding direct `/admin` smoke coverage so this regression path stays visible.
|
Follow-up: the first pass at Milestone 4 removed shell-level `OnAfterRenderAsync`, but did not yet split `Session.InitializeAsync()` by route. The final follow-up fix made startup genuinely route-scoped by keeping `/admin` off play-only campaign scope and SSE startup, gating the full shell behind authenticated initialization, and adding direct `/admin` smoke coverage so this regression path stays visible.
|
||||||
|
|
||||||
|
Follow-up 2: gating the entire authenticated shell behind `HasSessionInitialized` produced another large first-batch subtree swap and broke the `/campaigns` to `/play` refresh path. The final stabilization renders a consistent route skeleton from batch 1, derives loading UI from `HasSessionInitialized` instead of mutating shared loading state in `WorkspaceRouteView`, and refreshes route-specific scope explicitly when the same `Workspace` instance changes from one authenticated route to another.
|
||||||
|
|
||||||
This section must be updated after each major milestone. When the implementation is complete, summarize which parts of the old workspace architecture were fully removed, which compatibility constraints remain, and whether the final startup path still depends on any multi-batch structural rendering.
|
This section must be updated after each major milestone. When the implementation is complete, summarize which parts of the old workspace architecture were fully removed, which compatibility constraints remain, and whether the final startup path still depends on any multi-batch structural rendering.
|
||||||
|
|
||||||
## Context and Orientation
|
## Context and Orientation
|
||||||
|
|||||||
Reference in New Issue
Block a user