Added tag filter state persistence.
This commit is contained in:
109
JSMR.UI.Blazor/Filters/TagFilterState.cs
Normal file
109
JSMR.UI.Blazor/Filters/TagFilterState.cs
Normal file
@@ -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<SearchTagsRequest>
|
||||||
|
{
|
||||||
|
public string? Keywords { get; init; }
|
||||||
|
public int PageNumber { get; init; } = 1;
|
||||||
|
public int PageSize { get; init; } = 100;
|
||||||
|
|
||||||
|
public IReadOnlyList<SortOption<TagSortField>> 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<SortOption<TagSortField>> ParseSortOptions(string? value)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(value))
|
||||||
|
return DefaultSort();
|
||||||
|
|
||||||
|
var result = new List<SortOption<TagSortField>>();
|
||||||
|
|
||||||
|
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<TagSortField>(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<SortOption<TagSortField>> 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<SortOption<TagSortField>> sortOptions)
|
||||||
|
=> sortOptions.Count == 1
|
||||||
|
&& sortOptions[0].Field == TagSortField.Name
|
||||||
|
&& sortOptions[0].Direction == Application.Common.Search.SortDirection.Ascending;
|
||||||
|
}
|
||||||
@@ -13,14 +13,18 @@
|
|||||||
@using JSMR.UI.Blazor.Filters
|
@using JSMR.UI.Blazor.Filters
|
||||||
@using JSMR.UI.Blazor.Models
|
@using JSMR.UI.Blazor.Models
|
||||||
@using JSMR.UI.Blazor.Services
|
@using JSMR.UI.Blazor.Services
|
||||||
|
@using JSMR.UI.Blazor.Shared
|
||||||
@using Microsoft.AspNetCore.WebUtilities
|
@using Microsoft.AspNetCore.WebUtilities
|
||||||
|
@using System.Text.Json
|
||||||
|
|
||||||
|
@inherits SearchPageBase<TagFilterState, TagSearchItem>
|
||||||
|
|
||||||
<PageTitle>Tags</PageTitle>
|
<PageTitle>Tags</PageTitle>
|
||||||
|
|
||||||
<div class="fdsfds">
|
<div class="fdsfds">
|
||||||
<AntDesign.Card Title=@("Tags") Class="ant-blurred-card">
|
<AntDesign.Card Title=@("Tags") Class="ant-blurred-card">
|
||||||
<Extra>
|
<Extra>
|
||||||
<AntDesign.Input TValue="string" Value="Keywords" ValueChanged="OnKeywordsChanged" DebounceMilliseconds="500" Placeholder="Filter">
|
<AntDesign.Input TValue="string" Value="State.Keywords" ValueChanged="OnKeywordsChanged" DebounceMilliseconds="500" Placeholder="Filter">
|
||||||
<Prefix>
|
<Prefix>
|
||||||
<AntDesign.Icon Type="@AntDesign.IconType.Outline.Search" />
|
<AntDesign.Icon Type="@AntDesign.IconType.Outline.Search" />
|
||||||
</Prefix>
|
</Prefix>
|
||||||
@@ -28,10 +32,10 @@
|
|||||||
</Extra>
|
</Extra>
|
||||||
<Body>
|
<Body>
|
||||||
<AntDesign.Table Responsive
|
<AntDesign.Table Responsive
|
||||||
DataSource="@(searchResults?.Items ?? Enumerable.Empty<TagSearchItem>())"
|
DataSource="@(Result?.Items ?? Enumerable.Empty<TagSearchItem>())"
|
||||||
Total="@(searchResults?.TotalItems ?? 0)"
|
Total="@(Result?.TotalItems ?? 0)"
|
||||||
TItem="TagSearchItem"
|
TItem="TagSearchItem"
|
||||||
Loading="LoadingData"
|
Loading="IsLoading"
|
||||||
HidePagination="@true"
|
HidePagination="@true"
|
||||||
RemoteDataSource="@true"
|
RemoteDataSource="@true"
|
||||||
RowKey="x=>x.TagId"
|
RowKey="x=>x.TagId"
|
||||||
@@ -82,7 +86,11 @@
|
|||||||
</AntDesign.ActionColumn>
|
</AntDesign.ActionColumn>
|
||||||
</ColumnDefinitions>
|
</ColumnDefinitions>
|
||||||
</AntDesign.Table>
|
</AntDesign.Table>
|
||||||
<JPagination2 PageNumber="PageNumber" PageNumberChanged="OnPageNumberChanged" PageSize="PageSize" PageSizeChanged="OnPageSizeChanged" TotalItems="TotalItems" />
|
<JPagination2 PageNumber="State.PageNumber"
|
||||||
|
PageNumberChanged="@(pageNumber => UpdateAsync(State with { PageNumber = pageNumber }))"
|
||||||
|
PageSize="State.PageSize"
|
||||||
|
PageSizeChanged="@(pageSize => UpdateAsync(State with { PageSize = pageSize, PageNumber = 1 }))"
|
||||||
|
TotalItems="@(Result?.TotalItems ?? 0)" />
|
||||||
</Body>
|
</Body>
|
||||||
</AntDesign.Card>
|
</AntDesign.Card>
|
||||||
</div>
|
</div>
|
||||||
@@ -157,98 +165,30 @@
|
|||||||
[Inject]
|
[Inject]
|
||||||
ModalService ModalService { get; set; } = default!;
|
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<PaginationTotalContext, string> ShowTotal = ctx => $"{ctx.Range.from} - {ctx.Range.to} of {ctx.Total} items";
|
|
||||||
|
|
||||||
public bool LoadingData { get; set; }
|
|
||||||
|
|
||||||
SearchResult<TagSearchItem>? searchResults;
|
|
||||||
|
|
||||||
protected override async Task OnInitializedAsync()
|
|
||||||
{
|
|
||||||
//await UpdateDataAsync(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task OnKeywordsChanged(string? newKeywords)
|
public async Task OnKeywordsChanged(string? newKeywords)
|
||||||
{
|
{
|
||||||
Keywords = newKeywords;
|
await UpdateAsync(State with
|
||||||
await UpdateDataAsync(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task OnPaginationChange(PaginationEventArgs args)
|
|
||||||
{
|
{
|
||||||
bool resetPageNumber = PageSize != args.PageSize;
|
Keywords = newKeywords,
|
||||||
|
PageNumber = 1
|
||||||
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<SortOption<TagSortField>> _sortOptions =
|
|
||||||
[
|
|
||||||
new(TagSortField.Name, Application.Common.Search.SortDirection.Ascending)
|
|
||||||
];
|
|
||||||
|
|
||||||
private async Task HandleTableChange(AntDesign.TableModels.QueryModel<TagSearchItem> queryModel)
|
private async Task HandleTableChange(AntDesign.TableModels.QueryModel<TagSearchItem> queryModel)
|
||||||
{
|
{
|
||||||
// PageNumber = queryModel.PageIndex;
|
if (IsLoading)
|
||||||
// PageSize = queryModel.PageSize;
|
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<SortOption<TagSortField>> MapSortOptions(AntDesign.TableModels.QueryModel<TagSearchItem> queryModel)
|
private List<SortOption<TagSortField>> MapSortOptions(AntDesign.TableModels.QueryModel<TagSearchItem> queryModel)
|
||||||
@@ -329,16 +269,15 @@
|
|||||||
item.Blacklisted = response.TagStatus is TagStatus.Blacklisted;
|
item.Blacklisted = response.TagStatus is TagStatus.Blacklisted;
|
||||||
}
|
}
|
||||||
|
|
||||||
await InvokeAsync(StateHasChanged);
|
//await InvokeAsync(StateHasChanged);
|
||||||
|
|
||||||
var config = new NotificationConfig()
|
MessageConfig messageConfig = new()
|
||||||
{
|
{
|
||||||
Message = $"Tag Status Update",
|
Content = $"Tag '{item.Name}' set to {status.ToString()}.",
|
||||||
Description = $"Tag '{item.Name}' set to {status.ToString()}.",
|
Type = MessageType.Success
|
||||||
Placement = NotificationPlacement.Top
|
|
||||||
};
|
};
|
||||||
|
|
||||||
await NotificationService.Open(config);
|
_ = MessageService.OpenAsync(messageConfig);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task OpenSetEnglishNameModal(TagSearchItem item)
|
private async Task OpenSetEnglishNameModal(TagSearchItem item)
|
||||||
@@ -381,11 +320,59 @@
|
|||||||
|
|
||||||
await InvokeAsync(StateHasChanged);
|
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",
|
Content = $"Tag '{item.Name}' English name set to '{response.EnglishName}'.",
|
||||||
Description = $"Tag '{item.Name}' English name set to '{response.EnglishName}'.",
|
Type = MessageType.Success
|
||||||
Placement = NotificationPlacement.Top
|
};
|
||||||
});
|
|
||||||
|
_ = 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<SearchResult<TagSearchItem>> ExecuteSearchAsync(TagFilterState state, CancellationToken ct)
|
||||||
|
{
|
||||||
|
var response = await Client.SearchAsync(state.ToSearchRequest(), ct);
|
||||||
|
return response?.Results ?? new SearchResult<TagSearchItem>();
|
||||||
|
}
|
||||||
|
|
||||||
|
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<SortOption<TagSortField>> left,
|
||||||
|
IReadOnlyList<SortOption<TagSortField>> right)
|
||||||
|
{
|
||||||
|
return left.Count == right.Count
|
||||||
|
&& left.Zip(right).All(x =>
|
||||||
|
x.First.Field == x.Second.Field &&
|
||||||
|
x.First.Direction == x.Second.Direction);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -80,7 +80,7 @@ public abstract class SearchPageBase<TState, TItem> : ComponentBase, IAsyncDispo
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
IsLoading = true;
|
IsLoading = true;
|
||||||
//StateHasChanged();
|
StateHasChanged();
|
||||||
|
|
||||||
_cancellationTokenSource.Cancel();
|
_cancellationTokenSource.Cancel();
|
||||||
_cancellationTokenSource = new();
|
_cancellationTokenSource = new();
|
||||||
|
|||||||
Reference in New Issue
Block a user