< Summary

Line coverage
100%
Covered lines: 54
Uncovered lines: 0
Coverable lines: 54
Total lines: 344
Line coverage: 100%
Branch coverage
100%
Covered branches: 12
Total branches: 12
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%22100%
File 2: .ctor()100%11100%
File 2: .cctor()100%11100%
File 2: get_SuggestedFilePath()100%22100%
File 2: AutoGenerate()100%44100%
File 2: OnDefinitionJsonChanged(...)100%11100%
File 2: ApplyDefinition()100%44100%

File(s)

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

#LineLine coverage
 1@using Chronicis.Shared.DTOs
 2@using Chronicis.Client.Models
 3@using Chronicis.Client.Components.Shared
 4
 5<div class="rdg-container">
 6    <MudText Typo="Typo.h5" Class="rdg-section-title mb-4">Render Definition Generator</MudText>
 7    <MudText Typo="Typo.body2" Class="mud-text-secondary mb-4">
 8        Load a sample external resource record, auto-generate a render definition, edit it, and preview the result.
 9    </MudText>
 10
 11    @* --- Step 1: Record picker --- *@
 12    <MudPaper Elevation="1" Class="pa-4 mb-4">
 13        <MudText Typo="Typo.subtitle1" Class="mb-3">1. Select a sample record</MudText>
 14        <div class="d-flex align-center gap-3 flex-wrap">
 15            <MudSelect T="string" @bind-Value="_selectedSource" Label="Source"
 16                       Variant="Variant.Outlined" Margin="Margin.Dense" Style="max-width: 160px;">
 17                <MudSelectItem Value="@("ros")">ROS</MudSelectItem>
 18                <MudSelectItem Value="@("srd")">SRD</MudSelectItem>
 19            </MudSelect>
 20
 21            <MudAutocomplete T="string"
 22                             Value="_recordId"
 23                             Label="Record ID (e.g., bestiary/beast/garoug)"
 24                             SearchFunc="SearchRecords"
 25                             Variant="Variant.Outlined"
 26                             Margin="Margin.Dense"
 27                             Style="min-width: 400px;"
 28                             DebounceInterval="300"
 29                             MinCharacters="2"
 30                             MaxItems="15"
 31                             ResetValueOnEmptyText="false"
 32                             CoerceText="true"
 33                             ValueChanged="OnRecordSelected" />
 34
 35            <MudButton Variant="Variant.Filled" Color="Color.Primary"
 36                       OnClick="LoadSampleRecord" Disabled="_isLoading"
 37                       StartIcon="@Icons.Material.Filled.Download">
 38                Load
 39            </MudButton>
 40        </div>
 41
 42        @if (!string.IsNullOrEmpty(_loadError))
 43        {
 44            <MudAlert Severity="Severity.Error" Class="mt-3" Dense="true">@_loadError</MudAlert>
 45        }
 46        @if (!string.IsNullOrEmpty(_existingDefStatus))
 47        {
 48            <MudAlert Severity="Severity.Info" Class="mt-3" Dense="true">@_existingDefStatus</MudAlert>
 49        }
 50    </MudPaper>
 51
 1652    @if (_sampleContent != null)
 53    {
 54        @* --- Step 2: Generate / Edit --- *@
 55        <MudPaper Elevation="1" Class="pa-4 mb-4">
 56            <div class="d-flex align-center justify-space-between mb-3">
 57                <MudText Typo="Typo.subtitle1">2. Render Definition JSON</MudText>
 58                <div class="d-flex gap-2">
 59                    <MudButton Variant="Variant.Outlined" Color="Color.Primary"
 60                               OnClick="AutoGenerate" Size="Size.Small"
 61                               StartIcon="@Icons.Material.Filled.AutoAwesome">
 62                        Auto-Generate
 63                    </MudButton>
 64                    <MudButton Variant="Variant.Outlined" Color="Color.Primary"
 65                               OnClick="ApplyDefinition" Size="Size.Small"
 66                               Disabled="@(!_definitionDirty)"
 67                               StartIcon="@Icons.Material.Filled.Refresh">
 68                        Apply Preview
 69                    </MudButton>
 70                </div>
 71            </div>
 72
 73            <MudTextField @bind-Value="_definitionJson"
 74                          Lines="25"
 75                          Variant="Variant.Outlined"
 76                          Style="font-family: 'Cascadia Code', 'Fira Code', monospace; font-size: 0.8rem;"
 77                          Immediate="false"
 78                          DebounceInterval="500"
 79                          TextChanged="OnDefinitionJsonChanged"
 80                          Error="@(!string.IsNullOrEmpty(_jsonError))"
 81                          ErrorText="@_jsonError" />
 82
 83            @if (!string.IsNullOrEmpty(_jsonError))
 84            {
 85                <MudAlert Severity="Severity.Warning" Class="mt-2" Dense="true">@_jsonError</MudAlert>
 86            }
 87        </MudPaper>
 88
 89        @* --- Step 3: Preview --- *@
 90        <MudPaper Elevation="1" Class="pa-4 mb-4">
 91            <div class="d-flex align-center justify-space-between mb-3">
 92                <MudText Typo="Typo.subtitle1">3. Preview</MudText>
 93                <MudText Typo="Typo.caption" Class="mud-text-secondary">
 94                    @(_sampleContent.Title) — @(_sampleContent.Id)
 95                </MudText>
 96            </div>
 97
 98            <div class="rdg-preview-panel chronicis-scrollbar-light">
 99                <ExternalLinkDetailPanel Content="@_sampleContent"
 100                                         DefinitionOverride="@_activeDefinition" />
 101            </div>
 102        </MudPaper>
 103
 104        @* --- Step 4: Output path info --- *@
 105        <MudPaper Elevation="1" Class="pa-4">
 106            <MudText Typo="Typo.subtitle1" Class="mb-2">4. Export</MudText>
 107            <MudText Typo="Typo.body2" Class="mud-text-secondary mb-3">
 108                Copy the JSON above and save to the appropriate render definition file.
 109            </MudText>
 110            <MudTextField Value="@SuggestedFilePath" ReadOnly="true" Label="Suggested file path"
 111                          Variant="Variant.Outlined" Margin="Margin.Dense"
 112                          Adornment="Adornment.End" AdornmentIcon="@Icons.Material.Filled.ContentCopy" />
 113        </MudPaper>
 114    }
 115</div>

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

