dashboard: lazy-load BrowsePage via DashboardBrowseService

This commit is contained in:
Joseph Doherty
2026-05-28 13:10:10 -04:00
parent ba157b4b4f
commit 310dfab8b4
7 changed files with 444 additions and 12 deletions
@@ -2,15 +2,21 @@
@*
Recursive Browse hierarchy node. Renders one Galaxy object, its child
objects (recursively), and its attributes as right-clickable tag rows.
Expansion state is local; children render only while expanded.
objects (recursively, lazy-loaded on first expand), and its attributes as
right-clickable tag rows. Expansion state is local; children render only
while expanded.
The expand triangle is shown whenever the server's child_has_children
projector hint is set (HasChildrenHint), even before children have been
loaded — clicking it triggers OnLoadChildren so the parent page can fill
in Node.Children, then the view re-renders.
*@
<div class="tree-node">
<div class="tree-row @(Node.IsArea ? "tree-row-area" : "tree-row-object")">
@if (Node.HasChildren)
@if (ShowToggle())
{
<button type="button" class="tree-toggle" @onclick="Toggle" aria-label="Toggle">
<button type="button" class="tree-toggle" @onclick="ToggleAsync" aria-label="Toggle">
@(_expanded ? "▾" : "▸")
</button>
}
@@ -18,7 +24,7 @@
{
<span class="tree-toggle tree-toggle-empty"></span>
}
<span class="tree-label" @onclick="Toggle">
<span class="tree-label" @onclick="ToggleAsync">
<span class="tree-icon">@(Node.IsArea ? "▣" : "◇")</span>
<span class="tree-name">@Node.DisplayName</span>
@if (!string.IsNullOrWhiteSpace(Node.Object.TagName)
@@ -31,9 +37,27 @@
@if (_expanded)
{
<div class="tree-children">
@if (Node.LoadState == BrowseLoadState.Loading)
{
<div class="tree-load-status text-secondary">
<span class="tree-toggle tree-toggle-empty"></span>
<span>⌛ Loading…</span>
</div>
}
else if (Node.LoadState == BrowseLoadState.Error)
{
<div class="tree-load-status text-danger">
<span class="tree-toggle tree-toggle-empty"></span>
<span>Failed to load: @Node.LoadError</span>
</div>
}
@foreach (DashboardBrowseNode child in Node.Children)
{
<BrowseTreeNodeView Node="child" OnAddTag="OnAddTag" OnTagContextMenu="OnTagContextMenu" />
<BrowseTreeNodeView Node="child"
OnAddTag="OnAddTag"
OnTagContextMenu="OnTagContextMenu"
OnLoadChildren="OnLoadChildren" />
}
@foreach (GalaxyAttribute attr in Node.Attributes)
{
@@ -75,13 +99,52 @@
[Parameter]
public EventCallback<(MouseEventArgs Event, GalaxyAttribute Attribute)> OnTagContextMenu { get; set; }
/// <summary>
/// Invoked on first expand when the projector hint says this node has children
/// but they have not been fetched yet. The callback is expected to populate
/// <see cref="DashboardBrowseNode.Children"/> on the node it receives and then
/// trigger a re-render.
/// </summary>
[Parameter]
public Func<DashboardBrowseNode, Task>? OnLoadChildren { get; set; }
private bool _expanded;
private void Toggle()
// The triangle is shown whenever the projector says children exist (even
// pre-load), or attributes are already present, or already-loaded children
// are sitting on the node.
private bool ShowToggle()
{
if (Node.HasChildren)
return Node.HasChildrenHint
|| Node.Attributes.Count > 0
|| Node.Children.Count > 0;
}
private async Task ToggleAsync()
{
if (!ShowToggle())
{
_expanded = !_expanded;
return;
}
_expanded = !_expanded;
if (_expanded
&& Node.HasChildrenHint
&& Node.LoadState == BrowseLoadState.NotLoaded
&& OnLoadChildren is not null)
{
Node.LoadState = BrowseLoadState.Loading;
try
{
await OnLoadChildren(Node);
Node.LoadState = BrowseLoadState.Loaded;
}
catch (Exception ex)
{
Node.LoadState = BrowseLoadState.Error;
Node.LoadError = ex.Message;
}
}
}