feat(m9/T23b): folder reorder menu items + root context menu

This commit is contained in:
Joseph Doherty
2026-06-18 11:12:34 -04:00
parent efe3ada03d
commit 314c7dea23
3 changed files with 234 additions and 3 deletions
@@ -2,6 +2,7 @@
@using ZB.MOM.WW.ScadaBridge.Security
@using ZB.MOM.WW.ScadaBridge.Commons.Entities.Templates
@using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Repositories
@using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums
@using ZB.MOM.WW.ScadaBridge.CentralUI.Components.Shared
@using ZB.MOM.WW.ScadaBridge.TemplateEngine
@using ZB.MOM.WW.ScadaBridge.TemplateEngine.Services
@@ -94,7 +95,13 @@
</div>
</div>
<div style="max-height: calc(100vh - 200px); overflow-y: auto; padding: 4px;">
@* The root zone fills the scroll area so a right-click on empty space
(or below the tree) opens a root-level context menu — New Folder /
New Template at root. Node-level right-clicks are handled by the
TreeView's own context menu (preventDefault'd there), so they don't
bubble to this handler. *@
<div class="tv-root-zone" style="max-height: calc(100vh - 200px); min-height: 120px; overflow-y: auto; padding: 4px;"
@oncontextmenu="OpenRootContextMenu" @oncontextmenu:preventDefault="true">
<TemplateFolderTree @ref="_tree"
Folders="_folders"
Templates="_templates"
@@ -109,10 +116,21 @@
@RenderNodeContextMenu(node)
</ContextMenu>
<EmptyContent>
<span class="text-muted fst-italic">No templates yet. Use the buttons above to create a folder or template.</span>
<span class="text-muted fst-italic">No templates yet. Use the buttons above (or right-click here) to create a folder or template.</span>
</EmptyContent>
</TemplateFolderTree>
</div>
@if (_showRootMenu)
{
<div class="tv-ctx-overlay" @onclick="DismissRootContextMenu"
style="position:fixed;top:0;left:0;width:100vw;height:100vh;z-index:1049;background:transparent;"></div>
<div class="dropdown-menu show tv-root-menu" @onclick="DismissRootContextMenu"
style="position:fixed;top:@(_rootMenuY)px;left:@(_rootMenuX)px;z-index:1050;">
<button class="dropdown-item" @onclick="() => OpenNewFolderDialog(null)">New Folder</button>
<button class="dropdown-item" @onclick='() => NavigationManager.NavigateTo("/design/templates/create")'>New Template</button>
</div>
}
}
</div>
@@ -240,6 +258,11 @@
<button class="dropdown-item" @onclick="() => OpenRenameFolderDialog(node.Id, node.Name)">Rename</button>
<button class="dropdown-item" @onclick="() => OpenMoveFolderDialog(node.Id, node.Name)">Move to Folder…</button>
<div class="dropdown-divider"></div>
<button class="dropdown-item" disabled="@IsFirstSibling(node.Id)"
@onclick="() => ReorderFolder(node.Id, ReorderDirection.Up)">Move up</button>
<button class="dropdown-item" disabled="@IsLastSibling(node.Id)"
@onclick="() => ReorderFolder(node.Id, ReorderDirection.Down)">Move down</button>
<div class="dropdown-divider"></div>
<button class="dropdown-item text-danger" @onclick="() => DeleteFolder(node.Id, node.Name)">Delete</button>
break;
@@ -381,6 +404,69 @@
}
}
// ---- Sibling reorder (T23b) ----
// Dispatches the reorder the same way Move/Rename do — directly through
// TemplateFolderService (the backend swaps SortOrder with the adjacent
// same-parent sibling, no-op at the ends) — then reloads so the new order
// shows. Folder listing is ordered by SortOrder, so the swap is visible
// after reload.
private async Task ReorderFolder(int folderId, ReorderDirection direction)
{
var user = await GetCurrentUserAsync();
var result = await TemplateFolderService.ReorderFolderAsync(folderId, direction, user);
if (result.IsSuccess)
{
await LoadTemplatesAsync();
}
else
{
_toast.ShowError(result.Error);
}
}
// Sibling-order helpers for disabling Move up/down at the ends. Mirrors the
// backend ordering (ascending SortOrder, then Name). Returns false when the
// folder is unknown so the menu stays enabled and the backend no-op guards.
private bool IsFirstSibling(int folderId)
{
var ordered = OrderedSiblings(folderId);
return ordered.Count > 0 && ordered[0].Id == folderId;
}
private bool IsLastSibling(int folderId)
{
var ordered = OrderedSiblings(folderId);
return ordered.Count > 0 && ordered[^1].Id == folderId;
}
private List<TemplateFolder> OrderedSiblings(int folderId)
{
var folder = _folders.FirstOrDefault(f => f.Id == folderId);
if (folder == null) return new List<TemplateFolder>();
return _folders
.Where(f => f.ParentFolderId == folder.ParentFolderId)
.OrderBy(f => f.SortOrder)
.ThenBy(f => f.Name, StringComparer.OrdinalIgnoreCase)
.ToList();
}
// ---- Root-level context menu (T23b) ----
// Right-click on the tree zone (empty space or below the tree) offers
// New Folder / New Template at root. Node right-clicks are handled by the
// TreeView's own context menu and never reach here.
private bool _showRootMenu;
private double _rootMenuX;
private double _rootMenuY;
private void OpenRootContextMenu(MouseEventArgs e)
{
_rootMenuX = e.ClientX;
_rootMenuY = e.ClientY;
_showRootMenu = true;
}
private void DismissRootContextMenu() => _showRootMenu = false;
// Rename folder dialog state
private bool _showRenameFolderDialog;
private int _renameFolderId;
@@ -48,7 +48,7 @@ else
aria-expanded="@(isBranch ? (isExpanded ? "true" : "false") : null)"
aria-selected="@(isSelected ? "true" : null)">
<div class="@rowClasses" style="padding-left: @(depth * IndentPx)px; --tv-depth: @depth;"
@oncontextmenu="(e) => OnContextMenu(e, item)" @oncontextmenu:preventDefault="@(ContextMenu != null)">
@oncontextmenu="(e) => OnContextMenu(e, item)" @oncontextmenu:preventDefault="@(ContextMenu != null)" @oncontextmenu:stopPropagation="@(ContextMenu != null)">
@if (isBranch)
{
<span class="tv-toggle" @onclick="() => ToggleExpand(key)" @onclick:stopPropagation><i class="bi bi-chevron-right"></i></span>