@using ZB.MOM.WW.OtOpcUa.Admin.Services @using ZB.MOM.WW.OtOpcUa.Configuration.Entities @inject UnsService UnsSvc
Drag any line in the UNS Lines table onto an area row in UNS Areas to re-parent it. A preview modal shows the impact (equipment re-home count) + lets you confirm or cancel. If another operator modifies the draft while you're confirming, you'll see a 409 refresh-required modal instead of clobbering their work.

UNS Areas

@if (_areas is null) {

Loading…

} else if (_areas.Count == 0) {

No areas yet.

} else { @foreach (var a in _areas) { }
AreaIdName(drop target)
@a.UnsAreaId @a.Name drop here
} @if (_showAreaForm) {
}

UNS Lines

@if (_lines is null) {

Loading…

} else if (_lines.Count == 0) {

No lines yet.

} else { @foreach (var l in _lines) { }
LineIdAreaName
@l.UnsLineId @l.UnsAreaId @l.Name
} @if (_showLineForm && _areas is not null) {
}
@* Preview / confirm modal for a pending drag-drop move *@ @if (_pendingPreview is not null) { } @* 409 concurrent-edit modal — another operator changed the draft between preview + commit *@ @if (_conflictMessage is not null) { } @code { [Parameter] public long GenerationId { get; set; } [Parameter] public string ClusterId { get; set; } = string.Empty; private List? _areas; private List? _lines; private bool _showAreaForm; private bool _showLineForm; private string _newAreaName = string.Empty; private string _newLineName = string.Empty; private string _newLineAreaId = string.Empty; private string? _dragLineId; private string? _hoverAreaId; private UnsImpactPreview? _pendingPreview; private UnsMoveOperation? _pendingMove; private bool _committing; private string? _conflictMessage; protected override async Task OnParametersSetAsync() => await ReloadAsync(); private async Task ReloadAsync() { _areas = await UnsSvc.ListAreasAsync(GenerationId, CancellationToken.None); _lines = await UnsSvc.ListLinesAsync(GenerationId, CancellationToken.None); } private async Task AddAreaAsync() { if (string.IsNullOrWhiteSpace(_newAreaName)) return; await UnsSvc.AddAreaAsync(GenerationId, ClusterId, _newAreaName, notes: null, CancellationToken.None); _newAreaName = string.Empty; _showAreaForm = false; await ReloadAsync(); } private async Task AddLineAsync() { if (string.IsNullOrWhiteSpace(_newLineName) || string.IsNullOrWhiteSpace(_newLineAreaId)) return; await UnsSvc.AddLineAsync(GenerationId, _newLineAreaId, _newLineName, notes: null, CancellationToken.None); _newLineName = string.Empty; _showLineForm = false; await ReloadAsync(); } private void OnAreaDragOver(DragEventArgs _, string areaId) => _hoverAreaId = areaId; private async Task OnLineDroppedAsync(string targetAreaId) { var lineId = _dragLineId; _hoverAreaId = null; _dragLineId = null; if (string.IsNullOrWhiteSpace(lineId)) return; var line = _lines?.FirstOrDefault(l => l.UnsLineId == lineId); if (line is null || line.UnsAreaId == targetAreaId) return; var snapshot = await UnsSvc.LoadSnapshotAsync(GenerationId, CancellationToken.None); var move = new UnsMoveOperation( Kind: UnsMoveKind.LineMove, SourceClusterId: ClusterId, TargetClusterId: ClusterId, SourceLineId: lineId, TargetAreaId: targetAreaId); try { _pendingPreview = UnsImpactAnalyzer.Analyze(snapshot, move); _pendingMove = move; } catch (Exception ex) { _conflictMessage = ex.Message; // CrossCluster or validation failure surfaces here } } private void CancelMove() { _pendingPreview = null; _pendingMove = null; } private async Task ConfirmMoveAsync() { if (_pendingPreview is null || _pendingMove is null) return; _committing = true; try { await UnsSvc.MoveLineAsync( GenerationId, _pendingPreview.RevisionToken, _pendingMove.SourceLineId!, _pendingMove.TargetAreaId!, CancellationToken.None); _pendingPreview = null; _pendingMove = null; await ReloadAsync(); } catch (DraftRevisionConflictException ex) { _pendingPreview = null; _pendingMove = null; _conflictMessage = ex.Message; } finally { _committing = false; } } private async Task ReloadAfterConflict() { _conflictMessage = null; await ReloadAsync(); } }