From c60aad9df4d5645da865e04576b1832a534e306c Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Mon, 11 May 2026 11:20:42 -0400 Subject: [PATCH] feat(ui/templates): native HTML5 drag-drop reorganization --- .../Components/Pages/Design/Templates.razor | 179 +++++++++++++----- 1 file changed, 134 insertions(+), 45 deletions(-) diff --git a/src/ScadaLink.CentralUI/Components/Pages/Design/Templates.razor b/src/ScadaLink.CentralUI/Components/Pages/Design/Templates.razor index 17deddd..2782a52 100644 --- a/src/ScadaLink.CentralUI/Components/Pages/Design/Templates.razor +++ b/src/ScadaLink.CentralUI/Components/Pages/Design/Templates.razor @@ -144,23 +144,26 @@
- - - @RenderNodeLabel(node) - - - @RenderNodeContextMenu(node) - - - No templates yet. Use the buttons above to create a folder or template. - - +
+ + + @RenderNodeLabel(node) + + + @RenderNodeContextMenu(node) + + + No templates yet. Use the buttons above to create a folder or template. + + +
@@ -389,6 +392,75 @@ private TreeView _tree = default!; + // ---- Drag-and-drop state ---- + private (TmplNodeKind kind, int id)? _dragPayload; + private string? _dragOverKey; + + private void OnDragStart(TmplNode node) + { + if (node.Kind == TmplNodeKind.Composition) return; + _dragPayload = (node.Kind, node.EntityId); + } + + private void OnDragEnd() + { + _dragPayload = null; + _dragOverKey = null; + } + + private void OnDragEnter(TmplNode targetFolder) + { + if (_dragPayload == null) return; + if (targetFolder.Kind != TmplNodeKind.Folder) return; + _dragOverKey = targetFolder.Key; + } + + private void OnDragLeave(TmplNode targetFolder) + { + if (_dragOverKey == targetFolder.Key) _dragOverKey = null; + } + + private async Task OnDrop(TmplNode targetFolder) + { + if (_dragPayload is not { } payload) return; + if (targetFolder.Kind != TmplNodeKind.Folder) return; + + var user = await GetCurrentUserAsync(); + if (payload.kind == TmplNodeKind.Folder) + { + var result = await TemplateFolderService.MoveFolderAsync(payload.id, targetFolder.EntityId, user); + if (result.IsFailure) _toast.ShowError(result.Error); + } + else if (payload.kind == TmplNodeKind.Template) + { + var result = await TemplateService.MoveTemplateAsync(payload.id, targetFolder.EntityId, user); + if (result.IsFailure) _toast.ShowError(result.Error); + } + + OnDragEnd(); + await LoadTemplatesAsync(); + } + + private async Task OnDropOnRoot() + { + if (_dragPayload is not { } payload) return; + + var user = await GetCurrentUserAsync(); + if (payload.kind == TmplNodeKind.Folder) + { + var result = await TemplateFolderService.MoveFolderAsync(payload.id, null, user); + if (result.IsFailure) _toast.ShowError(result.Error); + } + else if (payload.kind == TmplNodeKind.Template) + { + var result = await TemplateService.MoveTemplateAsync(payload.id, null, user); + if (result.IsFailure) _toast.ShowError(result.Error); + } + + OnDragEnd(); + await LoadTemplatesAsync(); + } + private RenderFragment RenderTemplateDetail() => __builder => {
@@ -519,34 +591,51 @@ private RenderFragment RenderNodeLabel(TmplNode node) => __builder => { - switch (node.Kind) - { - case TmplNodeKind.Folder: - 📁 - @node.Label - @node.Children.Count - break; - case TmplNodeKind.Template: - @node.Label - if (node.Template?.ParentTemplateId is int pid) - { - inherits @(_templates.FirstOrDefault(t => t.Id == pid)?.Name) - } - - @node.Template!.Attributes.Count attr, - @node.Template.Alarms.Count alm, - @node.Template.Scripts.Count scr - - if (node.Template.Compositions.Count > 0) - { - @node.Template.Compositions.Count comp - } - break; - case TmplNodeKind.Composition: - @node.Label - → @(_templates.FirstOrDefault(t => t.Id == node.Composition!.ComposedTemplateId)?.Name ?? $"#{node.Composition!.ComposedTemplateId}") - break; - } + var draggable = node.Kind != TmplNodeKind.Composition; + var isDropTarget = node.Kind == TmplNodeKind.Folder; + var classes = "d-inline-block " + (_dragOverKey == node.Key ? "bg-info bg-opacity-25" : ""); + var style = node.Kind == TmplNodeKind.Composition && _dragPayload != null + ? "opacity: 0.5;" : ""; + +
+ @switch (node.Kind) + { + case TmplNodeKind.Folder: + 📁 + @node.Label + @node.Children.Count + break; + case TmplNodeKind.Template: + @node.Label + if (node.Template?.ParentTemplateId is int pid) + { + inherits @(_templates.FirstOrDefault(t => t.Id == pid)?.Name) + } + + @node.Template!.Attributes.Count attr, + @node.Template.Alarms.Count alm, + @node.Template.Scripts.Count scr + + if (node.Template.Compositions.Count > 0) + { + @node.Template.Compositions.Count comp + } + break; + case TmplNodeKind.Composition: + var composedName = _templates.FirstOrDefault(t => t.Id == node.Composition!.ComposedTemplateId)?.Name + ?? $"#{node.Composition!.ComposedTemplateId}"; + @node.Label + → @composedName + break; + } +
}; private async Task OnTreeNodeSelected(object? key)