diff --git a/Lombiq.HelpfulExtensions.Tests.UI/Constants/GeneratedMigrationCodes.cs b/Lombiq.HelpfulExtensions.Tests.UI/Constants/GeneratedMigrationCodes.cs
index f03fda00..02626b98 100644
--- a/Lombiq.HelpfulExtensions.Tests.UI/Constants/GeneratedMigrationCodes.cs
+++ b/Lombiq.HelpfulExtensions.Tests.UI/Constants/GeneratedMigrationCodes.cs
@@ -1,39 +1,48 @@
-using System;
+using Shouldly;
+using System.Diagnostics.CodeAnalysis;
namespace Lombiq.HelpfulExtensions.Tests.UI.Constants;
internal static class GeneratedMigrationCodes
{
- // Environment.NewLine is used for this to work regardless of the mismatch of line ending style between the code
- // file, the platform where it was compiled, and where it was executed.
- public static string Page =
- string.Join(
- Environment.NewLine,
- "_contentDefinitionManager.AlterTypeDefinition(\"Page\", type => type",
- " .DisplayedAs(\"Page\")",
- " .Creatable()",
- " .Listable()",
- " .Draftable()",
- " .Versionable()",
- " .Securable()",
- " .WithPart(\"TitlePart\", part => part",
- " .WithPosition(\"0\")",
- " )",
- " .WithPart(\"AutoroutePart\", part => part",
- " .WithPosition(\"1\")",
- " .WithSettings(new AutoroutePartSettings",
- " {",
- " AllowCustomPath = true,",
- " ShowHomepageOption = true,",
- " Pattern = \"{{ Model.ContentItem | display_text | slugify }}\",",
- " })",
- " )",
- " .WithPart(\"FlowPart\", part => part",
- " .WithPosition(\"2\")",
- " )",
- " .WithPart(\"Page\", part => part",
- " .WithPosition(\"3\")",
- " )",
- ");",
- string.Empty);
+ [SuppressMessage(
+ "Usage",
+ "MA0136:Raw String contains an implicit end of line character",
+ Justification = "It's wrapped to prevent issues related to that.")]
+ private const string Page =
+ """
+ _contentDefinitionManager.AlterTypeDefinition("Page", type => type
+ .DisplayedAs("Page")
+ .Creatable()
+ .Listable()
+ .Draftable()
+ .Versionable()
+ .Securable()
+ .WithPart("TitlePart", part => part
+ .WithPosition("0")
+ )
+ .WithPart("AutoroutePart", part => part
+ .WithPosition("1")
+ .WithSettings(new AutoroutePartSettings
+ {
+ AllowCustomPath = true,
+ Pattern = {{ Model.ContentItem | display_text | slugify }},
+ ShowHomepageOption = true,
+ AllowUpdatePath = false,
+ AllowDisabled = false,
+ AllowRouteContainedItems = false,
+ ManageContainedItemRoutes = false,
+ AllowAbsolutePath = false,
+ })
+ )
+ .WithPart("FlowPart", part => part
+ .WithPosition("2")
+ )
+ .WithPart("Page", part => part
+ .WithPosition("3")
+ )
+ );
+ """;
+
+ public static void ShouldBePage(string actual) => actual.ShouldBeByLine(Page);
}
diff --git a/Lombiq.HelpfulExtensions.Tests.UI/Extensions/TestCaseUITestContextExtensions.cs b/Lombiq.HelpfulExtensions.Tests.UI/Extensions/TestCaseUITestContextExtensions.cs
index 3765cc78..ef9a75a7 100644
--- a/Lombiq.HelpfulExtensions.Tests.UI/Extensions/TestCaseUITestContextExtensions.cs
+++ b/Lombiq.HelpfulExtensions.Tests.UI/Extensions/TestCaseUITestContextExtensions.cs
@@ -116,7 +116,8 @@ await context.RetryIfStaleOrFailAsync(async () =>
{
await context.ClickReliablyOnAsync(By.ClassName("toggle-showing-generated-migration-code"));
- context.Get(By.Id("generated-migration-code").OfAnyVisibility()).GetValue().ShouldBe(GeneratedMigrationCodes.Page);
+ GeneratedMigrationCodes.ShouldBePage(
+ context.Get(By.Id("generated-migration-code").OfAnyVisibility()).GetValue());
// Making sure that the collapsible area is open.
context.Get(By.CssSelector("#generated-migration-code-container.collapse.show"));
diff --git a/Lombiq.HelpfulExtensions.Tests.UI/Lombiq.HelpfulExtensions.Tests.UI.csproj b/Lombiq.HelpfulExtensions.Tests.UI/Lombiq.HelpfulExtensions.Tests.UI.csproj
index ebf73a3f..4e7c80f8 100644
--- a/Lombiq.HelpfulExtensions.Tests.UI/Lombiq.HelpfulExtensions.Tests.UI.csproj
+++ b/Lombiq.HelpfulExtensions.Tests.UI/Lombiq.HelpfulExtensions.Tests.UI.csproj
@@ -36,7 +36,7 @@
-
+
diff --git a/Lombiq.HelpfulExtensions/Controllers/ContentSetController.cs b/Lombiq.HelpfulExtensions/Controllers/ContentSetController.cs
index 18e73ed5..ab7a2559 100644
--- a/Lombiq.HelpfulExtensions/Controllers/ContentSetController.cs
+++ b/Lombiq.HelpfulExtensions/Controllers/ContentSetController.cs
@@ -7,7 +7,7 @@
namespace Lombiq.HelpfulExtensions.Extensions.ContentSets.Controllers;
[Feature(FeatureIds.ContentSets)]
-public class ContentSetController : Controller
+public sealed class ContentSetController : Controller
{
private readonly IContentSetManager _contentSetManager;
private readonly IOrchardHelper _orchardHelper;
diff --git a/Lombiq.HelpfulExtensions/Controllers/OrchardRecipeMigrationAdminController.cs b/Lombiq.HelpfulExtensions/Controllers/OrchardRecipeMigrationAdminController.cs
index f2bf5e6f..5012ee14 100644
--- a/Lombiq.HelpfulExtensions/Controllers/OrchardRecipeMigrationAdminController.cs
+++ b/Lombiq.HelpfulExtensions/Controllers/OrchardRecipeMigrationAdminController.cs
@@ -16,7 +16,7 @@ namespace Lombiq.HelpfulExtensions.Extensions.OrchardRecipeMigration.Controllers
[Admin]
[Feature(FeatureIds.OrchardRecipeMigration)]
-public class OrchardRecipeMigrationAdminController : Controller
+public sealed class OrchardRecipeMigrationAdminController : Controller
{
private readonly INotifier _notifier;
private readonly IOrchardExportToRecipeConverter _converter;
diff --git a/Lombiq.HelpfulExtensions/Extensions/CodeGeneration/CodeGenerationDisplayDriver.cs b/Lombiq.HelpfulExtensions/Extensions/CodeGeneration/CodeGenerationDisplayDriver.cs
index 67edb7f2..82e1ff70 100644
--- a/Lombiq.HelpfulExtensions/Extensions/CodeGeneration/CodeGenerationDisplayDriver.cs
+++ b/Lombiq.HelpfulExtensions/Extensions/CodeGeneration/CodeGenerationDisplayDriver.cs
@@ -1,25 +1,29 @@
using Microsoft.Extensions.Localization;
-using Newtonsoft.Json.Linq;
using OrchardCore.ContentManagement.Metadata.Models;
using OrchardCore.ContentManagement.Metadata.Settings;
using OrchardCore.ContentTypes.Editors;
+using OrchardCore.DisplayManagement.Handlers;
using OrchardCore.DisplayManagement.Views;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text;
+using System.Text.Json.Nodes;
namespace Lombiq.HelpfulExtensions.Extensions.CodeGeneration;
-public class CodeGenerationDisplayDriver : ContentTypeDefinitionDisplayDriver
+public sealed class CodeGenerationDisplayDriver : ContentTypeDefinitionDisplayDriver
{
+ private const int IndentationDepth = 4;
+ private const string EmptyString = "\"\"";
+
private readonly IStringLocalizer T;
public CodeGenerationDisplayDriver(IStringLocalizer stringLocalizer) =>
T = stringLocalizer;
- public override IDisplayResult Edit(ContentTypeDefinition model) =>
+ public override IDisplayResult Edit(ContentTypeDefinition model, BuildEditorContext context) =>
Initialize(
"ContentTypeMigrations_Edit",
viewModel => viewModel.MigrationCodeLazy = new Lazy(() =>
@@ -37,7 +41,7 @@ public override IDisplayResult Edit(ContentTypeDefinition model) =>
codeBuilder.AppendLine(CultureInfo.InvariantCulture, $" .DisplayedAs(\"{model.DisplayName}\")");
GenerateCodeForSettings(codeBuilder, model.GetSettings());
- AddSettingsWithout(codeBuilder, model.Settings, 4);
+ AddSettingsWithout(codeBuilder, model.Settings);
GenerateCodeForParts(codeBuilder, model.Parts);
codeBuilder.AppendLine(");");
@@ -63,7 +67,7 @@ private void GenerateCodeForParts(StringBuilder codeBuilder, IEnumerable(codeBuilder, part.Settings, 8);
+ AddSettingsWithout(codeBuilder, part.Settings, 2 * IndentationDepth);
// Checking if anything was added to the part's settings.
if (codeBuilder.Length == partStartingLength)
@@ -102,7 +106,7 @@ private void GenerateCodeForPartsWithFields(
AddWithLine(codeBuilder, nameof(partSettings.Description), partSettings.Description);
AddWithLine(codeBuilder, nameof(partSettings.DefaultPosition), partSettings.DefaultPosition);
- AddSettingsWithout(codeBuilder, part.Settings, 4);
+ AddSettingsWithout(codeBuilder, part.Settings);
foreach (var field in part.Fields)
{
@@ -116,7 +120,7 @@ private void GenerateCodeForPartsWithFields(
AddWithLine(codeBuilder, nameof(fieldSettings.DisplayMode), fieldSettings.DisplayMode);
AddWithLine(codeBuilder, nameof(fieldSettings.Position), fieldSettings.Position);
- AddSettingsWithout(codeBuilder, field.Settings, 8);
+ AddSettingsWithout(codeBuilder, field.Settings, 2 * IndentationDepth);
codeBuilder.AppendLine(" )");
}
@@ -125,32 +129,20 @@ private void GenerateCodeForPartsWithFields(
}
}
- private string ConvertJToken(JToken jToken, int indentationDepth)
- {
- switch (jToken)
+ private string ConvertNode(JsonNode node, int indentationDepth) =>
+ node switch
{
- case JValue jValue:
- var value = jValue.Value;
- return value switch
- {
- bool boolValue => boolValue ? "true" : "false",
- string => $"\"{value}\"",
- _ => value?.ToString()?.Replace(',', '.'), // Replace decimal commas.
- };
- case JArray jArray:
- return ConvertJArray(jArray, indentationDepth);
- case JObject jObject:
- return ConvertJObject(jObject, indentationDepth);
- default:
- throw new NotSupportedException($"Settings values of type {jToken.GetType()} are not supported.");
- }
- }
+ JsonValue jsonValue => jsonValue.ToString(),
+ JsonArray jsonArray => ConvertJsonArray(jsonArray, indentationDepth),
+ JsonObject jsonObject => ConvertJsonObject(jsonObject, indentationDepth),
+ _ => throw new NotSupportedException($"Settings values of type {node.GetType()} are not supported."),
+ };
- private string ConvertJArray(JArray jArray, int indentationDepth)
+ private string ConvertJsonArray(JsonArray jArray, int indentationDepth)
{
- var indentation = new string(' ', indentationDepth + 4);
+ var indentation = new string(' ', indentationDepth + IndentationDepth);
- var items = jArray.Select(item => ConvertJToken(item, indentationDepth + 8)).ToList();
+ var items = jArray.Select(item => ConvertNode(item, indentationDepth + (2 * IndentationDepth))).ToList();
// If the items are formatted (for ListValueOption) then don't inject line-by-line formatting.
if (items.Exists(item => item.ContainsOrdinalIgnoreCase(Environment.NewLine)))
@@ -164,7 +156,7 @@ private string ConvertJArray(JArray jArray, int indentationDepth)
stringArrayCodeBuilder.AppendLine();
stringArrayCodeBuilder.AppendLine(CultureInfo.InvariantCulture, $"{indentation}{{");
- var itemIndentation = new string(' ', indentationDepth + 8);
+ var itemIndentation = new string(' ', indentationDepth + (2 * IndentationDepth));
foreach (var item in items)
{
@@ -176,53 +168,46 @@ private string ConvertJArray(JArray jArray, int indentationDepth)
return stringArrayCodeBuilder.ToString();
}
- private string ConvertJObject(JObject jObject, int indentationDepth)
+ private string ConvertJsonObject(JsonObject jsonObject, int indentationDepth)
{
var braceIndentation = new string(' ', indentationDepth);
- var propertyIndentation = new string(' ', indentationDepth + 4);
- if (jObject["name"] != null && jObject["value"] != null)
+ var propertyIndentation = new string(' ', indentationDepth + IndentationDepth);
+ if (jsonObject["name"] is { } name && jsonObject["value"] is { } value)
{
var objectCodeBuilder = new StringBuilder();
objectCodeBuilder.AppendLine(CultureInfo.InvariantCulture, $"{braceIndentation}new ListValueOption");
objectCodeBuilder.AppendLine(CultureInfo.InvariantCulture, $"{braceIndentation}{{");
- objectCodeBuilder.AppendLine(CultureInfo.InvariantCulture, $"{propertyIndentation}Name = \"{jObject["name"]}\",");
- objectCodeBuilder.AppendLine(CultureInfo.InvariantCulture, $"{propertyIndentation}Value = \"{jObject["value"]}\",");
+ objectCodeBuilder.AppendLine(CultureInfo.InvariantCulture, $"{propertyIndentation}Name = \"{name}\",");
+ objectCodeBuilder.AppendLine(CultureInfo.InvariantCulture, $"{propertyIndentation}Value = \"{value}\",");
objectCodeBuilder.AppendLine(CultureInfo.InvariantCulture, $"{braceIndentation}}},");
return objectCodeBuilder.ToString();
}
// Using a quoted string so it doesn't mess up the syntax highlighting of the rest of the code.
- return T["\"FIX ME! Couldn't determine the actual type to instantiate.\" {0}", jObject.ToString()];
+ return T["\"FIX ME! Couldn't determine the actual type to instantiate.\" {0}", jsonObject.ToString()];
}
- private void AddSettingsWithout(StringBuilder codeBuilder, JObject settings, int indentationDepth)
+ private void AddSettingsWithout(StringBuilder codeBuilder, JsonObject settings, int indentationDepth = IndentationDepth)
{
var indentation = new string(' ', indentationDepth);
- var filteredSettings = ((IEnumerable>)settings)
- .Where(setting => setting.Key != typeof(T).Name);
+ var filteredSettings = settings
+ .Where(pair => pair.Key != typeof(T).Name && pair.Value is JsonObject)
+ .Select(pair => (pair.Key, (JsonObject)pair.Value));
- foreach (var setting in filteredSettings)
+ foreach (var (typeName, properties) in filteredSettings)
{
- var properties = setting.Value.Where(property => property is JProperty).Cast().ToArray();
+ if (properties.Count == 0) continue;
- if (properties.Length == 0) continue;
-
- codeBuilder.AppendLine(CultureInfo.InvariantCulture, $"{indentation}.WithSettings(new {setting.Key}");
+ codeBuilder.AppendLine(CultureInfo.InvariantCulture, $"{indentation}.WithSettings(new {typeName}");
codeBuilder.AppendLine(indentation + "{");
- // This doesn't support multi-level object hierarchies for settings but come on, who uses complex settings
- // objects?
- for (int i = 0; i < properties.Length; i++)
+ foreach (var (name, value) in properties)
{
- var property = properties[i];
-
- var propertyValue = ConvertJToken(property.Value, indentationDepth);
-
- propertyValue ??= "\"\"";
-
- codeBuilder.AppendLine(CultureInfo.InvariantCulture, $"{indentation} {property.Name} = {propertyValue},");
+ codeBuilder.AppendLine(
+ CultureInfo.InvariantCulture,
+ $"{indentation} {name} = {ConvertNode(value, indentationDepth) ?? EmptyString},");
}
codeBuilder.AppendLine(indentation + "})");
diff --git a/Lombiq.HelpfulExtensions/Extensions/CodeGeneration/Readme.md b/Lombiq.HelpfulExtensions/Extensions/CodeGeneration/Readme.md
new file mode 100644
index 00000000..a802cac1
--- /dev/null
+++ b/Lombiq.HelpfulExtensions/Extensions/CodeGeneration/Readme.md
@@ -0,0 +1,7 @@
+# Code Generation Helpful Extensions
+
+## Content definition code generation
+
+Generates migration code from content definitions. You can use this to create (or edit) a content type on the admin and then move its creation to a migration class. Generated migration code is displayed under the content types' editors, just enable the feature. Check out [this demo video](https://www.youtube.com/watch?v=KOlsLaIzgm8) to see this in action.
+
+![Content definition code generation textbox on the admin, showing generated migration code for the Page content type.](../../../Docs/Attachments/ContentTypeCodeGeneration.png)
diff --git a/Lombiq.HelpfulExtensions/Extensions/CodeGeneration/Startup.cs b/Lombiq.HelpfulExtensions/Extensions/CodeGeneration/Startup.cs
index 7bc42fed..7efbe83b 100644
--- a/Lombiq.HelpfulExtensions/Extensions/CodeGeneration/Startup.cs
+++ b/Lombiq.HelpfulExtensions/Extensions/CodeGeneration/Startup.cs
@@ -8,7 +8,7 @@
namespace Lombiq.HelpfulExtensions.Extensions.CodeGeneration;
[Feature(FeatureIds.CodeGeneration)]
-public class Startup : StartupBase
+public sealed class Startup : StartupBase
{
public override void ConfigureServices(IServiceCollection services) =>
services.AddScoped();
diff --git a/Lombiq.HelpfulExtensions/Extensions/ContentSets/Drivers/ContentSetContentPickerFieldDisplayDriver.cs b/Lombiq.HelpfulExtensions/Extensions/ContentSets/Drivers/ContentSetContentPickerFieldDisplayDriver.cs
index dfe278f0..afbf003f 100644
--- a/Lombiq.HelpfulExtensions/Extensions/ContentSets/Drivers/ContentSetContentPickerFieldDisplayDriver.cs
+++ b/Lombiq.HelpfulExtensions/Extensions/ContentSets/Drivers/ContentSetContentPickerFieldDisplayDriver.cs
@@ -14,7 +14,7 @@
namespace Lombiq.HelpfulExtensions.Extensions.ContentSets.Drivers;
-public class ContentSetContentPickerFieldDisplayDriver : ContentFieldDisplayDriver
+public sealed class ContentSetContentPickerFieldDisplayDriver : ContentFieldDisplayDriver
{
private readonly IContentDefinitionManager _contentDefinitionManager;
private readonly IContentSetManager _contentSetManager;
diff --git a/Lombiq.HelpfulExtensions/Extensions/ContentSets/Drivers/ContentSetPartDisplayDriver.cs b/Lombiq.HelpfulExtensions/Extensions/ContentSets/Drivers/ContentSetPartDisplayDriver.cs
index 9b7f7cf1..7642a5ef 100644
--- a/Lombiq.HelpfulExtensions/Extensions/ContentSets/Drivers/ContentSetPartDisplayDriver.cs
+++ b/Lombiq.HelpfulExtensions/Extensions/ContentSets/Drivers/ContentSetPartDisplayDriver.cs
@@ -6,7 +6,7 @@
using Microsoft.Extensions.Localization;
using OrchardCore.ContentManagement.Display.ContentDisplay;
using OrchardCore.ContentManagement.Display.Models;
-using OrchardCore.DisplayManagement.ModelBinding;
+using OrchardCore.DisplayManagement.Handlers;
using OrchardCore.DisplayManagement.Views;
using OrchardCore.Entities;
using System.Collections.Generic;
@@ -14,7 +14,7 @@
namespace Lombiq.HelpfulExtensions.Extensions.ContentSets.Drivers;
-public class ContentSetPartDisplayDriver : ContentPartDisplayDriver
+public sealed class ContentSetPartDisplayDriver : ContentPartDisplayDriver
{
private const string ShapeType = $"{nameof(ContentSetPart)}_{CommonContentDisplayTypes.SummaryAdmin}";
@@ -64,23 +64,17 @@ public override IDisplayResult Edit(ContentSetPart part, BuildPartEditorContext
context.IsNew))
.Location($"Parts:0%{context.TypePartDefinition.Name};0");
- public override async Task UpdateAsync(
- ContentSetPart part,
- IUpdateModel updater,
- UpdatePartEditorContext context)
+ public override async Task UpdateAsync(ContentSetPart part, UpdatePartEditorContext context)
{
- var viewModel = new ContentSetPartViewModel();
+ var viewModel = await context.CreateModelAsync(Prefix);
- if (await updater.TryUpdateModelAsync(viewModel, Prefix))
- {
- part.Key = viewModel.Key;
+ part.Key = viewModel.Key;
- // Need to do this here to support displaying the message to save before adding when the
- // item has not been saved yet.
- if (string.IsNullOrEmpty(part.ContentSet))
- {
- part.ContentSet = _idGenerator.GenerateUniqueId();
- }
+ // Need to do this here to support displaying the message to save before adding when the item has not been saved
+ // yet.
+ if (string.IsNullOrEmpty(part.ContentSet))
+ {
+ part.ContentSet = _idGenerator.GenerateUniqueId();
}
return await EditAsync(part, context);
diff --git a/Lombiq.HelpfulExtensions/Extensions/ContentSets/Migrations.cs b/Lombiq.HelpfulExtensions/Extensions/ContentSets/Migrations.cs
index 46c3a4c4..d6285494 100644
--- a/Lombiq.HelpfulExtensions/Extensions/ContentSets/Migrations.cs
+++ b/Lombiq.HelpfulExtensions/Extensions/ContentSets/Migrations.cs
@@ -9,7 +9,7 @@
namespace Lombiq.HelpfulExtensions.Extensions.ContentSets;
-public class Migrations : DataMigration
+public sealed class Migrations : DataMigration
{
private readonly IContentDefinitionManager _contentDefinitionManager;
diff --git a/Lombiq.HelpfulExtensions/Extensions/ContentSets/Readme.md b/Lombiq.HelpfulExtensions/Extensions/ContentSets/Readme.md
new file mode 100644
index 00000000..c908960d
--- /dev/null
+++ b/Lombiq.HelpfulExtensions/Extensions/ContentSets/Readme.md
@@ -0,0 +1,20 @@
+# Content Sets
+
+Adds an attachable (named) content part that ties together a content item and a set of variants for it. This is similar to Content Localization part, but generic and not tied to the culture options. The `IContentSetEventHandler.GetSupportedOptionsAsync()` extension point is used to generate the valid options for a content item with an attached `ContentSetPart`.
+
+The content items are indexed into the `ContentSetIndex`. The `IContentSetManager` has methods to retrieve the existing content items (or just the index rows) for a specific content set.
+
+When the content part is attached, a new dropdown is added to the content item in the admin dashboard's content items list. This is similar in design to the dropdown added by the Content Localization part. The label is the named part's display text and you can use the dropdown (or the listing in the editor view) to select an option. When an option is selected the content item is cloned and that option's key assigned to it.
+
+## Generating content set options
+
+You can generate your custom content set options two ways:
+
+- Create a service which implements the `IContentSetEventHandler` interface.
+- Create a workflow with the _Creating Content Set_ startup event. The workflow should return an output `MemberLinks` which should contain an array of `{ "Key": string, "DisplayText": string }` objects. Further details can be seen on the event's editor screen.
+
+The latter can be used even if you don't have access to the code, e.g. on DotNest. With either approach you only have to provide the `Key` and `DisplayText` properties, anything else is automatically filled in by the module. In both cases you have access to the context such as the current content item's key, the related part's part definition, etc. You can use this information to only create options selectively.
+
+## Content Set Content Picker Field
+
+You can add this content field to any content item that also has a Content Set part. The field's technical name should be the same as the attached part's technical name. Besides that, no further configuration is needed. If there are available variants for a content item with this field, it will display a comma separated list of links where the option names are the link text.
diff --git a/Lombiq.HelpfulExtensions/Extensions/ContentSets/Startup.cs b/Lombiq.HelpfulExtensions/Extensions/ContentSets/Startup.cs
index ecb4456f..76136cc0 100644
--- a/Lombiq.HelpfulExtensions/Extensions/ContentSets/Startup.cs
+++ b/Lombiq.HelpfulExtensions/Extensions/ContentSets/Startup.cs
@@ -13,7 +13,7 @@
namespace Lombiq.HelpfulExtensions.Extensions.ContentSets;
[Feature(FeatureIds.ContentSets)]
-public class Startup : StartupBase
+public sealed class Startup : StartupBase
{
public override void ConfigureServices(IServiceCollection services)
{
diff --git a/Lombiq.HelpfulExtensions/Extensions/ContentSets/ViewModels/ContentSetPartViewModel.cs b/Lombiq.HelpfulExtensions/Extensions/ContentSets/ViewModels/ContentSetPartViewModel.cs
index 81bfca84..2ba3221a 100644
--- a/Lombiq.HelpfulExtensions/Extensions/ContentSets/ViewModels/ContentSetPartViewModel.cs
+++ b/Lombiq.HelpfulExtensions/Extensions/ContentSets/ViewModels/ContentSetPartViewModel.cs
@@ -8,6 +8,7 @@
using OrchardCore.ContentManagement.Metadata.Settings;
using System.Collections.Generic;
using System.Linq;
+using System.Text.Json.Nodes;
using System.Threading.Tasks;
namespace Lombiq.HelpfulExtensions.Extensions.ContentSets.ViewModels;
@@ -33,8 +34,7 @@ public class ContentSetPartViewModel
public string DisplayName =>
Definition?
.Settings?
- .Property(nameof(ContentTypePartSettings))?
- .Value
+ .GetMaybe(nameof(ContentTypePartSettings))?
.ToObject()?
.DisplayName ?? Definition?.Name;
diff --git a/Lombiq.HelpfulExtensions/Extensions/ContentSets/Workflows/Drivers/ContentSetCreatingEventDisplayDriver.cs b/Lombiq.HelpfulExtensions/Extensions/ContentSets/Workflows/Drivers/ContentSetCreatingEventDisplayDriver.cs
index e452782e..85f209de 100644
--- a/Lombiq.HelpfulExtensions/Extensions/ContentSets/Workflows/Drivers/ContentSetCreatingEventDisplayDriver.cs
+++ b/Lombiq.HelpfulExtensions/Extensions/ContentSets/Workflows/Drivers/ContentSetCreatingEventDisplayDriver.cs
@@ -10,7 +10,7 @@
namespace Lombiq.HelpfulExtensions.Extensions.ContentSets.Workflows.Drivers;
-public class ContentSetCreatingEventDisplayDriver : DocumentedEventActivityDisplayDriverBase
+public sealed class ContentSetCreatingEventDisplayDriver : DocumentedEventActivityDisplayDriverBase
{
private readonly IHtmlLocalizer H;
diff --git a/Lombiq.HelpfulExtensions/Extensions/ContentSets/Workflows/Drivers/ContentSetGetSupportedOptionsEventDisplayDriver.cs b/Lombiq.HelpfulExtensions/Extensions/ContentSets/Workflows/Drivers/ContentSetGetSupportedOptionsEventDisplayDriver.cs
index b1bcd1df..78cf7529 100644
--- a/Lombiq.HelpfulExtensions/Extensions/ContentSets/Workflows/Drivers/ContentSetGetSupportedOptionsEventDisplayDriver.cs
+++ b/Lombiq.HelpfulExtensions/Extensions/ContentSets/Workflows/Drivers/ContentSetGetSupportedOptionsEventDisplayDriver.cs
@@ -12,7 +12,7 @@
namespace Lombiq.HelpfulExtensions.Extensions.ContentSets.Workflows.Drivers;
-public class ContentSetGetSupportedOptionsEventDisplayDriver :
+public sealed class ContentSetGetSupportedOptionsEventDisplayDriver :
DocumentedEventActivityDisplayDriverBase
{
private readonly IHtmlLocalizer H;
diff --git a/Lombiq.HelpfulExtensions/Extensions/ContentSets/Workflows/Handlers/WorkflowContentSetEventHandler.cs b/Lombiq.HelpfulExtensions/Extensions/ContentSets/Workflows/Handlers/WorkflowContentSetEventHandler.cs
index f7e34c3b..91621e9d 100644
--- a/Lombiq.HelpfulExtensions/Extensions/ContentSets/Workflows/Handlers/WorkflowContentSetEventHandler.cs
+++ b/Lombiq.HelpfulExtensions/Extensions/ContentSets/Workflows/Handlers/WorkflowContentSetEventHandler.cs
@@ -4,8 +4,6 @@
using Lombiq.HelpfulExtensions.Extensions.ContentSets.Workflows.Activities;
using Lombiq.HelpfulExtensions.Extensions.ContentSets.Workflows.Models;
using Lombiq.HelpfulLibraries.OrchardCore.Workflow;
-using Newtonsoft.Json;
-using Newtonsoft.Json.Linq;
using OrchardCore.ContentManagement;
using OrchardCore.ContentManagement.Metadata.Models;
using OrchardCore.Workflows.Models;
@@ -13,6 +11,8 @@
using System.Collections.Generic;
using System.Dynamic;
using System.Linq;
+using System.Text.Json;
+using System.Text.Json.Nodes;
using System.Threading.Tasks;
namespace Lombiq.HelpfulExtensions.Extensions.ContentSets.Workflows.Handlers;
@@ -54,14 +54,16 @@ public async Task> GetSupportedOptionsAsync
case ContentSetLinkViewModel viewModel:
links.Add(viewModel);
break;
- case IEnumerable