feat(ui/templates): native HTML5 drag-drop reorganization
This commit is contained in:
@@ -144,6 +144,8 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style="max-height: calc(100vh - 160px); overflow-y: auto;">
|
<div style="max-height: calc(100vh - 160px); overflow-y: auto;">
|
||||||
|
<div @ondragover:preventDefault="true" @ondrop="OnDropOnRoot"
|
||||||
|
style="min-height: 100%; padding: 4px;">
|
||||||
<TreeView @ref="_tree" TItem="TmplNode" Items="_treeRoots"
|
<TreeView @ref="_tree" TItem="TmplNode" Items="_treeRoots"
|
||||||
ChildrenSelector="n => n.Children"
|
ChildrenSelector="n => n.Children"
|
||||||
HasChildrenSelector="n => n.Kind != TmplNodeKind.Composition && n.Children.Count > 0"
|
HasChildrenSelector="n => n.Kind != TmplNodeKind.Composition && n.Children.Count > 0"
|
||||||
@@ -163,6 +165,7 @@
|
|||||||
</TreeView>
|
</TreeView>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="col-md-8 col-lg-9">
|
<div class="col-md-8 col-lg-9">
|
||||||
@if (_selectedTemplate == null)
|
@if (_selectedTemplate == null)
|
||||||
@@ -389,6 +392,75 @@
|
|||||||
|
|
||||||
private TreeView<TmplNode> _tree = default!;
|
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 =>
|
private RenderFragment RenderTemplateDetail() => __builder =>
|
||||||
{
|
{
|
||||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||||
@@ -519,7 +591,21 @@
|
|||||||
|
|
||||||
private RenderFragment RenderNodeLabel(TmplNode node) => __builder =>
|
private RenderFragment RenderNodeLabel(TmplNode node) => __builder =>
|
||||||
{
|
{
|
||||||
switch (node.Kind)
|
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:
|
case TmplNodeKind.Folder:
|
||||||
<span class="me-1">📁</span>
|
<span class="me-1">📁</span>
|
||||||
@@ -543,10 +629,13 @@
|
|||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case TmplNodeKind.Composition:
|
case TmplNodeKind.Composition:
|
||||||
|
var composedName = _templates.FirstOrDefault(t => t.Id == node.Composition!.ComposedTemplateId)?.Name
|
||||||
|
?? $"#{node.Composition!.ComposedTemplateId}";
|
||||||
<span>@node.Label</span>
|
<span>@node.Label</span>
|
||||||
<span class="text-muted small ms-1">→ @(_templates.FirstOrDefault(t => t.Id == node.Composition!.ComposedTemplateId)?.Name ?? $"#{node.Composition!.ComposedTemplateId}")</span>
|
<span class="text-muted small ms-1">→ @composedName</span>
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
</div>
|
||||||
};
|
};
|
||||||
|
|
||||||
private async Task OnTreeNodeSelected(object? key)
|
private async Task OnTreeNodeSelected(object? key)
|
||||||
|
|||||||
Reference in New Issue
Block a user