< Summary

Line coverage
0%
Covered lines: 0
Uncovered lines: 127
Coverable lines: 127
Total lines: 322
Line coverage: 0%
Branch coverage
0%
Covered branches: 0
Total branches: 76
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/Quests/ArcQuestEditor.razor

#LineLine coverage
 1@using Chronicis.Shared.DTOs.Quests
 2@using Chronicis.Shared.Enums
 3@using Microsoft.JSInterop
 4
 5<div class="arc-quest-editor">
 06    @if (_quest != null)
 7    {
 8        <!-- Quest Title -->
 9        <MudTextField @bind-Value="_editTitle"
 10                      Label="Quest Title"
 11                      Variant="Variant.Outlined"
 12                      Class="mb-3"
 13                      Immediate="true"
 14                      OnBlur="SaveTitleAsync"
 15                      @onkeydown="OnTitleKeyDown" />
 16
 17        <!-- Status and Toggles -->
 18        <div class="d-flex gap-3 mb-3">
 19            <MudSelect T="QuestStatus"
 20                       Value="_editStatus"
 21                       Label="Status"
 22                       Variant="Variant.Outlined"
 23                       ValueChanged="OnStatusChanged">
 24                <MudSelectItem T="QuestStatus" Value="QuestStatus.Active">Active</MudSelectItem>
 25                <MudSelectItem T="QuestStatus" Value="QuestStatus.Completed">Completed</MudSelectItem>
 26                <MudSelectItem T="QuestStatus" Value="QuestStatus.Failed">Failed</MudSelectItem>
 27                <MudSelectItem T="QuestStatus" Value="QuestStatus.Abandoned">Abandoned</MudSelectItem>
 28            </MudSelect>
 29
 30            <MudSwitch T="bool"
 31                       Value="_editIsGmOnly"
 32                       Label="GM Only"
 33                       Color="Color.Warning"
 34                       ValueChanged="OnGmOnlyChanged" />
 35
 36            <MudNumericField T="int"
 37                             Value="_editSortOrder"
 38                             Label="Sort Order"
 39                             Variant="Variant.Outlined"
 40                             Min="0"
 41                             ValueChanged="OnSortOrderChanged" />
 42        </div>
 43
 44        <!-- Description (TipTap Editor) -->
 45        <MudText Typo="Typo.subtitle2" Class="mb-2">Description</MudText>
 46        <div id="@EditorId" class="chronicis-editor-content quest-description-editor"></div>
 47
 48        <!-- Save Status -->
 49        <div class="mt-3">
 50            <SaveStatusIndicator IsSaving="_isSaving" HasUnsavedChanges="_hasUnsavedChanges" />
 51        </div>
 52    }
 53    else
 54    {
 55        <MudAlert Severity="Severity.Info">
 56            Select a quest to edit
 57        </MudAlert>
 58    }
 59</div>

/home/runner/work/chronicis/chronicis/src/Chronicis.Client/Components/Quests/ArcQuestEditor.razor.cs

