519 lines
18 KiB
Plaintext
519 lines
18 KiB
Plaintext
@page "/tables"
|
|
@rendermode InteractiveServer
|
|
@using System
|
|
@using System.Linq
|
|
@using RolemasterDb.App.Frontend.AppState
|
|
@inject NavigationManager NavigationManager
|
|
@inject LookupService LookupService
|
|
@inject RolemasterDb.App.Frontend.AppState.PinnedTablesState PinnedTablesState
|
|
@inject RolemasterDb.App.Frontend.AppState.RecentTablesState RecentTablesState
|
|
@inject RolemasterDb.App.Frontend.AppState.TableContextState TableContextState
|
|
|
|
<PageTitle>Critical Tables</PageTitle>
|
|
|
|
<section class="panel tables-page">
|
|
<TablesPageHeader/>
|
|
|
|
@if (referenceData is null)
|
|
{
|
|
<p class="muted">Loading table list...</p>
|
|
}
|
|
else if (!referenceData.CriticalTables.Any())
|
|
{
|
|
<p class="muted">No critical tables are available yet.</p>
|
|
}
|
|
else
|
|
{
|
|
<div class="tables-reference-layout">
|
|
<aside class="tables-reference-rail">
|
|
<TablesIndexRail
|
|
Tables="referenceData.CriticalTables"
|
|
SelectedTableSlug="selectedTableSlug"
|
|
PinnedTableSlugs="PinnedTablesState.Items.Select(item => item.Slug).ToArray()"
|
|
RecentTableSlugs="RecentTablesState.Items.Select(item => item.Slug).ToArray()"
|
|
IsPinned="PinnedTablesState.IsPinned"
|
|
OnSelectTable="SelectTableAsync"/>
|
|
</aside>
|
|
|
|
<div class="tables-reference-main">
|
|
@if (!hasResolvedStoredTableSelection)
|
|
{
|
|
<p class="muted">Restoring table context...</p>
|
|
}
|
|
else if (isDetailLoading)
|
|
{
|
|
<p class="muted">Loading the selected table...</p>
|
|
}
|
|
else if (!string.IsNullOrWhiteSpace(detailError))
|
|
{
|
|
<p class="error-text">@detailError</p>
|
|
}
|
|
else if (tableDetail is null)
|
|
{
|
|
<p class="muted">The selected table could not be loaded.</p>
|
|
}
|
|
else if (tableDetail is { } detail)
|
|
{
|
|
<div class="table-shell">
|
|
<TablesContextBar
|
|
Detail="detail"
|
|
IsPinned="PinnedTablesState.IsPinned(detail.Slug)"
|
|
OnTogglePin="TogglePinnedTableAsync"
|
|
IsLegendOpen="isLegendOpen"
|
|
OnToggleLegend="ToggleLegend"/>
|
|
|
|
<TablesCanvas
|
|
Detail="detail"
|
|
CurrentMode="referenceMode"
|
|
SelectedGroupKey="selectedGroupKey"
|
|
SelectedColumnKey="selectedColumnKey"
|
|
RollJumpValue="rollJumpValue"
|
|
DensityMode="densityMode"
|
|
SelectedCell="selectedCell"
|
|
OnSelectCell="SelectCell"/>
|
|
|
|
@if (isLegendOpen)
|
|
{
|
|
<TablesLegend LegendEntries="@(detail.Legend ?? Array.Empty<CriticalTableLegendEntry>())"/>
|
|
}
|
|
</div>
|
|
}
|
|
</div>
|
|
</div>
|
|
|
|
<TablesSelectionMenu
|
|
SelectedCellDetail="SelectedCellDetail"
|
|
OnEdit="OpenSelectedCellEditorAsync"
|
|
OnCurate="OpenSelectedCellCurationAsync"/>
|
|
}
|
|
</section>
|
|
|
|
@if (isEditorOpen)
|
|
{
|
|
<CriticalCellEditorDialog
|
|
@key="editorModel"
|
|
Model="editorModel"
|
|
ComparisonBaseline="editorComparisonBaselineModel"
|
|
IsLoading="isEditorLoading"
|
|
IsReparsing="isEditorReparsing"
|
|
IsSaving="isEditorSaving"
|
|
LoadErrorMessage="@editorLoadError"
|
|
ReparseErrorMessage="@editorReparseError"
|
|
SaveErrorMessage="@editorSaveError"
|
|
OnClose="CloseCellEditorAsync"
|
|
OnReparse="ReparseCellEditorAsync"
|
|
OnSave="SaveCellEditorAsync"/>
|
|
}
|
|
|
|
@code {
|
|
private const string ContextDestination = "tables";
|
|
private LookupReferenceData? referenceData;
|
|
private CriticalTableDetail? tableDetail;
|
|
private string selectedTableSlug = string.Empty;
|
|
private bool isDetailLoading;
|
|
private string? detailError;
|
|
private bool isEditorOpen;
|
|
private bool isEditorLoading;
|
|
private bool isEditorReparsing;
|
|
private bool isEditorSaving;
|
|
private string? editorLoadError;
|
|
private string? editorReparseError;
|
|
private string? editorSaveError;
|
|
private int? editingResultId;
|
|
private CriticalCellEditorModel? editorModel;
|
|
private CriticalCellEditorModel? editorComparisonBaselineModel;
|
|
private string referenceMode = TablesReferenceMode.Reference;
|
|
private string selectedGroupKey = string.Empty;
|
|
private string selectedColumnKey = string.Empty;
|
|
private string rollJumpValue = string.Empty;
|
|
private string densityMode = TablesDensityMode.Comfortable;
|
|
private TablesCellSelection? selectedCell;
|
|
private bool isLegendOpen;
|
|
private bool hasResolvedStoredTableSelection;
|
|
|
|
private CriticalTableReference? SelectedTableReference =>
|
|
referenceData?.CriticalTables.FirstOrDefault(item => string.Equals(item.Key, selectedTableSlug, StringComparison.OrdinalIgnoreCase));
|
|
|
|
private CriticalTableCellDetail? SelectedCellDetail =>
|
|
selectedCell is null ? null : tableDetail?.Cells.FirstOrDefault(cell => cell.ResultId == selectedCell.ResultId);
|
|
|
|
protected override async Task OnInitializedAsync()
|
|
{
|
|
referenceData = await LookupService.GetReferenceDataAsync();
|
|
}
|
|
|
|
private async Task SelectTableAsync(string tableSlug)
|
|
{
|
|
selectedTableSlug = tableSlug;
|
|
await LoadTableDetailAsync();
|
|
await PersistAndSyncTableContextAsync();
|
|
}
|
|
|
|
private async Task LoadTableDetailAsync(TableContextSnapshot? routeContext = null)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(selectedTableSlug))
|
|
{
|
|
tableDetail = null;
|
|
return;
|
|
}
|
|
|
|
isDetailLoading = true;
|
|
detailError = null;
|
|
tableDetail = null;
|
|
|
|
try
|
|
{
|
|
tableDetail = await LookupService.GetCriticalTableAsync(selectedTableSlug);
|
|
if (tableDetail is null)
|
|
{
|
|
detailError = "The selected table could not be loaded.";
|
|
NormalizeViewStateForCurrentDetail();
|
|
return;
|
|
}
|
|
|
|
await RecordRecentTableVisitAsync();
|
|
ApplyRouteContext(routeContext);
|
|
NormalizeViewStateForCurrentDetail();
|
|
}
|
|
catch (Exception exception)
|
|
{
|
|
detailError = exception.Message;
|
|
NormalizeViewStateForCurrentDetail();
|
|
}
|
|
finally
|
|
{
|
|
isDetailLoading = false;
|
|
}
|
|
}
|
|
|
|
protected override async Task OnAfterRenderAsync(bool firstRender)
|
|
{
|
|
if (firstRender)
|
|
{
|
|
await RecentTablesState.InitializeAsync();
|
|
await PinnedTablesState.InitializeAsync();
|
|
}
|
|
|
|
if (!hasResolvedStoredTableSelection && referenceData?.CriticalTables.Count > 0)
|
|
{
|
|
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))
|
|
{
|
|
selectedTableSlug = resolvedTableSlug;
|
|
await LoadTableDetailAsync(initialContext);
|
|
await PersistAndSyncTableContextAsync();
|
|
await InvokeAsync(StateHasChanged);
|
|
return;
|
|
}
|
|
|
|
ApplyRouteContext(initialContext);
|
|
NormalizeViewStateForCurrentDetail();
|
|
await PersistAndSyncTableContextAsync();
|
|
}
|
|
}
|
|
|
|
private async Task OpenCellEditorAsync(int resultId)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(selectedTableSlug))
|
|
{
|
|
return;
|
|
}
|
|
|
|
editorLoadError = null;
|
|
editorReparseError = null;
|
|
editorSaveError = null;
|
|
editorModel = null;
|
|
editorComparisonBaselineModel = null;
|
|
editingResultId = resultId;
|
|
isEditorReparsing = false;
|
|
isEditorSaving = false;
|
|
isEditorLoading = true;
|
|
isEditorOpen = true;
|
|
|
|
try
|
|
{
|
|
var response = await LookupService.GetCriticalCellEditorAsync(selectedTableSlug, resultId);
|
|
if (response is null)
|
|
{
|
|
editorLoadError = "The selected cell could not be loaded for editing.";
|
|
editorModel = null;
|
|
return;
|
|
}
|
|
|
|
editorModel = CriticalCellEditorModel.FromResponse(response);
|
|
}
|
|
catch (Exception exception)
|
|
{
|
|
editorLoadError = exception.Message;
|
|
editorModel = null;
|
|
}
|
|
finally
|
|
{
|
|
isEditorLoading = false;
|
|
}
|
|
}
|
|
|
|
private async Task CloseCellEditorAsync()
|
|
{
|
|
isEditorOpen = false;
|
|
isEditorLoading = false;
|
|
isEditorReparsing = false;
|
|
isEditorSaving = false;
|
|
editorLoadError = null;
|
|
editorReparseError = null;
|
|
editorSaveError = null;
|
|
editingResultId = null;
|
|
editorModel = null;
|
|
editorComparisonBaselineModel = null;
|
|
await InvokeAsync(StateHasChanged);
|
|
}
|
|
|
|
private async Task ReparseCellEditorAsync()
|
|
{
|
|
if (editorModel is null || string.IsNullOrWhiteSpace(selectedTableSlug) || editingResultId is null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
isEditorReparsing = true;
|
|
editorReparseError = null;
|
|
|
|
try
|
|
{
|
|
var comparisonBaseline = editorModel.Clone();
|
|
var response = await ReparseCriticalCellAsync(editorModel, editingResultId.Value);
|
|
if (response is null)
|
|
{
|
|
editorReparseError = "The selected cell could not be re-parsed.";
|
|
return;
|
|
}
|
|
|
|
editorComparisonBaselineModel = comparisonBaseline;
|
|
editorModel = CriticalCellEditorModel.FromResponse(response);
|
|
await InvokeAsync(StateHasChanged);
|
|
}
|
|
catch (Exception exception)
|
|
{
|
|
editorReparseError = exception.Message;
|
|
}
|
|
finally
|
|
{
|
|
isEditorReparsing = false;
|
|
}
|
|
}
|
|
|
|
private Task<CriticalCellEditorResponse?> ReparseCriticalCellAsync(CriticalCellEditorModel model, int resultId) =>
|
|
LookupService.ReparseCriticalCellAsync(selectedTableSlug, resultId, model.ToRequest());
|
|
|
|
private async Task SaveCellEditorAsync()
|
|
{
|
|
if (editorModel is null || string.IsNullOrWhiteSpace(selectedTableSlug) || editingResultId is null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
isEditorSaving = true;
|
|
editorSaveError = null;
|
|
|
|
try
|
|
{
|
|
var response = await LookupService.UpdateCriticalCellAsync(selectedTableSlug, editingResultId.Value, editorModel.ToRequest());
|
|
if (response is null)
|
|
{
|
|
editorSaveError = "The selected cell could not be saved.";
|
|
return;
|
|
}
|
|
|
|
await LoadTableDetailAsync();
|
|
await CloseCellEditorAsync();
|
|
}
|
|
catch (Exception exception)
|
|
{
|
|
editorSaveError = exception.Message;
|
|
}
|
|
finally
|
|
{
|
|
isEditorSaving = false;
|
|
}
|
|
}
|
|
|
|
private Task TogglePinnedTableAsync()
|
|
{
|
|
if (SelectedTableReference is not { } selectedTable)
|
|
{
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
return PinnedTablesState.ToggleAsync(selectedTable.Key, selectedTable.Label, selectedTable.Family, selectedTable.CurationPercentage);
|
|
}
|
|
|
|
private Task RecordRecentTableVisitAsync()
|
|
{
|
|
if (SelectedTableReference is not { } selectedTable)
|
|
{
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
return RecentTablesState.RecordVisitAsync(selectedTable.Key, selectedTable.Label, selectedTable.Family, 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, GroupKey: selectedCell?.GroupKey, ColumnKey: selectedCell?.ColumnKey, RollBand: selectedCell?.RollBand, ResultId: selectedCell?.ResultId, Mode: RolemasterDb.App.Frontend.AppState.TableContextMode.Reference);
|
|
|
|
private void SelectCell(TablesCellSelection selection)
|
|
{
|
|
if (tableDetail?.Cells.Any(cell => cell.ResultId == selection.ResultId) != true)
|
|
{
|
|
selectedCell = null;
|
|
return;
|
|
}
|
|
|
|
selectedCell = selection;
|
|
}
|
|
|
|
private Task OpenSelectedCellEditorAsync() =>
|
|
selectedCell is null ? Task.CompletedTask : OpenCellEditorAsync(selectedCell.ResultId);
|
|
|
|
private async Task OpenSelectedCellCurationAsync()
|
|
{
|
|
if (SelectedCellDetail is not { } cellDetail || string.IsNullOrWhiteSpace(selectedTableSlug))
|
|
{
|
|
return;
|
|
}
|
|
|
|
var snapshot = new TableContextSnapshot(TableSlug: selectedTableSlug, GroupKey: cellDetail.GroupKey, ColumnKey: cellDetail.ColumnKey, RollBand: cellDetail.RollBand, ResultId: cellDetail.ResultId, Mode: TableContextMode.Curation, QueueScope: CurationQueueScopes.SelectedTable);
|
|
|
|
await TableContextState.PersistAsync("curation", snapshot);
|
|
NavigationManager.NavigateTo(TableContextState.BuildUri("/curation", snapshot));
|
|
}
|
|
|
|
private Task ToggleLegend()
|
|
{
|
|
isLegendOpen = !isLegendOpen;
|
|
return Task.CompletedTask;
|
|
}
|
|
|
|
private void ApplyRouteContext(TableContextSnapshot? routeContext)
|
|
{
|
|
if (tableDetail is null)
|
|
{
|
|
selectedCell = null;
|
|
return;
|
|
}
|
|
|
|
var resolvedCell = TableContextCellResolver.FindCell(tableDetail, routeContext);
|
|
selectedCell = resolvedCell is null ? null : new TablesCellSelection(resolvedCell.ResultId, resolvedCell.RollBand, resolvedCell.ColumnKey, resolvedCell.GroupKey);
|
|
}
|
|
|
|
private void NormalizeViewStateForCurrentDetail()
|
|
{
|
|
referenceMode = NormalizeMode(referenceMode);
|
|
|
|
if (tableDetail is null)
|
|
{
|
|
selectedGroupKey = string.Empty;
|
|
selectedColumnKey = string.Empty;
|
|
rollJumpValue = string.Empty;
|
|
densityMode = NormalizeDensityMode(densityMode);
|
|
selectedCell = null;
|
|
return;
|
|
}
|
|
|
|
if (tableDetail.Groups.All(group => !string.Equals(group.Key, selectedGroupKey, StringComparison.OrdinalIgnoreCase)))
|
|
{
|
|
selectedGroupKey = string.Empty;
|
|
}
|
|
|
|
var matchingColumns = tableDetail.Columns.Where(column => string.IsNullOrWhiteSpace(selectedColumnKey) || string.Equals(column.Key, selectedColumnKey, StringComparison.OrdinalIgnoreCase)).ToList();
|
|
|
|
if (matchingColumns.Count == 0)
|
|
{
|
|
selectedColumnKey = string.Empty;
|
|
}
|
|
|
|
rollJumpValue = NormalizeRollInput(rollJumpValue);
|
|
densityMode = NormalizeDensityMode(densityMode);
|
|
|
|
if (selectedCell is not null && tableDetail.Cells.All(cell => cell.ResultId != selectedCell.ResultId))
|
|
{
|
|
selectedCell = null;
|
|
}
|
|
|
|
NormalizeSelectedCellForCurrentView();
|
|
}
|
|
|
|
private static string NormalizeMode(string? mode) =>
|
|
mode switch
|
|
{
|
|
TablesReferenceMode.NeedsCuration => TablesReferenceMode.NeedsCuration,
|
|
TablesReferenceMode.Curated => TablesReferenceMode.Curated,
|
|
_ => TablesReferenceMode.Reference
|
|
};
|
|
|
|
private static string NormalizeDensityMode(string? mode) =>
|
|
string.Equals(mode, TablesDensityMode.Dense, StringComparison.Ordinal) ? TablesDensityMode.Dense : TablesDensityMode.Comfortable;
|
|
|
|
private static string NormalizeRollInput(string? value)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(value))
|
|
{
|
|
return string.Empty;
|
|
}
|
|
|
|
var digitsOnly = new string(value.Where(char.IsDigit).ToArray());
|
|
return digitsOnly.Length == 0 ? string.Empty : digitsOnly;
|
|
}
|
|
|
|
private void NormalizeSelectedCellForCurrentView()
|
|
{
|
|
if (selectedCell is null || tableDetail is null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var cell = tableDetail.Cells.FirstOrDefault(item => item.ResultId == selectedCell.ResultId);
|
|
if (cell is null || !MatchesCurrentView(cell))
|
|
{
|
|
selectedCell = null;
|
|
}
|
|
}
|
|
|
|
private bool MatchesCurrentView(CriticalTableCellDetail cell)
|
|
{
|
|
if (!string.IsNullOrWhiteSpace(selectedGroupKey) && !string.Equals(cell.GroupKey, selectedGroupKey, StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (!string.IsNullOrWhiteSpace(selectedColumnKey) && !string.Equals(cell.ColumnKey, selectedColumnKey, StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
return referenceMode switch
|
|
{
|
|
TablesReferenceMode.NeedsCuration => !cell.IsCurated,
|
|
TablesReferenceMode.Curated => cell.IsCurated,
|
|
_ => true
|
|
};
|
|
}
|
|
|
|
} |