diff --git a/.gitignore b/.gitignore index 0d410d770..937ca0665 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,4 @@ wwwroot/ node_modules/ *.user .pnpm-debug.log +*.orig diff --git a/Lombiq.DataTables.Samples/Controllers/SampleController.cs b/Lombiq.DataTables.Samples/Controllers/SampleController.cs index 30e1057a7..34ece028e 100644 --- a/Lombiq.DataTables.Samples/Controllers/SampleController.cs +++ b/Lombiq.DataTables.Samples/Controllers/SampleController.cs @@ -9,7 +9,7 @@ namespace Lombiq.DataTables.Samples.Controllers; -public class SampleController : Controller +public sealed class SampleController : Controller { private readonly ISession _session; diff --git a/Lombiq.DataTables.Samples/Lombiq.DataTables.Samples.csproj b/Lombiq.DataTables.Samples/Lombiq.DataTables.Samples.csproj index 29c04b190..ae6bd4038 100644 --- a/Lombiq.DataTables.Samples/Lombiq.DataTables.Samples.csproj +++ b/Lombiq.DataTables.Samples/Lombiq.DataTables.Samples.csproj @@ -33,8 +33,8 @@ - - + + diff --git a/Lombiq.DataTables.Samples/Migrations/EmployeeDataTableMigrations.cs b/Lombiq.DataTables.Samples/Migrations/EmployeeDataTableMigrations.cs index 419678ff4..b71c15227 100644 --- a/Lombiq.DataTables.Samples/Migrations/EmployeeDataTableMigrations.cs +++ b/Lombiq.DataTables.Samples/Migrations/EmployeeDataTableMigrations.cs @@ -7,7 +7,7 @@ namespace Lombiq.DataTables.Samples.Migrations; // The migration for the data table index must inherit from IndexDataMigration so they can all be registered // together in Startup in a tightly coupled fashion that reduces the chance of mistakes. -public class EmployeeDataTableMigrations : IndexDataMigration +public sealed class EmployeeDataTableMigrations : IndexDataMigration { protected override void CreateIndex(ICreateTableCommand table) => table diff --git a/Lombiq.DataTables.Samples/Migrations/EmployeeMigrations.cs b/Lombiq.DataTables.Samples/Migrations/EmployeeMigrations.cs index 5a53c3530..343262d36 100644 --- a/Lombiq.DataTables.Samples/Migrations/EmployeeMigrations.cs +++ b/Lombiq.DataTables.Samples/Migrations/EmployeeMigrations.cs @@ -11,7 +11,7 @@ namespace Lombiq.DataTables.Samples.Migrations; // Just the bare minimum to set up the content type for storing the sample data. -public class EmployeeMigrations : DataMigration +public sealed class EmployeeMigrations : DataMigration { private readonly IContentDefinitionManager _contentDefinitionManager; diff --git a/Lombiq.DataTables.Samples/Startup.cs b/Lombiq.DataTables.Samples/Startup.cs index a19b8d819..ad670dc7a 100644 --- a/Lombiq.DataTables.Samples/Startup.cs +++ b/Lombiq.DataTables.Samples/Startup.cs @@ -10,7 +10,7 @@ namespace Lombiq.DataTables.Samples; -public class Startup : StartupBase +public sealed class Startup : StartupBase { public override void ConfigureServices(IServiceCollection services) { @@ -28,7 +28,7 @@ public override void ConfigureServices(IServiceCollection services) EmployeeDataTableMigrations, SampleIndexBasedDataTableDataProvider>(); - services.AddScoped(); + services.AddNavigationProvider(); } } diff --git a/Lombiq.DataTables/Controllers/Api/ChildRowsController.cs b/Lombiq.DataTables/Controllers/Api/ChildRowsController.cs index a288eda7e..d33a541a5 100644 --- a/Lombiq.DataTables/Controllers/Api/ChildRowsController.cs +++ b/Lombiq.DataTables/Controllers/Api/ChildRowsController.cs @@ -8,7 +8,7 @@ namespace Lombiq.DataTables.Controllers.Api; -public class ChildRowsController : Controller +public sealed class ChildRowsController : Controller { private readonly IEnumerable _dataTableDataProviderAccessor; private readonly IAuthorizationService _authorizationService; diff --git a/Lombiq.DataTables/Controllers/Api/RowsController.cs b/Lombiq.DataTables/Controllers/Api/RowsController.cs index 789d2b90c..d12ed720a 100644 --- a/Lombiq.DataTables/Controllers/Api/RowsController.cs +++ b/Lombiq.DataTables/Controllers/Api/RowsController.cs @@ -1,16 +1,16 @@ using Lombiq.DataTables.Models; using Lombiq.DataTables.Services; +using Lombiq.HelpfulLibraries.AspNetCore.Mvc; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Localization; -using Newtonsoft.Json; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; namespace Lombiq.DataTables.Controllers.Api; -public class RowsController : Controller +public sealed class RowsController : Controller { private readonly IEnumerable _dataTableDataProviderAccessor; private readonly Dictionary _exportServices; @@ -32,7 +32,7 @@ public RowsController( /// /// Gets the current table view's rows. /// - /// The request to fulfill serialized as JSON. + /// The request to fulfill serialized as JSON. /// The response for this API call. /// /// @@ -53,12 +53,11 @@ public RowsController( /// [IgnoreAntiforgeryToken] [HttpGet] - public async Task> Get(string requestJson) + public async Task> Get([FromJsonQueryString(Name = "requestJson")] DataTableDataRequest request) { - if (string.IsNullOrEmpty(requestJson)) ModelState.AddModelError(nameof(requestJson), "Controller parameter is missing."); + if (request == null) return BadRequest(); if (!ModelState.IsValid) return BadRequest(ModelState); - var request = JsonConvert.DeserializeObject(requestJson); var dataProvider = _dataTableDataProviderAccessor.GetDataProvider(request.DataProvider); if (dataProvider == null) { @@ -87,13 +86,12 @@ public async Task> Get(string requestJson) } public async Task> Export( - string requestJson, + [FromJsonQueryString(Name = "requestJson")] DataTableDataRequest request, string name = null, bool exportAll = true) { if (!ModelState.IsValid) return BadRequest(ModelState); - var request = JsonConvert.DeserializeObject(requestJson); if (exportAll) { request.Start = 0; diff --git a/Lombiq.DataTables/Controllers/TableController.cs b/Lombiq.DataTables/Controllers/TableController.cs index 331e69db0..dc774948f 100644 --- a/Lombiq.DataTables/Controllers/TableController.cs +++ b/Lombiq.DataTables/Controllers/TableController.cs @@ -1,31 +1,31 @@ using Lombiq.DataTables.Services; using Lombiq.DataTables.ViewModels; -using Lombiq.HelpfulLibraries.OrchardCore.Mvc; using Microsoft.AspNetCore.Mvc; -using Newtonsoft.Json.Linq; using OrchardCore.Admin; using System.Collections.Generic; using System.Linq; +using System.Text.Json.Nodes; using System.Threading.Tasks; namespace Lombiq.DataTables.Controllers; -[Admin] -public class TableController : Controller +public sealed class TableController : Controller { private readonly IEnumerable _dataTableDataProviders; public TableController(IEnumerable dataTableDataProviders) => _dataTableDataProviders = dataTableDataProviders; - [AdminRoute("DataTable/{providerName}/{queryId?}")] + [Admin("DataTable/{providerName}/{queryId?}")] public async Task Get(string providerName, string queryId = null, bool paging = true, bool viewAction = false) { if (!ModelState.IsValid) return BadRequest(ModelState); var provider = _dataTableDataProviders.Single(provider => provider.Name == providerName); if (string.IsNullOrEmpty(queryId)) queryId = providerName; - var definition = new DataTableDefinitionViewModel(JObject.FromObject(new { paging, viewAction })) + + var additionalDatatableOptions = JObject.FromObject(new { paging, viewAction })!.AsObject(); + var definition = new DataTableDefinitionViewModel(additionalDatatableOptions) { DataProvider = providerName, QueryId = queryId, diff --git a/Lombiq.DataTables/Extensions/DataTableDataProviderExtensions.cs b/Lombiq.DataTables/Extensions/DataTableDataProviderExtensions.cs index 92ec4d902..ef92521c4 100644 --- a/Lombiq.DataTables/Extensions/DataTableDataProviderExtensions.cs +++ b/Lombiq.DataTables/Extensions/DataTableDataProviderExtensions.cs @@ -5,13 +5,13 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.Localization; -using Newtonsoft.Json; using OrchardCore.Mvc.Core.Utilities; using OrchardCore.Security.Permissions; using System; using System.Collections.Generic; using System.Linq; using System.Security.Claims; +using System.Text.Json; using System.Threading.Tasks; using static Lombiq.DataTables.Constants.SortingDirection; @@ -167,7 +167,7 @@ public static string GetCustomActionsJson( IHttpContextAccessor hca, LinkGenerator linkGenerator, IStringLocalizer actionsStringLocalizer) => - JsonConvert.SerializeObject(GetCustomActions( + JsonSerializer.Serialize(GetCustomActions( dataProvider, contentItemId, canDelete, diff --git a/Lombiq.DataTables/Extensions/JsonNodeExtensions.cs b/Lombiq.DataTables/Extensions/JsonNodeExtensions.cs new file mode 100644 index 000000000..3c6a3b5ee --- /dev/null +++ b/Lombiq.DataTables/Extensions/JsonNodeExtensions.cs @@ -0,0 +1,11 @@ +namespace System.Text.Json.Nodes; + +public static class JsonNodeExtensions +{ + /// + /// Checks if the provided is object and has a Type property, and if its value + /// matches . + /// + public static bool HasMatchingTypeProperty(this JsonNode node) => + node is JsonObject jsonObject && jsonObject["Type"]?.ToString() == typeof(T).Name; +} diff --git a/Lombiq.DataTables/Liquid/ActionsLiquidFilter.cs b/Lombiq.DataTables/Liquid/ActionsLiquidFilter.cs index 3a4928467..4c4b5289e 100644 --- a/Lombiq.DataTables/Liquid/ActionsLiquidFilter.cs +++ b/Lombiq.DataTables/Liquid/ActionsLiquidFilter.cs @@ -5,13 +5,13 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.Localization; -using Newtonsoft.Json.Linq; using OrchardCore.ContentManagement; using OrchardCore.DisplayManagement; using OrchardCore.Liquid; using System; using System.IO; using System.Text.Encodings.Web; +using System.Text.Json.Nodes; using System.Threading.Tasks; namespace Lombiq.DataTables.Liquid; @@ -71,7 +71,7 @@ public ValueTask ProcessAsync(FluidValue input, FilterArguments argu FluidValues.Object => input!.ToObjectValue() switch { ActionsDescriptor model => FromObjectAsync(model, title, returnUrl), - JToken jToken => FromObjectAsync(jToken.ToObject(), title, returnUrl), + JsonNode jsonNode => FromObjectAsync(jsonNode.ToObject(), title, returnUrl), { } unknown => throw GetException(unknown), _ => throw new ArgumentNullException(nameof(input)), }, diff --git a/Lombiq.DataTables/Lombiq.DataTables.csproj b/Lombiq.DataTables/Lombiq.DataTables.csproj index a1a6f54c9..73960064b 100644 --- a/Lombiq.DataTables/Lombiq.DataTables.csproj +++ b/Lombiq.DataTables/Lombiq.DataTables.csproj @@ -38,11 +38,10 @@ - - - - + + + @@ -52,8 +51,8 @@ - - + + diff --git a/Lombiq.DataTables/LombiqTests/Services/TestingFilter.cs b/Lombiq.DataTables/LombiqTests/Services/TestingFilter.cs index 9db13d7be..c2b39ccd5 100644 --- a/Lombiq.DataTables/LombiqTests/Services/TestingFilter.cs +++ b/Lombiq.DataTables/LombiqTests/Services/TestingFilter.cs @@ -7,7 +7,7 @@ namespace Lombiq.DataTables.LombiqTests.Services; -public class TestingFilter : IAsyncResultFilter +public sealed class TestingFilter : IAsyncResultFilter { private readonly ILayoutAccessor _layoutAccessor; private readonly IShapeFactory _shapeFactory; diff --git a/Lombiq.DataTables/LombiqTests/Startup.cs b/Lombiq.DataTables/LombiqTests/Startup.cs index 4ad9a8946..171f4f733 100644 --- a/Lombiq.DataTables/LombiqTests/Startup.cs +++ b/Lombiq.DataTables/LombiqTests/Startup.cs @@ -6,7 +6,7 @@ namespace Lombiq.DataTables.LombiqTests; [RequireFeatures("Lombiq.Tests.UI.Shortcuts")] -public class Startup : StartupBase +public sealed class Startup : StartupBase { public override void ConfigureServices(IServiceCollection services) => services.Configure(options => options.Filters.Add(typeof(TestingFilter))); diff --git a/Lombiq.DataTables/Migrations/ColumnsDefinitionMigrations.cs b/Lombiq.DataTables/Migrations/ColumnsDefinitionMigrations.cs index f9cac6604..d6f449541 100644 --- a/Lombiq.DataTables/Migrations/ColumnsDefinitionMigrations.cs +++ b/Lombiq.DataTables/Migrations/ColumnsDefinitionMigrations.cs @@ -6,7 +6,7 @@ namespace Lombiq.DataTables.Migrations; -public class ColumnsDefinitionMigrations : DataMigration +public sealed class ColumnsDefinitionMigrations : DataMigration { private readonly IContentDefinitionManager _contentDefinitionManager; diff --git a/Lombiq.DataTables/Models/DataTableChildRowResponse.cs b/Lombiq.DataTables/Models/DataTableChildRowResponse.cs index 39858db81..b27ae234f 100644 --- a/Lombiq.DataTables/Models/DataTableChildRowResponse.cs +++ b/Lombiq.DataTables/Models/DataTableChildRowResponse.cs @@ -1,12 +1,13 @@ -using Newtonsoft.Json; -using Newtonsoft.Json.Serialization; +using System.Text.Json.Serialization; namespace Lombiq.DataTables.Models; -[JsonObject(NamingStrategyType = typeof(CamelCaseNamingStrategy))] public class DataTableChildRowResponse { + [JsonPropertyName("error")] public string Error { get; set; } + + [JsonPropertyName("content")] public string Content { get; set; } public static DataTableChildRowResponse ErrorResult(string errorText) => new() { Error = errorText }; diff --git a/Lombiq.DataTables/Models/DataTableColumn.cs b/Lombiq.DataTables/Models/DataTableColumn.cs index 3f68b4e8f..6cd7ee9fc 100644 --- a/Lombiq.DataTables/Models/DataTableColumn.cs +++ b/Lombiq.DataTables/Models/DataTableColumn.cs @@ -1,14 +1,21 @@ -using Newtonsoft.Json; -using Newtonsoft.Json.Serialization; +using System.Text.Json.Serialization; namespace Lombiq.DataTables.Models; -[JsonObject(NamingStrategyType = typeof(CamelCaseNamingStrategy))] public class DataTableColumn { + [JsonPropertyName("data")] public string Data { get; set; } + + [JsonPropertyName("name")] public string Name { get; set; } + + [JsonPropertyName("searchable")] public bool Searchable { get; set; } + + [JsonPropertyName("orderable")] public bool Orderable { get; set; } + + [JsonPropertyName("search")] public DataTableSearchParameters Search { get; set; } } diff --git a/Lombiq.DataTables/Models/DataTableDataRequest.cs b/Lombiq.DataTables/Models/DataTableDataRequest.cs index 97634a552..b44565680 100644 --- a/Lombiq.DataTables/Models/DataTableDataRequest.cs +++ b/Lombiq.DataTables/Models/DataTableDataRequest.cs @@ -1,20 +1,28 @@ -using Newtonsoft.Json; -using Newtonsoft.Json.Serialization; using System.Collections.Generic; using System.Linq; +using System.Text.Json.Serialization; namespace Lombiq.DataTables.Models; -[JsonObject(NamingStrategyType = typeof(CamelCaseNamingStrategy))] public class DataTableDataRequest { public string QueryId { get; set; } + public string DataProvider { get; set; } + + [JsonRequired] public int Draw { get; set; } + + [JsonRequired] public int Start { get; set; } + + [JsonRequired] public int Length { get; set; } + public IEnumerable ColumnFilters { get; set; } + public DataTableSearchParameters Search { get; set; } + public IEnumerable Order { get; set; } public bool HasSearch => !string.IsNullOrWhiteSpace(Search?.Value); diff --git a/Lombiq.DataTables/Models/DataTableDataResponse.cs b/Lombiq.DataTables/Models/DataTableDataResponse.cs index f2aefd6cc..b5efec007 100644 --- a/Lombiq.DataTables/Models/DataTableDataResponse.cs +++ b/Lombiq.DataTables/Models/DataTableDataResponse.cs @@ -1,10 +1,8 @@ -using Newtonsoft.Json; -using Newtonsoft.Json.Serialization; using System.Collections.Generic; +using System.Text.Json.Serialization; namespace Lombiq.DataTables.Models; -[JsonObject(NamingStrategyType = typeof(CamelCaseNamingStrategy))] public class DataTableDataResponse { /// @@ -14,28 +12,33 @@ public class DataTableDataResponse /// /// For internal use only. It's overwritten during normal use. /// - [JsonProperty] + [JsonPropertyName("draw")] + [JsonInclude] internal int Draw { get; set; } /// /// Gets or sets the extra informational field that shows the actual total if filtering (such as keyword /// search) is used. When not filtering it must be the same as . /// + [JsonPropertyName("recordsTotal")] public int RecordsTotal { get; set; } /// /// Gets or sets the total number of results; used for paging. /// + [JsonPropertyName("recordsFiltered")] public int RecordsFiltered { get; set; } /// /// Gets or sets the table contents of the current page. /// + [JsonPropertyName("data")] public IEnumerable Data { get; set; } /// /// Gets or sets the user-facing error message in case something went wrong. /// + [JsonPropertyName("error")] public string Error { get; set; } /// diff --git a/Lombiq.DataTables/Models/DataTableOrder.cs b/Lombiq.DataTables/Models/DataTableOrder.cs index 8184a3700..dc2891e42 100644 --- a/Lombiq.DataTables/Models/DataTableOrder.cs +++ b/Lombiq.DataTables/Models/DataTableOrder.cs @@ -1,15 +1,25 @@ using Lombiq.DataTables.Constants; -using Newtonsoft.Json; -using Newtonsoft.Json.Serialization; +using System.Diagnostics.CodeAnalysis; +using System.Text.Json.Serialization; namespace Lombiq.DataTables.Models; -[JsonObject(NamingStrategyType = typeof(CamelCaseNamingStrategy))] public class DataTableOrder { public string Column { get; set; } + + [JsonIgnore] public SortingDirection Direction { get; set; } + [JsonPropertyName("direction")] + [JsonInclude] + [SuppressMessage("CodeQuality", "IDE0051:Remove unused private members", Justification = "It's used for JSON conversion.")] + private string DirectionString + { + get => IsAscending ? "ascending" : "descending"; + set => Direction = value == "descending" ? SortingDirection.Descending : SortingDirection.Ascending; + } + [JsonIgnore] public bool IsAscending => Direction == SortingDirection.Ascending; } diff --git a/Lombiq.DataTables/Models/DataTableRow.cs b/Lombiq.DataTables/Models/DataTableRow.cs index 6bdde4cd7..c064e5fbd 100644 --- a/Lombiq.DataTables/Models/DataTableRow.cs +++ b/Lombiq.DataTables/Models/DataTableRow.cs @@ -1,36 +1,52 @@ -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using Newtonsoft.Json.Serialization; using System.Collections.Generic; using System.Linq; +using System.Text.Json.Nodes; +using System.Text.Json.Serialization; namespace Lombiq.DataTables.Models; -[JsonObject(NamingStrategyType = typeof(CamelCaseNamingStrategy))] public class DataTableRow { [JsonExtensionData] - internal IDictionary ValuesDictionary { get; set; } + [JsonInclude] + [JsonPropertyName("valuesDictionary")] + internal IDictionary ValuesDictionary { get; set; } = new Dictionary(); + [JsonPropertyName("id")] public int Id { get; set; } public string this[string name] { - get => ValuesDictionary.TryGetValue(name, out var value) ? value.Value() : null; + get => ValuesDictionary.GetMaybe(name)?.ToString(); set => ValuesDictionary[name] = value; } - public DataTableRow() => ValuesDictionary = new Dictionary(); + public DataTableRow() { } - public DataTableRow(int id, IDictionary valuesDictionary) + public DataTableRow(int id, IDictionary valuesDictionary) { Id = id; - ValuesDictionary = valuesDictionary; + + if (valuesDictionary != null) + { + foreach (var (key, value) in valuesDictionary) + { + ValuesDictionary[key] = value; + } + } } public IEnumerable GetValues() => - ValuesDictionary.Values.Select(value => value.Value()); + ValuesDictionary.Values.Select(value => value.ToString()); public IEnumerable GetValuesOrderedByColumns(IEnumerable columnDefinitions) => columnDefinitions.Select(columnDefinition => this[columnDefinition.Name] ?? string.Empty); + + internal JsonNode GetValueAsJsonNode(string name) => + ValuesDictionary.GetMaybe(name) switch + { + JsonNode node => node, + { } otherValue => JObject.FromObject(otherValue), + null => null, + }; } diff --git a/Lombiq.DataTables/Models/DataTableSearchParameters.cs b/Lombiq.DataTables/Models/DataTableSearchParameters.cs index a43b9cd18..598c273fd 100644 --- a/Lombiq.DataTables/Models/DataTableSearchParameters.cs +++ b/Lombiq.DataTables/Models/DataTableSearchParameters.cs @@ -1,13 +1,12 @@ -using Newtonsoft.Json; -using Newtonsoft.Json.Serialization; +using System.Text.Json.Serialization; namespace Lombiq.DataTables.Models; -[JsonObject(NamingStrategyType = typeof(CamelCaseNamingStrategy))] public class DataTableSearchParameters { + [JsonPropertyName("value")] public string Value { get; set; } - [JsonProperty(PropertyName = "regex")] - public bool IsRegex { get; set; } + [JsonPropertyName("regex")] + public bool? IsRegex { get; set; } } diff --git a/Lombiq.DataTables/Models/ExportDate.cs b/Lombiq.DataTables/Models/ExportDate.cs index b8b82671a..f24d9b7e9 100644 --- a/Lombiq.DataTables/Models/ExportDate.cs +++ b/Lombiq.DataTables/Models/ExportDate.cs @@ -1,7 +1,7 @@ -using Newtonsoft.Json.Linq; using NodaTime; using System; using System.Diagnostics.CodeAnalysis; +using System.Text.Json.Nodes; namespace Lombiq.DataTables.Models; @@ -21,10 +21,11 @@ public class ExportDate public int Day { get; set; } public string ExcelFormat { get; set; } - public static bool IsInstance(JObject jObject) => - jObject[nameof(Type)]?.ToString() == nameof(ExportDate); + public static bool IsInstance(JsonObject jsonObject) => + jsonObject.HasMatchingTypeProperty(); - public static string GetText(JObject jObject) => ((LocalDate)jObject.ToObject()).ToShortDateString(); + public static string GetText(JsonObject jsonObject) => + ((LocalDate)jsonObject.ToObject()).ToShortDateString(); public static implicit operator ExportDate(LocalDate localDate) => new() diff --git a/Lombiq.DataTables/Models/ExportLink.cs b/Lombiq.DataTables/Models/ExportLink.cs index e03388470..14028a0aa 100644 --- a/Lombiq.DataTables/Models/ExportLink.cs +++ b/Lombiq.DataTables/Models/ExportLink.cs @@ -1,9 +1,9 @@ #nullable enable -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; +using System.Text.Json.Nodes; +using System.Text.Json.Serialization; namespace Lombiq.DataTables.Models; @@ -18,12 +18,12 @@ public class ExportLink #pragma warning restore IDE0079 // Remove unnecessary suppression public string Type => nameof(ExportLink); public string Url { get; set; } - public JToken Text { get; set; } + public JsonNode Text { get; set; } - [JsonProperty] + [JsonInclude] public IDictionary Attributes { get; internal set; } = new Dictionary(); - public ExportLink(string url, JToken text, IDictionary? attributes = null) + public ExportLink(string url, JsonNode text, IDictionary? attributes = null) { Url = url; Text = text; @@ -32,8 +32,8 @@ public ExportLink(string url, JToken text, IDictionary? attribut public override string ToString() => Text.ToString(); - public static bool IsInstance(JObject jObject) => - jObject[nameof(Type)]?.ToString() == nameof(ExportLink); + public static bool IsInstance(JsonObject jsonObject) => + jsonObject.HasMatchingTypeProperty(); - public static string? GetText(JObject jObject) => jObject[nameof(Text)]?.ToString(); + public static string? GetText(JsonObject jObject) => jObject[nameof(Text)]?.ToString(); } diff --git a/Lombiq.DataTables/Models/VueModel.cs b/Lombiq.DataTables/Models/VueModel.cs index 44fbcf852..eb9e7384d 100644 --- a/Lombiq.DataTables/Models/VueModel.cs +++ b/Lombiq.DataTables/Models/VueModel.cs @@ -1,14 +1,15 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Localization; using Microsoft.AspNetCore.Mvc.Routing; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; using OrchardCore.ContentManagement; using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; using System.Globalization; using System.Linq; +using System.Text.Json; +using System.Text.Json.Nodes; +using System.Text.Json.Serialization; using System.Threading.Tasks; namespace Lombiq.DataTables.Models; @@ -20,44 +21,51 @@ public class VueModel /// component property client-side. Even then, you need to provide either this or if the /// column is meant to be . /// - [JsonProperty("text", NullValueHandling = NullValueHandling.Ignore)] + [JsonPropertyName("text")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string Text { get; set; } /// /// Gets or sets the HTML content to be rendered inside the cell. When used and are ignored. /// - [JsonProperty("html", NullValueHandling = NullValueHandling.Ignore)] + [JsonPropertyName("html")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string Html { get; set; } /// /// Gets or sets the value used for sorting. If or empty, the value of is /// used for sorting. /// - [JsonProperty("sort", NullValueHandling = NullValueHandling.Ignore)] + [JsonPropertyName("sort")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public object Sort { get; set; } /// /// Gets or sets the URL to be used in the href attributes. When this is used is ignored. /// - [JsonProperty("href", NullValueHandling = NullValueHandling.Ignore)] + [JsonPropertyName("href")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public string Href { get; set; } - [JsonProperty("multipleLinks", NullValueHandling = NullValueHandling.Ignore)] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("multipleLinks")] public IEnumerable MultipleLinks { get; set; } /// /// Gets or sets the Bootstrap badge class of the cell. To be used along with and optionally . /// - [JsonProperty("badge", NullValueHandling = NullValueHandling.Ignore)] + [JsonPropertyName("badge")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public object Badge { get; set; } /// /// Gets or sets the data used as extra information to be consumed by special event so the contents can be /// updated with JavaScript on client side before each render. /// - [JsonProperty("special", NullValueHandling = NullValueHandling.Ignore)] + [JsonPropertyName("special")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] public object Special { get; set; } /// @@ -75,8 +83,10 @@ public class VueModel [JsonIgnore] public IEnumerable HiddenInputs { get; set; } - [JsonProperty("hiddenInput", NullValueHandling = NullValueHandling.Ignore)] - [SuppressMessage("CodeQuality", "IDE0051:Remove unused private members", Justification = "It's used by JSON.NET.")] + [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] + [JsonPropertyName("hiddenInput")] + [JsonInclude] + [SuppressMessage("CodeQuality", "IDE0051:Remove unused private members", Justification = "It's used for JSON conversion.")] private object HiddenInputSerialize { get => (object)HiddenInput ?? HiddenInputs; @@ -84,10 +94,10 @@ private object HiddenInputSerialize { switch (value) { - case JArray hiddenInputs: + case JsonArray hiddenInputs: HiddenInputs = hiddenInputs.ToObject>(); break; - case JObject hiddenInput: + case JsonObject hiddenInput: HiddenInput = hiddenInput.ToObject(); break; case null: @@ -126,14 +136,14 @@ public VueModel SetHiddenInput(string name, string value) return this; } - public static async Task TableToJsonAsync( + public static async Task TableToJsonAsync( IEnumerable collection, Func>> select) { var rows = (await collection.AwaitEachAsync(select)) .Select((row, rowIndex) => { - var castRow = row.ToDictionary(pair => pair.Key, pair => JToken.FromObject(pair.Value)); + var castRow = row.ToDictionary(pair => pair.Key, pair => JsonSerializer.SerializeToNode(pair.Value)); castRow["$rowIndex"] = rowIndex; return castRow; }); @@ -153,19 +163,19 @@ public static IDictionary CreateTextForIcbinDataTable(IHtmlLocal public class HiddenInputValue { - [JsonProperty("name")] + [JsonPropertyName("name")] public string Name { get; set; } - [JsonProperty("value")] + [JsonPropertyName("value")] public string Value { get; set; } } public class MultipleHrefValue { - [JsonProperty("url")] + [JsonPropertyName("url")] public string Url { get; set; } - [JsonProperty("text")] + [JsonPropertyName("text")] public string Text { get; set; } } diff --git a/Lombiq.DataTables/Models/VueModelCheckbox.cs b/Lombiq.DataTables/Models/VueModelCheckbox.cs index 1d3192292..7361ff5b7 100644 --- a/Lombiq.DataTables/Models/VueModelCheckbox.cs +++ b/Lombiq.DataTables/Models/VueModelCheckbox.cs @@ -1,5 +1,5 @@ -using Newtonsoft.Json; using System.Diagnostics.CodeAnalysis; +using System.Text.Json.Serialization; namespace Lombiq.DataTables.Models; @@ -13,18 +13,18 @@ public class VueModelCheckbox "CA1822:Mark members as static", Justification = "It's necessary to be instance-level for JSON serialization.")] #pragma warning restore IDE0079 // Remove unnecessary suppression - [JsonProperty("type")] + [JsonPropertyName("type")] public string Type => "checkbox"; - [JsonProperty("name")] + [JsonPropertyName("name")] public string Name { get; set; } - [JsonProperty("text")] + [JsonPropertyName("text")] public string Text { get; set; } - [JsonProperty("value")] + [JsonPropertyName("value")] public bool? Value { get; set; } - [JsonProperty("classes")] + [JsonPropertyName("classes")] public string Classes { get; set; } = string.Empty; } diff --git a/Lombiq.DataTables/Services/DataTableDataProviderBase.cs b/Lombiq.DataTables/Services/DataTableDataProviderBase.cs index bd86a5b37..7dab61c45 100644 --- a/Lombiq.DataTables/Services/DataTableDataProviderBase.cs +++ b/Lombiq.DataTables/Services/DataTableDataProviderBase.cs @@ -4,7 +4,6 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.Localization; -using Newtonsoft.Json.Linq; using OrchardCore.ContentManagement; using OrchardCore.DisplayManagement; using OrchardCore.Liquid; @@ -13,6 +12,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Text.Json.Nodes; using System.Text.RegularExpressions; using System.Threading.Tasks; using YesSql; @@ -85,17 +85,12 @@ protected virtual DataTableColumnsDefinition GetColumnsDefinitionInner(string qu $"You must override {nameof(GetColumnsDefinitionAsync)} or {nameof(GetColumnsDefinitionInner)}."); protected static IEnumerable SubstituteByColumn( - IEnumerable json, + IEnumerable json, IList columns) => json.Select((result, index) => new DataTableRow(index, columns - .Select(column => (column.Name, column.Regex, Token: result.SelectToken(column.Name, errorWhenNoMatch: false))) - .ToDictionary( - cell => cell.Name, - cell => cell.Regex is { } regex - ? new JValue(cell.Token?.ToString().RegexReplace(regex.From, regex.To, RegexOptions.Singleline) // #spell-check-ignore-line - ?? string.Empty) - : cell.Token))); + .Select(column => new DataTableCell(column.Name, column.Regex, Token: result.SelectNode(column.Name))) + .ToDictionary(cell => cell.Name, cell => cell.ApplyRegexToToken()))); protected async Task RenderLiquidAsync(IEnumerable rowList, IList liquidColumns) { @@ -119,4 +114,12 @@ protected async Task RenderLiquidAsync(IEnumerable rowList, IList< protected static Task> PaginateAsync(IQuery query, DataTableDataRequest request) where T : class => query.PaginateAsync(request.Start / request.Length, request.Length); + + private sealed record DataTableCell(string Name, (string From, string To)? Regex, JsonNode Token) + { + public JsonNode ApplyRegexToToken() => + Regex is var (from, to) && Token?.ToString() is { } value + ? value.RegexReplace(from, to, RegexOptions.Singleline) + : Token; + } } diff --git a/Lombiq.DataTables/Services/DateTimeJsonConverter.cs b/Lombiq.DataTables/Services/DateTimeJsonConverter.cs new file mode 100644 index 000000000..cc628e2b9 --- /dev/null +++ b/Lombiq.DataTables/Services/DateTimeJsonConverter.cs @@ -0,0 +1,45 @@ +using Lombiq.DataTables.Models; +using System; +using System.Globalization; +using System.Text.Json; +using System.Text.Json.Nodes; +using System.Text.Json.Serialization; + +namespace Lombiq.DataTables.Services; + +public class DateTimeJsonConverter : JsonConverter +{ + public override DateTime Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + var node = JsonNode.Parse(ref reader); + + if (node?.GetValueKind() == JsonValueKind.String) + { + return DateTime.Parse(node.GetValue(), CultureInfo.InvariantCulture); + } + + if (node.HasMatchingTypeProperty()) + { + return node.ToObject().ToDateTime(); + } + + if (node.HasMatchingTypeProperty()) + { + return (DateTime)node.ToObject(); + } + + throw new InvalidOperationException("Unable to parse JSON!"); + } + + public override void Write(Utf8JsonWriter writer, DateTime value, JsonSerializerOptions options) => + JObject + .FromObject(new DateTimeTicks(value.Ticks, value.Kind))! + .WriteTo(writer, options); + + public sealed record DateTimeTicks(long Ticks, DateTimeKind Kind) + { + public string Type => nameof(DateTimeTicks); + + public DateTime ToDateTime() => new(Ticks, Kind); + } +} diff --git a/Lombiq.DataTables/Services/DeletedContentItemDataTableDataProvider.cs b/Lombiq.DataTables/Services/DeletedContentItemDataTableDataProvider.cs index 4de0df9c3..4eb2de21b 100644 --- a/Lombiq.DataTables/Services/DeletedContentItemDataTableDataProvider.cs +++ b/Lombiq.DataTables/Services/DeletedContentItemDataTableDataProvider.cs @@ -7,7 +7,6 @@ using Microsoft.Extensions.Localization; using OrchardCore.ContentManagement; using OrchardCore.ContentManagement.Records; -using OrchardCore.Contents; using OrchardCore.Mvc.Core.Utilities; using OrchardCore.Navigation; using OrchardCore.Security.Permissions; @@ -15,6 +14,7 @@ using System.Linq; using System.Threading.Tasks; using YesSql; +using static OrchardCore.Contents.CommonPermissions; namespace Lombiq.DataTables.Services; @@ -37,8 +37,7 @@ public DeletedContentItemDataTableDataProvider( public override LocalizedString Description => T["Deleted Content Items"]; - public override IEnumerable AllowedPermissions => - [Permissions.DeleteContent]; + public override IEnumerable AllowedPermissions => [DeleteContent]; protected override async Task GetResultsAsync(DataTableDataRequest request) => new(await GetDeletedContentItemIndicesAsync(_session, request.QueryId)); @@ -71,7 +70,7 @@ public static NavigationItemBuilder AddMenuItem( providerName = nameof(DeletedContentItemDataTableDataProvider), queryId, }) - .Permission(Permissions.DeleteContent) + .Permission(DeleteContent) .LocalNav(); /// diff --git a/Lombiq.DataTables/Services/ExcelDataTableExportService.cs b/Lombiq.DataTables/Services/ExcelDataTableExportService.cs index 90c78e3da..b2a1d9cb8 100644 --- a/Lombiq.DataTables/Services/ExcelDataTableExportService.cs +++ b/Lombiq.DataTables/Services/ExcelDataTableExportService.cs @@ -1,11 +1,12 @@ using ClosedXML.Excel; using Lombiq.DataTables.Models; using Microsoft.Extensions.Localization; -using Newtonsoft.Json.Linq; using System; using System.Collections.Generic; using System.IO; using System.Linq; +using System.Text.Json; +using System.Text.Json.Nodes; using System.Threading.Tasks; namespace Lombiq.DataTables.Services; @@ -29,7 +30,7 @@ public async Task ExportAsync( var columns = columnsDefinition.Columns.Where(column => column.Exportable).ToList(); var response = await dataProvider.GetRowsAsync(request); var results = response.Data - .Select(item => columns.Select(column => item.ValuesDictionary[column.Name]).ToArray()) + .Select(item => columns.Select(column => item.GetValueAsJsonNode(column.Name)).ToArray()) .ToArray(); return CollectionToStream( @@ -61,7 +62,7 @@ public async Task ExportAsync( public static Stream CollectionToStream( string worksheetName, string[] columns, - JToken[][] results, + JsonNode[][] results, IStringLocalizer localizer, string error = null, IDictionary customNumberFormat = null) @@ -110,34 +111,37 @@ public static Stream CollectionToStream( return Save(workbook); } - private static void CreateTableCell(IXLCell cell, JToken value, string dateFormat, IStringLocalizer localizer) + private static void CreateTableCell(IXLCell cell, JsonNode node, string dateFormat, IStringLocalizer localizer) { - if (value is JObject linkJObject && ExportLink.IsInstance(linkJObject)) + if (node.HasMatchingTypeProperty()) { - var link = linkJObject.ToObject(); + var link = node.ToObject(); if (link != null) cell.FormulaA1 = $"HYPERLINK(\"{link.Url}\",\"{link.Text}\")"; } - else if (value is JObject dateJObject && ExportDate.IsInstance(dateJObject)) + else if (node.HasMatchingTypeProperty()) { - var date = dateJObject.ToObject(); + var date = node.ToObject(); cell.Value = (DateTime)date!; cell.Style.DateFormat.Format = date?.ExcelFormat ?? dateFormat; } - else + else if (node.HasMatchingTypeProperty()) { - if (value.Type == JTokenType.Date) cell.Style.DateFormat.Format = dateFormat; - - cell.Value = value.Type switch + cell.Value = node.ToObject().ToDateTime(); + cell.Style.DateFormat.Format = dateFormat; + } + else if (node is JsonArray array) + { + cell.Value = string.Join(", ", array.Select(item => item.ToString())); + } + else if (node is JsonValue value) + { + cell.Value = value.GetValueKind() switch { - JTokenType.Boolean => value.ToObject() - ? localizer["Yes"].Value - : localizer["No"].Value, - JTokenType.Date => value.ToObject(), - JTokenType.Float => value.ToObject(), - JTokenType.Integer => value.ToObject(), - JTokenType.Null => Blank.Value, - JTokenType.TimeSpan => value.ToObject(), - JTokenType.Array => string.Join(", ", ((JArray)value).Select(item => item.ToString())), + JsonValueKind.True => localizer["Yes"].Value, + JsonValueKind.False => localizer["No"].Value, + JsonValueKind.Undefined => string.Empty, + JsonValueKind.Null => string.Empty, + JsonValueKind.Number => value.ToString().Contains('.') ? value.Value() : value.Value(), _ => value.ToString(), }; } diff --git a/Lombiq.DataTables/Services/IndexBasedDataTableDataProvider.cs b/Lombiq.DataTables/Services/IndexBasedDataTableDataProvider.cs index 33361abf8..e72106ed2 100644 --- a/Lombiq.DataTables/Services/IndexBasedDataTableDataProvider.cs +++ b/Lombiq.DataTables/Services/IndexBasedDataTableDataProvider.cs @@ -2,11 +2,11 @@ using Lombiq.DataTables.Constants; using Lombiq.DataTables.Models; using Microsoft.AspNetCore.Authorization; -using Newtonsoft.Json.Linq; using OrchardCore.ContentManagement; using System; using System.Collections.Generic; using System.Linq; +using System.Text.Json.Nodes; using System.Threading.Tasks; using YesSql; using YesSql.Indexes; @@ -60,7 +60,7 @@ public override async Task GetRowsAsync(DataTableDataRequ var queryResults = await transaction.Connection.QueryAsync(sql, query.Parameters, transaction); var rowList = SubstituteByColumn( - (await TransformAsync(queryResults)).Select(JObject.FromObject), + (await TransformAsync(queryResults)).Select(item => JObject.FromObject(item)), columnsDefinition.Columns.ToList()) .Select((item, index) => new DataTableRow(index, JObject.FromObject(item))) .ToList(); diff --git a/Lombiq.DataTables/Services/JsonResultDataTableDataProvider.cs b/Lombiq.DataTables/Services/JsonResultDataTableDataProvider.cs index a5ae833d4..c1fefdcfb 100644 --- a/Lombiq.DataTables/Services/JsonResultDataTableDataProvider.cs +++ b/Lombiq.DataTables/Services/JsonResultDataTableDataProvider.cs @@ -1,9 +1,10 @@ using Lombiq.DataTables.Models; using Microsoft.Extensions.Localization; -using Newtonsoft.Json.Linq; using System; using System.Collections.Generic; using System.Linq; +using System.Text.Json; +using System.Text.Json.Nodes; using System.Threading.Tasks; namespace Lombiq.DataTables.Services; @@ -41,7 +42,9 @@ public override async Task GetRowsAsync(DataTableDataRequ if (metaData.CountFiltered >= 0) recordsFiltered = metaData.CountFiltered; - var json = results[0] is JObject ? results.Cast() : results.Select(JObject.FromObject); + var json = results[0] is JsonObject + ? results.Cast() + : results.Select(result => JObject.FromObject(result)); if (!string.IsNullOrEmpty(order.Column)) json = OrderByColumn(json, order); if (request.Search?.IsRegex == true) @@ -101,12 +104,12 @@ private static (IEnumerable Results, int Count) Search( string searchValue, IReadOnlyCollection columnFilters) { - static string PrepareToken(JToken token) => - token switch + static string PrepareToken(JsonNode node) => + node switch { - JObject link when ExportLink.IsInstance(link) => ExportLink.GetText(link), - JObject date when ExportDate.IsInstance(date) => ExportDate.GetText(date), - { } => token.ToString(), + JsonObject link when ExportLink.IsInstance(link) => ExportLink.GetText(link), + JsonObject date when ExportDate.IsInstance(date) => ExportDate.GetText(date), + { } => node.ToString(), null => null, }; @@ -117,7 +120,7 @@ JObject date when ExportDate.IsInstance(date) => ExportDate.GetText(date), filteredRows = filteredRows.Where(row => columnFilters.All(filter => row.ValuesDictionary.TryGetValue(filter.Name, out var token) && - token?.ToString().Contains(filter.Search.Value, StringComparison.InvariantCulture) == true)); + token?.ToString()?.Contains(filter.Search.Value, StringComparison.InvariantCulture) == true)); } if (hasSearch) @@ -130,8 +133,8 @@ JObject date when ExportDate.IsInstance(date) => ExportDate.GetText(date), words.TrueForAll(word => columns.Any(filter => filter.Searchable && - row.ValuesDictionary.TryGetValue(filter.Name, out var token) && - PrepareToken(token)?.Contains(word, StringComparison.InvariantCultureIgnoreCase) == true))); + row.GetValueAsJsonNode(filter.Name) is { } jsonNode && + PrepareToken(jsonNode)?.Contains(word, StringComparison.InvariantCultureIgnoreCase) == true))); } var list = filteredRows.ToList(); @@ -139,41 +142,74 @@ JObject date when ExportDate.IsInstance(date) => ExportDate.GetText(date), } /// - /// When overridden in a derived class it gets the content which is then turned into if + /// When overridden in a derived class it gets the content which is then turned into if /// necessary and then queried down using the column names into a dictionary. /// /// The input of . /// A list of results or s. protected abstract Task GetResultsAsync(DataTableDataRequest request); - private IEnumerable OrderByColumn(IEnumerable json, DataTableOrder order) + private static IEnumerable OrderByColumn(IEnumerable json, DataTableOrder order) { var orderColumnName = order.Column.Replace('_', '.'); - JToken Selector(JObject x) + var intermediate = json.Select(item => OrderByColumnItem.Create(item, orderColumnName)); + + return (order.IsAscending ? intermediate.Order() : intermediate.OrderDescending()).Select(item => item.Original); + } + + private sealed record OrderByColumnItem(JsonObject Original, IComparable OrderBy) : IComparable + { + public static OrderByColumnItem Create(JsonObject item, string jsonPathQuery) { - var jToken = x.SelectToken(orderColumnName); + if (item.SelectNode(jsonPathQuery) is not { } node) return new(item, OrderBy: null); - if (jToken is JObject jObject) + if (node.HasMatchingTypeProperty()) { - if (jObject.ContainsKey(nameof(ExportLink.Text))) - { - jToken = jObject[nameof(ExportLink.Text)]; - } - else if (ExportDate.IsInstance(jObject)) - { - jToken = (DateTime)jToken.ToObject(); - } + node = ExportLink.GetText(node.AsObject()); + } + else if (node.HasMatchingTypeProperty()) + { + node = (DateTime)node.ToObject(); + } + else if (node.HasMatchingTypeProperty()) + { + node = node.ToObject().ToDateTime(); } - return jToken switch + var orderBy = node switch { null => null, - JValue jValue when jValue.Type != JTokenType.String => jValue, - _ => jToken.ToString().ToUpperInvariant(), + JsonValue value => value.ToComparable(), + _ => node.ToString().ToUpperInvariant(), }; + + return new(item, orderBy); } - return order.IsAscending ? json.OrderBy(Selector) : json.OrderByDescending(Selector); + public int CompareTo(object obj) + { + var thisOrderBy = OrderBy ?? string.Empty; + var thatOrderBy = (obj as OrderByColumnItem)?.OrderBy ?? string.Empty; + + if (thisOrderBy.GetType() != thatOrderBy.GetType()) + { + if (thisOrderBy is decimal && decimal.TryParse(thatOrderBy.ToString(), out var thatDecimal)) + { + thatOrderBy = thatDecimal; + } + else if (thatOrderBy is decimal && decimal.TryParse(thisOrderBy.ToString(), out var thisDecimal)) + { + thisOrderBy = thisDecimal; + } + else + { + thisOrderBy = thisOrderBy.ToString() ?? string.Empty; + thatOrderBy = thatOrderBy.ToString() ?? string.Empty; + } + } + + return thisOrderBy.CompareTo(thatOrderBy); + } } } diff --git a/Lombiq.DataTables/Startup.cs b/Lombiq.DataTables/Startup.cs index 29eddb224..bcdd332f2 100644 --- a/Lombiq.DataTables/Startup.cs +++ b/Lombiq.DataTables/Startup.cs @@ -4,12 +4,10 @@ using Lombiq.DataTables.TagHelpers; using Lombiq.HelpfulLibraries.AspNetCore.Middlewares; using Lombiq.HelpfulLibraries.OrchardCore.DependencyInjection; -using Lombiq.HelpfulLibraries.OrchardCore.Mvc; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Routing; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; -using OrchardCore.Admin; using OrchardCore.Data.Migration; using OrchardCore.Liquid; using OrchardCore.Modules; @@ -18,12 +16,8 @@ namespace Lombiq.DataTables; -public class Startup : StartupBase +public sealed class Startup : StartupBase { - private readonly AdminOptions _adminOptions; - - public Startup(IOptions adminOptions) => _adminOptions = adminOptions.Value; - public override void ConfigureServices(IServiceCollection services) { services.AddDataTableExportService(); @@ -41,8 +35,6 @@ public override void ConfigureServices(IServiceCollection services) services.AddScoped(); services.AddDataTableDataProvider(); - - AdminRouteAttributeRouteMapper.AddToServices(services); } public override void Configure(IApplicationBuilder app, IEndpointRouteBuilder routes, IServiceProvider serviceProvider) => diff --git a/Lombiq.DataTables/Tests/Lombiq.DataTables.Tests.UI/Lombiq.DataTables.Tests.UI.csproj b/Lombiq.DataTables/Tests/Lombiq.DataTables.Tests.UI/Lombiq.DataTables.Tests.UI.csproj index ea2b53f7f..80ec6909e 100644 --- a/Lombiq.DataTables/Tests/Lombiq.DataTables.Tests.UI/Lombiq.DataTables.Tests.UI.csproj +++ b/Lombiq.DataTables/Tests/Lombiq.DataTables.Tests.UI/Lombiq.DataTables.Tests.UI.csproj @@ -25,7 +25,7 @@ - + diff --git a/Lombiq.DataTables/Tests/Lombiq.DataTables.Tests/Lombiq.DataTables.Tests.csproj b/Lombiq.DataTables/Tests/Lombiq.DataTables.Tests/Lombiq.DataTables.Tests.csproj index 142be2eef..16728da8a 100644 --- a/Lombiq.DataTables/Tests/Lombiq.DataTables.Tests/Lombiq.DataTables.Tests.csproj +++ b/Lombiq.DataTables/Tests/Lombiq.DataTables.Tests/Lombiq.DataTables.Tests.csproj @@ -6,7 +6,7 @@ - + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/Lombiq.DataTables/Tests/Lombiq.DataTables.Tests/MockDataProvider.cs b/Lombiq.DataTables/Tests/Lombiq.DataTables.Tests/MockDataProvider.cs index 52c728ae3..0cf70bcd3 100644 --- a/Lombiq.DataTables/Tests/Lombiq.DataTables.Tests/MockDataProvider.cs +++ b/Lombiq.DataTables/Tests/Lombiq.DataTables.Tests/MockDataProvider.cs @@ -4,7 +4,6 @@ using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Localization; using Microsoft.Extensions.Options; -using Newtonsoft.Json.Linq; using OrchardCore.DisplayManagement.Liquid; using OrchardCore.Liquid.Services; using OrchardCore.Localization; @@ -12,12 +11,21 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Text.Json; using System.Threading.Tasks; namespace Lombiq.DataTables.Tests; public class MockDataProvider : JsonResultDataTableDataProvider { + private static readonly JsonSerializerOptions _options = new() + { + Converters = + { + new DateTimeJsonConverter(), + }, + }; + public DataTableColumnsDefinition Definition { get; set; } private readonly object[][] _dataSet; @@ -52,8 +60,9 @@ protected override async Task GetResultsA .Columns .Select((item, index) => new { item.Name, Index = index }); - return new(_dataSet.Select(row => - JObject.FromObject(columns.ToDictionary(column => column.Name, column => row[column.Index])))); + return new(_dataSet.Select(row => JsonSerializer.SerializeToNode( + columns.ToDictionary(column => column.Name, column => row[column.Index]), + _options))); } protected override DataTableColumnsDefinition GetColumnsDefinitionInner(string queryId) => Definition; diff --git a/Lombiq.DataTables/Tests/Lombiq.DataTables.Tests/UnitTests/Services/ExportTests.cs b/Lombiq.DataTables/Tests/Lombiq.DataTables.Tests/UnitTests/Services/ExportTests.cs index 1c5f15066..fda90276a 100644 --- a/Lombiq.DataTables/Tests/Lombiq.DataTables.Tests/UnitTests/Services/ExportTests.cs +++ b/Lombiq.DataTables/Tests/Lombiq.DataTables.Tests/UnitTests/Services/ExportTests.cs @@ -253,11 +253,11 @@ private static async Task TryExportWithFallbackFontsAsync( { return await service.ExportAsync(provider, request, customNumberFormat: customNumberFormat); } - catch (MissingFontTableException missingFontTableException) + catch (Exception exception) when (exception is MissingFontTableException or NotSupportedException) { DebugHelper.WriteLineTimestamped( $"Attempt {(i + 1).ToTechnicalString()} of exporting the data table with the font " + - $"{fallbackFont} failed with the MissingFontTableException: {missingFontTableException.Message}."); + $"{fallbackFont} failed with the {exception.GetType().Name}: {exception.Message}."); if (i + 1 < maxAttempts) { diff --git a/Lombiq.DataTables/ViewModels/DataTableDefinitionViewModel.cs b/Lombiq.DataTables/ViewModels/DataTableDefinitionViewModel.cs index 1a8111c52..d3c411113 100644 --- a/Lombiq.DataTables/ViewModels/DataTableDefinitionViewModel.cs +++ b/Lombiq.DataTables/ViewModels/DataTableDefinitionViewModel.cs @@ -1,4 +1,4 @@ -using Newtonsoft.Json.Linq; +using System.Text.Json.Nodes; namespace Lombiq.DataTables.ViewModels; @@ -11,9 +11,9 @@ public class DataTableDefinitionViewModel : DataTableDataViewModel public string QueryStringParametersLocalStorageKey { get; set; } public string DataProvider { get; set; } public string DataTableCssClasses { get; set; } - public JObject AdditionalDatatableOptions { get; } + public JsonObject AdditionalDatatableOptions { get; } - public DataTableDefinitionViewModel(JObject additionalDatatableOptions = null) + public DataTableDefinitionViewModel(JsonObject additionalDatatableOptions = null) { if (additionalDatatableOptions != null) AdditionalDatatableOptions = additionalDatatableOptions; } diff --git a/Lombiq.DataTables/Views/Lombiq.DataTable.cshtml b/Lombiq.DataTables/Views/Lombiq.DataTable.cshtml index 6787c32d2..e492a0dbd 100644 --- a/Lombiq.DataTables/Views/Lombiq.DataTable.cshtml +++ b/Lombiq.DataTables/Views/Lombiq.DataTable.cshtml @@ -1,29 +1,28 @@ @using OrchardCore.ResourceManagement -@using System.Globalization -@using Newtonsoft.Json.Linq @using Lombiq.DataTables.Services +@using System.Text.Json @inject IEnumerable Providers @inject IResourceManager ResourceManager @{ - var viewModel = Model.ViewModel as DataTableDefinitionViewModel; + var viewModel = Model.ViewModel as DataTableDefinitionViewModel ?? new DataTableDefinitionViewModel(); - var pageSize = viewModel?.PageSize ?? Site.PageSize; - var skip = viewModel?.Skip ?? 0; + var pageSize = viewModel.PageSize ?? Site.PageSize; + var skip = viewModel.Skip ?? 0; - string queryId = viewModel?.QueryId ?? Model.QueryId?.ToString() ?? ""; - string dataProvider = viewModel?.DataProvider ?? Model.Provider?.ToString() ?? ""; - var columnsDefinition = viewModel?.ColumnsDefinition ?? await Providers + string queryId = viewModel.QueryId ?? Model.QueryId?.ToString() ?? ""; + string dataProvider = viewModel.DataProvider ?? Model.Provider?.ToString() ?? ""; + var columnsDefinition = viewModel.ColumnsDefinition ?? await Providers .Single(provider => provider.GetType().Name == dataProvider) .GetColumnsDefinitionAsync(queryId); var columns = columnsDefinition?.Columns.ToList() ?? new List(); - var childRowsEnabled = viewModel?.ChildRowsEnabled ?? false; - var progressiveLoadingEnabled = viewModel?.ProgressiveLoadingEnabled ?? false; - var queryStringParametersLocalStorageKey = viewModel?.QueryStringParametersLocalStorageKey ?? ""; - var dataTableId = string.IsNullOrEmpty(viewModel?.DataTableId) ? ElementNames.DataTableElementName : viewModel.DataTableId; - var dataTableCssClasses = string.IsNullOrEmpty(viewModel?.DataTableCssClasses) ? "" : viewModel.DataTableCssClasses; + var childRowsEnabled = viewModel.ChildRowsEnabled; + var progressiveLoadingEnabled = viewModel.ProgressiveLoadingEnabled; + var queryStringParametersLocalStorageKey = viewModel.QueryStringParametersLocalStorageKey ?? ""; + var dataTableId = string.IsNullOrEmpty(viewModel.DataTableId) ? ElementNames.DataTableElementName : viewModel.DataTableId; + var dataTableCssClasses = string.IsNullOrEmpty(viewModel.DataTableCssClasses) ? "" : viewModel.DataTableCssClasses; var defaultSortingColumnIndex = Math.Max( 0, Math.Max(columns.FindIndex(column => column.Orderable), columns.FindIndex(column => column.Name == columnsDefinition?.DefaultSortingColumnName))); @@ -31,9 +30,9 @@ var defaultSortingDirection = columnsDefinition?.DefaultSortingDirection ?? SortingDirection.Ascending; var defaultSortingDirectionValue = defaultSortingDirection == SortingDirection.Ascending ? "asc" : "desc"; - var rowApiUrl = Url.Action(nameof(RowsController.Get), typeof(RowsController).ControllerName(), new { Area = FeatureIds.Area }); - var childRowApiUrl = Url.Action(nameof(ChildRowsController.Get), typeof(ChildRowsController).ControllerName(), new { Area = FeatureIds.Area }); - var exportApiUrl = Url.Action(nameof(RowsController.Export), typeof(RowsController).ControllerName(), new { Area = FeatureIds.Area }); + var rowsApiUrl = Url.Action(nameof(RowsController.Get), typeof(RowsController).ControllerName(), new { FeatureIds.Area }); + var childRowApiUrl = Url.Action(nameof(ChildRowsController.Get), typeof(ChildRowsController).ControllerName(), new { FeatureIds.Area }); + var exportApiUrl = Url.Action(nameof(RowsController.Export), typeof(RowsController).ControllerName(), new { FeatureIds.Area }); const string rowElementWithChildRowVisibleModifier = ElementNames.DataTableRowElementName + "_childRowVisible"; const string childRowElementName = ElementNames.DataTableElementName + "__childRow"; @@ -48,8 +47,56 @@ ResourceManager.RegisterResource("stylesheet", ResourceNames.DataTables.Bootstrap4); ResourceManager.RegisterResource("stylesheet", ResourceNames.DataTables.Bootstrap4Buttons); - dynamic additionalOptions = viewModel?.AdditionalDatatableOptions; - var hasViewAction = (bool)(additionalOptions?.viewAction == true); + var hasViewAction = viewModel.AdditionalDatatableOptions?["viewAction"].GetValueKind() == JsonValueKind.True; + + var defaultOrder = new[] { new[] { defaultSortingColumnIndex.ToString(), defaultSortingDirectionValue } }; + var dataTablesOptions = new Dictionary { ["order"] = defaultOrder }; + if (pageSize > 0) { dataTablesOptions["pageLength"] = pageSize; } + if (viewModel.AdditionalDatatableOptions is { } options) + { + foreach (var (key, value) in options) + { + dataTablesOptions[key] = value; + } + } + + var pluginOptions = new Dictionary + { + ["rowClassName"] = ElementNames.DataTableRowElementName, + ["dataProvider"] = dataProvider, + ["rowsApiUrl"] = rowsApiUrl, + ["export"] = new + { + textAll = T["Export All"].Value, + textVisible = T["Export Visible"].Value, + api = exportApiUrl, + }, + ["texts"] = new + { + yes = T["Yes"].Value, + no = T["No"].Value, + }, + ["errorsSelector"] = '.' + errorsElementName, + ["serverSidePagingEnabled"] = !progressiveLoadingEnabled, + ["queryStringParametersLocalStorageKey"] = queryStringParametersLocalStorageKey, + ["progressiveLoadingOptions"] = new + { + progressiveLoadingEnabled, + skip, + batchSize = pageSize, + }, + ["childRowOptions"] = new + { + childRowsEnabled, + asyncLoading = true, + apiUrl = childRowApiUrl, + childRowClassName = childRowElementName, + toggleChildRowButtonClassName = toggleChildRowButtonElementName, + childRowVisibleClassName = rowElementWithChildRowVisibleModifier, + }, + ["culture"] = Orchard.CultureName(), + }; + if (!string.IsNullOrWhiteSpace(queryId)) { pluginOptions["queryId"] = queryId; } } @await DisplayAsync(await New.Lombiq_DataTable_Resources(ViewModel: viewModel)) @@ -97,88 +144,25 @@ diff --git a/NuGet.config b/NuGet.config new file mode 100644 index 000000000..ba8c556e7 --- /dev/null +++ b/NuGet.config @@ -0,0 +1,10 @@ + + + + + + + + + +