feat(uns): ancestor-aware tree filter on the global UNS page (task #136)
v2-ci / build (push) Failing after 5m21s
v2-ci / unit-tests (tests/Core/ZB.MOM.WW.OtOpcUa.Cluster.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.ControlPlane.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Security.Tests) (push) Has been skipped
v2-ci / integration (tests/Server/ZB.MOM.WW.OtOpcUa.Host.IntegrationTests) (push) Has been skipped
v2-ci / integration (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests) (push) Has been skipped
v2-ci / build (push) Failing after 5m21s
v2-ci / unit-tests (tests/Core/ZB.MOM.WW.OtOpcUa.Cluster.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.ControlPlane.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Security.Tests) (push) Has been skipped
v2-ci / integration (tests/Server/ZB.MOM.WW.OtOpcUa.Host.IntegrationTests) (push) Has been skipped
v2-ci / integration (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests) (push) Has been skipped
The /uns filter was per-level: it matched only a node's direct children by DisplayName and only under already-expanded nodes, so typing "blender" at the top matched nothing — the structural ancestors don't contain the text and weren't expanded. Rework UnsTree to the standard tree-filter behaviour: - A node is shown if it self-matches, sits under a matched ancestor, or has a matching descendant (VisibleUnder). - The path to a match auto-expands (chevron + child block follow a filter- derived `childrenShown`, not node.Expanded), and the whole subtree under a matched node is shown. - Lazy tag children are only considered once their equipment is loaded, so the filter never triggers lazy loads; the bounded structural tree keeps the recursive walk cheap. Clearing the filter restores the user's manual expand state (node.Expanded is untouched). Build clean; AdminUI.Tests 216/216.
This commit is contained in:
@@ -4,9 +4,9 @@
|
||||
Children and raises callbacks; the host page owns expansion + lazy loading. *@
|
||||
@using ZB.MOM.WW.OtOpcUa.AdminUI.Uns
|
||||
|
||||
@foreach (var root in Roots)
|
||||
@foreach (var root in Roots.Where(r => VisibleUnder(r, false)))
|
||||
{
|
||||
@RenderNode(root, 0)
|
||||
@RenderNode(root, 0, false)
|
||||
}
|
||||
|
||||
@code {
|
||||
@@ -28,19 +28,28 @@
|
||||
/// <summary>Raised when the user toggles a node's expansion; the host owns the resulting state + lazy load.</summary>
|
||||
[Parameter] public EventCallback<UnsNode> OnToggleExpand { get; set; }
|
||||
|
||||
/// <summary>Optional case-insensitive substring filter applied to visible children by DisplayName.</summary>
|
||||
/// <summary>
|
||||
/// Optional case-insensitive substring filter on DisplayName. A node is shown when it matches,
|
||||
/// sits under a matching ancestor, or has a matching descendant, and the path to matches
|
||||
/// auto-expands. Lazy tag children are only considered once their equipment has been loaded.
|
||||
/// </summary>
|
||||
[Parameter] public string? Filter { get; set; }
|
||||
|
||||
private RenderFragment RenderNode(UnsNode node, int depth) => __builder =>
|
||||
private RenderFragment RenderNode(UnsNode node, int depth, bool ancestorMatched) => __builder =>
|
||||
{
|
||||
var indent = $"padding-left:{depth * 18}px";
|
||||
var hasExpander = node.Children.Count > 0 || node.HasLazyChildren;
|
||||
// Under an active filter the matched path auto-expands, so the chevron and the child block
|
||||
// follow `childrenShown` (derived from the filter) rather than the user's node.Expanded flag.
|
||||
var matched = ancestorMatched || SelfMatches(node);
|
||||
var visibleChildren = HasFilter ? VisibleChildren(node, matched).ToList() : node.Children;
|
||||
var childrenShown = HasFilter ? visibleChildren.Count > 0 : node.Expanded;
|
||||
<div @key="node.Key" class="d-flex align-items-center gap-1 py-1" style="@indent">
|
||||
@if (hasExpander)
|
||||
{
|
||||
<button type="button" class="btn btn-sm btn-link p-0"
|
||||
@onclick="@(() => OnToggleExpand.InvokeAsync(node))" style="width:18px">
|
||||
@(node.Expanded ? "▼" : "▶")
|
||||
@(childrenShown ? "▼" : "▶")
|
||||
</button>
|
||||
}
|
||||
else
|
||||
@@ -66,11 +75,11 @@
|
||||
@node.Error
|
||||
</div>
|
||||
}
|
||||
else if (node.Expanded)
|
||||
else if (childrenShown)
|
||||
{
|
||||
@foreach (var child in FilterChildren(node))
|
||||
@foreach (var child in visibleChildren)
|
||||
{
|
||||
@RenderNode(child, depth + 1)
|
||||
@RenderNode(child, depth + 1, matched)
|
||||
}
|
||||
}
|
||||
};
|
||||
@@ -128,15 +137,34 @@
|
||||
}
|
||||
};
|
||||
|
||||
private IEnumerable<UnsNode> FilterChildren(UnsNode node)
|
||||
/// <summary>Whether a non-empty filter is currently active.</summary>
|
||||
private bool HasFilter => !string.IsNullOrWhiteSpace(Filter);
|
||||
|
||||
/// <summary>Whether the node's own DisplayName matches the active filter (case-insensitive substring).</summary>
|
||||
private bool SelfMatches(UnsNode node)
|
||||
{
|
||||
var f = Filter?.Trim();
|
||||
if (string.IsNullOrEmpty(f))
|
||||
{
|
||||
return node.Children;
|
||||
}
|
||||
|
||||
return node.Children.Where(c =>
|
||||
c.DisplayName.Contains(f, StringComparison.OrdinalIgnoreCase));
|
||||
return !string.IsNullOrEmpty(f)
|
||||
&& node.DisplayName.Contains(f, StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Whether the node should appear under the active filter: it self-matches, sits under a matched
|
||||
/// ancestor, or has a (loaded) descendant that matches. Unloaded lazy tag children can't be
|
||||
/// inspected, so an unexpanded equipment matches on its own name only. The structural tree is
|
||||
/// bounded, so this recursive walk is cheap.
|
||||
/// </summary>
|
||||
private bool VisibleUnder(UnsNode node, bool ancestorMatched)
|
||||
{
|
||||
if (!HasFilter) { return true; }
|
||||
if (ancestorMatched || SelfMatches(node)) { return true; }
|
||||
return node.Children.Any(c => VisibleUnder(c, false));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The children to render under the active filter: the whole child list when the node (or an
|
||||
/// ancestor) matched, otherwise only the branches that still contain a match.
|
||||
/// </summary>
|
||||
private IEnumerable<UnsNode> VisibleChildren(UnsNode node, bool matched) =>
|
||||
matched ? node.Children : node.Children.Where(c => VisibleUnder(c, false));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user