feat(ui/templates): native HTML5 drag-drop reorganization
This commit is contained in:
@@ -144,23 +144,26 @@
|
||||
</div>
|
||||
|
||||
<div style="max-height: calc(100vh - 160px); overflow-y: auto;">
|
||||
<TreeView @ref="_tree" TItem="TmplNode" Items="_treeRoots"
|
||||
ChildrenSelector="n => n.Children"
|
||||
HasChildrenSelector="n => n.Kind != TmplNodeKind.Composition && n.Children.Count > 0"
|
||||
KeySelector="n => (object)n.Key"
|
||||
StorageKey="templates-tree"
|
||||
Selectable="true"
|
||||
SelectedKeyChanged="OnTreeNodeSelected">
|
||||
<NodeContent Context="node">
|
||||
@RenderNodeLabel(node)
|
||||
</NodeContent>
|
||||
<ContextMenu Context="node">
|
||||
@RenderNodeContextMenu(node)
|
||||
</ContextMenu>
|
||||
<EmptyContent>
|
||||
<span class="text-muted fst-italic">No templates yet. Use the buttons above to create a folder or template.</span>
|
||||
</EmptyContent>
|
||||
</TreeView>
|
||||
<div @ondragover:preventDefault="true" @ondrop="OnDropOnRoot"
|
||||
style="min-height: 100%; padding: 4px;">
|
||||
<TreeView @ref="_tree" TItem="TmplNode" Items="_treeRoots"
|
||||
ChildrenSelector="n => n.Children"
|
||||
HasChildrenSelector="n => n.Kind != TmplNodeKind.Composition && n.Children.Count > 0"
|
||||
KeySelector="n => (object)n.Key"
|
||||
StorageKey="templates-tree"
|
||||
Selectable="true"
|
||||
SelectedKeyChanged="OnTreeNodeSelected">
|
||||
<NodeContent Context="node">
|
||||
@RenderNodeLabel(node)
|
||||
</NodeContent>
|
||||
<ContextMenu Context="node">
|
||||
@RenderNodeContextMenu(node)
|
||||
</ContextMenu>
|
||||
<EmptyContent>
|
||||
<span class="text-muted fst-italic">No templates yet. Use the buttons above to create a folder or template.</span>
|
||||
</EmptyContent>
|
||||
</TreeView>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -389,6 +392,75 @@
|
||||
|
||||
private TreeView<TmplNode> _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 =>
|
||||
{
|
||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||
@@ -519,34 +591,51 @@
|
||||
|
||||
private RenderFragment RenderNodeLabel(TmplNode node) => __builder =>
|
||||
{
|
||||
switch (node.Kind)
|
||||
{
|
||||
case TmplNodeKind.Folder:
|
||||
<span class="me-1">📁</span>
|
||||
<span>@node.Label</span>
|
||||
<span class="badge bg-light text-dark ms-2">@node.Children.Count</span>
|
||||
break;
|
||||
case TmplNodeKind.Template:
|
||||
<strong>@node.Label</strong>
|
||||
if (node.Template?.ParentTemplateId is int pid)
|
||||
{
|
||||
<span class="text-muted small ms-1">inherits @(_templates.FirstOrDefault(t => t.Id == pid)?.Name)</span>
|
||||
}
|
||||
<span class="badge bg-light text-dark ms-2">
|
||||
@node.Template!.Attributes.Count attr,
|
||||
@node.Template.Alarms.Count alm,
|
||||
@node.Template.Scripts.Count scr
|
||||
</span>
|
||||
if (node.Template.Compositions.Count > 0)
|
||||
{
|
||||
<span class="badge bg-info text-dark ms-1">@node.Template.Compositions.Count comp</span>
|
||||
}
|
||||
break;
|
||||
case TmplNodeKind.Composition:
|
||||
<span>@node.Label</span>
|
||||
<span class="text-muted small ms-1">→ @(_templates.FirstOrDefault(t => t.Id == node.Composition!.ComposedTemplateId)?.Name ?? $"#{node.Composition!.ComposedTemplateId}")</span>
|
||||
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;" : "";
|
||||
|
||||
<div class="@classes" style="@style"
|
||||
draggable="@(draggable ? "true" : "false")"
|
||||
@ondragstart="() => OnDragStart(node)"
|
||||
@ondragend="OnDragEnd"
|
||||
@ondragenter="() => OnDragEnter(node)"
|
||||
@ondragleave="() => OnDragLeave(node)"
|
||||
@ondragover:preventDefault="@isDropTarget"
|
||||
@ondrop="() => OnDrop(node)">
|
||||
@switch (node.Kind)
|
||||
{
|
||||
case TmplNodeKind.Folder:
|
||||
<span class="me-1">📁</span>
|
||||
<span>@node.Label</span>
|
||||
<span class="badge bg-light text-dark ms-2">@node.Children.Count</span>
|
||||
break;
|
||||
case TmplNodeKind.Template:
|
||||
<strong>@node.Label</strong>
|
||||
if (node.Template?.ParentTemplateId is int pid)
|
||||
{
|
||||
<span class="text-muted small ms-1">inherits @(_templates.FirstOrDefault(t => t.Id == pid)?.Name)</span>
|
||||
}
|
||||
<span class="badge bg-light text-dark ms-2">
|
||||
@node.Template!.Attributes.Count attr,
|
||||
@node.Template.Alarms.Count alm,
|
||||
@node.Template.Scripts.Count scr
|
||||
</span>
|
||||
if (node.Template.Compositions.Count > 0)
|
||||
{
|
||||
<span class="badge bg-info text-dark ms-1">@node.Template.Compositions.Count comp</span>
|
||||
}
|
||||
break;
|
||||
case TmplNodeKind.Composition:
|
||||
var composedName = _templates.FirstOrDefault(t => t.Id == node.Composition!.ComposedTemplateId)?.Name
|
||||
?? $"#{node.Composition!.ComposedTemplateId}";
|
||||
<span>@node.Label</span>
|
||||
<span class="text-muted small ms-1">→ @composedName</span>
|
||||
break;
|
||||
}
|
||||
</div>
|
||||
};
|
||||
|
||||
private async Task OnTreeNodeSelected(object? key)
|
||||
|
||||
Reference in New Issue
Block a user