Implement queue-first curation workflow

This commit is contained in:
2026-04-12 00:45:50 +02:00
parent 752593fa62
commit 7843073d13
15 changed files with 1295 additions and 471 deletions

View File

@@ -0,0 +1,99 @@
@using RolemasterDb.App.Frontend.Curation
<div class="critical-editor-card curation-queue-bar">
<div class="curation-queue-bar-header">
<div class="curation-queue-heading">
<h1 class="panel-title">Curation</h1>
<span class="muted">Queue-first repair workspace</span>
</div>
<div class="curation-queue-links">
@if (!string.IsNullOrWhiteSpace(TablesHref))
{
<a class="btn btn-secondary" href="@TablesHref">Open tables</a>
}
@if (!string.IsNullOrWhiteSpace(DiagnosticsHref))
{
<a class="btn btn-secondary" href="@DiagnosticsHref">Open diagnostics</a>
}
</div>
</div>
<SegmentedTabs
Items="ScopeItems"
SelectedValue="SelectedScope"
SelectedValueChanged="SelectedScopeChanged"
AriaLabel="Curation queue scope"
CssClass="curation-scope-tabs"/>
@if (string.Equals(SelectedScope, CurationQueueScopes.SelectedTable, StringComparison.Ordinal))
{
<div class="field-shell curation-table-select">
<label for="curation-table-select">Table scope</label>
<select
id="curation-table-select"
class="input-shell"
value="@SelectedTableSlug"
@onchange="HandleTableChanged"
disabled="@IsBusy">
@foreach (var table in Tables)
{
<option value="@table.Key">@table.Label</option>
}
</select>
</div>
}
@if (CurrentItem is not null)
{
<div class="curation-queue-summary">
<StatusChip Tone="warning">Needs curation</StatusChip>
<strong>@CurrentItem.TableName</strong>
<span>Roll band <strong>@CurrentItem.RollBand</strong></span>
<span>Severity <strong>@CurrentItem.ColumnLabel</strong></span>
@if (!string.IsNullOrWhiteSpace(CurrentItem.GroupLabel))
{
<span>Variant <strong>@CurrentItem.GroupLabel</strong></span>
}
<span>Result ID <strong>@CurrentItem.ResultId</strong></span>
</div>
}
</div>
@code {
[Parameter]
public IReadOnlyList<SegmentedTabItem> ScopeItems { get; set; } = [];
[Parameter]
public string SelectedScope { get; set; } = CurationQueueScopes.AllTables;
[Parameter, EditorRequired]
public EventCallback<string> SelectedScopeChanged { get; set; }
[Parameter]
public IReadOnlyList<CriticalTableReference> Tables { get; set; } = [];
[Parameter]
public string SelectedTableSlug { get; set; } = string.Empty;
[Parameter, EditorRequired]
public EventCallback<string> SelectedTableSlugChanged { get; set; }
[Parameter]
public CurationQueueItem? CurrentItem { get; set; }
[Parameter]
public bool IsBusy { get; set; }
[Parameter]
public string? TablesHref { get; set; }
[Parameter]
public string? DiagnosticsHref { get; set; }
private Task HandleTableChanged(ChangeEventArgs args) =>
SelectedTableSlugChanged.InvokeAsync(args.Value?.ToString() ?? string.Empty);
}

View File

@@ -0,0 +1,27 @@
<div class="critical-editor-card curation-empty-state">
<div>
<h2 class="panel-title">@Title</h2>
<p class="muted">@Message</p>
</div>
@if (!string.IsNullOrWhiteSpace(ActionHref) && !string.IsNullOrWhiteSpace(ActionLabel))
{
<a class="btn btn-secondary" href="@ActionHref">@ActionLabel</a>
}
</div>
@code {
[Parameter]
public string Title { get; set; } = "Queue empty";
[Parameter]
public string Message { get; set; } = string.Empty;
[Parameter]
public string? ActionHref { get; set; }
[Parameter]
public string? ActionLabel { get; set; }
}

View File

