Refine critical curation table workflows
This commit is contained in:
@@ -3,6 +3,7 @@
|
||||
@using System
|
||||
@using System.Collections.Generic
|
||||
@using System.Diagnostics.CodeAnalysis
|
||||
@using System.Linq
|
||||
@inject IJSRuntime JSRuntime
|
||||
@inject LookupService LookupService
|
||||
|
||||
@@ -11,25 +12,51 @@
|
||||
<section class="panel tables-page">
|
||||
<div class="table-browser-toolbar">
|
||||
<div class="table-selector">
|
||||
<label for="critical-table-select">Table</label>
|
||||
<select
|
||||
id="critical-table-select"
|
||||
class="input-shell"
|
||||
value="@selectedTableSlug"
|
||||
@onchange="HandleTableChanged"
|
||||
disabled="@IsTableSelectionDisabled">
|
||||
@if (referenceData is null)
|
||||
{
|
||||
<option value="">Loading tables...</option>
|
||||
}
|
||||
else
|
||||
{
|
||||
@foreach (var table in referenceData.CriticalTables)
|
||||
<label id="critical-table-selector-label">Table</label>
|
||||
<div class="table-select-shell">
|
||||
<button
|
||||
type="button"
|
||||
class="input-shell table-select-trigger"
|
||||
aria-haspopup="listbox"
|
||||
aria-expanded="@isTableMenuOpen"
|
||||
aria-labelledby="critical-table-selector-label critical-table-selector-value"
|
||||
@onclick="ToggleTableMenu"
|
||||
disabled="@IsTableSelectionDisabled">
|
||||
<span class="table-select-trigger-copy">
|
||||
<span id="critical-table-selector-value" class="table-select-trigger-title">@GetSelectedTableLabel()</span>
|
||||
</span>
|
||||
@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>
|
||||
|
||||
<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>
|
||||
<p class="table-browser-reading-hint">@readingHint</p>
|
||||
</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>
|
||||
|
||||
<div class="table-scroll">
|
||||
@@ -120,18 +147,38 @@
|
||||
{
|
||||
@if (TryGetCell(rollBand.Label, group.Key, column.Key, out var groupedCell))
|
||||
{
|
||||
<td
|
||||
class="@GetCellCssClass(groupedCell)"
|
||||
tabindex="0"
|
||||
title="@GetCellTitle(groupedCell)"
|
||||
aria-label="@GetCellTitle(groupedCell)"
|
||||
@onclick="() => OpenCellEditorAsync(groupedCell.ResultId)"
|
||||
@onkeydown="args => HandleCellKeyDownAsync(args, groupedCell.ResultId)">
|
||||
<CompactCriticalCell
|
||||
IsCurated="@groupedCell.IsCurated"
|
||||
Description="@(groupedCell.Description ?? string.Empty)"
|
||||
Effects="@(groupedCell.Effects ?? Array.Empty<CriticalEffectLookupResponse>())"
|
||||
Branches="@(groupedCell.Branches ?? Array.Empty<CriticalBranchLookupResponse>())" />
|
||||
<td class="@GetCellCssClass(groupedCell)">
|
||||
<div class="critical-table-cell-shell">
|
||||
<div class="critical-table-cell-actions">
|
||||
@if (groupedCell.IsCurated)
|
||||
{
|
||||
<span class="critical-cell-status-chip is-curated">Curated</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<button
|
||||
type="button"
|
||||
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>
|
||||
}
|
||||
else
|
||||
@@ -149,18 +196,38 @@
|
||||
{
|
||||
@if (TryGetCell(rollBand.Label, null, column.Key, out var cell))
|
||||
{
|
||||
<td
|
||||
class="@GetCellCssClass(cell)"
|
||||
tabindex="0"
|
||||
title="@GetCellTitle(cell)"
|
||||
aria-label="@GetCellTitle(cell)"
|
||||
@onclick="() => OpenCellEditorAsync(cell.ResultId)"
|
||||
@onkeydown="args => HandleCellKeyDownAsync(args, cell.ResultId)">
|
||||
<CompactCriticalCell
|
||||
IsCurated="@cell.IsCurated"
|
||||
Description="@(cell.Description ?? string.Empty)"
|
||||
Effects="@(cell.Effects ?? Array.Empty<CriticalEffectLookupResponse>())"
|
||||
Branches="@(cell.Branches ?? Array.Empty<CriticalBranchLookupResponse>())" />
|
||||
<td class="@GetCellCssClass(cell)">
|
||||
<div class="critical-table-cell-shell">
|
||||
<div class="critical-table-cell-actions">
|
||||
@if (cell.IsCurated)
|
||||
{
|
||||
<span class="critical-cell-status-chip is-curated">Curated</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<button
|
||||
type="button"
|
||||
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>
|
||||
}
|
||||
else
|
||||
@@ -206,6 +273,20 @@
|
||||
}
|
||||
</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)
|
||||
{
|
||||
<CriticalCellEditorDialog
|
||||
@@ -244,6 +325,15 @@
|
||||
private int? editingResultId;
|
||||
private CriticalCellEditorModel? editorModel;
|
||||
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()
|
||||
{
|
||||
@@ -254,9 +344,25 @@
|
||||
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();
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
{
|
||||
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) =>
|
||||
cell.IsCurated
|
||||
? "critical-table-cell is-editable is-curated"
|
||||
: "critical-table-cell is-editable needs-curation";
|
||||
? "critical-table-cell is-curated"
|
||||
: "critical-table-cell needs-curation";
|
||||
|
||||
private static string GetCellTitle(CriticalTableCellDetail cell) =>
|
||||
cell.IsCurated
|
||||
? "Curated cell. Click to edit this cell."
|
||||
: "Needs curation. Click to edit this cell.";
|
||||
private string GetSelectedTableLabel() =>
|
||||
SelectedTableReference?.Label ?? "Select a table";
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user