< Summary

Line coverage
100%
Covered lines: 19
Uncovered lines: 0
Coverable lines: 19
Total lines: 295
Line coverage: 100%
Branch coverage
100%
Covered branches: 8
Total branches: 8
Branch coverage: 100%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
File 1: BuildRenderTree(...)100%44100%
File 2: .ctor()100%11100%
File 2: OnPageTypeChanged(...)100%44100%
File 2: ResetCreateForm()100%11100%

File(s)

/home/runner/work/chronicis/chronicis/src/Chronicis.Client/Components/Admin/AdminTutorialsPanel.razor

#LineLine coverage
 1@using Chronicis.Shared.DTOs
 2
 3<div class="atp-container">
 4    <MudText Typo="Typo.h5" Class="mb-2">Tutorial Mappings</MudText>
 5    <MudText Typo="Typo.body2" Class="mud-text-secondary mb-4">
 6        Create contextual tutorial mappings and open tutorial articles in the normal editor.
 7    </MudText>
 8
 9    <MudPaper Elevation="1" Class="pa-4 mb-4">
 10        <MudText Typo="Typo.subtitle1" Class="mb-3">Create Tutorial</MudText>
 11
 12        <MudGrid Spacing="2">
 13            <MudItem xs="12" md="5">
 14                <MudSelect T="string"
 15                           Label="Page Type"
 16                           Variant="Variant.Outlined"
 17                           Margin="Margin.Dense"
 18                           Value="_selectedPageType"
 19                           ValueChanged="OnPageTypeChanged">
 20                    @foreach (var option in _pageTypeOptions)
 21                    {
 22                        <MudSelectItem T="string" Value="@option.PageType">@option.DisplayLabel</MudSelectItem>
 23                    }
 24                </MudSelect>
 25            </MudItem>
 26
 27            <MudItem xs="12" md="4">
 28                <MudTextField T="string"
 29                              @bind-Value="_pageTypeName"
 30                              Label="Page Type Name"
 31                              Variant="Variant.Outlined"
 32                              Margin="Margin.Dense" />
 33            </MudItem>
 34
 35            <MudItem xs="12" md="3">
 36                <MudTextField T="string"
 37                              @bind-Value="_tutorialTitle"
 38                              Label="Tutorial Article Title"
 39                              Variant="Variant.Outlined"
 40                              Margin="Margin.Dense" />
 41            </MudItem>
 42        </MudGrid>
 43
 44        <div class="d-flex align-center gap-3 mt-2">
 45            <MudButton Variant="Variant.Filled"
 46                       Color="Color.Primary"
 47                       StartIcon="@Icons.Material.Filled.Add"
 48                       Disabled="@(_isCreating || _isLoading)"
 49                       OnClick="CreateTutorialAsync">
 50                Create Tutorial
 51            </MudButton>
 52
 53            <MudText Typo="Typo.caption" Class="mud-text-secondary">
 54                Creates a new <code>ArticleType.Tutorial</code> article and mapping, then opens the article editor.
 55            </MudText>
 56        </div>
 57    </MudPaper>
 58
 259    @if (_isLoading)
 60    {
 61        <MudProgressLinear Indeterminate="true" Color="Color.Primary" Class="mb-4" />
 62    }
 163    else if (!string.IsNullOrWhiteSpace(_loadError))
 64    {
 65        <MudAlert Severity="Severity.Error" Class="mb-4">@_loadError</MudAlert>
 66    }
 67
 68    <MudTable Items="_mappings"
 69              Dense="true"
 70              Hover="true"
 71              Striped="true"
 72              Loading="_isLoading">
 73        <ToolBarContent>
 74            <MudText Typo="Typo.subtitle1">@_mappings.Count tutorial mapping(s)</MudText>
 75            <MudSpacer />
 76            <MudTooltip Text="Refresh">
 77                <MudIconButton Icon="@Icons.Material.Filled.Refresh"
 78                               Color="Color.Default"
 79                               OnClick="LoadMappingsAsync" />
 80            </MudTooltip>
 81        </ToolBarContent>
 82        <HeaderContent>
 83            <MudTh>Page Type</MudTh>
 84            <MudTh>Page Type Name</MudTh>
 85            <MudTh>Title</MudTh>
 86            <MudTh>Last Modified</MudTh>
 87            <MudTh></MudTh>
 88        </HeaderContent>
 89        <RowTemplate>
 90            <MudTd DataLabel="Page Type">
 91                <MudText Typo="Typo.body2" Style="font-weight:600;">@context.PageType</MudText>
 92            </MudTd>
 93            <MudTd DataLabel="Page Type Name">@context.PageTypeName</MudTd>
 94            <MudTd DataLabel="Title">@context.Title</MudTd>
 95            <MudTd DataLabel="Last Modified">@context.ModifiedAt.ToLocalTime().ToString("yyyy-MM-dd HH:mm")</MudTd>
 96            <MudTd>
 97                <MudTooltip Text="Open tutorial article in editor">
 98                    <MudIconButton Icon="@Icons.Material.Filled.Edit"
 99                                   Color="Color.Primary"
 100                                   Size="Size.Small"
 101                                   OnClick="@(() => OpenTutorialEditorAsync(context.PageType, context.ArticleId))" />
 102                </MudTooltip>
 103            </MudTd>
 104        </RowTemplate>
 105        <NoRecordsContent>
 106            <MudText Class="pa-4 mud-text-secondary">No tutorial mappings found.</MudText>
 107        </NoRecordsContent>
 108    </MudTable>
 109</div>

