// // Licensed under the MIT License. See LICENSE file in the project root for full license information. // namespace MUnique.OpenMU.Web.AdminPanel.Pages; using System.ComponentModel; using System.Threading; using Microsoft.AspNetCore.Components; using Microsoft.AspNetCore.Components.QuickGrid; using Microsoft.AspNetCore.Components.Routing; using Microsoft.Extensions.Logging; using Microsoft.JSInterop; using MUnique.OpenMU.DataModel.Configuration; using MUnique.OpenMU.DataModel.Entities; using MUnique.OpenMU.Persistence; using MUnique.OpenMU.Web.AdminPanel.Properties; using MUnique.OpenMU.Web.Shared.Components.Toast; using MUnique.OpenMU.Web.Shared.Services; /// /// Razor page which shows objects of the specified type in a grid. /// public partial class Merchants : ComponentBase, IAsyncDisposable { private readonly PaginationState _merchantPagination = new() { ItemsPerPage = 20 }; private readonly PaginationState _itemPagination = new() { ItemsPerPage = 20 }; private Task? _loadTask; private CancellationTokenSource? _disposeCts; private List? _viewModels; private MerchantStorageViewModel? _selectedMerchant; private IContext? _persistenceContext; private IDisposable? _navigationLockDisposable; /// /// Gets or sets the data source. /// [Inject] public IDataSource DataSource { get; set; } = null!; /// /// Gets or sets the context provider. /// [Inject] public IPersistenceContextProvider ContextProvider { get; set; } = null!; /// /// Gets or sets the toast service. /// [Inject] public IToastService ToastService { get; set; } = null!; /// /// Gets or sets the navigation manager. /// [Inject] public NavigationManager NavigationManager { get; set; } = null!; /// /// Gets or sets the java script runtime. /// [Inject] public IJSRuntime JavaScript { get; set; } = null!; /// /// Gets or sets the logger. /// [Inject] public ILogger Logger { get; set; } = null!; /// /// Gets or sets the loading overlay service. /// [Inject] public LoadingOverlayService LoadingService { get; set; } = null!; private IQueryable? ViewModels => this._viewModels?.AsQueryable(); /// public async ValueTask DisposeAsync() { this._navigationLockDisposable?.Dispose(); this._navigationLockDisposable = null; await (this._disposeCts?.CancelAsync() ?? Task.CompletedTask).ConfigureAwait(false); this._disposeCts?.Dispose(); this._disposeCts = null; try { await (this._loadTask ?? Task.CompletedTask).ConfigureAwait(false); } catch (OperationCanceledException) { // we can ignore that ... } catch { // and we should not throw exceptions in the dispose method ... } } /// protected override Task OnInitializedAsync() { this._navigationLockDisposable = this.NavigationManager.RegisterLocationChangingHandler(this.OnBeforeInternalNavigationAsync); return base.OnInitializedAsync(); } /// protected override async Task OnParametersSetAsync() { var cts = new CancellationTokenSource(); this._disposeCts = cts; this._loadTask = Task.Run(() => this.LoadDataAsync(cts.Token)); await base.OnParametersSetAsync().ConfigureAwait(true); } private async ValueTask OnBeforeInternalNavigationAsync(LocationChangingContext context) { if (this._persistenceContext?.HasChanges is not true) { return; } var isConfirmed = await this.JavaScript.InvokeAsync( "window.confirm", Resources.UnsavedChangesQuestion) .ConfigureAwait(true); if (!isConfirmed) { context.PreventNavigation(); } else { await this.DataSource.DiscardChangesAsync().ConfigureAwait(true); } } private async Task LoadDataAsync(CancellationToken cancellationToken) { using var loading = this.LoadingService.ShowLoadingIndicator(); try { cancellationToken.ThrowIfCancellationRequested(); this._persistenceContext = await this.DataSource.GetContextAsync(cancellationToken).ConfigureAwait(true); await this.DataSource.GetOwnerAsync(cancellationToken: cancellationToken).ConfigureAwait(true); var data = this.DataSource.GetAll() .Where(m => m is { ObjectKind: NpcObjectKind.PassiveNpc, MerchantStore: { } }); this._viewModels = data .Select(o => new MerchantStorageViewModel(o)) .OrderBy(o => o.Name) .ToList(); await this.InvokeAsync(this.StateHasChanged).ConfigureAwait(true); } catch (OperationCanceledException) { // Expected when navigating away - ignore } catch (Exception ex) { this.Logger.LogError(ex, "Error loading merchant data"); } } private async Task OnMerchantEditClickAsync(MerchantStorageViewModel context) { this._selectedMerchant = context; await this.InvokeAsync(async () => await this._itemPagination.SetCurrentPageIndexAsync(0).ConfigureAwait(true)).ConfigureAwait(true); } private async Task OnSaveButtonClickAsync() { try { if (this._persistenceContext is { } context) { var success = await context.SaveChangesAsync().ConfigureAwait(true); var text = success ? Resources.SavedChanges : Resources.NoChangesToSave; this.ToastService.ShowSuccess(text); } else { this.ToastService.ShowError(Resources.FailedByUninitializedContext); } } catch (Exception ex) { this.Logger.LogError(ex, $"An unexpected error occurred on save: {ex.Message}"); this.ToastService.ShowError(string.Format(Resources.UnexpectedErrorOccurred, ex.Message)); } } private async Task OnCancelButtonClickAsync() { if (this._persistenceContext?.HasChanges is true) { var previousMerchantId = this._selectedMerchant?.Id; await this.DataSource.DiscardChangesAsync().ConfigureAwait(true); await this.LoadDataAsync(this._disposeCts?.Token ?? default).ConfigureAwait(true); if (previousMerchantId is { } id && this._viewModels is not null) { this._selectedMerchant = this._viewModels.FirstOrDefault(vm => vm.Id == id); } } } private async Task OnBackButtonClickAsync() { if (this._persistenceContext?.HasChanges is true) { var isConfirmed = await this.JavaScript.InvokeAsync( "window.confirm", Resources.UnsavedChangesQuestion) .ConfigureAwait(true); if (!isConfirmed) { return; } await this.OnCancelButtonClickAsync().ConfigureAwait(true); } this._selectedMerchant = null; } /// /// The view model for a merchant store. /// public class MerchantStorageViewModel { /// /// Initializes a new instance of the class. /// /// The merchant. public MerchantStorageViewModel(MonsterDefinition merchant) { this.Merchant = merchant; this.Id = merchant.GetId(); } /// /// Gets the identifier. /// [Browsable(false)] public Guid Id { get; } /// /// Gets the merchant definition. /// [Browsable(false)] public MonsterDefinition Merchant { get; } /// /// Gets the name of the merchant. /// [Browsable(false)] public string Name => this.Merchant.Designation; /// /// Gets the items of the merchant. /// public ICollection Items => this.Merchant.MerchantStore!.Items; } }