+ @attr.DataType
+ @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"). *@
+
+ of @(attr.ElementDataType ?? DataType.String)
+
+ }
+
@(attr.Value ?? "—")
- 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. *@
+
+ @if (_overrideErrors.TryGetValue(attr.Name, out var listErr))
+ {
+
}
@@ -507,6 +545,17 @@
// Overrides
private List _overrideAttrs = new();
private Dictionary _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 _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> _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 _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 ──────────
+
+ /// Working rows for a List attribute's override (whole-list replacement).
+ private List GetListRows(string attrName)
+ => _listRows.TryGetValue(attrName, out var rows) ? rows : (_listRows[attrName] = new());
+
+ private void OnListRowsChanged(string attrName, List rows)
+ {
+ _listRows[attrName] = rows;
+ // A fresh edit clears any stale validation error for this attribute.
+ _overrideErrors.Remove(attrName);
+ }
+
+ /// True if a persisted override row exists for the attribute (so Clear is offered).
+ private bool HasOverrideRow(string attrName) => _existingOverrides.ContainsKey(attrName);
+
+ ///
+ /// 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.
+ ///
+ private static List 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