< Summary

Line coverage
100%
Covered lines: 19
Uncovered lines: 0
Coverable lines: 19
Total lines: 292
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.Abstractions;
 2using Chronicis.Client.Services;
 3using Chronicis.Shared.DTOs;
 4using Chronicis.Shared.Enums;
 5using Chronicis.Shared.Extensions;
 6using Microsoft.AspNetCore.Components;
 7using MudBlazor;
 8
 9namespace Chronicis.Client.Components.Admin;
 10
 11/// <summary>
 12/// SysAdmin panel for managing tutorial page mappings and opening tutorial articles in the editor.
 13/// </summary>
 14public partial class AdminTutorialsPanel : ComponentBase
 15{
 16    [Inject] private IAdminApiService AdminApi { get; set; } = default!;
 17    [Inject] private IArticleApiService ArticleApi { get; set; } = default!;
 18    [Inject] private IAppNavigator AppNavigator { get; set; } = default!;
 19    [Inject] private ISnackbar Snackbar { get; set; } = default!;
 20    [Inject] private ILogger<AdminTutorialsPanel> Logger { get; set; } = default!;
 21
 622    private readonly IReadOnlyList<TutorialPageTypeOption> _pageTypeOptions = TutorialPageTypes.All;
 23
 624    private List<TutorialMappingDto> _mappings = new();
 25    private bool _isLoading;
 26    private bool _isCreating;
 27    private string? _loadError;
 28
 29    private string? _selectedPageType;
 630    private string _pageTypeName = string.Empty;
 631    private string _tutorialTitle = string.Empty;
 32
 33    protected override async Task OnInitializedAsync()
 34        => await LoadMappingsAsync();
 35
 36    internal async Task LoadMappingsAsync()
 37    {
 38        _isLoading = true;
 39        _loadError = null;
 40        StateHasChanged();
 41
 42        try
 43        {
 44            _mappings = await AdminApi.GetTutorialMappingsAsync();
 45        }
 46        catch (Exception ex)
 47        {
 48            _loadError = "Failed to load tutorial mappings.";
 49            Logger.LogError(ex, "Error loading tutorial mappings");
 50        }
 51        finally
 52        {
 53            _isLoading = false;
 54        }
 55    }
 56
 57    private void OnPageTypeChanged(string? pageType)
 58    {
 359        _selectedPageType = pageType;
 60
 361        var selectedOption = TutorialPageTypes.Find(pageType);
 362        if (selectedOption == null)
 63        {
 164            return;
 65        }
 66
 267        _pageTypeName = selectedOption.DefaultName;
 268        _tutorialTitle = selectedOption.PageType == "Page:Default"
 269            ? "Default Tutorial"
 270            : $"{selectedOption.DefaultName} Tutorial";
 271    }
 72
 73    private async Task CreateTutorialAsync()
 74    {
 75        var pageType = _selectedPageType?.Trim();
 76        var pageTypeName = _pageTypeName.Trim();
 77        var tutorialTitle = _tutorialTitle.Trim();
 78
 79        if (string.IsNullOrWhiteSpace(pageType))
 80        {
 81            Snackbar.Add("Select a page type.", Severity.Warning);
 82            return;
 83        }
 84
 85        if (string.IsNullOrWhiteSpace(pageTypeName))
 86        {
 87            Snackbar.Add("Page type name is required.", Severity.Warning);
 88            return;
 89        }
 90
 91        if (string.IsNullOrWhiteSpace(tutorialTitle))
 92        {
 93            Snackbar.Add("Tutorial article title is required.", Severity.Warning);
 94            return;
 95        }
 96
 97        _isCreating = true;
 98        try
 99        {
 100            var created = await AdminApi.CreateTutorialMappingAsync(new TutorialMappingCreateDto
 101            {
 102                PageType = pageType,
 103                PageTypeName = pageTypeName,
 104                Title = tutorialTitle,
 105                Body = "<p>Write tutorial content here.</p>"
 106            });
 107
 108            if (created == null)
 109            {
 110                Snackbar.Add("Failed to create tutorial mapping. Check for duplicate page type or authorization.", Sever
 111                return;
 112            }
 113
 114            Snackbar.Add($"Created tutorial mapping for {created.PageType}.", Severity.Success);
 115            ResetCreateForm();
 116            await LoadMappingsAsync();
 117            await OpenTutorialEditorByArticleIdAsync(created.ArticleId);
 118        }
 119        catch (Exception ex)
 120        {
 121            Snackbar.Add("Unexpected error creating tutorial mapping.", Severity.Error);
 122            Logger.LogError(ex, "Error creating tutorial mapping for {PageType}", pageType);
 123        }
 124        finally
 125        {
 126            _isCreating = false;
 127        }
 128    }
 129
 130    private async Task OpenTutorialEditorAsync(string pageType, Guid articleId)
 131    {
 132        Logger.LogInformation(
 133            "Opening tutorial editor from admin row {PageType} -> {ArticleId}",
 134            pageType,
 135            articleId);
 136
 137        await OpenTutorialEditorByArticleIdAsync(articleId);
 138    }
 139
 140    private async Task OpenTutorialEditorByArticleIdAsync(Guid articleId)
 141    {
 142        try
 143        {
 144            var article = await ArticleApi.GetArticleDetailAsync(articleId);
 145            if (article == null)
 146            {
 147                Snackbar.Add("Tutorial article could not be loaded.", Severity.Error);
 148                return;
 149            }
 150
 151            if (article.Breadcrumbs == null || article.Breadcrumbs.Count == 0)
 152            {
 153                Snackbar.Add("Tutorial article path could not be resolved.", Severity.Error);
 154                return;
 155            }
 156
 157            if (article.Type == ArticleType.Tutorial &&
 158                article.WorldId == Guid.Empty &&
 159                !string.IsNullOrWhiteSpace(article.Slug))
 160            {
 161                await AppNavigator.GoToTutorialAsync(article.Slug);
 162                return;
 163            }
 164
 165            Logger.LogWarningSanitized(
 166                "Article {ArticleId} is not a tutorial; admin tutorial editor will not navigate.",
 167                articleId);
 168            Snackbar.Add("Article is not a tutorial.", Severity.Warning);
 169        }
 170        catch (Exception ex)
 171        {
 172            Snackbar.Add("Failed to open tutorial article editor.", Severity.Error);
 173            Logger.LogError(ex, "Error opening tutorial article editor for {ArticleId}", articleId);
 174        }
 175    }
 176
 177    private void ResetCreateForm()
 178    {
 1179        _selectedPageType = null;
 1180        _pageTypeName = string.Empty;
 1181        _tutorialTitle = string.Empty;
 1182    }
 183}