@RenderNodeLabel(node)
@@ -109,6 +125,10 @@
private List _templates = new();
private List _folders = new();
+ // Search text bound to the filter input; passed as Filter to TemplateFolderTree
+ // which handles substring matching and ancestor auto-expand internally.
+ private string _searchText = string.Empty;
+
private bool _loading = true;
private string? _errorMessage;
diff --git a/tests/ZB.MOM.WW.ScadaBridge.CentralUI.Tests/TemplatesPageTests.cs b/tests/ZB.MOM.WW.ScadaBridge.CentralUI.Tests/TemplatesPageTests.cs
index 30b36fdf..d8e067ce 100644
--- a/tests/ZB.MOM.WW.ScadaBridge.CentralUI.Tests/TemplatesPageTests.cs
+++ b/tests/ZB.MOM.WW.ScadaBridge.CentralUI.Tests/TemplatesPageTests.cs
@@ -127,6 +127,55 @@ public class TemplatesPageTests : BunitContext
Assert.Contains("bi-arrow-return-right", cut.Markup);
}
+ [Fact]
+ public void SearchBox_IsPresentAndBound_ToTemplateFolderTreeFilter()
+ {
+ // Seed two templates in the same folder so we can confirm the filter narrows
+ // the visible set and that clearing the input restores the full tree.
+ var folder = new TemplateFolder("Controllers") { Id = 1 };
+ var alpha = new Template("AlphaDevice") { Id = 10, FolderId = 1 };
+ var beta = new Template("BetaDevice") { Id = 20, FolderId = 1 };
+
+ _repo.GetAllTemplatesAsync(Arg.Any())
+ .Returns(Task.FromResult>(new List { alpha, beta }));
+ _repo.GetAllFoldersAsync(Arg.Any())
+ .Returns(Task.FromResult>(new List { folder }));
+
+ var cut = Render();
+
+ // 1. A search input must exist in the rendered output.
+ var search = cut.Find("input[type='text'][placeholder*='earch']");
+ Assert.NotNull(search);
+
+ // 2. Typing a substring hides non-matching templates. The TemplateFolderTree
+ // filter auto-expands ancestors of matches via its _initiallyExpanded hook,
+ // so "AlphaDevice" appears without a manual toggle click. "BetaDevice" is
+ // absent from the filtered tree entirely.
+ search.Input("Alpha");
+
+ Assert.Contains("AlphaDevice", cut.Markup);
+ Assert.DoesNotContain("BetaDevice", cut.Markup);
+
+ // 3. Clearing the input restores the full tree. The folder may now be collapsed
+ // (prior expansion state is stored per-key; after filter-clear the tree uses
+ // saved state). Expand manually to verify both templates are reachable.
+ search.Input("");
+
+ // Both templates must be in the tree; expand the folder if needed.
+ var folderToggleAfter = cut.FindAll("li[role='treeitem']")
+ .FirstOrDefault(li => li.TextContent.Contains("Controllers"))
+ ?.QuerySelector(".tv-toggle");
+
+ // Only click if the folder is not yet expanded (aria-expanded='false').
+ var folderLi = cut.FindAll("li[role='treeitem']")
+ .FirstOrDefault(li => li.TextContent.Contains("Controllers"));
+ if (folderLi?.GetAttribute("aria-expanded") != "true")
+ folderToggleAfter?.Click();
+
+ Assert.Contains("AlphaDevice", cut.Markup);
+ Assert.Contains("BetaDevice", cut.Markup);
+ }
+
}
internal sealed class TestAuthStateProvider : AuthenticationStateProvider