#LineLine coverage
 1using System.Text.Json;
 2using System.Text.Json.Serialization;
 3using Chronicis.Client.Models;
 4using Chronicis.Client.Services;
 5using Chronicis.Shared.DTOs;
 6using Microsoft.AspNetCore.Components;
 7
 8namespace Chronicis.Client.Components.Admin;
 9
 10public partial class RenderDefinitionGenerator : ComponentBase
 11{
 12    [Inject] private IExternalLinkApiService ExternalLinkApi { get; set; } = default!;
 13    [Inject] private IRenderDefinitionService RenderDefService { get; set; } = default!;
 14    [Inject] private ILogger<RenderDefinitionGenerator> Logger { get; set; } = default!;
 15
 3016    private string _selectedSource = "ros";
 3017    private string _recordId = "";
 18    private bool _isLoading;
 19    private string? _loadError;
 20    private string? _existingDefStatus;
 21
 22    private ExternalLinkContentDto? _sampleContent;
 3023    private string _definitionJson = "";
 24    private string? _jsonError;
 25    private bool _definitionDirty;
 26    private RenderDefinition? _activeDefinition;
 27
 28    // Autocomplete support
 129    private static readonly JsonSerializerOptions WriteOptions = new()
 130    {
 131        WriteIndented = true,
 132        DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
 133    };
 34
 35    private string SuggestedFilePath
 36    {
 37        get
 38        {
 439            if (string.IsNullOrEmpty(_recordId))
 340                return "";
 141            var parts = _recordId.Split('/');
 142            var category = parts[0];
 143            return $"wwwroot/render-definitions/{_selectedSource}/{category}.json";
 44        }
 45    }
 46
 47    private async Task<IEnumerable<string>> SearchRecords(string query, CancellationToken ct)
 48    {
 49        if (string.IsNullOrWhiteSpace(query) || query.Length < 2)
 50            return Enumerable.Empty<string>();
 51
 52        try
 53        {
 54            var results = await ExternalLinkApi.GetSuggestionsAsync(
 55                null, _selectedSource, query, ct);
 56            return results.Select(r => r.Id);
 57        }
 58        catch (OperationCanceledException)
 59        {
 60            return Enumerable.Empty<string>();
 61        }
 62        catch (Exception ex)
 63        {
 64            Logger.LogWarning(ex, "Autocomplete search failed for {Query}", query);
 65            return Enumerable.Empty<string>();
 66        }
 67    }
 68
 69    private async Task OnRecordSelected(string? value)
 70    {
 71        _recordId = value ?? "";
 72        if (!string.IsNullOrWhiteSpace(_recordId))
 73            await LoadSampleRecord();
 74    }
 75
 76    private async Task LoadSampleRecord()
 77    {
 78        if (string.IsNullOrWhiteSpace(_recordId))
 79        {
 80            _loadError = "Please enter a record ID.";
 81            return;
 82        }
 83
 84        _isLoading = true;
 85        _loadError = null;
 86        _existingDefStatus = null;
 87        _sampleContent = null;
 88        _activeDefinition = null;
 89        _definitionJson = "";
 90        StateHasChanged();
 91
 92        try
 93        {
 94            var content = await ExternalLinkApi.GetContentAsync(
 95                _selectedSource, _recordId.Trim(), CancellationToken.None);
 96
 97            if (content == null)
 98            {
 99                _loadError = $"Record not found: {_selectedSource}/{_recordId}";
 100            }
 101            else if (string.IsNullOrWhiteSpace(content.JsonData))
 102            {
 103                _loadError = "Record loaded but has no JsonData — structured rendering unavailable.";
 104                _sampleContent = content;
 105            }
 106            else
 107            {
 108                _sampleContent = content;
 109                Logger.LogInformation("Loaded sample record: {Id} with {Len} bytes of JSON",
 110                    content.Id, content.JsonData.Length);
 111
 112                // Try to load an existing render definition for this record's category
 113                await TryLoadExistingDefinition();
 114            }
 115        }
 116        catch (Exception ex)
 117        {
 118            _loadError = $"Failed to load record: {ex.Message}";
 119            Logger.LogError(ex, "Error loading sample record {Source}/{Id}", _selectedSource, _recordId);
 120        }
 121        finally
 122        {
 123            _isLoading = false;
 124        }
 125    }
 126
 127    private async Task TryLoadExistingDefinition()
 128    {
 129        if (_sampleContent == null)
 130            return;
 131
 132        try
 133        {
 134            var lastSlash = _sampleContent.Id.LastIndexOf('/');
 135            var categoryPath = lastSlash > 0 ? _sampleContent.Id[..lastSlash] : null;
 136
 137            var existing = await RenderDefService.ResolveAsync(_sampleContent.Source, categoryPath);
 138
 139            // Check if it's the built-in default or an actual file definition
 140            // The built-in default has no DisplayName and empty Sections
 141            var isDefault = existing.Sections.Count == 0 && string.IsNullOrEmpty(existing.DisplayName);
 142
 143            if (!isDefault)
 144            {
 145                _definitionJson = JsonSerializer.Serialize(existing, WriteOptions);
 146                _activeDefinition = existing;
 147                _definitionDirty = false;
 148                _jsonError = null;
 149                _existingDefStatus = $"Loaded existing definition ({existing.Sections.Count} sections)";
 150                Logger.LogInformation("Loaded existing render definition for {Source}/{Category}",
 151                    _sampleContent.Source, categoryPath);
 152            }
 153            else
 154            {
 155                _existingDefStatus = "No custom definition found — use Auto-Generate to create one.";
 156            }
 157        }
 158        catch (Exception ex)
 159        {
 160            Logger.LogWarning(ex, "Failed to load existing render definition");
 161            _existingDefStatus = "Could not check for existing definition.";
 162        }
 163    }
 164
 165    private void AutoGenerate()
 166    {
 3167        if (_sampleContent?.JsonData == null)
 1168            return;
 169
 170        try
 171        {
 2172            using var doc = JsonDocument.Parse(_sampleContent.JsonData);
 1173            var definition = RenderDefinitionGeneratorService.Generate(doc.RootElement);
 1174            _definitionJson = JsonSerializer.Serialize(definition, WriteOptions);
 1175            _jsonError = null;
 1176            _definitionDirty = false;
 177
 1178            _activeDefinition = definition;
 1179            Logger.LogInformation("Auto-generated definition with {Sections} sections, {Hidden} hidden fields",
 1180                definition.Sections.Count, definition.Hidden.Count);
 1181        }
 1182        catch (Exception ex)
 183        {
 1184            _jsonError = $"Generation failed: {ex.Message}";
 1185            Logger.LogError(ex, "Auto-generation failed");
 1186        }
 2187    }
 188
 189    private void OnDefinitionJsonChanged(string newJson)
 190    {
 3191        _definitionJson = newJson;
 3192        _definitionDirty = true;
 3193        _jsonError = null;
 194
 195        try
 196        {
 3197            JsonSerializer.Deserialize<RenderDefinition>(newJson, WriteOptions);
 1198        }
 2199        catch (JsonException ex)
 200        {
 2201            _jsonError = $"Invalid JSON: {ex.Message}";
 2202        }
 3203    }
 204
 205    private void ApplyDefinition()
 206    {
 4207        if (string.IsNullOrWhiteSpace(_definitionJson))
 1208            return;
 209
 210        try
 211        {
 3212            var definition = JsonSerializer.Deserialize<RenderDefinition>(_definitionJson, WriteOptions);
 2213            if (definition == null)
 214            {
 1215                _jsonError = "Deserialized to null.";
 1216                return;
 217            }
 218
 1219            _activeDefinition = definition;
 1220            _definitionDirty = false;
 1221            _jsonError = null;
 1222            Logger.LogInformation("Applied edited definition: {Sections} sections", definition.Sections.Count);
 1223        }
 1224        catch (JsonException ex)
 225        {
 1226            _jsonError = $"Cannot apply — invalid JSON: {ex.Message}";
 1227        }
 3228    }
 229}