< Summary

Information
Class: Chronicis.Client.ViewModels.WorldDetailViewModel
Assembly: Chronicis.Client
File(s): /home/runner/work/chronicis/chronicis/src/Chronicis.Client/ViewModels/WorldDetailViewModel.cs
Line coverage
100%
Covered lines: 57
Uncovered lines: 0
Coverable lines: 57
Total lines: 328
Line coverage: 100%
Branch coverage
100%
Covered branches: 10
Total branches: 10
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_IsLoading()100%11100%
set_IsLoading(...)100%11100%
get_World()100%11100%
set_World(...)100%11100%
get_EditName()100%11100%
set_EditName(...)100%22100%
get_EditDescription()100%11100%
set_EditDescription(...)100%22100%
get_EditPrivateNotes()100%11100%
set_EditPrivateNotes(...)100%44100%
get_IsSaving()100%11100%
set_IsSaving(...)100%11100%
get_HasUnsavedChanges()100%11100%
set_HasUnsavedChanges(...)100%11100%
get_Breadcrumbs()100%11100%
set_Breadcrumbs(...)100%11100%
get_CurrentUserId()100%11100%
set_CurrentUserId(...)100%11100%
get_IsCurrentUserGm()100%11100%
set_IsCurrentUserGm(...)100%11100%
get_IsCurrentUserWorldOwner()100%11100%
set_IsCurrentUserWorldOwner(...)100%11100%
get_CanManageWorldDetails()100%22100%
get_CanViewPrivateNotes()100%11100%
NavigateToArticle(...)100%11100%

File(s)

/home/runner/work/chronicis/chronicis/src/Chronicis.Client/ViewModels/WorldDetailViewModel.cs

