< Summary

Information
Class: Chronicis.Client.Components.Articles.ArticleTreeView
Assembly: Chronicis.Client
File(s): /home/runner/work/chronicis/chronicis/src/Chronicis.Client/Components/Articles/ArticleTreeView.razor
Line coverage
0%
Covered lines: 0
Uncovered lines: 96
Coverable lines: 96
Total lines: 312
Line coverage: 0%
Branch coverage
0%
Covered branches: 0
Total branches: 70
Branch coverage: 0%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
BuildRenderTree(...)0%272160%
OnInitializedAsync()0%2040%
OnTreeStateChanged()100%210%
HasVisibleNodes()100%210%
HandleSelect()0%156120%
GetAllNodes()0%2040%
HandleToggle(...)100%210%
HandleAddChild()0%4260%
HandleDelete()0%342180%
CreateWorld()0%4260%
OnDraggedNodeIdChanged(...)100%210%
HandleMove()0%620%
HandleRootDragOver(...)100%210%
HandleRootDragLeave(...)100%210%
HandleRootDrop()0%620%
Dispose()100%210%

File(s)

/home/runner/work/chronicis/chronicis/src/Chronicis.Client/Components/Articles/ArticleTreeView.razor

#LineLine coverage
 1@using Chronicis.Client.Models
 2@using Chronicis.Client.Components.Dialogs
 3@using Chronicis.Shared.DTOs
 4@inject ITreeStateService TreeState
 5@inject ISnackbar Snackbar
 6@inject NavigationManager Navigation
 7@inject IArticleApiService ArticleApi
 8@inject IAppContextService AppContext
 9@inject IDialogService DialogService
 10@inject IJSRuntime JSRuntime
 11@implements IDisposable
 12
 13<div class="article-tree @(_draggedNodeId.HasValue ? "article-tree--dragging" : "")">
 014    @if (TreeState.IsLoading)
 15    {
 16        <div class="article-tree__loading">
 17            <MudProgressCircular Color="Color.Primary" Indeterminate="true" Size="Size.Small" />
 18            <MudText Typo="Typo.body2" Class="ml-2">Loading...</MudText>
 19        </div>
 20    }
 021    else if (!TreeState.RootNodes.Any())
 22    {
 23        <div class="article-tree__empty">
 24            <div class="article-tree__empty-icon">🌍</div>
 25            <MudText Typo="Typo.body2" Class="mt-2 mb-3">No worlds yet</MudText>
 26            <MudButton
 27                Variant="Variant.Text"
 28                Color="Color.Primary"
 29                Size="Size.Small"
 30                StartIcon="@Icons.Material.Filled.Add"
 31                OnClick="CreateWorld">
 32                Create Your First World
 33            </MudButton>
 34        </div>
 35    }
 036    else if (TreeState.IsSearchActive && !HasVisibleNodes())
 37    {
 38        <div class="article-tree__empty">
 39            <div class="article-tree__empty-icon">🔍</div>
 40            <MudText Typo="Typo.body2" Class="mt-2">
 041                No articles match "@TreeState.SearchQuery"
 42            </MudText>
 43        </div>
 44    }
 45    else
 46    {
 47        <div class="article-tree__nodes">
 48            @* Dashboard link *@
 49            <MudNavLink Href="/dashboard"
 50                    Match="NavLinkMatch.All"
 51                    Class="article-tree__dashboard-link">
 52                <div style="width: 30px;"></div>
 53                <MudIcon Icon="@Icons.Material.Filled.Home"
 54                            Color="Color.Primary"
 55                            Size="Size.Small"
 56                            Style="margin-right: 8px;" />
 57                <span style="vertical-align: top;">Dashboard</span>
 58            </MudNavLink>
 59
 60            <MudDivider Class="my-2" Style="opacity: 0.3;" />
 61
 62            @* Worlds and their contents *@
 063            @foreach (var node in TreeState.RootNodes.Where(n => n.IsVisible))
 64            {
 65                <ArticleTreeNode
 66                    Node="node"
 67                    OnSelect="HandleSelect"
 68                    OnToggle="HandleToggle"
 69                    OnAddChild="HandleAddChild"
 70                    OnDelete="HandleDelete"
 71                    OnMove="HandleMove"
 72                    DraggedNodeId="_draggedNodeId"
 73                    DraggedNodeIdChanged="OnDraggedNodeIdChanged" />
 74            }
 75
 76            <MudDivider Class="my-2" Style="opacity: 0.3;" />
 77
 78            @* Drop zone to make items root-level *@
 079            @if (_draggedNodeId.HasValue)
 80            {
 81                <div class="article-tree__root-drop-zone @(_isOverRootZone ? "article-tree__root-drop-zone--active" : ""
 82                     @ondragover="HandleRootDragOver"
 83                     @ondragover:preventDefault="true"
 84                     @ondragleave="HandleRootDragLeave"
 85                     @ondrop="HandleRootDrop"
 86                     @ondrop:preventDefault="true">
 87                    <MudIcon Icon="@Icons.Material.Filled.ArrowUpward" Size="Size.Small" Class="mr-2" />
 88                    <span>Move to root level</span>
 89                </div>
 90            }
 91        </div>
 92    }
 93</div>
 94
 95@code {
 96    private Guid? _draggedNodeId;
 97    private bool _isOverRootZone;
 98
 99    protected override async Task OnInitializedAsync()
 100    {
 0101        TreeState.OnStateChanged += OnTreeStateChanged;
 102
 103        // Initialize tree if not already loaded
 0104        if (!TreeState.RootNodes.Any() && !TreeState.IsLoading)
 105        {
 0106            await TreeState.InitializeAsync();
 107        }
 0108    }
 109
 110    private void OnTreeStateChanged()
 111    {
 0112        InvokeAsync(StateHasChanged);
 0113    }
 114
 115    private bool HasVisibleNodes()
 116    {
 0117        return TreeState.RootNodes.Any(n => n.IsVisible);
 118    }
 119
 120    private async Task HandleSelect(Guid nodeId)
 121    {
 122        // Get the node to determine its type
 0123        var node = TreeState.RootNodes
 0124            .SelectMany(GetAllNodes)
 0125            .FirstOrDefault(n => n.Id == nodeId);
 126
 0127        if (node == null) return;
 128
 129        // Route based on node type
 0130        switch (node.NodeType)
 131        {
 132            case TreeNodeType.World:
 0133                Navigation.NavigateTo($"/world/{nodeId}");
 0134                break;
 135
 136            case TreeNodeType.Campaign:
 0137                Navigation.NavigateTo($"/campaign/{nodeId}");
 0138                break;
 139
 140            case TreeNodeType.Arc:
 0141                Navigation.NavigateTo($"/arc/{nodeId}");
 0142                break;
 143
 144            case TreeNodeType.VirtualGroup:
 145                // Virtual groups just expand/collapse, don't navigate
 0146                TreeState.ToggleNode(nodeId);
 0147                break;
 148
 149            case TreeNodeType.Article:
 0150                TreeState.ExpandPathToAndSelect(nodeId);
 0151                var article = await ArticleApi.GetArticleDetailAsync(nodeId);
 0152                if (article != null && article.Breadcrumbs.Any())
 153                {
 0154                    var path = string.Join("/", article.Breadcrumbs.Select(b => b.Slug));
 0155                    Navigation.NavigateTo($"/article/{path}");
 156                }
 157                break;
 158        }
 0159    }
 160
 161    // Helper to flatten tree for searching
 162    private IEnumerable<TreeNode> GetAllNodes(TreeNode node)
 163    {
 0164        yield return node;
 0165        foreach (var child in node.Children)
 166        {
 0167            foreach (var descendant in GetAllNodes(child))
 168            {
 0169                yield return descendant;
 170            }
 171        }
 0172    }
 173
 174    private void HandleToggle(Guid nodeId)
 175    {
 0176        TreeState.ToggleNode(nodeId);
 0177    }
 178
 179    private async Task HandleAddChild(Guid parentId)
 180    {
 0181        var newId = await TreeState.CreateChildArticleAsync(parentId);
 182
 0183        if (newId.HasValue)
 184        {
 0185            Snackbar.Add("Article created", Severity.Success);
 186
 187            // Navigate to the new article
 0188            var article = await ArticleApi.GetArticleDetailAsync(newId.Value);
 0189            if (article != null && article.Breadcrumbs.Any())
 190            {
 0191                var path = string.Join("/", article.Breadcrumbs.Select(b => b.Slug));
 0192                Navigation.NavigateTo($"/article/{path}");
 193            }
 194        }
 195        else
 196        {
 0197            Snackbar.Add("Failed to create article", Severity.Error);
 198        }
 0199    }
 200
 201    private async Task HandleDelete(Guid nodeId)
 202    {
 203        // Get node info for confirmation message
 0204        var article = await ArticleApi.GetArticleDetailAsync(nodeId);
 0205        if (article == null) return;
 206
 0207        var title = string.IsNullOrWhiteSpace(article.Title) ? "(Untitled)" : article.Title;
 0208        var message = $"Are you sure you want to delete '{title}'?";
 209
 0210        if (article.ChildCount > 0)
 211        {
 0212            var childText = article.ChildCount == 1 ? "1 child article" : $"{article.ChildCount} child articles";
 0213            message += $"\n\n⚠️ WARNING: This will also delete {childText} and all their descendants.";
 214        }
 215
 0216        message += "\n\nThis action cannot be undone.";
 217
 0218        var confirmed = await JSRuntime.InvokeAsync<bool>("confirm", message);
 0219        if (!confirmed) return;
 220
 0221        var success = await TreeState.DeleteArticleAsync(nodeId);
 222
 0223        if (success)
 224        {
 0225            Snackbar.Add("Article deleted", Severity.Success);
 226
 227            // Navigate to parent or dashboard
 0228            if (article.ParentId.HasValue)
 229            {
 0230                var parent = await ArticleApi.GetArticleDetailAsync(article.ParentId.Value);
 0231                if (parent != null && parent.Breadcrumbs.Any())
 232                {
 0233                    var path = string.Join("/", parent.Breadcrumbs.Select(b => b.Slug));
 0234                    Navigation.NavigateTo($"/article/{path}");
 235                }
 236            }
 237            else
 238            {
 0239                Navigation.NavigateTo("/dashboard");
 240            }
 241        }
 242        else
 243        {
 0244            Snackbar.Add("Failed to delete article", Severity.Error);
 245        }
 0246    }
 247
 248    private async Task CreateWorld()
 249    {
 0250        var dialog = await DialogService.ShowAsync<CreateWorldDialog>("New World");
 0251        var result = await dialog.Result;
 252
 0253        if (result != null && !result.Canceled && result.Data is WorldDto world)
 254        {
 0255            await TreeState.RefreshAsync();
 0256            Navigation.NavigateTo($"/world/{world.Id}");
 0257            Snackbar.Add("World created", Severity.Success);
 258        }
 0259    }
 260
 261    // ============================================
 262    // Drag and Drop
 263    // ============================================
 264
 265    private void OnDraggedNodeIdChanged(Guid? nodeId)
 266    {
 0267        _draggedNodeId = nodeId;
 0268        _isOverRootZone = false;
 0269        StateHasChanged();
 0270    }
 271
 272    private async Task HandleMove((Guid ArticleId, Guid? NewParentId) moveInfo)
 273    {
 0274        var success = await TreeState.MoveArticleAsync(moveInfo.ArticleId, moveInfo.NewParentId);
 275
 0276        if (success)
 277        {
 0278            Snackbar.Add("Article moved", Severity.Success);
 279        }
 280        else
 281        {
 0282            Snackbar.Add("Cannot move article here", Severity.Warning);
 283        }
 284
 0285        _draggedNodeId = null;
 0286    }
 287
 288    private void HandleRootDragOver(DragEventArgs e)
 289    {
 0290        _isOverRootZone = true;
 0291    }
 292
 293    private void HandleRootDragLeave(DragEventArgs e)
 294    {
 0295        _isOverRootZone = false;
 0296    }
 297
 298    private async Task HandleRootDrop(DragEventArgs e)
 299    {
 0300        _isOverRootZone = false;
 301
 0302        if (_draggedNodeId.HasValue)
 303        {
 0304            await HandleMove((_draggedNodeId.Value, null));
 305        }
 0306    }
 307
 308    public void Dispose()
 309    {
 0310        TreeState.OnStateChanged -= OnTreeStateChanged;
 0311    }
 312}