From b3f08ab47beb5bfbb56f4512e138510e2628469e Mon Sep 17 00:00:00 2001 From: Natalia Portillo Date: Sat, 9 May 2026 13:43:23 +0100 Subject: [PATCH] Add pagination support for document retrieval by letter and year - Introduced CountRequestBuilder for counting documents by letter and year. - Updated WithCItemRequestBuilder and WithYearItemRequestBuilder to include Count property. - Modified DocumentsRequestBuilder to support pagination with skip and take query parameters. - Enhanced DocumentsController to handle pagination in GetDocumentsByLetterAsync and GetDocumentsByYearAsync methods. - Updated Search.razor and Search.razor.cs to implement infinite scrolling and display total document counts. - Refactored DocumentsService to support pagination and counting for document retrieval methods. --- .../Item/Count/CountRequestBuilder.cs | 91 ++++++++++ .../ByLetter/Item/WithCItemRequestBuilder.cs | 30 +++- .../ByYear/Item/Count/CountRequestBuilder.cs | 91 ++++++++++ .../ByYear/Item/WithYearItemRequestBuilder.cs | 30 +++- .../Documents/DocumentsRequestBuilder.cs | 24 ++- Marechai.ApiClient/kiota-lock.json | 2 +- .../Controllers/DocumentsController.cs | 149 ++++++++++------ Marechai/Pages/Documents/Search.razor | 38 ++-- Marechai/Pages/Documents/Search.razor.cs | 168 +++++++++++++++--- Marechai/Services/DocumentsService.cs | 56 +++++- 10 files changed, 564 insertions(+), 115 deletions(-) create mode 100644 Marechai.ApiClient/Documents/ByLetter/Item/Count/CountRequestBuilder.cs create mode 100644 Marechai.ApiClient/Documents/ByYear/Item/Count/CountRequestBuilder.cs diff --git a/Marechai.ApiClient/Documents/ByLetter/Item/Count/CountRequestBuilder.cs b/Marechai.ApiClient/Documents/ByLetter/Item/Count/CountRequestBuilder.cs new file mode 100644 index 00000000..c0c2261e --- /dev/null +++ b/Marechai.ApiClient/Documents/ByLetter/Item/Count/CountRequestBuilder.cs @@ -0,0 +1,91 @@ +// +#pragma warning disable CS0618 +using Marechai.ApiClient.Models; +using Microsoft.Kiota.Abstractions.Extensions; +using Microsoft.Kiota.Abstractions.Serialization; +using Microsoft.Kiota.Abstractions; +using System.Collections.Generic; +using System.IO; +using System.Threading.Tasks; +using System.Threading; +using System; +namespace Marechai.ApiClient.Documents.ByLetter.Item.Count +{ + /// + /// Builds and executes requests for operations under \documents\by-letter\{c}\count + /// + [global::System.CodeDom.Compiler.GeneratedCode("Kiota", "1.0.0")] + public partial class CountRequestBuilder : BaseRequestBuilder + { + /// + /// Instantiates a new and sets the default values. + /// + /// Path parameters for the request + /// The request adapter to use to execute the requests. + public CountRequestBuilder(Dictionary pathParameters, IRequestAdapter requestAdapter) : base(requestAdapter, "{+baseurl}/documents/by-letter/{c}/count", pathParameters) + { + } + /// + /// Instantiates a new and sets the default values. + /// + /// The raw URL to use for the request builder. + /// The request adapter to use to execute the requests. + public CountRequestBuilder(string rawUrl, IRequestAdapter requestAdapter) : base(requestAdapter, "{+baseurl}/documents/by-letter/{c}/count", rawUrl) + { + } + /// A + /// Cancellation token to use when cancelling requests + /// Configuration for the request such as headers, query parameters, and middleware options. + /// When receiving a 400 status code +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER +#nullable enable + public async Task GetAsync(Action>? requestConfiguration = default, CancellationToken cancellationToken = default) + { +#nullable restore +#else + public async Task GetAsync(Action> requestConfiguration = default, CancellationToken cancellationToken = default) + { +#endif + var requestInfo = ToGetRequestInformation(requestConfiguration); + var errorMapping = new Dictionary> + { + { "400", global::Marechai.ApiClient.Models.ProblemDetails.CreateFromDiscriminatorValue }, + }; + return await RequestAdapter.SendPrimitiveAsync(requestInfo, errorMapping, cancellationToken).ConfigureAwait(false); + } + /// A + /// Configuration for the request such as headers, query parameters, and middleware options. +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER +#nullable enable + public RequestInformation ToGetRequestInformation(Action>? requestConfiguration = default) + { +#nullable restore +#else + public RequestInformation ToGetRequestInformation(Action> requestConfiguration = default) + { +#endif + var requestInfo = new RequestInformation(Method.GET, UrlTemplate, PathParameters); + requestInfo.Configure(requestConfiguration); + requestInfo.Headers.TryAdd("Accept", "text/plain;q=0.9"); + return requestInfo; + } + /// + /// Returns a request builder with the provided arbitrary URL. Using this method means any other path or query parameters are ignored. + /// + /// A + /// The raw URL to use for the request builder. + public global::Marechai.ApiClient.Documents.ByLetter.Item.Count.CountRequestBuilder WithUrl(string rawUrl) + { + return new global::Marechai.ApiClient.Documents.ByLetter.Item.Count.CountRequestBuilder(rawUrl, RequestAdapter); + } + /// + /// Configuration for the request such as headers, query parameters, and middleware options. + /// + [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 + { + } + } +} +#pragma warning restore CS0618 diff --git a/Marechai.ApiClient/Documents/ByLetter/Item/WithCItemRequestBuilder.cs b/Marechai.ApiClient/Documents/ByLetter/Item/WithCItemRequestBuilder.cs index 3b33feac..f2fe347b 100644 --- a/Marechai.ApiClient/Documents/ByLetter/Item/WithCItemRequestBuilder.cs +++ b/Marechai.ApiClient/Documents/ByLetter/Item/WithCItemRequestBuilder.cs @@ -1,5 +1,6 @@ // #pragma warning disable CS0618 +using Marechai.ApiClient.Documents.ByLetter.Item.Count; using Marechai.ApiClient.Models; using Microsoft.Kiota.Abstractions.Extensions; using Microsoft.Kiota.Abstractions.Serialization; @@ -17,12 +18,17 @@ namespace Marechai.ApiClient.Documents.ByLetter.Item [global::System.CodeDom.Compiler.GeneratedCode("Kiota", "1.0.0")] public partial class WithCItemRequestBuilder : BaseRequestBuilder { + /// The count property + public global::Marechai.ApiClient.Documents.ByLetter.Item.Count.CountRequestBuilder Count + { + get => new global::Marechai.ApiClient.Documents.ByLetter.Item.Count.CountRequestBuilder(PathParameters, RequestAdapter); + } /// /// Instantiates a new and sets the default values. /// /// Path parameters for the request /// The request adapter to use to execute the requests. - public WithCItemRequestBuilder(Dictionary pathParameters, IRequestAdapter requestAdapter) : base(requestAdapter, "{+baseurl}/documents/by-letter/{c}", pathParameters) + public WithCItemRequestBuilder(Dictionary pathParameters, IRequestAdapter requestAdapter) : base(requestAdapter, "{+baseurl}/documents/by-letter/{c}{?skip*,take*}", pathParameters) { } /// @@ -30,7 +36,7 @@ namespace Marechai.ApiClient.Documents.ByLetter.Item /// /// The raw URL to use for the request builder. /// The request adapter to use to execute the requests. - public WithCItemRequestBuilder(string rawUrl, IRequestAdapter requestAdapter) : base(requestAdapter, "{+baseurl}/documents/by-letter/{c}", rawUrl) + public WithCItemRequestBuilder(string rawUrl, IRequestAdapter requestAdapter) : base(requestAdapter, "{+baseurl}/documents/by-letter/{c}{?skip*,take*}", rawUrl) { } /// A List<global::Marechai.ApiClient.Models.DocumentDto> @@ -39,11 +45,11 @@ namespace Marechai.ApiClient.Documents.ByLetter.Item /// When receiving a 400 status code #if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER #nullable enable - public async Task?> GetAsync(Action>? requestConfiguration = default, CancellationToken cancellationToken = default) + public async Task?> GetAsync(Action>? requestConfiguration = default, CancellationToken cancellationToken = default) { #nullable restore #else - public async Task> GetAsync(Action> requestConfiguration = default, CancellationToken cancellationToken = default) + public async Task> GetAsync(Action> requestConfiguration = default, CancellationToken cancellationToken = default) { #endif var requestInfo = ToGetRequestInformation(requestConfiguration); @@ -58,11 +64,11 @@ namespace Marechai.ApiClient.Documents.ByLetter.Item /// Configuration for the request such as headers, query parameters, and middleware options. #if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER #nullable enable - public RequestInformation ToGetRequestInformation(Action>? requestConfiguration = default) + public RequestInformation ToGetRequestInformation(Action>? requestConfiguration = default) { #nullable restore #else - public RequestInformation ToGetRequestInformation(Action> requestConfiguration = default) + public RequestInformation ToGetRequestInformation(Action> requestConfiguration = default) { #endif var requestInfo = new RequestInformation(Method.GET, UrlTemplate, PathParameters); @@ -79,12 +85,22 @@ namespace Marechai.ApiClient.Documents.ByLetter.Item { return new global::Marechai.ApiClient.Documents.ByLetter.Item.WithCItemRequestBuilder(rawUrl, RequestAdapter); } + [global::System.CodeDom.Compiler.GeneratedCode("Kiota", "1.0.0")] + #pragma warning disable CS1591 + public partial class WithCItemRequestBuilderGetQueryParameters + #pragma warning restore CS1591 + { + [QueryParameter("skip")] + public int? Skip { get; set; } + [QueryParameter("take")] + public int? Take { get; set; } + } /// /// Configuration for the request such as headers, query parameters, and middleware options. /// [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 WithCItemRequestBuilderGetRequestConfiguration : RequestConfiguration + public partial class WithCItemRequestBuilderGetRequestConfiguration : RequestConfiguration { } } diff --git a/Marechai.ApiClient/Documents/ByYear/Item/Count/CountRequestBuilder.cs b/Marechai.ApiClient/Documents/ByYear/Item/Count/CountRequestBuilder.cs new file mode 100644 index 00000000..8a93d2f6 --- /dev/null +++ b/Marechai.ApiClient/Documents/ByYear/Item/Count/CountRequestBuilder.cs @@ -0,0 +1,91 @@ +// +#pragma warning disable CS0618 +using Marechai.ApiClient.Models; +using Microsoft.Kiota.Abstractions.Extensions; +using Microsoft.Kiota.Abstractions.Serialization; +using Microsoft.Kiota.Abstractions; +using System.Collections.Generic; +using System.IO; +using System.Threading.Tasks; +using System.Threading; +using System; +namespace Marechai.ApiClient.Documents.ByYear.Item.Count +{ + /// + /// Builds and executes requests for operations under \documents\by-year\{year}\count + /// + [global::System.CodeDom.Compiler.GeneratedCode("Kiota", "1.0.0")] + public partial class CountRequestBuilder : BaseRequestBuilder + { + /// + /// Instantiates a new and sets the default values. + /// + /// Path parameters for the request + /// The request adapter to use to execute the requests. + public CountRequestBuilder(Dictionary pathParameters, IRequestAdapter requestAdapter) : base(requestAdapter, "{+baseurl}/documents/by-year/{year}/count", pathParameters) + { + } + /// + /// Instantiates a new and sets the default values. + /// + /// The raw URL to use for the request builder. + /// The request adapter to use to execute the requests. + public CountRequestBuilder(string rawUrl, IRequestAdapter requestAdapter) : base(requestAdapter, "{+baseurl}/documents/by-year/{year}/count", rawUrl) + { + } + /// A + /// Cancellation token to use when cancelling requests + /// Configuration for the request such as headers, query parameters, and middleware options. + /// When receiving a 400 status code +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER +#nullable enable + public async Task GetAsync(Action>? requestConfiguration = default, CancellationToken cancellationToken = default) + { +#nullable restore +#else + public async Task GetAsync(Action> requestConfiguration = default, CancellationToken cancellationToken = default) + { +#endif + var requestInfo = ToGetRequestInformation(requestConfiguration); + var errorMapping = new Dictionary> + { + { "400", global::Marechai.ApiClient.Models.ProblemDetails.CreateFromDiscriminatorValue }, + }; + return await RequestAdapter.SendPrimitiveAsync(requestInfo, errorMapping, cancellationToken).ConfigureAwait(false); + } + /// A + /// Configuration for the request such as headers, query parameters, and middleware options. +#if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER +#nullable enable + public RequestInformation ToGetRequestInformation(Action>? requestConfiguration = default) + { +#nullable restore +#else + public RequestInformation ToGetRequestInformation(Action> requestConfiguration = default) + { +#endif + var requestInfo = new RequestInformation(Method.GET, UrlTemplate, PathParameters); + requestInfo.Configure(requestConfiguration); + requestInfo.Headers.TryAdd("Accept", "text/plain;q=0.9"); + return requestInfo; + } + /// + /// Returns a request builder with the provided arbitrary URL. Using this method means any other path or query parameters are ignored. + /// + /// A + /// The raw URL to use for the request builder. + public global::Marechai.ApiClient.Documents.ByYear.Item.Count.CountRequestBuilder WithUrl(string rawUrl) + { + return new global::Marechai.ApiClient.Documents.ByYear.Item.Count.CountRequestBuilder(rawUrl, RequestAdapter); + } + /// + /// Configuration for the request such as headers, query parameters, and middleware options. + /// + [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 + { + } + } +} +#pragma warning restore CS0618 diff --git a/Marechai.ApiClient/Documents/ByYear/Item/WithYearItemRequestBuilder.cs b/Marechai.ApiClient/Documents/ByYear/Item/WithYearItemRequestBuilder.cs index f5f621e5..1a7a60f4 100644 --- a/Marechai.ApiClient/Documents/ByYear/Item/WithYearItemRequestBuilder.cs +++ b/Marechai.ApiClient/Documents/ByYear/Item/WithYearItemRequestBuilder.cs @@ -1,5 +1,6 @@ // #pragma warning disable CS0618 +using Marechai.ApiClient.Documents.ByYear.Item.Count; using Marechai.ApiClient.Models; using Microsoft.Kiota.Abstractions.Extensions; using Microsoft.Kiota.Abstractions.Serialization; @@ -17,12 +18,17 @@ namespace Marechai.ApiClient.Documents.ByYear.Item [global::System.CodeDom.Compiler.GeneratedCode("Kiota", "1.0.0")] public partial class WithYearItemRequestBuilder : BaseRequestBuilder { + /// The count property + public global::Marechai.ApiClient.Documents.ByYear.Item.Count.CountRequestBuilder Count + { + get => new global::Marechai.ApiClient.Documents.ByYear.Item.Count.CountRequestBuilder(PathParameters, RequestAdapter); + } /// /// Instantiates a new and sets the default values. /// /// Path parameters for the request /// The request adapter to use to execute the requests. - public WithYearItemRequestBuilder(Dictionary pathParameters, IRequestAdapter requestAdapter) : base(requestAdapter, "{+baseurl}/documents/by-year/{year}", pathParameters) + public WithYearItemRequestBuilder(Dictionary pathParameters, IRequestAdapter requestAdapter) : base(requestAdapter, "{+baseurl}/documents/by-year/{year}{?skip*,take*}", pathParameters) { } /// @@ -30,7 +36,7 @@ namespace Marechai.ApiClient.Documents.ByYear.Item /// /// The raw URL to use for the request builder. /// The request adapter to use to execute the requests. - public WithYearItemRequestBuilder(string rawUrl, IRequestAdapter requestAdapter) : base(requestAdapter, "{+baseurl}/documents/by-year/{year}", rawUrl) + public WithYearItemRequestBuilder(string rawUrl, IRequestAdapter requestAdapter) : base(requestAdapter, "{+baseurl}/documents/by-year/{year}{?skip*,take*}", rawUrl) { } /// A List<global::Marechai.ApiClient.Models.DocumentDto> @@ -39,11 +45,11 @@ namespace Marechai.ApiClient.Documents.ByYear.Item /// When receiving a 400 status code #if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER #nullable enable - public async Task?> GetAsync(Action>? requestConfiguration = default, CancellationToken cancellationToken = default) + public async Task?> GetAsync(Action>? requestConfiguration = default, CancellationToken cancellationToken = default) { #nullable restore #else - public async Task> GetAsync(Action> requestConfiguration = default, CancellationToken cancellationToken = default) + public async Task> GetAsync(Action> requestConfiguration = default, CancellationToken cancellationToken = default) { #endif var requestInfo = ToGetRequestInformation(requestConfiguration); @@ -58,11 +64,11 @@ namespace Marechai.ApiClient.Documents.ByYear.Item /// Configuration for the request such as headers, query parameters, and middleware options. #if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER #nullable enable - public RequestInformation ToGetRequestInformation(Action>? requestConfiguration = default) + public RequestInformation ToGetRequestInformation(Action>? requestConfiguration = default) { #nullable restore #else - public RequestInformation ToGetRequestInformation(Action> requestConfiguration = default) + public RequestInformation ToGetRequestInformation(Action> requestConfiguration = default) { #endif var requestInfo = new RequestInformation(Method.GET, UrlTemplate, PathParameters); @@ -79,12 +85,22 @@ namespace Marechai.ApiClient.Documents.ByYear.Item { return new global::Marechai.ApiClient.Documents.ByYear.Item.WithYearItemRequestBuilder(rawUrl, RequestAdapter); } + [global::System.CodeDom.Compiler.GeneratedCode("Kiota", "1.0.0")] + #pragma warning disable CS1591 + public partial class WithYearItemRequestBuilderGetQueryParameters + #pragma warning restore CS1591 + { + [QueryParameter("skip")] + public int? Skip { get; set; } + [QueryParameter("take")] + public int? Take { get; set; } + } /// /// Configuration for the request such as headers, query parameters, and middleware options. /// [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 WithYearItemRequestBuilderGetRequestConfiguration : RequestConfiguration + public partial class WithYearItemRequestBuilderGetRequestConfiguration : RequestConfiguration { } } diff --git a/Marechai.ApiClient/Documents/DocumentsRequestBuilder.cs b/Marechai.ApiClient/Documents/DocumentsRequestBuilder.cs index f4857443..7bee688f 100644 --- a/Marechai.ApiClient/Documents/DocumentsRequestBuilder.cs +++ b/Marechai.ApiClient/Documents/DocumentsRequestBuilder.cs @@ -96,7 +96,7 @@ namespace Marechai.ApiClient.Documents /// /// Path parameters for the request /// The request adapter to use to execute the requests. - public DocumentsRequestBuilder(Dictionary pathParameters, IRequestAdapter requestAdapter) : base(requestAdapter, "{+baseurl}/documents", pathParameters) + public DocumentsRequestBuilder(Dictionary pathParameters, IRequestAdapter requestAdapter) : base(requestAdapter, "{+baseurl}/documents{?skip*,take*}", pathParameters) { } /// @@ -104,7 +104,7 @@ namespace Marechai.ApiClient.Documents /// /// The raw URL to use for the request builder. /// The request adapter to use to execute the requests. - public DocumentsRequestBuilder(string rawUrl, IRequestAdapter requestAdapter) : base(requestAdapter, "{+baseurl}/documents", rawUrl) + public DocumentsRequestBuilder(string rawUrl, IRequestAdapter requestAdapter) : base(requestAdapter, "{+baseurl}/documents{?skip*,take*}", rawUrl) { } /// A List<global::Marechai.ApiClient.Models.DocumentDto> @@ -113,11 +113,11 @@ namespace Marechai.ApiClient.Documents /// When receiving a 400 status code #if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER #nullable enable - public async Task?> GetAsync(Action>? requestConfiguration = default, CancellationToken cancellationToken = default) + public async Task?> GetAsync(Action>? requestConfiguration = default, CancellationToken cancellationToken = default) { #nullable restore #else - public async Task> GetAsync(Action> requestConfiguration = default, CancellationToken cancellationToken = default) + public async Task> GetAsync(Action> requestConfiguration = default, CancellationToken cancellationToken = default) { #endif var requestInfo = ToGetRequestInformation(requestConfiguration); @@ -156,11 +156,11 @@ namespace Marechai.ApiClient.Documents /// Configuration for the request such as headers, query parameters, and middleware options. #if NETSTANDARD2_1_OR_GREATER || NETCOREAPP3_1_OR_GREATER #nullable enable - public RequestInformation ToGetRequestInformation(Action>? requestConfiguration = default) + public RequestInformation ToGetRequestInformation(Action>? requestConfiguration = default) { #nullable restore #else - public RequestInformation ToGetRequestInformation(Action> requestConfiguration = default) + public RequestInformation ToGetRequestInformation(Action> requestConfiguration = default) { #endif var requestInfo = new RequestInformation(Method.GET, UrlTemplate, PathParameters); @@ -196,12 +196,22 @@ namespace Marechai.ApiClient.Documents { return new global::Marechai.ApiClient.Documents.DocumentsRequestBuilder(rawUrl, RequestAdapter); } + [global::System.CodeDom.Compiler.GeneratedCode("Kiota", "1.0.0")] + #pragma warning disable CS1591 + public partial class DocumentsRequestBuilderGetQueryParameters + #pragma warning restore CS1591 + { + [QueryParameter("skip")] + public int? Skip { get; set; } + [QueryParameter("take")] + public int? Take { get; set; } + } /// /// Configuration for the request such as headers, query parameters, and middleware options. /// [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 DocumentsRequestBuilderGetRequestConfiguration : RequestConfiguration + public partial class DocumentsRequestBuilderGetRequestConfiguration : RequestConfiguration { } /// diff --git a/Marechai.ApiClient/kiota-lock.json b/Marechai.ApiClient/kiota-lock.json index d527c501..056b3e85 100644 --- a/Marechai.ApiClient/kiota-lock.json +++ b/Marechai.ApiClient/kiota-lock.json @@ -1,5 +1,5 @@ { - "descriptionHash": "05BE48FD51F874E189911DDDA45748ED43817287B4B417F7FF40DC1B78731E8B001029682B5D38DB20AE5381D0936FBD8DC9B0AD3BFBF5B27C95367171A72346", + "descriptionHash": "88FC8B79411530BB81782CD42A511DADF507AC58B4C0B94B56BCE73F5702466A5E2F08BA5F1B9D11E0E9F640718165360D81522DE7EE2CCC65FDD75987BF30F4", "descriptionLocation": "../../../../../tmp/openapi.json", "lockFileVersion": "1.0.0", "kiotaVersion": "1.31.1", diff --git a/Marechai.Server/Controllers/DocumentsController.cs b/Marechai.Server/Controllers/DocumentsController.cs index 74ea5e80..fbd1c181 100644 --- a/Marechai.Server/Controllers/DocumentsController.cs +++ b/Marechai.Server/Controllers/DocumentsController.cs @@ -27,6 +27,7 @@ using System; using System.Collections.Generic; using System.Linq; using System.Security.Claims; +using System.Threading; using System.Threading.Tasks; using Marechai.Data; using Marechai.Data.Dtos; @@ -108,67 +109,113 @@ public class DocumentsController(MarechaiContext context) : ControllerBase [AllowAnonymous] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest)] - public Task> GetDocumentsByLetterAsync(char c) => context.Documents - .Where(d => - (d.SortTitle != null && EF.Functions.Like(d.SortTitle, $"{c}%")) || - (d.SortTitle == null && EF.Functions.Like(d.Title, $"{c}%"))) - .OrderBy(d => MarechaiContext.NaturalSortKey(d.SortTitle)) - .ThenBy(d => MarechaiContext.NaturalSortKey(d.Title)) - .ThenBy(d => d.Published) - .Select(d => new DocumentDto - { - Id = d.Id, - Title = d.Title, - NativeTitle = d.NativeTitle, - SortTitle = d.SortTitle, - Published = d.Published, - PublishedPrecision = d.PublishedPrecision, - CountryId = d.CountryId, - Country = d.Country.Name - }) - .ToListAsync(); + public Task> GetDocumentsByLetterAsync(char c, [FromQuery] int? skip = null, + [FromQuery] int? take = null, + CancellationToken cancellationToken = default) + { + IQueryable ordered = context.Documents + .Where(d => + (d.SortTitle != null && EF.Functions.Like(d.SortTitle, $"{c}%")) || + (d.SortTitle == null && EF.Functions.Like(d.Title, $"{c}%"))) + .OrderBy(d => MarechaiContext.NaturalSortKey(d.SortTitle)) + .ThenBy(d => MarechaiContext.NaturalSortKey(d.Title)) + .ThenBy(d => d.Published); + + if(skip.HasValue) ordered = ordered.Skip(skip.Value); + if(take.HasValue) ordered = ordered.Take(take.Value); + + return ordered.Select(d => new DocumentDto + { + Id = d.Id, + Title = d.Title, + NativeTitle = d.NativeTitle, + SortTitle = d.SortTitle, + Published = d.Published, + PublishedPrecision = d.PublishedPrecision, + CountryId = d.CountryId, + Country = d.Country.Name + }) + .ToListAsync(cancellationToken); + } + + [HttpGet("by-letter/{c}/count")] + [AllowAnonymous] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + public Task GetDocumentsByLetterCountAsync(char c, CancellationToken cancellationToken = default) => + context.Documents + .Where(d => + (d.SortTitle != null && EF.Functions.Like(d.SortTitle, $"{c}%")) || + (d.SortTitle == null && EF.Functions.Like(d.Title, $"{c}%"))) + .CountAsync(cancellationToken); [HttpGet("by-year/{year:int}")] [AllowAnonymous] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest)] - public Task> GetDocumentsByYearAsync(int year) => context.Documents - .Where(d => d.Published != null && d.Published.Value.Year == year) - .OrderBy(d => MarechaiContext.NaturalSortKey(d.SortTitle)) - .ThenBy(d => MarechaiContext.NaturalSortKey(d.Title)) - .ThenBy(d => d.Published) - .Select(d => new DocumentDto - { - Id = d.Id, - Title = d.Title, - NativeTitle = d.NativeTitle, - SortTitle = d.SortTitle, - Published = d.Published, - PublishedPrecision = d.PublishedPrecision, - CountryId = d.CountryId, - Country = d.Country.Name - }) - .ToListAsync(); + public Task> GetDocumentsByYearAsync(int year, [FromQuery] int? skip = null, + [FromQuery] int? take = null, + CancellationToken cancellationToken = default) + { + IQueryable ordered = context.Documents + .Where(d => d.Published != null && d.Published.Value.Year == year) + .OrderBy(d => MarechaiContext.NaturalSortKey(d.SortTitle)) + .ThenBy(d => MarechaiContext.NaturalSortKey(d.Title)) + .ThenBy(d => d.Published); + + if(skip.HasValue) ordered = ordered.Skip(skip.Value); + if(take.HasValue) ordered = ordered.Take(take.Value); + + return ordered.Select(d => new DocumentDto + { + Id = d.Id, + Title = d.Title, + NativeTitle = d.NativeTitle, + SortTitle = d.SortTitle, + Published = d.Published, + PublishedPrecision = d.PublishedPrecision, + CountryId = d.CountryId, + Country = d.Country.Name + }) + .ToListAsync(cancellationToken); + } + + [HttpGet("by-year/{year:int}/count")] + [AllowAnonymous] + [ProducesResponseType(StatusCodes.Status200OK)] + [ProducesResponseType(StatusCodes.Status400BadRequest)] + public Task GetDocumentsByYearCountAsync(int year, CancellationToken cancellationToken = default) => + context.Documents.Where(d => d.Published != null && d.Published.Value.Year == year) + .CountAsync(cancellationToken); [HttpGet] [AllowAnonymous] [ProducesResponseType(StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status400BadRequest)] - public Task> GetAsync() => context.Documents.OrderBy(b => MarechaiContext.NaturalSortKey(b.SortTitle)) - .ThenBy(b => b.Published) - .ThenBy(b => MarechaiContext.NaturalSortKey(b.Title)) - .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 - }) - .ToListAsync(); + public Task> GetAsync([FromQuery] int? skip = null, [FromQuery] int? take = null, + CancellationToken cancellationToken = default) + { + IQueryable ordered = context.Documents + .OrderBy(b => MarechaiContext.NaturalSortKey(b.SortTitle)) + .ThenBy(b => b.Published) + .ThenBy(b => MarechaiContext.NaturalSortKey(b.Title)); + + if(skip.HasValue) ordered = ordered.Skip(skip.Value); + if(take.HasValue) ordered = ordered.Take(take.Value); + + 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 + }) + .ToListAsync(cancellationToken); + } [HttpGet("{id:long}")] [AllowAnonymous] diff --git a/Marechai/Pages/Documents/Search.razor b/Marechai/Pages/Documents/Search.razor index 65c1f0dc..a2567661 100644 --- a/Marechai/Pages/Documents/Search.razor +++ b/Marechai/Pages/Documents/Search.razor @@ -32,7 +32,7 @@ @inherits OwningComponentBase @inject IStringLocalizer L -@if(_documents is null) +@if(!_initialized) {
@@ -53,23 +53,31 @@ } -@if(_documents?.Count > 0) +@if(_total > 0) { - @string.Format(L["{0} documents found in the database."], _documents.Count) - - @foreach(DocumentDto document in _documents) + @string.Format(L["{0} documents found in the database."], _total) +
+ + + +
+ @document.Title + @if(document.Published.HasValue) + { + @document.Published.Value.Year + } +
+
+
+
+
+ @if(_loadingMore) { - -
- @document.Title - @if(document.Published.HasValue) - { - @document.Published.Value.Year - } -
-
+
+ +
} - +
} else { diff --git a/Marechai/Pages/Documents/Search.razor.cs b/Marechai/Pages/Documents/Search.razor.cs index 15ca071e..fcb67868 100644 --- a/Marechai/Pages/Documents/Search.razor.cs +++ b/Marechai/Pages/Documents/Search.razor.cs @@ -23,20 +23,57 @@ // Copyright © 2003-2026 Natalia Portillo *******************************************************************************/ +/****************************************************************************** +// MARECHAI: Master repository of computing history artifacts information +// ---------------------------------------------------------------------------- +// +// Author(s) : Natalia Portillo +// +// --[ License ] -------------------------------------------------------------- +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU General Public License as +// published by the Free Software Foundation, either version 3 of the +// License, or (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License +// along with this program. If not, see . +// +// ---------------------------------------------------------------------------- +// Copyright © 2003-2026 Natalia Portillo +*******************************************************************************/ + +using System; using System.Collections.Generic; using System.Threading.Tasks; using Marechai.ApiClient.Models; using Microsoft.AspNetCore.Components; +using Microsoft.JSInterop; namespace Marechai.Pages.Documents; -public partial class Search +public partial class Search : IAsyncDisposable { - char? _character; - List _documents; - bool _loaded; - string _lastStartingCharacter; - int? _lastYear; + const int _pageSize = 100; + readonly string _sentinelId = $"documents-search-sentinel-{Guid.NewGuid():N}"; + readonly string _scrollerId = $"documents-search-scroll-{Guid.NewGuid():N}"; + readonly List _documents = []; + char? _character; + bool _initialized; + bool _loadingMore; + bool _observerRegistered; + string _lastStartingCharacter; + int? _lastYear; + int _total; + DotNetObjectReference _selfRef; + + [Inject] + IJSRuntime JS { get; set; } [Parameter] public int? Year { get; set; } @@ -44,38 +81,127 @@ public partial class Search [Parameter] public string StartingCharacter { get; set; } + bool HasMore => _documents.Count < _total; + protected override void OnParametersSet() { if(Year == _lastYear && StartingCharacter == _lastStartingCharacter) return; _lastYear = Year; _lastStartingCharacter = StartingCharacter; - _loaded = false; + _initialized = false; } protected override async Task OnAfterRenderAsync(bool firstRender) { - if(_loaded) return; - - _character = null; - - if(!string.IsNullOrWhiteSpace(StartingCharacter) && StartingCharacter.Length == 1) + if(!_initialized) { - _character = StartingCharacter[0]; + await UnobserveAsync(); - // ToUpper() - if(_character >= 'a' && _character <= 'z') _character -= (char)32; + _character = null; + _total = 0; + _documents.Clear(); - // Check if not letter or number - if(_character < '0' || _character > '9' && _character < 'A' || _character > 'Z') _character = null; + if(!string.IsNullOrWhiteSpace(StartingCharacter) && StartingCharacter.Length == 1) + { + _character = StartingCharacter[0]; + + // ToUpper() + if(_character >= 'a' && _character <= 'z') _character -= (char)32; + + // Check if not letter or number + if(_character < '0' || _character > '9' && _character < 'A' || _character > 'Z') _character = null; + } + + Task countTask = GetCurrentCountAsync(); + Task> firstPageTask = FetchBatchAsync(0, _pageSize); + + await Task.WhenAll(countTask, firstPageTask); + + _total = countTask.Result; + _documents.AddRange(firstPageTask.Result); + _initialized = true; + + StateHasChanged(); + return; } - if(_character.HasValue) _documents = await Service.GetDocumentsByLetterAsync(_character.Value); + if(!_observerRegistered && HasMore) + { + _selfRef ??= DotNetObjectReference.Create(this); - if(Year.HasValue && _documents is null) _documents = await Service.GetDocumentsByYearAsync(Year.Value); + try + { + await JS.InvokeVoidAsync("marechaiInfiniteScroll.observe", _sentinelId, _selfRef, $"#{_scrollerId}", + "200px"); + _observerRegistered = true; + } + catch(JSDisconnectedException) + { + // Circuit gone — nothing to do. + } + } + } - _documents ??= await Service.GetDocumentsAsync(); - _loaded = true; + [JSInvokable] + public async Task OnSentinelVisibleAsync() + { + if(_loadingMore || !HasMore) return; + + _loadingMore = true; StateHasChanged(); + + try + { + List next = await FetchBatchAsync(_documents.Count, _pageSize); + _documents.AddRange(next); + + if(!HasMore) await UnobserveAsync(); + } + finally + { + _loadingMore = false; + StateHasChanged(); + } + } + + Task GetCurrentCountAsync() + { + if(_character.HasValue) return Service.GetDocumentsByLetterCountAsync(_character.Value); + + if(Year.HasValue) return Service.GetDocumentsByYearCountAsync(Year.Value); + + return Service.GetDocumentsCountAsync(); + } + + Task> FetchBatchAsync(int skip, int take) + { + if(_character.HasValue) return Service.GetDocumentsByLetterAsync(_character.Value, skip, take); + + if(Year.HasValue) return Service.GetDocumentsByYearAsync(Year.Value, skip, take); + + return Service.GetDocumentsAsync(skip, take); + } + + async Task UnobserveAsync() + { + if(!_observerRegistered) return; + + try + { + await JS.InvokeVoidAsync("marechaiInfiniteScroll.unobserve", _sentinelId); + } + catch(JSDisconnectedException) + { + // Circuit gone; nothing to do. + } + + _observerRegistered = false; + } + + public async ValueTask DisposeAsync() + { + await UnobserveAsync(); + _selfRef?.Dispose(); } } diff --git a/Marechai/Services/DocumentsService.cs b/Marechai/Services/DocumentsService.cs index 84fe5530..41bd27aa 100644 --- a/Marechai/Services/DocumentsService.cs +++ b/Marechai/Services/DocumentsService.cs @@ -25,6 +25,7 @@ using System; using System.Collections.Generic; +using System.Threading; using System.Threading.Tasks; using Marechai.ApiClient.Models; using Microsoft.Kiota.Abstractions; @@ -103,11 +104,16 @@ public class DocumentsService(Marechai.ApiClient.Client client) } } - public async Task> GetDocumentsByLetterAsync(char c) + public async Task> GetDocumentsByLetterAsync(char c, int? skip = null, int? take = null, + CancellationToken cancellationToken = default) { try { - List documents = await client.Documents.ByLetter[c.ToString()].GetAsync(); + List documents = await client.Documents.ByLetter[c.ToString()].GetAsync(rc => + { + if(skip.HasValue) rc.QueryParameters.Skip = skip.Value; + if(take.HasValue) rc.QueryParameters.Take = take.Value; + }, cancellationToken); return documents ?? []; } @@ -117,11 +123,30 @@ public class DocumentsService(Marechai.ApiClient.Client client) } } - public async Task> GetDocumentsByYearAsync(int year) + public async Task GetDocumentsByLetterCountAsync(char c, CancellationToken cancellationToken = default) { try { - List documents = await client.Documents.ByYear[year].GetAsync(); + int? count = await client.Documents.ByLetter[c.ToString()].Count.GetAsync(cancellationToken: cancellationToken); + + return count ?? 0; + } + catch + { + return 0; + } + } + + public async Task> GetDocumentsByYearAsync(int year, int? skip = null, int? take = null, + CancellationToken cancellationToken = default) + { + try + { + List documents = await client.Documents.ByYear[year].GetAsync(rc => + { + if(skip.HasValue) rc.QueryParameters.Skip = skip.Value; + if(take.HasValue) rc.QueryParameters.Take = take.Value; + }, cancellationToken); return documents ?? []; } @@ -131,11 +156,30 @@ public class DocumentsService(Marechai.ApiClient.Client client) } } - public async Task> GetDocumentsAsync() + public async Task GetDocumentsByYearCountAsync(int year, CancellationToken cancellationToken = default) { try { - List documents = await client.Documents.GetAsync(); + int? count = await client.Documents.ByYear[year].Count.GetAsync(cancellationToken: cancellationToken); + + return count ?? 0; + } + catch + { + return 0; + } + } + + public async Task> GetDocumentsAsync(int? skip = null, int? take = null, + CancellationToken cancellationToken = default) + { + try + { + List documents = await client.Documents.GetAsync(rc => + { + if(skip.HasValue) rc.QueryParameters.Skip = skip.Value; + if(take.HasValue) rc.QueryParameters.Take = take.Value; + }, cancellationToken); return documents ?? []; }