feat(adminui): add generic CollectionEditor<TRow> modal list editor
This commit is contained in:
+129
@@ -0,0 +1,129 @@
|
||||
@* Generic modal-per-row list editor. The parent owns the List<TRow> (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
|
||||
|
||||
<section class="panel rise mt-3" style="@_styleDelay">
|
||||
<div class="panel-head d-flex align-items-center">
|
||||
<span>@Title (@Items.Count)</span>
|
||||
<button type="button" class="btn btn-sm btn-outline-primary ms-auto" @onclick="Add">+ Add @ItemNoun</button>
|
||||
</div>
|
||||
@if (Items.Count == 0)
|
||||
{
|
||||
<div style="padding:1rem" class="text-muted">No @ItemNoun.ToLowerInvariant() rows.</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="table-wrap">
|
||||
<table class="data-table">
|
||||
<thead>@HeaderTemplate</thead>
|
||||
<tbody>
|
||||
@for (var i = 0; i < Items.Count; i++)
|
||||
{
|
||||
var idx = i;
|
||||
<tr>
|
||||
@RowTemplate(Items[idx])
|
||||
<td class="text-end" style="white-space:nowrap">
|
||||
<button type="button" class="btn btn-sm btn-link p-0 me-2" @onclick="() => Edit(idx)">Edit</button>
|
||||
<button type="button" class="btn btn-sm btn-link p-0 text-danger" @onclick="() => Delete(idx)">Delete</button>
|
||||
</td>
|
||||
</tr>
|
||||
}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
}
|
||||
</section>
|
||||
|
||||
@if (_modalOpen && _working is not null)
|
||||
{
|
||||
<div class="modal-backdrop fade show" style="display:block"></div>
|
||||
<div class="modal fade show" tabindex="-1" role="dialog" style="display:block">
|
||||
<div class="modal-dialog modal-lg" role="document">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">@(_editIndex is null ? $"Add {ItemNoun}" : $"Edit {ItemNoun}")</h5>
|
||||
<button type="button" class="btn-close" aria-label="Close" @onclick="Cancel"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
@EditTemplate(_working)
|
||||
@if (!string.IsNullOrEmpty(_validationError))
|
||||
{
|
||||
<div class="text-danger small mt-2">@_validationError</div>
|
||||
}
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-outline-secondary" @onclick="Cancel">Cancel</button>
|
||||
<button type="button" class="btn btn-primary" @onclick="Commit">Save</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
@code {
|
||||
[Parameter, EditorRequired] public List<TRow> 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<TRow> RowTemplate { get; set; } = default!;
|
||||
[Parameter, EditorRequired] public RenderFragment<TRow> EditTemplate { get; set; } = default!;
|
||||
[Parameter, EditorRequired] public Func<TRow> NewRow { get; set; } = default!;
|
||||
[Parameter, EditorRequired] public Func<TRow, TRow> Clone { get; set; } = default!;
|
||||
[Parameter] public Func<TRow, IReadOnlyList<TRow>, 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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user