@@ -0,0 +1,248 @@
@using System.Collections.Generic
@using System.Linq
@using Microsoft.AspNetCore.Components.Web
@if (IsLoading)
{
<div class="curation-workspace-shell">
<p class="muted" role="status">@LoadingMessage</p>
</div>
}
else if (Model is null)
{
<div class="curation-workspace-shell">
@if (!string.IsNullOrWhiteSpace(GetVisibleErrorMessage()))
{
<p class="error-text critical-editor-error" role="alert">@GetVisibleErrorMessage()</p>
}
else
{
<p class="muted">@EmptyMessage</p>
}
</div>
}
else
{
<div class="curation-workspace-shell">
@if (!string.IsNullOrWhiteSpace(GetVisibleErrorMessage()))
{
<p class="error-text critical-editor-error" role="alert">@GetVisibleErrorMessage()</p>
}
<div class="critical-curation-grid">
<div class="critical-editor-card critical-curation-preview-card @(IsQuickParseMode ? "is-quick-parse" : null)">
@if (IsQuickParseMode)
{
<CriticalCellQuickParseEditor
@ref="quickParseEditor"
Model="Model"
IsDisabled="@(IsSaving || IsReparsing)"
TextAreaCssClass="input-shell critical-editor-textarea critical-curation-quick-parse-textarea"
OnReparse="OnReparse"/>
}
else
{
<button
type="button"
class="critical-curation-preview-button"
@onclick="OnEnterQuickParse"
disabled="@(IsSaving || IsReparsing)">
<CompactCriticalCell
Description="@Model.DescriptionText"
Effects="@CriticalCellPresentation.BuildPreviewEffects(Model)"
Branches="@CriticalCellPresentation.BuildPreviewBranches(Model)"/>
</button>
}
</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="@CriticalCellPresentation.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>
@if (!IsQuickParseMode)
{
@if (GetUsedLegendEntries(Model, LegendEntries) is { Count: > 0 } usedLegendEntries)
{
<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 class="curation-workspace-actions">
@if (IsQuickParseMode)
{
<button type="button" class="btn btn-link" @onclick="OnCancelQuickParse" disabled="@(IsSaving || IsReparsing)">Cancel</button>
<button
type="button"
class="btn-ritual"
@onclick="HandleReparseClickAsync"
@onclick:stopPropagation="true"
@onclick:preventDefault="true"
disabled="@(IsSaving || IsReparsing)">
@(IsReparsing ? ReparseBusyLabel : ReparseActionLabel)
</button>
}
else
{
@if (ShowSecondaryAction)
{
<button type="button" class="@SecondaryActionCssClass" @onclick="OnSecondaryAction" disabled="@(IsSaving || IsReparsing)">
@SecondaryActionLabel
</button>
}
@if (ShowPrimaryAction)
{
<button type="button" class="@PrimaryActionCssClass" @onclick="OnPrimaryAction" disabled="@(IsSaving || IsReparsing)">
@(IsSaving ? PrimaryBusyLabel : PrimaryActionLabel)
</button>
}
}
</div>
</div>
}
@code {
[Parameter, EditorRequired]
public CriticalCellEditorModel? Model { get; set; }
[Parameter]
public bool IsLoading { get; set; }
[Parameter]
public bool IsSaving { get; set; }
[Parameter]
public bool IsReparsing { get; set; }
[Parameter]
public bool IsQuickParseMode { get; set; }
[Parameter]
public string? ErrorMessage { get; set; }
[Parameter]
public string? QuickParseErrorMessage { get; set; }
[Parameter]
public IReadOnlyList<CriticalTableLegendEntry>? LegendEntries { get; set; }
[Parameter]
public string LoadingMessage { get; set; } = "Loading curation preview...";
[Parameter]
public string EmptyMessage { get; set; } = "No curation preview is available.";
[Parameter]
public string PrimaryActionLabel { get; set; } = "Mark curated";
[Parameter]
public string PrimaryBusyLabel { get; set; } = "Saving...";
[Parameter]
public string PrimaryActionCssClass { get; set; } = "btn-ritual";
[Parameter]
public bool ShowPrimaryAction { get; set; } = true;
[Parameter]
public string SecondaryActionLabel { get; set; } = "Open full editor";
[Parameter]
public string SecondaryActionCssClass { get; set; } = "btn btn-secondary";
[Parameter]
public bool ShowSecondaryAction { get; set; } = true;
[Parameter]
public string ReparseActionLabel { get; set; } = "Parse";
[Parameter]
public string ReparseBusyLabel { get; set; } = "Parsing...";
[Parameter, EditorRequired]
public EventCallback OnPrimaryAction { get; set; }
[Parameter, EditorRequired]
public EventCallback OnSecondaryAction { get; set; }
[Parameter, EditorRequired]
public EventCallback OnEnterQuickParse { get; set; }
[Parameter, EditorRequired]
public EventCallback OnCancelQuickParse { get; set; }
[Parameter, EditorRequired]
public EventCallback OnReparse { get; set; }
private CriticalCellQuickParseEditor? quickParseEditor;
private bool shouldFocusQuickParseEditor;
private bool wasQuickParseMode;
protected override void OnParametersSet()
{
if (IsQuickParseMode && !wasQuickParseMode)
{
shouldFocusQuickParseEditor = true;
}
wasQuickParseMode = IsQuickParseMode;
}
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (shouldFocusQuickParseEditor && quickParseEditor is not null)
{
shouldFocusQuickParseEditor = false;
await quickParseEditor.FocusAsync();
}
}
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();
}
private async Task HandleReparseClickAsync(MouseEventArgs _)
{
if (quickParseEditor is not null)
{
await quickParseEditor.ReparseAsync();
return;
}
await OnReparse.InvokeAsync();
}
private string? GetVisibleErrorMessage() =>
IsQuickParseMode && !string.IsNullOrWhiteSpace(QuickParseErrorMessage) ? QuickParseErrorMessage : ErrorMessage;
}