feat(adminui): B2-WP6 browse re-target — /raw device browse commits raw tags
Browse device… on a /raw Device/TagGroup opens the discovery-browser modal against the merged Driver+Device config and commits selected browse leaves as raw Tag rows under the target, via IRawTreeService.ImportTagsAsync. - RawBrowseModal.razor: two-tier browse gate (bespoke IDriverBrowser → universal DiscoveryDriverBrowser via SupportsOnlineDiscovery → disabled), merged config from LoadMergedProbeConfigAsync, multi-select over DriverBrowseTree, opt-in 'create matching tag-groups' folder mirror, commit via ImportTagsAsync. - RawBrowseCommitMapper: pure leaf→RawTagImportRow mapper — DriverDataType→ OPC UA type, per-driver address field (nodeId/tagPath/symbolPath/address/ attributeRef), target-group + mirror path combine. Unit-tested (39 tests). - DriverBrowseTree: additive multi-select mode (checkboxes + ancestor-path callback), default off preserves single-select pickers. - RawTree: OnBrowseDevice wired to the modal (Device + TagGroup menus). Claude-Session: https://claude.ai/code/session_01LVneM3eh1UtJxEisFXgmox
This commit is contained in:
+47
-6
@@ -20,7 +20,7 @@
|
||||
}
|
||||
else
|
||||
{
|
||||
@foreach (var n in _roots) { @RenderNode(n, 0) }
|
||||
@foreach (var n in _roots) { @RenderNode(n, 0, System.Array.Empty<string>()) }
|
||||
}
|
||||
</div>
|
||||
|
||||
@@ -34,6 +34,19 @@
|
||||
/// <summary>Fired when the user clicks a leaf (or any node — caller decides what to do with it).</summary>
|
||||
[Parameter] public EventCallback<BrowseNode> OnNodeSelected { get; set; }
|
||||
|
||||
/// <summary>When true, leaves render a selection checkbox and clicking a leaf toggles it (WP6 "Browse
|
||||
/// device…" multi-select) instead of single-selecting. Folders still expand/navigate as usual. Default
|
||||
/// false preserves the single-select address-picker behavior.</summary>
|
||||
[Parameter] public bool MultiSelect { get; set; }
|
||||
|
||||
/// <summary>In multi-select mode, the set of currently-selected leaf NodeIds (drives checkbox state +
|
||||
/// row highlight). Owned by the parent; the tree only reads it.</summary>
|
||||
[Parameter] public IReadOnlyCollection<string>? SelectedLeafIds { get; set; }
|
||||
|
||||
/// <summary>In multi-select mode, fired when a leaf's checkbox is toggled — carries the leaf plus its
|
||||
/// captured ancestor-folder display-name path (root→parent) for optional TagGroup mirroring.</summary>
|
||||
[Parameter] public EventCallback<ZB.MOM.WW.OtOpcUa.AdminUI.Browsing.BrowseLeafSelection> OnLeafToggled { get; set; }
|
||||
|
||||
private bool _loading = true;
|
||||
private string? _error;
|
||||
private List<TreeItem>? _roots;
|
||||
@@ -86,10 +99,28 @@
|
||||
await OnNodeSelected.InvokeAsync(item.Node);
|
||||
}
|
||||
|
||||
private RenderFragment RenderNode(TreeItem item, int depth) => __builder =>
|
||||
private async Task ToggleLeafAsync(TreeItem item, IReadOnlyList<string> ancestorPath)
|
||||
{
|
||||
await OnLeafToggled.InvokeAsync(
|
||||
new ZB.MOM.WW.OtOpcUa.AdminUI.Browsing.BrowseLeafSelection(item.Node, ancestorPath));
|
||||
}
|
||||
|
||||
private bool IsLeafSelected(string nodeId) => SelectedLeafIds?.Contains(nodeId) == true;
|
||||
|
||||
private static IReadOnlyList<string> Append(IReadOnlyList<string> path, string segment)
|
||||
{
|
||||
var list = new List<string>(path.Count + 1);
|
||||
list.AddRange(path);
|
||||
list.Add(segment);
|
||||
return list;
|
||||
}
|
||||
|
||||
private RenderFragment RenderNode(TreeItem item, int depth, IReadOnlyList<string> ancestorPath) => __builder =>
|
||||
{
|
||||
var indent = $"padding-left:{depth * 18}px";
|
||||
var selectedCls = _selectedNodeIdLocal == item.Node.NodeId ? "bg-primary-subtle" : "";
|
||||
var isMultiLeaf = MultiSelect && item.Node.Kind == BrowseNodeKind.Leaf;
|
||||
var selectedCls = (isMultiLeaf ? IsLeafSelected(item.Node.NodeId) : _selectedNodeIdLocal == item.Node.NodeId)
|
||||
? "bg-primary-subtle" : "";
|
||||
<div class="d-flex align-items-center gap-1 py-1 @selectedCls" style="@indent">
|
||||
@if (item.Node.Kind == BrowseNodeKind.Folder && item.Node.HasChildrenHint)
|
||||
{
|
||||
@@ -102,8 +133,18 @@
|
||||
{
|
||||
<span style="width:18px"></span>
|
||||
}
|
||||
<a href="#" @onclick="@(() => SelectAsync(item))" @onclick:preventDefault
|
||||
class="text-decoration-none mono small">@item.Node.DisplayName</a>
|
||||
@if (isMultiLeaf)
|
||||
{
|
||||
<input type="checkbox" class="form-check-input" checked="@IsLeafSelected(item.Node.NodeId)"
|
||||
@onchange="@(() => ToggleLeafAsync(item, ancestorPath))" />
|
||||
<a href="#" @onclick="@(() => ToggleLeafAsync(item, ancestorPath))" @onclick:preventDefault
|
||||
class="text-decoration-none mono small">@item.Node.DisplayName</a>
|
||||
}
|
||||
else
|
||||
{
|
||||
<a href="#" @onclick="@(() => SelectAsync(item))" @onclick:preventDefault
|
||||
class="text-decoration-none mono small">@item.Node.DisplayName</a>
|
||||
}
|
||||
@if (item.Node.Kind == BrowseNodeKind.Leaf)
|
||||
{
|
||||
<span class="chip chip-idle ms-1" style="font-size:0.7rem">leaf</span>
|
||||
@@ -129,7 +170,7 @@
|
||||
@bind="item.Filter" @bind:event="oninput" />
|
||||
@foreach (var c in FilterChildren(item))
|
||||
{
|
||||
@RenderNode(c, depth + 1)
|
||||
@RenderNode(c, depth + 1, Append(ancestorPath, item.Node.DisplayName))
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user