< Summary

Line coverage
0%
Covered lines: 0
Uncovered lines: 128
Coverable lines: 128
Total lines: 344
Line coverage: 0%
Branch coverage
0%
Covered branches: 0
Total branches: 42
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/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
 042        @if (!string.IsNullOrEmpty(_loadError))
 43        {
 044            <MudAlert Severity="Severity.Error" Class="mt-3" Dense="true">@_loadError</MudAlert>
 45        }
 046        @if (!string.IsNullOrEmpty(_existingDefStatus))
 47        {
 048            <MudAlert Severity="Severity.Info" Class="mt-3" Dense="true">@_existingDefStatus</MudAlert>
 49        }
 50    </MudPaper>
 51
 052    @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
 083            @if (!string.IsNullOrEmpty(_jsonError))
 84            {
 085                <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">
 094                    @(_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{
 012    [Inject] private IExternalLinkApiService ExternalLinkApi { get; set; } = default!;
 013    [Inject] private IRenderDefinitionService RenderDefService { get; set; } = default!;
 014    [Inject] private ILogger<RenderDefinitionGenerator> Logger { get; set; } = default!;
 15
 016    private string _selectedSource = "ros";
 017    private string _recordId = "";
 18    private bool _isLoading;
 19    private string? _loadError;
 20    private string? _existingDefStatus;
 21
 22    private ExternalLinkContentDto? _sampleContent;
 023    private string _definitionJson = "";
 24    private string? _jsonError;
 25    private bool _definitionDirty;
 26    private RenderDefinition? _activeDefinition;
 27
 28    // Autocomplete support
 029    private static readonly JsonSerializerOptions WriteOptions = new()
 030    {
 031        WriteIndented = true,
 032        DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
 033    };
 34
 35    private string SuggestedFilePath
 36    {
 37        get
 38        {
 039            if (string.IsNullOrEmpty(_recordId))
 040                return "";
 041            var parts = _recordId.Split('/');
 042            var category = parts.Length > 0 ? parts[0] : "unknown";
 043            return $"wwwroot/render-definitions/{_selectedSource}/{category}.json";
 44        }
 45    }
 46
 47    private async Task<IEnumerable<string>> SearchRecords(string query, CancellationToken ct)
 48    {
 049        if (string.IsNullOrWhiteSpace(query) || query.Length < 2)
 050            return Enumerable.Empty<string>();
 51
 52        try
 53        {
 054            var results = await ExternalLinkApi.GetSuggestionsAsync(
 055                null, _selectedSource, query, ct);
 056            return results.Select(r => r.Id);
 57        }
 058        catch (OperationCanceledException)
 59        {
 060            return Enumerable.Empty<string>();
 61        }
 062        catch (Exception ex)
 63        {
 064            Logger.LogWarning(ex, "Autocomplete search failed for {Query}", query);
 065            return Enumerable.Empty<string>();
 66        }
 067    }
 68
 69    private async Task OnRecordSelected(string? value)
 70    {
 071        _recordId = value ?? "";
 072        if (!string.IsNullOrWhiteSpace(_recordId))
 073            await LoadSampleRecord();
 074    }
 75
 76    private async Task LoadSampleRecord()
 77    {
 078        if (string.IsNullOrWhiteSpace(_recordId))
 79        {
 080            _loadError = "Please enter a record ID.";
 081            return;
 82        }
 83
 084        _isLoading = true;
 085        _loadError = null;
 086        _existingDefStatus = null;
 087        _sampleContent = null;
 088        _activeDefinition = null;
 089        _definitionJson = "";
 090        StateHasChanged();
 91
 92        try
 93        {
 094            var content = await ExternalLinkApi.GetContentAsync(
 095                _selectedSource, _recordId.Trim(), CancellationToken.None);
 96
 097            if (content == null)
 98            {
 099                _loadError = $"Record not found: {_selectedSource}/{_recordId}";
 100            }
 0101            else if (string.IsNullOrWhiteSpace(content.JsonData))
 102            {
 0103                _loadError = "Record loaded but has no JsonData — structured rendering unavailable.";
 0104                _sampleContent = content;
 105            }
 106            else
 107            {
 0108                _sampleContent = content;
 0109                Logger.LogInformation("Loaded sample record: {Id} with {Len} bytes of JSON",
 0110                    content.Id, content.JsonData.Length);
 111
 112                // Try to load an existing render definition for this record's category
 0113                await TryLoadExistingDefinition();
 114            }
 0115        }
 0116        catch (Exception ex)
 117        {
 0118            _loadError = $"Failed to load record: {ex.Message}";
 0119            Logger.LogError(ex, "Error loading sample record {Source}/{Id}", _selectedSource, _recordId);
 0120        }
 121        finally
 122        {
 0123            _isLoading = false;
 124        }
 0125    }
 126
 127    private async Task TryLoadExistingDefinition()
 128    {
 0129        if (_sampleContent == null)
 0130            return;
 131
 132        try
 133        {
 0134            var lastSlash = _sampleContent.Id.LastIndexOf('/');
 0135            var categoryPath = lastSlash > 0 ? _sampleContent.Id[..lastSlash] : null;
 136
 0137            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
 0141            var isDefault = existing.Sections.Count == 0 && string.IsNullOrEmpty(existing.DisplayName);
 142
 0143            if (!isDefault)
 144            {
 0145                _definitionJson = JsonSerializer.Serialize(existing, WriteOptions);
 0146                _activeDefinition = existing;
 0147                _definitionDirty = false;
 0148                _jsonError = null;
 0149                _existingDefStatus = $"Loaded existing definition ({existing.Sections.Count} sections)";
 0150                Logger.LogInformation("Loaded existing render definition for {Source}/{Category}",
 0151                    _sampleContent.Source, categoryPath);
 152            }
 153            else
 154            {
 0155                _existingDefStatus = "No custom definition found — use Auto-Generate to create one.";
 156            }
 0157        }
 0158        catch (Exception ex)
 159        {
 0160            Logger.LogWarning(ex, "Failed to load existing render definition");
 0161            _existingDefStatus = "Could not check for existing definition.";
 0162        }
 0163    }
 164
 165    private void AutoGenerate()
 166    {
 0167        if (_sampleContent?.JsonData == null)
 0168            return;
 169
 170        try
 171        {
 0172            using var doc = JsonDocument.Parse(_sampleContent.JsonData);
 0173            var definition = RenderDefinitionGeneratorService.Generate(doc.RootElement);
 0174            _definitionJson = JsonSerializer.Serialize(definition, WriteOptions);
 0175            _jsonError = null;
 0176            _definitionDirty = false;
 177
 0178            _activeDefinition = definition;
 0179            Logger.LogInformation("Auto-generated definition with {Sections} sections, {Hidden} hidden fields",
 0180                definition.Sections.Count, definition.Hidden.Count);
 0181        }
 0182        catch (Exception ex)
 183        {
 0184            _jsonError = $"Generation failed: {ex.Message}";
 0185            Logger.LogError(ex, "Auto-generation failed");
 0186        }
 0187    }
 188
 189    private void OnDefinitionJsonChanged(string newJson)
 190    {
 0191        _definitionJson = newJson;
 0192        _definitionDirty = true;
 0193        _jsonError = null;
 194
 195        try
 196        {
 0197            JsonSerializer.Deserialize<RenderDefinition>(newJson, WriteOptions);
 0198        }
 0199        catch (JsonException ex)
 200        {
 0201            _jsonError = $"Invalid JSON: {ex.Message}";
 0202        }
 0203    }
 204
 205    private void ApplyDefinition()
 206    {
 0207        if (string.IsNullOrWhiteSpace(_definitionJson))
 0208            return;
 209
 210        try
 211        {
 0212            var definition = JsonSerializer.Deserialize<RenderDefinition>(_definitionJson, WriteOptions);
 0213            if (definition == null)
 214            {
 0215                _jsonError = "Deserialized to null.";
 0216                return;
 217            }
 218
 0219            _activeDefinition = definition;
 0220            _definitionDirty = false;
 0221            _jsonError = null;
 0222            Logger.LogInformation("Applied edited definition: {Sections} sections", definition.Sections.Count);
 0223        }
 0224        catch (JsonException ex)
 225        {
 0226            _jsonError = $"Cannot apply — invalid JSON: {ex.Message}";
 0227        }
 0228    }
 229}