feat(m9/T22): template tree search box (wire TemplateFolderTree.Filter)
This commit is contained in:
@@ -79,12 +79,28 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style="max-height: calc(100vh - 160px); overflow-y: auto; padding: 4px;">
|
<div class="mb-3" style="max-width: 320px;">
|
||||||
|
<div class="input-group input-group-sm">
|
||||||
|
<input type="text" class="form-control form-control-sm"
|
||||||
|
placeholder="Search templates..."
|
||||||
|
value="@_searchText"
|
||||||
|
@oninput="e => _searchText = e.Value?.ToString() ?? string.Empty" />
|
||||||
|
@if (!string.IsNullOrEmpty(_searchText))
|
||||||
|
{
|
||||||
|
<button class="btn btn-outline-secondary" type="button"
|
||||||
|
title="Clear search"
|
||||||
|
@onclick="() => _searchText = string.Empty">✕</button>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div style="max-height: calc(100vh - 200px); overflow-y: auto; padding: 4px;">
|
||||||
<TemplateFolderTree @ref="_tree"
|
<TemplateFolderTree @ref="_tree"
|
||||||
Folders="_folders"
|
Folders="_folders"
|
||||||
Templates="_templates"
|
Templates="_templates"
|
||||||
SelectionMode="TreeViewSelectionMode.Single"
|
SelectionMode="TreeViewSelectionMode.Single"
|
||||||
ExtraTemplateChildren="BuildCompositionLeavesFor"
|
ExtraTemplateChildren="BuildCompositionLeavesFor"
|
||||||
|
Filter="@_searchText"
|
||||||
StorageKey="templates-tree">
|
StorageKey="templates-tree">
|
||||||
<NodeContent Context="node">
|
<NodeContent Context="node">
|
||||||
@RenderNodeLabel(node)
|
@RenderNodeLabel(node)
|
||||||
@@ -109,6 +125,10 @@
|
|||||||
private List<Template> _templates = new();
|
private List<Template> _templates = new();
|
||||||
private List<TemplateFolder> _folders = new();
|
private List<TemplateFolder> _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 bool _loading = true;
|
||||||
private string? _errorMessage;
|
private string? _errorMessage;
|
||||||
|
|
||||||
|
|||||||
@@ -127,6 +127,55 @@ public class TemplatesPageTests : BunitContext
|
|||||||
Assert.Contains("bi-arrow-return-right", cut.Markup);
|
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<CancellationToken>())
|
||||||
|
.Returns(Task.FromResult<IReadOnlyList<Template>>(new List<Template> { alpha, beta }));
|
||||||
|
_repo.GetAllFoldersAsync(Arg.Any<CancellationToken>())
|
||||||
|
.Returns(Task.FromResult<IReadOnlyList<TemplateFolder>>(new List<TemplateFolder> { folder }));
|
||||||
|
|
||||||
|
var cut = Render<TemplatesPage>();
|
||||||
|
|
||||||
|
// 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
|
internal sealed class TestAuthStateProvider : AuthenticationStateProvider
|
||||||
|
|||||||
Reference in New Issue
Block a user