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