diff --git a/src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Pages/Design/Templates.razor b/src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Pages/Design/Templates.razor index 90c4f73f..8c0d4a2e 100644 --- a/src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Pages/Design/Templates.razor +++ b/src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Pages/Design/Templates.razor @@ -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 @@ -
+ @* 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. *@ +
- No templates yet. Use the buttons above to create a folder or template. + No templates yet. Use the buttons above (or right-click here) to create a folder or template.
+ + @if (_showRootMenu) + { +
+ + } }
@@ -240,6 +258,11 @@ + + + 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 OrderedSiblings(int folderId) + { + var folder = _folders.FirstOrDefault(f => f.Id == folderId); + if (folder == null) return new List(); + 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; diff --git a/src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Shared/TreeView.razor b/src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Shared/TreeView.razor index 40954c1a..0d1be6fd 100644 --- a/src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Shared/TreeView.razor +++ b/src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Shared/TreeView.razor @@ -48,7 +48,7 @@ else aria-expanded="@(isBranch ? (isExpanded ? "true" : "false") : null)" aria-selected="@(isSelected ? "true" : null)">
+ @oncontextmenu="(e) => OnContextMenu(e, item)" @oncontextmenu:preventDefault="@(ContextMenu != null)" @oncontextmenu:stopPropagation="@(ContextMenu != null)"> @if (isBranch) { diff --git a/tests/ZB.MOM.WW.ScadaBridge.CentralUI.Tests/TemplatesPageTests.cs b/tests/ZB.MOM.WW.ScadaBridge.CentralUI.Tests/TemplatesPageTests.cs index d8e067ce..ea535ca8 100644 --- a/tests/ZB.MOM.WW.ScadaBridge.CentralUI.Tests/TemplatesPageTests.cs +++ b/tests/ZB.MOM.WW.ScadaBridge.CentralUI.Tests/TemplatesPageTests.cs @@ -1,12 +1,14 @@ using System.Security.Claims; using ZB.MOM.WW.ScadaBridge.Security; using Bunit; +using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components.Authorization; using Microsoft.Extensions.DependencyInjection; using NSubstitute; using ZB.MOM.WW.ScadaBridge.Commons.Entities.Templates; using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Repositories; using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Services; +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; @@ -176,6 +178,149 @@ public class TemplatesPageTests : BunitContext Assert.Contains("BetaDevice", cut.Markup); } + // ======================================================================== + // M9-T23b: folder sibling reorder menu items + root context menu + // ======================================================================== + + // Seeds two root sibling folders (Alpha @ SortOrder 0, Beta @ SortOrder 1) and + // wires the repo so TemplateFolderService.ReorderFolderAsync runs end-to-end. + private (TemplateFolder alpha, TemplateFolder beta) SeedTwoRootSiblings() + { + var alpha = new TemplateFolder("Alpha") { Id = 1, ParentFolderId = null, SortOrder = 0 }; + var beta = new TemplateFolder("Beta") { Id = 2, ParentFolderId = null, SortOrder = 1 }; + var all = new List { alpha, beta }; + + _repo.GetAllTemplatesAsync(Arg.Any()) + .Returns(Task.FromResult>(new List