Add shared table context state

This commit is contained in:
2026-03-21 14:04:34 +01:00
parent aa0639ef66
commit bf19374558
6 changed files with 175 additions and 108 deletions

View File

@@ -5,11 +5,10 @@
@using System.Diagnostics.CodeAnalysis
@using System.Linq
@inject NavigationManager NavigationManager
@inject IJSRuntime JSRuntime
@inject LookupService LookupService
@inject RolemasterDb.App.Frontend.AppState.PinnedTablesState PinnedTablesState
@inject RolemasterDb.App.Frontend.AppState.RecentTablesState RecentTablesState
@inject RolemasterDb.App.Frontend.AppState.TableContextUrlSerializer TableContextUrlSerializer
@inject RolemasterDb.App.Frontend.AppState.TableContextState TableContextState
<PageTitle>Critical Tables</PageTitle>
@@ -82,6 +81,10 @@
{
<p class="muted">No critical tables are available yet.</p>
}
else if (!hasResolvedStoredTableSelection)
{
<p class="muted">Restoring table context...</p>
}
else if (isDetailLoading)
{
<p class="muted">Loading the selected table...</p>
@@ -227,7 +230,7 @@
}
@code {
private const string SelectedTableStorageKey = "rolemaster.tables.selectedTable";
private const string ContextDestination = "tables";
private LookupReferenceData? referenceData;
private CriticalTableDetail? tableDetail;
private string selectedTableSlug = string.Empty;
@@ -285,9 +288,8 @@
{
selectedTableSlug = tableSlug;
isTableMenuOpen = false;
await PersistSelectedTableAsync(tableSlug);
await LoadTableDetailAsync();
SyncTableContextUrl();
await PersistAndSyncTableContextAsync();
}
private async Task LoadTableDetailAsync()
@@ -336,35 +338,26 @@
if (!hasResolvedStoredTableSelection && referenceData?.CriticalTables.Count > 0)
{
try
var initialContext = await TableContextState.RestoreAsync(
NavigationManager.Uri,
ContextDestination,
referenceData.CriticalTables,
RolemasterDb.App.Frontend.AppState.TableContextMode.Reference);
hasResolvedStoredTableSelection = true;
var resolvedTableSlug = initialContext.TableSlug ?? string.Empty;
if (string.IsNullOrWhiteSpace(selectedTableSlug) ||
!string.Equals(resolvedTableSlug, selectedTableSlug, StringComparison.OrdinalIgnoreCase))
{
var routeContext = TableContextUrlSerializer.Parse(NavigationManager.Uri);
var storedTableSlug = await JSRuntime.InvokeAsync<string?>("localStorage.getItem", SelectedTableStorageKey);
hasResolvedStoredTableSelection = true;
var resolvedTableSlug = ResolveSelectedTableSlug(routeContext.TableSlug ?? storedTableSlug);
if (string.IsNullOrWhiteSpace(selectedTableSlug) ||
!string.Equals(resolvedTableSlug, selectedTableSlug, StringComparison.OrdinalIgnoreCase))
{
selectedTableSlug = resolvedTableSlug;
await LoadTableDetailAsync();
await PersistSelectedTableAsync(selectedTableSlug);
SyncTableContextUrl();
await InvokeAsync(StateHasChanged);
return;
}
if (!string.Equals(storedTableSlug, selectedTableSlug, StringComparison.OrdinalIgnoreCase))
{
await PersistSelectedTableAsync(selectedTableSlug);
}
SyncTableContextUrl();
}
catch (InvalidOperationException)
{
// During prerender localStorage is unavailable. Retry after interactive render.
selectedTableSlug = resolvedTableSlug;
await LoadTableDetailAsync();
await PersistAndSyncTableContextAsync();
await InvokeAsync(StateHasChanged);
return;
}
await PersistAndSyncTableContextAsync();
}
}
@@ -781,46 +774,6 @@
private string GetSelectedTableLabel() =>
SelectedTableReference?.Label ?? "Select a table";
private string ResolveSelectedTableSlug(string? storedTableSlug)
{
if (referenceData is null || referenceData.CriticalTables.Count == 0)
{
return string.Empty;
}
if (!string.IsNullOrWhiteSpace(storedTableSlug) &&
referenceData.CriticalTables.Any(item => string.Equals(item.Key, storedTableSlug, StringComparison.OrdinalIgnoreCase)))
{
return storedTableSlug;
}
return referenceData.CriticalTables.First().Key;
}
private Task PersistSelectedTableAsync(string tableSlug) =>
JSRuntime.InvokeVoidAsync("localStorage.setItem", SelectedTableStorageKey, tableSlug).AsTask();
private void SyncTableContextUrl()
{
if (string.IsNullOrWhiteSpace(selectedTableSlug))
{
return;
}
var targetUri = TableContextUrlSerializer.BuildRelativeUri(
"/tables",
new RolemasterDb.App.Frontend.AppState.TableContextSnapshot(
TableSlug: selectedTableSlug,
Mode: RolemasterDb.App.Frontend.AppState.TableContextMode.Reference));
if (string.Equals(NavigationManager.ToBaseRelativePath(NavigationManager.Uri), targetUri.TrimStart('/'), StringComparison.Ordinal))
{
return;
}
NavigationManager.NavigateTo(targetUri, replace: true);
}
private Task TogglePinnedTableAsync()
{
if (SelectedTableReference is not { } selectedTable)
@@ -849,6 +802,25 @@
selectedTable.CurationPercentage);
}
private async Task PersistAndSyncTableContextAsync()
{
var snapshot = BuildCurrentTableContext();
await TableContextState.PersistAsync(ContextDestination, snapshot);
var targetUri = TableContextState.BuildUri("/tables", snapshot);
if (string.Equals(NavigationManager.ToBaseRelativePath(NavigationManager.Uri), targetUri.TrimStart('/'), StringComparison.Ordinal))
{
return;
}
NavigationManager.NavigateTo(targetUri, replace: true);
}
private RolemasterDb.App.Frontend.AppState.TableContextSnapshot BuildCurrentTableContext() =>
new(
TableSlug: selectedTableSlug,
Mode: RolemasterDb.App.Frontend.AppState.TableContextMode.Reference);
private string GetTableOptionCssClass(CriticalTableReference table)
{
var classes = new List<string>();

View File

@@ -3,7 +3,7 @@
@using System.Linq
@inject NavigationManager NavigationManager
@inject LookupService LookupService
@inject RolemasterDb.App.Frontend.AppState.TableContextUrlSerializer TableContextUrlSerializer
@inject RolemasterDb.App.Frontend.AppState.TableContextState TableContextState
<section class="panel diagnostics-page tooling-surface">
<header class="diagnostics-page-header">
@@ -21,6 +21,10 @@
{
<p class="muted">No critical tables are available yet.</p>
}
else if (!hasInitializedContext)
{
<p class="muted">Restoring diagnostic context...</p>
}
else
{
<div class="diagnostics-selector-grid">
@@ -160,15 +164,33 @@
private bool isDiagnosticsLoading;
private string? detailError;
private string? diagnosticsError;
private bool hasInitializedContext;
private bool isBusy => isDetailLoading || isDiagnosticsLoading;
private string ContextDestination => "tools.diagnostics";
protected override async Task OnInitializedAsync()
{
referenceData = await LookupService.GetReferenceDataAsync();
var routeContext = TableContextUrlSerializer.Parse(NavigationManager.Uri);
selectedTableSlug = ResolveSelectedTableSlug(routeContext.TableSlug);
await LoadTableDetailAsync(routeContext);
}
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (!firstRender || hasInitializedContext || referenceData?.CriticalTables.Count is not > 0)
{
return;
}
var initialContext = await TableContextState.RestoreAsync(
NavigationManager.Uri,
ContextDestination,
referenceData.CriticalTables,
RolemasterDb.App.Frontend.AppState.TableContextMode.Diagnostics);
selectedTableSlug = initialContext.TableSlug ?? string.Empty;
hasInitializedContext = true;
await LoadTableDetailAsync(initialContext);
await InvokeAsync(StateHasChanged);
}
private async Task HandleTableChanged(ChangeEventArgs args)
@@ -181,7 +203,7 @@
{
selectedRollBand = args.Value?.ToString() ?? string.Empty;
ResolveSelectedCell();
SyncRouteContext();
await PersistAndSyncRouteContextAsync();
await LoadSelectedCellDiagnosticsAsync();
}
@@ -189,7 +211,7 @@
{
selectedGroupKey = NormalizeOptionalText(args.Value?.ToString());
ResolveSelectedCell();
SyncRouteContext();
await PersistAndSyncRouteContextAsync();
await LoadSelectedCellDiagnosticsAsync();
}
@@ -197,7 +219,7 @@
{
selectedColumnKey = args.Value?.ToString() ?? string.Empty;
ResolveSelectedCell();
SyncRouteContext();
await PersistAndSyncRouteContextAsync();
await LoadSelectedCellDiagnosticsAsync();
}
@@ -238,7 +260,7 @@
ResolveSelectedCell();
}
SyncRouteContext();
await PersistAndSyncRouteContextAsync();
await LoadSelectedCellDiagnosticsAsync();
}
catch (Exception exception)
@@ -370,7 +392,7 @@
}
diagnosticsModel = CriticalCellEditorModel.FromResponse(response);
SyncRouteContext();
await PersistAndSyncRouteContextAsync();
}
catch (Exception exception)
{
@@ -385,39 +407,24 @@
private static string? NormalizeOptionalText(string? value) =>
string.IsNullOrWhiteSpace(value) ? null : value.Trim();
private string ResolveSelectedTableSlug(string? tableSlug)
{
if (referenceData is null || referenceData.CriticalTables.Count == 0)
{
return string.Empty;
}
if (!string.IsNullOrWhiteSpace(tableSlug) &&
referenceData.CriticalTables.Any(item => string.Equals(item.Key, tableSlug, StringComparison.OrdinalIgnoreCase)))
{
return tableSlug;
}
return referenceData.CriticalTables.First().Key;
}
private void SyncRouteContext()
private async Task PersistAndSyncRouteContextAsync()
{
if (string.IsNullOrWhiteSpace(selectedTableSlug))
{
return;
}
var targetUri = TableContextUrlSerializer.BuildRelativeUri(
"/tools/diagnostics",
new RolemasterDb.App.Frontend.AppState.TableContextSnapshot(
TableSlug: selectedTableSlug,
GroupKey: selectedGroupKey,
ColumnKey: selectedColumnKey,
RollBand: selectedRollBand,
ResultId: selectedCell?.ResultId,
Mode: RolemasterDb.App.Frontend.AppState.TableContextMode.Diagnostics));
var snapshot = new RolemasterDb.App.Frontend.AppState.TableContextSnapshot(
TableSlug: selectedTableSlug,
GroupKey: selectedGroupKey,
ColumnKey: selectedColumnKey,
RollBand: selectedRollBand,
ResultId: selectedCell?.ResultId,
Mode: RolemasterDb.App.Frontend.AppState.TableContextMode.Diagnostics);
await TableContextState.PersistAsync(ContextDestination, snapshot);
var targetUri = TableContextState.BuildUri("/tools/diagnostics", snapshot);
if (string.Equals(NavigationManager.ToBaseRelativePath(NavigationManager.Uri), targetUri.TrimStart('/'), StringComparison.Ordinal))
{
return;