< Summary

Information
Class: Chronicis.Client.Services.TreeStateService
Assembly: Chronicis.Client
File(s): /home/runner/work/chronicis/chronicis/src/Chronicis.Client/Services/TreeStateService.cs
Line coverage
100%
Covered lines: 48
Uncovered lines: 0
Coverable lines: 48
Total lines: 326
Line coverage: 100%
Branch coverage
100%
Covered branches: 14
Total branches: 14
Branch coverage: 100%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.ctor(...)100%11100%
get_RootNodes()100%11100%
get_SelectedNodeId()100%11100%
get_SearchQuery()100%11100%
get_IsSearchActive()100%11100%
get_IsLoading()100%11100%
get_CachedArticles()100%11100%
get_HasCachedData()100%22100%
NotifyStateChanged()100%22100%
ExpandNode(...)100%22100%
CollapseNode(...)100%22100%
ToggleNode(...)100%22100%
SelectNode(...)100%11100%
ExpandPathToAndSelect(...)100%11100%
UpdateNodeDisplay(...)100%22100%
UpdateNodeVisibility(...)100%22100%
SetSearchQuery(...)100%11100%
ClearSearch()100%11100%
GetExpandedNodeIds()100%11100%
RestoreExpandedNodes(...)100%11100%
TryGetNode(...)100%11100%

File(s)

/home/runner/work/chronicis/chronicis/src/Chronicis.Client/Services/TreeStateService.cs

