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 @@
+
+
+
+
+
+
+
+
+
+