From a4cdedc31c50af73360477a28cb1290df7a720e5 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Thu, 22 Oct 2026 17:53:06 -0400 Subject: [PATCH] fix(adminui): confirm equipment-page and script deletes (R04 H1) The equipment page's Remove reference / Delete virtual tag / Delete alarm row buttons and the ScriptEdit Delete ran on the first click. They now go through RawConfirmDialog, the same confirm /raw uses. The ScriptEdit change is limited to the delete button and its dialog. Refs #745 Claude-Session: https://claude.ai/code/session_01Ub9hGmYLQvn9XETzw7CMsT --- .../Components/Pages/ScriptEdit.razor | 9 +++- .../Components/Pages/Uns/EquipmentPage.razor | 53 +++++++++++++++++-- .../Uns/DeleteConfirmationWiringTests.cs | 40 ++++++++++++++ 3 files changed, 98 insertions(+), 4 deletions(-) create mode 100644 tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/Uns/DeleteConfirmationWiringTests.cs diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/ScriptEdit.razor b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/ScriptEdit.razor index e0a1d8af..e6553dfe 100644 --- a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/ScriptEdit.razor +++ b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/ScriptEdit.razor @@ -11,6 +11,7 @@ @using System.Text @using ZB.MOM.WW.OtOpcUa.Configuration @using ZB.MOM.WW.OtOpcUa.Configuration.Entities +@using ZB.MOM.WW.OtOpcUa.AdminUI.Components.Shared.Raw @inject IDbContextFactory DbFactory @inject NavigationManager Nav @@ -69,9 +70,14 @@ else Cancel - @if (!IsNew) { } + @if (!IsNew) { } + + @* R04 H1: the delete used to run on the first click. *@ + } @@ -84,6 +90,7 @@ else private bool _loaded; private bool _busy; private string? _error; + private bool _confirmDeleteVisible; protected override async Task OnInitializedAsync() { diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Uns/EquipmentPage.razor b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Uns/EquipmentPage.razor index 4673b93f..105fa06b 100644 --- a/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Uns/EquipmentPage.razor +++ b/src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Uns/EquipmentPage.razor @@ -11,6 +11,7 @@ @using ZB.MOM.WW.OtOpcUa.Configuration.Validation @using ZB.MOM.WW.OtOpcUa.AdminUI.Uns @using ZB.MOM.WW.OtOpcUa.AdminUI.Components.Shared.Uns +@using ZB.MOM.WW.OtOpcUa.AdminUI.Components.Shared.Raw @inject IUnsTreeService Svc @inject NavigationManager Nav @@ -187,7 +188,7 @@ else
- +
@@ -240,7 +241,7 @@ else
- +
@@ -295,7 +296,7 @@ else
- +
@@ -310,6 +311,11 @@ else + + @* One confirm for every destructive row action on this page (R04 H1) — the same RawConfirmDialog + /raw routes its deletes through. A mis-click used to delete immediately. *@ + } } @@ -403,6 +409,47 @@ else else { _refError = res.Error; } } + // --- Delete confirmation (R04 H1) --- + private bool _confirmVisible; + private string _confirmTitle = ""; + private string _confirmMessage = ""; + private string _confirmLabel = "Delete"; + private Func? _confirmAction; + + private void AskConfirm(string title, string message, string label, Func action) + { + _confirmTitle = title; + _confirmMessage = message; + _confirmLabel = label; + _confirmAction = action; + _confirmVisible = true; + } + + private async Task RunConfirmedAsync() + { + var action = _confirmAction; + _confirmAction = null; + if (action is not null) await action(); + } + + private void ConfirmRemoveReference(EquipmentReferenceRow r) => AskConfirm( + "Remove reference", + $"Remove the reference '{r.EffectiveName}'? After the next deploy its UNS node is gone, and any client subscribed to it loses it. The raw tag is not affected.", + "Remove", + () => RemoveReference(r)); + + private void ConfirmDeleteVirtualTag(string vtagId, string name) => AskConfirm( + "Delete virtual tag", + $"Delete the virtual tag '{name}'? After the next deploy its OPC UA node is gone, and any client subscribed to it loses it.", + "Delete", + () => DeleteVirtualTag(vtagId)); + + private void ConfirmDeleteAlarm(string scriptedAlarmId, string name) => AskConfirm( + "Delete scripted alarm", + $"Delete the scripted alarm '{name}'? After the next deploy its condition is gone, and any client subscribed to it loses it.", + "Delete", + () => DeleteAlarm(scriptedAlarmId)); + private async Task RemoveReference(EquipmentReferenceRow r) { _refError = null; diff --git a/tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/Uns/DeleteConfirmationWiringTests.cs b/tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/Uns/DeleteConfirmationWiringTests.cs new file mode 100644 index 00000000..3505c267 --- /dev/null +++ b/tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/Uns/DeleteConfirmationWiringTests.cs @@ -0,0 +1,40 @@ +using System.Text.RegularExpressions; +using Shouldly; +using Xunit; + +namespace ZB.MOM.WW.OtOpcUa.AdminUI.Tests.Uns; + +/// +/// R04 H1 source pin (no bUnit): every destructive button on the equipment page and the script editor +/// opens a confirm dialog instead of deleting on the first click. The live check (each delete confirms) +/// is in the Wave 4 docker-dev pass. +/// +[Trait("Category", "Unit")] +public sealed class DeleteConfirmationWiringTests +{ + [Fact] + public void Equipment_page_routes_every_destructive_row_action_through_the_confirm() + { + var src = AdminUiSource.Read("Components", "Pages", "Uns", "EquipmentPage.razor"); + + src.ShouldContain(" ConfirmRemoveReference(r)\""); + src.ShouldContain("@onclick=\"() => ConfirmDeleteVirtualTag(v.VirtualTagId, v.Name)\""); + src.ShouldContain("@onclick=\"() => ConfirmDeleteAlarm(a.ScriptedAlarmId, a.Name)\""); + + // No danger button may call a delete handler directly. + Regex.Matches(src, @"btn-outline-danger[^>]*@onclick=""\(\) => (Delete|Remove)\w*\(").Count.ShouldBe(0); + // Positive control: the page still has its three danger buttons. + Regex.Matches(src, "btn-outline-danger").Count.ShouldBe(3); + } + + [Fact] + public void Script_editor_delete_confirms() + { + var src = AdminUiSource.Read("Components", "Pages", "ScriptEdit.razor"); + + src.ShouldContain("@onclick=\"() => _confirmDeleteVisible = true\""); + src.ShouldContain("OnConfirm=\"DeleteAsync\""); + src.ShouldNotContain("@onclick=\"DeleteAsync\""); + } +}