@* Recursive, lazily-expanding renderer for the v3 Raw project tree (/raw).
Unlike UnsTree (which eager-loads the whole structural tree and never touches
the RawNode lazy fields), THIS component actually exercises the lazy plumbing:
expanding an unloaded container calls IRawTreeService.LoadChildrenAsync, appends
the returned level into node.Children and flips Loaded/Loading, showing a spinner
in flight and an error row on failure. Per-node context menus (right-click surface
+ "⋯" fallback) are built per RawNodeKind.
Wave-B integration (this pass): every named handler now opens the REAL modal/dialog
(Configure driver/device, Edit/Manual/CSV tag surfaces) or calls IRawTreeService
directly (New folder/driver/group, Rename, Delete, Toggle) via the small dialogs
built alongside — RawNameDialog / RawConfirmDialog / RawDriverTypeDialog — and
refreshes the affected subtree afterwards (reload the container for create-under
ops, the parent for item-level ops). Only Browse device stays a placeholder (Wave C
owns it). The former RawStubModal is repurposed here as the shared message surface
for delete-blockers, rename warnings, and the Browse "coming soon" note. This
component is a single instance that recurses via the RenderNode RenderFragment, so it
owns one set of modals + one injected service for the whole tree. *@
@using ZB.MOM.WW.OtOpcUa.AdminUI.Uns
@using ZB.MOM.WW.OtOpcUa.AdminUI.Components.Shared.Raw
@using ZB.MOM.WW.OtOpcUa.AdminUI.Components.Shared.Drivers
@inject IRawTreeService Svc
@foreach (var root in Roots)
{
@RenderNode(root, 0)
}
@* --- Real modals + dialogs (one instance each for the whole recursive tree) --- *@
@* Shared message surface (repurposed RawStubModal) — delete blockers, rename warnings, Browse note. *@
@code {
/// The top-level Raw nodes to render (the enterprise roots from LoadRootsAsync).
[Parameter, EditorRequired] public IReadOnlyList Roots { get; set; } = default!;
///
/// Optional safety valve: raised when a mutation's affected node has no in-tree parent to reload
/// (only reachable for a cluster-less top-level node). Every menu-bearing node's parent is in the
/// tree today, so this is normally a no-op; the page may wire it to reload roots if that ever changes.
///
[Parameter] public EventCallback OnRootsChanged { get; set; }
// ---------------------------------------------------------------------------
// v3 Batch 3 — raw-tag PICKER mode (opt-in; default off keeps the /raw usage byte-unchanged).
// In picker mode the mutation context-menus are suppressed: Tag leaves render a multi-select
// checkbox, and Device/TagGroup containers offer a "select all tags below" menu. The equipment
// page's "+ Add reference" modal drives this against a single cluster-scoped root.
// ---------------------------------------------------------------------------
/// When true, render the tree as a raw-tag picker (checkboxes + select-all) instead of
/// the editing tree. Default false — the /raw editing usage is unaffected.
[Parameter] public bool PickerMode { get; set; }
/// The shared, caller-owned set of selected raw TagIds (picker mode only). The component
/// mutates it in place and raises after every change.
[Parameter] public HashSet? SelectedTagIds { get; set; }
/// Raised after the selection set changes (picker mode) so the host can update its count / footer.
[Parameter] public EventCallback OnSelectionChanged { get; set; }
/// Supplies the raw TagIds beneath a Device/TagGroup node for the "select all tags below"
/// affordance (picker mode). The host wires this to IUnsTreeService.LoadDescendantTagIdsAsync.
[Parameter] public Func>>? ResolveDescendantTagIds { get; set; }
// --- Configure-driver modal (edit) ---
private bool _driverCfgVisible;
private string? _driverCfgId;
private RawNode? _driverCfgNode;
// --- Device modal (create + edit) ---
private bool _deviceVisible;
private string? _deviceEditId;
private string? _deviceCreateDriverId;
private string? _deviceDriverType;
private RawNode? _deviceNode;
private bool _deviceIsCreate;
// --- Edit-tag modal ---
private bool _tagVisible;
private string? _tagId;
private string? _tagDriverType;
private RawNode? _tagNode;
// --- Manual tag-entry modal ---
private bool _manualVisible;
private string _manualDeviceId = "";
private string? _manualTagGroupId;
private string _manualDriverType = "";
private RawNode? _manualNode;
// --- CSV import modal ---
private bool _csvImportVisible;
private string _csvImportDeviceId = "";
private string? _csvImportTagGroupId;
private string _csvImportDriverType = "";
private RawNode? _csvImportNode;
// --- CSV export modal (read-only, no refresh) ---
private bool _csvExportVisible;
private string _csvExportDeviceId = "";
private string? _csvExportTagGroupId;
private string _csvExportDriverType = "";
// --- Browse-device modal (WP6 discovery-browser re-target) ---
private bool _browseVisible;
private string _browseDeviceId = "";
private string? _browseTagGroupId;
private string _browseDriverType = "";
private RawNode? _browseNode;
// --- Name dialog (New folder/group, Rename) ---
private bool _nameVisible;
private string _nameTitle = "";
private string _nameInitial = "";
private Func? _nameSubmit;
// --- Confirm dialog (deletes) ---
private bool _confirmVisible;
private string _confirmTitle = "";
private string _confirmMessage = "";
private Func? _confirmAction;
// --- Driver-type dialog (New driver) ---
private bool _driverTypeVisible;
private Func<(string DriverType, string Name), Task>? _driverTypeSubmit;
// --- Shared message surface (repurposed RawStubModal): delete blockers, rename warnings, Browse note ---
private bool _msgVisible;
private string _msgTitle = "";
private string _msgMessage = "";
private string? _msgNodeName;
// ---------------------------------------------------------------------------
// Row rendering
// ---------------------------------------------------------------------------
private RenderFragment RenderNode(RawNode node, int depth) => __builder =>
{
var indent = $"padding-left:{depth * 18}px";
var hasExpander = node.HasLazyChildren || node.Children.Count > 0;
var muted = node.Enabled == false && node.Kind is RawNodeKind.Driver or RawNodeKind.Device;
var items = MenuFor(node);