Refine critical curation table workflows

This commit is contained in:
2026-03-18 00:45:51 +01:00
parent 45873cd60c
commit d6c4ac653c
4 changed files with 646 additions and 122 deletions

View File

@@ -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();
}
}