mirror of
https://github.com/claunia/marechai.git
synced 2026-07-08 17:57:08 +00:00
Enhance Documents management; add filtering, sorting, and pagination capabilities to DocumentsController and DocumentsService, update UI in Documents.razor with MudDataGrid integration, and improve data retrieval logic for better performance.
This commit is contained in:
@@ -22,7 +22,7 @@ namespace Marechai.ApiClient.Documents.Count
|
||||
/// </summary>
|
||||
/// <param name="pathParameters">Path parameters for the request</param>
|
||||
/// <param name="requestAdapter">The request adapter to use to execute the requests.</param>
|
||||
public CountRequestBuilder(Dictionary<string, object> pathParameters, IRequestAdapter requestAdapter) : base(requestAdapter, "{+baseurl}/documents/count", pathParameters)
|
||||
public CountRequestBuilder(Dictionary<string, object> pathParameters, IRequestAdapter requestAdapter) : base(requestAdapter, "{+baseurl}/documents/count{?filters*}", pathParameters)
|
||||
{
|
||||
}
|
||||
/// <summary>
|
||||
@@ -30,7 +30,7 @@ namespace Marechai.ApiClient.Documents.Count
|
||||
/// </summary>
|
||||
/// <param name="rawUrl">The raw URL to use for the request builder.</param>
|
||||
/// <param name="requestAdapter">The request adapter to use to execute the requests.</param>
|
||||
public CountRequestBuilder(string rawUrl, IRequestAdapter requestAdapter) : base(requestAdapter, "{+baseurl}/documents/count", rawUrl)
|
||||
public CountRequestBuilder(string rawUrl, IRequestAdapter requestAdapter) : base(requestAdapter, "{+baseurl}/documents/count{?filters*}", rawUrl)
|
||||
{
|
||||
}
|
||||
/// <returns>A <see cref="int"/></returns>
|
||||
@@ -39,11 +39,11 @@ namespace Marechai.ApiClient.Documents.Count
|
||||
/// <exception cref="global::Marechai.ApiClient.Models.ProblemDetails">When receiving a 400 status code</exception>
|
||||
#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER
|
||||
#nullable enable
|
||||
public async Task<int?> GetAsync(Action<RequestConfiguration<DefaultQueryParameters>>? requestConfiguration = default, CancellationToken cancellationToken = default)
|
||||
public async Task<int?> GetAsync(Action<RequestConfiguration<global::Marechai.ApiClient.Documents.Count.CountRequestBuilder.CountRequestBuilderGetQueryParameters>>? requestConfiguration = default, CancellationToken cancellationToken = default)
|
||||
{
|
||||
#nullable restore
|
||||
#else
|
||||
public async Task<int?> GetAsync(Action<RequestConfiguration<DefaultQueryParameters>> requestConfiguration = default, CancellationToken cancellationToken = default)
|
||||
public async Task<int?> GetAsync(Action<RequestConfiguration<global::Marechai.ApiClient.Documents.Count.CountRequestBuilder.CountRequestBuilderGetQueryParameters>> requestConfiguration = default, CancellationToken cancellationToken = default)
|
||||
{
|
||||
#endif
|
||||
var requestInfo = ToGetRequestInformation(requestConfiguration);
|
||||
@@ -57,11 +57,11 @@ namespace Marechai.ApiClient.Documents.Count
|
||||
/// <param name="requestConfiguration">Configuration for the request such as headers, query parameters, and middleware options.</param>
|
||||
#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER
|
||||
#nullable enable
|
||||
public RequestInformation ToGetRequestInformation(Action<RequestConfiguration<DefaultQueryParameters>>? requestConfiguration = default)
|
||||
public RequestInformation ToGetRequestInformation(Action<RequestConfiguration<global::Marechai.ApiClient.Documents.Count.CountRequestBuilder.CountRequestBuilderGetQueryParameters>>? requestConfiguration = default)
|
||||
{
|
||||
#nullable restore
|
||||
#else
|
||||
public RequestInformation ToGetRequestInformation(Action<RequestConfiguration<DefaultQueryParameters>> requestConfiguration = default)
|
||||
public RequestInformation ToGetRequestInformation(Action<RequestConfiguration<global::Marechai.ApiClient.Documents.Count.CountRequestBuilder.CountRequestBuilderGetQueryParameters>> requestConfiguration = default)
|
||||
{
|
||||
#endif
|
||||
var requestInfo = new RequestInformation(Method.GET, UrlTemplate, PathParameters);
|
||||
@@ -78,12 +78,27 @@ namespace Marechai.ApiClient.Documents.Count
|
||||
{
|
||||
return new global::Marechai.ApiClient.Documents.Count.CountRequestBuilder(rawUrl, RequestAdapter);
|
||||
}
|
||||
[global::System.CodeDom.Compiler.GeneratedCode("Kiota", "1.0.0")]
|
||||
#pragma warning disable CS1591
|
||||
public partial class CountRequestBuilderGetQueryParameters
|
||||
#pragma warning restore CS1591
|
||||
{
|
||||
#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER
|
||||
#nullable enable
|
||||
[QueryParameter("filters")]
|
||||
public string[]? Filters { get; set; }
|
||||
#nullable restore
|
||||
#else
|
||||
[QueryParameter("filters")]
|
||||
public string[] Filters { get; set; }
|
||||
#endif
|
||||
}
|
||||
/// <summary>
|
||||
/// Configuration for the request such as headers, query parameters, and middleware options.
|
||||
/// </summary>
|
||||
[Obsolete("This class is deprecated. Please use the generic RequestConfiguration class generated by the generator.")]
|
||||
[global::System.CodeDom.Compiler.GeneratedCode("Kiota", "1.0.0")]
|
||||
public partial class CountRequestBuilderGetRequestConfiguration : RequestConfiguration<DefaultQueryParameters>
|
||||
public partial class CountRequestBuilderGetRequestConfiguration : RequestConfiguration<global::Marechai.ApiClient.Documents.Count.CountRequestBuilder.CountRequestBuilderGetQueryParameters>
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
@@ -90,7 +90,7 @@ namespace Marechai.ApiClient.Documents
|
||||
/// </summary>
|
||||
/// <param name="pathParameters">Path parameters for the request</param>
|
||||
/// <param name="requestAdapter">The request adapter to use to execute the requests.</param>
|
||||
public DocumentsRequestBuilder(Dictionary<string, object> pathParameters, IRequestAdapter requestAdapter) : base(requestAdapter, "{+baseurl}/documents{?skip*,take*}", pathParameters)
|
||||
public DocumentsRequestBuilder(Dictionary<string, object> pathParameters, IRequestAdapter requestAdapter) : base(requestAdapter, "{+baseurl}/documents{?filters*,skip*,sortBy*,sortDescending*,take*}", pathParameters)
|
||||
{
|
||||
}
|
||||
/// <summary>
|
||||
@@ -98,7 +98,7 @@ namespace Marechai.ApiClient.Documents
|
||||
/// </summary>
|
||||
/// <param name="rawUrl">The raw URL to use for the request builder.</param>
|
||||
/// <param name="requestAdapter">The request adapter to use to execute the requests.</param>
|
||||
public DocumentsRequestBuilder(string rawUrl, IRequestAdapter requestAdapter) : base(requestAdapter, "{+baseurl}/documents{?skip*,take*}", rawUrl)
|
||||
public DocumentsRequestBuilder(string rawUrl, IRequestAdapter requestAdapter) : base(requestAdapter, "{+baseurl}/documents{?filters*,skip*,sortBy*,sortDescending*,take*}", rawUrl)
|
||||
{
|
||||
}
|
||||
/// <returns>A List<global::Marechai.ApiClient.Models.DocumentDto></returns>
|
||||
@@ -195,8 +195,28 @@ namespace Marechai.ApiClient.Documents
|
||||
public partial class DocumentsRequestBuilderGetQueryParameters
|
||||
#pragma warning restore CS1591
|
||||
{
|
||||
#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER
|
||||
#nullable enable
|
||||
[QueryParameter("filters")]
|
||||
public string[]? Filters { get; set; }
|
||||
#nullable restore
|
||||
#else
|
||||
[QueryParameter("filters")]
|
||||
public string[] Filters { get; set; }
|
||||
#endif
|
||||
[QueryParameter("skip")]
|
||||
public int? Skip { get; set; }
|
||||
#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER
|
||||
#nullable enable
|
||||
[QueryParameter("sortBy")]
|
||||
public string? SortBy { get; set; }
|
||||
#nullable restore
|
||||
#else
|
||||
[QueryParameter("sortBy")]
|
||||
public string SortBy { get; set; }
|
||||
#endif
|
||||
[QueryParameter("sortDescending")]
|
||||
public bool? SortDescending { get; set; }
|
||||
[QueryParameter("take")]
|
||||
public int? Take { get; set; }
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"descriptionHash": "001FF5857D8AF75D32CA7A7B73390EED7EB388BD199A32E13EE496AF1FE7FD989B93DEBF12A7208B67DC427E3F9E52A029A8FFB60FC4E9563807A0D888DCD263",
|
||||
"descriptionHash": "54DE7B3B4686292383D880B07A74CC0E1F26D40780DE3D24A99996FBF17F18414E8CE3A0950563EAAF2947829C8A99C270E688BE582A361E58CFE9F0BE2463E9",
|
||||
"descriptionLocation": "../../../../../tmp/openapi.json",
|
||||
"lockFileVersion": "1.0.0",
|
||||
"kiotaVersion": "1.31.1",
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Security.Claims;
|
||||
using System.Threading;
|
||||
@@ -47,7 +48,9 @@ public class DocumentsController(MarechaiContext context) : ControllerBase
|
||||
[AllowAnonymous]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
public Task<int> GetDocumentsCountAsync() => context.Documents.CountAsync();
|
||||
public Task<int> GetDocumentsCountAsync([FromQuery(Name = "filters")] string[] filters = null,
|
||||
CancellationToken cancellationToken = default) =>
|
||||
ApplyFilters(context.Documents.AsNoTracking(), filters).CountAsync(cancellationToken);
|
||||
|
||||
[HttpGet("minimum-year")]
|
||||
[AllowAnonymous]
|
||||
@@ -194,30 +197,169 @@ public class DocumentsController(MarechaiContext context) : ControllerBase
|
||||
[AllowAnonymous]
|
||||
[ProducesResponseType(StatusCodes.Status200OK)]
|
||||
[ProducesResponseType(StatusCodes.Status400BadRequest)]
|
||||
public Task<List<DocumentDto>> GetAsync([FromQuery] int? skip = null, [FromQuery] int? take = null,
|
||||
public Task<List<DocumentDto>> GetAsync([FromQuery] int? skip = null,
|
||||
[FromQuery] int? take = null,
|
||||
[FromQuery] string sortBy = null,
|
||||
[FromQuery] bool sortDescending = false,
|
||||
[FromQuery(Name = "filters")] string[] filters = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
IQueryable<Document> ordered = context.Documents
|
||||
.OrderBy(b => MarechaiContext.NaturalSortKey(b.SortTitle))
|
||||
.ThenBy(b => b.Published)
|
||||
.ThenBy(b => MarechaiContext.NaturalSortKey(b.Title));
|
||||
IQueryable<Document> query = ApplyFilters(context.Documents.AsNoTracking(), filters);
|
||||
|
||||
if(skip.HasValue) ordered = ordered.Skip(skip.Value);
|
||||
if(take.HasValue) ordered = ordered.Take(take.Value);
|
||||
// When no user-supplied sort is set, keep the legacy default ordering
|
||||
// (SortTitle → Published → Title) so anonymous unfiltered consumers see
|
||||
// the same order they get today.
|
||||
IOrderedQueryable<Document> ordered = sortBy switch
|
||||
{
|
||||
"Title" => sortDescending
|
||||
? query.OrderByDescending(b => MarechaiContext.NaturalSortKey(b.Title))
|
||||
: query.OrderBy(b => MarechaiContext.NaturalSortKey(b.Title)),
|
||||
"Published" => sortDescending
|
||||
? query.OrderByDescending(b => b.Published)
|
||||
: query.OrderBy(b => b.Published),
|
||||
"Country" => sortDescending
|
||||
? query.OrderByDescending(b => MarechaiContext.NaturalSortKey(b.Country.Name))
|
||||
: query.OrderBy(b => MarechaiContext.NaturalSortKey(b.Country.Name)),
|
||||
"InternetArchiveUrl" => sortDescending
|
||||
? query.OrderByDescending(b => b.InternetArchiveUrl)
|
||||
: query.OrderBy(b => b.InternetArchiveUrl),
|
||||
_ => query.OrderBy(b => MarechaiContext.NaturalSortKey(b.SortTitle))
|
||||
.ThenBy(b => b.Published)
|
||||
.ThenBy(b => MarechaiContext.NaturalSortKey(b.Title))
|
||||
};
|
||||
|
||||
return ordered.Select(b => new DocumentDto
|
||||
{
|
||||
Id = b.Id,
|
||||
Title = b.Title,
|
||||
NativeTitle = b.NativeTitle,
|
||||
SortTitle = b.SortTitle,
|
||||
Published = b.Published,
|
||||
PublishedPrecision = b.PublishedPrecision,
|
||||
CountryId = b.CountryId,
|
||||
Country = b.Country.Name,
|
||||
InternetArchiveUrl = b.InternetArchiveUrl
|
||||
})
|
||||
.ToListAsync(cancellationToken);
|
||||
IQueryable<Document> paged = ordered;
|
||||
if(skip.HasValue) paged = paged.Skip(skip.Value);
|
||||
if(take.HasValue) paged = paged.Take(take.Value);
|
||||
|
||||
return paged.Select(b => new DocumentDto
|
||||
{
|
||||
Id = b.Id,
|
||||
Title = b.Title,
|
||||
NativeTitle = b.NativeTitle,
|
||||
SortTitle = b.SortTitle,
|
||||
Published = b.Published,
|
||||
PublishedPrecision = b.PublishedPrecision,
|
||||
CountryId = b.CountryId,
|
||||
Country = b.Country.Name,
|
||||
InternetArchiveUrl = b.InternetArchiveUrl
|
||||
})
|
||||
.ToListAsync(cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Translates MudDataGrid <c>FilterDefinition</c>s wired over the network as
|
||||
/// <c>"{Column}||{Operator}||{Value}"</c> triples into LINQ predicates against
|
||||
/// the <see cref="Document"/> entity. Recognized columns mirror the
|
||||
/// <c>PropertyColumn</c> names emitted by the admin grid: <c>Title</c>,
|
||||
/// <c>Published</c>, <c>Country</c>, <c>InternetArchiveUrl</c>. Unknown columns
|
||||
/// and operators are silently ignored — filters MudBlazor may emit for
|
||||
/// non-existent columns must never 400 the listing call.
|
||||
/// </summary>
|
||||
static IQueryable<Document> ApplyFilters(IQueryable<Document> query, string[] filters)
|
||||
{
|
||||
if(filters is null || filters.Length == 0) return query;
|
||||
|
||||
foreach(string raw in filters)
|
||||
{
|
||||
if(string.IsNullOrWhiteSpace(raw)) continue;
|
||||
|
||||
string[] parts = raw.Split("||", 3, StringSplitOptions.None);
|
||||
if(parts.Length < 2) continue;
|
||||
|
||||
string column = parts[0];
|
||||
string op = parts[1];
|
||||
string value = parts.Length >= 3 ? parts[2] : string.Empty;
|
||||
bool isEmpty = op == "is empty";
|
||||
bool isNotEmp = op == "is not empty";
|
||||
|
||||
// Skip non-empty-check operators with no value supplied so a stray
|
||||
// open-but-unfilled filter UI doesn't accidentally hide every row.
|
||||
if(!isEmpty && !isNotEmp && string.IsNullOrEmpty(value)) continue;
|
||||
|
||||
switch(column)
|
||||
{
|
||||
case "Title":
|
||||
query = op switch
|
||||
{
|
||||
"contains" => query.Where(d => d.Title.Contains(value)),
|
||||
"not contains" => query.Where(d => !d.Title.Contains(value)),
|
||||
"equals" => query.Where(d => d.Title == value),
|
||||
"not equals" => query.Where(d => d.Title != value),
|
||||
"starts with" => query.Where(d => d.Title.StartsWith(value)),
|
||||
"ends with" => query.Where(d => d.Title.EndsWith(value)),
|
||||
"is empty" => query.Where(d => d.Title == null || d.Title == string.Empty),
|
||||
"is not empty" => query.Where(d => d.Title != null && d.Title != string.Empty),
|
||||
_ => query
|
||||
};
|
||||
break;
|
||||
|
||||
case "Country":
|
||||
query = op switch
|
||||
{
|
||||
"contains" => query.Where(d => d.Country.Name.Contains(value)),
|
||||
"not contains" => query.Where(d => !d.Country.Name.Contains(value)),
|
||||
"equals" => query.Where(d => d.Country.Name == value),
|
||||
"not equals" => query.Where(d => d.Country.Name != value),
|
||||
"starts with" => query.Where(d => d.Country.Name.StartsWith(value)),
|
||||
"ends with" => query.Where(d => d.Country.Name.EndsWith(value)),
|
||||
"is empty" => query.Where(d => d.CountryId == null ||
|
||||
d.Country.Name == null ||
|
||||
d.Country.Name == string.Empty),
|
||||
"is not empty" => query.Where(d => d.CountryId != null &&
|
||||
d.Country.Name != null &&
|
||||
d.Country.Name != string.Empty),
|
||||
_ => query
|
||||
};
|
||||
break;
|
||||
|
||||
case "InternetArchiveUrl":
|
||||
query = op switch
|
||||
{
|
||||
"contains" => query.Where(d => d.InternetArchiveUrl != null &&
|
||||
d.InternetArchiveUrl.Contains(value)),
|
||||
"not contains" => query.Where(d => d.InternetArchiveUrl == null ||
|
||||
!d.InternetArchiveUrl.Contains(value)),
|
||||
"equals" => query.Where(d => d.InternetArchiveUrl == value),
|
||||
"not equals" => query.Where(d => d.InternetArchiveUrl != value),
|
||||
"starts with" => query.Where(d => d.InternetArchiveUrl != null &&
|
||||
d.InternetArchiveUrl.StartsWith(value)),
|
||||
"ends with" => query.Where(d => d.InternetArchiveUrl != null &&
|
||||
d.InternetArchiveUrl.EndsWith(value)),
|
||||
"is empty" => query.Where(d => d.InternetArchiveUrl == null ||
|
||||
d.InternetArchiveUrl == string.Empty),
|
||||
"is not empty" => query.Where(d => d.InternetArchiveUrl != null &&
|
||||
d.InternetArchiveUrl != string.Empty),
|
||||
_ => query
|
||||
};
|
||||
break;
|
||||
|
||||
case "Published":
|
||||
if(op == "is empty") { query = query.Where(d => d.Published == null); break; }
|
||||
if(op == "is not empty"){ query = query.Where(d => d.Published != null); break; }
|
||||
|
||||
if(!DateTime.TryParse(value, CultureInfo.InvariantCulture,
|
||||
DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal,
|
||||
out DateTime parsed))
|
||||
continue;
|
||||
|
||||
DateTime day = parsed.Date;
|
||||
|
||||
query = op switch
|
||||
{
|
||||
"is" => query.Where(d => d.Published.HasValue && d.Published.Value.Date == day),
|
||||
"is not" => query.Where(d => d.Published.HasValue && d.Published.Value.Date != day),
|
||||
"is after" => query.Where(d => d.Published.HasValue && d.Published.Value.Date > day),
|
||||
"is before" => query.Where(d => d.Published.HasValue && d.Published.Value.Date < day),
|
||||
"is on or after" => query.Where(d => d.Published.HasValue && d.Published.Value.Date >= day),
|
||||
"is on or before" => query.Where(d => d.Published.HasValue && d.Published.Value.Date <= day),
|
||||
_ => query
|
||||
};
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return query;
|
||||
}
|
||||
|
||||
[HttpGet("{id:long}")]
|
||||
|
||||
@@ -24,23 +24,15 @@
|
||||
<MudAlert Severity="Severity.Success" Class="mb-4" ShowCloseIcon="true" CloseIconClicked="() => _successMessage = null">@_successMessage</MudAlert>
|
||||
}
|
||||
|
||||
@if(_isLoading)
|
||||
{
|
||||
<div class="d-flex justify-center pa-8">
|
||||
<MudProgressCircular Color="Color.Primary" Indeterminate="true" Size="Size.Large"/>
|
||||
</div>
|
||||
return;
|
||||
}
|
||||
|
||||
<MudPaper Elevation="2" Class="rounded-lg">
|
||||
<MudToolBar Dense="true">
|
||||
<MudButton Variant="Variant.Filled" Color="Color.Primary" StartIcon="@Icons.Material.Filled.Add" OnClick="OpenAddDocumentDialog">@L["Add Document"]</MudButton>
|
||||
<MudButton Variant="Variant.Outlined" Color="Color.Primary" StartIcon="@Icons.Material.Filled.Upload" OnClick="OpenImportDialog" Class="ml-2">@L["Import CSV"]</MudButton>
|
||||
<MudSpacer/>
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Refresh" OnClick="LoadDocumentsAsync"/>
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Refresh" OnClick="async () => await _dataGrid.ReloadServerData()"/>
|
||||
</MudToolBar>
|
||||
|
||||
<MudDataGrid Items="_documents" Dense="true" Hover="true" Striped="true" Filterable="true">
|
||||
<MudDataGrid @ref="_dataGrid" T="DocumentDto" ServerData="ServerReload" Dense="true" Hover="true" Striped="true" Filterable="true" RowsPerPage="25">
|
||||
<Columns>
|
||||
<PropertyColumn Property="x => x.Title" Title="@L["Title"]"/>
|
||||
<PropertyColumn Property="x => x.Published" Title="@L["Published"]">
|
||||
@@ -49,7 +41,7 @@
|
||||
</CellTemplate>
|
||||
</PropertyColumn>
|
||||
<PropertyColumn Property="x => x.Country" Title="@L["Country"]"/>
|
||||
<TemplateColumn Title="@L["Internet Archive link"]" Sortable="false" Filterable="false">
|
||||
<PropertyColumn Property="x => x.InternetArchiveUrl" Title="@L["Internet Archive link"]">
|
||||
<CellTemplate>
|
||||
@if(!string.IsNullOrWhiteSpace(context.Item.InternetArchiveUrl))
|
||||
{
|
||||
@@ -62,7 +54,7 @@
|
||||
</MudTooltip>
|
||||
}
|
||||
</CellTemplate>
|
||||
</TemplateColumn>
|
||||
</PropertyColumn>
|
||||
<TemplateColumn Title="@L["Actions"]" Sortable="false" Filterable="false">
|
||||
<CellTemplate>
|
||||
<MudStack Row="true" Spacing="1">
|
||||
@@ -74,5 +66,8 @@
|
||||
</CellTemplate>
|
||||
</TemplateColumn>
|
||||
</Columns>
|
||||
<PagerContent>
|
||||
<MudDataGridPager T="DocumentDto" PageSizeOptions="new[] { 10, 25, 50, 100 }"/>
|
||||
</PagerContent>
|
||||
</MudDataGrid>
|
||||
</MudPaper>
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Marechai.ApiClient.Models;
|
||||
using MudBlazor;
|
||||
@@ -8,18 +11,81 @@ namespace Marechai.Pages.Admin;
|
||||
|
||||
public partial class Documents
|
||||
{
|
||||
string _errorMessage;
|
||||
bool _isLoading = true;
|
||||
string _successMessage;
|
||||
List<DocumentDto> _documents;
|
||||
string _errorMessage;
|
||||
string _successMessage;
|
||||
MudDataGrid<DocumentDto> _dataGrid;
|
||||
|
||||
protected override async Task OnInitializedAsync() => await LoadDocumentsAsync();
|
||||
|
||||
async Task LoadDocumentsAsync()
|
||||
async Task<GridData<DocumentDto>> ServerReload(GridState<DocumentDto> state, CancellationToken cancellationToken)
|
||||
{
|
||||
_isLoading = true;
|
||||
_documents = await DocumentsService.GetDocumentsAsync();
|
||||
_isLoading = false;
|
||||
int skip = state.Page * state.PageSize;
|
||||
int take = state.PageSize;
|
||||
|
||||
string sortBy = null;
|
||||
bool sortDescending = false;
|
||||
|
||||
SortDefinition<DocumentDto> sort = state.SortDefinitions.FirstOrDefault();
|
||||
|
||||
if(sort is not null)
|
||||
{
|
||||
sortBy = sort.SortBy;
|
||||
sortDescending = sort.Descending;
|
||||
}
|
||||
|
||||
// Translate MudBlazor FilterDefinitions to the "{Column}||{Operator}||{Value}"
|
||||
// wire format consumed by DocumentsController.ApplyFilters. Column comes from
|
||||
// PropertyColumn binding via fd.Column?.PropertyName which matches the
|
||||
// case labels in the controller switch (Title/Published/Country/InternetArchiveUrl).
|
||||
List<string> filters = null;
|
||||
|
||||
foreach(IFilterDefinition<DocumentDto> fd in state.FilterDefinitions)
|
||||
{
|
||||
string column = fd.Column?.PropertyName;
|
||||
string op = fd.Operator;
|
||||
if(string.IsNullOrEmpty(column) || string.IsNullOrEmpty(op)) continue;
|
||||
|
||||
bool isEmptyOp = op is "is empty" or "is not empty";
|
||||
|
||||
string value;
|
||||
|
||||
switch(fd.Value)
|
||||
{
|
||||
case null:
|
||||
if(!isEmptyOp) continue;
|
||||
value = string.Empty;
|
||||
break;
|
||||
case DateTime dt:
|
||||
value = dt.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture);
|
||||
break;
|
||||
case DateTimeOffset dto:
|
||||
value = dto.UtcDateTime.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture);
|
||||
break;
|
||||
case IFormattable f:
|
||||
value = f.ToString(null, CultureInfo.InvariantCulture);
|
||||
break;
|
||||
default:
|
||||
value = fd.Value.ToString();
|
||||
break;
|
||||
}
|
||||
|
||||
// Skip non-empty-check operators with no meaningful value so a freshly
|
||||
// opened (but unfilled) filter UI doesn't accidentally drop every row.
|
||||
if(!isEmptyOp && string.IsNullOrEmpty(value)) continue;
|
||||
|
||||
filters ??= [];
|
||||
filters.Add($"{column}||{op}||{value}");
|
||||
}
|
||||
|
||||
Task<int> countTask = DocumentsService.GetDocumentsCountAsync(filters, cancellationToken);
|
||||
Task<List<DocumentDto>> dataTask =
|
||||
DocumentsService.GetPagedAsync(skip, take, sortBy, sortDescending, filters, cancellationToken);
|
||||
|
||||
await Task.WhenAll(countTask, dataTask);
|
||||
|
||||
return new GridData<DocumentDto>
|
||||
{
|
||||
Items = dataTask.Result,
|
||||
TotalItems = countTask.Result
|
||||
};
|
||||
}
|
||||
|
||||
static string FormatDate(DateTimeOffset? date, int? precision = 0)
|
||||
@@ -65,7 +131,7 @@ public partial class Documents
|
||||
if(id is not null)
|
||||
{
|
||||
_successMessage = L["Document created successfully."];
|
||||
await LoadDocumentsAsync();
|
||||
await _dataGrid.ReloadServerData();
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -125,7 +191,7 @@ public partial class Documents
|
||||
if(succeeded)
|
||||
{
|
||||
_successMessage = L["Document updated successfully."];
|
||||
await LoadDocumentsAsync();
|
||||
await _dataGrid.ReloadServerData();
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -147,7 +213,7 @@ public partial class Documents
|
||||
DialogResult result = await dialog.Result;
|
||||
|
||||
if(result is { Canceled: false })
|
||||
await LoadDocumentsAsync();
|
||||
await _dataGrid.ReloadServerData();
|
||||
}
|
||||
|
||||
async Task ConfirmDeleteDocument(DocumentDto document)
|
||||
@@ -180,7 +246,7 @@ public partial class Documents
|
||||
if(succeeded)
|
||||
{
|
||||
_successMessage = L["Document deleted successfully."];
|
||||
await LoadDocumentsAsync();
|
||||
await _dataGrid.ReloadServerData();
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using Marechai.ApiClient.Models;
|
||||
@@ -34,11 +35,15 @@ namespace Marechai.Services;
|
||||
|
||||
public class DocumentsService(Marechai.ApiClient.Client client, ReferenceDataCache referenceData)
|
||||
{
|
||||
public async Task<int> GetDocumentsCountAsync()
|
||||
public async Task<int> GetDocumentsCountAsync(IReadOnlyList<string> filters = null,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
int? count = await client.Documents.Count.GetAsync();
|
||||
int? count = await client.Documents.Count.GetAsync(config =>
|
||||
{
|
||||
if(filters is { Count: > 0 }) config.QueryParameters.Filters = filters.ToArray();
|
||||
}, cancellationToken);
|
||||
|
||||
return count ?? 0;
|
||||
}
|
||||
@@ -189,6 +194,37 @@ public class DocumentsService(Marechai.ApiClient.Client client, ReferenceDataCac
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Server-side paged fetch used by the admin <c>MudDataGrid</c>. Pushes the
|
||||
/// sort + filter spec all the way down to the SQL query so paging,
|
||||
/// filtering and ordering happen in the database. Filters are
|
||||
/// <c>"{Column}||{Operator}||{Value}"</c> triples mirroring MudBlazor's
|
||||
/// <c>FilterDefinition</c> shape; <c>DocumentsController.ApplyFilters</c>
|
||||
/// translates them into LINQ predicates.
|
||||
/// </summary>
|
||||
public async Task<List<DocumentDto>> GetPagedAsync(int skip, int take, string sortBy, bool sortDescending,
|
||||
IReadOnlyList<string> filters,
|
||||
CancellationToken cancellationToken = default)
|
||||
{
|
||||
try
|
||||
{
|
||||
List<DocumentDto> documents = await client.Documents.GetAsync(config =>
|
||||
{
|
||||
config.QueryParameters.Skip = skip;
|
||||
config.QueryParameters.Take = take;
|
||||
if(!string.IsNullOrWhiteSpace(sortBy)) config.QueryParameters.SortBy = sortBy;
|
||||
if(sortDescending) config.QueryParameters.SortDescending = true;
|
||||
if(filters is { Count: > 0 }) config.QueryParameters.Filters = filters.ToArray();
|
||||
}, cancellationToken);
|
||||
|
||||
return documents ?? [];
|
||||
}
|
||||
catch
|
||||
{
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<DocumentDto> GetDocumentAsync(long id)
|
||||
{
|
||||
try
|
||||
|
||||
Reference in New Issue
Block a user