fix(adminui): CSV import grid shows the raw name/group of an invalid row (#847)

This commit is contained in:
Joseph Doherty
2026-11-11 03:41:54 -05:00
parent 481985e20b
commit 01a2013ea3
3 changed files with 50 additions and 10 deletions
@@ -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,