/home/runner/work/chronicis/chronicis/src/Chronicis.Client/Components/Admin/AdminTutorialsPanel.razor.cs

#LineLine coverage
 1using Chronicis.Client.Services;
 2using Chronicis.Shared.DTOs;
 3using Chronicis.Shared.Enums;
 4using Microsoft.AspNetCore.Components;
 5using MudBlazor;
 6
 7namespace Chronicis.Client.Components.Admin;
 8
 9/// <summary>
 10/// SysAdmin panel for managing tutorial page mappings and opening tutorial articles in the editor.
 11/// </summary>
 12public partial class AdminTutorialsPanel : ComponentBase
 13{
 14    [Inject] private IAdminApiService AdminApi { get; set; } = default!;
 15    [Inject] private IArticleApiService ArticleApi { get; set; } = default!;
 16    [Inject] private IBreadcrumbService BreadcrumbService { get; set; } = default!;
 17    [Inject] private NavigationManager Navigation { get; set; } = default!;
 18    [Inject] private ISnackbar Snackbar { get; set; } = default!;
 19    [Inject] private ILogger<AdminTutorialsPanel> Logger { get; set; } = default!;
 20
 621    private readonly IReadOnlyList<TutorialPageTypeOption> _pageTypeOptions = TutorialPageTypes.All;
 22
 623    private List<TutorialMappingDto> _mappings = new();
 24    private bool _isLoading;
 25    private bool _isCreating;
 26    private string? _loadError;
 27
 28    private string? _selectedPageType;
 629    private string _pageTypeName = string.Empty;
 630    private string _tutorialTitle = string.Empty;
 31
 32    protected override async Task OnInitializedAsync()
 33        => await LoadMappingsAsync();
 34
 35    internal async Task LoadMappingsAsync()
 36    {
 37        _isLoading = true;
 38        _loadError = null;
 39        StateHasChanged();
 40
 41        try
 42        {
 43            _mappings = await AdminApi.GetTutorialMappingsAsync();
 44        }
 45        catch (Exception ex)
 46        {
 47            _loadError = "Failed to load tutorial mappings.";
 48            Logger.LogError(ex, "Error loading tutorial mappings");
 49        }
 50        finally
 51        {
 52            _isLoading = false;
 53        }
 54    }
 55
 56    private void OnPageTypeChanged(string? pageType)
 57    {
 358        _selectedPageType = pageType;
 59
 360        var selectedOption = TutorialPageTypes.Find(pageType);
 361        if (selectedOption == null)
 62        {
 163            return;
 64        }
 65
 266        _pageTypeName = selectedOption.DefaultName;
 267        _tutorialTitle = selectedOption.PageType == "Page:Default"
 268            ? "Default Tutorial"
 269            : $"{selectedOption.DefaultName} Tutorial";
 270    }
 71
 72    private async Task CreateTutorialAsync()
 73    {
 74        var pageType = _selectedPageType?.Trim();
 75        var pageTypeName = _pageTypeName.Trim();
 76        var tutorialTitle = _tutorialTitle.Trim();
 77
 78        if (string.IsNullOrWhiteSpace(pageType))
 79        {
 80            Snackbar.Add("Select a page type.", Severity.Warning);
 81            return;
 82        }
 83
 84        if (string.IsNullOrWhiteSpace(pageTypeName))
 85        {
 86            Snackbar.Add("Page type name is required.", Severity.Warning);
 87            return;
 88        }
 89
 90        if (string.IsNullOrWhiteSpace(tutorialTitle))
 91        {
 92            Snackbar.Add("Tutorial article title is required.", Severity.Warning);
 93            return;
 94        }
 95
 96        _isCreating = true;
 97        try
 98        {
 99            var created = await AdminApi.CreateTutorialMappingAsync(new TutorialMappingCreateDto
 100            {
 101                PageType = pageType,
 102                PageTypeName = pageTypeName,
 103                Title = tutorialTitle,
 104                Body = "<p>Write tutorial content here.</p>"
 105            });
 106
 107            if (created == null)
 108            {
 109                Snackbar.Add("Failed to create tutorial mapping. Check for duplicate page type or authorization.", Sever
 110                return;
 111            }
 112
 113            Snackbar.Add($"Created tutorial mapping for {created.PageType}.", Severity.Success);
 114            ResetCreateForm();
 115            await LoadMappingsAsync();
 116            await OpenTutorialEditorByArticleIdAsync(created.ArticleId);
 117        }
 118        catch (Exception ex)
 119        {
 120            Snackbar.Add("Unexpected error creating tutorial mapping.", Severity.Error);
 121            Logger.LogError(ex, "Error creating tutorial mapping for {PageType}", pageType);
 122        }
 123        finally
 124        {
 125            _isCreating = false;
 126        }
 127    }
 128
 129    private async Task OpenTutorialEditorAsync(string pageType, Guid articleId)
 130    {
 131        Logger.LogInformation(
 132            "Opening tutorial editor from admin row {PageType} -> {ArticleId}",
 133            pageType,
 134            articleId);
 135
 136        await OpenTutorialEditorByArticleIdAsync(articleId);
 137    }
 138
 139    private async Task OpenTutorialEditorByArticleIdAsync(Guid articleId)
 140    {
 141        try
 142        {
 143            var article = await ArticleApi.GetArticleDetailAsync(articleId);
 144            if (article == null)
 145            {
 146                Snackbar.Add("Tutorial article could not be loaded.", Severity.Error);
 147                return;
 148            }
 149
 150            if (article.Breadcrumbs == null || article.Breadcrumbs.Count == 0)
 151            {
 152                Snackbar.Add("Tutorial article path could not be resolved.", Severity.Error);
 153                return;
 154            }
 155
 156            if (article.Type == ArticleType.Tutorial &&
 157                article.WorldId == Guid.Empty &&
 158                !string.IsNullOrWhiteSpace(article.Slug))
 159            {
 160                // Tutorial articles are global/system content. Navigating by slug avoids
 161                // edge cases in breadcrumb-derived paths for synthetic system-world rows.
 162                var tutorialUrl = $"/article/system-tutorial/{Uri.EscapeDataString(article.Slug)}";
 163                Navigation.NavigateTo(tutorialUrl, forceLoad: true);
 164                return;
 165            }
 166
 167            var articleUrl = BreadcrumbService.BuildArticleUrl(article.Breadcrumbs);
 168
 169            // Tutorial articles are intentionally excluded from the tree. Force a reload so
 170            // the /article page can bootstrap selection from the route before tree init completes.
 171            Navigation.NavigateTo(articleUrl, forceLoad: true);
 172        }
 173        catch (Exception ex)
 174        {
 175            Snackbar.Add("Failed to open tutorial article editor.", Severity.Error);
 176            Logger.LogError(ex, "Error opening tutorial article editor for {ArticleId}", articleId);
 177        }
 178    }
 179
 180    private void ResetCreateForm()
 181    {
 1182        _selectedPageType = null;
 1183        _pageTypeName = string.Empty;
 1184        _tutorialTitle = string.Empty;
 1185    }
 186}