Refine critical curation table workflows
This commit is contained in:
@@ -3,6 +3,7 @@
|
|||||||
@using System
|
@using System
|
||||||
@using System.Collections.Generic
|
@using System.Collections.Generic
|
||||||
@using System.Diagnostics.CodeAnalysis
|
@using System.Diagnostics.CodeAnalysis
|
||||||
|
@using System.Linq
|
||||||
@inject IJSRuntime JSRuntime
|
@inject IJSRuntime JSRuntime
|
||||||
@inject LookupService LookupService
|
@inject LookupService LookupService
|
||||||
|
|
||||||
@@ -11,25 +12,51 @@
|
|||||||
<section class="panel tables-page">
|
<section class="panel tables-page">
|
||||||
<div class="table-browser-toolbar">
|
<div class="table-browser-toolbar">
|
||||||
<div class="table-selector">
|
<div class="table-selector">
|
||||||
<label for="critical-table-select">Table</label>
|
<label id="critical-table-selector-label">Table</label>
|
||||||
<select
|
<div class="table-select-shell">
|
||||||
id="critical-table-select"
|
<button
|
||||||
class="input-shell"
|
type="button"
|
||||||
value="@selectedTableSlug"
|
class="input-shell table-select-trigger"
|
||||||
@onchange="HandleTableChanged"
|
aria-haspopup="listbox"
|
||||||
disabled="@IsTableSelectionDisabled">
|
aria-expanded="@isTableMenuOpen"
|
||||||
@if (referenceData is null)
|
aria-labelledby="critical-table-selector-label critical-table-selector-value"
|
||||||
{
|
@onclick="ToggleTableMenu"
|
||||||
<option value="">Loading tables...</option>
|
disabled="@IsTableSelectionDisabled">
|
||||||
}
|
<span class="table-select-trigger-copy">
|
||||||
else
|
<span id="critical-table-selector-value" class="table-select-trigger-title">@GetSelectedTableLabel()</span>
|
||||||
{
|
</span>
|
||||||
@foreach (var table in referenceData.CriticalTables)
|
@if (SelectedTableReference is { } selected)
|
||||||
{
|
{
|
||||||
<option value="@table.Key">@table.Label</option>
|
<span class="table-select-trigger-chips">
|
||||||
|
<span class="chip">@($"{selected.CurationPercentage}%")</span>
|
||||||
|
</span>
|
||||||
}
|
}
|
||||||
|
</button>
|
||||||
|
|
||||||
|
@if (isTableMenuOpen && referenceData is not null)
|
||||||
|
{
|
||||||
|
<button type="button" class="table-selector-backdrop" @onclick="CloseTableMenu" aria-label="Close table selector"></button>
|
||||||
|
|
||||||
|
<div class="table-select-menu" role="listbox" aria-labelledby="critical-table-selector-label">
|
||||||
|
@foreach (var table in referenceData.CriticalTables)
|
||||||
|
{
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
role="option"
|
||||||
|
aria-selected="@string.Equals(table.Key, selectedTableSlug, StringComparison.OrdinalIgnoreCase)"
|
||||||
|
class="table-select-option @GetTableOptionCssClass(table)"
|
||||||
|
@onclick="() => SelectTableAsync(table.Key)">
|
||||||
|
<span class="table-select-option-main">
|
||||||
|
<strong class="table-select-option-title">@table.Label</strong>
|
||||||
|
</span>
|
||||||
|
<span class="table-select-option-chips">
|
||||||
|
<span class="chip">@($"{table.CurationPercentage}%")</span>
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
}
|
}
|
||||||
</select>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p class="table-browser-toolbar-copy">Choose a table, then read from the roll band on the left across to the result you need.</p>
|
<p class="table-browser-toolbar-copy">Choose a table, then read from the roll band on the left across to the result you need.</p>
|
||||||
@@ -67,7 +94,7 @@
|
|||||||
<h2 class="panel-title">@detail.DisplayName</h2>
|
<h2 class="panel-title">@detail.DisplayName</h2>
|
||||||
<p class="table-browser-reading-hint">@readingHint</p>
|
<p class="table-browser-reading-hint">@readingHint</p>
|
||||||
</div>
|
</div>
|
||||||
<p class="table-browser-edit-hint">Click any filled result to correct it.</p>
|
<p class="table-browser-edit-hint">Use the curation action or edit action on any filled result.</p>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div class="table-scroll">
|
<div class="table-scroll">
|
||||||
@@ -120,18 +147,38 @@
|
|||||||
{
|
{
|
||||||
@if (TryGetCell(rollBand.Label, group.Key, column.Key, out var groupedCell))
|
@if (TryGetCell(rollBand.Label, group.Key, column.Key, out var groupedCell))
|
||||||
{
|
{
|
||||||
<td
|
<td class="@GetCellCssClass(groupedCell)">
|
||||||
class="@GetCellCssClass(groupedCell)"
|
<div class="critical-table-cell-shell">
|
||||||
tabindex="0"
|
<div class="critical-table-cell-actions">
|
||||||
title="@GetCellTitle(groupedCell)"
|
@if (groupedCell.IsCurated)
|
||||||
aria-label="@GetCellTitle(groupedCell)"
|
{
|
||||||
@onclick="() => OpenCellEditorAsync(groupedCell.ResultId)"
|
<span class="critical-cell-status-chip is-curated">Curated</span>
|
||||||
@onkeydown="args => HandleCellKeyDownAsync(args, groupedCell.ResultId)">
|
}
|
||||||
<CompactCriticalCell
|
else
|
||||||
IsCurated="@groupedCell.IsCurated"
|
{
|
||||||
Description="@(groupedCell.Description ?? string.Empty)"
|
<button
|
||||||
Effects="@(groupedCell.Effects ?? Array.Empty<CriticalEffectLookupResponse>())"
|
type="button"
|
||||||
Branches="@(groupedCell.Branches ?? Array.Empty<CriticalBranchLookupResponse>())" />
|
class="critical-cell-action-button is-curation"
|
||||||
|
title="Open the curation preview for this cell."
|
||||||
|
@onclick="() => OpenCellCurationAsync(groupedCell.ResultId)">
|
||||||
|
Needs Curation
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="critical-cell-action-button is-edit"
|
||||||
|
title="Open the full editor for this cell."
|
||||||
|
@onclick="() => OpenCellEditorAsync(groupedCell.ResultId)">
|
||||||
|
Edit
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<CompactCriticalCell
|
||||||
|
Description="@(groupedCell.Description ?? string.Empty)"
|
||||||
|
Effects="@(groupedCell.Effects ?? Array.Empty<CriticalEffectLookupResponse>())"
|
||||||
|
Branches="@(groupedCell.Branches ?? Array.Empty<CriticalBranchLookupResponse>())" />
|
||||||
|
</div>
|
||||||
</td>
|
</td>
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -149,18 +196,38 @@
|
|||||||
{
|
{
|
||||||
@if (TryGetCell(rollBand.Label, null, column.Key, out var cell))
|
@if (TryGetCell(rollBand.Label, null, column.Key, out var cell))
|
||||||
{
|
{
|
||||||
<td
|
<td class="@GetCellCssClass(cell)">
|
||||||
class="@GetCellCssClass(cell)"
|
<div class="critical-table-cell-shell">
|
||||||
tabindex="0"
|
<div class="critical-table-cell-actions">
|
||||||
title="@GetCellTitle(cell)"
|
@if (cell.IsCurated)
|
||||||
aria-label="@GetCellTitle(cell)"
|
{
|
||||||
@onclick="() => OpenCellEditorAsync(cell.ResultId)"
|
<span class="critical-cell-status-chip is-curated">Curated</span>
|
||||||
@onkeydown="args => HandleCellKeyDownAsync(args, cell.ResultId)">
|
}
|
||||||
<CompactCriticalCell
|
else
|
||||||
IsCurated="@cell.IsCurated"
|
{
|
||||||
Description="@(cell.Description ?? string.Empty)"
|
<button
|
||||||
Effects="@(cell.Effects ?? Array.Empty<CriticalEffectLookupResponse>())"
|
type="button"
|
||||||
Branches="@(cell.Branches ?? Array.Empty<CriticalBranchLookupResponse>())" />
|
class="critical-cell-action-button is-curation"
|
||||||
|
title="Open the curation preview for this cell."
|
||||||
|
@onclick="() => OpenCellCurationAsync(cell.ResultId)">
|
||||||
|
Needs Curation
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="critical-cell-action-button is-edit"
|
||||||
|
title="Open the full editor for this cell."
|
||||||
|
@onclick="() => OpenCellEditorAsync(cell.ResultId)">
|
||||||
|
Edit
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<CompactCriticalCell
|
||||||
|
Description="@(cell.Description ?? string.Empty)"
|
||||||
|
Effects="@(cell.Effects ?? Array.Empty<CriticalEffectLookupResponse>())"
|
||||||
|
Branches="@(cell.Branches ?? Array.Empty<CriticalBranchLookupResponse>())" />
|
||||||
|
</div>
|
||||||
</td>
|
</td>
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -206,6 +273,20 @@
|
|||||||
}
|
}
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
@if (isCurationOpen)
|
||||||
|
{
|
||||||
|
<CriticalCellCurationDialog
|
||||||
|
@key="curationModel"
|
||||||
|
Model="curationModel"
|
||||||
|
IsLoading="isCurationLoading"
|
||||||
|
IsSaving="isCurationSaving"
|
||||||
|
ErrorMessage="@curationError"
|
||||||
|
LegendEntries="@(tableDetail?.Legend ?? Array.Empty<CriticalTableLegendEntry>())"
|
||||||
|
OnClose="CloseCellCurationAsync"
|
||||||
|
OnMarkCurated="MarkCellCuratedAsync"
|
||||||
|
OnEdit="OpenEditorFromCurationAsync" />
|
||||||
|
}
|
||||||
|
|
||||||
@if (isEditorOpen)
|
@if (isEditorOpen)
|
||||||
{
|
{
|
||||||
<CriticalCellEditorDialog
|
<CriticalCellEditorDialog
|
||||||
@@ -244,6 +325,15 @@
|
|||||||
private int? editingResultId;
|
private int? editingResultId;
|
||||||
private CriticalCellEditorModel? editorModel;
|
private CriticalCellEditorModel? editorModel;
|
||||||
private CriticalCellEditorModel? editorComparisonBaselineModel;
|
private CriticalCellEditorModel? editorComparisonBaselineModel;
|
||||||
|
private bool isCurationOpen;
|
||||||
|
private bool isCurationLoading;
|
||||||
|
private bool isCurationSaving;
|
||||||
|
private string? curationError;
|
||||||
|
private int? curatingResultId;
|
||||||
|
private CriticalCellEditorModel? curationModel;
|
||||||
|
private bool isTableMenuOpen;
|
||||||
|
private CriticalTableReference? SelectedTableReference =>
|
||||||
|
referenceData?.CriticalTables.FirstOrDefault(item => string.Equals(item.Key, selectedTableSlug, StringComparison.OrdinalIgnoreCase));
|
||||||
|
|
||||||
protected override async Task OnInitializedAsync()
|
protected override async Task OnInitializedAsync()
|
||||||
{
|
{
|
||||||
@@ -254,9 +344,25 @@
|
|||||||
await LoadTableDetailAsync();
|
await LoadTableDetailAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task HandleTableChanged(ChangeEventArgs args)
|
private void ToggleTableMenu()
|
||||||
{
|
{
|
||||||
selectedTableSlug = args.Value?.ToString() ?? string.Empty;
|
if (IsTableSelectionDisabled)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
isTableMenuOpen = !isTableMenuOpen;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CloseTableMenu()
|
||||||
|
{
|
||||||
|
isTableMenuOpen = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task SelectTableAsync(string tableSlug)
|
||||||
|
{
|
||||||
|
selectedTableSlug = tableSlug;
|
||||||
|
isTableMenuOpen = false;
|
||||||
await LoadTableDetailAsync();
|
await LoadTableDetailAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -373,6 +479,173 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task OpenCellCurationAsync(int resultId)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(selectedTableSlug))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
isCurationSaving = false;
|
||||||
|
isCurationOpen = true;
|
||||||
|
await LoadCurationCellAsync(resultId, "The selected cell could not be loaded for curation.");
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task CloseCellCurationAsync()
|
||||||
|
{
|
||||||
|
isCurationOpen = false;
|
||||||
|
isCurationLoading = false;
|
||||||
|
isCurationSaving = false;
|
||||||
|
curationError = null;
|
||||||
|
curatingResultId = null;
|
||||||
|
curationModel = null;
|
||||||
|
await InvokeAsync(StateHasChanged);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task MarkCellCuratedAsync()
|
||||||
|
{
|
||||||
|
if (curationModel is null || string.IsNullOrWhiteSpace(selectedTableSlug) || curatingResultId is null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
isCurationSaving = true;
|
||||||
|
curationError = null;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
curationModel.IsCurated = true;
|
||||||
|
var response = await LookupService.UpdateCriticalCellAsync(selectedTableSlug, curatingResultId.Value, curationModel.ToRequest());
|
||||||
|
if (response is null)
|
||||||
|
{
|
||||||
|
curationError = "The selected cell could not be marked curated.";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await LoadTableDetailAsync();
|
||||||
|
|
||||||
|
var nextResultId = FindNextUncuratedResultId(curatingResultId.Value);
|
||||||
|
if (nextResultId is null)
|
||||||
|
{
|
||||||
|
await CloseCellCurationAsync();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await LoadCurationCellAsync(nextResultId.Value, "The next cell could not be loaded for curation.");
|
||||||
|
}
|
||||||
|
catch (Exception exception)
|
||||||
|
{
|
||||||
|
curationError = exception.Message;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
isCurationSaving = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task OpenEditorFromCurationAsync()
|
||||||
|
{
|
||||||
|
if (curatingResultId is null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var resultId = curatingResultId.Value;
|
||||||
|
await CloseCellCurationAsync();
|
||||||
|
await OpenCellEditorAsync(resultId);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task LoadCurationCellAsync(int resultId, string loadFailureMessage)
|
||||||
|
{
|
||||||
|
curationError = null;
|
||||||
|
curationModel = null;
|
||||||
|
curatingResultId = resultId;
|
||||||
|
isCurationLoading = true;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var response = await LookupService.GetCriticalCellEditorAsync(selectedTableSlug, resultId);
|
||||||
|
if (response is null)
|
||||||
|
{
|
||||||
|
curationError = loadFailureMessage;
|
||||||
|
curationModel = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
curationModel = CriticalCellEditorModel.FromResponse(response);
|
||||||
|
}
|
||||||
|
catch (Exception exception)
|
||||||
|
{
|
||||||
|
curationError = exception.Message;
|
||||||
|
curationModel = null;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
isCurationLoading = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private int? FindNextUncuratedResultId(int currentResultId)
|
||||||
|
{
|
||||||
|
if (tableDetail?.Cells is null || tableDetail.Cells.Count == 0)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var orderedCells = tableDetail.Cells
|
||||||
|
.OrderBy(cell => GetGroupSortOrder(cell.GroupKey))
|
||||||
|
.ThenBy(cell => GetColumnSortOrder(cell.ColumnKey))
|
||||||
|
.ThenBy(cell => GetRollBandSortOrder(cell.RollBand))
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
var currentIndex = orderedCells.FindIndex(cell => cell.ResultId == currentResultId);
|
||||||
|
for (var index = currentIndex + 1; index < orderedCells.Count; index++)
|
||||||
|
{
|
||||||
|
if (!orderedCells[index].IsCurated)
|
||||||
|
{
|
||||||
|
return orderedCells[index].ResultId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int GetGroupSortOrder(string? groupKey)
|
||||||
|
{
|
||||||
|
if (tableDetail is null || string.IsNullOrWhiteSpace(groupKey))
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
return tableDetail.Groups
|
||||||
|
.FirstOrDefault(group => string.Equals(group.Key, groupKey, StringComparison.OrdinalIgnoreCase))
|
||||||
|
?.SortOrder ?? int.MaxValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int GetColumnSortOrder(string columnKey)
|
||||||
|
{
|
||||||
|
if (tableDetail is null)
|
||||||
|
{
|
||||||
|
return int.MaxValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
return tableDetail.Columns
|
||||||
|
.FirstOrDefault(column => string.Equals(column.Key, columnKey, StringComparison.OrdinalIgnoreCase))
|
||||||
|
?.SortOrder ?? int.MaxValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
private int GetRollBandSortOrder(string rollBandLabel)
|
||||||
|
{
|
||||||
|
if (tableDetail is null)
|
||||||
|
{
|
||||||
|
return int.MaxValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
return tableDetail.RollBands
|
||||||
|
.FirstOrDefault(rollBand => string.Equals(rollBand.Label, rollBandLabel, StringComparison.OrdinalIgnoreCase))
|
||||||
|
?.SortOrder ?? int.MaxValue;
|
||||||
|
}
|
||||||
|
|
||||||
private async Task CloseCellEditorAsync()
|
private async Task CloseCellEditorAsync()
|
||||||
{
|
{
|
||||||
isEditorOpen = false;
|
isEditorOpen = false;
|
||||||
@@ -454,21 +727,24 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task HandleCellKeyDownAsync(KeyboardEventArgs args, int resultId)
|
|
||||||
{
|
|
||||||
if (args.Key is "Enter" or " ")
|
|
||||||
{
|
|
||||||
await OpenCellEditorAsync(resultId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static string GetCellCssClass(CriticalTableCellDetail cell) =>
|
private static string GetCellCssClass(CriticalTableCellDetail cell) =>
|
||||||
cell.IsCurated
|
cell.IsCurated
|
||||||
? "critical-table-cell is-editable is-curated"
|
? "critical-table-cell is-curated"
|
||||||
: "critical-table-cell is-editable needs-curation";
|
: "critical-table-cell needs-curation";
|
||||||
|
|
||||||
private static string GetCellTitle(CriticalTableCellDetail cell) =>
|
private string GetSelectedTableLabel() =>
|
||||||
cell.IsCurated
|
SelectedTableReference?.Label ?? "Select a table";
|
||||||
? "Curated cell. Click to edit this cell."
|
|
||||||
: "Needs curation. Click to edit this cell.";
|
private string GetTableOptionCssClass(CriticalTableReference table)
|
||||||
|
{
|
||||||
|
var classes = new List<string>();
|
||||||
|
|
||||||
|
if (string.Equals(table.Key, selectedTableSlug, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
classes.Add("is-selected");
|
||||||
|
}
|
||||||
|
|
||||||
|
classes.Add(table.CurationPercentage >= 100 ? "is-curated" : "needs-curation");
|
||||||
|
return string.Join(' ', classes);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,12 +2,6 @@
|
|||||||
@using RolemasterDb.App.Features
|
@using RolemasterDb.App.Features
|
||||||
|
|
||||||
<div class="critical-cell">
|
<div class="critical-cell">
|
||||||
<div class="critical-cell-status-row">
|
|
||||||
<span class="critical-cell-status-chip @(IsCurated ? "is-curated" : "needs-curation")">
|
|
||||||
@(IsCurated ? "Curated" : "Needs Curation")
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
@if (!string.IsNullOrWhiteSpace(Description))
|
@if (!string.IsNullOrWhiteSpace(Description))
|
||||||
{
|
{
|
||||||
<p class="critical-cell-description">@Description</p>
|
<p class="critical-cell-description">@Description</p>
|
||||||
@@ -46,9 +40,6 @@
|
|||||||
[Parameter, EditorRequired]
|
[Parameter, EditorRequired]
|
||||||
public string Description { get; set; } = string.Empty;
|
public string Description { get; set; } = string.Empty;
|
||||||
|
|
||||||
[Parameter]
|
|
||||||
public bool IsCurated { get; set; }
|
|
||||||
|
|
||||||
[Parameter]
|
[Parameter]
|
||||||
public IReadOnlyList<CriticalEffectLookupResponse>? Effects { get; set; }
|
public IReadOnlyList<CriticalEffectLookupResponse>? Effects { get; set; }
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,281 @@
|
|||||||
|
@using System.Collections.Generic
|
||||||
|
@using System.Linq
|
||||||
|
@using Microsoft.JSInterop
|
||||||
|
@using RolemasterDb.App.Features
|
||||||
|
@implements IAsyncDisposable
|
||||||
|
@inject IJSRuntime JSRuntime
|
||||||
|
|
||||||
|
<div class="critical-editor-backdrop"
|
||||||
|
@onpointerdown="HandleBackdropPointerDown"
|
||||||
|
@onpointerup="HandleBackdropPointerUp"
|
||||||
|
@onpointercancel="HandleBackdropPointerCancel">
|
||||||
|
<div class="critical-editor-dialog critical-curation-dialog"
|
||||||
|
@onpointerdown="HandleDialogPointerDown"
|
||||||
|
@onpointerup="HandleDialogPointerUp"
|
||||||
|
@onpointercancel="HandleDialogPointerCancel"
|
||||||
|
@onpointerdown:stopPropagation="true"
|
||||||
|
@onpointerup:stopPropagation="true"
|
||||||
|
@onpointercancel:stopPropagation="true">
|
||||||
|
<header class="critical-editor-header">
|
||||||
|
<div>
|
||||||
|
<h3 class="panel-title">Curate Result Card</h3>
|
||||||
|
@if (Model is not null)
|
||||||
|
{
|
||||||
|
<p class="muted critical-editor-meta">
|
||||||
|
<strong>@Model.TableName</strong>
|
||||||
|
<span> · Column <strong>@BuildColumnDisplayText(Model)</strong></span>
|
||||||
|
<span> · Roll band <strong>@Model.RollBand</strong></span>
|
||||||
|
</p>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
<button type="button" class="btn btn-link critical-editor-close" @onclick="OnClose">Close</button>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
@if (IsLoading)
|
||||||
|
{
|
||||||
|
<div class="critical-editor-body">
|
||||||
|
<p class="muted">Loading curation preview...</p>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
else if (!string.IsNullOrWhiteSpace(ErrorMessage))
|
||||||
|
{
|
||||||
|
<div class="critical-editor-body">
|
||||||
|
<p class="error-text critical-editor-error">@ErrorMessage</p>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
else if (Model is not null)
|
||||||
|
{
|
||||||
|
<div class="critical-editor-body critical-curation-body">
|
||||||
|
<div class="critical-curation-grid">
|
||||||
|
<div class="critical-editor-card critical-curation-preview-card">
|
||||||
|
<CompactCriticalCell
|
||||||
|
Description="@Model.DescriptionText"
|
||||||
|
Effects="@BuildPreviewEffects(Model)"
|
||||||
|
Branches="@BuildPreviewBranches(Model)" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="critical-editor-card critical-curation-source-card">
|
||||||
|
@if (!string.IsNullOrWhiteSpace(Model.SourceImageUrl))
|
||||||
|
{
|
||||||
|
<img
|
||||||
|
class="critical-curation-source-image"
|
||||||
|
src="@Model.SourceImageUrl"
|
||||||
|
alt="@BuildSourceImageAltText(Model)" />
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<div class="critical-curation-source-empty">
|
||||||
|
<p class="muted">No source image is available for this cell yet.</p>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@{
|
||||||
|
var usedLegendEntries = GetUsedLegendEntries(Model, LegendEntries);
|
||||||
|
}
|
||||||
|
|
||||||
|
@if (usedLegendEntries.Count > 0)
|
||||||
|
{
|
||||||
|
<div class="critical-curation-legend">
|
||||||
|
@foreach (var entry in usedLegendEntries)
|
||||||
|
{
|
||||||
|
<div class="critical-curation-legend-item" title="@entry.Tooltip">
|
||||||
|
<span class="legend-symbol">@entry.Symbol</span>
|
||||||
|
<strong>@entry.Label</strong>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<footer class="critical-editor-footer">
|
||||||
|
<button type="button" class="btn btn-link" @onclick="OnEdit" disabled="@(IsSaving)">Edit</button>
|
||||||
|
<button type="button" class="btn-ritual" @onclick="OnMarkCurated" disabled="@(IsSaving)">
|
||||||
|
@(IsSaving ? "Saving..." : "Mark as Curated")
|
||||||
|
</button>
|
||||||
|
</footer>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@code {
|
||||||
|
[Parameter, EditorRequired]
|
||||||
|
public CriticalCellEditorModel? Model { get; set; }
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public bool IsLoading { get; set; }
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public bool IsSaving { get; set; }
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public string? ErrorMessage { get; set; }
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public IReadOnlyList<CriticalTableLegendEntry>? LegendEntries { get; set; }
|
||||||
|
|
||||||
|
[Parameter, EditorRequired]
|
||||||
|
public EventCallback OnClose { get; set; }
|
||||||
|
|
||||||
|
[Parameter, EditorRequired]
|
||||||
|
public EventCallback OnMarkCurated { get; set; }
|
||||||
|
|
||||||
|
[Parameter, EditorRequired]
|
||||||
|
public EventCallback OnEdit { get; set; }
|
||||||
|
|
||||||
|
private IJSObjectReference? jsModule;
|
||||||
|
private bool isBackdropPointerDown;
|
||||||
|
|
||||||
|
protected override async Task OnAfterRenderAsync(bool firstRender)
|
||||||
|
{
|
||||||
|
if (!firstRender)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
jsModule = await JSRuntime.InvokeAsync<IJSObjectReference>(
|
||||||
|
"import",
|
||||||
|
"./Components/Shared/CriticalCellEditorDialog.razor.js");
|
||||||
|
|
||||||
|
await jsModule.InvokeVoidAsync("lockBackgroundScroll");
|
||||||
|
}
|
||||||
|
|
||||||
|
public async ValueTask DisposeAsync()
|
||||||
|
{
|
||||||
|
if (jsModule is null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await jsModule.InvokeVoidAsync("unlockBackgroundScroll");
|
||||||
|
await jsModule.DisposeAsync();
|
||||||
|
}
|
||||||
|
catch (JSDisconnectedException)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void HandleBackdropPointerDown()
|
||||||
|
{
|
||||||
|
isBackdropPointerDown = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task HandleBackdropPointerUp()
|
||||||
|
{
|
||||||
|
if (!isBackdropPointerDown)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
isBackdropPointerDown = false;
|
||||||
|
await OnClose.InvokeAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void HandleBackdropPointerCancel()
|
||||||
|
{
|
||||||
|
isBackdropPointerDown = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void HandleDialogPointerDown()
|
||||||
|
{
|
||||||
|
isBackdropPointerDown = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void HandleDialogPointerUp()
|
||||||
|
{
|
||||||
|
isBackdropPointerDown = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void HandleDialogPointerCancel()
|
||||||
|
{
|
||||||
|
isBackdropPointerDown = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string BuildSourceImageAltText(CriticalCellEditorModel model)
|
||||||
|
{
|
||||||
|
var segments = new List<string>
|
||||||
|
{
|
||||||
|
model.TableName,
|
||||||
|
$"roll band {model.RollBand}",
|
||||||
|
$"column {model.ColumnLabel}"
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(model.GroupLabel))
|
||||||
|
{
|
||||||
|
segments.Add($"variant {model.GroupLabel}");
|
||||||
|
}
|
||||||
|
|
||||||
|
return string.Join(", ", segments);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string BuildColumnDisplayText(CriticalCellEditorModel model) =>
|
||||||
|
string.IsNullOrWhiteSpace(model.GroupLabel)
|
||||||
|
? model.ColumnLabel
|
||||||
|
: $"{model.GroupLabel} / {model.ColumnLabel}";
|
||||||
|
|
||||||
|
private static IReadOnlyList<CriticalEffectLookupResponse> BuildPreviewEffects(CriticalCellEditorModel model) =>
|
||||||
|
model.Effects
|
||||||
|
.Select(effect => new CriticalEffectLookupResponse(
|
||||||
|
effect.EffectCode,
|
||||||
|
effect.Target,
|
||||||
|
effect.ValueInteger,
|
||||||
|
effect.ValueExpression,
|
||||||
|
effect.DurationRounds,
|
||||||
|
effect.PerRound,
|
||||||
|
effect.Modifier,
|
||||||
|
effect.BodyPart,
|
||||||
|
effect.IsPermanent,
|
||||||
|
effect.SourceType,
|
||||||
|
effect.SourceText))
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
private static IReadOnlyList<CriticalBranchLookupResponse> BuildPreviewBranches(CriticalCellEditorModel model) =>
|
||||||
|
model.Branches
|
||||||
|
.OrderBy(branch => branch.SortOrder)
|
||||||
|
.Select(branch => new CriticalBranchLookupResponse(
|
||||||
|
branch.BranchKind,
|
||||||
|
branch.ConditionKey,
|
||||||
|
branch.ConditionText,
|
||||||
|
branch.DescriptionText,
|
||||||
|
branch.RawAffixText,
|
||||||
|
branch.Effects
|
||||||
|
.Select(effect => new CriticalEffectLookupResponse(
|
||||||
|
effect.EffectCode,
|
||||||
|
effect.Target,
|
||||||
|
effect.ValueInteger,
|
||||||
|
effect.ValueExpression,
|
||||||
|
effect.DurationRounds,
|
||||||
|
effect.PerRound,
|
||||||
|
effect.Modifier,
|
||||||
|
effect.BodyPart,
|
||||||
|
effect.IsPermanent,
|
||||||
|
effect.SourceType,
|
||||||
|
effect.SourceText))
|
||||||
|
.ToList(),
|
||||||
|
branch.RawText,
|
||||||
|
branch.SortOrder))
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
private static IReadOnlyList<CriticalTableLegendEntry> GetUsedLegendEntries(
|
||||||
|
CriticalCellEditorModel model,
|
||||||
|
IReadOnlyList<CriticalTableLegendEntry>? legendEntries)
|
||||||
|
{
|
||||||
|
if (legendEntries is null || legendEntries.Count == 0)
|
||||||
|
{
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
var usedEffectCodes = model.Effects
|
||||||
|
.Select(effect => effect.EffectCode)
|
||||||
|
.Concat(model.Branches.SelectMany(branch => branch.Effects.Select(effect => effect.EffectCode)))
|
||||||
|
.Where(effectCode => !string.IsNullOrWhiteSpace(effectCode))
|
||||||
|
.ToHashSet(StringComparer.OrdinalIgnoreCase);
|
||||||
|
|
||||||
|
return legendEntries
|
||||||
|
.Where(entry => usedEffectCodes.Contains(entry.EffectCode))
|
||||||
|
.ToList();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -32,15 +32,6 @@
|
|||||||
<span> · Variant <strong>@Model.GroupLabel</strong></span>
|
<span> · Variant <strong>@Model.GroupLabel</strong></span>
|
||||||
}
|
}
|
||||||
</p>
|
</p>
|
||||||
<div class="critical-editor-status-row">
|
|
||||||
<span class="critical-editor-curation-badge @(Model.IsCurated ? "is-curated" : "needs-curation")">
|
|
||||||
@(Model.IsCurated ? "Curated" : "Needs Curation")
|
|
||||||
</span>
|
|
||||||
@if (Model.SourcePageNumber is not null)
|
|
||||||
{
|
|
||||||
<span class="chip">Source page @Model.SourcePageNumber</span>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -76,47 +67,6 @@
|
|||||||
<p class="error-text critical-editor-error">@SaveErrorMessage</p>
|
<p class="error-text critical-editor-error">@SaveErrorMessage</p>
|
||||||
}
|
}
|
||||||
|
|
||||||
<section class="critical-editor-source-grid">
|
|
||||||
<div class="critical-editor-card critical-editor-status-card">
|
|
||||||
<div class="critical-editor-section-header">
|
|
||||||
<div>
|
|
||||||
<h4>Curation State</h4>
|
|
||||||
<p class="muted">Curated cells are protected from importer content overwrites until you unmark them.</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<label class="critical-editor-curation-toggle">
|
|
||||||
<InputCheckbox @bind-Value="Model.IsCurated" />
|
|
||||||
<span>Mark this result curated</span>
|
|
||||||
</label>
|
|
||||||
<p class="muted critical-editor-inline-copy">
|
|
||||||
@(Model.IsCurated
|
|
||||||
? "This result will keep its current text, branches, and effects on later imports."
|
|
||||||
: "This result will be refreshed from the importer on later imports.")
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="critical-editor-card critical-editor-source-card">
|
|
||||||
<div class="critical-editor-section-header">
|
|
||||||
<div>
|
|
||||||
<h4>Source Cell</h4>
|
|
||||||
<p class="muted">Use the importer crop as a visual reference while curating the result.</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
@if (!string.IsNullOrWhiteSpace(Model.SourceImageUrl))
|
|
||||||
{
|
|
||||||
<img
|
|
||||||
class="critical-editor-source-image"
|
|
||||||
src="@Model.SourceImageUrl"
|
|
||||||
alt="@BuildSourceImageAltText(Model)" />
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
<p class="muted critical-editor-inline-copy">No source image is available for this cell yet.</p>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="critical-editor-section">
|
<section class="critical-editor-section">
|
||||||
<div class="critical-editor-section-header">
|
<div class="critical-editor-section-header">
|
||||||
<div>
|
<div>
|
||||||
@@ -133,12 +83,38 @@
|
|||||||
@(IsReparsing ? "Generating..." : "Generate From Quick Input")
|
@(IsReparsing ? "Generating..." : "Generate From Quick Input")
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="field-shell">
|
<div class="critical-editor-quick-parse-grid">
|
||||||
<label>Quick Parse Input</label>
|
<div class="field-shell">
|
||||||
<textarea
|
<label>Quick Parse Input</label>
|
||||||
class="input-shell critical-editor-textarea tall"
|
<textarea
|
||||||
value="@Model.QuickParseInput"
|
class="input-shell critical-editor-textarea tall"
|
||||||
@oninput="HandleQuickParseInputChanged"></textarea>
|
value="@Model.QuickParseInput"
|
||||||
|
@oninput="HandleQuickParseInputChanged"></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="critical-editor-quick-parse-source">
|
||||||
|
<div class="critical-editor-quick-parse-source-header">
|
||||||
|
<label>Source Cell</label>
|
||||||
|
@if (Model.SourcePageNumber is not null)
|
||||||
|
{
|
||||||
|
<span class="chip">Page @Model.SourcePageNumber</span>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="critical-editor-quick-parse-source-frame">
|
||||||
|
@if (!string.IsNullOrWhiteSpace(Model.SourceImageUrl))
|
||||||
|
{
|
||||||
|
<img
|
||||||
|
class="critical-editor-source-image is-inline"
|
||||||
|
src="@Model.SourceImageUrl"
|
||||||
|
alt="@BuildSourceImageAltText(Model)" />
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<p class="muted critical-editor-inline-copy">No source image is available for this cell yet.</p>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p class="muted critical-editor-advanced-hint">Example: <code>Foe brings his guard up, frightened by your display.</code> then <code>+5, 1mp</code> or <code>w/o shield: glancing blow, +15, 3s, 3np</code>.</p>
|
<p class="muted critical-editor-advanced-hint">Example: <code>Foe brings his guard up, frightened by your display.</code> then <code>+5, 1mp</code> or <code>w/o shield: glancing blow, +15, 3s, 3np</code>.</p>
|
||||||
<div class="critical-editor-quick-legend">
|
<div class="critical-editor-quick-legend">
|
||||||
|
|||||||
Reference in New Issue
Block a user