Add canonical tools child routes
This commit is contained in:
@@ -50,6 +50,7 @@ It is intentionally implementation-focused:
|
|||||||
| 2026-03-21 | Post-P1 fix 2 | Completed | Replaced the most visible light-only surface and control colors with theme-aware tokens so switching between `Light`, `Dark`, and `System` produces a clear visual change. |
|
| 2026-03-21 | Post-P1 fix 2 | Completed | Replaced the most visible light-only surface and control colors with theme-aware tokens so switching between `Light`, `Dark`, and `System` produces a clear visual change. |
|
||||||
| 2026-03-21 | Post-P1 fix 3 | Completed | Restored layout-level shell interactivity by rendering routed content in `InteractiveServer` mode, which re-enabled shell event handlers such as the hamburger menu and theme selector. |
|
| 2026-03-21 | Post-P1 fix 3 | Completed | Restored layout-level shell interactivity by rendering routed content in `InteractiveServer` mode, which re-enabled shell event handlers such as the hamburger menu and theme selector. |
|
||||||
| 2026-03-21 | Post-P1 fix 4 | Completed | Added early theme bootstrapping in `App.razor` and `theme.js` so the stored mode is applied before hydration and remains visible after refresh. |
|
| 2026-03-21 | Post-P1 fix 4 | Completed | Added early theme bootstrapping in `App.razor` and `theme.js` so the stored mode is applied before hydration and remains visible after refresh. |
|
||||||
|
| 2026-03-21 | P2.1 | Completed | Added canonical `/tools/diagnostics` and `/tools/api` routes with dedicated tooling page components, extracted the diagnostics and API content into shared tool components, and updated the `Tools` landing page to link to the new route structure. |
|
||||||
|
|
||||||
### Lessons Learned
|
### Lessons Learned
|
||||||
|
|
||||||
@@ -69,6 +70,7 @@ It is intentionally implementation-focused:
|
|||||||
- Theme infrastructure is not enough on its own. Any surface that keeps hardcoded light values will make the theme switch feel broken even when the selector logic is correct.
|
- Theme infrastructure is not enough on its own. Any surface that keeps hardcoded light values will make the theme switch feel broken even when the selector logic is correct.
|
||||||
- In Blazor Web Apps, page-level render modes do not automatically make layout-level controls interactive in the way this shell expects. The routed shell itself needs an interactive render boundary.
|
- In Blazor Web Apps, page-level render modes do not automatically make layout-level controls interactive in the way this shell expects. The routed shell itself needs an interactive render boundary.
|
||||||
- Persisted theme state should be applied before Blazor hydrates, not only after layout initialization. Otherwise refresh can look broken even when storage writes succeed.
|
- Persisted theme state should be applied before Blazor hydrates, not only after layout initialization. Otherwise refresh can look broken even when storage writes succeed.
|
||||||
|
- For route migration in Blazor, extracting the destination UI into shared components keeps canonical routes and temporary compatibility routes from drifting while the redirect phase is still pending.
|
||||||
|
|
||||||
## Target Outcomes
|
## Target Outcomes
|
||||||
|
|
||||||
@@ -348,6 +350,23 @@ Establish the shared shell, tokens, typography, and theme system that every dest
|
|||||||
|
|
||||||
## Phase 2: Shared Navigation, Search, And State Infrastructure
|
## Phase 2: Shared Navigation, Search, And State Infrastructure
|
||||||
|
|
||||||
|
### Status
|
||||||
|
|
||||||
|
`In progress`
|
||||||
|
|
||||||
|
### Task Progress
|
||||||
|
|
||||||
|
| Task | Status | Notes |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `P2.1` | Completed | Canonical `Tools` child routes now exist, backed by dedicated route pages and shared tooling content components. |
|
||||||
|
| `P2.2` | Pending | Old `/diagnostics` and `/api` routes still need to become compatibility forwards. |
|
||||||
|
| `P2.3` | Pending | Recent tables state has not been introduced yet. |
|
||||||
|
| `P2.4` | Pending | Pinned tables state has not been introduced yet. |
|
||||||
|
| `P2.5` | Pending | Table-context URL parsing and serialization still needs a shared model. |
|
||||||
|
| `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.8` | Pending | Table-selection logic still lives inside individual pages. |
|
||||||
|
|
||||||
### Goal
|
### Goal
|
||||||
|
|
||||||
Build the shared interaction infrastructure needed by multiple destinations before page-specific UI work deepens.
|
Build the shared interaction infrastructure needed by multiple destinations before page-specific UI work deepens.
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
@page "/tools/api"
|
||||||
|
|
||||||
|
<PageTitle>API Surface</PageTitle>
|
||||||
|
|
||||||
|
<ApiPageContent />
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
@page "/tools/diagnostics"
|
||||||
|
@rendermode InteractiveServer
|
||||||
|
|
||||||
|
<PageTitle>Diagnostics</PageTitle>
|
||||||
|
|
||||||
|
<DiagnosticsPageContent />
|
||||||
@@ -4,10 +4,10 @@
|
|||||||
|
|
||||||
<section class="panel tooling-surface">
|
<section class="panel tooling-surface">
|
||||||
<h1 class="panel-title">Tools</h1>
|
<h1 class="panel-title">Tools</h1>
|
||||||
<p class="panel-copy">Diagnostics and API documentation move under this destination in later phases. The landing page is in place now so the shell can navigate with the target destination model.</p>
|
<p class="panel-copy">Diagnostics and API documentation now live under the `Tools` destination so engineering workflows stay reachable without polluting player-facing navigation.</p>
|
||||||
|
|
||||||
<div class="action-row">
|
<div class="action-row">
|
||||||
<NavLink class="btn-link" href="diagnostics">Open diagnostics</NavLink>
|
<NavLink class="btn-link" href="/tools/diagnostics">Open diagnostics</NavLink>
|
||||||
<NavLink class="btn-link" href="api">Open API docs</NavLink>
|
<NavLink class="btn-link" href="/tools/api">Open API docs</NavLink>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
143
src/RolemasterDb.App/Components/Tools/ApiPageContent.razor
Normal file
143
src/RolemasterDb.App/Components/Tools/ApiPageContent.razor
Normal file
@@ -0,0 +1,143 @@
|
|||||||
|
<div class="api-grid">
|
||||||
|
<section class="panel tooling-surface">
|
||||||
|
<h2 class="panel-title">Reference data</h2>
|
||||||
|
<p class="panel-copy"><code>GET /api/reference-data</code></p>
|
||||||
|
<pre class="code-block">{
|
||||||
|
"attackTables": [
|
||||||
|
{
|
||||||
|
"key": "broadsword",
|
||||||
|
"label": "Broadsword",
|
||||||
|
"attackKind": "melee",
|
||||||
|
"fumbleMinRoll": 1,
|
||||||
|
"fumbleMaxRoll": 2
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"criticalTables": [
|
||||||
|
{
|
||||||
|
"key": "mana",
|
||||||
|
"label": "Mana Critical Strike Table",
|
||||||
|
"family": "standard",
|
||||||
|
"sourceDocument": "Mana.pdf",
|
||||||
|
"notes": "Imported from PDF XML extraction."
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}</pre>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="panel tooling-surface">
|
||||||
|
<h2 class="panel-title">Attack lookup</h2>
|
||||||
|
<p class="panel-copy"><code>POST /api/lookup/attack</code></p>
|
||||||
|
<pre class="code-block">{
|
||||||
|
"attackTable": "broadsword",
|
||||||
|
"armorType": "AT10",
|
||||||
|
"roll": 111,
|
||||||
|
"criticalRoll": 72
|
||||||
|
}</pre>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="panel tooling-surface">
|
||||||
|
<h2 class="panel-title">Critical lookup</h2>
|
||||||
|
<p class="panel-copy"><code>POST /api/lookup/critical</code></p>
|
||||||
|
<pre class="code-block">{
|
||||||
|
"criticalType": "mana",
|
||||||
|
"column": "E",
|
||||||
|
"roll": 100,
|
||||||
|
"group": null
|
||||||
|
}</pre>
|
||||||
|
<p class="panel-copy">Response now includes table metadata, roll-band bounds, raw imported cell text, parse status, and parsed JSON alongside the gameplay description.</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="panel tooling-surface">
|
||||||
|
<h2 class="panel-title">Cell editor load</h2>
|
||||||
|
<p class="panel-copy"><code>GET /api/tables/critical/{slug}/cells/{resultId}</code></p>
|
||||||
|
<pre class="code-block">{
|
||||||
|
"resultId": 412,
|
||||||
|
"tableSlug": "slash",
|
||||||
|
"tableName": "Slash Critical Strike Table",
|
||||||
|
"rollBand": "66-70",
|
||||||
|
"groupKey": null,
|
||||||
|
"columnKey": "C",
|
||||||
|
"isCurated": false,
|
||||||
|
"sourcePageNumber": 1,
|
||||||
|
"sourceImageUrl": "/api/tables/critical/slash/cells/412/source-image",
|
||||||
|
"rawCellText": "Original imported full cell text",
|
||||||
|
"descriptionText": "Current curated prose",
|
||||||
|
"rawAffixText": "+8H - 2S",
|
||||||
|
"parseStatus": "verified",
|
||||||
|
"parsedJson": "{\"version\":1,\"isDescriptionOverridden\":false,\"isRawAffixTextOverridden\":false,\"areEffectsOverridden\":false,\"areBranchesOverridden\":false,\"effects\":[],\"branches\":[]}",
|
||||||
|
"isDescriptionOverridden": false,
|
||||||
|
"isRawAffixTextOverridden": false,
|
||||||
|
"areEffectsOverridden": false,
|
||||||
|
"areBranchesOverridden": false,
|
||||||
|
"validationMessages": [],
|
||||||
|
"effects": [],
|
||||||
|
"branches": []
|
||||||
|
}</pre>
|
||||||
|
<p class="panel-copy">Use this to retrieve the full editable result graph for one critical-table cell, including nested branches, normalized effects, and review notes for unresolved quick-parse tokens.</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="panel tooling-surface">
|
||||||
|
<h2 class="panel-title">Cell source image</h2>
|
||||||
|
<p class="panel-copy"><code>GET /api/tables/critical/{slug}/cells/{resultId}/source-image</code></p>
|
||||||
|
<p class="panel-copy">Streams the importer-generated PNG crop for the current critical cell. Returns <code>404</code> when the row has no stored crop or the artifact is missing.</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="panel tooling-surface">
|
||||||
|
<h2 class="panel-title">Cell re-parse</h2>
|
||||||
|
<p class="panel-copy"><code>POST /api/tables/critical/{slug}/cells/{resultId}/reparse</code></p>
|
||||||
|
<pre class="code-block">{
|
||||||
|
"currentState": {
|
||||||
|
"rawCellText": "Strike to thigh. +8H\nWith greaves: blow glances aside.",
|
||||||
|
"descriptionText": "Curated prose",
|
||||||
|
"rawAffixText": "+8H",
|
||||||
|
"parseStatus": "partial",
|
||||||
|
"parsedJson": "{}",
|
||||||
|
"isCurated": false,
|
||||||
|
"isDescriptionOverridden": true,
|
||||||
|
"isRawAffixTextOverridden": false,
|
||||||
|
"areEffectsOverridden": false,
|
||||||
|
"areBranchesOverridden": false,
|
||||||
|
"effects": [],
|
||||||
|
"branches": []
|
||||||
|
}
|
||||||
|
}</pre>
|
||||||
|
<p class="panel-copy">Re-runs the shared single-cell parser, merges the generated result with the current override state, and returns the refreshed editor payload without saving changes. Unknown or partially parsed tokens are surfaced explicitly in the returned review data.</p>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="panel tooling-surface">
|
||||||
|
<h2 class="panel-title">Cell editor save</h2>
|
||||||
|
<p class="panel-copy"><code>PUT /api/tables/critical/{slug}/cells/{resultId}</code></p>
|
||||||
|
<pre class="code-block">{
|
||||||
|
"rawCellText": "Corrected imported text",
|
||||||
|
"descriptionText": "Rewritten prose after manual review",
|
||||||
|
"rawAffixText": "+10H - must parry 2 rnds",
|
||||||
|
"parseStatus": "manually_curated",
|
||||||
|
"parsedJson": "{\"reviewed\":true}",
|
||||||
|
"isCurated": true,
|
||||||
|
"isDescriptionOverridden": true,
|
||||||
|
"isRawAffixTextOverridden": false,
|
||||||
|
"areEffectsOverridden": false,
|
||||||
|
"areBranchesOverridden": false,
|
||||||
|
"effects": [
|
||||||
|
{
|
||||||
|
"effectCode": "direct_hits",
|
||||||
|
"target": null,
|
||||||
|
"valueInteger": 10,
|
||||||
|
"valueDecimal": null,
|
||||||
|
"valueExpression": null,
|
||||||
|
"durationRounds": null,
|
||||||
|
"perRound": null,
|
||||||
|
"modifier": null,
|
||||||
|
"bodyPart": null,
|
||||||
|
"isPermanent": false,
|
||||||
|
"sourceType": "symbol",
|
||||||
|
"sourceText": "+10H",
|
||||||
|
"originKey": "base:direct_hits:1",
|
||||||
|
"isOverridden": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"branches": []
|
||||||
|
}</pre>
|
||||||
|
<p class="panel-copy">The save endpoint replaces the stored base result, branch rows, and effect rows for that cell with the submitted curated payload.</p>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
@@ -0,0 +1,328 @@
|
|||||||
|
@using System
|
||||||
|
@using System.Collections.Generic
|
||||||
|
@using System.Linq
|
||||||
|
@inject LookupService LookupService
|
||||||
|
|
||||||
|
<section class="panel diagnostics-page tooling-surface">
|
||||||
|
<header class="diagnostics-page-header">
|
||||||
|
<div>
|
||||||
|
<h2 class="panel-title">Critical Cell Diagnostics</h2>
|
||||||
|
<p class="panel-copy">Engineering-only parser metadata, provenance, and payload inspection live here instead of inside the normal curation modal.</p>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
@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="diagnostics-selector-grid">
|
||||||
|
<div class="field-shell">
|
||||||
|
<label for="diagnostics-table-select">Table</label>
|
||||||
|
<select
|
||||||
|
id="diagnostics-table-select"
|
||||||
|
class="input-shell"
|
||||||
|
value="@selectedTableSlug"
|
||||||
|
@onchange="HandleTableChanged"
|
||||||
|
disabled="@isBusy">
|
||||||
|
@foreach (var table in referenceData.CriticalTables)
|
||||||
|
{
|
||||||
|
<option value="@table.Key">@table.Label</option>
|
||||||
|
}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="field-shell">
|
||||||
|
<label for="diagnostics-roll-band-select">Roll Band</label>
|
||||||
|
<select
|
||||||
|
id="diagnostics-roll-band-select"
|
||||||
|
class="input-shell"
|
||||||
|
value="@selectedRollBand"
|
||||||
|
@onchange="HandleRollBandChanged"
|
||||||
|
disabled="@(isBusy || tableDetail is null || !tableDetail.RollBands.Any())">
|
||||||
|
@if (tableDetail is not null)
|
||||||
|
{
|
||||||
|
@foreach (var rollBand in tableDetail.RollBands)
|
||||||
|
{
|
||||||
|
<option value="@rollBand.Label">@rollBand.Label</option>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if (tableDetail is { Groups.Count: > 0 })
|
||||||
|
{
|
||||||
|
<div class="field-shell">
|
||||||
|
<label for="diagnostics-group-select">Variant</label>
|
||||||
|
<select
|
||||||
|
id="diagnostics-group-select"
|
||||||
|
class="input-shell"
|
||||||
|
value="@selectedGroupKey"
|
||||||
|
@onchange="HandleGroupChanged"
|
||||||
|
disabled="@isBusy">
|
||||||
|
@foreach (var group in tableDetail.Groups)
|
||||||
|
{
|
||||||
|
<option value="@group.Key">@group.Label</option>
|
||||||
|
}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
<div class="field-shell">
|
||||||
|
<label for="diagnostics-column-select">Severity</label>
|
||||||
|
<select
|
||||||
|
id="diagnostics-column-select"
|
||||||
|
class="input-shell"
|
||||||
|
value="@selectedColumnKey"
|
||||||
|
@onchange="HandleColumnChanged"
|
||||||
|
disabled="@(isBusy || tableDetail is null || !tableDetail.Columns.Any())">
|
||||||
|
@if (tableDetail is not null)
|
||||||
|
{
|
||||||
|
@foreach (var column in tableDetail.Columns)
|
||||||
|
{
|
||||||
|
<option value="@column.Key">@column.Label</option>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@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.Cells.Any())
|
||||||
|
{
|
||||||
|
<p class="muted">The selected table has no filled cells to inspect.</p>
|
||||||
|
}
|
||||||
|
else if (selectedCell is null)
|
||||||
|
{
|
||||||
|
<div class="critical-editor-card nested">
|
||||||
|
<div class="critical-editor-card-header">
|
||||||
|
<div>
|
||||||
|
<strong>No Filled Cell At This Position</strong>
|
||||||
|
<p class="muted critical-editor-inline-copy">Pick another roll band, variant, or severity to inspect a stored result.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<div class="diagnostics-selection-summary">
|
||||||
|
<strong>Inspecting</strong>
|
||||||
|
<span>@tableDetail.DisplayName</span>
|
||||||
|
<span>· Roll band <strong>@selectedCell.RollBand</strong></span>
|
||||||
|
<span>· Severity <strong>@selectedCell.ColumnLabel</strong></span>
|
||||||
|
@if (!string.IsNullOrWhiteSpace(selectedCell.GroupLabel))
|
||||||
|
{
|
||||||
|
<span>· Variant <strong>@selectedCell.GroupLabel</strong></span>
|
||||||
|
}
|
||||||
|
<span>· Result ID <strong>@selectedCell.ResultId</strong></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if (isDiagnosticsLoading)
|
||||||
|
{
|
||||||
|
<p class="muted">Loading diagnostics...</p>
|
||||||
|
}
|
||||||
|
else if (!string.IsNullOrWhiteSpace(diagnosticsError))
|
||||||
|
{
|
||||||
|
<p class="error-text">@diagnosticsError</p>
|
||||||
|
}
|
||||||
|
else if (diagnosticsModel is not null)
|
||||||
|
{
|
||||||
|
<CriticalCellEngineeringDiagnostics Model="diagnosticsModel" />
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
@code {
|
||||||
|
private LookupReferenceData? referenceData;
|
||||||
|
private CriticalTableDetail? tableDetail;
|
||||||
|
private CriticalTableCellDetail? selectedCell;
|
||||||
|
private CriticalCellEditorModel? diagnosticsModel;
|
||||||
|
private string selectedTableSlug = string.Empty;
|
||||||
|
private string selectedRollBand = string.Empty;
|
||||||
|
private string selectedColumnKey = string.Empty;
|
||||||
|
private string? selectedGroupKey;
|
||||||
|
private bool isDetailLoading;
|
||||||
|
private bool isDiagnosticsLoading;
|
||||||
|
private string? detailError;
|
||||||
|
private string? diagnosticsError;
|
||||||
|
|
||||||
|
private bool isBusy => isDetailLoading || isDiagnosticsLoading;
|
||||||
|
|
||||||
|
protected override async Task OnInitializedAsync()
|
||||||
|
{
|
||||||
|
referenceData = await LookupService.GetReferenceDataAsync();
|
||||||
|
selectedTableSlug = referenceData.CriticalTables.FirstOrDefault()?.Key ?? string.Empty;
|
||||||
|
await LoadTableDetailAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task HandleTableChanged(ChangeEventArgs args)
|
||||||
|
{
|
||||||
|
selectedTableSlug = args.Value?.ToString() ?? string.Empty;
|
||||||
|
await LoadTableDetailAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task HandleRollBandChanged(ChangeEventArgs args)
|
||||||
|
{
|
||||||
|
selectedRollBand = args.Value?.ToString() ?? string.Empty;
|
||||||
|
ResolveSelectedCell();
|
||||||
|
await LoadSelectedCellDiagnosticsAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task HandleGroupChanged(ChangeEventArgs args)
|
||||||
|
{
|
||||||
|
selectedGroupKey = NormalizeOptionalText(args.Value?.ToString());
|
||||||
|
ResolveSelectedCell();
|
||||||
|
await LoadSelectedCellDiagnosticsAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task HandleColumnChanged(ChangeEventArgs args)
|
||||||
|
{
|
||||||
|
selectedColumnKey = args.Value?.ToString() ?? string.Empty;
|
||||||
|
ResolveSelectedCell();
|
||||||
|
await LoadSelectedCellDiagnosticsAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task LoadTableDetailAsync()
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(selectedTableSlug))
|
||||||
|
{
|
||||||
|
tableDetail = null;
|
||||||
|
selectedCell = null;
|
||||||
|
diagnosticsModel = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
isDetailLoading = true;
|
||||||
|
detailError = null;
|
||||||
|
diagnosticsError = null;
|
||||||
|
diagnosticsModel = null;
|
||||||
|
selectedCell = null;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
tableDetail = await LookupService.GetCriticalTableAsync(selectedTableSlug);
|
||||||
|
if (tableDetail is null)
|
||||||
|
{
|
||||||
|
detailError = "The selected table could not be loaded.";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
SetDefaultSelection(tableDetail);
|
||||||
|
ResolveSelectedCell();
|
||||||
|
await LoadSelectedCellDiagnosticsAsync();
|
||||||
|
}
|
||||||
|
catch (Exception exception)
|
||||||
|
{
|
||||||
|
detailError = exception.Message;
|
||||||
|
tableDetail = null;
|
||||||
|
selectedCell = null;
|
||||||
|
diagnosticsModel = null;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
isDetailLoading = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SetDefaultSelection(CriticalTableDetail detail)
|
||||||
|
{
|
||||||
|
if (!detail.Cells.Any())
|
||||||
|
{
|
||||||
|
selectedRollBand = detail.RollBands.FirstOrDefault()?.Label ?? string.Empty;
|
||||||
|
selectedColumnKey = detail.Columns.FirstOrDefault()?.Key ?? string.Empty;
|
||||||
|
selectedGroupKey = detail.Groups.FirstOrDefault()?.Key;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var rollOrder = detail.RollBands
|
||||||
|
.Select((rollBand, index) => new { rollBand.Label, index })
|
||||||
|
.ToDictionary(item => item.Label, item => item.index, StringComparer.Ordinal);
|
||||||
|
var columnOrder = detail.Columns
|
||||||
|
.Select((column, index) => new { column.Key, index })
|
||||||
|
.ToDictionary(item => item.Key, item => item.index, StringComparer.Ordinal);
|
||||||
|
var groupOrder = detail.Groups
|
||||||
|
.Select((group, index) => new { group.Key, index })
|
||||||
|
.ToDictionary(item => item.Key, item => item.index, StringComparer.Ordinal);
|
||||||
|
|
||||||
|
var firstCell = detail.Cells
|
||||||
|
.OrderBy(cell => rollOrder.GetValueOrDefault(cell.RollBand, int.MaxValue))
|
||||||
|
.ThenBy(cell =>
|
||||||
|
{
|
||||||
|
if (cell.GroupKey is null)
|
||||||
|
{
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return groupOrder.GetValueOrDefault(cell.GroupKey, int.MaxValue);
|
||||||
|
})
|
||||||
|
.ThenBy(cell => columnOrder.GetValueOrDefault(cell.ColumnKey, int.MaxValue))
|
||||||
|
.First();
|
||||||
|
|
||||||
|
selectedRollBand = firstCell.RollBand;
|
||||||
|
selectedColumnKey = firstCell.ColumnKey;
|
||||||
|
selectedGroupKey = firstCell.GroupKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ResolveSelectedCell()
|
||||||
|
{
|
||||||
|
if (tableDetail is null)
|
||||||
|
{
|
||||||
|
selectedCell = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
selectedCell = tableDetail.Cells.FirstOrDefault(cell =>
|
||||||
|
string.Equals(cell.RollBand, selectedRollBand, StringComparison.Ordinal) &&
|
||||||
|
string.Equals(cell.ColumnKey, selectedColumnKey, StringComparison.Ordinal) &&
|
||||||
|
string.Equals(cell.GroupKey ?? string.Empty, selectedGroupKey ?? string.Empty, StringComparison.Ordinal));
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task LoadSelectedCellDiagnosticsAsync()
|
||||||
|
{
|
||||||
|
diagnosticsError = null;
|
||||||
|
diagnosticsModel = null;
|
||||||
|
|
||||||
|
if (selectedCell is null || string.IsNullOrWhiteSpace(selectedTableSlug))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
isDiagnosticsLoading = true;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var response = await LookupService.GetCriticalCellEditorAsync(selectedTableSlug, selectedCell.ResultId);
|
||||||
|
if (response is null)
|
||||||
|
{
|
||||||
|
diagnosticsError = "The selected cell could not be loaded.";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
diagnosticsModel = CriticalCellEditorModel.FromResponse(response);
|
||||||
|
}
|
||||||
|
catch (Exception exception)
|
||||||
|
{
|
||||||
|
diagnosticsError = exception.Message;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
isDiagnosticsLoading = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string? NormalizeOptionalText(string? value) =>
|
||||||
|
string.IsNullOrWhiteSpace(value) ? null : value.Trim();
|
||||||
|
}
|
||||||
@@ -13,3 +13,4 @@
|
|||||||
@using RolemasterDb.App.Components.Layout
|
@using RolemasterDb.App.Components.Layout
|
||||||
@using RolemasterDb.App.Components.Shell
|
@using RolemasterDb.App.Components.Shell
|
||||||
@using RolemasterDb.App.Components.Shared
|
@using RolemasterDb.App.Components.Shared
|
||||||
|
@using RolemasterDb.App.Components.Tools
|
||||||
|
|||||||
Reference in New Issue
Block a user