feat(uns): area + line modals wired into the tree

This commit is contained in:
Joseph Doherty
2026-06-08 13:20:25 -04:00
parent 307cec5a3d
commit a4a9dc912a
6 changed files with 649 additions and 1 deletions
@@ -38,22 +38,107 @@
else
{
<div style="padding:.5rem 1rem">
<UnsTree Roots="_roots" Filter="_filter" OnToggleExpand="ToggleAsync" />
<UnsTree Roots="_roots" Filter="_filter"
OnToggleExpand="ToggleAsync"
OnAddChild="HandleAddChild"
OnEdit="HandleEdit"
OnDelete="HandleDelete" />
</div>
}
</section>
<AreaModal Visible="_areaModalVisible"
IsNew="_areaModalIsNew"
ClusterId="_areaModalClusterId"
Existing="_areaModalExisting"
Clusters="ClusterOptions"
OnSaved="OnModalSavedAsync"
OnCancel="CloseModals" />
<LineModal Visible="_lineModalVisible"
IsNew="_lineModalIsNew"
UnsAreaId="_lineModalAreaId"
Existing="_lineModalExisting"
Areas="_lineModalAreaOptions"
OnSaved="OnModalSavedAsync"
OnCancel="CloseModals" />
@if (_confirmNode 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" role="document">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">Delete @_confirmNode.Kind</h5>
<button type="button" class="btn-close" aria-label="Close" @onclick="CloseModals"></button>
</div>
<div class="modal-body">
<p>Delete <span class="mono">@_confirmNode.DisplayName</span>? This cannot be undone.</p>
@if (!string.IsNullOrWhiteSpace(_confirmError))
{
<div class="text-danger small mt-2">@_confirmError</div>
}
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline-secondary" @onclick="CloseModals" disabled="@_confirmBusy">Cancel</button>
<button type="button" class="btn btn-danger" @onclick="ConfirmDeleteAsync" disabled="@_confirmBusy">
@if (_confirmBusy) { <span class="spinner-border spinner-border-sm me-1"></span> }
Delete
</button>
</div>
</div>
</div>
</div>
}
@code {
private IReadOnlyList<UnsNode> _roots = Array.Empty<UnsNode>();
private string? _filter;
private bool _loading = true;
// --- Area modal state ---
private bool _areaModalVisible;
private bool _areaModalIsNew;
private string? _areaModalClusterId;
private AreaEditDto? _areaModalExisting;
// --- Line modal state ---
private bool _lineModalVisible;
private bool _lineModalIsNew;
private string? _lineModalAreaId;
private LineEditDto? _lineModalExisting;
private IReadOnlyList<(string Id, string Display)> _lineModalAreaOptions = Array.Empty<(string, string)>();
// --- Delete-confirm state ---
private UnsNode? _confirmNode;
private bool _confirmBusy;
private string? _confirmError;
/// <summary>The served-by cluster options for the AreaModal, derived from the loaded tree.</summary>
private IReadOnlyList<(string Id, string Display)> ClusterOptions =>
_roots
.SelectMany(ent => ent.Children)
.Where(n => n.Kind == UnsNodeKind.Cluster && n.EntityId is not null)
.Select(n => (n.EntityId!, n.DisplayName))
.ToList();
protected override async Task OnInitializedAsync()
{
_roots = await Svc.LoadStructureAsync();
_loading = false;
}
/// <summary>Returns the <c>(Id, Display)</c> area options inside a single cluster, for the line picker.</summary>
private IReadOnlyList<(string Id, string Display)> AreaOptionsForCluster(string? clusterId) =>
_roots
.SelectMany(ent => ent.Children)
.Where(c => c.Kind == UnsNodeKind.Cluster && c.ClusterId == clusterId)
.SelectMany(c => c.Children)
.Where(a => a.Kind == UnsNodeKind.Area && a.EntityId is not null)
.Select(a => (a.EntityId!, a.DisplayName))
.ToList();
/// <summary>
/// Toggles a node's expansion. For equipment nodes whose children have not yet
/// been loaded, lazily fetches the tag/virtual-tag leaves on first expand.
@@ -88,6 +173,146 @@
}
}
/// <summary>
/// Opens the create modal for a node's primary child: a cluster gets a new area; an area gets a
/// new line scoped to its cluster. Equipment "+ Tag" is handled in a later task.
/// </summary>
private void HandleAddChild(UnsNode node)
{
CloseModals();
switch (node.Kind)
{
case UnsNodeKind.Cluster:
_areaModalIsNew = true;
_areaModalExisting = null;
_areaModalClusterId = node.ClusterId ?? node.EntityId;
_areaModalVisible = true;
break;
case UnsNodeKind.Area:
_lineModalIsNew = true;
_lineModalExisting = null;
_lineModalAreaId = node.EntityId;
_lineModalAreaOptions = AreaOptionsForCluster(node.ClusterId);
_lineModalVisible = true;
break;
}
}
/// <summary>
/// Opens the edit modal for an Area or Line, loading the entity first to prefill the form and
/// capture its RowVersion. Other kinds are handled in later tasks.
/// </summary>
private async Task HandleEdit(UnsNode node)
{
CloseModals();
switch (node.Kind)
{
case UnsNodeKind.Area:
var area = await Svc.LoadAreaAsync(node.EntityId!);
if (area is null) { return; }
_areaModalIsNew = false;
_areaModalExisting = area;
_areaModalClusterId = area.ClusterId;
_areaModalVisible = true;
break;
case UnsNodeKind.Line:
var line = await Svc.LoadLineAsync(node.EntityId!);
if (line is null) { return; }
_lineModalIsNew = false;
_lineModalExisting = line;
_lineModalAreaId = line.UnsAreaId;
_lineModalAreaOptions = AreaOptionsForCluster(node.ClusterId);
_lineModalVisible = true;
break;
}
}
/// <summary>Opens the delete-confirm modal for a node, stashing it as the pending target.</summary>
private void HandleDelete(UnsNode node)
{
CloseModals();
_confirmNode = node;
}
/// <summary>
/// Performs the pending delete. Loads the entity's RowVersion first, then dispatches on Kind.
/// Area/Line are handled here; other kinds are wired in later tasks.
/// </summary>
private async Task ConfirmDeleteAsync()
{
if (_confirmNode is null) { return; }
_confirmBusy = true;
_confirmError = null;
try
{
var node = _confirmNode;
UnsMutationResult result;
switch (node.Kind)
{
case UnsNodeKind.Area:
var area = await Svc.LoadAreaAsync(node.EntityId!);
if (area is null) { await ReloadAndCloseAsync(); return; }
result = await Svc.DeleteAreaAsync(node.EntityId!, area.RowVersion);
break;
case UnsNodeKind.Line:
var line = await Svc.LoadLineAsync(node.EntityId!);
if (line is null) { await ReloadAndCloseAsync(); return; }
result = await Svc.DeleteLineAsync(node.EntityId!, line.RowVersion);
break;
default:
// Equipment/Tag/VirtualTag deletes are wired in later tasks.
result = new UnsMutationResult(true, null);
break;
}
if (result.Ok)
{
await ReloadAndCloseAsync();
}
else
{
_confirmError = result.Error;
}
}
finally
{
_confirmBusy = false;
}
}
/// <summary>Reloads the tree after a successful modal save and closes any open modal.</summary>
private async Task OnModalSavedAsync()
{
_roots = await Svc.LoadStructureAsync();
CloseModals();
StateHasChanged();
}
/// <summary>Reloads the tree after a successful delete and closes the confirm modal.</summary>
private async Task ReloadAndCloseAsync()
{
_roots = await Svc.LoadStructureAsync();
CloseModals();
StateHasChanged();
}
/// <summary>Closes every modal and clears its transient state.</summary>
private void CloseModals()
{
_areaModalVisible = false;
_areaModalExisting = null;
_lineModalVisible = false;
_lineModalExisting = null;
_lineModalAreaOptions = Array.Empty<(string, string)>();
_confirmNode = null;
_confirmError = null;
}
/// <summary>
/// Expands every structural node (Enterprise/Cluster/Area/Line). Equipment nodes
/// are intentionally left collapsed because expanding them would trigger lazy loads.