< 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
0%
Covered lines: 0
Uncovered lines: 126
Coverable lines: 126
Total lines: 321
Line coverage: 0%
Branch coverage
0%
Covered branches: 0
Total branches: 32
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/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
 028    private TreeNodeIndex _nodeIndex = new();
 029    private List<ArticleTreeDto> _cachedArticles = new();
 30    private bool _isLoading;
 31    private bool _isInitialized;
 32
 033    public TreeStateService(
 034        IArticleApiService articleApi,
 035        IWorldApiService worldApi,
 036        ICampaignApiService campaignApi,
 037        IArcApiService arcApi,
 038        IAppContextService appContext,
 039        ILocalStorageService localStorage,
 040        ILogger<TreeStateService> logger)
 41    {
 042        _logger = logger;
 43
 44        // Create internal components
 045        _dataBuilder = new TreeDataBuilder(articleApi, worldApi, campaignApi, arcApi, logger);
 046        _uiState = new TreeUiState(localStorage, logger);
 047        _mutations = new TreeMutations(articleApi, appContext, logger);
 48
 49        // Wire up refresh callback for mutations
 050        _mutations.SetRefreshCallback(RefreshAsync);
 051    }
 52
 53    // ============================================
 54    // State Properties
 55    // ============================================
 56
 057    public IReadOnlyList<TreeNode> RootNodes => _nodeIndex.RootNodes;
 058    public Guid? SelectedNodeId => _uiState.SelectedNodeId;
 059    public string SearchQuery => _uiState.SearchQuery;
 060    public bool IsSearchActive => _uiState.IsSearchActive;
 061    public bool IsLoading => _isLoading;
 062    public bool ShouldFocusTitle { get; set; }
 63
 64    /// <summary>
 65    /// Exposes the cached article list for other services to consume.
 66    /// Avoids duplicate API calls from Dashboard, etc.
 67    /// </summary>
 068    public IReadOnlyList<ArticleTreeDto> CachedArticles => _cachedArticles;
 69
 70    /// <summary>
 71    /// Indicates whether the tree has been initialized and CachedArticles is populated.
 72    /// </summary>
 073    public bool HasCachedData => _isInitialized && _cachedArticles.Any();
 74
 75    public event Action? OnStateChanged;
 76
 077    private void NotifyStateChanged() => OnStateChanged?.Invoke();
 78
 79    // ============================================
 80    // Initialization
 81    // ============================================
 82
 83    public async Task InitializeAsync()
 84    {
 085        if (_isInitialized)
 086            return;
 87
 088        _isLoading = true;
 089        NotifyStateChanged();
 90
 91        try
 92        {
 93            // Build the tree
 094            var buildResult = await _dataBuilder.BuildTreeAsync();
 095            _nodeIndex = buildResult.NodeIndex;
 096            _cachedArticles = buildResult.CachedArticles;
 97
 98            // Update internal components with new node index
 099            _uiState.SetNodeIndex(_nodeIndex);
 0100            _mutations.SetNodeIndex(_nodeIndex);
 101
 102            // Restore persisted state
 0103            await _uiState.RestoreExpandedStateFromStorageAsync();
 104
 0105            _isInitialized = true;
 106
 107            // Handle pending selection (if ExpandPathToAndSelect was called before init)
 0108            var pendingId = _uiState.ConsumePendingSelection();
 0109            if (pendingId.HasValue)
 110            {
 0111                _uiState.ExpandPathToAndSelect(pendingId.Value, _isInitialized);
 112            }
 0113        }
 0114        catch (Exception ex)
 115        {
 0116            _logger.LogError(ex, "Failed to initialize tree");
 0117            _nodeIndex = new TreeNodeIndex();
 0118            _cachedArticles = new List<ArticleTreeDto>();
 0119        }
 120        finally
 121        {
 0122            _isLoading = false;
 0123            NotifyStateChanged();
 124        }
 0125    }
 126
 127    public async Task RefreshAsync()
 128    {
 129        // Save current state before refresh
 0130        var previouslyExpanded = new HashSet<Guid>(_uiState.ExpandedNodeIds);
 0131        var previousSelection = _uiState.SelectedNodeId;
 132
 0133        _isLoading = true;
 0134        NotifyStateChanged();
 135
 136        try
 137        {
 138            // Rebuild the tree
 0139            var buildResult = await _dataBuilder.BuildTreeAsync();
 0140            _nodeIndex = buildResult.NodeIndex;
 0141            _cachedArticles = buildResult.CachedArticles;
 142
 143            // Update internal components with new node index
 0144            _uiState.SetNodeIndex(_nodeIndex);
 0145            _mutations.SetNodeIndex(_nodeIndex);
 146
 147            // Restore expanded state
 0148            _uiState.RestoreExpandedNodesPreserving(previouslyExpanded);
 149
 150            // Restore selection AND ensure path is expanded
 0151            if (previousSelection.HasValue && _nodeIndex.ContainsNode(previousSelection.Value))
 152            {
 0153                _uiState.ExpandPathToAndSelect(previousSelection.Value, _isInitialized);
 154            }
 155
 156            // Re-apply search filter if active
 0157            if (_uiState.IsSearchActive)
 158            {
 0159                _uiState.ApplySearchFilter();
 160            }
 0161        }
 0162        catch (Exception ex)
 163        {
 0164            _logger.LogError(ex, "Failed to refresh tree");
 0165        }
 166        finally
 167        {
 0168            _isLoading = false;
 0169            NotifyStateChanged();
 170        }
 0171    }
 172
 173    // ============================================
 174    // Node Operations (delegated to TreeUiState)
 175    // ============================================
 176
 177    public void ExpandNode(Guid nodeId)
 178    {
 0179        if (_uiState.ExpandNode(nodeId))
 180        {
 0181            NotifyStateChanged();
 182        }
 0183    }
 184
 185    public void CollapseNode(Guid nodeId)
 186    {
 0187        if (_uiState.CollapseNode(nodeId))
 188        {
 0189            NotifyStateChanged();
 190        }
 0191    }
 192
 193    public void ToggleNode(Guid nodeId)
 194    {
 0195        if (_uiState.ToggleNode(nodeId))
 196        {
 0197            NotifyStateChanged();
 198        }
 0199    }
 200
 201    public void SelectNode(Guid nodeId)
 202    {
 0203        _uiState.SelectNode(nodeId);
 0204        NotifyStateChanged();
 0205    }
 206
 207    public void ExpandPathToAndSelect(Guid nodeId)
 208    {
 0209        _uiState.ExpandPathToAndSelect(nodeId, _isInitialized);
 0210        NotifyStateChanged();
 0211    }
 212
 213    // ============================================
 214    // CRUD Operations (delegated to TreeMutations)
 215    // ============================================
 216
 217    public async Task<Guid?> CreateRootArticleAsync()
 218    {
 0219        var newId = await _mutations.CreateRootArticleAsync();
 220
 0221        if (newId.HasValue)
 222        {
 223            // Select the new node (refresh already happened via callback)
 0224            _uiState.SelectNode(newId.Value);
 0225            ShouldFocusTitle = true;
 0226            NotifyStateChanged();
 227        }
 228
 0229        return newId;
 0230    }
 231
 232    public async Task<Guid?> CreateChildArticleAsync(Guid parentId)
 233    {
 0234        var newId = await _mutations.CreateChildArticleAsync(parentId);
 235
 0236        if (newId.HasValue)
 237        {
 238            // Expand parent and select new node (refresh already happened via callback)
 0239            _uiState.ExpandNode(parentId);
 0240            _uiState.SelectNode(newId.Value);
 0241            ShouldFocusTitle = true;
 0242            NotifyStateChanged();
 243        }
 244
 0245        return newId;
 0246    }
 247
 248    public async Task<bool> DeleteArticleAsync(Guid articleId)
 249    {
 0250        var wasSelected = _uiState.SelectedNodeId == articleId;
 0251        var success = await _mutations.DeleteArticleAsync(articleId);
 252
 0253        if (success && wasSelected)
 254        {
 0255            _uiState.ClearSelection();
 256        }
 257
 0258        NotifyStateChanged();
 0259        return success;
 0260    }
 261
 262    public async Task<bool> MoveArticleAsync(Guid articleId, Guid? newParentId)
 263    {
 0264        var success = await _mutations.MoveArticleAsync(articleId, newParentId);
 265        // Refresh and notification handled by callback
 0266        return success;
 0267    }
 268
 269    public void UpdateNodeDisplay(Guid nodeId, string title, string? iconEmoji)
 270    {
 0271        if (_mutations.UpdateNodeDisplay(nodeId, title, iconEmoji))
 272        {
 0273            NotifyStateChanged();
 274        }
 0275    }
 276
 277    public void UpdateNodeVisibility(Guid nodeId, ArticleVisibility visibility)
 278    {
 0279        if (_mutations.UpdateNodeVisibility(nodeId, visibility))
 280        {
 0281            NotifyStateChanged();
 282        }
 0283    }
 284
 285    // ============================================
 286    // Search/Filter (delegated to TreeUiState)
 287    // ============================================
 288
 289    public void SetSearchQuery(string query)
 290    {
 0291        _uiState.SetSearchQuery(query);
 0292        NotifyStateChanged();
 0293    }
 294
 295    public void ClearSearch()
 296    {
 0297        _uiState.ClearSearch();
 0298        NotifyStateChanged();
 0299    }
 300
 301    // ============================================
 302    // Persistence (delegated to TreeUiState)
 303    // ============================================
 304
 0305    public IReadOnlySet<Guid> GetExpandedNodeIds() => _uiState.GetExpandedNodeIds();
 306
 307    public void RestoreExpandedNodes(IEnumerable<Guid> nodeIds)
 308    {
 0309        _uiState.RestoreExpandedNodes(nodeIds);
 0310        NotifyStateChanged();
 0311    }
 312
 313    // ============================================
 314    // Node Lookup
 315    // ============================================
 316
 317    public bool TryGetNode(Guid nodeId, out TreeNode? node)
 318    {
 0319        return _nodeIndex.TryGetNode(nodeId, out node);
 320    }
 321}