diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Shared/Drivers/CollectionEditor.razor b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Shared/Drivers/CollectionEditor.razor new file mode 100644 index 00000000..ea512559 --- /dev/null +++ b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Shared/Drivers/CollectionEditor.razor @@ -0,0 +1,129 @@ +@* Generic modal-per-row list editor. The parent owns the List (a MUTABLE row VM, + because driver contracts are immutable records). This renders a read-only table with + Add/Edit/Delete and a modal that edits a CLONED working copy — commit on Save, discard + on Cancel. NewRow builds a default VM; Clone copies one for the working copy; Validate + (optional) returns an error string to block commit or null to allow. *@ +@typeparam TRow + +
+
+ @Title (@Items.Count) + +
+ @if (Items.Count == 0) + { +
No @ItemNoun.ToLowerInvariant() rows.
+ } + else + { +
+ + @HeaderTemplate + + @for (var i = 0; i < Items.Count; i++) + { + var idx = i; + + @RowTemplate(Items[idx]) + + + } + +
+ + +
+
+ } +
+ +@if (_modalOpen && _working is not null) +{ + + +} + +@code { + [Parameter, EditorRequired] public List Items { get; set; } = default!; + [Parameter] public EventCallback ItemsChanged { get; set; } + [Parameter] public string Title { get; set; } = "Items"; + [Parameter] public string ItemNoun { get; set; } = "row"; + [Parameter] public string AnimationDelay { get; set; } = ".18s"; + [Parameter, EditorRequired] public RenderFragment HeaderTemplate { get; set; } = default!; + [Parameter, EditorRequired] public RenderFragment RowTemplate { get; set; } = default!; + [Parameter, EditorRequired] public RenderFragment EditTemplate { get; set; } = default!; + [Parameter, EditorRequired] public Func NewRow { get; set; } = default!; + [Parameter, EditorRequired] public Func Clone { get; set; } = default!; + [Parameter] public Func, int?, string?>? Validate { get; set; } + + private string _styleDelay => $"animation-delay:{AnimationDelay}"; + private bool _modalOpen; + private int? _editIndex; + private TRow? _working; + private string? _validationError; + + private void Add() + { + _editIndex = null; + _working = NewRow(); + _validationError = null; + _modalOpen = true; + } + + private void Edit(int index) + { + _editIndex = index; + _working = Clone(Items[index]); + _validationError = null; + _modalOpen = true; + } + + private async Task Delete(int index) + { + Items.RemoveAt(index); + await ItemsChanged.InvokeAsync(); + } + + private void Cancel() + { + _modalOpen = false; + _working = default; + _editIndex = null; + _validationError = null; + } + + private async Task Commit() + { + if (_working is null) return; + _validationError = Validate?.Invoke(_working, Items, _editIndex); + if (_validationError is not null) return; + + if (_editIndex is int i) Items[i] = _working; + else Items.Add(_working); + + _modalOpen = false; + _working = default; + _editIndex = null; + await ItemsChanged.InvokeAsync(); + } +}