Files
Tasks.Module/Components/Sidebar/FolderSidebar.razor
T
2026-08-07 02:17:32 +03:00

538 lines
17 KiB
Plaintext

@using MudBlazor
@using Tasks.Module.Models
@using Tasks.Module.Services
<MudStack Orientation="Orientation.Vertical" Spacing="0" Style="height: 100%;">
<MudStack Row="true" Justify="Justify.FlexStart" AlignItems="AlignItems.Center" Class="pa-2">
<MudText Typo="Typo.caption" Color="Color.Default" Class="ml-2">TASKS SHEETS</MudText>
<MudSpacer />
<MudStack Row="true" Spacing="1">
<MudIconButton Icon="@Icons.Material.Filled.CreateNewFolder" Size="Size.Small" Color="Color.Inherit" OnClick="@OnCreateFolder" Title="New Folder" />
<MudIconButton Icon="@Icons.Material.Filled.NoteAdd" Size="Size.Small" Color="Color.Inherit" OnClick="@OnCreateFile" Title="New Tasks Sheet" />
</MudStack>
</MudStack>
<div style="flex: 1; overflow-y: auto; min-height: 0;"
class="@(_rootDragOver ? "root-drop-zone-active" : "")"
@ondragenter="@OnRootDragEnter"
@ondragenter:preventDefault="true"
@ondragover="@OnRootDragOver"
@ondragover:preventDefault="true"
@ondragleave="@OnRootDragLeave"
@ondrop="@OnRootDrop"
@ondrop:preventDefault="true">
@if (_loading)
{
<MudProgressLinear Indeterminate="true" Class="my-2" />
}
else if (_treeItems.Any())
{
<MudStack Spacing="0" Class="pa-2">
@foreach (var item in _treeItems)
{
<FolderTreeItem Item="@item"
OnItemClick="@OnItemClick"
OnItemExpand="@OnItemExpand"
OnContextMenuWithArgs="@OnContextMenuWithArgs"
OnDragStart="@OnDragStartHandler"
OnDragEnd="@OnDragEndHandler"
OnItemDrop="@OnItemDropHandler" />
}
</MudStack>
}
else
{
<MudText Class="pa-4" Typo="Typo.body2" Color="Color.Secondary" Align="Align.Center">
No tasks sheets yet
</MudText>
}
</div>
</MudStack>
<style>
.root-drop-zone-active {
background-color: var(--mud-palette-primary-hover);
outline: 2px dashed var(--mud-palette-primary);
outline-offset: -4px;
}
</style>
@if (_contextMenuOpen)
{
<div Style="@GetContextMenuStyle()" @onclick:stopPropagation="true">
<MudPaper Elevation="4" Class="pa-2" Style="min-width: 160px;">
<MudStack Spacing="0">
<MudButton FullWidth="true" StartIcon="@Icons.Material.Filled.CreateNewFolder" OnClick="@CreateSubFolder" Disabled="@(_contextItemType != TreeItemType.Folder)">
New Subfolder
</MudButton>
<MudButton FullWidth="true" StartIcon="@Icons.Material.Filled.NoteAdd" OnClick="@CreateSubFile" Disabled="@(_contextItemType != TreeItemType.Folder)">
New Task List
</MudButton>
<MudDivider Class="my-1" />
<MudButton FullWidth="true" StartIcon="@Icons.Material.Filled.Delete" Color="Color.Error" OnClick="@DeleteSelected" Disabled="@(_contextItemId == null)">
Delete
</MudButton>
</MudStack>
</MudPaper>
</div>
<div style="position: fixed; top: 0; left: 0; right: 0; bottom: 0; z-index: 1000;" @onclick="CloseContextMenu" @oncontextmenu:preventDefault="true"></div>
}
@code {
private bool _loading = true;
private List<TreeItemDto> _treeItems = new();
private bool _contextMenuOpen = false;
private string? _contextItemId = null;
private TreeItemType _contextItemType = TreeItemType.Folder;
private double _contextMenuX = 0;
private double _contextMenuY = 0;
private bool _rootDragOver = false;
private TreeItemDto? _draggingItem = null;
private bool _isMoving = false;
private int _dragSessionId = 0;
[Inject] private IFolderService FolderService { get; set; } = null!;
[Inject] private ISnackbar Snackbar { get; set; } = null!;
[Inject] private IDialogService DialogService { get; set; } = null!;
[Inject] private NavigationManager NavigationManager { get; set; } = null!;
protected override async Task OnInitializedAsync()
{
await RefreshTree();
}
private async Task RefreshTree()
{
_loading = true;
try
{
var newTreeItems = new List<TreeItemDto>();
var rootFolders = await FolderService.GetAllRootFoldersAsync();
foreach (var folder in rootFolders)
{
var treeItem = await BuildTreeItem(folder);
newTreeItems.Add(treeItem);
}
var rootTasksSheets = await FolderService.GetRootTasksSheetsAsync();
foreach (var ts in rootTasksSheets)
{
newTreeItems.Add(new TreeItemDto
{
Id = ts.Id,
Name = ts.Title,
Type = TreeItemType.TasksSheet,
ParentId = null,
IsExpanded = false,
Children = new List<TreeItemDto>()
});
}
_treeItems = newTreeItems;
}
catch (Exception ex)
{
Snackbar.Add($"Error: {ex.Message}", Severity.Error);
}
finally
{
_loading = false;
}
}
private async Task<TreeItemDto> BuildTreeItem(FolderDto folder)
{
var item = new TreeItemDto
{
Id = folder.Id,
Name = folder.Name,
Type = TreeItemType.Folder,
ParentId = folder.ParentId,
IsExpanded = false,
Children = new List<TreeItemDto>()
};
foreach (var child in folder.Children)
{
var childItem = await BuildTreeItem(child);
item.Children.Add(childItem);
}
try
{
var tasksSheets = await FolderService.GetTasksSheetsInFolderAsync(folder.Id);
foreach (var ts in tasksSheets)
{
item.Children.Add(new TreeItemDto
{
Id = ts.Id,
Name = ts.Title,
Type = TreeItemType.TasksSheet,
ParentId = folder.Id,
IsExpanded = false,
Children = new List<TreeItemDto>()
});
}
}
catch
{
}
return item;
}
private async Task OnItemClick(TreeItemDto item)
{
if (item.Type == TreeItemType.Folder)
{
item.IsExpanded = !item.IsExpanded;
if (item.IsExpanded && !item.Children.Any())
{
await LoadFolderChildren(item);
}
}
else if (item.Type == TreeItemType.TasksSheet)
{
NavigationManager.NavigateTo($"/tasks/taskssheet/{item.Id}");
}
}
private async Task OnItemExpand(TreeItemDto item)
{
if (item.Type != TreeItemType.Folder) return;
item.IsExpanded = !item.IsExpanded;
if (item.IsExpanded && !item.Children.Any())
{
await LoadFolderChildren(item);
}
}
private async Task LoadFolderChildren(TreeItemDto item)
{
try
{
var folder = await FolderService.GetFolderByIdAsync(item.Id);
if (folder != null)
{
item.Children.Clear();
foreach (var child in folder.Children)
{
var childItem = await BuildTreeItem(child);
item.Children.Add(childItem);
}
var tasksSheets = await FolderService.GetTasksSheetsInFolderAsync(item.Id);
foreach (var ts in tasksSheets)
{
item.Children.Add(new TreeItemDto
{
Id = ts.Id,
Name = ts.Title,
Type = TreeItemType.TasksSheet,
ParentId = item.Id,
IsExpanded = false,
Children = new List<TreeItemDto>()
});
}
}
}
catch
{
}
}
private void OnContextMenuWithArgs((TreeItemDto, MouseEventArgs) args)
{
_contextItemId = args.Item1.Id;
_contextItemType = args.Item1.Type;
_contextMenuX = args.Item2.ClientX;
_contextMenuY = args.Item2.ClientY;
_contextMenuOpen = true;
}
private void CloseContextMenu()
{
_contextMenuOpen = false;
_contextItemId = null;
}
private string GetContextMenuStyle()
{
return $"position: fixed; left: {_contextMenuX}px; top: {_contextMenuY}px; z-index: 1100;";
}
private async Task OnCreateFolder()
{
var parameters = new DialogParameters<InputDialog>
{
{ "Title", "New Folder" },
{ "Label", "Folder Name" }
};
var dialog = await DialogService.ShowAsync<InputDialog>("New Folder", parameters);
var result = await dialog.Result;
if (!result.Canceled && result.Data is string name && !string.IsNullOrWhiteSpace(name))
{
try
{
await FolderService.CreateFolderAsync(new CreateFolderDto { Name = name });
Snackbar.Add($"Created folder '{name}'", Severity.Success);
await RefreshTree();
}
catch (Exception ex)
{
Snackbar.Add($"Error creating folder: {ex.Message}", Severity.Error);
}
}
}
private async Task OnCreateFile()
{
var parameters = new DialogParameters<InputDialog>
{
{ "Title", "New Tasks Sheet" },
{ "Label", "Tasks Sheet Name" }
};
var dialog = await DialogService.ShowAsync<InputDialog>("New Tasks Sheet", parameters);
var result = await dialog.Result;
if (!result.Canceled && result.Data is string name && !string.IsNullOrWhiteSpace(name))
{
try
{
await FolderService.CreateTasksSheetAsync(new CreateTasksSheetDto { Title = name });
Snackbar.Add($"Created tasks sheet '{name}'", Severity.Success);
await RefreshTree();
}
catch (Exception ex)
{
Snackbar.Add($"Error creating tasks sheet: {ex.Message}", Severity.Error);
}
}
}
private async Task CreateSubFolder()
{
if (_contextItemId == null) return;
var parentId = _contextItemId;
CloseContextMenu();
var parameters = new DialogParameters<InputDialog>
{
{ "Title", "New Subfolder" },
{ "Label", "Folder Name" }
};
var dialog = await DialogService.ShowAsync<InputDialog>("New Subfolder", parameters);
var result = await dialog.Result;
if (!result.Canceled && result.Data is string name && !string.IsNullOrWhiteSpace(name))
{
try
{
await FolderService.CreateFolderAsync(new CreateFolderDto { Name = name, ParentId = parentId });
Snackbar.Add($"Created subfolder '{name}'", Severity.Success);
await RefreshTree();
}
catch (Exception ex)
{
Snackbar.Add($"Error: {ex.Message}", Severity.Error);
}
}
}
private async Task CreateSubFile()
{
if (_contextItemId == null) return;
var parentId = _contextItemId;
CloseContextMenu();
var parameters = new DialogParameters<InputDialog>
{
{ "Title", "New Tasks Sheet" },
{ "Label", "Tasks Sheet Name" }
};
var dialog = await DialogService.ShowAsync<InputDialog>("New Tasks Sheet", parameters);
var result = await dialog.Result;
if (!result.Canceled && result.Data is string name && !string.IsNullOrWhiteSpace(name))
{
try
{
await FolderService.CreateTasksSheetAsync(new CreateTasksSheetDto { Title = name, ParentFolderId = parentId });
Snackbar.Add($"Created tasks sheet '{name}'", Severity.Success);
await RefreshTree();
}
catch (Exception ex)
{
Snackbar.Add($"Error: {ex.Message}", Severity.Error);
}
}
}
private async Task DeleteSelected()
{
if (_contextItemId == null) return;
var itemId = _contextItemId;
var itemType = _contextItemType;
CloseContextMenu();
try
{
if (itemType == TreeItemType.Folder)
{
await FolderService.DeleteFolderAsync(itemId);
Snackbar.Add("Folder deleted", Severity.Success);
}
await RefreshTree();
}
catch (Exception ex)
{
Snackbar.Add($"Error: {ex.Message}", Severity.Error);
}
}
private void OnDragStartHandler(TreeItemDto item)
{
_dragSessionId++;
_draggingItem = item;
item.IsDragSource = true;
}
private void OnDragEndHandler(TreeItemDto item)
{
_dragSessionId++;
_isMoving = false;
ClearDragState(_treeItems);
_rootDragOver = false;
_draggingItem = null;
}
private void ClearDragState(List<TreeItemDto> items)
{
foreach (var item in items)
{
item.IsDragSource = false;
item.IsDragOver = false;
if (item.Children.Any())
ClearDragState(item.Children);
}
}
private async Task OnItemDropHandler(TreeItemDto target)
{
var source = _draggingItem;
if (source == null) return;
var sessionId = _dragSessionId;
await MoveItem(source, target.Id, sessionId);
_draggingItem = null;
}
private void OnRootDragEnter()
{
if (_draggingItem != null) _rootDragOver = true;
}
private void OnRootDragOver()
{
if (_draggingItem != null) _rootDragOver = true;
}
private void OnRootDragLeave()
{
_rootDragOver = false;
}
private async Task OnRootDrop()
{
_rootDragOver = false;
var source = _draggingItem;
if (source == null) return;
var sessionId = _dragSessionId;
await MoveItem(source, null, sessionId);
_draggingItem = null;
}
private async Task MoveItem(TreeItemDto source, string? targetParentId, int sessionId)
{
if (_isMoving || sessionId != _dragSessionId) return;
_isMoving = true;
if (source.Id == targetParentId) { _isMoving = false; return; }
if (source.ParentId == targetParentId && targetParentId != null) { _isMoving = false; return; }
if (source.Type == TreeItemType.Folder && targetParentId != null)
{
if (await IsDescendantOf(source.Id, targetParentId))
{
Snackbar.Add("Cannot move a folder into one of its children.", Severity.Error);
_isMoving = false;
return;
}
}
try
{
bool success;
if (source.Type == TreeItemType.Folder)
{
success = await FolderService.MoveFolderAsync(source.Id, targetParentId);
}
else
{
success = await FolderService.MoveTasksSheetAsync(source.Id, targetParentId);
}
if (success)
{
Snackbar.Add($"Moved '{source.Name}'", Severity.Success);
await RefreshTree();
}
else
{
Snackbar.Add($"Could not move '{source.Name}' — item not found.", Severity.Warning);
}
}
catch (Exception ex)
{
Snackbar.Add($"Error moving item: {ex.Message}", Severity.Error);
}
finally
{
_isMoving = false;
}
}
private async Task<bool> IsDescendantOf(string ancestorId, string candidateId)
{
var queue = new Queue<string>();
queue.Enqueue(ancestorId);
var visited = new HashSet<string>();
while (queue.Count > 0)
{
var current = queue.Dequeue();
if (!visited.Add(current)) continue;
if (current == candidateId) return true;
var children = await FolderService.GetFolderByIdAsync(current);
if (children == null) continue;
foreach (var child in children.Children)
{
queue.Enqueue(child.Id);
}
}
return false;
}
}