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
@@ -0,0 +1,130 @@
@using ZB.MOM.WW.ScadaBridge.Commons.Types
@using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums
@*
Repeatable list-value editor for a structured multi-value (List) attribute.
Reveals an element-type <select> (the six valid scalar types) plus one text
input per element with add/remove controls. Binds a List<string> working
model; the host encodes it to canonical JSON via AttributeValueCodec.Encode.
MV-14 reuses this component for instance overrides. Public API:
- ElementDataType (two-way, bound) : the chosen element scalar.
- Rows (two-way, bound) : the per-element string values.
- ShowElementType (default true) : hide the type <select> when the
element type is fixed (e.g. an
instance override inherits it).
- Disabled (default false) : render read-only.
Both ElementDataTypeChanged and RowsChanged fire on every edit.
*@
<div class="attribute-list-editor">
@if (ShowElementType)
{
<div class="mb-2">
<label class="form-label">Element Type</label>
<select class="form-select" value="@ElementDataType" @onchange="OnElementTypeChanged" disabled="@Disabled">
@foreach (var dt in ElementTypes)
{
<option value="@dt">@dt</option>
}
</select>
</div>
}
<label class="form-label">List Values</label>
@if (Rows.Count == 0)
{
<p class="text-muted small mb-2">No elements. Use “Add element” to add one.</p>
}
else
{
<div class="d-flex flex-column gap-2 mb-2">
@for (var i = 0; i < Rows.Count; i++)
{
var index = i;
<div class="input-group input-group-sm">
<span class="input-group-text" style="min-width: 2.5rem;">@index</span>
<input type="text" class="form-control font-monospace"
value="@Rows[index]"
placeholder="@Placeholder()"
@oninput="e => OnRowInput(index, (string?)e.Value)"
disabled="@Disabled" />
@if (!Disabled)
{
<button type="button" class="btn btn-outline-danger"
aria-label="@($"Remove element {index}")"
@onclick="() => RemoveRow(index)">Remove</button>
}
</div>
}
</div>
}
@if (!Disabled)
{
<button type="button" class="btn btn-outline-secondary btn-sm" @onclick="AddRow">Add element</button>
}
</div>
@code {
/// <summary>The chosen element scalar type. Two-way bound.</summary>
[Parameter] public DataType ElementDataType { get; set; } = DataType.String;
[Parameter] public EventCallback<DataType> ElementDataTypeChanged { get; set; }
/// <summary>The per-element string values. Two-way bound.</summary>
[Parameter] public List<string> Rows { get; set; } = new();
[Parameter] public EventCallback<List<string>> RowsChanged { get; set; }
/// <summary>
/// When false, the element-type <select> is hidden — used where the element
/// type is fixed by the base attribute (e.g. instance overrides in MV-14).
/// </summary>
[Parameter] public bool ShowElementType { get; set; } = true;
/// <summary>Render every control read-only.</summary>
[Parameter] public bool Disabled { get; set; }
private static readonly DataType[] ElementTypes =
Enum.GetValues<DataType>()
.Where(AttributeValueCodec.IsValidElementType)
.ToArray();
private async Task OnElementTypeChanged(ChangeEventArgs e)
{
if (Enum.TryParse<DataType>((string?)e.Value, out var dt) && dt != ElementDataType)
{
ElementDataType = dt;
await ElementDataTypeChanged.InvokeAsync(dt);
}
}
private async Task OnRowInput(int index, string? value)
{
if (index < 0 || index >= Rows.Count) return;
Rows[index] = value ?? string.Empty;
await RowsChanged.InvokeAsync(Rows);
}
private async Task AddRow()
{
Rows.Add(string.Empty);
await RowsChanged.InvokeAsync(Rows);
}
private async Task RemoveRow(int index)
{
if (index < 0 || index >= Rows.Count) return;
Rows.RemoveAt(index);
await RowsChanged.InvokeAsync(Rows);
}
private string Placeholder() => ElementDataType switch
{
DataType.Int32 => "e.g. 42",
DataType.Float => "e.g. 3.14",
DataType.Double => "e.g. 3.14159",
DataType.Boolean => "true / false",
DataType.DateTime => "e.g. 2026-06-16T00:00:00Z",
_ => "text value"
};
}