Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/dev' into issue/OFFI-119
Browse files Browse the repository at this point in the history
  • Loading branch information
wAsnk committed Sep 30, 2024
2 parents 836ae44 + ff2e958 commit 58bde60
Show file tree
Hide file tree
Showing 70 changed files with 613 additions and 446 deletions.
Original file line number Diff line number Diff line change
@@ -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);
}
Original file line number Diff line number Diff line change
Expand Up @@ -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"));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
</ItemGroup>

<ItemGroup Condition="'$(NuGetBuild)' == 'true'">
<PackageReference Include="Lombiq.Tests.UI" Version="10.0.1" />
<PackageReference Include="Lombiq.Tests.UI" Version="11.0.0" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
@@ -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<CodeGenerationDisplayDriver> stringLocalizer) =>
T = stringLocalizer;

public override IDisplayResult Edit(ContentTypeDefinition model) =>
public override IDisplayResult Edit(ContentTypeDefinition model, BuildEditorContext context) =>
Initialize<ContentTypeMigrationsViewModel>(
"ContentTypeMigrations_Edit",
viewModel => viewModel.MigrationCodeLazy = new Lazy<string>(() =>
Expand All @@ -37,7 +41,7 @@ public override IDisplayResult Edit(ContentTypeDefinition model) =>
codeBuilder.AppendLine(CultureInfo.InvariantCulture, $" .DisplayedAs(\"{model.DisplayName}\")");

GenerateCodeForSettings(codeBuilder, model.GetSettings<ContentTypeSettings>());
AddSettingsWithout<ContentTypeSettings>(codeBuilder, model.Settings, 4);
AddSettingsWithout<ContentTypeSettings>(codeBuilder, model.Settings);
GenerateCodeForParts(codeBuilder, model.Parts);
codeBuilder.AppendLine(");");

Expand All @@ -63,7 +67,7 @@ private void GenerateCodeForParts(StringBuilder codeBuilder, IEnumerable<Content
AddWithLine(codeBuilder, nameof(partSettings.DisplayMode), partSettings.DisplayMode);
AddWithLine(codeBuilder, nameof(partSettings.Editor), partSettings.Editor);

AddSettingsWithout<ContentTypePartSettings>(codeBuilder, part.Settings, 8);
AddSettingsWithout<ContentTypePartSettings>(codeBuilder, part.Settings, 2 * IndentationDepth);

// Checking if anything was added to the part's settings.
if (codeBuilder.Length == partStartingLength)
Expand Down Expand Up @@ -102,7 +106,7 @@ private void GenerateCodeForPartsWithFields(
AddWithLine(codeBuilder, nameof(partSettings.Description), partSettings.Description);
AddWithLine(codeBuilder, nameof(partSettings.DefaultPosition), partSettings.DefaultPosition);

AddSettingsWithout<ContentPartSettings>(codeBuilder, part.Settings, 4);
AddSettingsWithout<ContentPartSettings>(codeBuilder, part.Settings);

foreach (var field in part.Fields)
{
Expand All @@ -116,7 +120,7 @@ private void GenerateCodeForPartsWithFields(
AddWithLine(codeBuilder, nameof(fieldSettings.DisplayMode), fieldSettings.DisplayMode);
AddWithLine(codeBuilder, nameof(fieldSettings.Position), fieldSettings.Position);

AddSettingsWithout<ContentPartFieldSettings>(codeBuilder, field.Settings, 8);
AddSettingsWithout<ContentPartFieldSettings>(codeBuilder, field.Settings, 2 * IndentationDepth);

codeBuilder.AppendLine(" )");
}
Expand All @@ -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)))
Expand All @@ -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)
{
Expand All @@ -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<T>(StringBuilder codeBuilder, JObject settings, int indentationDepth)
private void AddSettingsWithout<T>(StringBuilder codeBuilder, JsonObject settings, int indentationDepth = IndentationDepth)
{
var indentation = new string(' ', indentationDepth);

var filteredSettings = ((IEnumerable<KeyValuePair<string, JToken>>)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<JProperty>().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 + "})");
Expand Down
7 changes: 7 additions & 0 deletions Lombiq.HelpfulExtensions/Extensions/CodeGeneration/Readme.md
Original file line number Diff line number Diff line change
@@ -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)
Original file line number Diff line number Diff line change
Expand Up @@ -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<IContentTypeDefinitionDisplayDriver, CodeGenerationDisplayDriver>();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@

namespace Lombiq.HelpfulExtensions.Extensions.ContentSets.Drivers;

public class ContentSetContentPickerFieldDisplayDriver : ContentFieldDisplayDriver<ContentSetContentPickerField>
public sealed class ContentSetContentPickerFieldDisplayDriver : ContentFieldDisplayDriver<ContentSetContentPickerField>
{
private readonly IContentDefinitionManager _contentDefinitionManager;
private readonly IContentSetManager _contentSetManager;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,15 @@
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;
using System.Threading.Tasks;

namespace Lombiq.HelpfulExtensions.Extensions.ContentSets.Drivers;

public class ContentSetPartDisplayDriver : ContentPartDisplayDriver<ContentSetPart>
public sealed class ContentSetPartDisplayDriver : ContentPartDisplayDriver<ContentSetPart>
{
private const string ShapeType = $"{nameof(ContentSetPart)}_{CommonContentDisplayTypes.SummaryAdmin}";

Expand Down Expand Up @@ -64,23 +64,17 @@ public override IDisplayResult Edit(ContentSetPart part, BuildPartEditorContext
context.IsNew))
.Location($"Parts:0%{context.TypePartDefinition.Name};0");

public override async Task<IDisplayResult> UpdateAsync(
ContentSetPart part,
IUpdateModel updater,
UpdatePartEditorContext context)
public override async Task<IDisplayResult> UpdateAsync(ContentSetPart part, UpdatePartEditorContext context)
{
var viewModel = new ContentSetPartViewModel();
var viewModel = await context.CreateModelAsync<ContentSetPartViewModel>(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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

namespace Lombiq.HelpfulExtensions.Extensions.ContentSets;

public class Migrations : DataMigration
public sealed class Migrations : DataMigration
{
private readonly IContentDefinitionManager _contentDefinitionManager;

Expand Down
Loading

0 comments on commit 58bde60

Please sign in to comment.