fix(adminui): CSV import grid shows the raw name/group of an invalid row (#847)
This commit is contained in:
@@ -74,7 +74,7 @@
|
||||
<thead class="sticky-top bg-body">
|
||||
<tr>
|
||||
<th style="width:2.5rem"></th>
|
||||
<th>Row</th>
|
||||
<th title="Data-row number in the file (the header is row 0)">Row</th>
|
||||
<th>Name</th>
|
||||
<th>Group</th>
|
||||
<th>Verdict</th>
|
||||
@@ -89,7 +89,16 @@
|
||||
<tr @key="row.RowNumber" class="@(row.Ok ? "" : "table-danger")">
|
||||
<td>@(row.Ok ? "✓" : "✕")</td>
|
||||
<td class="mono">@row.RowNumber</td>
|
||||
<td class="mono">@row.Name</td>
|
||||
<td class="mono">
|
||||
@if (row.Name.Length == 0)
|
||||
{
|
||||
<span class="text-muted fst-italic">(blank name)</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
@row.Name
|
||||
}
|
||||
</td>
|
||||
<td class="mono small">@(row.EffectiveGroupPath.Length == 0 ? "(device root)" : row.EffectiveGroupPath)</td>
|
||||
<td>
|
||||
@if (row.Ok)
|
||||
|
||||
@@ -128,19 +128,24 @@ public static class RawTagCsvMapper
|
||||
var typedUsed = new List<string>();
|
||||
var overrodeFallback = new List<string>();
|
||||
|
||||
// Echoed onto an INVALID row too (lmxopcua#847): the grid's Name/Group columns used to render blank for
|
||||
// a rejected row, so the operator could not tell which tag failed. The raw cell is shown as typed.
|
||||
var rawName = Cell("Name");
|
||||
var rawGroup = CombineGroupPath(groupPathPrefix, Cell("TagGroupPath")) ?? "";
|
||||
|
||||
try
|
||||
{
|
||||
// --- Common identity/access fields ---
|
||||
var name = Cell("Name");
|
||||
if (RawPaths.ValidateSegment(name) is { } nameErr)
|
||||
{
|
||||
return Invalid(rowNumber, cells, $"Name: {nameErr}");
|
||||
return Invalid(rowNumber, cells, rawName, rawGroup, $"Name: {nameErr}");
|
||||
}
|
||||
|
||||
var dataType = Cell("DataType");
|
||||
if (dataType.Length == 0)
|
||||
{
|
||||
return Invalid(rowNumber, cells, "DataType is required.");
|
||||
return Invalid(rowNumber, cells, rawName, rawGroup, "DataType is required.");
|
||||
}
|
||||
|
||||
var accessRaw = Cell("AccessLevel");
|
||||
@@ -161,7 +166,7 @@ public static class RawTagCsvMapper
|
||||
var baseKeys = ReadRootKeys(baseJson.Length == 0 ? "{}" : baseJson, out var baseValid);
|
||||
if (!baseValid)
|
||||
{
|
||||
return Invalid(rowNumber, cells, "TagConfigJson is not well-formed JSON.");
|
||||
return Invalid(rowNumber, cells, rawName, rawGroup, "TagConfigJson is not well-formed JSON.");
|
||||
}
|
||||
|
||||
if (map is not null)
|
||||
@@ -219,7 +224,7 @@ public static class RawTagCsvMapper
|
||||
var n = CsvValues.ParseInt("ArrayLength", arrayLenRaw);
|
||||
if (n < 0)
|
||||
{
|
||||
return Invalid(rowNumber, cells, "ArrayLength must not be negative.");
|
||||
return Invalid(rowNumber, cells, rawName, rawGroup, "ArrayLength must not be negative.");
|
||||
}
|
||||
|
||||
arrayLength = (uint)n;
|
||||
@@ -243,14 +248,14 @@ public static class RawTagCsvMapper
|
||||
// claim the tag is already present.
|
||||
if (TagConfigValidator.Validate(driverType, json) is { } configError)
|
||||
{
|
||||
return Invalid(rowNumber, cells, $"TagConfig: {configError}");
|
||||
return Invalid(rowNumber, cells, rawName, rawGroup, $"TagConfig: {configError}");
|
||||
}
|
||||
|
||||
// The driver's Tag-ROW rules (access level / data type — lmxopcua#830): the SAME authority the commit
|
||||
// (RawTreeService.ImportTagsAsync) applies, so a row the commit would refuse is never previewed as OK.
|
||||
if (RawTagRowRules.Problem(driverType, access, dataType, json) is { } rowError)
|
||||
{
|
||||
return Invalid(rowNumber, cells, rowError);
|
||||
return Invalid(rowNumber, cells, rawName, rawGroup, rowError);
|
||||
}
|
||||
|
||||
var importRow = new RawTagImportRow(groupPath, new RawTagInput(name, dataType, access, writeIdempotent, pollGroupId, json));
|
||||
@@ -271,13 +276,15 @@ public static class RawTagCsvMapper
|
||||
}
|
||||
catch (FormatException ex)
|
||||
{
|
||||
return Invalid(rowNumber, cells, ex.Message);
|
||||
return Invalid(rowNumber, cells, rawName, rawGroup, ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
private static RawCsvReviewRow Invalid(int rowNumber, string[] cells, string error) => new()
|
||||
private static RawCsvReviewRow Invalid(int rowNumber, string[] cells, string rawName, string rawGroup, string error) => new()
|
||||
{
|
||||
RowNumber = rowNumber,
|
||||
Name = rawName,
|
||||
EffectiveGroupPath = rawGroup,
|
||||
Cells = cells,
|
||||
Headers = Array.Empty<string>(),
|
||||
Error = error,
|
||||
|
||||
@@ -47,6 +47,30 @@ public sealed class HeidenhainAuthoringUxTests
|
||||
result.CanCommit.ShouldBeFalse();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Csv_preview_invalid_rows_echo_the_raw_name_and_group_847()
|
||||
{
|
||||
// lmxopcua#847: an invalid row rendered a BLANK Name in the review grid, so the operator could not tell
|
||||
// which tag failed. Every rejection path must carry the raw Name/group cell as typed.
|
||||
const string header = "Name,DataType,AccessLevel,TagGroupPath,TagConfigJson";
|
||||
var exec = Json("""{"address":"Status/ExecutionState"}""");
|
||||
var result = RawTagCsvMapper.Parse(DriverTypeNames.Heidenhain,
|
||||
header + "\n" + string.Join("\n",
|
||||
$"rw,Int32,ReadWrite,Spindle,{exec}", // row rule refusal
|
||||
$"nodt,,Read,,{exec}", // DataType required
|
||||
$"badjson,Int32,Read,,{Json("{not json")}",
|
||||
$"bad/name,Int32,Read,,{exec}", // name segment refusal
|
||||
$"badacc,Int32,Bogus,,{exec}"), // FormatException path
|
||||
groupPathPrefix: "Axes");
|
||||
|
||||
result.Rows.Count.ShouldBe(5);
|
||||
result.Rows.ShouldAllBe(r => !r.Ok);
|
||||
result.Rows.Select(r => r.Name).ShouldBe(new[] { "rw", "nodt", "badjson", "bad/name", "badacc" });
|
||||
result.Rows[0].EffectiveGroupPath.ShouldBe("Axes/Spindle");
|
||||
result.Rows[1].EffectiveGroupPath.ShouldBe("Axes");
|
||||
result.Rows.Select(r => r.RowNumber).ShouldBe(new[] { 1, 2, 3, 4, 5 });
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Csv_preview_and_ImportTags_agree_row_for_row()
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user