#LineLine coverage
 1using Chronicis.Client.Abstractions;
 2using Chronicis.Client.Components.Dialogs;
 3using Chronicis.Client.Services;
 4using Chronicis.Shared.DTOs;
 5using Chronicis.Shared.Enums;
 6using Chronicis.Shared.Extensions;
 7using Microsoft.AspNetCore.Components.Authorization;
 8using MudBlazor;
 9
 10namespace Chronicis.Client.ViewModels;
 11
 12/// <summary>
 13/// Core ViewModel for the World Detail page.
 14/// Coordinates loading, saving, and article-creation actions for a world.
 15/// Sub-ViewModels (WorldLinksViewModel, WorldDocumentsViewModel, WorldSharingViewModel)
 16/// handle their respective tabs independently.
 17/// </summary>
 18public sealed class WorldDetailViewModel : ViewModelBase
 19{
 20    private readonly IWorldApiService _worldApi;
 21    private readonly ITreeStateService _treeState;
 22    private readonly IBreadcrumbService _breadcrumbService;
 23    private readonly IDialogService _dialogService;
 24    private readonly IAppNavigator _navigator;
 25    private readonly IUserNotifier _notifier;
 26    private readonly IPageTitleService _titleService;
 27    private readonly AuthenticationStateProvider _authStateProvider;
 28    private readonly ILogger<WorldDetailViewModel> _logger;
 29
 1930    private bool _isLoading = true;
 31    private WorldDetailDto? _world;
 1932    private string _editName = string.Empty;
 1933    private string _editDescription = string.Empty;
 1934    private string _editPrivateNotes = string.Empty;
 35    private bool _isSaving;
 36    private bool _hasUnsavedChanges;
 1937    private List<BreadcrumbItem> _breadcrumbs = new();
 38    private Guid _currentUserId;
 39    private bool _isCurrentUserGm;
 40    private bool _isCurrentUserWorldOwner;
 41
 1942    public WorldDetailViewModel(
 1943        IWorldApiService worldApi,
 1944        ITreeStateService treeState,
 1945        IBreadcrumbService breadcrumbService,
 1946        IDialogService dialogService,
 1947        IAppNavigator navigator,
 1948        IUserNotifier notifier,
 1949        IPageTitleService titleService,
 1950        AuthenticationStateProvider authStateProvider,
 1951        ILogger<WorldDetailViewModel> logger)
 52    {
 1953        _worldApi = worldApi;
 1954        _treeState = treeState;
 1955        _breadcrumbService = breadcrumbService;
 1956        _dialogService = dialogService;
 1957        _navigator = navigator;
 1958        _notifier = notifier;
 1959        _titleService = titleService;
 1960        _authStateProvider = authStateProvider;
 1961        _logger = logger;
 1962    }
 63
 64    public bool IsLoading
 65    {
 566        get => _isLoading;
 2767        private set => SetField(ref _isLoading, value);
 68    }
 69
 70    public WorldDetailDto? World
 71    {
 1172        get => _world;
 1073        private set => SetField(ref _world, value);
 74    }
 75
 76    public string EditName
 77    {
 578        get => _editName;
 79        set
 80        {
 1281            if (SetField(ref _editName, value))
 1282                HasUnsavedChanges = true;
 1283        }
 84    }
 85
 86    public string EditDescription
 87    {
 588        get => _editDescription;
 89        set
 90        {
 1091            if (SetField(ref _editDescription, value))
 992                HasUnsavedChanges = true;
 1093        }
 94    }
 95
 96    public string EditPrivateNotes
 97    {
 398        get => _editPrivateNotes;
 99        set
 100        {
 12101            if (SetField(ref _editPrivateNotes, value) && CanManageWorldDetails)
 1102                HasUnsavedChanges = true;
 12103        }
 104    }
 105
 106    public bool IsSaving
 107    {
 6108        get => _isSaving;
 4109        private set => SetField(ref _isSaving, value);
 110    }
 111
 112    public bool HasUnsavedChanges
 113    {
 6114        get => _hasUnsavedChanges;
 33115        set => SetField(ref _hasUnsavedChanges, value);
 116    }
 117
 118    public List<BreadcrumbItem> Breadcrumbs
 119    {
 1120        get => _breadcrumbs;
 10121        private set => SetField(ref _breadcrumbs, value);
 122    }
 123
 124    public Guid CurrentUserId
 125    {
 1126        get => _currentUserId;
 21127        private set => SetField(ref _currentUserId, value);
 128    }
 129
 130    public bool IsCurrentUserGm
 131    {
 19132        get => _isCurrentUserGm;
 21133        private set => SetField(ref _isCurrentUserGm, value);
 134    }
 135
 136    public bool IsCurrentUserWorldOwner
 137    {
 11138        get => _isCurrentUserWorldOwner;
 21139        private set => SetField(ref _isCurrentUserWorldOwner, value);
 140    }
 141
 14142    public bool CanManageWorldDetails => IsCurrentUserGm || IsCurrentUserWorldOwner;
 3143    public bool CanViewPrivateNotes => CanManageWorldDetails;
 144
 145    /// <summary>Loads the world and resolves the current user's role.</summary>
 146    public async Task LoadAsync(Guid worldId, WorldSharingViewModel sharingVm, WorldLinksViewModel linksVm, WorldDocumen
 147    {
 148        IsLoading = true;
 149
 150        try
 151        {
 152            CurrentUserId = Guid.Empty;
 153            IsCurrentUserGm = false;
 154            IsCurrentUserWorldOwner = false;
 155
 156            var authState = await _authStateProvider.GetAuthenticationStateAsync();
 157            var world = await _worldApi.GetWorldAsync(worldId);
 158
 159            if (world == null)
 160            {
 161                _navigator.NavigateTo("/dashboard", replace: true);
 162                return;
 163            }
 164
 165            World = world;
 166            EditName = world.Name;
 167            EditDescription = world.Description ?? string.Empty;
 168            EditPrivateNotes = world.PrivateNotes ?? string.Empty;
 169            HasUnsavedChanges = false;
 170            Breadcrumbs = _breadcrumbService.ForWorld(world);
 171
 172            // Resolve current user role
 173            if (world.Members != null)
 174            {
 175                var userEmail = authState.User.FindFirst("https://chronicis.app/email")?.Value
 176                    ?? authState.User.FindFirst(System.Security.Claims.ClaimTypes.Email)?.Value
 177                    ?? authState.User.FindFirst("email")?.Value;
 178
 179                if (!string.IsNullOrEmpty(userEmail))
 180                {
 181                    var currentMember = world.Members.FirstOrDefault(m =>
 182                        m.Email.Equals(userEmail, StringComparison.OrdinalIgnoreCase));
 183
 184                    if (currentMember != null)
 185                    {
 186                        CurrentUserId = currentMember.UserId;
 187                        IsCurrentUserGm = currentMember.Role == WorldRole.GM;
 188                        IsCurrentUserWorldOwner = world.OwnerId == currentMember.UserId;
 189                    }
 190                }
 191            }
 192
 193            await _titleService.SetTitleAsync(world.Name);
 194            _treeState.ExpandPathToAndSelect(worldId);
 195
 196            // Initialise sub-ViewModels
 197            sharingVm.InitializeFrom(world);
 198            await linksVm.LoadAsync(worldId);
 199            await documentsVm.LoadAsync(worldId);
 200        }
 201        catch (Exception ex)
 202        {
 203            _logger.LogErrorSanitized(ex, "Error loading world {WorldId}", worldId);
 204            _notifier.Error($"Failed to load world: {ex.Message}");
 205        }
 206        finally
 207        {
 208            IsLoading = false;
 209        }
 210    }
 211
 212    /// <summary>Saves name, description, and sharing settings.</summary>
 213    public async Task SaveAsync(WorldSharingViewModel sharingVm)
 214    {
 215        if (_world == null || !CanManageWorldDetails || IsSaving)
 216            return;
 217
 218        IsSaving = true;
 219
 220        try
 221        {
 222            var updateDto = new WorldUpdateDto
 223            {
 224                Name = EditName.Trim(),
 225                Description = string.IsNullOrWhiteSpace(EditDescription) ? null : EditDescription.Trim(),
 226                PrivateNotes = string.IsNullOrWhiteSpace(EditPrivateNotes) ? null : EditPrivateNotes,
 227                IsPublic = sharingVm.IsPublic
 228            };
 229
 230            var updated = await _worldApi.UpdateWorldAsync(_world.Id, updateDto);
 231            if (updated != null)
 232            {
 233                _world.Name = updated.Name;
 234                _world.Description = updated.Description;
 235                _world.PrivateNotes = string.IsNullOrWhiteSpace(EditPrivateNotes) ? null : EditPrivateNotes;
 236                _world.IsPublic = updated.IsPublic;
 237                _world.Slug = updated.Slug;
 238                HasUnsavedChanges = false;
 239
 240                await _treeState.RefreshAsync();
 241                await _titleService.SetTitleAsync(EditName);
 242
 243                _notifier.Success("World saved");
 244            }
 245        }
 246        catch (Exception ex)
 247        {
 248            _logger.LogErrorSanitized(ex, "Error saving world {WorldId}", _world?.Id);
 249            _notifier.Error($"Failed to save: {ex.Message}");
 250        }
 251        finally
 252        {
 253            IsSaving = false;
 254        }
 255    }
 256
 257    /// <summary>Reloads member count and list after a membership change.</summary>
 258    public async Task OnMembersChangedAsync()
 259    {
 260        if (_world == null)
 261            return;
 262
 263        var updated = await _worldApi.GetWorldAsync(_world.Id);
 264        if (updated != null)
 265        {
 266            _world.MemberCount = updated.MemberCount;
 267            _world.Members = updated.Members;
 268            RaisePropertyChanged(nameof(World));
 269        }
 270    }
 271
 272    public async Task CreateCampaignAsync()
 273    {
 274        var parameters = new DialogParameters { { "WorldId", _world!.Id } };
 275        var dialog = await _dialogService.ShowAsync<CreateCampaignDialog>("New Campaign", parameters);
 276        var result = await dialog.Result;
 277
 278        if (result != null && !result.Canceled && result.Data is CampaignDto campaign)
 279        {
 280            await _treeState.RefreshAsync();
 281            await _navigator.GoToCampaignAsync(campaign);
 282            _notifier.Success("Campaign created");
 283        }
 284    }
 285
 286    public async Task CreateCharacterAsync()
 287    {
 288        var parameters = new DialogParameters
 289        {
 290            { "WorldId", _world!.Id },
 291            { "ArticleType", ArticleType.Character }
 292        };
 293
 294        var dialog = await _dialogService.ShowAsync<CreateArticleDialog>("New Player Character", parameters);
 295        var result = await dialog.Result;
 296
 297        if (result != null && !result.Canceled && result.Data is ArticleDto article)
 298        {
 299            await _treeState.RefreshAsync();
 300            NavigateToArticle(article);
 301            _notifier.Success("Character created");
 302        }
 303    }
 304
 305    public async Task CreateWikiArticleAsync()
 306    {
 307        var parameters = new DialogParameters
 308        {
 309            { "WorldId", _world!.Id },
 310            { "ArticleType", ArticleType.WikiArticle }
 311        };
 312
 313        var dialog = await _dialogService.ShowAsync<CreateArticleDialog>("New Wiki Article", parameters);
 314        var result = await dialog.Result;
 315
 316        if (result != null && !result.Canceled && result.Data is ArticleDto article)
 317        {
 318            await _treeState.RefreshAsync();
 319            NavigateToArticle(article);
 320            _notifier.Success("Article created");
 321        }
 322    }
 323
 324    private void NavigateToArticle(ArticleDto article)
 325    {
 3326        _ = _navigator.GoToArticleAsync(article);
 3327    }
 328}