diff --git a/JSMR.UI.Blazor/Filters/TagFilterState.cs b/JSMR.UI.Blazor/Filters/TagFilterState.cs new file mode 100644 index 0000000..4c22ec2 --- /dev/null +++ b/JSMR.UI.Blazor/Filters/TagFilterState.cs @@ -0,0 +1,109 @@ +using JSMR.Application.Common.Search; +using JSMR.Application.Tags.Queries.Search; +using JSMR.Application.Tags.Queries.Search.Contracts; +using Microsoft.AspNetCore.WebUtilities; +using System.Globalization; + +namespace JSMR.UI.Blazor.Filters; + +public sealed record TagFilterState : IFilterState +{ + public string? Keywords { get; init; } + public int PageNumber { get; init; } = 1; + public int PageSize { get; init; } = 100; + + public IReadOnlyList> SortOptions { get; init; } = + [ + new(TagSortField.Name, SortDirection.Ascending) + ]; + + public QueryParameters ToQuery() + { + QueryParameters query = []; + + query.Set("keywords", Keywords); + + if (PageNumber != 1) + query.Set("pageNumber", PageNumber.ToString(CultureInfo.InvariantCulture)); + + if (PageSize != 100) + query.Set("pageSize", PageSize.ToString(CultureInfo.InvariantCulture)); + + if (!IsDefaultSort(SortOptions)) + { + query.Set("sort", string.Join(",", SortOptions.Select(x => + $"{x.Field}:{ToQueryDirection(x.Direction)}"))); + } + + return query; + } + + public static TagFilterState FromQuery(string query) + { + QueryParameters queryParameters = new(QueryHelpers.ParseQuery(query)); + + return new TagFilterState + { + Keywords = queryParameters.GetValue("keywords"), + PageNumber = Math.Max(1, queryParameters.GetInteger("pageNumber", 1)), + PageSize = queryParameters.GetInteger("pageSize", 100), + SortOptions = ParseSortOptions(queryParameters.GetValue("sort")) + }; + } + + public SearchTagsRequest ToSearchRequest() + { + return new( + Options: new() + { + PageNumber = PageNumber, + PageSize = PageSize, + Criteria = new() + { + Name = Keywords + }, + SortOptions = [.. SortOptions] + } + ); + } + + private static IReadOnlyList> ParseSortOptions(string? value) + { + if (string.IsNullOrWhiteSpace(value)) + return DefaultSort(); + + var result = new List>(); + + foreach (var part in value.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)) + { + var pieces = part.Split(':', 2, StringSplitOptions.TrimEntries); + + if (pieces.Length != 2) + continue; + + if (!Enum.TryParse(pieces[0], ignoreCase: true, out var field)) + continue; + + var direction = pieces[1].Equals("desc", StringComparison.OrdinalIgnoreCase) + ? Application.Common.Search.SortDirection.Descending + : Application.Common.Search.SortDirection.Ascending; + + result.Add(new(field, direction)); + } + + return result.Count > 0 ? result : DefaultSort(); + } + + private static IReadOnlyList> DefaultSort() => + [ + new(TagSortField.Name, Application.Common.Search.SortDirection.Ascending) + ]; + + private static string ToQueryDirection(Application.Common.Search.SortDirection direction) + => direction == Application.Common.Search.SortDirection.Descending ? "desc" : "asc"; + + private static bool IsDefaultSort(IReadOnlyList> sortOptions) + => sortOptions.Count == 1 + && sortOptions[0].Field == TagSortField.Name + && sortOptions[0].Direction == Application.Common.Search.SortDirection.Ascending; +} \ No newline at end of file diff --git a/JSMR.UI.Blazor/Pages/Tags.razor b/JSMR.UI.Blazor/Pages/Tags.razor index 154d143..d33ce12 100644 --- a/JSMR.UI.Blazor/Pages/Tags.razor +++ b/JSMR.UI.Blazor/Pages/Tags.razor @@ -13,14 +13,18 @@ @using JSMR.UI.Blazor.Filters @using JSMR.UI.Blazor.Models @using JSMR.UI.Blazor.Services +@using JSMR.UI.Blazor.Shared @using Microsoft.AspNetCore.WebUtilities +@using System.Text.Json + +@inherits SearchPageBase Tags
- + @@ -28,10 +32,10 @@ - +
@@ -157,98 +165,30 @@ [Inject] ModalService ModalService { get; set; } = default!; - public string? Keywords { get; set; } - public int PageNumber { get; set; } = 1; - public int PageSize { get; set; } = 100; - public int TotalItems => searchResults?.TotalItems ?? 0; - - Func ShowTotal = ctx => $"{ctx.Range.from} - {ctx.Range.to} of {ctx.Total} items"; - - public bool LoadingData { get; set; } - - SearchResult? searchResults; - - protected override async Task OnInitializedAsync() - { - //await UpdateDataAsync(true); - } - public async Task OnKeywordsChanged(string? newKeywords) { - Keywords = newKeywords; - await UpdateDataAsync(true); + await UpdateAsync(State with + { + Keywords = newKeywords, + PageNumber = 1 + }); } - public async Task OnPaginationChange(PaginationEventArgs args) - { - bool resetPageNumber = PageSize != args.PageSize; - - PageNumber = args.Page; - PageSize = args.PageSize; - - await UpdateDataAsync(resetPageNumber); - } - - public async Task OnPageNumberChanged(int newPageNumber) - { - PageNumber = newPageNumber; - await UpdateDataAsync(false); - } - - public async Task OnPageSizeChanged(int newPageSize) - { - PageSize = newPageSize; - await UpdateDataAsync(true); - } - - private async Task UpdateDataAsync(bool resetPageNumber) - { - if (resetPageNumber) - PageNumber = 1; - - await LoadTagsAsync(); - } - - private async Task LoadTagsAsync() - { - LoadingData = true; - - SearchTagsRequest request = new( - Options: new() - { - PageNumber = PageNumber, - PageSize = PageSize, - Criteria = new() - { - Name = Keywords - }, - SortOptions = [.. _sortOptions] - } - ); - - await JS.InvokeVoidAsync("pageHelpers.scrollToTop"); - var result = await Client.SearchAsync(request); - - searchResults = result?.Results ?? new(); - - LoadingData = false; - - //await InvokeAsync(StateHasChanged); - } - - private List> _sortOptions = - [ - new(TagSortField.Name, Application.Common.Search.SortDirection.Ascending) - ]; - private async Task HandleTableChange(AntDesign.TableModels.QueryModel queryModel) { - // PageNumber = queryModel.PageIndex; - // PageSize = queryModel.PageSize; + if (IsLoading) + return; - _sortOptions = MapSortOptions(queryModel); + var nextSortOptions = MapSortOptions(queryModel); - await LoadTagsAsync(); + if (SortOptionsEqual(nextSortOptions, State.SortOptions)) + return; + + await UpdateAsync(State with + { + SortOptions = nextSortOptions, + PageNumber = 1 + }); } private List> MapSortOptions(AntDesign.TableModels.QueryModel queryModel) @@ -329,16 +269,15 @@ item.Blacklisted = response.TagStatus is TagStatus.Blacklisted; } - await InvokeAsync(StateHasChanged); + //await InvokeAsync(StateHasChanged); - var config = new NotificationConfig() + MessageConfig messageConfig = new() { - Message = $"Tag Status Update", - Description = $"Tag '{item.Name}' set to {status.ToString()}.", - Placement = NotificationPlacement.Top + Content = $"Tag '{item.Name}' set to {status.ToString()}.", + Type = MessageType.Success }; - await NotificationService.Open(config); + _ = MessageService.OpenAsync(messageConfig); } private async Task OpenSetEnglishNameModal(TagSearchItem item) @@ -381,11 +320,59 @@ await InvokeAsync(StateHasChanged); - await NotificationService.Open(new NotificationConfig + // await NotificationService.Open(new NotificationConfig + // { + // Message = "Tag English Name Updated", + // Description = $"Tag '{item.Name}' English name set to '{response.EnglishName}'.", + // Placement = NotificationPlacement.Top + // }); + + MessageConfig messageConfig = new() { - Message = "Tag English Name Updated", - Description = $"Tag '{item.Name}' English name set to '{response.EnglishName}'.", - Placement = NotificationPlacement.Top - }); + Content = $"Tag '{item.Name}' English name set to '{response.EnglishName}'.", + Type = MessageType.Success + }; + + _ = MessageService.OpenAsync(messageConfig); + } + + protected override TagFilterState ParseStateFromUri(string absoluteUri) + => TagFilterState.FromQuery(new Uri(absoluteUri).Query); + + protected override string BuildUri(TagFilterState state) + { + var basePath = new Uri(Nav.Uri).GetLeftPart(UriPartial.Path); + return QueryHelpers.AddQueryString(basePath, state.ToQuery()); + } + + protected override bool IsThisPage(string absoluteUri) + => Nav.ToBaseRelativePath(absoluteUri).StartsWith("tags", StringComparison.OrdinalIgnoreCase); + + protected override async Task> ExecuteSearchAsync(TagFilterState state, CancellationToken ct) + { + var response = await Client.SearchAsync(state.ToSearchRequest(), ct); + return response?.Results ?? new SearchResult(); + } + + private AntDesign.SortDirection? GetAntSortDirection(TagSortField field) + { + var sort = State.SortOptions.FirstOrDefault(x => x.Field == field); + + return sort?.Direction switch + { + Application.Common.Search.SortDirection.Ascending => AntDesign.SortDirection.Ascending, + Application.Common.Search.SortDirection.Descending => AntDesign.SortDirection.Descending, + _ => null + }; + } + + private static bool SortOptionsEqual( + IReadOnlyList> left, + IReadOnlyList> right) + { + return left.Count == right.Count + && left.Zip(right).All(x => + x.First.Field == x.Second.Field && + x.First.Direction == x.Second.Direction); } } \ No newline at end of file diff --git a/JSMR.UI.Blazor/Shared/SearchPageBase.cs b/JSMR.UI.Blazor/Shared/SearchPageBase.cs index 0ef2b12..a7dae14 100644 --- a/JSMR.UI.Blazor/Shared/SearchPageBase.cs +++ b/JSMR.UI.Blazor/Shared/SearchPageBase.cs @@ -80,7 +80,7 @@ public abstract class SearchPageBase : ComponentBase, IAsyncDispo try { IsLoading = true; - //StateHasChanged(); + StateHasChanged(); _cancellationTokenSource.Cancel(); _cancellationTokenSource = new();