Add critical cell reparse comparison review
This commit is contained in:
@@ -507,6 +507,7 @@ The following groundwork is already implemented in the web app as of March 15, 2
|
|||||||
- the critical table browser is framed for play use rather than import administration
|
- the critical table browser is framed for play use rather than import administration
|
||||||
- critical cells support re-parse from shared parser logic
|
- critical cells support re-parse from shared parser logic
|
||||||
- advanced diagnostics have been separated from the primary editing flow
|
- advanced diagnostics have been separated from the primary editing flow
|
||||||
|
- advanced curation now includes a generated-versus-current comparison view for re-parse review
|
||||||
|
|
||||||
These changes are real and complete, but they are no longer the active roadmap because the detailed acceptance checklist still has unfinished items.
|
These changes are real and complete, but they are no longer the active roadmap because the detailed acceptance checklist still has unfinished items.
|
||||||
|
|
||||||
@@ -575,6 +576,17 @@ Acceptance criteria:
|
|||||||
|
|
||||||
### Phase 4: Compare and review workflow
|
### Phase 4: Compare and review workflow
|
||||||
|
|
||||||
|
Status:
|
||||||
|
|
||||||
|
- implemented in the web app on March 15, 2026
|
||||||
|
|
||||||
|
Implemented model:
|
||||||
|
|
||||||
|
- re-parse now returns the freshly generated parser output alongside the merged editor state
|
||||||
|
- the editor captures the pre-reparse card as a review baseline before applying the merged state
|
||||||
|
- advanced curation shows side-by-side cards for the prior edit, the fresh generated parse, and the merged post-reparse result when available
|
||||||
|
- diff summary chips call out result-text, base-effect, and condition changes without requiring raw JSON inspection
|
||||||
|
|
||||||
Scope:
|
Scope:
|
||||||
|
|
||||||
- add a clear comparison surface for generated result versus current edited result
|
- add a clear comparison surface for generated result versus current edited result
|
||||||
@@ -641,4 +653,4 @@ Mitigation:
|
|||||||
|
|
||||||
## Recommended Next Step
|
## Recommended Next Step
|
||||||
|
|
||||||
Implement the new Phase 3 next. The remaining structural gap is generated-versus-overridden state, so re-parse and future import refreshes can preserve intentional curation instead of treating all edited values the same.
|
Implement Phase 5 next. The remaining cleanup is to tighten the boundary between compact curation tools and engineering diagnostics so ordinary correction workflows stay user-facing while deeper diagnostics remain available behind an explicit advanced surface.
|
||||||
|
|||||||
@@ -206,6 +206,7 @@
|
|||||||
{
|
{
|
||||||
<CriticalCellEditorDialog
|
<CriticalCellEditorDialog
|
||||||
Model="editorModel"
|
Model="editorModel"
|
||||||
|
ComparisonBaseline="editorComparisonBaselineModel"
|
||||||
IsLoading="isEditorLoading"
|
IsLoading="isEditorLoading"
|
||||||
IsReparsing="isEditorReparsing"
|
IsReparsing="isEditorReparsing"
|
||||||
IsSaving="isEditorSaving"
|
IsSaving="isEditorSaving"
|
||||||
@@ -237,6 +238,7 @@
|
|||||||
private string? editorSaveError;
|
private string? editorSaveError;
|
||||||
private int? editingResultId;
|
private int? editingResultId;
|
||||||
private CriticalCellEditorModel? editorModel;
|
private CriticalCellEditorModel? editorModel;
|
||||||
|
private CriticalCellEditorModel? editorComparisonBaselineModel;
|
||||||
|
|
||||||
protected override async Task OnInitializedAsync()
|
protected override async Task OnInitializedAsync()
|
||||||
{
|
{
|
||||||
@@ -336,6 +338,7 @@
|
|||||||
editorReparseError = null;
|
editorReparseError = null;
|
||||||
editorSaveError = null;
|
editorSaveError = null;
|
||||||
editorModel = null;
|
editorModel = null;
|
||||||
|
editorComparisonBaselineModel = null;
|
||||||
editingResultId = resultId;
|
editingResultId = resultId;
|
||||||
isEditorReparsing = false;
|
isEditorReparsing = false;
|
||||||
isEditorSaving = false;
|
isEditorSaving = false;
|
||||||
@@ -376,6 +379,7 @@
|
|||||||
editorSaveError = null;
|
editorSaveError = null;
|
||||||
editingResultId = null;
|
editingResultId = null;
|
||||||
editorModel = null;
|
editorModel = null;
|
||||||
|
editorComparisonBaselineModel = null;
|
||||||
await InvokeAsync(StateHasChanged);
|
await InvokeAsync(StateHasChanged);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -391,6 +395,7 @@
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
var comparisonBaseline = editorModel.Clone();
|
||||||
var response = await LookupService.ReparseCriticalCellAsync(selectedTableSlug, editingResultId.Value, editorModel.ToRequest());
|
var response = await LookupService.ReparseCriticalCellAsync(selectedTableSlug, editingResultId.Value, editorModel.ToRequest());
|
||||||
if (response is null)
|
if (response is null)
|
||||||
{
|
{
|
||||||
@@ -398,6 +403,7 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
editorComparisonBaselineModel = comparisonBaseline;
|
||||||
editorModel = CriticalCellEditorModel.FromResponse(response);
|
editorModel = CriticalCellEditorModel.FromResponse(response);
|
||||||
}
|
}
|
||||||
catch (Exception exception)
|
catch (Exception exception)
|
||||||
|
|||||||
@@ -73,4 +73,22 @@ public sealed class CriticalBranchEditorModel
|
|||||||
effects.Count == 0
|
effects.Count == 0
|
||||||
? "{}"
|
? "{}"
|
||||||
: JsonSerializer.Serialize(new { effects = effects.Select(effect => effect.ToItem()).ToList() });
|
: JsonSerializer.Serialize(new { effects = effects.Select(effect => effect.ToItem()).ToList() });
|
||||||
|
|
||||||
|
public CriticalBranchEditorModel Clone() =>
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
BranchKind = BranchKind,
|
||||||
|
ConditionKey = ConditionKey,
|
||||||
|
ConditionText = ConditionText,
|
||||||
|
ConditionJson = ConditionJson,
|
||||||
|
RawText = RawText,
|
||||||
|
DescriptionText = DescriptionText,
|
||||||
|
RawAffixText = RawAffixText,
|
||||||
|
ParsedJson = ParsedJson,
|
||||||
|
SortOrder = SortOrder,
|
||||||
|
OriginKey = OriginKey,
|
||||||
|
IsOverridden = IsOverridden,
|
||||||
|
AreEffectsOverridden = AreEffectsOverridden,
|
||||||
|
Effects = Effects.Select(effect => effect.Clone()).ToList()
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -74,6 +74,10 @@
|
|||||||
{
|
{
|
||||||
<p class="muted critical-editor-advanced-hint">@GetParserNoteSummary(Model.ValidationMessages.Count)</p>
|
<p class="muted critical-editor-advanced-hint">@GetParserNoteSummary(Model.ValidationMessages.Count)</p>
|
||||||
}
|
}
|
||||||
|
@if (HasComparisonDifferences(Model, ComparisonBaseline))
|
||||||
|
{
|
||||||
|
<p class="muted critical-editor-advanced-hint">Fresh parsing differs from the current edited card. Review Generated Compare before saving if you want to keep the overrides.</p>
|
||||||
|
}
|
||||||
<div class="field-shell">
|
<div class="field-shell">
|
||||||
<label>Result Text</label>
|
<label>Result Text</label>
|
||||||
<InputTextArea class="input-shell critical-editor-textarea compact" @bind-Value="Model.DescriptionText" @bind-Value:after="MarkDescriptionOverridden" />
|
<InputTextArea class="input-shell critical-editor-textarea compact" @bind-Value="Model.DescriptionText" @bind-Value:after="MarkDescriptionOverridden" />
|
||||||
@@ -176,11 +180,55 @@
|
|||||||
<section class="critical-editor-section critical-editor-diagnostics-section">
|
<section class="critical-editor-section critical-editor-diagnostics-section">
|
||||||
<details class="critical-editor-advanced">
|
<details class="critical-editor-advanced">
|
||||||
<summary class="critical-editor-advanced-summary">
|
<summary class="critical-editor-advanced-summary">
|
||||||
<span>Advanced Diagnostics</span>
|
<span>Advanced Review & Diagnostics</span>
|
||||||
<span class="critical-editor-advanced-meta">@GetAdvancedSummary(Model)</span>
|
<span class="critical-editor-advanced-meta">@GetAdvancedSummary(Model, ComparisonBaseline)</span>
|
||||||
</summary>
|
</summary>
|
||||||
|
|
||||||
<div class="critical-editor-advanced-body">
|
<div class="critical-editor-advanced-body">
|
||||||
|
@if (Model.GeneratedState is not null)
|
||||||
|
{
|
||||||
|
<div class="critical-editor-card nested">
|
||||||
|
<div class="critical-editor-card-header">
|
||||||
|
<div>
|
||||||
|
<strong>Generated Compare</strong>
|
||||||
|
<p class="muted critical-editor-inline-copy">Compare the current edited card against the fresh parser output from the raw text.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="chip-row">
|
||||||
|
@foreach (var item in GetComparisonSummaryItems(Model, ComparisonBaseline))
|
||||||
|
{
|
||||||
|
<span class="chip">@item</span>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="critical-editor-compare-grid">
|
||||||
|
<CriticalResultPreviewCard
|
||||||
|
Title="Current Edited Card"
|
||||||
|
Caption="@GetCurrentComparisonCaption(ComparisonBaseline)"
|
||||||
|
Description="@GetComparisonSourceModel(Model, ComparisonBaseline).DescriptionText"
|
||||||
|
Effects="@BuildPreviewEffects(GetComparisonSourceModel(Model, ComparisonBaseline))"
|
||||||
|
Branches="@BuildPreviewBranches(GetComparisonSourceModel(Model, ComparisonBaseline))" />
|
||||||
|
<CriticalResultPreviewCard
|
||||||
|
Title="Generated From Raw Text"
|
||||||
|
Caption="This is the fresh parser output before override preservation."
|
||||||
|
Description="@Model.GeneratedState.DescriptionText"
|
||||||
|
Effects="@Model.GeneratedState.Effects"
|
||||||
|
Branches="@Model.GeneratedState.Branches"
|
||||||
|
Notes="@Model.GeneratedState.ValidationMessages" />
|
||||||
|
@if (ComparisonBaseline is not null)
|
||||||
|
{
|
||||||
|
<CriticalResultPreviewCard
|
||||||
|
Title="Current After Re-Parse"
|
||||||
|
Caption="This is the merged editor state after keeping the existing overrides."
|
||||||
|
Description="@Model.DescriptionText"
|
||||||
|
Effects="@BuildPreviewEffects(Model)"
|
||||||
|
Branches="@BuildPreviewBranches(Model)" />
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
<div class="critical-editor-card nested">
|
<div class="critical-editor-card nested">
|
||||||
<div class="critical-editor-card-header">
|
<div class="critical-editor-card-header">
|
||||||
<div>
|
<div>
|
||||||
@@ -277,6 +325,9 @@
|
|||||||
[Parameter, EditorRequired]
|
[Parameter, EditorRequired]
|
||||||
public CriticalCellEditorModel? Model { get; set; }
|
public CriticalCellEditorModel? Model { get; set; }
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public CriticalCellEditorModel? ComparisonBaseline { get; set; }
|
||||||
|
|
||||||
[Parameter]
|
[Parameter]
|
||||||
public bool IsLoading { get; set; }
|
public bool IsLoading { get; set; }
|
||||||
|
|
||||||
@@ -462,10 +513,23 @@
|
|||||||
effect.IsOverridden = true;
|
effect.IsOverridden = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string GetAdvancedSummary(CriticalCellEditorModel model)
|
private static string GetAdvancedSummary(CriticalCellEditorModel model, CriticalCellEditorModel? comparisonBaseline)
|
||||||
{
|
{
|
||||||
|
var differenceCount = GetComparisonDifferenceCount(model, comparisonBaseline);
|
||||||
var noteCount = model.ValidationMessages.Count;
|
var noteCount = model.ValidationMessages.Count;
|
||||||
return noteCount == 0 ? "Parser metadata and save payload" : $"{noteCount} parser note{(noteCount == 1 ? string.Empty : "s")}";
|
var segments = new List<string>();
|
||||||
|
|
||||||
|
if (differenceCount > 0)
|
||||||
|
{
|
||||||
|
segments.Add($"{differenceCount} review change{(differenceCount == 1 ? string.Empty : "s")}");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (noteCount > 0)
|
||||||
|
{
|
||||||
|
segments.Add($"{noteCount} parser note{(noteCount == 1 ? string.Empty : "s")}");
|
||||||
|
}
|
||||||
|
|
||||||
|
return segments.Count == 0 ? "Generated compare and diagnostics" : string.Join(" · ", segments);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string GetParserNoteSummary(int noteCount) =>
|
private static string GetParserNoteSummary(int noteCount) =>
|
||||||
@@ -494,6 +558,135 @@
|
|||||||
private static string BuildCurrentSavePayloadJson(CriticalCellEditorModel model) =>
|
private static string BuildCurrentSavePayloadJson(CriticalCellEditorModel model) =>
|
||||||
JsonSerializer.Serialize(model.ToRequest(), DiagnosticJsonOptions);
|
JsonSerializer.Serialize(model.ToRequest(), DiagnosticJsonOptions);
|
||||||
|
|
||||||
|
private static IReadOnlyList<string> GetComparisonSummaryItems(CriticalCellEditorModel model, CriticalCellEditorModel? comparisonBaseline)
|
||||||
|
{
|
||||||
|
if (model.GeneratedState is null)
|
||||||
|
{
|
||||||
|
return ["Generated comparison is unavailable."];
|
||||||
|
}
|
||||||
|
|
||||||
|
var items = new List<string>();
|
||||||
|
|
||||||
|
if (DescriptionDiffers(model, comparisonBaseline))
|
||||||
|
{
|
||||||
|
items.Add("Result text differs");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (EffectsDiffer(model, comparisonBaseline))
|
||||||
|
{
|
||||||
|
items.Add("Base effects differ");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (BranchesDiffer(model, comparisonBaseline))
|
||||||
|
{
|
||||||
|
items.Add("Conditions differ");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (items.Count == 0)
|
||||||
|
{
|
||||||
|
items.Add("Current card matches the fresh parse");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (model.GeneratedState.ValidationMessages.Count > 0)
|
||||||
|
{
|
||||||
|
items.Add($"{model.GeneratedState.ValidationMessages.Count} parser note{(model.GeneratedState.ValidationMessages.Count == 1 ? string.Empty : "s")}");
|
||||||
|
}
|
||||||
|
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int GetComparisonDifferenceCount(CriticalCellEditorModel model, CriticalCellEditorModel? comparisonBaseline)
|
||||||
|
{
|
||||||
|
var count = 0;
|
||||||
|
|
||||||
|
if (DescriptionDiffers(model, comparisonBaseline))
|
||||||
|
{
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (EffectsDiffer(model, comparisonBaseline))
|
||||||
|
{
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (BranchesDiffer(model, comparisonBaseline))
|
||||||
|
{
|
||||||
|
count++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool HasComparisonDifferences(CriticalCellEditorModel? model, CriticalCellEditorModel? comparisonBaseline) =>
|
||||||
|
model is not null && GetComparisonDifferenceCount(model, comparisonBaseline) > 0;
|
||||||
|
|
||||||
|
private static bool DescriptionDiffers(CriticalCellEditorModel model, CriticalCellEditorModel? comparisonBaseline) =>
|
||||||
|
model.GeneratedState is not null &&
|
||||||
|
!string.Equals(
|
||||||
|
NormalizeDisplayText(GetComparisonSourceModel(model, comparisonBaseline).DescriptionText),
|
||||||
|
NormalizeDisplayText(model.GeneratedState.DescriptionText),
|
||||||
|
StringComparison.Ordinal);
|
||||||
|
|
||||||
|
private static bool EffectsDiffer(CriticalCellEditorModel model, CriticalCellEditorModel? comparisonBaseline) =>
|
||||||
|
model.GeneratedState is not null &&
|
||||||
|
SerializeComparisonValue(BuildPreviewEffects(GetComparisonSourceModel(model, comparisonBaseline)).Select(ProjectEffectForComparison).ToList()) !=
|
||||||
|
SerializeComparisonValue(model.GeneratedState.Effects.Select(ProjectEffectForComparison).ToList());
|
||||||
|
|
||||||
|
private static bool BranchesDiffer(CriticalCellEditorModel model, CriticalCellEditorModel? comparisonBaseline) =>
|
||||||
|
model.GeneratedState is not null &&
|
||||||
|
SerializeComparisonValue(BuildPreviewBranches(GetComparisonSourceModel(model, comparisonBaseline)).Select(ProjectBranchForComparison).ToList()) !=
|
||||||
|
SerializeComparisonValue(model.GeneratedState.Branches.Select(ProjectBranchForComparison).ToList());
|
||||||
|
|
||||||
|
private static string NormalizeDisplayText(string? value) =>
|
||||||
|
value?.Trim() ?? string.Empty;
|
||||||
|
|
||||||
|
private static string SerializeComparisonValue<TValue>(TValue value) =>
|
||||||
|
JsonSerializer.Serialize(value, DiagnosticJsonOptions);
|
||||||
|
|
||||||
|
private static object ProjectEffectForComparison(CriticalEffectLookupResponse effect) => new
|
||||||
|
{
|
||||||
|
effect.EffectCode,
|
||||||
|
effect.Target,
|
||||||
|
effect.ValueInteger,
|
||||||
|
effect.ValueExpression,
|
||||||
|
effect.DurationRounds,
|
||||||
|
effect.PerRound,
|
||||||
|
effect.Modifier,
|
||||||
|
effect.BodyPart,
|
||||||
|
effect.IsPermanent,
|
||||||
|
SourceText = NormalizeDisplayText(effect.SourceText)
|
||||||
|
};
|
||||||
|
|
||||||
|
private static object ProjectBranchForComparison(CriticalBranchLookupResponse branch) => new
|
||||||
|
{
|
||||||
|
Condition = NormalizeDisplayText(branch.ConditionText),
|
||||||
|
Description = NormalizeDisplayText(branch.Description),
|
||||||
|
Effects = branch.Effects
|
||||||
|
.Select(ProjectEffectForComparison)
|
||||||
|
.ToList()
|
||||||
|
};
|
||||||
|
|
||||||
|
private static CriticalCellEditorModel GetComparisonSourceModel(
|
||||||
|
CriticalCellEditorModel model,
|
||||||
|
CriticalCellEditorModel? comparisonBaseline) =>
|
||||||
|
comparisonBaseline ?? model;
|
||||||
|
|
||||||
|
private static string GetCurrentComparisonCaption(CriticalCellEditorModel? comparisonBaseline) =>
|
||||||
|
comparisonBaseline is null
|
||||||
|
? "This is the result that will be saved."
|
||||||
|
: "This is the edited card before the last re-parse.";
|
||||||
|
|
||||||
|
private static IReadOnlyList<CriticalEffectLookupResponse> BuildPreviewEffects(CriticalCellEditorModel model) =>
|
||||||
|
model.Effects
|
||||||
|
.Select(CreatePreviewEffect)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
|
private static IReadOnlyList<CriticalBranchLookupResponse> BuildPreviewBranches(CriticalCellEditorModel model) =>
|
||||||
|
model.Branches
|
||||||
|
.OrderBy(branch => branch.SortOrder)
|
||||||
|
.Select(CreatePreviewBranch)
|
||||||
|
.ToList();
|
||||||
|
|
||||||
private static List<(string Scope, string EffectLabel, string SourceType, string? SourceText)> GetEffectMetadataRows(CriticalCellEditorModel model)
|
private static List<(string Scope, string EffectLabel, string SourceType, string? SourceText)> GetEffectMetadataRows(CriticalCellEditorModel model)
|
||||||
{
|
{
|
||||||
var rows = new List<(string Scope, string EffectLabel, string SourceType, string? SourceText)>();
|
var rows = new List<(string Scope, string EffectLabel, string SourceType, string? SourceText)>();
|
||||||
@@ -541,6 +734,22 @@
|
|||||||
effect.IsPermanent,
|
effect.IsPermanent,
|
||||||
effect.SourceType,
|
effect.SourceType,
|
||||||
effect.SourceText);
|
effect.SourceText);
|
||||||
|
|
||||||
|
private static CriticalEffectLookupResponse CreatePreviewEffect(CriticalEffectEditorModel effect) =>
|
||||||
|
CreateBadgeEffect(effect);
|
||||||
|
|
||||||
|
private static CriticalBranchLookupResponse CreatePreviewBranch(CriticalBranchEditorModel branch) =>
|
||||||
|
new(
|
||||||
|
branch.BranchKind,
|
||||||
|
branch.ConditionKey,
|
||||||
|
branch.ConditionText,
|
||||||
|
branch.DescriptionText,
|
||||||
|
branch.RawAffixText,
|
||||||
|
branch.Effects
|
||||||
|
.Select(CreatePreviewEffect)
|
||||||
|
.ToList(),
|
||||||
|
branch.RawText,
|
||||||
|
branch.SortOrder);
|
||||||
}
|
}
|
||||||
|
|
||||||
@functions {
|
@functions {
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ public sealed class CriticalCellEditorModel
|
|||||||
public List<string> ValidationMessages { get; set; } = [];
|
public List<string> ValidationMessages { get; set; } = [];
|
||||||
public List<CriticalEffectEditorModel> Effects { get; set; } = [];
|
public List<CriticalEffectEditorModel> Effects { get; set; } = [];
|
||||||
public List<CriticalBranchEditorModel> Branches { get; set; } = [];
|
public List<CriticalBranchEditorModel> Branches { get; set; } = [];
|
||||||
|
public CriticalCellComparisonState? GeneratedState { get; set; }
|
||||||
|
|
||||||
public static CriticalCellEditorModel FromResponse(CriticalCellEditorResponse response) =>
|
public static CriticalCellEditorModel FromResponse(CriticalCellEditorResponse response) =>
|
||||||
new()
|
new()
|
||||||
@@ -52,7 +53,8 @@ public sealed class CriticalCellEditorModel
|
|||||||
AreBranchesOverridden = response.AreBranchesOverridden,
|
AreBranchesOverridden = response.AreBranchesOverridden,
|
||||||
ValidationMessages = response.ValidationMessages.ToList(),
|
ValidationMessages = response.ValidationMessages.ToList(),
|
||||||
Effects = response.Effects.Select(CriticalEffectEditorModel.FromItem).ToList(),
|
Effects = response.Effects.Select(CriticalEffectEditorModel.FromItem).ToList(),
|
||||||
Branches = response.Branches.Select(CriticalBranchEditorModel.FromItem).ToList()
|
Branches = response.Branches.Select(CriticalBranchEditorModel.FromItem).ToList(),
|
||||||
|
GeneratedState = response.GeneratedState
|
||||||
};
|
};
|
||||||
|
|
||||||
public CriticalCellUpdateRequest ToRequest()
|
public CriticalCellUpdateRequest ToRequest()
|
||||||
@@ -83,6 +85,34 @@ public sealed class CriticalCellEditorModel
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public CriticalCellEditorModel Clone() =>
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
ResultId = ResultId,
|
||||||
|
TableSlug = TableSlug,
|
||||||
|
TableName = TableName,
|
||||||
|
SourceDocument = SourceDocument,
|
||||||
|
RollBand = RollBand,
|
||||||
|
GroupKey = GroupKey,
|
||||||
|
GroupLabel = GroupLabel,
|
||||||
|
ColumnKey = ColumnKey,
|
||||||
|
ColumnLabel = ColumnLabel,
|
||||||
|
ColumnRole = ColumnRole,
|
||||||
|
RawCellText = RawCellText,
|
||||||
|
DescriptionText = DescriptionText,
|
||||||
|
RawAffixText = RawAffixText,
|
||||||
|
ParseStatus = ParseStatus,
|
||||||
|
ParsedJson = ParsedJson,
|
||||||
|
IsDescriptionOverridden = IsDescriptionOverridden,
|
||||||
|
IsRawAffixTextOverridden = IsRawAffixTextOverridden,
|
||||||
|
AreEffectsOverridden = AreEffectsOverridden,
|
||||||
|
AreBranchesOverridden = AreBranchesOverridden,
|
||||||
|
ValidationMessages = ValidationMessages.ToList(),
|
||||||
|
Effects = Effects.Select(effect => effect.Clone()).ToList(),
|
||||||
|
Branches = Branches.Select(branch => branch.Clone()).ToList(),
|
||||||
|
GeneratedState = GeneratedState
|
||||||
|
};
|
||||||
|
|
||||||
private static string ResolveParseStatus(
|
private static string ResolveParseStatus(
|
||||||
IReadOnlyList<CriticalEffectEditorModel> effects,
|
IReadOnlyList<CriticalEffectEditorModel> effects,
|
||||||
IReadOnlyList<CriticalBranchEditorModel> branches) =>
|
IReadOnlyList<CriticalBranchEditorModel> branches) =>
|
||||||
|
|||||||
@@ -54,4 +54,23 @@ public sealed class CriticalEffectEditorModel
|
|||||||
SourceText,
|
SourceText,
|
||||||
OriginKey,
|
OriginKey,
|
||||||
IsOverridden);
|
IsOverridden);
|
||||||
|
|
||||||
|
public CriticalEffectEditorModel Clone() =>
|
||||||
|
new()
|
||||||
|
{
|
||||||
|
EffectCode = EffectCode,
|
||||||
|
Target = Target,
|
||||||
|
ValueInteger = ValueInteger,
|
||||||
|
ValueDecimal = ValueDecimal,
|
||||||
|
ValueExpression = ValueExpression,
|
||||||
|
DurationRounds = DurationRounds,
|
||||||
|
PerRound = PerRound,
|
||||||
|
Modifier = Modifier,
|
||||||
|
BodyPart = BodyPart,
|
||||||
|
IsPermanent = IsPermanent,
|
||||||
|
SourceType = SourceType,
|
||||||
|
SourceText = SourceText,
|
||||||
|
OriginKey = OriginKey,
|
||||||
|
IsOverridden = IsOverridden
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,57 @@
|
|||||||
|
@using System
|
||||||
|
@using System.Collections.Generic
|
||||||
|
@using RolemasterDb.App.Features
|
||||||
|
|
||||||
|
<div class="critical-editor-compare-card">
|
||||||
|
<div class="critical-editor-compare-card-header">
|
||||||
|
<div>
|
||||||
|
<strong>@Title</strong>
|
||||||
|
@if (!string.IsNullOrWhiteSpace(Caption))
|
||||||
|
{
|
||||||
|
<p class="muted critical-editor-inline-copy">@Caption</p>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@if ((Effects?.Count ?? 0) == 0 && (Branches?.Count ?? 0) == 0 && string.IsNullOrWhiteSpace(Description))
|
||||||
|
{
|
||||||
|
<p class="muted">No visible result is available for this card.</p>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<CompactCriticalCell
|
||||||
|
Description="@Description"
|
||||||
|
Effects="Effects"
|
||||||
|
Branches="Branches" />
|
||||||
|
}
|
||||||
|
|
||||||
|
@if ((Notes?.Count ?? 0) > 0)
|
||||||
|
{
|
||||||
|
<div class="critical-editor-validation-list">
|
||||||
|
@foreach (var note in Notes!)
|
||||||
|
{
|
||||||
|
<p class="critical-editor-validation-item">@note</p>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
@code {
|
||||||
|
[Parameter, EditorRequired]
|
||||||
|
public string Title { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public string? Caption { get; set; }
|
||||||
|
|
||||||
|
[Parameter, EditorRequired]
|
||||||
|
public string Description { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public IReadOnlyList<CriticalEffectLookupResponse>? Effects { get; set; }
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public IReadOnlyList<CriticalBranchLookupResponse>? Branches { get; set; }
|
||||||
|
|
||||||
|
[Parameter]
|
||||||
|
public IReadOnlyList<string>? Notes { get; set; }
|
||||||
|
}
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
using System.Collections.Generic;
|
||||||
|
|
||||||
|
namespace RolemasterDb.App.Features;
|
||||||
|
|
||||||
|
public sealed record CriticalCellComparisonState(
|
||||||
|
string DescriptionText,
|
||||||
|
IReadOnlyList<CriticalEffectLookupResponse> Effects,
|
||||||
|
IReadOnlyList<CriticalBranchLookupResponse> Branches,
|
||||||
|
IReadOnlyList<string> ValidationMessages);
|
||||||
@@ -24,4 +24,5 @@ public sealed record CriticalCellEditorResponse(
|
|||||||
bool AreBranchesOverridden,
|
bool AreBranchesOverridden,
|
||||||
IReadOnlyList<string> ValidationMessages,
|
IReadOnlyList<string> ValidationMessages,
|
||||||
IReadOnlyList<CriticalEffectEditorItem> Effects,
|
IReadOnlyList<CriticalEffectEditorItem> Effects,
|
||||||
IReadOnlyList<CriticalBranchEditorItem> Branches);
|
IReadOnlyList<CriticalBranchEditorItem> Branches,
|
||||||
|
CriticalCellComparisonState? GeneratedState);
|
||||||
|
|||||||
@@ -290,7 +290,14 @@ public sealed class LookupService(IDbContextFactory<RolemasterDbContext> dbConte
|
|||||||
item => item.Id == resultId && item.CriticalTable.Slug == normalizedSlug,
|
item => item.Id == resultId && item.CriticalTable.Slug == normalizedSlug,
|
||||||
cancellationToken);
|
cancellationToken);
|
||||||
|
|
||||||
return result is null ? null : CreateCellEditorResponse(result);
|
if (result is null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var currentState = CreateCurrentEditorState(result);
|
||||||
|
var generatedContent = await ParseCriticalCellContentAsync(dbContext, result.CriticalTableId, currentState.RawCellText, cancellationToken);
|
||||||
|
return CreateCellEditorResponse(result, currentState, generatedContent.ValidationErrors, CreateComparisonState(generatedContent));
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<CriticalCellEditorResponse?> ReparseCriticalCellAsync(
|
public async Task<CriticalCellEditorResponse?> ReparseCriticalCellAsync(
|
||||||
@@ -322,7 +329,7 @@ public sealed class LookupService(IDbContextFactory<RolemasterDbContext> dbConte
|
|||||||
var content = SharedParsing.CriticalCellTextParser.Parse(currentState.RawCellText, affixLegend);
|
var content = SharedParsing.CriticalCellTextParser.Parse(currentState.RawCellText, affixLegend);
|
||||||
var generatedState = CreateGeneratedEditorState(content);
|
var generatedState = CreateGeneratedEditorState(content);
|
||||||
var mergedState = MergeGeneratedState(currentState, generatedState);
|
var mergedState = MergeGeneratedState(currentState, generatedState);
|
||||||
return CreateCellEditorResponse(result, mergedState, content.ValidationErrors);
|
return CreateCellEditorResponse(result, mergedState, content.ValidationErrors, CreateComparisonState(content));
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<CriticalCellEditorResponse?> UpdateCriticalCellAsync(
|
public async Task<CriticalCellEditorResponse?> UpdateCriticalCellAsync(
|
||||||
@@ -363,7 +370,8 @@ public sealed class LookupService(IDbContextFactory<RolemasterDbContext> dbConte
|
|||||||
|
|
||||||
await dbContext.SaveChangesAsync(cancellationToken);
|
await dbContext.SaveChangesAsync(cancellationToken);
|
||||||
|
|
||||||
return CreateCellEditorResponse(result, request, []);
|
var generatedContent = await ParseCriticalCellContentAsync(dbContext, result.CriticalTableId, request.RawCellText, cancellationToken);
|
||||||
|
return CreateCellEditorResponse(result, request, generatedContent.ValidationErrors, CreateComparisonState(generatedContent));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static IReadOnlyList<CriticalTableLegendEntry> BuildLegend(IReadOnlyList<CriticalTableCellDetail> cells)
|
private static IReadOnlyList<CriticalTableLegendEntry> BuildLegend(IReadOnlyList<CriticalTableCellDetail> cells)
|
||||||
@@ -424,16 +432,11 @@ public sealed class LookupService(IDbContextFactory<RolemasterDbContext> dbConte
|
|||||||
effect.SourceType,
|
effect.SourceType,
|
||||||
effect.SourceText);
|
effect.SourceText);
|
||||||
|
|
||||||
private static CriticalCellEditorResponse CreateCellEditorResponse(CriticalResult result)
|
|
||||||
{
|
|
||||||
var state = CreateCurrentEditorState(result);
|
|
||||||
return CreateCellEditorResponse(result, state, []);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static CriticalCellEditorResponse CreateCellEditorResponse(
|
private static CriticalCellEditorResponse CreateCellEditorResponse(
|
||||||
CriticalResult result,
|
CriticalResult result,
|
||||||
CriticalCellUpdateRequest state,
|
CriticalCellUpdateRequest state,
|
||||||
IReadOnlyList<string> validationMessages)
|
IReadOnlyList<string> validationMessages,
|
||||||
|
CriticalCellComparisonState? generatedState)
|
||||||
{
|
{
|
||||||
var snapshotJson = CriticalCellEditorSnapshot.FromRequest(state).ToJson();
|
var snapshotJson = CriticalCellEditorSnapshot.FromRequest(state).ToJson();
|
||||||
|
|
||||||
@@ -459,7 +462,8 @@ public sealed class LookupService(IDbContextFactory<RolemasterDbContext> dbConte
|
|||||||
state.AreBranchesOverridden,
|
state.AreBranchesOverridden,
|
||||||
validationMessages.ToList(),
|
validationMessages.ToList(),
|
||||||
state.Effects.ToList(),
|
state.Effects.ToList(),
|
||||||
state.Branches.ToList());
|
state.Branches.ToList(),
|
||||||
|
generatedState);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static CriticalBranchLookupResponse CreateBranchLookupResponse(CriticalBranch branch) =>
|
private static CriticalBranchLookupResponse CreateBranchLookupResponse(CriticalBranch branch) =>
|
||||||
@@ -521,6 +525,18 @@ public sealed class LookupService(IDbContextFactory<RolemasterDbContext> dbConte
|
|||||||
.ToList());
|
.ToList());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static CriticalCellComparisonState CreateComparisonState(SharedParsing.CriticalCellParseContent content) =>
|
||||||
|
new(
|
||||||
|
content.DescriptionText,
|
||||||
|
content.Effects
|
||||||
|
.Select(CreateEffectLookupResponse)
|
||||||
|
.ToList(),
|
||||||
|
content.Branches
|
||||||
|
.OrderBy(branch => branch.SortOrder)
|
||||||
|
.Select(CreateBranchLookupResponse)
|
||||||
|
.ToList(),
|
||||||
|
content.ValidationErrors.ToList());
|
||||||
|
|
||||||
private static CriticalEffectEditorItem CreateEffectEditorItem(CriticalEffect effect, string originKey) =>
|
private static CriticalEffectEditorItem CreateEffectEditorItem(CriticalEffect effect, string originKey) =>
|
||||||
new(
|
new(
|
||||||
effect.EffectCode,
|
effect.EffectCode,
|
||||||
@@ -806,6 +822,33 @@ public sealed class LookupService(IDbContextFactory<RolemasterDbContext> dbConte
|
|||||||
SourceText = NormalizeOptionalText(effect.SourceText)
|
SourceText = NormalizeOptionalText(effect.SourceText)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private static CriticalEffectLookupResponse CreateEffectLookupResponse(SharedParsing.ParsedCriticalEffect effect) =>
|
||||||
|
new(
|
||||||
|
effect.EffectCode,
|
||||||
|
effect.Target,
|
||||||
|
effect.ValueInteger,
|
||||||
|
effect.ValueExpression,
|
||||||
|
effect.DurationRounds,
|
||||||
|
effect.PerRound,
|
||||||
|
effect.Modifier,
|
||||||
|
effect.BodyPart,
|
||||||
|
effect.IsPermanent,
|
||||||
|
effect.SourceType,
|
||||||
|
effect.SourceText);
|
||||||
|
|
||||||
|
private static CriticalBranchLookupResponse CreateBranchLookupResponse(SharedParsing.ParsedCriticalBranch branch) =>
|
||||||
|
new(
|
||||||
|
branch.BranchKind,
|
||||||
|
branch.ConditionKey,
|
||||||
|
branch.ConditionText,
|
||||||
|
branch.DescriptionText,
|
||||||
|
branch.RawAffixText,
|
||||||
|
branch.Effects
|
||||||
|
.Select(CreateEffectLookupResponse)
|
||||||
|
.ToList(),
|
||||||
|
branch.RawText,
|
||||||
|
branch.SortOrder);
|
||||||
|
|
||||||
private static string ResolveParseStatus(
|
private static string ResolveParseStatus(
|
||||||
IReadOnlyList<SharedParsing.ParsedCriticalEffect> effects,
|
IReadOnlyList<SharedParsing.ParsedCriticalEffect> effects,
|
||||||
IReadOnlyList<SharedParsing.ParsedCriticalBranch> branches) =>
|
IReadOnlyList<SharedParsing.ParsedCriticalBranch> branches) =>
|
||||||
@@ -876,6 +919,16 @@ public sealed class LookupService(IDbContextFactory<RolemasterDbContext> dbConte
|
|||||||
supportsPowerPointModifier);
|
supportsPowerPointModifier);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static async Task<SharedParsing.CriticalCellParseContent> ParseCriticalCellContentAsync(
|
||||||
|
RolemasterDbContext dbContext,
|
||||||
|
int tableId,
|
||||||
|
string rawCellText,
|
||||||
|
CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
var affixLegend = await BuildSharedAffixLegendAsync(dbContext, tableId, cancellationToken);
|
||||||
|
return SharedParsing.CriticalCellTextParser.Parse(rawCellText, affixLegend);
|
||||||
|
}
|
||||||
|
|
||||||
private static bool IsLegendSymbolEffectCode(string effectCode) =>
|
private static bool IsLegendSymbolEffectCode(string effectCode) =>
|
||||||
effectCode is CriticalEffectCodes.MustParryRounds
|
effectCode is CriticalEffectCodes.MustParryRounds
|
||||||
or CriticalEffectCodes.NoParryRounds
|
or CriticalEffectCodes.NoParryRounds
|
||||||
|
|||||||
@@ -834,6 +834,25 @@ textarea {
|
|||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.critical-editor-compare-grid {
|
||||||
|
display: grid;
|
||||||
|
gap: 0.85rem;
|
||||||
|
grid-template-columns: repeat(auto-fit, minmax(240px, 1fr));
|
||||||
|
}
|
||||||
|
|
||||||
|
.critical-editor-compare-card {
|
||||||
|
display: grid;
|
||||||
|
gap: 0.75rem;
|
||||||
|
padding: 0.9rem;
|
||||||
|
border-radius: 16px;
|
||||||
|
background: rgba(255, 255, 255, 0.72);
|
||||||
|
border: 1px solid rgba(127, 96, 55, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.critical-editor-compare-card .critical-cell {
|
||||||
|
min-height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
.critical-editor-inline-list {
|
.critical-editor-inline-list {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 0.55rem;
|
gap: 0.55rem;
|
||||||
|
|||||||
Reference in New Issue
Block a user