refactor(ui/templates): drop row kebabs; double-click opens templates

The right-click context menu is now the single entry point for every
per-row action — folders, templates, and composition leaves. Drop the
⋮ kebab buttons that duplicated the menu and the click-to-open
behavior that was easy to trigger by accident while navigating the
tree. Templates and composition slots open on double-click instead.

- RenderNodeKebab removed entirely.
- Selectable / SelectedKeyChanged / OnTreeNodeSelected dropped from
  the TreeView wiring — single-click no longer navigates.
- New OpenTemplate(id) helper bound to @ondblclick on Template and
  Composition labels.
This commit is contained in:
Joseph Doherty
2026-05-12 09:50:22 -04:00
parent 54338abdce
commit 1f86945d46

View File

@@ -83,9 +83,7 @@
ChildrenSelector="n => n.Children" ChildrenSelector="n => n.Children"
HasChildrenSelector="n => n.Kind != TmplNodeKind.Composition && n.Children.Count > 0" HasChildrenSelector="n => n.Kind != TmplNodeKind.Composition && n.Children.Count > 0"
KeySelector="n => (object)n.Key" KeySelector="n => (object)n.Key"
StorageKey="templates-tree" StorageKey="templates-tree">
Selectable="true"
SelectedKeyChanged="OnTreeNodeSelected">
<NodeContent Context="node"> <NodeContent Context="node">
@RenderNodeLabel(node) @RenderNodeLabel(node)
</NodeContent> </NodeContent>
@@ -234,6 +232,9 @@
private TreeView<TmplNode> _tree = default!; private TreeView<TmplNode> _tree = default!;
private void OpenTemplate(int templateId) =>
NavigationManager.NavigateTo($"/design/templates/{templateId}");
private RenderFragment RenderNodeLabel(TmplNode node) => __builder => private RenderFragment RenderNodeLabel(TmplNode node) => __builder =>
{ {
switch (node.Kind) switch (node.Kind)
@@ -249,83 +250,23 @@
<span class="badge rounded-pill bg-secondary-subtle text-secondary-emphasis">@node.Children.Count</span> <span class="badge rounded-pill bg-secondary-subtle text-secondary-emphasis">@node.Children.Count</span>
</span> </span>
} }
@RenderNodeKebab(node)
break; break;
case TmplNodeKind.Template: case TmplNodeKind.Template:
<span class="tv-glyph"><i class="bi bi-file-earmark-text"></i></span> <span class="tv-glyph"><i class="bi bi-file-earmark-text"></i></span>
<span class="tv-label @(node.Children.Count > 0 ? "fw-semibold" : "")" <span class="tv-label @(node.Children.Count > 0 ? "fw-semibold" : "")"
title="@node.Label">@node.Label</span> title="@node.Label"
@RenderNodeKebab(node) @ondblclick="() => OpenTemplate(node.EntityId)">@node.Label</span>
break; break;
case TmplNodeKind.Composition: case TmplNodeKind.Composition:
<span class="tv-glyph"><i class="bi bi-arrow-return-right"></i></span> <span class="tv-glyph"><i class="bi bi-arrow-return-right"></i></span>
<span class="tv-label" title="@node.Label">@node.Label</span> <span class="tv-label" title="@node.Label"
@RenderNodeKebab(node) @ondblclick="() => OpenTemplate(node.Composition!.ComposedTemplateId)">@node.Label</span>
break; break;
} }
}; };
private RenderFragment RenderNodeKebab(TmplNode node) => __builder =>
{
<span class="tv-kebab dropdown ms-auto" @onclick:stopPropagation="true">
<button type="button"
class="btn btn-link btn-sm p-0 px-1 text-secondary tv-kebab-toggle"
data-bs-toggle="dropdown"
aria-expanded="false"
aria-label="@($"More actions for {node.Label}")">
<i class="bi bi-three-dots-vertical"></i>
</button>
<ul class="dropdown-menu dropdown-menu-end">
@switch (node.Kind)
{
case TmplNodeKind.Folder:
<li><button class="dropdown-item" @onclick="() => OpenNewFolderDialog(node.EntityId)">New Folder</button></li>
<li><button class="dropdown-item" @onclick='() => NavigationManager.NavigateTo($"/design/templates/create?folderId={node.EntityId}")'>New Template</button></li>
<li><button class="dropdown-item" @onclick="() => OpenRenameFolderDialog(node.EntityId, node.Label)">Rename</button></li>
<li><button class="dropdown-item" @onclick="() => OpenMoveFolderDialog(node.EntityId, node.Label)">Move to Folder…</button></li>
<li><hr class="dropdown-divider" /></li>
<li><button class="dropdown-item text-danger" @onclick="() => DeleteFolder(node.EntityId, node.Label)">Delete</button></li>
break;
case TmplNodeKind.Template:
<li><button class="dropdown-item" @onclick='() => NavigationManager.NavigateTo($"/design/templates/create?parentId={node.EntityId}")'>New Inheriting Template</button></li>
<li><button class="dropdown-item" @onclick="() => OpenComposeDialog(node.Template!)">Compose into…</button></li>
<li><button class="dropdown-item" @onclick="() => OpenMoveTemplateDialog(node.EntityId, node.Label)">Move to Folder…</button></li>
<li><hr class="dropdown-divider" /></li>
<li><button class="dropdown-item text-danger" @onclick="() => DeleteTemplate(node.Template!)">Delete</button></li>
break;
case TmplNodeKind.Composition:
<li><button class="dropdown-item" @onclick='() => NavigationManager.NavigateTo($"/design/templates/{node.Composition!.ComposedTemplateId}")'>Open composed template</button></li>
<li><button class="dropdown-item" @onclick="() => RenameComposition(node.Composition!)">Rename…</button></li>
<li><hr class="dropdown-divider" /></li>
<li><button class="dropdown-item text-danger" @onclick="() => DeleteComposition(node.Composition!)">Delete</button></li>
break;
}
</ul>
</span>
};
private void OnTreeNodeSelected(object? key)
{
if (key is not string s) return;
if (s.StartsWith("t:") && int.TryParse(s[2..], out var tid))
{
NavigationManager.NavigateTo($"/design/templates/{tid}");
}
else if (s.StartsWith("c:") && int.TryParse(s[2..], out var cid))
{
var comp = _templates.SelectMany(t => t.Compositions).FirstOrDefault(c => c.Id == cid);
if (comp != null)
{
NavigationManager.NavigateTo($"/design/templates/{comp.ComposedTemplateId}");
}
}
// Folder selection is intentionally a no-op (use right-click for folder actions).
}
private RenderFragment RenderNodeContextMenu(TmplNode node) => __builder => private RenderFragment RenderNodeContextMenu(TmplNode node) => __builder =>
{ {
switch (node.Kind) switch (node.Kind)