feat(ui): List attribute override editor in InstanceConfigure
When overriding a List attribute, render the shared AttributeListEditor (whole-list replacement; element type fixed by the base, shown read-only via ShowElementType=false) instead of the single-line input. Loading an existing override decodes its JSON into rows (malformed -> empty); saving encodes rows to canonical JSON with a pre-submit Decode round-trip guard surfacing element errors inline. Clearing removes the InstanceAttributeOverride row (repository-direct, mirroring native-alarm-source overrides). Non-List override UX unchanged.
This commit is contained in:
+176
-10
@@ -5,6 +5,7 @@
|
||||
@using ZB.MOM.WW.ScadaBridge.Commons.Entities.Templates
|
||||
@using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Repositories
|
||||
@using ZB.MOM.WW.ScadaBridge.Commons.Messages.Management
|
||||
@using ZB.MOM.WW.ScadaBridge.Commons.Types
|
||||
@using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums
|
||||
@using ZB.MOM.WW.ScadaBridge.TemplateEngine.Flattening
|
||||
@using ZB.MOM.WW.ScadaBridge.TemplateEngine.Services
|
||||
@@ -184,26 +185,63 @@
|
||||
}
|
||||
else
|
||||
{
|
||||
<table class="table table-sm table-bordered mb-0">
|
||||
<table class="table table-sm table-bordered mb-0 align-middle">
|
||||
<thead class="table-light">
|
||||
<tr>
|
||||
<th>Attribute</th>
|
||||
<th>Type</th>
|
||||
<th>Template Value</th>
|
||||
<th style="width: 280px;">Override Value</th>
|
||||
<th style="width: 320px;">Override Value</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@foreach (var attr in _overrideAttrs)
|
||||
{
|
||||
<tr>
|
||||
<tr data-test="override-row-@attr.Name">
|
||||
<td class="small">@attr.Name</td>
|
||||
<td><span class="badge bg-light text-dark">@attr.DataType</span></td>
|
||||
<td>
|
||||
<span class="badge bg-light text-dark">@attr.DataType</span>
|
||||
@if (attr.DataType == DataType.List)
|
||||
{
|
||||
@* Element type is fixed by the base attribute — shown
|
||||
read-only here (the List editor renders it hidden via
|
||||
ShowElementType="false"). *@
|
||||
<span class="badge bg-light text-dark border ms-1"
|
||||
data-test="override-element-type">
|
||||
of @(attr.ElementDataType ?? DataType.String)
|
||||
</span>
|
||||
}
|
||||
</td>
|
||||
<td class="small text-muted">@(attr.Value ?? "—")</td>
|
||||
<td>
|
||||
<input type="text" class="form-control form-control-sm"
|
||||
value="@GetOverrideValue(attr.Name)"
|
||||
@onchange="(e) => OnOverrideChanged(attr.Name, e)" />
|
||||
@if (attr.DataType == DataType.List)
|
||||
{
|
||||
@* Whole-list replacement: the shared editor renders the
|
||||
element-type select hidden (fixed by the base) plus the
|
||||
repeatable rows. Clearing removes the override row. *@
|
||||
<AttributeListEditor ElementDataType="@(attr.ElementDataType ?? DataType.String)"
|
||||
Rows="@GetListRows(attr.Name)"
|
||||
RowsChanged="@(r => OnListRowsChanged(attr.Name, r))"
|
||||
ShowElementType="false" />
|
||||
@if (_overrideErrors.TryGetValue(attr.Name, out var listErr))
|
||||
{
|
||||
<div class="alert alert-danger small mt-2 mb-0"
|
||||
data-test="override-list-error">@listErr</div>
|
||||
}
|
||||
@if (HasOverrideRow(attr.Name))
|
||||
{
|
||||
<button class="btn btn-outline-danger btn-sm mt-2"
|
||||
data-test="override-clear-btn"
|
||||
@onclick="() => ClearListOverride(attr.Name)"
|
||||
disabled="@_saving">Clear Override</button>
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
<input type="text" class="form-control form-control-sm"
|
||||
value="@GetOverrideValue(attr.Name)"
|
||||
@onchange="(e) => OnOverrideChanged(attr.Name, e)" />
|
||||
}
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
@@ -507,6 +545,17 @@
|
||||
// Overrides
|
||||
private List<TemplateAttribute> _overrideAttrs = new();
|
||||
private Dictionary<string, string?> _overrideValues = new();
|
||||
// MV-14: existing override rows keyed by attribute name — tracks which
|
||||
// attributes already have a persisted override (so List rows know whether a
|
||||
// Clear is available) and carries the row Id for repository-direct delete.
|
||||
private Dictionary<string, InstanceAttributeOverride> _existingOverrides = new();
|
||||
// MV-14: per-List working rows (whole-list replacement), keyed by attribute
|
||||
// name. Seeded on load from the effective value; encoded to canonical JSON on
|
||||
// save. Element type is fixed by the base attribute.
|
||||
private Dictionary<string, List<string>> _listRows = new();
|
||||
// MV-14: per-attribute validation errors surfaced inline (e.g. an
|
||||
// un-parseable List element caught on the pre-submit round-trip).
|
||||
private Dictionary<string, string> _overrideErrors = new();
|
||||
|
||||
// Alarm overrides — read-only state pulled from the repo. The edit modal
|
||||
// is the only mutation path (one alarm at a time).
|
||||
@@ -595,7 +644,23 @@
|
||||
_overrideAttrs = attrs.Where(a => !a.IsLocked).ToList();
|
||||
var existingOverrides = await TemplateEngineRepository.GetOverridesByInstanceIdAsync(Id);
|
||||
foreach (var o in existingOverrides)
|
||||
{
|
||||
_overrideValues[o.AttributeName] = o.OverrideValue;
|
||||
_existingOverrides[o.AttributeName] = o;
|
||||
}
|
||||
|
||||
// MV-14: seed the per-List working rows. A List attribute's editor is
|
||||
// initialized from the effective value — the existing override JSON if
|
||||
// present, otherwise the template default — decoded into string rows
|
||||
// using the element type fixed by the base attribute. A malformed
|
||||
// stored value falls back to empty rows (the editor still opens).
|
||||
foreach (var attr in _overrideAttrs.Where(a => a.DataType == DataType.List))
|
||||
{
|
||||
var effective = _existingOverrides.TryGetValue(attr.Name, out var ovr)
|
||||
? ovr.OverrideValue
|
||||
: attr.Value;
|
||||
_listRows[attr.Name] = DecodeListRows(effective, attr.ElementDataType);
|
||||
}
|
||||
|
||||
// Alarm overrides — load all non-locked template alarms and
|
||||
// existing override rows. Pre-seed the dirty maps from existing
|
||||
@@ -805,15 +870,116 @@
|
||||
else _overrideValues[attrName] = val;
|
||||
}
|
||||
|
||||
// ── MV-14: structured List (multi-value) overrides ──────────
|
||||
|
||||
/// <summary>Working rows for a List attribute's override (whole-list replacement).</summary>
|
||||
private List<string> GetListRows(string attrName)
|
||||
=> _listRows.TryGetValue(attrName, out var rows) ? rows : (_listRows[attrName] = new());
|
||||
|
||||
private void OnListRowsChanged(string attrName, List<string> rows)
|
||||
{
|
||||
_listRows[attrName] = rows;
|
||||
// A fresh edit clears any stale validation error for this attribute.
|
||||
_overrideErrors.Remove(attrName);
|
||||
}
|
||||
|
||||
/// <summary>True if a persisted override row exists for the attribute (so Clear is offered).</summary>
|
||||
private bool HasOverrideRow(string attrName) => _existingOverrides.ContainsKey(attrName);
|
||||
|
||||
/// <summary>
|
||||
/// Decodes a stored List JSON value into editable string rows using the
|
||||
/// element type fixed by the base attribute. A malformed stored value (e.g.
|
||||
/// hand-edited or an element-type mismatch) falls back to empty rows rather
|
||||
/// than crashing the editor — mirrors TemplateEdit.DecodeListRows.
|
||||
/// </summary>
|
||||
private static 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();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Removes a List attribute's override row entirely (repository-direct, the
|
||||
/// same pattern as native-alarm-source overrides) and resets the editor to
|
||||
/// the inherited template value.
|
||||
/// </summary>
|
||||
private async Task ClearListOverride(string attrName)
|
||||
{
|
||||
_saving = true;
|
||||
try
|
||||
{
|
||||
if (_existingOverrides.TryGetValue(attrName, out var ovr))
|
||||
{
|
||||
await TemplateEngineRepository.DeleteInstanceAttributeOverrideAsync(ovr.Id);
|
||||
await TemplateEngineRepository.SaveChangesAsync();
|
||||
_existingOverrides.Remove(attrName);
|
||||
}
|
||||
_overrideValues.Remove(attrName);
|
||||
_overrideErrors.Remove(attrName);
|
||||
|
||||
// Reset the editor to the inherited template default.
|
||||
var attr = _overrideAttrs.FirstOrDefault(a => a.Name == attrName);
|
||||
_listRows[attrName] = DecodeListRows(attr?.Value, attr?.ElementDataType);
|
||||
_toast.ShowSuccess($"Cleared override on '{attrName}'.");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_toast.ShowError($"Clear failed: {ex.Message}");
|
||||
}
|
||||
_saving = false;
|
||||
}
|
||||
|
||||
private async Task SaveOverrides()
|
||||
{
|
||||
_saving = true;
|
||||
try
|
||||
{
|
||||
_overrideErrors.Clear();
|
||||
var user = await GetCurrentUserAsync();
|
||||
foreach (var (attrName, value) in _overrideValues)
|
||||
await InstanceService.SetAttributeOverrideAsync(Id, attrName, value, user);
|
||||
_toast.ShowSuccess($"Saved {_overrideValues.Count} override(s).");
|
||||
|
||||
// Build the set of override values to persist. Scalars come straight
|
||||
// from the single-input map (unchanged). List attributes encode their
|
||||
// working rows to canonical JSON; each is round-tripped through Decode
|
||||
// first to surface any un-parseable element (mirrors TemplateEdit) —
|
||||
// an invalid element aborts the whole save and is shown inline.
|
||||
var toSave = new Dictionary<string, string?>(_overrideValues);
|
||||
var listAttrs = _overrideAttrs.Where(a => a.DataType == DataType.List).ToList();
|
||||
var hasError = false;
|
||||
foreach (var attr in listAttrs)
|
||||
{
|
||||
var elementType = attr.ElementDataType ?? DataType.String;
|
||||
var json = AttributeValueCodec.Encode(GetListRows(attr.Name));
|
||||
try { AttributeValueCodec.Decode(json, DataType.List, elementType); }
|
||||
catch (FormatException ex) { _overrideErrors[attr.Name] = ex.Message; hasError = true; continue; }
|
||||
toSave[attr.Name] = json;
|
||||
}
|
||||
|
||||
if (hasError)
|
||||
{
|
||||
_toast.ShowError("Some List overrides have invalid elements — see the highlighted rows.");
|
||||
_saving = false;
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var (attrName, value) in toSave)
|
||||
{
|
||||
var result = await InstanceService.SetAttributeOverrideAsync(Id, attrName, value, user);
|
||||
if (result.IsSuccess)
|
||||
_existingOverrides[attrName] = result.Value!;
|
||||
}
|
||||
_toast.ShowSuccess($"Saved {toSave.Count} override(s).");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user