feat(centralui): NodeBrowserDialog.SelectableNodeClasses — opt-in Object node selection (default unchanged)

Claude-Session: https://claude.ai/code/session_01G2GHPnbbKGvyyfbr2XBRVH
This commit is contained in:
Joseph Doherty
2026-08-21 06:07:12 -04:00
parent 8d17af37ac
commit ab6e3432bd
3 changed files with 90 additions and 11 deletions
@@ -56,12 +56,24 @@
@* A button, not an href="javascript:void(0)" anchor:
this selects a node rather than navigating, and the
pseudo-href was a CSP/a11y smell. Matches the tree
label in TreeRow.razor. *@
<button type="button"
class="btn btn-link p-0 text-start align-baseline @(node.NodeId == _selectedNodeId ? "fw-bold text-primary" : "")"
@onclick="() => SelectBrowseNode(node)">
@node.DisplayName
</button>
label in TreeRow.razor. A node class the caller does
not accept renders as plain text — nothing to click. *@
@if (IsSelectable(node.NodeClass))
{
<button type="button"
class="btn btn-link p-0 text-start align-baseline @(node.NodeId == _selectedNodeId ? "fw-bold text-primary" : "")"
@onclick="() => SelectBrowseNode(node)">
@node.DisplayName
</button>
@if (node.NodeClass == BrowseNodeClass.Object)
{
<span class="badge bg-body-secondary text-body-secondary border ms-1">object</span>
}
}
else
{
<span class="text-muted">@node.DisplayName</span>
}
@if (!string.IsNullOrEmpty(node.DataType))
{
<span class="badge bg-body-secondary text-body-secondary border ms-1">@node.DataType</span>
@@ -90,7 +102,7 @@
<ul class="list-unstyled mb-0">
@foreach (var node in _rootNodes)
{
<TreeRow Node="node" OnToggle="ToggleAsync" OnSelect="Select" OnLoadMore="LoadMoreAsync" SelectedNodeId="@_selectedNodeId" />
<TreeRow Node="node" OnToggle="ToggleAsync" OnSelect="Select" OnLoadMore="LoadMoreAsync" SelectedNodeId="@_selectedNodeId" Selectable="IsSelectable" />
}
</ul>
}
@@ -113,6 +125,10 @@
}
</div>
<div class="modal-footer">
@if (SelectableNodeClasses.Contains(BrowseNodeClass.Object))
{
<span class="text-muted small me-3">Select an object or tag</span>
}
<span class="me-auto text-muted">Selected: <code>@(_selectedNodeId ?? "(none)")</code></span>
<div class="btn-group btn-group-sm" role="group">
<button class="btn btn-outline-secondary" @onclick="Cancel">Cancel</button>
@@ -147,6 +163,15 @@
/// always fail. Default true preserves OPC UA behaviour.</summary>
[Parameter] public bool ShowSearch { get; set; } = true;
/// <summary>
/// Additive: node classes the picker accepts. Default Variable-only (every
/// existing caller). The create page passes Object+Variable so a module's
/// container can be chosen (design 2026-08-21).
/// </summary>
[Parameter] public IReadOnlyCollection<BrowseNodeClass> SelectableNodeClasses { get; set; } = new[] { BrowseNodeClass.Variable };
private bool IsSelectable(BrowseNodeClass nodeClass) => SelectableNodeClasses.Contains(nodeClass);
private bool _isVisible;
private string? _selectedNodeId;
private BrowseNode? _selectedNode;
@@ -355,7 +380,7 @@
// Select button confirms and OnSelected fires with this node id.
private void SelectBrowseNode(BrowseNode node)
{
if (node.NodeClass != BrowseNodeClass.Variable) return;
if (!IsSelectable(node.NodeClass)) return;
_selectedNodeId = node.NodeId;
_manualNodeId = node.NodeId;
_selectedNode = node;
@@ -363,7 +388,7 @@
private void Select(TreeNode node)
{
if (node.NodeClass != BrowseNodeClass.Variable) return;
if (!IsSelectable(node.NodeClass)) return;
_selectedNodeId = node.NodeId;
_manualNodeId = node.NodeId;
_selectedNode = node.Source;
@@ -11,7 +11,7 @@
}
</span>
@if (Node.NodeClass == BrowseNodeClass.Variable)
@if (IsSelectable)
{
@* Display name (+ type badge) on the first line, the node id clipped
beneath it: an OPC UA node id runs 60–120 characters and inline it
@@ -49,7 +49,7 @@
<ul class="list-unstyled ms-4">
@foreach (var child in Node.Children)
{
<TreeRow Node="child" OnToggle="OnToggle" OnSelect="OnSelect" OnLoadMore="OnLoadMore" SelectedNodeId="@SelectedNodeId" />
<TreeRow Node="child" OnToggle="OnToggle" OnSelect="OnSelect" OnLoadMore="OnLoadMore" SelectedNodeId="@SelectedNodeId" Selectable="Selectable" />
}
@if (!string.IsNullOrEmpty(Node.ContinuationToken))
{
@@ -74,4 +74,14 @@
[Parameter] public EventCallback<NodeBrowserDialog.TreeNode> OnSelect { get; set; }
[Parameter] public EventCallback<NodeBrowserDialog.TreeNode> OnLoadMore { get; set; }
[Parameter] public string? SelectedNodeId { get; set; }
/// <summary>
/// Additive: which node classes the picker accepts, supplied by
/// <see cref="NodeBrowserDialog.SelectableNodeClasses"/>. Null keeps the historical
/// Variable-only behaviour. The expand toggle renders regardless, so a selectable
/// Object node can still be drilled into.
/// </summary>
[Parameter] public Func<BrowseNodeClass, bool>? Selectable { get; set; }
private bool IsSelectable => Selectable?.Invoke(Node.NodeClass) ?? Node.NodeClass == BrowseNodeClass.Variable;
}