feat(ui): List attribute editor in TemplateEdit

This commit is contained in:
Joseph Doherty
2026-06-16 16:20:08 -04:00
parent 85db4571b2
commit ba7331e67c
3 changed files with 355 additions and 7 deletions
@@ -4,6 +4,7 @@
@using ZB.MOM.WW.ScadaBridge.Commons.Entities.Templates
@using ZB.MOM.WW.ScadaBridge.Commons.Entities.Sites
@using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Repositories
@using ZB.MOM.WW.ScadaBridge.Commons.Types
@using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums
@using ZB.MOM.WW.ScadaBridge.TemplateEngine
@using ZB.MOM.WW.ScadaBridge.TemplateEngine.Services
@@ -80,6 +81,10 @@
private string _attrName = string.Empty;
private string? _attrValue;
private DataType _attrDataType;
// List-attribute authoring state (DataType.List only): the element scalar
// type + the per-element string rows. Encoded to canonical JSON on submit.
private DataType _attrElementDataType = DataType.String;
private List<string> _attrListRows = new();
private bool _attrIsLocked;
private string? _attrDataSourceRef;
private string? _attrFormError;
@@ -553,17 +558,32 @@
</div>
<div class="col-12">
<label class="form-label">Data Type</label>
<select class="form-select" @bind="_attrDataType">
<select class="form-select" value="@_attrDataType" @onchange="OnAttrDataTypeChanged" disabled="@editing">
@foreach (var dt in Enum.GetValues<DataType>())
{
<option value="@dt">@dt</option>
}
</select>
@if (editing)
{
<div class="form-text">Data type is fixed once the attribute is created.</div>
}
</div>
<div class="col-12">
<label class="form-label">Value</label>
<input type="text" class="form-control" @bind="_attrValue" />
</div>
@if (_attrDataType == DataType.List)
{
<div class="col-12">
<AttributeListEditor @bind-ElementDataType="_attrElementDataType"
@bind-Rows="_attrListRows"
ShowElementType="@(!editing)" />
</div>
}
else
{
<div class="col-12">
<label class="form-label">Value</label>
<input type="text" class="form-control" @bind="_attrValue" />
</div>
}
<div class="col-12">
<label class="form-label">Data Source Ref</label>
<input type="text" class="form-control" @bind="_attrDataSourceRef" placeholder="Tag path" />
@@ -1535,6 +1555,8 @@
_attrName = string.Empty;
_attrValue = null;
_attrDataType = default;
_attrElementDataType = DataType.String;
_attrListRows = new();
_attrIsLocked = false;
_attrDataSourceRef = null;
}
@@ -1547,6 +1569,8 @@
_attrName = attr.Name;
_attrValue = attr.Value;
_attrDataType = attr.DataType;
_attrElementDataType = attr.ElementDataType ?? DataType.String;
_attrListRows = DecodeListRows(attr.Value, attr.ElementDataType);
_attrIsLocked = attr.IsLocked;
_attrDataSourceRef = attr.DataSourceReference;
}
@@ -1558,12 +1582,72 @@
_attrFormError = null;
}
// Switching the data type clears stale list state so a List ⇄ scalar
// toggle never carries the other mode's value into the submit.
private void OnAttrDataTypeChanged(ChangeEventArgs e)
{
if (!Enum.TryParse<DataType>((string?)e.Value, out var dt) || dt == _attrDataType) return;
_attrDataType = dt;
if (dt == DataType.List)
{
_attrValue = null;
if (_attrListRows.Count == 0) _attrListRows = new();
if (!AttributeValueCodec.IsValidElementType(_attrElementDataType))
_attrElementDataType = DataType.String;
}
else
{
_attrListRows = new();
}
}
// Decodes a stored List JSON value into editable string rows. A malformed
// stored value (e.g. hand-edited / element-type mismatch) is shown as empty
// rather than crashing the editor — the user can rebuild it.
private List<string> DecodeListRows(string? value, DataType? elementType)
{
if (string.IsNullOrEmpty(value)) return new();
try
{
var decoded = AttributeValueCodec.Decode(value, DataType.List, elementType ?? DataType.String);
if (decoded is System.Collections.IEnumerable items)
return items.Cast<object?>()
.Select(x => AttributeValueCodec.Encode(x) ?? string.Empty)
.ToList();
}
catch (FormatException)
{
// Malformed stored value — start from empty so the editor still opens.
}
return new();
}
private async Task SaveAttribute()
{
if (_selectedTemplate == null) return;
_attrFormError = null;
if (string.IsNullOrWhiteSpace(_attrName)) { _attrFormError = "Name is required."; return; }
// Resolve the value + element type per data type. List attributes encode
// their rows to canonical JSON and validate them locally before submit
// (TemplateService persists directly and does not list-validate).
string? attrValue;
DataType? elementType;
if (_attrDataType == DataType.List)
{
elementType = _attrElementDataType;
attrValue = AttributeValueCodec.Encode(_attrListRows);
// Round-trip through Decode to surface any un-parseable element
// (e.g. non-numeric in an Int32 list) before hitting the server.
try { AttributeValueCodec.Decode(attrValue, DataType.List, elementType); }
catch (FormatException ex) { _attrFormError = ex.Message; return; }
}
else
{
elementType = null;
attrValue = _attrValue?.Trim();
}
var user = await GetCurrentUserAsync();
if (_editAttrId is int id)
@@ -1573,7 +1657,8 @@
var proposed = new TemplateAttribute(existing.Name)
{
DataType = _attrDataType,
Value = _attrValue?.Trim(),
ElementDataType = elementType,
Value = attrValue,
IsLocked = _attrIsLocked,
DataSourceReference = _attrDataSourceRef?.Trim(),
Description = existing.Description,
@@ -1598,7 +1683,8 @@
var attr = new TemplateAttribute(_attrName.Trim())
{
DataType = _attrDataType,
Value = _attrValue?.Trim(),
ElementDataType = elementType,
Value = attrValue,
IsLocked = _attrIsLocked,
DataSourceReference = _attrDataSourceRef?.Trim()
};