From 603995d43a388517f759bc71b224540670467a2a Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Fri, 22 May 2026 01:32:37 -0400 Subject: [PATCH] feat(centralui): ExecutionTree node double-click raises OnNodeActivated --- .../Components/Audit/ExecutionTree.razor | 4 +- .../Components/Audit/ExecutionTree.razor.cs | 9 +++ .../Components/Audit/ExecutionTree.razor.css | 6 +- .../Components/Audit/ExecutionTreeTests.cs | 57 +++++++++++++++++++ 4 files changed, 74 insertions(+), 2 deletions(-) diff --git a/src/ScadaLink.CentralUI/Components/Audit/ExecutionTree.razor b/src/ScadaLink.CentralUI/Components/Audit/ExecutionTree.razor index eb017a3..cc0c365 100644 --- a/src/ScadaLink.CentralUI/Components/Audit/ExecutionTree.razor +++ b/src/ScadaLink.CentralUI/Components/Audit/ExecutionTree.razor @@ -45,7 +45,8 @@ } -
+
} diff --git a/src/ScadaLink.CentralUI/Components/Audit/ExecutionTree.razor.cs b/src/ScadaLink.CentralUI/Components/Audit/ExecutionTree.razor.cs index c415d48..fc773f7 100644 --- a/src/ScadaLink.CentralUI/Components/Audit/ExecutionTree.razor.cs +++ b/src/ScadaLink.CentralUI/Components/Audit/ExecutionTree.razor.cs @@ -78,6 +78,15 @@ public partial class ExecutionTree /// [Parameter] public int Depth { get; set; } + /// + /// Raised when a node is double-clicked, carrying that node's + /// . The same callback is + /// threaded unchanged into every recursive child instance, so a + /// double-click on a node at any depth invokes the root-supplied handler + /// (used to open the node detail modal). + /// + [Parameter] public EventCallback OnNodeActivated { get; set; } + // The subtrees this instance renders: assembled from Nodes on the root, // or taken straight from PreBuiltRoots on a nested instance. private IReadOnlyList _rootsToRender = Array.Empty(); diff --git a/src/ScadaLink.CentralUI/Components/Audit/ExecutionTree.razor.css b/src/ScadaLink.CentralUI/Components/Audit/ExecutionTree.razor.css index 8f483a7..db6acab 100644 --- a/src/ScadaLink.CentralUI/Components/Audit/ExecutionTree.razor.css +++ b/src/ScadaLink.CentralUI/Components/Audit/ExecutionTree.razor.css @@ -25,7 +25,10 @@ position: relative; } -/* The node card: a flex row of [toggle][body]. */ +/* The node card: a flex row of [toggle][body]. + user-select: none — the body is double-clickable (opens the node detail + modal), so suppress the text selection a double-click would otherwise + leave behind. */ .execution-tree-node { display: flex; align-items: flex-start; @@ -35,6 +38,7 @@ border: 1px solid var(--bs-border-color); border-radius: 0.375rem; background-color: var(--bs-body-bg); + user-select: none; } /* The execution the user drilled in from — a left accent rule + tinted diff --git a/tests/ScadaLink.CentralUI.Tests/Components/Audit/ExecutionTreeTests.cs b/tests/ScadaLink.CentralUI.Tests/Components/Audit/ExecutionTreeTests.cs index 43b5825..88d31a8 100644 --- a/tests/ScadaLink.CentralUI.Tests/Components/Audit/ExecutionTreeTests.cs +++ b/tests/ScadaLink.CentralUI.Tests/Components/Audit/ExecutionTreeTests.cs @@ -227,6 +227,63 @@ public class ExecutionTreeTests : BunitContext cut.Find($"[data-test=\"tree-toggle-{root}\"]").GetAttribute("aria-expanded")); } + [Fact] + public void DoubleClickingNode_RaisesOnNodeActivated_WithExecutionId() + { + // Double-clicking a node's body raises OnNodeActivated carrying that + // node's ExecutionId — the affordance a later task uses to open the + // node detail modal. + var root = Guid.Parse("aaaaaaaa-4444-4444-4444-444444444444"); + var child = Guid.Parse("bbbbbbbb-4444-4444-4444-444444444444"); + var nodes = new List + { + Node(root, null), + Node(child, root), + }; + + Guid? activated = null; + var cut = Render(p => p + .Add(c => c.Nodes, nodes) + .Add(c => c.ArrivedFromExecutionId, root) + .Add(c => c.OnNodeActivated, (Guid id) => activated = id)); + + var rootBody = cut.Find($"[data-test=\"tree-node-{root}\"] .execution-tree-body"); + rootBody.DoubleClick(); + + Assert.Equal(root, activated); + } + + [Fact] + public void DoubleClickingNestedNode_BubblesOnNodeActivated_ToRoot() + { + // root → child → grandchild. Double-clicking a deeply nested node's + // body invokes the SAME root-supplied callback — the EventCallback is + // threaded unchanged down every recursive ExecutionTree instance. + var root = Guid.Parse("aaaaaaaa-5555-5555-5555-555555555555"); + var child = Guid.Parse("bbbbbbbb-5555-5555-5555-555555555555"); + var grandchild = Guid.Parse("cccccccc-5555-5555-5555-555555555555"); + var nodes = new List + { + Node(root, null), + Node(child, root), + Node(grandchild, child), + }; + + Guid? activated = null; + var cut = Render(p => p + .Add(c => c.Nodes, nodes) + .Add(c => c.ArrivedFromExecutionId, root) + .Add(c => c.OnNodeActivated, (Guid id) => activated = id)); + + // Double-click the grandchild (two recursion levels deep). + cut.Find($"[data-test=\"tree-node-{grandchild}\"] .execution-tree-body").DoubleClick(); + Assert.Equal(grandchild, activated); + + // And the child (one level deep) — both reach the root's callback. + cut.Find($"[data-test=\"tree-node-{child}\"] .execution-tree-body").DoubleClick(); + Assert.Equal(child, activated); + } + private static int CountOccurrences(string haystack, string needle) { int count = 0, idx = 0;