< Summary

Information
Class: Chronicis.Client.Components.Articles.ArticleMetadataDrawer
Assembly: Chronicis.Client
File(s): /home/runner/work/chronicis/chronicis/src/Chronicis.Client/Components/Articles/ArticleMetadataDrawer.razor
Line coverage
0%
Covered lines: 0
Uncovered lines: 204
Coverable lines: 204
Total lines: 668
Line coverage: 0%
Branch coverage
0%
Covered branches: 0
Total branches: 102
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/ArticleMetadataDrawer.razor

#LineLine coverage
 1@* ArticleMetadataDrawer.razor - Right-side metadata panel for articles *@
 2@* REFACTORED: Now manages data loading for backlinks, outgoing links, and external links *@
 3
 4@using Chronicis.Shared.Enums
 5@using Chronicis.Shared.DTOs
 6@inject IArticleApiService ArticleApi
 7@inject ILinkApiService LinkApi
 8@inject IArticleExternalLinkApiService ExternalLinkApi
 9@inject IArticleCacheService ArticleCache
 10@inject IUserApiService UserApi
 11@inject ITreeStateService TreeState
 12@inject NavigationManager Navigation
 13@inject ISnackbar Snackbar
 14
 15<style>
 16    .metadata-drawer,
 17    .metadata-drawer .mud-drawer-content {
 18        background-color: transparent !important;
 19        border-right-color: transparent;
 20    }
 21    .metadata-drawer .mud-drawer-content {
 22        padding: 8px;
 23        background-color: var(--chronicis-soft-off-white) !important;
 24    }
 25    .metadata-drawer.mud-drawer-persistent {
 26        height: auto !important;
 27    }
 28    .metadata-drawer p {
 29        font-size: 0.8rem;
 30    }
 31
 32    /* Scrollable metadata drawer */
 33    .metadata-drawer-scrollable {
 34        height: calc(100vh - 180px);
 35        overflow-y: auto;
 36        overflow-x: hidden;
 37        padding-bottom: 24px;
 38    }
 39
 40    /* Section styling */
 41    .metadata-section {
 42        margin-bottom: 8px;
 43    }
 44
 45    .section-header {
 46        display: flex;
 47        align-items: center;
 48        gap: 8px;
 49        padding: 12px 16px 8px 16px;
 50    }
 51
 52    .section-icon {
 53        color: var(--chronicis-beige-gold);
 54    }
 55
 56    .section-title {
 57        color: var(--mud-palette-text-secondary);
 58        text-transform: uppercase;
 59        letter-spacing: 0.75px;
 60        font-size: 0.75rem;
 61        font-weight: 600;
 62    }
 63
 64    /* Metadata row styling */
 65    .metadata-row {
 66        display: flex;
 67        align-items: flex-start;
 68        padding: 8px 16px;
 69        gap: 12px;
 70    }
 71
 72    .metadata-row .mud-icon-root {
 73        color: var(--mud-palette-text-secondary);
 74        margin-top: 2px;
 75    }
 76
 77    .metadata-content {
 78        flex: 1;
 79        min-width: 0;
 80    }
 81
 82    .metadata-label {
 83        font-size: 0.7rem;
 84        color: var(--mud-palette-text-secondary);
 85        text-transform: uppercase;
 86        letter-spacing: 0.5px;
 87        margin-bottom: 2px;
 88        font-weight: 500;
 89    }
 90
 91    .metadata-value {
 92        font-size: 0.875rem;
 93        color: var(--mud-palette-text-primary);
 94        word-break: break-word;
 95    }
 96
 97    .metadata-value-secondary {
 98        font-size: 0.75rem;
 99        color: var(--mud-palette-text-secondary);
 100        margin-top: 2px;
 101    }
 102</style>
 103
 104<MudDrawer @bind-Open="IsOpen"
 105           @bind-Open:after="OnOpenChangedInternal"
 106           Fixed="true"
 107           Anchor="Anchor.End"
 108           Elevation="9999"
 109           Variant="@DrawerVariant.Persistent"
 110           ClipMode="@DrawerClipMode.Always"
 111           Class="metadata-drawer">
 112    <MudDrawerHeader Class="backlinks-header">
 113        <MudText Typo="Typo.h6" Class="backlinks-title">
 114            <MudIcon Icon="@Icons.Material.Filled.IntegrationInstructions" Size="Size.Small" />
 115            Metadata
 116        </MudText>
 117    </MudDrawerHeader>
 118    <MudDrawerContainer Class="metadata-drawer-scrollable chronicis-scrollbar-light">
 119
 120        <!-- ========================================= -->
 121        <!-- SECTION 1: LINKS & CONNECTIONS           -->
 122        <!-- ========================================= -->
 123        <div class="metadata-section">
 124            <div class="section-header">
 125                <MudIcon Icon="@Icons.Material.Filled.AccountTree" Size="Size.Small" Class="section-icon" />
 126                <MudText Typo="Typo.subtitle2" Class="section-title">Links & Connections</MudText>
 127            </div>
 128        </div>
 129
 130        <!-- Outgoing Links Panel - REFACTORED to use data parameters -->
 131        <OutgoingLinksPanel OutgoingLinks="_outgoingLinks"
 132                            IsLoading="_loadingOutgoingLinks"
 133                            OnNavigateToArticle="HandleNavigateToArticle" />
 134
 135        <MudDivider Class="my-2" Style="opacity: 0.2;" />
 136
 137        <!-- Backlinks Panel - REFACTORED to use data parameters -->
 138        <BacklinksPanel Backlinks="_backlinks"
 139                        IsLoading="_loadingBacklinks"
 140                        OnNavigateToArticle="HandleNavigateToArticle" />
 141
 142        <MudDivider Class="my-2" Style="opacity: 0.2;" />
 143
 144        <!-- External Links Panel - REFACTORED to use data parameters -->
 145        <ExternalLinksPanel ExternalLinks="_externalLinks"
 146                            IsLoading="_loadingExternalLinks" />
 147
 148        <MudDivider Class="my-4" Style="opacity: 0.3;" />
 149
 150        <!-- ========================================= -->
 151        <!-- SECTION 2: QUICK ACTIONS (Creator Only)  -->
 152        <!-- ========================================= -->
 0153        @if (_isCreator)
 154        {
 155            <!-- Visibility Section -->
 156            <div class="metadata-section">
 157                <div class="section-header">
 158                    <MudIcon Icon="@GetVisibilityIcon(_currentVisibility)" Size="Size.Small" Class="section-icon" />
 159                    <MudText Typo="Typo.subtitle2" Class="section-title">Visibility</MudText>
 160                </div>
 161
 162                <div class="px-4 pb-3">
 163                    <MudSelect T="ArticleVisibility"
 164                               Value="@_currentVisibility"
 165                               ValueChanged="@HandleVisibilityChanged"
 166                               Variant="Variant.Outlined"
 167                               Dense="true"
 168                               Disabled="_isSavingVisibility"
 169                               AnchorOrigin="Origin.BottomCenter"
 170                               TransformOrigin="Origin.TopCenter">
 171                        <MudSelectItem Value="ArticleVisibility.Public">
 172                            <div class="d-flex align-center gap-2">
 173                                <MudIcon Icon="@Icons.Material.Filled.Public" Size="Size.Small" />
 174                                <span>Public</span>
 175                            </div>
 176                        </MudSelectItem>
 177                        <MudSelectItem Value="ArticleVisibility.MembersOnly">
 178                            <div class="d-flex align-center gap-2">
 179                                <MudIcon Icon="@Icons.Material.Filled.Group" Size="Size.Small" />
 180                                <span>Members Only</span>
 181                            </div>
 182                        </MudSelectItem>
 183                        <MudSelectItem Value="ArticleVisibility.Private">
 184                            <div class="d-flex align-center gap-2">
 185                                <MudIcon Icon="@Icons.Material.Filled.Lock" Size="Size.Small" />
 186                                <span>Private</span>
 187                            </div>
 188                        </MudSelectItem>
 189                    </MudSelect>
 190                    <MudText Typo="Typo.caption" Class="mt-1" Style="color: var(--mud-palette-text-secondary); font-size
 0191                        @GetVisibilityDescription(_currentVisibility)
 192                    </MudText>
 193                </div>
 194            </div>
 195
 196            <!-- Aliases Section -->
 197            <div class="metadata-section">
 198                <div class="section-header">
 199                    <MudIcon Icon="@Icons.Material.Filled.Link" Size="Size.Small" Class="section-icon" />
 200                    <MudText Typo="Typo.subtitle2" Class="section-title">Aliases</MudText>
 201                </div>
 202
 203                <div class="px-4 pb-3">
 204                    <MudTextField @bind-Value="_aliasesText"
 205                                  Placeholder="Enter aliases, separated by commas"
 206                                  Variant="Variant.Outlined"
 207                                  Lines="2"
 208                                  HelperText="Alternative names for linking"
 209                                  Disabled="_isSavingAliases"
 210                                  Immediate="false"
 211                                  DebounceInterval="500"
 212                                  OnBlur="HandleAliasesBlur" />
 0213                    @if (_isSavingAliases)
 214                    {
 215                        <MudProgressLinear Color="Color.Primary" Indeterminate="true" Class="mt-1" />
 216                    }
 217                </div>
 218            </div>
 219
 220            <MudDivider Class="my-4" Style="opacity: 0.3;" />
 221        }
 222
 223        <!-- ========================================= -->
 224        <!-- SECTION 3: ARTICLE INFORMATION           -->
 225        <!-- ========================================= -->
 226        <div class="metadata-section">
 227            <div class="section-header">
 228                <MudIcon Icon="@Icons.Material.Filled.Info" Size="Size.Small" Class="section-icon" />
 229                <MudText Typo="Typo.subtitle2" Class="section-title">Article Information</MudText>
 230            </div>
 231        </div>
 232
 233        <!-- Article Type -->
 234        <div class="metadata-row">
 235            <MudIcon Icon="@GetArticleTypeIcon(Article.Type)" Size="Size.Small" />
 236            <div class="metadata-content">
 237                <div class="metadata-label">Type</div>
 0238                <div class="metadata-value">@GetArticleTypeLabel(Article.Type)</div>
 239            </div>
 240        </div>
 241
 242        <!-- Visibility (for non-creators, read-only) -->
 0243        @if (!_isCreator)
 244        {
 245            <div class="metadata-row">
 246                <MudIcon Icon="@GetVisibilityIcon(Article.Visibility)" Size="Size.Small" />
 247                <div class="metadata-content">
 248                    <div class="metadata-label">Visibility</div>
 0249                    <div class="metadata-value">@GetVisibilityLabel(Article.Visibility)</div>
 250                </div>
 251            </div>
 252        }
 253
 254        <!-- Effective Date -->
 255        <div class="metadata-row">
 256            <MudIcon Icon="@Icons.Material.Filled.Event" Size="Size.Small" />
 257            <div class="metadata-content">
 258                <div class="metadata-label">Effective Date</div>
 0259                <div class="metadata-value">@Article.EffectiveDate.ToString("MMMM d, yyyy")</div>
 0260                <div class="metadata-value-secondary">@Article.EffectiveDate.ToString("h:mm tt")</div>
 261            </div>
 262        </div>
 263
 264        <!-- Created By -->
 265        <div class="metadata-row">
 266            <MudIcon Icon="@Icons.Material.Filled.PersonAdd" Size="Size.Small" />
 267            <div class="metadata-content">
 268                <div class="metadata-label">Created By</div>
 0269                <div class="metadata-value">@(Article.CreatedByName ?? "Unknown")</div>
 0270                <div class="metadata-value-secondary">@Article.CreatedAt.ToString("MMM d, yyyy 'at' h:mm tt")</div>
 271            </div>
 272        </div>
 273
 274        <!-- Last Modified By (only show if modified) -->
 0275        @if (Article.ModifiedAt.HasValue)
 276        {
 277            <div class="metadata-row">
 278                <MudIcon Icon="@Icons.Material.Filled.Edit" Size="Size.Small" />
 279                <div class="metadata-content">
 280                    <div class="metadata-label">Last Modified By</div>
 0281                    <div class="metadata-value">@(Article.LastModifiedByName ?? Article.CreatedByName ?? "Unknown")</div
 0282                    <div class="metadata-value-secondary">@Article.ModifiedAt.Value.ToString("MMM d, yyyy 'at' h:mm tt")
 283                </div>
 284            </div>
 285        }
 286
 287        <!-- Article ID -->
 288        <div class="metadata-row">
 289            <MudIcon Icon="@Icons.Material.Filled.Fingerprint" Size="Size.Small" />
 290            <div class="metadata-content">
 291                <div class="metadata-label">Article ID</div>
 0292                <div class="metadata-value" style="font-family: monospace; font-size: 0.7rem;">@Article.Id</div>
 293            </div>
 294        </div>
 295
 296        <!-- World ID -->
 0297        @if (Article.WorldId.HasValue)
 298        {
 299            <div class="metadata-row">
 300                <MudIcon Icon="@Icons.Material.Filled.Public" Size="Size.Small" />
 301                <div class="metadata-content">
 302                    <div class="metadata-label">World ID</div>
 0303                    <div class="metadata-value" style="font-family: monospace; font-size: 0.7rem;">@Article.WorldId</div
 304                </div>
 305            </div>
 306        }
 307
 308        <!-- Campaign ID -->
 0309        @if (Article.CampaignId.HasValue)
 310        {
 311            <div class="metadata-row">
 312                <MudIcon Icon="@Icons.Material.Filled.Map" Size="Size.Small" />
 313                <div class="metadata-content">
 314                    <div class="metadata-label">Campaign ID</div>
 0315                    <div class="metadata-value" style="font-family: monospace; font-size: 0.7rem;">@Article.CampaignId</
 316                </div>
 317            </div>
 318        }
 319
 320        <!-- Arc ID -->
 0321        @if (Article.ArcId.HasValue)
 322        {
 323            <div class="metadata-row">
 324                <MudIcon Icon="@Icons.Material.Filled.Timeline" Size="Size.Small" />
 325                <div class="metadata-content">
 326                    <div class="metadata-label">Arc ID</div>
 0327                    <div class="metadata-value" style="font-family: monospace; font-size: 0.7rem;">@Article.ArcId</div>
 328                </div>
 329            </div>
 330        }
 331
 332    </MudDrawerContainer>
 333</MudDrawer>
 334
 335@code {
 336    // Link panel state - managed by THIS component
 0337    private List<BacklinkDto> _backlinks = new();
 338    private bool _loadingBacklinks = false;
 339
 0340    private List<BacklinkDto> _outgoingLinks = new();
 341    private bool _loadingOutgoingLinks = false;
 342
 0343    private List<ArticleExternalLinkDto> _externalLinks = new();
 344    private bool _loadingExternalLinks = false;
 345
 0346    private Guid _lastArticleId = Guid.Empty;
 347    private bool _isCreator;
 348    private ArticleVisibility _currentVisibility;
 349    private bool _isSavingVisibility;
 350    private Guid? _currentUserId;
 351
 352    // Aliases
 0353    private string _aliasesText = string.Empty;
 0354    private string _originalAliasesText = string.Empty;
 355    private bool _isSavingAliases;
 356
 357    [Parameter]
 0358    public ArticleDto Article { get; set; } = null!;
 359
 360    [Parameter]
 0361    public bool IsOpen { get; set; }
 362
 363    [Parameter]
 0364    public EventCallback<bool> IsOpenChanged { get; set; }
 365
 366    [Parameter]
 0367    public EventCallback OnArticleVisibilityChanged { get; set; }
 368
 369    protected override async Task OnInitializedAsync()
 370    {
 0371        var user = await UserApi.GetUserProfileAsync();
 0372        _currentUserId = user?.Id;
 0373    }
 374
 375    protected override async Task OnParametersSetAsync()
 376    {
 0377        _isCreator = _currentUserId.HasValue && Article.CreatedBy == _currentUserId.Value;
 0378        _currentVisibility = Article.Visibility;
 379
 380        // Initialize aliases text from Article
 0381        var newAliasesText = Article.Aliases != null && Article.Aliases.Any()
 0382            ? string.Join(", ", Article.Aliases.Select(a => a.AliasText))
 0383            : string.Empty;
 384
 385        // Only update if the article changed (not just user editing)
 0386        if (_originalAliasesText != newAliasesText || string.IsNullOrEmpty(_aliasesText))
 387        {
 0388            _aliasesText = newAliasesText;
 0389            _originalAliasesText = newAliasesText;
 390        }
 391
 392        // Load links when article changes
 0393        var articleChanged = Article.Id != _lastArticleId;
 0394        if (articleChanged)
 395        {
 0396            _lastArticleId = Article.Id;
 0397            _backlinks = new();
 0398            _outgoingLinks = new();
 0399            _externalLinks = new();
 400
 401            // Only load if drawer is already open
 0402            if (IsOpen)
 403            {
 0404                await Task.WhenAll(
 0405                    LoadBacklinksAsync(),
 0406                    LoadOutgoingLinksAsync(),
 0407                    LoadExternalLinksAsync()
 0408                );
 409            }
 410        }
 0411    }
 412
 413    private async Task HandleVisibilityChanged(ArticleVisibility newVisibility)
 414    {
 0415        if (_isSavingVisibility || newVisibility == _currentVisibility) return;
 416
 0417        var previousVisibility = _currentVisibility;
 0418        _isSavingVisibility = true;
 0419        _currentVisibility = newVisibility; // Optimistic update
 0420        StateHasChanged();
 421
 422        try
 423        {
 0424            var updateDto = new ArticleUpdateDto
 0425            {
 0426                Title = Article.Title,
 0427                Body = Article.Body,
 0428                Visibility = newVisibility
 0429            };
 430
 0431            await ArticleApi.UpdateArticleAsync(Article.Id, updateDto);
 432
 0433            Article.Visibility = newVisibility;
 0434            TreeState.UpdateNodeVisibility(Article.Id, newVisibility);
 435
 0436            Snackbar.Add($"Visibility changed to {GetVisibilityLabel(newVisibility)}", Severity.Success);
 437
 0438            await OnArticleVisibilityChanged.InvokeAsync();
 0439        }
 0440        catch (Exception ex)
 441        {
 0442            Snackbar.Add($"Failed to update visibility: {ex.Message}", Severity.Error);
 0443            _currentVisibility = previousVisibility; // Revert on failure
 0444        }
 445        finally
 446        {
 0447            _isSavingVisibility = false;
 0448            StateHasChanged();
 449        }
 0450    }
 451
 0452    private static string GetVisibilityIcon(ArticleVisibility visibility) => visibility switch
 0453    {
 0454        ArticleVisibility.Public => Icons.Material.Filled.Public,
 0455        ArticleVisibility.MembersOnly => Icons.Material.Filled.Group,
 0456        ArticleVisibility.Private => Icons.Material.Filled.Lock,
 0457        _ => Icons.Material.Filled.Help
 0458    };
 459
 0460    private static string GetVisibilityLabel(ArticleVisibility visibility) => visibility switch
 0461    {
 0462        ArticleVisibility.Public => "Public",
 0463        ArticleVisibility.MembersOnly => "Members Only",
 0464        ArticleVisibility.Private => "Private",
 0465        _ => "Unknown"
 0466    };
 467
 0468    private static string GetVisibilityDescription(ArticleVisibility visibility) => visibility switch
 0469    {
 0470        ArticleVisibility.Public => "Anyone can view this article, including visitors to public worlds.",
 0471        ArticleVisibility.MembersOnly => "Only members of this world can view this article.",
 0472        ArticleVisibility.Private => "Only you can view this article. Hidden from all other users.",
 0473        _ => ""
 0474    };
 475
 0476    private static string GetArticleTypeIcon(ArticleType type) => type switch
 0477    {
 0478        ArticleType.WikiArticle => Icons.Material.Filled.Article,
 0479        ArticleType.Character => Icons.Material.Filled.Person,
 0480        ArticleType.CharacterNote => Icons.Material.Filled.Note,
 0481        ArticleType.Session => Icons.Material.Filled.Casino,
 0482        ArticleType.SessionNote => Icons.Material.Filled.EditNote,
 0483        ArticleType.Legacy => Icons.Material.Filled.History,
 0484        _ => Icons.Material.Filled.Description
 0485    };
 486
 0487    private static string GetArticleTypeLabel(ArticleType type) => type switch
 0488    {
 0489        ArticleType.WikiArticle => "Wiki Article",
 0490        ArticleType.Character => "Character",
 0491        ArticleType.CharacterNote => "Character Note",
 0492        ArticleType.Session => "Session",
 0493        ArticleType.SessionNote => "Session Note",
 0494        ArticleType.Legacy => "Legacy Article",
 0495        _ => "Article"
 0496    };
 497
 498    private async Task HandleAliasesBlur()
 499    {
 500        // Normalize for comparison: trim, dedupe, sort
 0501        var normalizedNew = NormalizeAliasesText(_aliasesText);
 0502        var normalizedOriginal = NormalizeAliasesText(_originalAliasesText);
 503
 0504        if (normalizedNew == normalizedOriginal || _isSavingAliases)
 505        {
 0506            return;
 507        }
 508
 0509        _isSavingAliases = true;
 0510        StateHasChanged();
 511
 512        try
 513        {
 0514            var result = await ArticleApi.UpdateAliasesAsync(Article.Id, _aliasesText);
 515
 0516            if (result != null)
 517            {
 518                // Update the article's aliases from the response
 0519                Article.Aliases = result.Aliases;
 520
 521                // Update our tracking
 0522                _originalAliasesText = _aliasesText;
 523
 0524                Snackbar.Add("Aliases updated", Severity.Success);
 525            }
 526            else
 527            {
 0528                Snackbar.Add("Failed to update aliases", Severity.Error);
 529                // Revert to original
 0530                _aliasesText = _originalAliasesText;
 531            }
 0532        }
 0533        catch (Exception ex)
 534        {
 0535            Snackbar.Add($"Failed to update aliases: {ex.Message}", Severity.Error);
 0536            _aliasesText = _originalAliasesText;
 0537        }
 538        finally
 539        {
 0540            _isSavingAliases = false;
 0541            StateHasChanged();
 542        }
 0543    }
 544
 545    private static string NormalizeAliasesText(string? text)
 546    {
 0547        if (string.IsNullOrWhiteSpace(text))
 0548            return string.Empty;
 549
 0550        var aliases = text
 0551            .Split(',', StringSplitOptions.RemoveEmptyEntries)
 0552            .Select(a => a.Trim().ToLowerInvariant())
 0553            .Where(a => !string.IsNullOrWhiteSpace(a))
 0554            .Distinct()
 0555            .OrderBy(a => a);
 556
 0557        return string.Join(",", aliases);
 558    }
 559
 560    public async Task RefreshPanelsAsync()
 561    {
 0562        await Task.WhenAll(
 0563            LoadBacklinksAsync(),
 0564            LoadOutgoingLinksAsync(),
 0565            LoadExternalLinksAsync()
 0566        );
 0567    }
 568
 569    private async Task OnOpenChangedInternal()
 570    {
 0571        await IsOpenChanged.InvokeAsync(IsOpen);
 572
 573        // Load links when drawer opens (if not already loaded)
 0574        if (IsOpen)
 575        {
 0576            var tasks = new List<Task>();
 577
 0578            if (!_backlinks.Any() && !_loadingBacklinks)
 0579                tasks.Add(LoadBacklinksAsync());
 580
 0581            if (!_outgoingLinks.Any() && !_loadingOutgoingLinks)
 0582                tasks.Add(LoadOutgoingLinksAsync());
 583
 0584            if (!_externalLinks.Any() && !_loadingExternalLinks)
 0585                tasks.Add(LoadExternalLinksAsync());
 586
 0587            if (tasks.Any())
 0588                await Task.WhenAll(tasks);
 589        }
 0590    }
 591
 592    private async Task LoadBacklinksAsync()
 593    {
 0594        _loadingBacklinks = true;
 0595        StateHasChanged();
 596
 597        try
 598        {
 0599            _backlinks = await LinkApi.GetBacklinksAsync(Article.Id);
 0600        }
 0601        catch (Exception)
 602        {
 0603            _backlinks = new List<BacklinkDto>();
 0604        }
 605        finally
 606        {
 0607            _loadingBacklinks = false;
 0608            StateHasChanged();
 609        }
 0610    }
 611
 612    private async Task LoadOutgoingLinksAsync()
 613    {
 0614        _loadingOutgoingLinks = true;
 0615        StateHasChanged();
 616
 617        try
 618        {
 0619            _outgoingLinks = await LinkApi.GetOutgoingLinksAsync(Article.Id);
 0620        }
 0621        catch (Exception)
 622        {
 0623            _outgoingLinks = new List<BacklinkDto>();
 0624        }
 625        finally
 626        {
 0627            _loadingOutgoingLinks = false;
 0628            StateHasChanged();
 629        }
 0630    }
 631
 632    private async Task LoadExternalLinksAsync()
 633    {
 0634        _loadingExternalLinks = true;
 0635        StateHasChanged();
 636
 637        try
 638        {
 0639            _externalLinks = await ExternalLinkApi.GetExternalLinksAsync(Article.Id);
 0640        }
 0641        catch (Exception)
 642        {
 0643            _externalLinks = new List<ArticleExternalLinkDto>();
 0644        }
 645        finally
 646        {
 0647            _loadingExternalLinks = false;
 0648            StateHasChanged();
 649        }
 0650    }
 651
 652    private async Task HandleNavigateToArticle(Guid articleId)
 653    {
 654        try
 655        {
 0656            var path = await ArticleCache.GetNavigationPathAsync(articleId);
 657
 0658            if (!string.IsNullOrEmpty(path))
 659            {
 0660                Navigation.NavigateTo($"/article/{path}");
 661            }
 0662        }
 0663        catch (Exception)
 664        {
 665            // Navigation failed - could log or show error
 0666        }
 0667    }
 668}