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 collection when collection.CastWhere() is { } objects && objects.Any(): - links.AddRange(JToken.FromObject(objects).ToObject>()); - break; case ExpandoObject expandoObject: - links.Add(JToken.FromObject(expandoObject).ToObject()); + links.Add(SerializeAndDeserialize(expandoObject)); + break; + case IEnumerable collection when + collection.CastWhere().ToList() is { } objects && + objects.Count != 0: + links.AddRange(SerializeAndDeserialize>(objects)); break; case string json when !string.IsNullOrWhiteSpace(json): - links.AddRange(JsonConvert.DeserializeObject>(json)); + links.AddRange(JsonSerializer.Deserialize>(json)); break; default: continue; } @@ -79,4 +81,7 @@ public Task CreatingAsync( new CreatingContext(content, definition, contentSet, newKey), $"{nameof(WorkflowContentSetEventHandler)}.{nameof(CreatingAsync)}" + $"({content.ContentItemId}, {definition.Name}, {contentSet}, {newKey})"); + + private static T SerializeAndDeserialize(object source) => + JNode.FromObject(source).ToObject(); } diff --git a/Lombiq.HelpfulExtensions/Extensions/ContentSets/Workflows/Startup.cs b/Lombiq.HelpfulExtensions/Extensions/ContentSets/Workflows/Startup.cs index 1d8a7e8b..7b186714 100644 --- a/Lombiq.HelpfulExtensions/Extensions/ContentSets/Workflows/Startup.cs +++ b/Lombiq.HelpfulExtensions/Extensions/ContentSets/Workflows/Startup.cs @@ -10,7 +10,7 @@ namespace Lombiq.HelpfulExtensions.Extensions.ContentSets.Workflows; [Feature(FeatureIds.ContentSets)] [RequireFeatures("OrchardCore.Workflows")] -public class Startup : StartupBase +public sealed class Startup : StartupBase { public override void ConfigureServices(IServiceCollection services) { diff --git a/Lombiq.HelpfulExtensions/Extensions/ContentTypes/Migrations.cs b/Lombiq.HelpfulExtensions/Extensions/ContentTypes/Migrations.cs index 946d2380..c787a166 100644 --- a/Lombiq.HelpfulExtensions/Extensions/ContentTypes/Migrations.cs +++ b/Lombiq.HelpfulExtensions/Extensions/ContentTypes/Migrations.cs @@ -7,7 +7,7 @@ namespace Lombiq.HelpfulExtensions.Extensions.ContentTypes; -public class Migrations : DataMigration +public sealed class Migrations : DataMigration { private readonly IContentDefinitionManager _contentDefinitionManager; diff --git a/Lombiq.HelpfulExtensions/Extensions/ContentTypes/Readme.md b/Lombiq.HelpfulExtensions/Extensions/ContentTypes/Readme.md new file mode 100644 index 00000000..9e842e25 --- /dev/null +++ b/Lombiq.HelpfulExtensions/Extensions/ContentTypes/Readme.md @@ -0,0 +1,7 @@ +# Helpful Content Types + +Includes basic content types that are added by built-in Orchard Core recipes though in case of using a custom setup recipe these can be added by this feature too. + +Includes: + +- Page: Highly customizable page content type with FlowPart and AutoroutePart. diff --git a/Lombiq.HelpfulExtensions/Extensions/ContentTypes/Startup.cs b/Lombiq.HelpfulExtensions/Extensions/ContentTypes/Startup.cs index fda56a78..a61e8105 100644 --- a/Lombiq.HelpfulExtensions/Extensions/ContentTypes/Startup.cs +++ b/Lombiq.HelpfulExtensions/Extensions/ContentTypes/Startup.cs @@ -8,7 +8,7 @@ namespace Lombiq.HelpfulExtensions.Extensions.ContentTypes; [Feature(FeatureIds.ContentTypes)] -public class Startup : StartupBase +public sealed class Startup : StartupBase { public override void ConfigureServices(IServiceCollection services) => services.AddDataMigration(); diff --git a/Lombiq.HelpfulExtensions/Extensions/Emails/Extensions/EmailSenderShellScopeExtensions.cs b/Lombiq.HelpfulExtensions/Extensions/Emails/Extensions/EmailSenderShellScopeExtensions.cs index 0b3dd43f..d6629e1e 100644 --- a/Lombiq.HelpfulExtensions/Extensions/Emails/Extensions/EmailSenderShellScopeExtensions.cs +++ b/Lombiq.HelpfulExtensions/Extensions/Emails/Extensions/EmailSenderShellScopeExtensions.cs @@ -18,8 +18,8 @@ public static class EmailSenderShellScopeExtensions public static void SendEmailDeferred(this ShellScope shellScope, EmailParameters parameters) => shellScope.AddDeferredTask(async scope => { - var smtpService = scope.ServiceProvider.GetRequiredService(); - var result = await smtpService.SendAsync(new MailMessage + var emailService = scope.ServiceProvider.GetRequiredService(); + var result = await emailService.SendAsync(new MailMessage { Sender = parameters.Sender, To = parameters.To?.Join(","), diff --git a/Lombiq.HelpfulExtensions/Extensions/Emails/Readme.md b/Lombiq.HelpfulExtensions/Extensions/Emails/Readme.md new file mode 100644 index 00000000..a21830f9 --- /dev/null +++ b/Lombiq.HelpfulExtensions/Extensions/Emails/Readme.md @@ -0,0 +1,30 @@ +# Emails and Email Templates + +## Email Templates + +Provides a shape-based email template rendering service. The email templates are represented by email template IDs that are also used to identify the corresponding shape using the following pattern: `EmailTemplate__{EmailTemplateID}`. E.g., for the `ContactUs` email template you need to create a shape with the `EmailTemplate__ContactUs` shape type. + +In the email template shapes use the `Layout__EmailTemplate` as the `ViewLayout` to wrap it with a simple HTML layout. + +To extend the layout you can override the `EmailTemplate_LayoutInjections` shape and inject content to the specific zones provided by the layout to activate it in every email template. E.g., + +```html + + Best,
+ My Awesome Team +
+``` + +To add inline styles include: + +```html + + + +``` + +## Deferred email sending + +Use the `ShellScope.Current.SendEmailDeferred()` for sending emails. It'll send emails after the shell scope has ended without blocking the request. diff --git a/Lombiq.HelpfulExtensions/Extensions/Emails/Startup.cs b/Lombiq.HelpfulExtensions/Extensions/Emails/Startup.cs index 6af711be..35a68ed3 100644 --- a/Lombiq.HelpfulExtensions/Extensions/Emails/Startup.cs +++ b/Lombiq.HelpfulExtensions/Extensions/Emails/Startup.cs @@ -9,7 +9,7 @@ namespace Lombiq.HelpfulExtensions.Extensions.Emails; [Feature(FeatureIds.Emails)] -public class Startup : StartupBase +public sealed class Startup : StartupBase { public override void ConfigureServices(IServiceCollection services) { diff --git a/Lombiq.HelpfulExtensions/Extensions/Flows/Drivers/AdditionalStylingPartDisplay.cs b/Lombiq.HelpfulExtensions/Extensions/Flows/Drivers/AdditionalStylingPartDisplay.cs index f53d18f6..a106123a 100644 --- a/Lombiq.HelpfulExtensions/Extensions/Flows/Drivers/AdditionalStylingPartDisplay.cs +++ b/Lombiq.HelpfulExtensions/Extensions/Flows/Drivers/AdditionalStylingPartDisplay.cs @@ -1,21 +1,21 @@ using Lombiq.HelpfulExtensions.Extensions.Flows.Models; using OrchardCore.ContentManagement; using OrchardCore.ContentManagement.Display.ContentDisplay; -using OrchardCore.DisplayManagement.ModelBinding; +using OrchardCore.DisplayManagement.Handlers; using OrchardCore.DisplayManagement.Views; using System.Threading.Tasks; namespace Lombiq.HelpfulExtensions.Extensions.Flows.Drivers; -public class AdditionalStylingPartDisplay : ContentDisplayDriver +public sealed class AdditionalStylingPartDisplay : ContentDisplayDriver { - public override IDisplayResult Edit(ContentItem model, IUpdateModel updater) => + public override IDisplayResult Edit(ContentItem model, BuildEditorContext context) => Initialize( $"{nameof(AdditionalStylingPart)}_Edit", viewModel => PopulateViewModel(model, viewModel)) .PlaceInZone("Footer", 3); - public override async Task UpdateAsync(ContentItem model, IUpdateModel updater) + public override async Task UpdateAsync(ContentItem model, UpdateEditorContext context) { var additionalStylingPart = model.As(); @@ -24,9 +24,9 @@ public override async Task UpdateAsync(ContentItem model, IUpdat return null; } - await model.AlterAsync(model => updater.TryUpdateModelAsync(model, Prefix)); + await model.AlterAsync(model => context.Updater.TryUpdateModelAsync(model, Prefix)); - return await EditAsync(model, updater); + return await EditAsync(model, context); } private static void PopulateViewModel(ContentItem model, AdditionalStylingPart viewModel) diff --git a/Lombiq.HelpfulExtensions/Extensions/Flows/FlowPartShapeTableProvider.cs b/Lombiq.HelpfulExtensions/Extensions/Flows/FlowPartShapeTableProvider.cs index bd729f62..10464bfc 100644 --- a/Lombiq.HelpfulExtensions/Extensions/Flows/FlowPartShapeTableProvider.cs +++ b/Lombiq.HelpfulExtensions/Extensions/Flows/FlowPartShapeTableProvider.cs @@ -1,10 +1,16 @@ using OrchardCore.DisplayManagement.Descriptors; +using System.Threading.Tasks; namespace Lombiq.HelpfulExtensions.Extensions.Flows; internal sealed class FlowPartShapeTableProvider : IShapeTableProvider { - public void Discover(ShapeTableBuilder builder) => builder - .Describe("FlowPart") - .OnDisplaying(displaying => displaying.Shape.Metadata.Alternates.Add("Lombiq_HelpfulExtensions_Flows_FlowPart")); + public ValueTask DiscoverAsync(ShapeTableBuilder builder) + { + builder + .Describe("FlowPart") + .OnDisplaying(displaying => displaying.Shape.Metadata.Alternates.Add("Lombiq_HelpfulExtensions_Flows_FlowPart")); + + return ValueTask.CompletedTask; + } } diff --git a/Lombiq.HelpfulExtensions/Extensions/Flows/Readme.md b/Lombiq.HelpfulExtensions/Extensions/Flows/Readme.md new file mode 100644 index 00000000..c7ff0090 --- /dev/null +++ b/Lombiq.HelpfulExtensions/Extensions/Flows/Readme.md @@ -0,0 +1,5 @@ +# Flows Helpful Extensions + +Adds additional styling capabilities to the OrchardCore.Flows feature by making it possible to add classes to widgets in the Flow Part editor. Just add `AdditionalStylingPart` to the content type using `FlowPart`. + +![Custom classes editor on a widget contained in Flow Part.](../../../Docs/Attachments/FlowPartCustomClasses.png) diff --git a/Lombiq.HelpfulExtensions/Extensions/Flows/Startup.cs b/Lombiq.HelpfulExtensions/Extensions/Flows/Startup.cs index 3bcc4430..45dea147 100644 --- a/Lombiq.HelpfulExtensions/Extensions/Flows/Startup.cs +++ b/Lombiq.HelpfulExtensions/Extensions/Flows/Startup.cs @@ -14,7 +14,7 @@ namespace Lombiq.HelpfulExtensions.Extensions.Flows; [Feature(FeatureIds.Flows)] -public class Startup : StartupBase +public sealed class Startup : StartupBase { public override void ConfigureServices(IServiceCollection services) { diff --git a/Lombiq.HelpfulExtensions/Extensions/GoogleTag/GoogleTagLiquidParserTag.cs b/Lombiq.HelpfulExtensions/Extensions/GoogleTag/GoogleTagLiquidParserTag.cs new file mode 100644 index 00000000..303f7969 --- /dev/null +++ b/Lombiq.HelpfulExtensions/Extensions/GoogleTag/GoogleTagLiquidParserTag.cs @@ -0,0 +1,50 @@ +using Fluid; +using Fluid.Ast; +using Fluid.Values; +using Lombiq.HelpfulLibraries.OrchardCore.Liquid; +using OrchardCore.DisplayManagement.Liquid.Tags; +using System.Collections.Generic; +using System.IO; +using System.Text.Encodings.Web; +using System.Threading.Tasks; + +namespace Lombiq.HelpfulExtensions.Extensions.GoogleTag; + +public class GoogleTagLiquidParserTag : ILiquidParserTag +{ + public async ValueTask WriteToAsync( + IReadOnlyList argumentsList, + TextWriter writer, + TextEncoder encoder, + TemplateContext context) + { + var arguments = new List + { + new(null, new LiteralExpression(new StringValue(GoogleTagViewModel.ShapeType))), + }; + + foreach (var argument in argumentsList) + { + if (argument.Name == "property_id") + { + await AddStringAsync(arguments, nameof(GoogleTagViewModel.GoogleTagPropertyId), argument, context); + } + else if (argument.Name == "cookie_domain") + { + await AddStringAsync(arguments, nameof(GoogleTagViewModel.CookieDomain), argument, context); + } + } + + return await ShapeTag.WriteToAsync(arguments, writer, encoder, context); + } + + private static async Task AddStringAsync( + List arguments, + string newName, + FilterArgument argument, + TemplateContext context) + { + var newValue = await argument.Expression.EvaluateAsync(context); + arguments.Add(new(newName, new LiteralExpression(newValue))); + } +} diff --git a/Lombiq.HelpfulExtensions/Extensions/GoogleTag/GoogleTagTagHelper.cs b/Lombiq.HelpfulExtensions/Extensions/GoogleTag/GoogleTagTagHelper.cs new file mode 100644 index 00000000..366e8694 --- /dev/null +++ b/Lombiq.HelpfulExtensions/Extensions/GoogleTag/GoogleTagTagHelper.cs @@ -0,0 +1,26 @@ +using Lombiq.HelpfulLibraries.OrchardCore.TagHelpers; +using Microsoft.AspNetCore.Razor.TagHelpers; +using OrchardCore.DisplayManagement; +using System.Threading.Tasks; + +namespace Lombiq.HelpfulExtensions.Extensions.GoogleTag; + +[HtmlTargetElement("google-tag")] +public class GoogleTagTagHelper : ShapeTagHelperBase +{ + [HtmlAttributeName("property-id")] + public string PropertyId { get; set; } + + [HtmlAttributeName("cookie-domain")] + public string CookieDomain { get; set; } + + public GoogleTagTagHelper(IDisplayHelper displayHelper, IShapeFactory shapeFactory) + : base(displayHelper, shapeFactory) + { + } + + protected override string ShapeType => GoogleTagViewModel.ShapeType; + + protected override ValueTask GetViewModelAsync(TagHelperContext context, TagHelperOutput output) => + ValueTask.FromResult(new GoogleTagViewModel(PropertyId, CookieDomain)); +} diff --git a/Lombiq.HelpfulExtensions/Extensions/GoogleTag/GoogleTagViewModel.cs b/Lombiq.HelpfulExtensions/Extensions/GoogleTag/GoogleTagViewModel.cs new file mode 100644 index 00000000..8064e1c3 --- /dev/null +++ b/Lombiq.HelpfulExtensions/Extensions/GoogleTag/GoogleTagViewModel.cs @@ -0,0 +1,20 @@ +using OrchardCore.DisplayManagement.Views; + +namespace Lombiq.HelpfulExtensions.Extensions.GoogleTag; + +public class GoogleTagViewModel : ShapeViewModel +{ + public const string ShapeType = "GoogleTag"; + + public string GoogleTagPropertyId { get; set; } + public string CookieDomain { get; set; } + + public GoogleTagViewModel() => Metadata.Type = ShapeType; + + public GoogleTagViewModel(string googleTagPropertyId, string cookieDomain) + : this() + { + GoogleTagPropertyId = googleTagPropertyId; + CookieDomain = cookieDomain; + } +} diff --git a/Lombiq.HelpfulExtensions/Extensions/GoogleTag/Startup.cs b/Lombiq.HelpfulExtensions/Extensions/GoogleTag/Startup.cs new file mode 100644 index 00000000..ca3e1429 --- /dev/null +++ b/Lombiq.HelpfulExtensions/Extensions/GoogleTag/Startup.cs @@ -0,0 +1,14 @@ +using Microsoft.Extensions.DependencyInjection; +using OrchardCore.Modules; + +namespace Lombiq.HelpfulExtensions.Extensions.GoogleTag; + +[Feature(FeatureIds.GoogleTag)] +public sealed class Startup : StartupBase +{ + public override void ConfigureServices(IServiceCollection services) + { + services.AddTagHelpers(); + services.AddLiquidParserTag("google_tag"); + } +} diff --git a/Lombiq.HelpfulExtensions/Extensions/OrchardRecipeMigration/Navigation/AdminMenu.cs b/Lombiq.HelpfulExtensions/Extensions/OrchardRecipeMigration/Navigation/AdminMenu.cs index 3ada4e8a..c745bdc8 100644 --- a/Lombiq.HelpfulExtensions/Extensions/OrchardRecipeMigration/Navigation/AdminMenu.cs +++ b/Lombiq.HelpfulExtensions/Extensions/OrchardRecipeMigration/Navigation/AdminMenu.cs @@ -8,7 +8,7 @@ namespace Lombiq.HelpfulExtensions.Extensions.OrchardRecipeMigration.Navigation; -public class AdminMenu : INavigationProvider +public sealed class AdminMenu : INavigationProvider { private readonly IHttpContextAccessor _hca; private readonly IStringLocalizer T; @@ -19,9 +19,9 @@ public AdminMenu(IHttpContextAccessor hca, IStringLocalizer stringLoc T = stringLocalizer; } - public Task BuildNavigationAsync(string name, NavigationBuilder builder) + public ValueTask BuildNavigationAsync(string name, NavigationBuilder builder) { - if (!name.EqualsOrdinalIgnoreCase("admin")) return Task.CompletedTask; + if (!name.EqualsOrdinalIgnoreCase("admin")) return ValueTask.CompletedTask; builder.Add(T["Configuration"], configuration => configuration .Add(T["Import/Export"], importExport => importExport @@ -30,6 +30,6 @@ public Task BuildNavigationAsync(string name, NavigationBuilder builder) .LocalNav() ))); - return Task.CompletedTask; + return ValueTask.CompletedTask; } } diff --git a/Lombiq.HelpfulExtensions/Extensions/OrchardRecipeMigration/Readme.md b/Lombiq.HelpfulExtensions/Extensions/OrchardRecipeMigration/Readme.md new file mode 100644 index 00000000..e6d7a4e9 --- /dev/null +++ b/Lombiq.HelpfulExtensions/Extensions/OrchardRecipeMigration/Readme.md @@ -0,0 +1,32 @@ +# Orchard 1 Recipe Migration + +Contains extendable services which convert an _export.xml_ file from Orchard 1 into an Orchard Core recipe JSON file with a `content` step. Content Type exports are not supported, because the service relies on the existence of the target content types to initialize the content items that go into the recipe. + +The converter can be accessed from the Admin dashboard in the **Configuration > Import/Export > Orchard 1 Recipe Migration** menu item. + +To extend the built-in functionality implement these services: + +- `IOrchardContentConverter`: Used to set up a single new `ContentItem` using the data in the matching `` entry. +- `IOrchardExportConverter`: Used to update or filter the final list of content items. It has access to the entire export XML file. +- `IOrchardUserConverter`: Used to create a new `User` using the data in the matching `` entry. + +The built-in converters handle the following O1 content parts: + +- `CommonPart` +- `AutoroutePart` +- `BodyPart` +- `TitlePart` +- `IdentityPart` +- `ListPart` +- `GraphMetadata` (added by ) +- `UserPart` + +Additionally, if a custom converter fills in the `OrchardIds` content part's `Parent` property on the generated content item, then it also adds it to the parent content item's `ListPart`. + +## User Migration + +Migrating users from Orchard 1 is also possible with this feature: Import the same way as other Orchard 1 contents, and users will be generated automatically, meaning you don't have to do anything else. + +Each generated user will have the corresponding email, user-name, roles, and a new random password. **Keep in mind that in Orchard 1, user-names could contain any characters, but in Orchard Core, it is limited by default.** [Check out the default configuration](https://github.com/OrchardCMS/OrchardCore/blob/main/src/OrchardCore.Modules/OrchardCore.Setup/Startup.cs#L44) and adjust it in your application if needed. + +If your use-case would be different than what's done in the default user converter, implement the `IOrchardUserConverter` service, and the default one won't be executed. diff --git a/Lombiq.HelpfulExtensions/Extensions/OrchardRecipeMigration/Services/OrchardExportToRecipeConverter.cs b/Lombiq.HelpfulExtensions/Extensions/OrchardRecipeMigration/Services/OrchardExportToRecipeConverter.cs index 6c13dcef..42290669 100644 --- a/Lombiq.HelpfulExtensions/Extensions/OrchardRecipeMigration/Services/OrchardExportToRecipeConverter.cs +++ b/Lombiq.HelpfulExtensions/Extensions/OrchardRecipeMigration/Services/OrchardExportToRecipeConverter.cs @@ -1,4 +1,3 @@ -using Newtonsoft.Json.Linq; using OrchardCore.ContentManagement; using OrchardCore.ContentManagement.Metadata; using OrchardCore.Entities; @@ -6,6 +5,7 @@ using OrchardCore.Users.Models; using System.Collections.Generic; using System.Linq; +using System.Text.Json.Nodes; using System.Threading.Tasks; using System.Xml.Linq; using System.Xml.XPath; @@ -72,8 +72,8 @@ await _contentConverters await converter.UpdateContentItemsAsync(export, contentItems); } - var recipe = JObject.FromObject(new RecipeDescriptor()); - recipe["steps"] = JArray.FromObject(new[] { new { name = "content", data = contentItems } }); + var recipe = JObject.FromObject(new RecipeDescriptor())!; + recipe["steps"] = JObject.FromObject(new[] { new { name = "content", data = contentItems } }); return recipe.ToString(); } diff --git a/Lombiq.HelpfulExtensions/Extensions/OrchardRecipeMigration/Startup.cs b/Lombiq.HelpfulExtensions/Extensions/OrchardRecipeMigration/Startup.cs index fedf1f51..88cfcb6d 100644 --- a/Lombiq.HelpfulExtensions/Extensions/OrchardRecipeMigration/Startup.cs +++ b/Lombiq.HelpfulExtensions/Extensions/OrchardRecipeMigration/Startup.cs @@ -7,7 +7,7 @@ namespace Lombiq.HelpfulExtensions.Extensions.OrchardRecipeMigration; [Feature(FeatureIds.OrchardRecipeMigration)] -public class Startup : StartupBase +public sealed class Startup : StartupBase { public override void ConfigureServices(IServiceCollection services) { @@ -17,6 +17,6 @@ public override void ConfigureServices(IServiceCollection services) services.AddScoped(); services.AddScoped(); - services.AddScoped(); + services.AddNavigationProvider(); } } diff --git a/Lombiq.HelpfulExtensions/Extensions/Security/Driver/StrictSecuritySettingsDisplayDriver.cs b/Lombiq.HelpfulExtensions/Extensions/Security/Driver/StrictSecuritySettingsDisplayDriver.cs index f3b60b79..2d4e1c35 100644 --- a/Lombiq.HelpfulExtensions/Extensions/Security/Driver/StrictSecuritySettingsDisplayDriver.cs +++ b/Lombiq.HelpfulExtensions/Extensions/Security/Driver/StrictSecuritySettingsDisplayDriver.cs @@ -3,14 +3,15 @@ using OrchardCore.ContentManagement.Metadata.Models; using OrchardCore.ContentManagement.Metadata.Settings; using OrchardCore.ContentTypes.Editors; +using OrchardCore.DisplayManagement.Handlers; using OrchardCore.DisplayManagement.Views; using System.Threading.Tasks; namespace Lombiq.HelpfulExtensions.Extensions.Security.Driver; -public class StrictSecuritySettingsDisplayDriver : ContentTypeDefinitionDisplayDriver +public sealed class StrictSecuritySettingsDisplayDriver : ContentTypeDefinitionDisplayDriver { - public override IDisplayResult Edit(ContentTypeDefinition model) => + public override IDisplayResult Edit(ContentTypeDefinition model, BuildEditorContext context) => Initialize("StrictSecuritySetting_Edit", viewModel => { var settings = model.GetSettings(); @@ -20,16 +21,13 @@ public override IDisplayResult Edit(ContentTypeDefinition model) => public override async Task UpdateAsync(ContentTypeDefinition model, UpdateTypeEditorContext context) { - var viewModel = new StrictSecuritySettingsViewModel(); + var viewModel = await context.CreateModelAsync(Prefix); - if (await context.Updater.TryUpdateModelAsync(viewModel, Prefix)) - { - // Securable must be enabled for Strict Securable to make sense. Also checked on the client side too. - if (model.GetSettings()?.Securable != true) viewModel.Enabled = false; + // Securable must be enabled for Strict Securable to make sense. Also checked on the client side too. + if (model.GetSettings()?.Securable != true) viewModel.Enabled = false; - context.Builder.MergeSettings(settings => settings.Enabled = viewModel.Enabled); - } + context.Builder.MergeSettings(settings => settings.Enabled = viewModel.Enabled); - return Edit(model); + return await EditAsync(model, context); } } diff --git a/Lombiq.HelpfulExtensions/Extensions/Security/Readme.md b/Lombiq.HelpfulExtensions/Extensions/Security/Readme.md new file mode 100644 index 00000000..bbf23e00 --- /dev/null +++ b/Lombiq.HelpfulExtensions/Extensions/Security/Readme.md @@ -0,0 +1,15 @@ +# Security Extensions + +## Strict Security + +When applied to a content type definition, `StrictSecuritySetting` requires the user to have the exact Securable permission for that content type. For example if you apply it to Page, then just having the common ViewContent permission won't be enough and you must explicitly have the View_Page permission too. Don't worry, the normal implications such as ViewOwn being fulfilled by View still apply within the content type, they just no longer imply their common counterparts. + +Make content type use strict security in migration: + +```csharp +_contentDefinitionManager.AlterTypeDefinition("Page", type => type + .Securable() + .WithSettings(new StrictSecuritySettings { Enabled = true })); +``` + +You can also enable it by going to the content type editor on the admin side and checking the _Strict Securable_ checkbox. diff --git a/Lombiq.HelpfulExtensions/Extensions/Security/Startup.cs b/Lombiq.HelpfulExtensions/Extensions/Security/Startup.cs index 2afd37e9..26f68d29 100644 --- a/Lombiq.HelpfulExtensions/Extensions/Security/Startup.cs +++ b/Lombiq.HelpfulExtensions/Extensions/Security/Startup.cs @@ -9,7 +9,7 @@ namespace Lombiq.HelpfulExtensions.Extensions.Security; [Feature(FeatureIds.Security)] -public class Startup : StartupBase +public sealed class Startup : StartupBase { public override void ConfigureServices(IServiceCollection services) { diff --git a/Lombiq.HelpfulExtensions/Extensions/ShapeTracing/Readme.md b/Lombiq.HelpfulExtensions/Extensions/ShapeTracing/Readme.md new file mode 100644 index 00000000..f802853a --- /dev/null +++ b/Lombiq.HelpfulExtensions/Extensions/ShapeTracing/Readme.md @@ -0,0 +1,3 @@ +# Shape Tracing Helpful Extensions + +Adds a dump of metadata to the output about every shape. This will help you understand how a shape is displayed and how you can override it. Just check out the HTML output. You can see a video demo of this feature in action [on YouTube](https://www.youtube.com/watch?v=WI4TEKVc9SA). diff --git a/Lombiq.HelpfulExtensions/Extensions/ShapeTracing/Startup.cs b/Lombiq.HelpfulExtensions/Extensions/ShapeTracing/Startup.cs index 288ae47c..7cd3b0e3 100644 --- a/Lombiq.HelpfulExtensions/Extensions/ShapeTracing/Startup.cs +++ b/Lombiq.HelpfulExtensions/Extensions/ShapeTracing/Startup.cs @@ -5,7 +5,7 @@ namespace Lombiq.HelpfulExtensions.Extensions.ShapeTracing; [Feature(FeatureIds.ShapeTracing)] -public class Startup : StartupBase +public sealed class Startup : StartupBase { public override void ConfigureServices(IServiceCollection services) => services.AddScoped(); diff --git a/Lombiq.HelpfulExtensions/Extensions/SiteTexts/LocalizationMigrations.cs b/Lombiq.HelpfulExtensions/Extensions/SiteTexts/LocalizationMigrations.cs index f52bdd82..9f2ea27c 100644 --- a/Lombiq.HelpfulExtensions/Extensions/SiteTexts/LocalizationMigrations.cs +++ b/Lombiq.HelpfulExtensions/Extensions/SiteTexts/LocalizationMigrations.cs @@ -6,7 +6,7 @@ namespace Lombiq.HelpfulExtensions.Extensions.SiteTexts; -public class LocalizationMigrations : DataMigration +public sealed class LocalizationMigrations : DataMigration { private readonly IContentDefinitionManager _contentDefinitionManager; diff --git a/Lombiq.HelpfulExtensions/Extensions/SiteTexts/SiteTextMigrations.cs b/Lombiq.HelpfulExtensions/Extensions/SiteTexts/SiteTextMigrations.cs index b7fb7ffe..444e3dca 100644 --- a/Lombiq.HelpfulExtensions/Extensions/SiteTexts/SiteTextMigrations.cs +++ b/Lombiq.HelpfulExtensions/Extensions/SiteTexts/SiteTextMigrations.cs @@ -10,7 +10,7 @@ namespace Lombiq.HelpfulExtensions.Extensions.SiteTexts; -public class SiteTextMigrations : DataMigration +public sealed class SiteTextMigrations : DataMigration { private readonly IContentDefinitionManager _contentDefinitionManager; diff --git a/Lombiq.HelpfulExtensions/Extensions/SiteTexts/Startup.cs b/Lombiq.HelpfulExtensions/Extensions/SiteTexts/Startup.cs index 36b3bf94..0cd92cb8 100644 --- a/Lombiq.HelpfulExtensions/Extensions/SiteTexts/Startup.cs +++ b/Lombiq.HelpfulExtensions/Extensions/SiteTexts/Startup.cs @@ -6,7 +6,7 @@ namespace Lombiq.HelpfulExtensions.Extensions.SiteTexts; [Feature(FeatureIds.SiteTexts)] -public class Startup : StartupBase +public sealed class Startup : StartupBase { public override void ConfigureServices(IServiceCollection services) { @@ -17,11 +17,11 @@ public override void ConfigureServices(IServiceCollection services) } [RequireFeatures("OrchardCore.ContentLocalization")] -public class ContentLocalizationStartup : StartupBase +public sealed class ContentLocalizationStartup : StartupBase { public override void ConfigureServices(IServiceCollection services) { - services.RemoveImplementations(); + services.RemoveImplementationsOf(); services.AddScoped(); } } diff --git a/Lombiq.HelpfulExtensions/Extensions/TargetBlank/Filters/TargetBlankFilter.cs b/Lombiq.HelpfulExtensions/Extensions/TargetBlank/Filters/TargetBlankFilter.cs index d89bfc13..79204157 100644 --- a/Lombiq.HelpfulExtensions/Extensions/TargetBlank/Filters/TargetBlankFilter.cs +++ b/Lombiq.HelpfulExtensions/Extensions/TargetBlank/Filters/TargetBlankFilter.cs @@ -6,7 +6,7 @@ namespace Lombiq.HelpfulExtensions.Extensions.TargetBlank.Filters; -public class TargetBlankFilter : IAsyncResultFilter +public sealed class TargetBlankFilter : IAsyncResultFilter { private readonly IResourceManager _resourceManager; diff --git a/Lombiq.HelpfulExtensions/Extensions/TargetBlank/Readme.md b/Lombiq.HelpfulExtensions/Extensions/TargetBlank/Readme.md new file mode 100644 index 00000000..8a7a1ea7 --- /dev/null +++ b/Lombiq.HelpfulExtensions/Extensions/TargetBlank/Readme.md @@ -0,0 +1,3 @@ +# Target blank + +Gives all external links the `target="_blank"` attribute. diff --git a/Lombiq.HelpfulExtensions/Extensions/TargetBlank/Startup.cs b/Lombiq.HelpfulExtensions/Extensions/TargetBlank/Startup.cs index 4cab442a..3af621a0 100644 --- a/Lombiq.HelpfulExtensions/Extensions/TargetBlank/Startup.cs +++ b/Lombiq.HelpfulExtensions/Extensions/TargetBlank/Startup.cs @@ -11,7 +11,7 @@ namespace Lombiq.HelpfulExtensions.Extensions.TargetBlank; [Feature(FeatureIds.TargetBlank)] -public class Startup : StartupBase +public sealed class Startup : StartupBase { public override void ConfigureServices(IServiceCollection services) { diff --git a/Lombiq.HelpfulExtensions/Extensions/Trumbowyg/Readme.md b/Lombiq.HelpfulExtensions/Extensions/Trumbowyg/Readme.md new file mode 100644 index 00000000..403f5138 --- /dev/null +++ b/Lombiq.HelpfulExtensions/Extensions/Trumbowyg/Readme.md @@ -0,0 +1,47 @@ +# Trumbowyg code-snippet + +Adds prettified code-snippet inserting functionality to Trumbowyg editor by using a slightly modified version of [Trumbowyg highlight plugin](https://alex-d.github.io/Trumbowyg/documentation/plugins/#plugin-highlight). You need to add the highlight button to your Trumbowyg editor options to enable it. + +```text +{ + btns: [ + ['highlight'] + ], +} +``` + +[Prism](https://prismjs.com/) is used to prettify the code. Currently the following formats are supported: + +- clike +- cpp +- cs +- csharp +- css +- dotnet +- graphql +- html +- js +- json +- markup-templating +- mathml +- md +- plsql +- powershell +- scss +- sql +- ssml +- svg +- ts +- xml +- yaml +- yml + +Then you need to link the Trumbowyg and Prism styles and scripts where you want it to be used. E.g. if you want to add it to BlogPost content type you can do it with the help of [Lombiq.HelpfulLibraries.OrchardCore](https://github.com/Lombiq/Helpful-Libraries/blob/dev/Lombiq.HelpfulLibraries.OrchardCore/Readme.md) in a IResourceFilterProvider: + +```csharp +builder.WhenContentType("BlogPost").RegisterStylesheet(Lombiq.HelpfulExtensions.Constants.ResourceNames.Prism); +builder.WhenContentType("BlogPost").RegisterFootScript(Lombiq.HelpfulExtensions.Constants.ResourceNames.Prism); + +builder.WhenContentTypeEditor("BlogPost").RegisterFootScript(Lombiq.HelpfulExtensions.Constants.ResourceNames.TrumbowygHighlight); +builder.WhenContentTypeEditor("BlogPost").RegisterStylesheet(Lombiq.HelpfulExtensions.Constants.ResourceNames.TrumbowygHighlight); +``` diff --git a/Lombiq.HelpfulExtensions/Extensions/Trumbowyg/Startup.cs b/Lombiq.HelpfulExtensions/Extensions/Trumbowyg/Startup.cs index 339ac552..64a1b050 100644 --- a/Lombiq.HelpfulExtensions/Extensions/Trumbowyg/Startup.cs +++ b/Lombiq.HelpfulExtensions/Extensions/Trumbowyg/Startup.cs @@ -6,7 +6,7 @@ namespace Lombiq.HelpfulExtensions.Extensions.Trumbowyg; [Feature(FeatureIds.Trumbowyg)] -public class Startup : StartupBase +public sealed class Startup : StartupBase { public override void ConfigureServices(IServiceCollection services) => services.AddTransient, TrumbowygResourceManagementOptionsConfiguration>(); diff --git a/Lombiq.HelpfulExtensions/Extensions/Trumbowyg/TrumbowygResourceManagementOptionsConfiguration.cs b/Lombiq.HelpfulExtensions/Extensions/Trumbowyg/TrumbowygResourceManagementOptionsConfiguration.cs index eb026898..d9a76a60 100644 --- a/Lombiq.HelpfulExtensions/Extensions/Trumbowyg/TrumbowygResourceManagementOptionsConfiguration.cs +++ b/Lombiq.HelpfulExtensions/Extensions/Trumbowyg/TrumbowygResourceManagementOptionsConfiguration.cs @@ -1,11 +1,9 @@ using Microsoft.Extensions.Options; -using OrchardCore.Modules; using OrchardCore.ResourceManagement; using static Lombiq.HelpfulExtensions.Constants.ResourceNames; namespace Lombiq.HelpfulExtensions.Extensions.Trumbowyg; -[Feature(FeatureIds.Trumbowyg)] public class TrumbowygResourceManagementOptionsConfiguration : IConfigureOptions { private const string WwwRoot = "~/" + FeatureIds.Base + "/"; diff --git a/Lombiq.HelpfulExtensions/Extensions/Widgets/Drivers/ConditionDisplayDriver.cs b/Lombiq.HelpfulExtensions/Extensions/Widgets/Drivers/ConditionDisplayDriver.cs index 2143a528..2408be61 100644 --- a/Lombiq.HelpfulExtensions/Extensions/Widgets/Drivers/ConditionDisplayDriver.cs +++ b/Lombiq.HelpfulExtensions/Extensions/Widgets/Drivers/ConditionDisplayDriver.cs @@ -10,12 +10,12 @@ namespace Lombiq.HelpfulExtensions.Extensions.Widgets.Drivers; public abstract class ConditionDisplayDriver : DisplayDriver where TCondition : Condition { - public override IDisplayResult Display(TCondition model) => + public override IDisplayResult Display(TCondition model, BuildDisplayContext context) => Combine( InitializeDisplayType(CommonContentDisplayTypes.Summary, model), InitializeDisplayType(CommonContentDisplayTypes.Thumbnail, model)); - public override IDisplayResult Edit(TCondition model) => + public override IDisplayResult Edit(TCondition model, BuildEditorContext context) => Combine( InitializeDisplayType(CommonContentDisplayTypes.Detail, model, "Title"), GetEditor(model)); diff --git a/Lombiq.HelpfulExtensions/Extensions/Widgets/Drivers/MvcConditionDisplayDriver.cs b/Lombiq.HelpfulExtensions/Extensions/Widgets/Drivers/MvcConditionDisplayDriver.cs index 54df6e64..2693e76e 100644 --- a/Lombiq.HelpfulExtensions/Extensions/Widgets/Drivers/MvcConditionDisplayDriver.cs +++ b/Lombiq.HelpfulExtensions/Extensions/Widgets/Drivers/MvcConditionDisplayDriver.cs @@ -3,18 +3,19 @@ using Microsoft.AspNetCore.Html; using Microsoft.AspNetCore.Mvc.Localization; using Microsoft.Extensions.Localization; -using Newtonsoft.Json; -using OrchardCore.DisplayManagement.ModelBinding; +using OrchardCore.DisplayManagement.Handlers; using OrchardCore.DisplayManagement.Views; using System.Linq; +using System.Text.Json; using System.Threading.Tasks; namespace Lombiq.HelpfulExtensions.Extensions.Widgets.Drivers; -public class MvcConditionDisplayDriver : ConditionDisplayDriver +public sealed class MvcConditionDisplayDriver : ConditionDisplayDriver { private readonly IHtmlLocalizer H; private readonly IStringLocalizer T; + public MvcConditionDisplayDriver( IHtmlLocalizer htmlLocalizer, IStringLocalizer stringLocalizer) @@ -37,30 +38,28 @@ protected override IDisplayResult GetEditor(MvcCondition model) => } }).PlaceInContent(); - public override async Task UpdateAsync(MvcCondition model, IUpdateModel updater) + public override async Task UpdateAsync(MvcCondition model, UpdateEditorContext context) { - var viewModel = new MvcConditionViewModel(); - if (await updater.TryUpdateModelAsync(viewModel, Prefix)) + var viewModel = await context.CreateModelAsync(Prefix); + + if (viewModel.OtherRouteNames.Count != viewModel.OtherRouteValues.Count) { - if (viewModel.OtherRouteNames.Count != viewModel.OtherRouteValues.Count) - { - updater.ModelState.AddModelError( - nameof(viewModel.OtherRouteNames), - T["The count of other route value names didn't match the count of other route values."]); - } + context.Updater.ModelState.AddModelError( + nameof(viewModel.OtherRouteNames), + T["The count of other route value names didn't match the count of other route values."]); + } - model.Area = viewModel.Area; - model.Controller = viewModel.Controller; - model.Action = viewModel.Action; + model.Area = viewModel.Area; + model.Controller = viewModel.Controller; + model.Action = viewModel.Action; - model.OtherRouteValues.Clear(); - for (var i = 0; i < viewModel.OtherRouteNames.Count; i++) - { - model.OtherRouteValues[viewModel.OtherRouteNames[i]] = viewModel.OtherRouteValues[i]; - } + model.OtherRouteValues.Clear(); + for (var i = 0; i < viewModel.OtherRouteNames.Count; i++) + { + model.OtherRouteValues[viewModel.OtherRouteNames[i]] = viewModel.OtherRouteValues[i]; } - return Edit(model); + return await EditAsync(model, context); } protected override ConditionViewModel GetConditionViewModel(MvcCondition condition) @@ -77,7 +76,7 @@ static IHtmlContentBuilder AppendIfNotEmpty(IHtmlContentBuilder summaryHint, str if (condition.OtherRouteValues.Any()) { summaryHint = summaryHint.AppendHtml( - H["Other route values: {0}", JsonConvert.SerializeObject(condition.OtherRouteValues)]); + H["Other route values: {0}", JsonSerializer.Serialize(condition.OtherRouteValues)]); } return new ConditionViewModel diff --git a/Lombiq.HelpfulExtensions/Extensions/Widgets/Drivers/MvcConditionEvaluatorDriver.cs b/Lombiq.HelpfulExtensions/Extensions/Widgets/Drivers/MvcConditionEvaluatorDriver.cs index 7a29c2c1..019ea078 100644 --- a/Lombiq.HelpfulExtensions/Extensions/Widgets/Drivers/MvcConditionEvaluatorDriver.cs +++ b/Lombiq.HelpfulExtensions/Extensions/Widgets/Drivers/MvcConditionEvaluatorDriver.cs @@ -8,7 +8,7 @@ namespace Lombiq.HelpfulExtensions.Extensions.Widgets.Drivers; -public class MvcConditionEvaluatorDriver : ContentDisplayDriver, IConditionEvaluator +public sealed class MvcConditionEvaluatorDriver : ContentDisplayDriver, IConditionEvaluator { private readonly IHttpContextAccessor _hca; diff --git a/Lombiq.HelpfulExtensions/Extensions/Widgets/Liquid/MenuWidgetLiquidFilter.cs b/Lombiq.HelpfulExtensions/Extensions/Widgets/Liquid/MenuWidgetLiquidFilter.cs index 6254aca8..5f2e7c50 100644 --- a/Lombiq.HelpfulExtensions/Extensions/Widgets/Liquid/MenuWidgetLiquidFilter.cs +++ b/Lombiq.HelpfulExtensions/Extensions/Widgets/Liquid/MenuWidgetLiquidFilter.cs @@ -6,13 +6,14 @@ using Microsoft.AspNetCore.Mvc.Infrastructure; using Microsoft.AspNetCore.Mvc.Routing; using Microsoft.Extensions.Localization; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; using OrchardCore.Liquid; using OrchardCore.Navigation; using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; +using System.Text.Json; +using System.Text.Json.Nodes; +using System.Text.Json.Serialization; using System.Threading.Tasks; namespace Lombiq.HelpfulExtensions.Extensions.Widgets.Liquid; @@ -45,23 +46,18 @@ public ValueTask ProcessAsync(FluidValue input, FilterArguments argu localNav = arguments[nameof(localNav)].ToBooleanValue(); classes = arguments[nameof(classes)].ToStringValue(); - var converter = new LocalizedStringJsonConverter(T); - var serializer = new JsonSerializer(); - serializer.Converters.Add(converter); - var serializerSettings = new JsonSerializerSettings(); - serializerSettings.Converters.Add(converter); - + var serializerOptions = LocalizedStringJsonConverter.Add(T); var menuItems = input?.Type switch { - FluidValues.String => JsonConvert.DeserializeObject>( + FluidValues.String => JsonSerializer.Deserialize>( input!.ToStringValue(), - serializerSettings), + serializerOptions), FluidValues.Object => input!.ToObjectValue() switch { IEnumerable enumerable => enumerable.AsList(), MenuItem single => [single], - JArray jArray => jArray.ToObject>(serializer), - JObject jObject => [jObject.ToObject(serializer)], + JsonArray jsonArray => jsonArray.ToObject>(serializerOptions), + JsonObject jsonObject => [jsonObject.ToObject(serializerOptions)], _ => null, }, _ => null, @@ -102,32 +98,36 @@ public class LocalizedStringJsonConverter : JsonConverter { private readonly IStringLocalizer T; - public LocalizedStringJsonConverter(IStringLocalizer stringLocalizer) => + private LocalizedStringJsonConverter(IStringLocalizer stringLocalizer) => T = stringLocalizer; - public override void WriteJson(JsonWriter writer, LocalizedString value, JsonSerializer serializer) => - writer.WriteValue(value?.Value); + public override void Write(Utf8JsonWriter writer, LocalizedString value, JsonSerializerOptions options) => + writer.WriteStringValue(value?.Value); [SuppressMessage("Style", "IDE0010:Add missing cases", Justification = "We don't want to handle other token types.")] - public override LocalizedString ReadJson( - JsonReader reader, - Type objectType, - LocalizedString existingValue, - bool hasExistingValue, - JsonSerializer serializer) + public override LocalizedString Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) { switch (reader.TokenType) { - case JsonToken.String: - return JToken.ReadFrom(reader).ToObject() is { } text ? T[text] : null; - case JsonToken.StartObject: + case JsonTokenType.String: + return JsonSerializer.Deserialize(ref reader, options) is { } text ? T[text] : null; + case JsonTokenType.StartObject: var data = new Dictionary( - JToken.ReadFrom(reader).ToObject>(), + JsonSerializer.Deserialize>(ref reader, options), StringComparer.OrdinalIgnoreCase); return new LocalizedString(data[nameof(LocalizedString.Name)], data[nameof(LocalizedString.Value)]); default: throw new InvalidOperationException("Unable to parse JSON!"); } } + + public static JsonSerializerOptions Add(IStringLocalizer stringLocalizer, JsonSerializerOptions options = null) + { + options ??= new JsonSerializerOptions(JsonSerializerOptions.Default); + options.Converters.RemoveAll(converter => converter is JsonConverter); + options.Converters.Add(new LocalizedStringJsonConverter(stringLocalizer)); + + return options; + } } } diff --git a/Lombiq.HelpfulExtensions/Extensions/Widgets/Migrations.cs b/Lombiq.HelpfulExtensions/Extensions/Widgets/Migrations.cs index 00147782..1f6d5ffb 100644 --- a/Lombiq.HelpfulExtensions/Extensions/Widgets/Migrations.cs +++ b/Lombiq.HelpfulExtensions/Extensions/Widgets/Migrations.cs @@ -9,7 +9,7 @@ namespace Lombiq.HelpfulExtensions.Extensions.Widgets; -public class Migrations : DataMigration +public sealed class Migrations : DataMigration { private readonly IContentDefinitionManager _contentDefinitionManager; diff --git a/Lombiq.HelpfulExtensions/Extensions/Widgets/Readme.md b/Lombiq.HelpfulExtensions/Extensions/Widgets/Readme.md new file mode 100644 index 00000000..ecf32d5c --- /dev/null +++ b/Lombiq.HelpfulExtensions/Extensions/Widgets/Readme.md @@ -0,0 +1,10 @@ +# Helpful Widgets + +Adds multiple helpful widget content types. These are basic widgets that are added by built-in Orchard Core recipes though in case of using a custom setup recipe these can be added by this feature too. + +Includes: + +- ContainerWidget: Works as a container for further widgets. It has a FlowPart attached to it so it can contain additional widgets as well. +- HtmlWidget: Adds HTML editing and displaying capabilities using a WYSIWYG editor. +- LiquidWidget: Adds Liquid code editing and rendering capabilities. +- MenuWidget: Renders a Bootstrap navigation menu as a widget using the provided `MenuItem`s. diff --git a/Lombiq.HelpfulExtensions/Extensions/Widgets/Startup.cs b/Lombiq.HelpfulExtensions/Extensions/Widgets/Startup.cs index 3308e134..49c3eb32 100644 --- a/Lombiq.HelpfulExtensions/Extensions/Widgets/Startup.cs +++ b/Lombiq.HelpfulExtensions/Extensions/Widgets/Startup.cs @@ -18,7 +18,7 @@ namespace Lombiq.HelpfulExtensions.Extensions.Widgets; [Feature(FeatureIds.Widgets)] -public class Startup : StartupBase +public sealed class Startup : StartupBase { public override void ConfigureServices(IServiceCollection services) { @@ -26,7 +26,7 @@ public override void ConfigureServices(IServiceCollection services) services .AddScoped, MvcConditionDisplayDriver>() - .AddCondition>() + .AddRuleCondition>() .AddScoped(sp => (IContentDisplayDriver)sp.GetRequiredService()); services.AddTagHelpers(); diff --git a/Lombiq.HelpfulExtensions/Extensions/Workflows/Drivers/GenerateResetPasswordTokenTaskDisplayDriver.cs b/Lombiq.HelpfulExtensions/Extensions/Workflows/Drivers/GenerateResetPasswordTokenTaskDisplayDriver.cs index 32be3ce2..25496d64 100644 --- a/Lombiq.HelpfulExtensions/Extensions/Workflows/Drivers/GenerateResetPasswordTokenTaskDisplayDriver.cs +++ b/Lombiq.HelpfulExtensions/Extensions/Workflows/Drivers/GenerateResetPasswordTokenTaskDisplayDriver.cs @@ -1,16 +1,14 @@ using Lombiq.HelpfulExtensions.Extensions.Workflows.Activities; using Lombiq.HelpfulExtensions.Extensions.Workflows.ViewModels; using Microsoft.Extensions.Localization; -using OrchardCore.DisplayManagement.ModelBinding; +using OrchardCore.DisplayManagement.Handlers; using OrchardCore.DisplayManagement.Views; -using OrchardCore.Users.Models; using OrchardCore.Workflows.Display; -using OrchardCore.Workflows.Models; using System.Threading.Tasks; namespace Lombiq.HelpfulExtensions.Extensions.Workflows.Drivers; -public class GenerateResetPasswordTokenTaskDisplayDriver : ActivityDisplayDriver< +public sealed class GenerateResetPasswordTokenTaskDisplayDriver : ActivityDisplayDriver< GenerateResetPasswordTokenTask, GenerateResetPasswordTokenTaskViewModel> { @@ -26,16 +24,14 @@ protected override void EditActivity(GenerateResetPasswordTokenTask activity, Ge model.ResetPasswordUrlPropertyKey = activity.ResetPasswordUrlPropertyKey; } - public override async Task UpdateAsync(GenerateResetPasswordTokenTask model, IUpdateModel updater) + public override async Task UpdateAsync(GenerateResetPasswordTokenTask activity, UpdateEditorContext context) { - var viewModel = new GenerateResetPasswordTokenTaskViewModel(); - if (await updater.TryUpdateModelAsync(viewModel, Prefix)) - { - model.User = new WorkflowExpression(viewModel.UserExpression); - model.ResetPasswordTokenPropertyKey = viewModel.ResetPasswordTokenPropertyKey; - model.ResetPasswordUrlPropertyKey = viewModel.ResetPasswordUrlPropertyKey; - } + var viewModel = await context.CreateModelAsync(Prefix); - return Edit(model); + activity.User = new(viewModel.UserExpression); + activity.ResetPasswordTokenPropertyKey = viewModel.ResetPasswordTokenPropertyKey; + activity.ResetPasswordUrlPropertyKey = viewModel.ResetPasswordUrlPropertyKey; + + return await EditAsync(activity, context); } } diff --git a/Lombiq.HelpfulExtensions/Extensions/Workflows/Readme.md b/Lombiq.HelpfulExtensions/Extensions/Workflows/Readme.md new file mode 100644 index 00000000..35e74192 --- /dev/null +++ b/Lombiq.HelpfulExtensions/Extensions/Workflows/Readme.md @@ -0,0 +1,3 @@ +# Reset Password activity + +Adds a workflow activity that generates a reset password token for the specified user. You can define the source of the User object using a JavaScript expression. It will set the token and the URL to the workflow `LastResult` property and optionally it can set them to the `Properties` dictionary to a key that you define as an activity parameter. diff --git a/Lombiq.HelpfulExtensions/Extensions/Workflows/Startup.cs b/Lombiq.HelpfulExtensions/Extensions/Workflows/Startup.cs index 0a71351e..07a89b2f 100644 --- a/Lombiq.HelpfulExtensions/Extensions/Workflows/Startup.cs +++ b/Lombiq.HelpfulExtensions/Extensions/Workflows/Startup.cs @@ -9,7 +9,7 @@ namespace Lombiq.HelpfulExtensions.Extensions.Workflows; [Feature(FeatureIds.ResetPasswordActivity)] -public class Startup : StartupBase +public sealed class Startup : StartupBase { public override void ConfigureServices(IServiceCollection services) { diff --git a/Lombiq.HelpfulExtensions/FeatureIds.cs b/Lombiq.HelpfulExtensions/FeatureIds.cs index 3cd3b3ee..180370db 100644 --- a/Lombiq.HelpfulExtensions/FeatureIds.cs +++ b/Lombiq.HelpfulExtensions/FeatureIds.cs @@ -19,4 +19,5 @@ public static class FeatureIds public const string Workflows = FeatureIdPrefix + nameof(Workflows); public const string Trumbowyg = FeatureIdPrefix + nameof(Trumbowyg); public const string ResetPasswordActivity = Workflows + "." + nameof(ResetPasswordActivity); + public const string GoogleTag = FeatureIdPrefix + nameof(GoogleTag); } diff --git a/Lombiq.HelpfulExtensions/Lombiq.HelpfulExtensions.csproj b/Lombiq.HelpfulExtensions/Lombiq.HelpfulExtensions.csproj index c3a4c031..22b4df16 100644 --- a/Lombiq.HelpfulExtensions/Lombiq.HelpfulExtensions.csproj +++ b/Lombiq.HelpfulExtensions/Lombiq.HelpfulExtensions.csproj @@ -35,18 +35,18 @@ - - - - - - - - - - - - + + + + + + + + + + + + @@ -56,7 +56,7 @@ - + diff --git a/Lombiq.HelpfulExtensions/Manifest.cs b/Lombiq.HelpfulExtensions/Manifest.cs index 29ff8210..265e8f3a 100644 --- a/Lombiq.HelpfulExtensions/Manifest.cs +++ b/Lombiq.HelpfulExtensions/Manifest.cs @@ -89,6 +89,7 @@ Dependencies = [ "OrchardCore.Email", + "OrchardCore.Email.Smtp", ] )] @@ -145,3 +146,10 @@ Category = "Content", Description = "Adds option for inserting code snippets in Trumbowyg editor." )] + +[assembly: Feature( + Id = GoogleTag, + Name = "Lombiq Helpful Extensions - Google Tag", + Category = "Content", + Description = "Adds a shape along with Razor and Liquid tag helpers for Google Analytics." +)] diff --git a/Lombiq.HelpfulExtensions/Views/ContentSetContentPickerField.cshtml b/Lombiq.HelpfulExtensions/Views/ContentSetContentPickerField.cshtml index 4e5884f1..62ea50f2 100644 --- a/Lombiq.HelpfulExtensions/Views/ContentSetContentPickerField.cshtml +++ b/Lombiq.HelpfulExtensions/Views/ContentSetContentPickerField.cshtml @@ -2,12 +2,13 @@ @using OrchardCore @using OrchardCore.ContentManagement.Metadata.Settings @using OrchardCore.Mvc.Utilities +@using System.Text.Json.Nodes @{ var name = (Model.PartFieldDefinition.PartDefinition.Name + "-" + Model.PartFieldDefinition.Name).HtmlClassify(); - var settings = Model.PartFieldDefinition.Settings.GetMaybe(nameof(ContentPartFieldSettings)); - var displayName = settings?.ToObject()?.DisplayName ?? Model.PartFieldDefinition.Name; + var settings = Model.PartFieldDefinition.Settings?.ToObject(); + var displayName = settings?.DisplayName ?? Model.PartFieldDefinition.Name; var links = Model .MemberLinks diff --git a/Lombiq.HelpfulExtensions/Views/ContentSetPart.MemberLink.cshtml b/Lombiq.HelpfulExtensions/Views/ContentSetPart.MemberLink.cshtml index bc6f1ec2..aa371ffc 100644 --- a/Lombiq.HelpfulExtensions/Views/ContentSetPart.MemberLink.cshtml +++ b/Lombiq.HelpfulExtensions/Views/ContentSetPart.MemberLink.cshtml @@ -13,8 +13,7 @@
  • @if (!string.IsNullOrEmpty(link.ContentItemId)) { - var url = Orchard.Action(controller => controller.Edit( - link.ContentItemId)); + var url = Orchard.Action(controller => controller.Edit(link.ContentItemId)); @link.DisplayText diff --git a/Lombiq.HelpfulExtensions/Views/GoogleTag.cshtml b/Lombiq.HelpfulExtensions/Views/GoogleTag.cshtml new file mode 100644 index 00000000..2d592612 --- /dev/null +++ b/Lombiq.HelpfulExtensions/Views/GoogleTag.cshtml @@ -0,0 +1,38 @@ +@model dynamic + +@using Lombiq.HelpfulLibraries.OrchardCore.Security +@using Microsoft.AspNetCore.Http.Features; +@using Microsoft.Extensions.Hosting; +@using Lombiq.HelpfulExtensions.Extensions.GoogleTag + +@inject IHostEnvironment HostEnvironment + +@{ + var trackingConsentFeature = ViewContext.HttpContext.Features.Get(); +} + +@if (HostEnvironment.IsProduction() && trackingConsentFeature is not { CanTrack: false }) +{ + GoogleAnalyticsContentSecurityPolicyProvider.EnableForCurrentRequest(Context); + + var viewModel = Model as GoogleTagViewModel ?? new GoogleTagViewModel + { + GoogleTagPropertyId = Model.GoogleTagPropertyId, + CookieDomain = Model.CookieDomain, + }; + var cookieDomain = string.Empty; + + if (!string.IsNullOrEmpty(viewModel.CookieDomain)) + { + cookieDomain = $", {{'cookie_domain': '{viewModel.CookieDomain}' }}"; + } + + + +} diff --git a/Lombiq.HelpfulExtensions/Views/MvcCondition.Fields.Edit.cshtml b/Lombiq.HelpfulExtensions/Views/MvcCondition.Fields.Edit.cshtml index f75783b8..34df5816 100644 --- a/Lombiq.HelpfulExtensions/Views/MvcCondition.Fields.Edit.cshtml +++ b/Lombiq.HelpfulExtensions/Views/MvcCondition.Fields.Edit.cshtml @@ -1,4 +1,5 @@ -@using Newtonsoft.Json +@using System.Text.Json +@using Lombiq.HelpfulLibraries.OrchardCore.TagHelpers @model MvcConditionViewModel @{ @@ -19,7 +20,7 @@
    diff --git a/Lombiq.HelpfulExtensions/Views/StrictSecuritySetting.Edit.cshtml b/Lombiq.HelpfulExtensions/Views/StrictSecuritySetting.Edit.cshtml index d590018f..d6effe07 100644 --- a/Lombiq.HelpfulExtensions/Views/StrictSecuritySetting.Edit.cshtml +++ b/Lombiq.HelpfulExtensions/Views/StrictSecuritySetting.Edit.cshtml @@ -14,7 +14,7 @@