Implement queue-first curation workflow
This commit is contained in:
@@ -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);
|
||||
|
||||
}
|
||||
@@ -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; }
|
||||
|
||||
}
|
||||
248
src/RolemasterDb.App/Components/Curation/CurationWorkspace.razor
Normal file
248
src/RolemasterDb.App/Components/Curation/CurationWorkspace.razor
Normal 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;
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user