#LineLine coverage
 1using Blazored.LocalStorage;
 2using Chronicis.Client.Models;
 3using Chronicis.Client.Services.Tree;
 4using Chronicis.Shared.DTOs;
 5using Chronicis.Shared.Enums;
 6
 7namespace Chronicis.Client.Services;
 8
 9/// <summary>
 10/// Service for managing the navigation tree state.
 11/// Builds a hierarchical tree with Worlds, Virtual Groups, Campaigns, Arcs, and Articles.
 12///
 13/// This is a facade that delegates to internal components:
 14/// - TreeDataBuilder: Builds the tree structure from API data
 15/// - TreeUiState: Manages expansion, selection, search, and persistence
 16/// - TreeMutations: Handles create, move, delete, and update operations
 17/// </summary>
 18public class TreeStateService : ITreeStateService
 19{
 20    private readonly ILogger<TreeStateService> _logger;
 21
 22    // Internal delegated components
 23    private readonly TreeDataBuilder _dataBuilder;
 24    private readonly TreeUiState _uiState;
 25    private readonly TreeMutations _mutations;
 26
 27    // Shared state
 1528    private TreeNodeIndex _nodeIndex = new();
 1529    private List<ArticleTreeDto> _cachedArticles = new();
 30    private bool _isLoading;
 31    private bool _isInitialized;
 32
 33    public TreeStateService(
 34        IArticleApiService articleApi,
 35        IWorldApiService worldApi,
 36        ICampaignApiService campaignApi,
 37        IArcApiService arcApi,
 38        ISessionApiService sessionApi,
 39        IMapApiService mapApi,
 40        IAppContextService appContext,
 41        ILocalStorageService localStorage,
 42        ILogger<TreeStateService> logger)
 43    {
 1544        _logger = logger;
 45
 46        // Create internal components
 1547        _dataBuilder = new TreeDataBuilder(articleApi, worldApi, campaignApi, arcApi, sessionApi, mapApi, logger);
 1548        _uiState = new TreeUiState(localStorage, logger);
 1549        _mutations = new TreeMutations(articleApi, sessionApi, appContext, logger);
 50
 51        // Wire up refresh callback for mutations
 1552        _mutations.SetRefreshCallback(RefreshAsync);
 1553    }
 54
 55    // ============================================
 56    // State Properties
 57    // ============================================
 58
 659    public IReadOnlyList<TreeNode> RootNodes => _nodeIndex.RootNodes;
 260    public Guid? SelectedNodeId => _uiState.SelectedNodeId;
 261    public string SearchQuery => _uiState.SearchQuery;
 162    public bool IsSearchActive => _uiState.IsSearchActive;
 463    public bool IsLoading => _isLoading;
 64    public bool ShouldFocusTitle { get; set; }
 65
 66    /// <summary>
 67    /// Exposes the cached article list for other services to consume.
 68    /// Avoids duplicate API calls from Dashboard, etc.
 69    /// </summary>
 570    public IReadOnlyList<ArticleTreeDto> CachedArticles => _cachedArticles;
 71
 72    /// <summary>
 73    /// Indicates whether the tree has been initialized and CachedArticles is populated.
 74    /// </summary>
 275    public bool HasCachedData => _isInitialized && _cachedArticles.Any();
 76
 77    public event Action? OnStateChanged;
 78
 6379    private void NotifyStateChanged() => OnStateChanged?.Invoke();
 80
 81    // ============================================
 82    // Initialization
 83    // ============================================
 84
 85    public async Task InitializeAsync()
 86    {
 87        if (_isInitialized)
 88            return;
 89
 90        _isLoading = true;
 91        NotifyStateChanged();
 92
 93        try
 94        {
 95            // Build the tree
 96            var buildResult = await _dataBuilder.BuildTreeAsync();
 97            _nodeIndex = buildResult.NodeIndex;
 98            _cachedArticles = buildResult.CachedArticles;
 99
 100            // Update internal components with new node index
 101            _uiState.SetNodeIndex(_nodeIndex);
 102            _mutations.SetNodeIndex(_nodeIndex);
 103
 104            // Restore persisted state
 105            await _uiState.RestoreExpandedStateFromStorageAsync();
 106
 107            _isInitialized = true;
 108
 109            // Handle pending selection (if ExpandPathToAndSelect was called before init)
 110            var pendingId = _uiState.ConsumePendingSelection();
 111            if (pendingId.HasValue)
 112            {
 113                _uiState.ExpandPathToAndSelect(pendingId.Value, _isInitialized);
 114            }
 115        }
 116        catch (Exception ex)
 117        {
 118            _logger.LogError(ex, "Failed to initialize tree");
 119            _nodeIndex = new TreeNodeIndex();
 120            _cachedArticles = new List<ArticleTreeDto>();
 121        }
 122        finally
 123        {
 124            _isLoading = false;
 125            NotifyStateChanged();
 126        }
 127    }
 128
 129    public async Task RefreshAsync()
 130    {
 131        // Save current state before refresh
 132        var previouslyExpanded = new HashSet<Guid>(_uiState.ExpandedNodeIds);
 133        var previousSelection = _uiState.SelectedNodeId;
 134
 135        _isLoading = true;
 136        NotifyStateChanged();
 137
 138        try
 139        {
 140            // Rebuild the tree
 141            var buildResult = await _dataBuilder.BuildTreeAsync();
 142            _nodeIndex = buildResult.NodeIndex;
 143            _cachedArticles = buildResult.CachedArticles;
 144
 145            // Update internal components with new node index
 146            _uiState.SetNodeIndex(_nodeIndex);
 147            _mutations.SetNodeIndex(_nodeIndex);
 148
 149            // Restore expanded state
 150            _uiState.RestoreExpandedNodesPreserving(previouslyExpanded);
 151
 152            // Restore selection AND ensure path is expanded
 153            if (previousSelection.HasValue && _nodeIndex.ContainsNode(previousSelection.Value))
 154            {
 155                _uiState.ExpandPathToAndSelect(previousSelection.Value, _isInitialized);
 156            }
 157
 158            // Re-apply search filter if active
 159            if (_uiState.IsSearchActive)
 160            {
 161                _uiState.ApplySearchFilter();
 162            }
 163        }
 164        catch (Exception ex)
 165        {
 166            _logger.LogError(ex, "Failed to refresh tree");
 167        }
 168        finally
 169        {
 170            _isLoading = false;
 171            NotifyStateChanged();
 172        }
 173    }
 174
 175    // ============================================
 176    // Node Operations (delegated to TreeUiState)
 177    // ============================================
 178
 179    public void ExpandNode(Guid nodeId)
 180    {
 1181        if (_uiState.ExpandNode(nodeId))
 182        {
 1183            NotifyStateChanged();
 184        }
 1185    }
 186
 187    public void CollapseNode(Guid nodeId)
 188    {
 1189        if (_uiState.CollapseNode(nodeId))
 190        {
 1191            NotifyStateChanged();
 192        }
 1193    }
 194
 195    public void ToggleNode(Guid nodeId)
 196    {
 1197        if (_uiState.ToggleNode(nodeId))
 198        {
 1199            NotifyStateChanged();
 200        }
 1201    }
 202
 203    public void SelectNode(Guid nodeId)
 204    {
 3205        _uiState.SelectNode(nodeId);
 3206        NotifyStateChanged();
 3207    }
 208
 209    public void ExpandPathToAndSelect(Guid nodeId)
 210    {
 2211        _uiState.ExpandPathToAndSelect(nodeId, _isInitialized);
 2212        NotifyStateChanged();
 2213    }
 214
 215    // ============================================
 216    // CRUD Operations (delegated to TreeMutations)
 217    // ============================================
 218
 219    public async Task<Guid?> CreateRootArticleAsync()
 220    {
 221        var newId = await _mutations.CreateRootArticleAsync();
 222
 223        if (newId.HasValue)
 224        {
 225            // Select the new node (refresh already happened via callback)
 226            _uiState.SelectNode(newId.Value);
 227            ShouldFocusTitle = true;
 228            NotifyStateChanged();
 229        }
 230
 231        return newId;
 232    }
 233
 234    public async Task<Guid?> CreateChildArticleAsync(Guid parentId)
 235    {
 236        var isArcChildCreate = _nodeIndex.TryGetNode(parentId, out var parentNode)
 237            && parentNode?.NodeType == TreeNodeType.Arc;
 238
 239        var newId = await _mutations.CreateChildArticleAsync(parentId);
 240
 241        if (newId.HasValue)
 242        {
 243            // Expand parent and select new node (refresh already happened via callback)
 244            _uiState.ExpandNode(parentId);
 245            _uiState.SelectNode(newId.Value);
 246            ShouldFocusTitle = !isArcChildCreate;
 247            NotifyStateChanged();
 248        }
 249
 250        return newId;
 251    }
 252
 253    public async Task<bool> DeleteArticleAsync(Guid articleId)
 254    {
 255        var wasSelected = _uiState.SelectedNodeId == articleId;
 256        var success = await _mutations.DeleteArticleAsync(articleId);
 257
 258        if (success && wasSelected)
 259        {
 260            _uiState.ClearSelection();
 261        }
 262
 263        NotifyStateChanged();
 264        return success;
 265    }
 266
 267    public async Task<bool> MoveArticleAsync(Guid articleId, Guid? newParentId)
 268    {
 269        var success = await _mutations.MoveArticleAsync(articleId, newParentId);
 270        // Refresh and notification handled by callback
 271        return success;
 272    }
 273
 274    public void UpdateNodeDisplay(Guid nodeId, string title, string? iconEmoji)
 275    {
 2276        if (_mutations.UpdateNodeDisplay(nodeId, title, iconEmoji))
 277        {
 1278            NotifyStateChanged();
 279        }
 2280    }
 281
 282    public void UpdateNodeVisibility(Guid nodeId, ArticleVisibility visibility)
 283    {
 2284        if (_mutations.UpdateNodeVisibility(nodeId, visibility))
 285        {
 1286            NotifyStateChanged();
 287        }
 2288    }
 289
 290    // ============================================
 291    // Search/Filter (delegated to TreeUiState)
 292    // ============================================
 293
 294    public void SetSearchQuery(string query)
 295    {
 2296        _uiState.SetSearchQuery(query);
 2297        NotifyStateChanged();
 2298    }
 299
 300    public void ClearSearch()
 301    {
 1302        _uiState.ClearSearch();
 1303        NotifyStateChanged();
 1304    }
 305
 306    // ============================================
 307    // Persistence (delegated to TreeUiState)
 308    // ============================================
 309
 1310    public IReadOnlySet<Guid> GetExpandedNodeIds() => _uiState.GetExpandedNodeIds();
 311
 312    public void RestoreExpandedNodes(IEnumerable<Guid> nodeIds)
 313    {
 1314        _uiState.RestoreExpandedNodes(nodeIds);
 1315        NotifyStateChanged();
 1316    }
 317
 318    // ============================================
 319    // Node Lookup
 320    // ============================================
 321
 322    public bool TryGetNode(Guid nodeId, out TreeNode? node)
 323    {
 2324        return _nodeIndex.TryGetNode(nodeId, out node);
 325    }
 326}