< Summary

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

Feature is only available for sponsors

Upgrade to PRO version

Metrics

File(s)

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

#LineLine coverage
 1@using Chronicis.Client.Models
 2@using Chronicis.Shared.Enums
 3@inject NavigationManager Navigation
 4@inject IJSRuntime JSRuntime
 5@inject IWorldApiService WorldApi
 6@inject ISnackbar Snackbar
 7
 8<div class="tree-node @GetNodeClasses()">
 9
 10    <div class="@GetContentClasses()"
 11         @onclick="OnNodeClick"
 12         @onclick:stopPropagation="true"
 13         @oncontextmenu="OpenContextMenu"
 14         @oncontextmenu:preventDefault="true"
 15         draggable="@Node.IsDraggable.ToString().ToLower()"
 16         @ondragstart="HandleDragStart"
 17         @ondragstart:stopPropagation="true"
 18         @ondragend="HandleDragEnd"
 19         @ondragover="HandleDragOver"
 20         @ondragover:preventDefault="true"
 21         @ondragleave="HandleDragLeave"
 22         @ondrop="HandleDrop"
 23         @ondrop:preventDefault="true">
 24
 25        @* Expand/Collapse button *@
 026        @if (Node.HasChildren)
 27        {
 28            <MudIconButton
 29                Icon="@(Node.IsExpanded ? Icons.Material.Filled.ExpandMore : Icons.Material.Filled.ChevronRight)"
 30                Size="Size.Small"
 31                Class="tree-node__expand-btn"
 32                OnClick="@OnExpandClickWrapper" />
 33        }
 34        else
 35        {
 36            <div class="tree-node__expand-spacer"></div>
 37        }
 38
 39        @* Icon - based on node type, or lock icon if private *@
 40        @{
 041            var isPrivate = Node.NodeType == TreeNodeType.Article && Node.Visibility == ArticleVisibility.Private;
 042            var showAiBadge = Node.NodeType == TreeNodeType.Article && Node.HasAISummary;
 043            var icon = GetNodeIcon();
 044            var iconColorClass = GetIconColorClass();
 045            var titleClasses = GetTitleClasses();
 46        }
 047        @if (isPrivate)
 48        {
 49            <MudTooltip Text="Private - only you can see this" Delay="300">
 50                <MudIcon
 51                    Icon="@Icons.Material.Filled.Lock"
 52                    Size="Size.Small"
 53                    Class="tree-node__icon tree-node__icon--material tree-node__icon--private" />
 54            </MudTooltip>
 55        }
 056        else if (!string.IsNullOrEmpty(icon) && icon.StartsWith("fa-"))
 57        {
 58            <span class="tree-node__icon-wrapper">
 59                <i class="@($"{icon} tree-node__icon tree-node__icon--fa {iconColorClass}")"></i>
 060                @if (showAiBadge)
 61                {
 62                    <span class="tree-node__ai-badge" title="Has AI Summary">✨</span>
 63                }
 64            </span>
 65        }
 066        else if (!string.IsNullOrEmpty(icon) && !icon.StartsWith("@"))
 67        {
 68            <span class="tree-node__icon-wrapper">
 069                <span class="tree-node__icon tree-node__icon--emoji">@icon</span>
 070                @if (showAiBadge)
 71                {
 72                    <span class="tree-node__ai-badge" title="Has AI Summary">✨</span>
 73                }
 74            </span>
 75        }
 76        else
 77        {
 78            <span class="tree-node__icon-wrapper">
 79                <MudIcon
 80                    Icon="@GetMaterialIcon()"
 81                    Size="Size.Small"
 82                    Class="@($"tree-node__icon tree-node__icon--material {iconColorClass}")" />
 083                @if (showAiBadge)
 84                {
 85                    <span class="tree-node__ai-badge" title="Has AI Summary">✨</span>
 86                }
 87            </span>
 88        }
 89
 90        @* Title with tooltip *@
 91        <MudTooltip Text="@Node.DisplayTitle" Delay="500" Placement="Placement.Bottom">
 092            <span class="@($"tree-node__title {titleClasses}")">@Node.DisplayTitle</span>
 93        </MudTooltip>
 94
 95
 96    </div>
 97
 98    @* Children *@
 099    @if (Node.HasChildren && Node.IsExpanded && Node.Children.Count > 0)
 100    {
 101        <div class="tree-node__children">
 0102            @foreach (var child in Node.Children.Where(c => c.IsVisible))
 103            {
 104                <ArticleTreeNode
 105                    Node="child"
 106                    OnSelect="OnSelect"
 107                    OnToggle="OnToggle"
 108                    OnAddChild="OnAddChild"
 109                    OnDelete="OnDelete"
 110                    OnMove="OnMove"
 111                    DraggedNodeId="DraggedNodeId"
 112                    DraggedNodeIdChanged="DraggedNodeIdChanged" />
 113            }
 114        </div>
 115    }
 116
 117    @* Empty state for virtual groups *@
 0118    @if (Node.NodeType == TreeNodeType.VirtualGroup && Node.IsExpanded && Node.Children.Count == 0)
 119    {
 120        <div class="tree-node__children">
 121            <div class="tree-node__empty-group">
 122                <MudButton
 123                    Variant="Variant.Text"
 124                    Size="Size.Small"
 125                    StartIcon="@Icons.Material.Filled.Add"
 0126                    OnClick="@(() => OnAddChild.InvokeAsync(Node.Id))">
 0127                    @GetAddChildLabel()
 128                </MudButton>
 129            </div>
 130        </div>
 131    }
 132</div>
 133
 134@code {
 135    [Parameter, EditorRequired]
 0136    public TreeNode Node { get; set; } = null!;
 137
 138    [Parameter]
 0139    public EventCallback<Guid> OnSelect { get; set; }
 140
 141    [Parameter]
 0142    public EventCallback<Guid> OnToggle { get; set; }
 143
 144    [Parameter]
 0145    public EventCallback<Guid> OnAddChild { get; set; }
 146
 147    [Parameter]
 0148    public EventCallback<Guid> OnDelete { get; set; }
 149
 150    [Parameter]
 0151    public EventCallback<(Guid ArticleId, Guid? NewParentId)> OnMove { get; set; }
 152
 153    [Parameter]
 0154    public Guid? DraggedNodeId { get; set; }
 155
 156    [Parameter]
 0157    public EventCallback<Guid?> DraggedNodeIdChanged { get; set; }
 158
 159    private bool _isDragOver;
 160
 161#pragma warning disable CS0649 // Field is never assigned - context menu feature not yet implemented
 162    private MudMenu? _contextMenu;
 163#pragma warning restore CS0649
 164
 165    private string GetNodeClasses()
 166    {
 0167        var classes = new List<string> { "tree-node" };
 168
 0169        if (Node.IsSelected) classes.Add("tree-node--selected");
 0170        if (!Node.IsVisible) classes.Add("tree-node--hidden");
 0171        if (DraggedNodeId == Node.Id) classes.Add("tree-node--dragging");
 172
 173        // Add type-specific classes
 0174        classes.Add($"tree-node--{Node.NodeType.ToString().ToLower()}");
 175
 0176        return string.Join(" ", classes);
 177    }
 178
 179    private string GetContentClasses()
 180    {
 0181        var classes = new List<string> { "tree-node__content" };
 182
 0183        if (_isDragOver && CanDropHere()) classes.Add("tree-node__content--drop-target");
 0184        if (!Node.IsSelectable) classes.Add("tree-node__content--not-selectable");
 185
 0186        return string.Join(" ", classes);
 187    }
 188
 189    private string GetTitleClasses()
 190    {
 0191        return Node.NodeType switch
 0192        {
 0193            TreeNodeType.World => "tree-node__title--world",
 0194            TreeNodeType.Campaign => "tree-node__title--campaign",
 0195            TreeNodeType.Arc => "tree-node__title--arc",
 0196            TreeNodeType.VirtualGroup => "tree-node__title--virtual",
 0197            _ => ""
 0198        };
 199    }
 200
 201    private string GetIconColorClass()
 202    {
 0203        return Node.NodeType switch
 0204        {
 0205            TreeNodeType.World => "tree-node__icon--world",
 0206            TreeNodeType.Campaign => "tree-node__icon--campaign",
 0207            TreeNodeType.Arc => "tree-node__icon--arc",
 0208            TreeNodeType.VirtualGroup => "tree-node__icon--virtual",
 0209            _ => ""
 0210        };
 211    }
 212
 213    private string GetNodeIcon()
 214    {
 215        // Use custom icon if set
 0216        if (!string.IsNullOrEmpty(Node.IconEmoji))
 217        {
 0218            return Node.IconEmoji;
 219        }
 220
 221        // Otherwise use default based on type
 0222        return Node.GetDefaultIcon();
 223    }
 224
 225    private string GetMaterialIcon()
 226    {
 0227        return Node.NodeType switch
 0228        {
 0229            TreeNodeType.World => Icons.Material.Filled.Public,
 0230            TreeNodeType.VirtualGroup => Node.VirtualGroupType switch
 0231            {
 0232                VirtualGroupType.Campaigns => Icons.Material.Filled.Book,
 0233                VirtualGroupType.PlayerCharacters => Icons.Material.Filled.Group,
 0234                VirtualGroupType.Wiki => Icons.Material.Filled.MenuBook,
 0235                VirtualGroupType.Uncategorized => Icons.Material.Filled.FolderOpen,
 0236                _ => Icons.Material.Filled.Folder
 0237            },
 0238            TreeNodeType.Campaign => Icons.Material.Filled.AutoStories,
 0239            TreeNodeType.Arc => Icons.Material.Filled.Bookmark,
 0240            TreeNodeType.Article => Node.HasChildren ? Icons.Material.Filled.Folder : Icons.Material.Filled.Description,
 0241            _ => Icons.Material.Filled.Description
 0242        };
 243    }
 244
 245    private bool ShowContextMenu()
 246    {
 0247        return Node.CanAddChildren || Node.NodeType == TreeNodeType.Article;
 248    }
 249
 250    private string GetAddChildLabel()
 251    {
 0252        return Node.NodeType switch
 0253        {
 0254            TreeNodeType.VirtualGroup => Node.VirtualGroupType switch
 0255            {
 0256                VirtualGroupType.Wiki => "New Wiki Article",
 0257                VirtualGroupType.PlayerCharacters => "New Character",
 0258                VirtualGroupType.Campaigns => "New Campaign",
 0259                VirtualGroupType.Uncategorized => "New Article",
 0260                _ => "Add Item"
 0261            },
 0262            TreeNodeType.Arc => "New Session",
 0263            TreeNodeType.Article => "Add Child",
 0264            _ => "Add"
 0265        };
 266    }
 267
 268    private async Task OnNodeClick()
 269    {
 270        // Navigate based on node type
 0271        switch (Node.NodeType)
 272        {
 273            case TreeNodeType.World:
 0274                Navigation.NavigateTo($"/world/{Node.Id}");
 0275                break;
 276
 277            case TreeNodeType.Campaign:
 0278                Navigation.NavigateTo($"/campaign/{Node.Id}");
 0279                break;
 280
 281            case TreeNodeType.Arc:
 0282                Navigation.NavigateTo($"/arc/{Node.Id}");
 0283                break;
 284
 285            case TreeNodeType.Article:
 0286                await OnSelect.InvokeAsync(Node.Id);
 0287                break;
 288
 289            case TreeNodeType.ExternalLink:
 290                // Check if this is a document (not a regular external link)
 0291                if (Node.AdditionalData?.ContainsKey("IsDocument") == true)
 292                {
 293                    // Handle document download
 0294                    await HandleDocumentDownload();
 295                }
 0296                else if (!string.IsNullOrEmpty(Node.Url))
 297                {
 298                    // Open external URL in new tab
 0299                    await JSRuntime.InvokeVoidAsync("open", Node.Url, "_blank");
 300                }
 0301                break;
 302
 303            case TreeNodeType.VirtualGroup:
 304                // Just toggle expand for virtual groups
 0305                await OnToggle.InvokeAsync(Node.Id);
 306                break;
 307        }
 0308    }
 309
 310    private async Task OpenContextMenu(MouseEventArgs e)
 311    {
 0312        if (_contextMenu != null && ShowContextMenu())
 313        {
 0314            await _contextMenu.OpenMenuAsync(e);
 315        }
 0316    }
 317
 318    private async Task OnExpandClickWrapper(MouseEventArgs e)
 319    {
 0320        await OnToggle.InvokeAsync(Node.Id);
 0321    }
 322
 323    private async Task HandleDragStart(DragEventArgs e)
 324    {
 0325        if (Node.IsDraggable)
 326        {
 0327            await DraggedNodeIdChanged.InvokeAsync(Node.Id);
 328        }
 0329    }
 330
 331    private async Task HandleDragEnd(DragEventArgs e)
 332    {
 0333        await DraggedNodeIdChanged.InvokeAsync(null);
 0334        _isDragOver = false;
 0335    }
 336
 337    private void HandleDragOver(DragEventArgs e)
 338    {
 0339        if (CanDropHere())
 340        {
 0341            _isDragOver = true;
 342        }
 0343    }
 344
 345    private void HandleDragLeave(DragEventArgs e)
 346    {
 0347        _isDragOver = false;
 0348    }
 349
 350    private async Task HandleDrop(DragEventArgs e)
 351    {
 0352        _isDragOver = false;
 353
 0354        if (DraggedNodeId.HasValue && CanDropHere())
 355        {
 356            // For virtual groups, we drop as a child of the group (becomes root-level in that category)
 357            // For articles, we drop as a child of the article
 0358            await OnMove.InvokeAsync((DraggedNodeId.Value, Node.Id));
 359        }
 360
 0361        await DraggedNodeIdChanged.InvokeAsync(null);
 0362    }
 363
 364    private bool CanDropHere()
 365    {
 0366        if (!DraggedNodeId.HasValue) return false;
 0367        if (DraggedNodeId.Value == Node.Id) return false; // Can't drop on self
 0368        if (!Node.IsDropTarget) return false;
 369
 0370        return true;
 371    }
 372
 373    private async Task HandleDocumentDownload()
 374    {
 375        try
 376        {
 0377            var download = await WorldApi.DownloadDocumentAsync(Node.Id);
 378
 0379            if (download == null)
 380            {
 0381                Snackbar.Add("Failed to get download URL", Severity.Error);
 0382                return;
 383            }
 384
 385            // Open the SAS URL in a new tab for download
 0386            await JSRuntime.InvokeVoidAsync("open", download.DownloadUrl, "_blank");
 387
 0388            Snackbar.Add($"Opening {download.FileName}", Severity.Success);
 0389        }
 0390        catch (Exception ex)
 391        {
 0392            Snackbar.Add($"Error downloading document: {ex.Message}", Severity.Error);
 0393        }
 0394    }
 395}