feat(m9/T23a): folder sibling reorder (ReorderFolderAsync + command + handler)

This commit is contained in:
Joseph Doherty
2026-06-18 11:00:57 -04:00
parent 0bd5e0986f
commit e3bc19c673
6 changed files with 245 additions and 2 deletions
@@ -1,8 +1,11 @@
using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums;
namespace ZB.MOM.WW.ScadaBridge.Commons.Messages.Management;
public record ListTemplateFoldersCommand;
public record CreateTemplateFolderCommand(string Name, int? ParentFolderId);
public record RenameTemplateFolderCommand(int FolderId, string NewName);
public record MoveTemplateFolderCommand(int FolderId, int? NewParentFolderId);
public record ReorderTemplateFolderCommand(int FolderId, ReorderDirection Direction);
public record DeleteTemplateFolderCommand(int FolderId);
public record MoveTemplateToFolderCommand(int TemplateId, int? NewFolderId);
@@ -0,0 +1,16 @@
namespace ZB.MOM.WW.ScadaBridge.Commons.Types.Enums;
/// <summary>
/// Direction for a sibling reorder operation. Ordering is ascending by
/// <c>SortOrder</c>, so <see cref="Up"/> moves an item toward a lower sort
/// order (earlier in the list) and <see cref="Down"/> toward a higher sort
/// order (later in the list).
/// </summary>
public enum ReorderDirection
{
/// <summary>Move toward a lower sort order (swap with the previous sibling).</summary>
Up,
/// <summary>Move toward a higher sort order (swap with the next sibling).</summary>
Down
}
@@ -632,7 +632,10 @@ public class TemplateEngineRepository : ITemplateEngineRepository
/// <inheritdoc />
public async Task<IReadOnlyList<TemplateFolder>> GetAllFoldersAsync(CancellationToken cancellationToken = default)
=> await _context.TemplateFolders.ToListAsync(cancellationToken);
=> await _context.TemplateFolders
.OrderBy(f => f.SortOrder)
.ThenBy(f => f.Name)
.ToListAsync(cancellationToken);
/// <inheritdoc />
public async Task AddFolderAsync(TemplateFolder folder, CancellationToken cancellationToken = default)
@@ -198,7 +198,7 @@ public class ManagementActor : ReceiveActor
or CreateApiMethodCommand or UpdateApiMethodCommand or DeleteApiMethodCommand
or UpdateAreaCommand
or CreateTemplateFolderCommand or RenameTemplateFolderCommand
or MoveTemplateFolderCommand or DeleteTemplateFolderCommand
or MoveTemplateFolderCommand or ReorderTemplateFolderCommand or DeleteTemplateFolderCommand
or MoveTemplateToFolderCommand
or ExportBundleCommand => Roles.Designer,
@@ -267,6 +267,7 @@ public class ManagementActor : ReceiveActor
CreateTemplateFolderCommand cmd => await HandleCreateTemplateFolder(sp, cmd, user.Username),
RenameTemplateFolderCommand cmd => await HandleRenameTemplateFolder(sp, cmd, user.Username),
MoveTemplateFolderCommand cmd => await HandleMoveTemplateFolder(sp, cmd, user.Username),
ReorderTemplateFolderCommand cmd => await HandleReorderTemplateFolder(sp, cmd, user.Username),
DeleteTemplateFolderCommand cmd => await HandleDeleteTemplateFolder(sp, cmd, user.Username),
MoveTemplateToFolderCommand cmd => await HandleMoveTemplateToFolder(sp, cmd, user.Username),
@@ -619,6 +620,13 @@ public class ManagementActor : ReceiveActor
return result.IsSuccess ? result.Value : throw new ManagementCommandException(result.Error);
}
private static async Task<object?> HandleReorderTemplateFolder(IServiceProvider sp, ReorderTemplateFolderCommand cmd, string user)
{
var svc = sp.GetRequiredService<TemplateFolderService>();
var result = await svc.ReorderFolderAsync(cmd.FolderId, cmd.Direction, user);
return result.IsSuccess ? result.Value : throw new ManagementCommandException(result.Error);
}
private static async Task<object?> HandleDeleteTemplateFolder(IServiceProvider sp, DeleteTemplateFolderCommand cmd, string user)
{
var svc = sp.GetRequiredService<TemplateFolderService>();
@@ -2,6 +2,7 @@ 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;
using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums;
namespace ZB.MOM.WW.ScadaBridge.TemplateEngine.Services;
@@ -152,6 +153,56 @@ public class TemplateFolderService
return Result<TemplateFolder>.Success(folder);
}
/// <summary>
/// Reorders a template folder relative to its siblings under the same parent by
/// swapping <see cref="TemplateFolder.SortOrder"/> with the adjacent sibling in the
/// requested direction. Siblings are ordered ascending by <see cref="TemplateFolder.SortOrder"/>
/// then <see cref="TemplateFolder.Name"/>; <see cref="ReorderDirection.Up"/> swaps with the
/// previous sibling and <see cref="ReorderDirection.Down"/> with the next. Reordering past
/// either end is a no-op (returns success without persisting or auditing).
/// </summary>
/// <param name="folderId">Id of the folder to reorder.</param>
/// <param name="direction">Direction to move the folder within its sibling set.</param>
/// <param name="user">Username of the actor performing the operation (for audit).</param>
/// <param name="cancellationToken">Cancellation token.</param>
/// <returns>A task that resolves to the reordered <see cref="TemplateFolder"/>, or a failure result if the folder is not found.</returns>
public async Task<Result<TemplateFolder>> ReorderFolderAsync(
int folderId, ReorderDirection direction, string user,
CancellationToken cancellationToken = default)
{
var folder = await _repository.GetFolderByIdAsync(folderId, cancellationToken);
if (folder == null)
return Result<TemplateFolder>.Failure($"Folder with ID {folderId} not found.");
var all = await _repository.GetAllFoldersAsync(cancellationToken);
// Deterministic sibling ordering: ascending SortOrder, then Name as tie-breaker.
var siblings = all
.Where(f => f.ParentFolderId == folder.ParentFolderId)
.OrderBy(f => f.SortOrder)
.ThenBy(f => f.Name, StringComparer.OrdinalIgnoreCase)
.ToList();
var index = siblings.FindIndex(f => f.Id == folderId);
var adjacentIndex = direction == ReorderDirection.Up ? index - 1 : index + 1;
// At the end of the sibling set in the requested direction: no-op.
if (adjacentIndex < 0 || adjacentIndex >= siblings.Count)
return Result<TemplateFolder>.Success(folder);
var adjacent = siblings[adjacentIndex];
// Swap sort order with the adjacent sibling.
(folder.SortOrder, adjacent.SortOrder) = (adjacent.SortOrder, folder.SortOrder);
await _repository.UpdateFolderAsync(folder, cancellationToken);
await _repository.UpdateFolderAsync(adjacent, cancellationToken);
await _repository.SaveChangesAsync(cancellationToken);
await _auditService.LogAsync(user, "Reorder", "TemplateFolder", folder.Id.ToString(), folder.Name, folder, cancellationToken);
return Result<TemplateFolder>.Success(folder);
}
/// <summary>
/// Deletes an empty template folder. Fails if the folder contains sub-folders or templates.
/// </summary>