Add shared table context state
This commit is contained in:
@@ -55,6 +55,7 @@ It is intentionally implementation-focused:
|
|||||||
| 2026-03-21 | P2.3 | Completed | Added a shared `RecentTablesState` service backed by browser storage, centralized the recents storage key, and started recording successful table visits from the `Tables` page so later omnibox and rail work has real shared data. |
|
| 2026-03-21 | P2.3 | Completed | Added a shared `RecentTablesState` service backed by browser storage, centralized the recents storage key, and started recording successful table visits from the `Tables` page so later omnibox and rail work has real shared data. |
|
||||||
| 2026-03-21 | P2.4 | Completed | Added a shared `PinnedTablesState` service with browser persistence, centralized the pin storage key, initialized pin state in the `Tables` page, and added a first live pin/unpin action plus pinned status chips so later omnibox and navigation work have real saved pins to consume. |
|
| 2026-03-21 | P2.4 | Completed | Added a shared `PinnedTablesState` service with browser persistence, centralized the pin storage key, initialized pin state in the `Tables` page, and added a first live pin/unpin action plus pinned status chips so later omnibox and navigation work have real saved pins to consume. |
|
||||||
| 2026-03-21 | P2.5 | Completed | Added shared table-context URL types and a serializer, registered the serializer centrally, and started round-tripping table context through `/tables` and `/tools/diagnostics` so direct links restore the selected table and diagnostic cell context instead of relying only on local storage defaults. |
|
| 2026-03-21 | P2.5 | Completed | Added shared table-context URL types and a serializer, registered the serializer centrally, and started round-tripping table context through `/tables` and `/tools/diagnostics` so direct links restore the selected table and diagnostic cell context instead of relying only on local storage defaults. |
|
||||||
|
| 2026-03-21 | P2.8 | Completed | Added a shared `TableContextState` service on top of browser storage and the URL serializer, moved the `Tables` page off page-local table selection persistence, and switched diagnostics to the same restore/persist/build-URI flow so table context logic now lives in shared frontend state instead of being reinvented per page. |
|
||||||
|
|
||||||
### Lessons Learned
|
### Lessons Learned
|
||||||
|
|
||||||
@@ -79,6 +80,7 @@ It is intentionally implementation-focused:
|
|||||||
- Shared browser-backed state becomes more useful once one real page writes to it immediately. Recording recents from `Tables` now keeps later omnibox work from being blocked on synthetic placeholder data.
|
- Shared browser-backed state becomes more useful once one real page writes to it immediately. Recording recents from `Tables` now keeps later omnibox work from being blocked on synthetic placeholder data.
|
||||||
- Shared pinned-state services also need one live writer early. A minimal pin/unpin affordance in the current `Tables` page is enough to validate persistence before the larger navigation surfaces consume pins.
|
- Shared pinned-state services also need one live writer early. A minimal pin/unpin affordance in the current `Tables` page is enough to validate persistence before the larger navigation surfaces consume pins.
|
||||||
- URL serializers only pay off when wired into real pages. Using the shared serializer in both `Tables` and diagnostics now gives the project one verified round-trip path before the larger table-context service lands.
|
- URL serializers only pay off when wired into real pages. Using the shared serializer in both `Tables` and diagnostics now gives the project one verified round-trip path before the larger table-context service lands.
|
||||||
|
- The serializer alone is not the reusable boundary. The maintainable seam is a shared context-state helper that owns restore, persist, normalization, and URI-building conventions while pages keep only workflow-specific selection logic.
|
||||||
|
|
||||||
## Target Outcomes
|
## Target Outcomes
|
||||||
|
|
||||||
@@ -373,7 +375,7 @@ Establish the shared shell, tokens, typography, and theme system that every dest
|
|||||||
| `P2.5` | Completed | Shared table-context URLs now parse and serialize through one helper and are consumed by `/tables` and `/tools/diagnostics`. |
|
| `P2.5` | Completed | Shared table-context URLs now parse and serialize through one helper and are consumed by `/tables` and `/tools/diagnostics`. |
|
||||||
| `P2.6` | Pending | The shell omnibox is still a placeholder trigger. |
|
| `P2.6` | Pending | The shell omnibox is still a placeholder trigger. |
|
||||||
| `P2.7` | Pending | Shared primitives for chips, tabs, drawers, and inspector sections are not extracted yet. |
|
| `P2.7` | Pending | Shared primitives for chips, tabs, drawers, and inspector sections are not extracted yet. |
|
||||||
| `P2.8` | Pending | Table-selection logic still lives inside individual pages. |
|
| `P2.8` | Completed | Table-context restore, persistence, normalization, and URI building now flow through one shared state service used by `Tables` and diagnostics. |
|
||||||
|
|
||||||
### Goal
|
### Goal
|
||||||
|
|
||||||
|
|||||||
@@ -5,11 +5,10 @@
|
|||||||
@using System.Diagnostics.CodeAnalysis
|
@using System.Diagnostics.CodeAnalysis
|
||||||
@using System.Linq
|
@using System.Linq
|
||||||
@inject NavigationManager NavigationManager
|
@inject NavigationManager NavigationManager
|
||||||
@inject IJSRuntime JSRuntime
|
|
||||||
@inject LookupService LookupService
|
@inject LookupService LookupService
|
||||||
@inject RolemasterDb.App.Frontend.AppState.PinnedTablesState PinnedTablesState
|
@inject RolemasterDb.App.Frontend.AppState.PinnedTablesState PinnedTablesState
|
||||||
@inject RolemasterDb.App.Frontend.AppState.RecentTablesState RecentTablesState
|
@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>
|
<PageTitle>Critical Tables</PageTitle>
|
||||||
|
|
||||||
@@ -82,6 +81,10 @@
|
|||||||
{
|
{
|
||||||
<p class="muted">No critical tables are available yet.</p>
|
<p class="muted">No critical tables are available yet.</p>
|
||||||
}
|
}
|
||||||
|
else if (!hasResolvedStoredTableSelection)
|
||||||
|
{
|
||||||
|
<p class="muted">Restoring table context...</p>
|
||||||
|
}
|
||||||
else if (isDetailLoading)
|
else if (isDetailLoading)
|
||||||
{
|
{
|
||||||
<p class="muted">Loading the selected table...</p>
|
<p class="muted">Loading the selected table...</p>
|
||||||
@@ -227,7 +230,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
@code {
|
@code {
|
||||||
private const string SelectedTableStorageKey = "rolemaster.tables.selectedTable";
|
private const string ContextDestination = "tables";
|
||||||
private LookupReferenceData? referenceData;
|
private LookupReferenceData? referenceData;
|
||||||
private CriticalTableDetail? tableDetail;
|
private CriticalTableDetail? tableDetail;
|
||||||
private string selectedTableSlug = string.Empty;
|
private string selectedTableSlug = string.Empty;
|
||||||
@@ -285,9 +288,8 @@
|
|||||||
{
|
{
|
||||||
selectedTableSlug = tableSlug;
|
selectedTableSlug = tableSlug;
|
||||||
isTableMenuOpen = false;
|
isTableMenuOpen = false;
|
||||||
await PersistSelectedTableAsync(tableSlug);
|
|
||||||
await LoadTableDetailAsync();
|
await LoadTableDetailAsync();
|
||||||
SyncTableContextUrl();
|
await PersistAndSyncTableContextAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task LoadTableDetailAsync()
|
private async Task LoadTableDetailAsync()
|
||||||
@@ -336,35 +338,26 @@
|
|||||||
|
|
||||||
if (!hasResolvedStoredTableSelection && referenceData?.CriticalTables.Count > 0)
|
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);
|
selectedTableSlug = resolvedTableSlug;
|
||||||
var storedTableSlug = await JSRuntime.InvokeAsync<string?>("localStorage.getItem", SelectedTableStorageKey);
|
await LoadTableDetailAsync();
|
||||||
hasResolvedStoredTableSelection = true;
|
await PersistAndSyncTableContextAsync();
|
||||||
|
await InvokeAsync(StateHasChanged);
|
||||||
var resolvedTableSlug = ResolveSelectedTableSlug(routeContext.TableSlug ?? storedTableSlug);
|
return;
|
||||||
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.
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
await PersistAndSyncTableContextAsync();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -781,46 +774,6 @@
|
|||||||
private string GetSelectedTableLabel() =>
|
private string GetSelectedTableLabel() =>
|
||||||
SelectedTableReference?.Label ?? "Select a table";
|
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()
|
private Task TogglePinnedTableAsync()
|
||||||
{
|
{
|
||||||
if (SelectedTableReference is not { } selectedTable)
|
if (SelectedTableReference is not { } selectedTable)
|
||||||
@@ -849,6 +802,25 @@
|
|||||||
selectedTable.CurationPercentage);
|
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)
|
private string GetTableOptionCssClass(CriticalTableReference table)
|
||||||
{
|
{
|
||||||
var classes = new List<string>();
|
var classes = new List<string>();
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
@using System.Linq
|
@using System.Linq
|
||||||
@inject NavigationManager NavigationManager
|
@inject NavigationManager NavigationManager
|
||||||
@inject LookupService LookupService
|
@inject LookupService LookupService
|
||||||
@inject RolemasterDb.App.Frontend.AppState.TableContextUrlSerializer TableContextUrlSerializer
|
@inject RolemasterDb.App.Frontend.AppState.TableContextState TableContextState
|
||||||
|
|
||||||
<section class="panel diagnostics-page tooling-surface">
|
<section class="panel diagnostics-page tooling-surface">
|
||||||
<header class="diagnostics-page-header">
|
<header class="diagnostics-page-header">
|
||||||
@@ -21,6 +21,10 @@
|
|||||||
{
|
{
|
||||||
<p class="muted">No critical tables are available yet.</p>
|
<p class="muted">No critical tables are available yet.</p>
|
||||||
}
|
}
|
||||||
|
else if (!hasInitializedContext)
|
||||||
|
{
|
||||||
|
<p class="muted">Restoring diagnostic context...</p>
|
||||||
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
<div class="diagnostics-selector-grid">
|
<div class="diagnostics-selector-grid">
|
||||||
@@ -160,15 +164,33 @@
|
|||||||
private bool isDiagnosticsLoading;
|
private bool isDiagnosticsLoading;
|
||||||
private string? detailError;
|
private string? detailError;
|
||||||
private string? diagnosticsError;
|
private string? diagnosticsError;
|
||||||
|
private bool hasInitializedContext;
|
||||||
|
|
||||||
private bool isBusy => isDetailLoading || isDiagnosticsLoading;
|
private bool isBusy => isDetailLoading || isDiagnosticsLoading;
|
||||||
|
private string ContextDestination => "tools.diagnostics";
|
||||||
|
|
||||||
protected override async Task OnInitializedAsync()
|
protected override async Task OnInitializedAsync()
|
||||||
{
|
{
|
||||||
referenceData = await LookupService.GetReferenceDataAsync();
|
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)
|
private async Task HandleTableChanged(ChangeEventArgs args)
|
||||||
@@ -181,7 +203,7 @@
|
|||||||
{
|
{
|
||||||
selectedRollBand = args.Value?.ToString() ?? string.Empty;
|
selectedRollBand = args.Value?.ToString() ?? string.Empty;
|
||||||
ResolveSelectedCell();
|
ResolveSelectedCell();
|
||||||
SyncRouteContext();
|
await PersistAndSyncRouteContextAsync();
|
||||||
await LoadSelectedCellDiagnosticsAsync();
|
await LoadSelectedCellDiagnosticsAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -189,7 +211,7 @@
|
|||||||
{
|
{
|
||||||
selectedGroupKey = NormalizeOptionalText(args.Value?.ToString());
|
selectedGroupKey = NormalizeOptionalText(args.Value?.ToString());
|
||||||
ResolveSelectedCell();
|
ResolveSelectedCell();
|
||||||
SyncRouteContext();
|
await PersistAndSyncRouteContextAsync();
|
||||||
await LoadSelectedCellDiagnosticsAsync();
|
await LoadSelectedCellDiagnosticsAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -197,7 +219,7 @@
|
|||||||
{
|
{
|
||||||
selectedColumnKey = args.Value?.ToString() ?? string.Empty;
|
selectedColumnKey = args.Value?.ToString() ?? string.Empty;
|
||||||
ResolveSelectedCell();
|
ResolveSelectedCell();
|
||||||
SyncRouteContext();
|
await PersistAndSyncRouteContextAsync();
|
||||||
await LoadSelectedCellDiagnosticsAsync();
|
await LoadSelectedCellDiagnosticsAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -238,7 +260,7 @@
|
|||||||
ResolveSelectedCell();
|
ResolveSelectedCell();
|
||||||
}
|
}
|
||||||
|
|
||||||
SyncRouteContext();
|
await PersistAndSyncRouteContextAsync();
|
||||||
await LoadSelectedCellDiagnosticsAsync();
|
await LoadSelectedCellDiagnosticsAsync();
|
||||||
}
|
}
|
||||||
catch (Exception exception)
|
catch (Exception exception)
|
||||||
@@ -370,7 +392,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
diagnosticsModel = CriticalCellEditorModel.FromResponse(response);
|
diagnosticsModel = CriticalCellEditorModel.FromResponse(response);
|
||||||
SyncRouteContext();
|
await PersistAndSyncRouteContextAsync();
|
||||||
}
|
}
|
||||||
catch (Exception exception)
|
catch (Exception exception)
|
||||||
{
|
{
|
||||||
@@ -385,39 +407,24 @@
|
|||||||
private static string? NormalizeOptionalText(string? value) =>
|
private static string? NormalizeOptionalText(string? value) =>
|
||||||
string.IsNullOrWhiteSpace(value) ? null : value.Trim();
|
string.IsNullOrWhiteSpace(value) ? null : value.Trim();
|
||||||
|
|
||||||
private string ResolveSelectedTableSlug(string? tableSlug)
|
private async Task PersistAndSyncRouteContextAsync()
|
||||||
{
|
|
||||||
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()
|
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(selectedTableSlug))
|
if (string.IsNullOrWhiteSpace(selectedTableSlug))
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var targetUri = TableContextUrlSerializer.BuildRelativeUri(
|
var snapshot = new RolemasterDb.App.Frontend.AppState.TableContextSnapshot(
|
||||||
"/tools/diagnostics",
|
TableSlug: selectedTableSlug,
|
||||||
new RolemasterDb.App.Frontend.AppState.TableContextSnapshot(
|
GroupKey: selectedGroupKey,
|
||||||
TableSlug: selectedTableSlug,
|
ColumnKey: selectedColumnKey,
|
||||||
GroupKey: selectedGroupKey,
|
RollBand: selectedRollBand,
|
||||||
ColumnKey: selectedColumnKey,
|
ResultId: selectedCell?.ResultId,
|
||||||
RollBand: selectedRollBand,
|
Mode: RolemasterDb.App.Frontend.AppState.TableContextMode.Diagnostics);
|
||||||
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))
|
if (string.Equals(NavigationManager.ToBaseRelativePath(NavigationManager.Uri), targetUri.TrimStart('/'), StringComparison.Ordinal))
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -5,4 +5,7 @@ public static class BrowserStorageKeys
|
|||||||
public const string ThemeMode = "rolemaster.theme.mode";
|
public const string ThemeMode = "rolemaster.theme.mode";
|
||||||
public const string PinnedTables = "rolemaster.tables.pinned";
|
public const string PinnedTables = "rolemaster.tables.pinned";
|
||||||
public const string RecentTables = "rolemaster.tables.recent";
|
public const string RecentTables = "rolemaster.tables.recent";
|
||||||
|
|
||||||
|
public static string TableContext(string destination) =>
|
||||||
|
$"rolemaster.tables.context.{destination}";
|
||||||
}
|
}
|
||||||
|
|||||||
82
src/RolemasterDb.App/Frontend/AppState/TableContextState.cs
Normal file
82
src/RolemasterDb.App/Frontend/AppState/TableContextState.cs
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
using System.Text.Json;
|
||||||
|
using RolemasterDb.App.Features;
|
||||||
|
|
||||||
|
namespace RolemasterDb.App.Frontend.AppState;
|
||||||
|
|
||||||
|
public sealed class TableContextState(
|
||||||
|
BrowserStorageService browserStorage,
|
||||||
|
TableContextUrlSerializer serializer)
|
||||||
|
{
|
||||||
|
private static readonly JsonSerializerOptions SerializerOptions = new(JsonSerializerDefaults.Web);
|
||||||
|
|
||||||
|
public async Task<TableContextSnapshot> RestoreAsync(
|
||||||
|
string currentUri,
|
||||||
|
string destination,
|
||||||
|
IReadOnlyList<CriticalTableReference> availableTables,
|
||||||
|
TableContextMode defaultMode)
|
||||||
|
{
|
||||||
|
var routeContext = serializer.Parse(currentUri);
|
||||||
|
if (!string.IsNullOrWhiteSpace(routeContext.TableSlug))
|
||||||
|
{
|
||||||
|
return Normalize(routeContext, availableTables, defaultMode);
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var storedValue = await browserStorage.GetItemAsync(BrowserStorageKeys.TableContext(destination));
|
||||||
|
if (!string.IsNullOrWhiteSpace(storedValue))
|
||||||
|
{
|
||||||
|
var storedContext = JsonSerializer.Deserialize<TableContextSnapshot>(storedValue, SerializerOptions);
|
||||||
|
if (storedContext is not null)
|
||||||
|
{
|
||||||
|
return Normalize(storedContext, availableTables, defaultMode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (InvalidOperationException)
|
||||||
|
{
|
||||||
|
// JS interop is unavailable during prerender. Fall back to route/default context.
|
||||||
|
}
|
||||||
|
catch (JsonException)
|
||||||
|
{
|
||||||
|
// Ignore malformed storage payloads and fall back to route/default context.
|
||||||
|
}
|
||||||
|
|
||||||
|
return Normalize(routeContext, availableTables, defaultMode);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task PersistAsync(string destination, TableContextSnapshot context)
|
||||||
|
{
|
||||||
|
var serialized = JsonSerializer.Serialize(context, SerializerOptions);
|
||||||
|
return browserStorage.SetItemAsync(BrowserStorageKeys.TableContext(destination), serialized).AsTask();
|
||||||
|
}
|
||||||
|
|
||||||
|
public string BuildUri(string basePath, TableContextSnapshot context) =>
|
||||||
|
serializer.BuildRelativeUri(basePath, context);
|
||||||
|
|
||||||
|
public string ResolveTableSlug(IReadOnlyList<CriticalTableReference> availableTables, string? preferredSlug)
|
||||||
|
{
|
||||||
|
if (availableTables.Count == 0)
|
||||||
|
{
|
||||||
|
return string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(preferredSlug) &&
|
||||||
|
availableTables.Any(item => string.Equals(item.Key, preferredSlug, StringComparison.OrdinalIgnoreCase)))
|
||||||
|
{
|
||||||
|
return preferredSlug;
|
||||||
|
}
|
||||||
|
|
||||||
|
return availableTables[0].Key;
|
||||||
|
}
|
||||||
|
|
||||||
|
private TableContextSnapshot Normalize(
|
||||||
|
TableContextSnapshot context,
|
||||||
|
IReadOnlyList<CriticalTableReference> availableTables,
|
||||||
|
TableContextMode defaultMode) =>
|
||||||
|
context with
|
||||||
|
{
|
||||||
|
TableSlug = ResolveTableSlug(availableTables, context.TableSlug),
|
||||||
|
Mode = context.Mode ?? defaultMode
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -15,6 +15,7 @@ builder.Services.AddScoped<LookupService>();
|
|||||||
builder.Services.AddScoped<BrowserStorageService>();
|
builder.Services.AddScoped<BrowserStorageService>();
|
||||||
builder.Services.AddScoped<PinnedTablesState>();
|
builder.Services.AddScoped<PinnedTablesState>();
|
||||||
builder.Services.AddScoped<RecentTablesState>();
|
builder.Services.AddScoped<RecentTablesState>();
|
||||||
|
builder.Services.AddScoped<TableContextState>();
|
||||||
builder.Services.AddSingleton<TableContextUrlSerializer>();
|
builder.Services.AddSingleton<TableContextUrlSerializer>();
|
||||||
builder.Services.AddScoped<ThemeState>();
|
builder.Services.AddScoped<ThemeState>();
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user