#LineLine coverage
 1using Chronicis.Client.Services;
 2using Chronicis.Shared.DTOs.Quests;
 3using Chronicis.Shared.Enums;
 4using Microsoft.AspNetCore.Components;
 5using Microsoft.AspNetCore.Components.Web;
 6using Microsoft.JSInterop;
 7using MudBlazor;
 8
 9namespace Chronicis.Client.Components.Quests;
 10
 11public partial class ArcQuestEditor : ComponentBase, IAsyncDisposable
 12{
 13    [Parameter]
 014    public QuestDto? Quest { get; set; }
 15
 16    [Parameter]
 017    public EventCallback<QuestDto> OnQuestUpdated { get; set; }
 18
 019    [Inject] private IQuestApiService QuestApi { get; set; } = default!;
 020    [Inject] private IJSRuntime JSRuntime { get; set; } = default!;
 021    [Inject] private ISnackbar Snackbar { get; set; } = default!;
 22
 23    private QuestDto? _quest;
 024    private string _editTitle = string.Empty;
 025    private string _editDescription = string.Empty;
 26    private QuestStatus _editStatus;
 27    private bool _editIsGmOnly;
 28    private int _editSortOrder;
 29
 30    private bool _isSaving;
 31    private bool _hasUnsavedChanges;
 32    private bool _editorInitialized;
 33    private bool _disposed;
 34    private DotNetObjectReference<ArcQuestEditor>? _dotNetHelper;
 35    private Timer? _autoSaveTimer;
 36
 037    private string EditorId => $"quest-desc-editor-{_quest?.Id ?? Guid.Empty}";
 38
 39    protected override async Task OnParametersSetAsync()
 40    {
 41        // Quest changed - dispose old editor and initialize new one
 042        if (_quest?.Id != Quest?.Id)
 43        {
 044            await DisposeEditorAsync();
 45
 046            _quest = Quest;
 47
 048            if (_quest != null)
 49            {
 050                _editTitle = _quest.Title;
 051                _editDescription = _quest.Description ?? string.Empty;
 052                _editStatus = _quest.Status;
 053                _editIsGmOnly = _quest.IsGmOnly;
 054                _editSortOrder = _quest.SortOrder;
 055                _hasUnsavedChanges = false;
 56            }
 57        }
 058    }
 59
 60    protected override async Task OnAfterRenderAsync(bool firstRender)
 61    {
 062        if (firstRender)
 63        {
 064            _dotNetHelper = DotNetObjectReference.Create(this);
 65        }
 66
 067        if (_quest != null && !_editorInitialized && !_disposed && _dotNetHelper != null)
 68        {
 069            await Task.Delay(100); // Small delay to ensure DOM is ready
 070            await InitializeEditorAsync();
 71        }
 072    }
 73
 74    private async Task InitializeEditorAsync()
 75    {
 076        if (_editorInitialized || _disposed || _dotNetHelper == null)
 077            return;
 78
 79        try
 80        {
 081            await JSRuntime.InvokeVoidAsync("initializeTipTapEditor", EditorId, _editDescription, _dotNetHelper);
 82
 083            if (_disposed)
 084                return;
 85
 086            await JSRuntime.InvokeVoidAsync("initializeWikiLinkAutocomplete", EditorId, _dotNetHelper);
 087            _editorInitialized = true;
 088        }
 089        catch (ObjectDisposedException)
 90        {
 91            // Expected during navigation
 092        }
 093        catch (JSDisconnectedException)
 94        {
 95            // Expected during navigation
 096        }
 097        catch (Exception ex)
 98        {
 099            if (!_disposed)
 100            {
 0101                Snackbar.Add($"Failed to initialize quest editor: {ex.Message}", Severity.Warning);
 102            }
 0103        }
 0104    }
 105
 106    private async Task DisposeEditorAsync()
 107    {
 0108        if (_editorInitialized && !_disposed)
 109        {
 110            try
 111            {
 0112                await JSRuntime.InvokeVoidAsync("destroyTipTapEditor", EditorId);
 0113            }
 0114            catch
 115            {
 116                // Ignore errors during disposal
 0117            }
 118            finally
 119            {
 0120                _editorInitialized = false;
 121            }
 122        }
 0123    }
 124
 125    [JSInvokable]
 126    public void OnEditorUpdate(string html)
 127    {
 0128        _editDescription = html;
 0129        _hasUnsavedChanges = true;
 130
 131        // Debounce auto-save (0.5s delay)
 0132        _autoSaveTimer?.Dispose();
 0133        _autoSaveTimer = new Timer(async _ => await AutoSaveAsync(), null, 500, Timeout.Infinite);
 0134    }
 135
 136    private async Task AutoSaveAsync()
 137    {
 0138        await InvokeAsync(async () =>
 0139        {
 0140            if (_hasUnsavedChanges && !_isSaving)
 0141            {
 0142                await SaveQuestAsync();
 0143            }
 0144        });
 0145    }
 146
 147    private async Task OnTitleKeyDown(KeyboardEventArgs e)
 148    {
 0149        if (e.Key == "Enter")
 150        {
 0151            await SaveTitleAsync();
 152        }
 0153    }
 154
 155    private async Task SaveTitleAsync()
 156    {
 0157        if (_quest != null && _editTitle != _quest.Title)
 158        {
 0159            _hasUnsavedChanges = true;
 0160            await SaveQuestAsync();
 161        }
 0162    }
 163
 164    private void OnStatusChanged(QuestStatus newStatus)
 165    {
 0166        _editStatus = newStatus;
 0167        _ = SaveQuestAsync();
 0168    }
 169
 170    private void OnGmOnlyChanged(bool newValue)
 171    {
 0172        _editIsGmOnly = newValue;
 0173        _ = SaveQuestAsync();
 0174    }
 175
 176    private void OnSortOrderChanged(int newValue)
 177    {
 0178        _editSortOrder = newValue;
 0179        _ = SaveQuestAsync();
 0180    }
 181
 182    private async Task SaveQuestAsync()
 183    {
 0184        if (_quest == null || _isSaving)
 0185            return;
 186
 0187        _isSaving = true;
 0188        StateHasChanged();
 189
 190        try
 191        {
 0192            var editDto = new QuestEditDto
 0193            {
 0194                Title = string.IsNullOrWhiteSpace(_editTitle) ? null : _editTitle.Trim(),
 0195                Description = string.IsNullOrWhiteSpace(_editDescription) ? null : _editDescription,
 0196                Status = _editStatus,
 0197                IsGmOnly = _editIsGmOnly,
 0198                SortOrder = _editSortOrder,
 0199                RowVersion = _quest.RowVersion
 0200            };
 201
 0202            var updated = await QuestApi.UpdateQuestAsync(_quest.Id, editDto);
 203
 0204            if (updated != null)
 205            {
 206                // Success - update local state with server response
 0207                _quest = updated;
 0208                _editTitle = updated.Title;
 0209                _editDescription = updated.Description ?? string.Empty;
 0210                _editStatus = updated.Status;
 0211                _editIsGmOnly = updated.IsGmOnly;
 0212                _editSortOrder = updated.SortOrder;
 0213                _hasUnsavedChanges = false;
 214
 0215                await OnQuestUpdated.InvokeAsync(updated);
 216            }
 217            else
 218            {
 219                // 409 conflict was already handled by QuestApiService
 220                // Reload from server
 0221                var current = await QuestApi.GetQuestAsync(_quest.Id);
 0222                if (current != null)
 223                {
 0224                    _quest = current;
 0225                    _editTitle = current.Title;
 0226                    _editDescription = current.Description ?? string.Empty;
 0227                    _editStatus = current.Status;
 0228                    _editIsGmOnly = current.IsGmOnly;
 0229                    _editSortOrder = current.SortOrder;
 230
 231                    // Reinitialize editor with server content
 0232                    await DisposeEditorAsync();
 0233                    await InitializeEditorAsync();
 234                }
 235            }
 0236        }
 0237        catch (Exception ex)
 238        {
 0239            Snackbar.Add($"Failed to save quest: {ex.Message}", Severity.Error);
 0240        }
 241        finally
 242        {
 0243            _isSaving = false;
 0244            StateHasChanged();
 245        }
 0246    }
 247
 248    public async ValueTask DisposeAsync()
 249    {
 0250        if (_disposed)
 0251            return;
 252
 0253        _disposed = true;
 0254        _autoSaveTimer?.Dispose();
 255
 256        // Properly dispose TipTap editor
 0257        await DisposeEditorAsync();
 258
 0259        _dotNetHelper?.Dispose();
 260
 0261        GC.SuppressFinalize(this);
 0262    }
 263}