< 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: 60
Uncovered lines: 0
Coverable lines: 60
Total lines: 343
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_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%44100%

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
 2030    private bool _isLoading = true;
 31    private WorldDetailDto? _world;
 2032    private string _editName = string.Empty;
 2033    private string _editDescription = string.Empty;
 2034    private string _editPrivateNotes = string.Empty;
 35    private bool _isSaving;
 36    private bool _hasUnsavedChanges;
 2037    private List<BreadcrumbItem> _breadcrumbs = new();
 38    private Guid _currentUserId;
 39    private bool _isCurrentUserGm;
 40    private bool _isCurrentUserWorldOwner;
 41
 2042    public WorldDetailViewModel(
 2043        IWorldApiService worldApi,
 2044        ITreeStateService treeState,
 2045        IBreadcrumbService breadcrumbService,
 2046        IDialogService dialogService,
 2047        IAppNavigator navigator,
 2048        IUserNotifier notifier,
 2049        IPageTitleService titleService,
 2050        AuthenticationStateProvider authStateProvider,
 2051        ILogger<WorldDetailViewModel> logger)
 52    {
 2053        _worldApi = worldApi;
 2054        _treeState = treeState;
 2055        _breadcrumbService = breadcrumbService;
 2056        _dialogService = dialogService;
 2057        _navigator = navigator;
 2058        _notifier = notifier;
 2059        _titleService = titleService;
 2060        _authStateProvider = authStateProvider;
 2061        _logger = logger;
 2062    }
 63
 64    public bool IsLoading
 65    {
 566        get => _isLoading;
 2967        private set => SetField(ref _isLoading, value);
 68    }
 69
 70    public WorldDetailDto? World
 71    {
 1172        get => _world;
 1173        private set => SetField(ref _world, value);
 74    }
 75
 76    public string EditName
 77    {
 578        get => _editName;
 79        set
 80        {
 1381            if (SetField(ref _editName, value))
 1382                HasUnsavedChanges = true;
 1383        }
 84    }
 85
 86    public string EditDescription
 87    {
 588        get => _editDescription;
 89        set
 90        {
 1191            if (SetField(ref _editDescription, value))
 1092                HasUnsavedChanges = true;
 1193        }
 94    }
 95
 96    public string EditPrivateNotes
 97    {
 398        get => _editPrivateNotes;
 99        set
 100        {
 13101            if (SetField(ref _editPrivateNotes, value) && CanManageWorldDetails)
 1102                HasUnsavedChanges = true;
 13103        }
 104    }
 105
 106    public bool IsSaving
 107    {
 7108        get => _isSaving;
 4109        private set => SetField(ref _isSaving, value);
 110    }
 111
 112    public bool HasUnsavedChanges
 113    {
 6114        get => _hasUnsavedChanges;
 36115        set => SetField(ref _hasUnsavedChanges, value);
 116    }
 117
 118    public List<BreadcrumbItem> Breadcrumbs
 119    {
 1120        get => _breadcrumbs;
 11121        private set => SetField(ref _breadcrumbs, value);
 122    }
 123
 124    public Guid CurrentUserId
 125    {
 1126        get => _currentUserId;
 23127        private set => SetField(ref _currentUserId, value);
 128    }
 129
 130    public bool IsCurrentUserGm
 131    {
 20132        get => _isCurrentUserGm;
 23133        private set => SetField(ref _isCurrentUserGm, value);
 134    }
 135
 136    public bool IsCurrentUserWorldOwner
 137    {
 11138        get => _isCurrentUserWorldOwner;
 23139        private set => SetField(ref _isCurrentUserWorldOwner, value);
 140    }
 141
 15142    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        if (sharingVm.IsPublic && !sharingVm.SlugIsAvailable)
 219        {
 220            _notifier.Warning("Please enter a valid, available public slug before saving");
 221            return;
 222        }
 223
 224        IsSaving = true;
 225
 226        try
 227        {
 228            var updateDto = new WorldUpdateDto
 229            {
 230                Name = EditName.Trim(),
 231                Description = string.IsNullOrWhiteSpace(EditDescription) ? null : EditDescription.Trim(),
 232                PrivateNotes = string.IsNullOrWhiteSpace(EditPrivateNotes) ? null : EditPrivateNotes,
 233                IsPublic = sharingVm.IsPublic,
 234                PublicSlug = sharingVm.IsPublic ? sharingVm.PublicSlug.Trim().ToLowerInvariant() : null
 235            };
 236
 237            var updated = await _worldApi.UpdateWorldAsync(_world.Id, updateDto);
 238            if (updated != null)
 239            {
 240                _world.Name = updated.Name;
 241                _world.Description = updated.Description;
 242                _world.PrivateNotes = string.IsNullOrWhiteSpace(EditPrivateNotes) ? null : EditPrivateNotes;
 243                _world.IsPublic = updated.IsPublic;
 244                _world.PublicSlug = updated.PublicSlug;
 245                HasUnsavedChanges = false;
 246
 247                await _treeState.RefreshAsync();
 248                await _titleService.SetTitleAsync(EditName);
 249
 250                _notifier.Success("World saved");
 251            }
 252        }
 253        catch (Exception ex)
 254        {
 255            _logger.LogErrorSanitized(ex, "Error saving world {WorldId}", _world?.Id);
 256            _notifier.Error($"Failed to save: {ex.Message}");
 257        }
 258        finally
 259        {
 260            IsSaving = false;
 261        }
 262    }
 263
 264    /// <summary>Reloads member count and list after a membership change.</summary>
 265    public async Task OnMembersChangedAsync()
 266    {
 267        if (_world == null)
 268            return;
 269
 270        var updated = await _worldApi.GetWorldAsync(_world.Id);
 271        if (updated != null)
 272        {
 273            _world.MemberCount = updated.MemberCount;
 274            _world.Members = updated.Members;
 275            RaisePropertyChanged(nameof(World));
 276        }
 277    }
 278
 279    public async Task CreateCampaignAsync()
 280    {
 281        var parameters = new DialogParameters { { "WorldId", _world!.Id } };
 282        var dialog = await _dialogService.ShowAsync<CreateCampaignDialog>("New Campaign", parameters);
 283        var result = await dialog.Result;
 284
 285        if (result != null && !result.Canceled && result.Data is CampaignDto campaign)
 286        {
 287            await _treeState.RefreshAsync();
 288            _navigator.NavigateTo($"/campaign/{campaign.Id}");
 289            _notifier.Success("Campaign created");
 290        }
 291    }
 292
 293    public async Task CreateCharacterAsync()
 294    {
 295        var parameters = new DialogParameters
 296        {
 297            { "WorldId", _world!.Id },
 298            { "ArticleType", ArticleType.Character }
 299        };
 300
 301        var dialog = await _dialogService.ShowAsync<CreateArticleDialog>("New Player Character", parameters);
 302        var result = await dialog.Result;
 303
 304        if (result != null && !result.Canceled && result.Data is ArticleDto article)
 305        {
 306            await _treeState.RefreshAsync();
 307            NavigateToArticle(article);
 308            _notifier.Success("Character created");
 309        }
 310    }
 311
 312    public async Task CreateWikiArticleAsync()
 313    {
 314        var parameters = new DialogParameters
 315        {
 316            { "WorldId", _world!.Id },
 317            { "ArticleType", ArticleType.WikiArticle }
 318        };
 319
 320        var dialog = await _dialogService.ShowAsync<CreateArticleDialog>("New Wiki Article", parameters);
 321        var result = await dialog.Result;
 322
 323        if (result != null && !result.Canceled && result.Data is ArticleDto article)
 324        {
 325            await _treeState.RefreshAsync();
 326            NavigateToArticle(article);
 327            _notifier.Success("Article created");
 328        }
 329    }
 330
 331    private void NavigateToArticle(ArticleDto article)
 332    {
 3333        if (article.Breadcrumbs != null && article.Breadcrumbs.Any())
 334        {
 1335            var path = _breadcrumbService.BuildArticleUrl(article.Breadcrumbs);
 1336            _navigator.NavigateTo(path);
 337        }
 338        else
 339        {
 2340            _navigator.NavigateTo($"/article/{article.Slug}");
 341        }
 2342    }
 343}