diff --git a/src/Umbraco.Core/Models/Blocks/BlockEditorData.cs b/src/Umbraco.Core/Models/Blocks/BlockEditorData.cs index 633d509487..8671f7b887 100644 --- a/src/Umbraco.Core/Models/Blocks/BlockEditorData.cs +++ b/src/Umbraco.Core/Models/Blocks/BlockEditorData.cs @@ -40,7 +40,7 @@ public class BlockEditorData /// /// Returns the layout for this specific property editor /// - public IEnumerable? Layout => BlockValue.Layout.TryGetValue(_propertyEditorAlias, out IEnumerable? layout) ? layout : null; + public IEnumerable? Layout => BlockValue.GetLayouts(_propertyEditorAlias); /// /// Returns the reference to the original BlockValue diff --git a/src/Umbraco.Core/Models/Blocks/BlockEditorDataConverter.cs b/src/Umbraco.Core/Models/Blocks/BlockEditorDataConverter.cs index 735c06196b..c68963b7cd 100644 --- a/src/Umbraco.Core/Models/Blocks/BlockEditorDataConverter.cs +++ b/src/Umbraco.Core/Models/Blocks/BlockEditorDataConverter.cs @@ -15,21 +15,23 @@ public abstract class BlockEditorDataConverter where TValue : BlockValue, new() where TLayout : class, IBlockLayoutItem, new() { - private readonly string _propertyEditorAlias; private readonly IJsonSerializer _jsonSerializer; - [Obsolete("Use the constructor that takes IJsonSerializer. Will be removed in V15.")] + [Obsolete("Use the non-obsolete constructor. Will be removed in V15.")] protected BlockEditorDataConverter(string propertyEditorAlias) : this(propertyEditorAlias, StaticServiceProvider.Instance.GetRequiredService()) { } + [Obsolete("Use the non-obsolete constructor. Will be removed in V15.")] protected BlockEditorDataConverter(string propertyEditorAlias, IJsonSerializer jsonSerializer) + : this(jsonSerializer) { - _propertyEditorAlias = propertyEditorAlias; - _jsonSerializer = jsonSerializer; } + protected BlockEditorDataConverter(IJsonSerializer jsonSerializer) + => _jsonSerializer = jsonSerializer; + public bool TryDeserialize(string json, [MaybeNullWhen(false)] out BlockEditorData blockEditorData) { try @@ -60,16 +62,15 @@ public abstract class BlockEditorDataConverter public BlockEditorData Convert(TValue? value) { - if (value?.Layout == null) + var propertyEditorAlias = new TValue().PropertyEditorAlias; + IEnumerable? layouts = value?.GetLayouts(propertyEditorAlias); + if (layouts is null) { return BlockEditorData.Empty; } - IEnumerable references = - value.Layout.TryGetValue(_propertyEditorAlias, out IEnumerable? layout) - ? GetBlockReferences(layout) - : Enumerable.Empty(); + IEnumerable references = GetBlockReferences(layouts); - return new BlockEditorData(_propertyEditorAlias, references, value); + return new BlockEditorData(propertyEditorAlias, references, value!); } } diff --git a/src/Umbraco.Core/Models/Blocks/BlockGridEditorDataConverter.cs b/src/Umbraco.Core/Models/Blocks/BlockGridEditorDataConverter.cs index b28a10c995..b771ed1e3c 100644 --- a/src/Umbraco.Core/Models/Blocks/BlockGridEditorDataConverter.cs +++ b/src/Umbraco.Core/Models/Blocks/BlockGridEditorDataConverter.cs @@ -19,7 +19,7 @@ public class BlockGridEditorDataConverter : BlockEditorDataConverter { + public override string PropertyEditorAlias => Constants.PropertyEditors.Aliases.BlockGrid; } diff --git a/src/Umbraco.Core/Models/Blocks/BlockListValue.cs b/src/Umbraco.Core/Models/Blocks/BlockListValue.cs index b39c6f4aee..06d7a6126e 100644 --- a/src/Umbraco.Core/Models/Blocks/BlockListValue.cs +++ b/src/Umbraco.Core/Models/Blocks/BlockListValue.cs @@ -2,4 +2,5 @@ public class BlockListValue : BlockValue { + public override string PropertyEditorAlias => Constants.PropertyEditors.Aliases.BlockList; } diff --git a/src/Umbraco.Core/Models/Blocks/BlockValue.cs b/src/Umbraco.Core/Models/Blocks/BlockValue.cs index d1182765dd..7b9e88d468 100644 --- a/src/Umbraco.Core/Models/Blocks/BlockValue.cs +++ b/src/Umbraco.Core/Models/Blocks/BlockValue.cs @@ -3,11 +3,22 @@ namespace Umbraco.Cms.Core.Models.Blocks; -public abstract class BlockValue where TLayout : IBlockLayoutItem +public abstract class BlockValue : BlockValue + where TLayout : IBlockLayoutItem { - public IDictionary> Layout { get; set; } = null!; + public IEnumerable? GetLayouts(string propertyEditorAlias) + => Layout.TryGetValue(propertyEditorAlias, out IEnumerable? layouts) is true + ? layouts.OfType() + : null; +} + +public abstract class BlockValue +{ + public IDictionary> Layout { get; set; } = new Dictionary>(); public List ContentData { get; set; } = new(); public List SettingsData { get; set; } = new(); + + public abstract string PropertyEditorAlias { get; } } diff --git a/src/Umbraco.Core/Models/Blocks/RichTextBlockValue.cs b/src/Umbraco.Core/Models/Blocks/RichTextBlockValue.cs index a951a21a28..60b3c78e14 100644 --- a/src/Umbraco.Core/Models/Blocks/RichTextBlockValue.cs +++ b/src/Umbraco.Core/Models/Blocks/RichTextBlockValue.cs @@ -2,4 +2,5 @@ namespace Umbraco.Cms.Core.Models.Blocks; public class RichTextBlockValue : BlockValue { + public override string PropertyEditorAlias => Constants.PropertyEditors.Aliases.TinyMce; } diff --git a/src/Umbraco.Core/Models/Blocks/RichTextEditorBlockDataConverter.cs b/src/Umbraco.Core/Models/Blocks/RichTextEditorBlockDataConverter.cs index ec149cd381..183dabe420 100644 --- a/src/Umbraco.Core/Models/Blocks/RichTextEditorBlockDataConverter.cs +++ b/src/Umbraco.Core/Models/Blocks/RichTextEditorBlockDataConverter.cs @@ -1,15 +1,23 @@ -namespace Umbraco.Cms.Core.Models.Blocks; +using Umbraco.Cms.Core.Serialization; + +namespace Umbraco.Cms.Core.Models.Blocks; /// /// Data converter for blocks in the richtext property editor /// public sealed class RichTextEditorBlockDataConverter : BlockEditorDataConverter { + [Obsolete("Use the constructor that takes IJsonSerializer. Will be removed in V15.")] public RichTextEditorBlockDataConverter() : base(Constants.PropertyEditors.Aliases.TinyMce) { } + public RichTextEditorBlockDataConverter(IJsonSerializer jsonSerializer) + : base(jsonSerializer) + { + } + protected override IEnumerable GetBlockReferences(IEnumerable layout) => layout.Select(x => new ContentAndSettingsReference(x.ContentUdi, x.SettingsUdi)).ToList(); } diff --git a/src/Umbraco.Infrastructure/PropertyEditors/BlockValuePropertyIndexValueFactory.cs b/src/Umbraco.Infrastructure/PropertyEditors/BlockValuePropertyIndexValueFactory.cs index 92fbe5c20b..c5cbb8d79e 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/BlockValuePropertyIndexValueFactory.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/BlockValuePropertyIndexValueFactory.cs @@ -3,11 +3,9 @@ using Microsoft.Extensions.Options; using Umbraco.Cms.Core.Configuration.Models; -using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.Blocks; using Umbraco.Cms.Core.Serialization; -using Umbraco.Cms.Core.Services; namespace Umbraco.Cms.Core.PropertyEditors; @@ -23,16 +21,6 @@ internal sealed class BlockValuePropertyIndexValueFactory : { } - [Obsolete("Use constructor that doesn't take IContentTypeService, scheduled for removal in V15")] - public BlockValuePropertyIndexValueFactory( - PropertyEditorCollection propertyEditorCollection, - IContentTypeService contentTypeService, - IJsonSerializer jsonSerializer, - IOptionsMonitor indexingSettings) - : this(propertyEditorCollection, jsonSerializer, indexingSettings) - { - } - protected override IContentType? GetContentTypeOfNestedItem(BlockItemData input, IDictionary contentTypeDictionary) => contentTypeDictionary.TryGetValue(input.ContentTypeKey, out var result) ? result : null; @@ -41,14 +29,9 @@ internal sealed class BlockValuePropertyIndexValueFactory : protected override IEnumerable GetDataItems(IndexValueFactoryBlockValue input) => input.ContentData; - internal class IndexValueFactoryBlockValue : BlockValue + // we only care about the content data when extracting values for indexing - not the layouts nor the settings + internal class IndexValueFactoryBlockValue { - } - - internal class IndexValueFactoryBlockLayoutItem : IBlockLayoutItem - { - public Udi? ContentUdi { get; set; } - - public Udi? SettingsUdi { get; set; } + public List ContentData { get; set; } = new(); } } diff --git a/src/Umbraco.Infrastructure/PropertyEditors/RichTextPropertyEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/RichTextPropertyEditor.cs index de69f23f8e..461c32ebb9 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/RichTextPropertyEditor.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/RichTextPropertyEditor.cs @@ -274,6 +274,6 @@ public class RichTextPropertyEditor : DataEditor } private BlockEditorValues CreateBlockEditorValues() - => new(new RichTextEditorBlockDataConverter(), _contentTypeService, _logger); + => new(new RichTextEditorBlockDataConverter(_jsonSerializer), _contentTypeService, _logger); } } diff --git a/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/RichTextBlockPropertyValueCreator.cs b/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/RichTextBlockPropertyValueCreator.cs index 2c9f36192d..9ec91fa0de 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/RichTextBlockPropertyValueCreator.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/RichTextBlockPropertyValueCreator.cs @@ -2,16 +2,24 @@ // See LICENSE for more details. using Umbraco.Cms.Core.Models.Blocks; +using Umbraco.Cms.Core.Serialization; namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters; internal class RichTextBlockPropertyValueCreator : BlockPropertyValueCreatorBase { + private readonly IJsonSerializer _jsonSerializer; private readonly RichTextBlockPropertyValueConstructorCache _constructorCache; - public RichTextBlockPropertyValueCreator(BlockEditorConverter blockEditorConverter, RichTextBlockPropertyValueConstructorCache constructorCache) + public RichTextBlockPropertyValueCreator( + BlockEditorConverter blockEditorConverter, + IJsonSerializer jsonSerializer, + RichTextBlockPropertyValueConstructorCache constructorCache) : base(blockEditorConverter) - => _constructorCache = constructorCache; + { + _jsonSerializer = jsonSerializer; + _constructorCache = constructorCache; + } public RichTextBlockModel CreateBlockModel(PropertyCacheLevel referenceCacheLevel, RichTextBlockValue blockValue, bool preview, RichTextConfiguration.RichTextBlockConfiguration[] blockConfigurations) { @@ -24,7 +32,7 @@ internal class RichTextBlockPropertyValueCreator : BlockPropertyValueCreatorBase return blockModel; } - protected override BlockEditorDataConverter CreateBlockEditorDataConverter() => new RichTextEditorBlockDataConverter(); + protected override BlockEditorDataConverter CreateBlockEditorDataConverter() => new RichTextEditorBlockDataConverter(_jsonSerializer); protected override BlockItemActivator CreateBlockItemActivator() => new RichTextBlockItemActivator(BlockEditorConverter, _constructorCache); diff --git a/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/RteBlockRenderingValueConverter.cs b/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/RteBlockRenderingValueConverter.cs index 9fc9cd00cd..8a0ec3ba4a 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/RteBlockRenderingValueConverter.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/RteBlockRenderingValueConverter.cs @@ -189,7 +189,7 @@ public class RteBlockRenderingValueConverter : SimpleTinyMceValueConverter, IDel return null; } - var creator = new RichTextBlockPropertyValueCreator(_blockEditorConverter, _constructorCache); + var creator = new RichTextBlockPropertyValueCreator(_blockEditorConverter, _jsonSerializer, _constructorCache); return creator.CreateBlockModel(referenceCacheLevel, blocks, preview, configuration.Blocks); } diff --git a/src/Umbraco.Infrastructure/Serialization/JsonBlockValueConverter.cs b/src/Umbraco.Infrastructure/Serialization/JsonBlockValueConverter.cs new file mode 100644 index 0000000000..1e6f9e6898 --- /dev/null +++ b/src/Umbraco.Infrastructure/Serialization/JsonBlockValueConverter.cs @@ -0,0 +1,189 @@ +using System.Text.Json; +using System.Text.Json.Serialization; +using Umbraco.Cms.Core.Models.Blocks; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Infrastructure.Serialization; + +/// +/// JSON converter for block values, because block value layouts are strongly typed but different from implementation to implementation. +/// +public class JsonBlockValueConverter : JsonConverter +{ + public override bool CanConvert(Type typeToConvert) => typeToConvert.IsAssignableTo(typeof(BlockValue)); + + /// + public override BlockValue Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) + { + if (reader.TokenType != JsonTokenType.StartObject) + { + throw new JsonException("Expected start object"); + } + + BlockValue? blockValue; + try + { + blockValue = (BlockValue?)Activator.CreateInstance(typeToConvert); + } + catch (Exception ex) + { + throw new JsonException($"Unable to create an instance of {nameof(BlockValue)} from type: {typeToConvert.FullName}. Please make sure the type has an default (parameterless) constructor. See the inner exception for more details.", ex); + } + + if (blockValue is null) + { + throw new JsonException($"Could not create an instance of {nameof(BlockValue)} from type: {typeToConvert.FullName}."); + } + + while (reader.Read()) + { + if (reader.TokenType is JsonTokenType.EndObject) + { + break; + } + + if (reader.TokenType is JsonTokenType.PropertyName) + { + var propertyName = reader.GetString(); + if (propertyName is null) + { + continue; + } + + switch (propertyName.ToFirstUpperInvariant()) + { + case nameof(BlockValue.ContentData): + blockValue.ContentData = DeserializeBlockItemData(ref reader, options, typeToConvert, nameof(BlockValue.ContentData)); + break; + case nameof(BlockValue.SettingsData): + blockValue.SettingsData = DeserializeBlockItemData(ref reader, options, typeToConvert, nameof(BlockValue.SettingsData)); + break; + case nameof(BlockValue.Layout): + DeserializeAndSetLayout(ref reader, options, typeToConvert, blockValue); + break; + } + } + } + + return blockValue; + } + + public override void Write(Utf8JsonWriter writer, BlockValue value, JsonSerializerOptions options) + { + value.Layout.TryGetValue(value.PropertyEditorAlias, out IEnumerable? blockLayoutItems); + blockLayoutItems ??= Enumerable.Empty(); + + writer.WriteStartObject(); + + writer.WritePropertyName(nameof(BlockValue.ContentData).ToFirstLowerInvariant()); + JsonSerializer.Serialize(writer, value.ContentData, options); + + if (value.SettingsData is not null) + { + writer.WritePropertyName(nameof(BlockValue.SettingsData).ToFirstLowerInvariant()); + JsonSerializer.Serialize(writer, value.SettingsData, options); + } + + Type layoutItemType = GetLayoutItemType(value.GetType()); + + writer.WriteStartObject(nameof(BlockValue.Layout)); + + if (blockLayoutItems.Any()) + { + writer.WriteStartArray(value.PropertyEditorAlias); + foreach (IBlockLayoutItem blockLayoutItem in blockLayoutItems) + { + JsonSerializer.Serialize(writer, blockLayoutItem, layoutItemType, options); + } + writer.WriteEndArray(); + } + + writer.WriteEndObject(); + + writer.WriteEndObject(); + } + + private static Type GetLayoutItemType(Type blockValueType) + { + Type? layoutItemType = blockValueType.BaseType?.GenericTypeArguments.FirstOrDefault(); + if (layoutItemType is null || layoutItemType.Implements() is false) + { + throw new JsonException($"The {nameof(BlockValue)} implementation should have an {nameof(IBlockLayoutItem)} type as its first generic type argument - found: {layoutItemType?.FullName ?? "none"}."); + } + + return layoutItemType; + } + + private List DeserializeBlockItemData(ref Utf8JsonReader reader, JsonSerializerOptions options, Type typeToConvert, string propertyName) + => JsonSerializer.Deserialize>(ref reader, options) + ?? throw new JsonException($"Unable to deserialize {propertyName} from type: {typeToConvert.FullName}."); + + private void DeserializeAndSetLayout(ref Utf8JsonReader reader, JsonSerializerOptions options, Type typeToConvert, BlockValue blockValue) + { + // the block editor layouts collection can contain layouts from any number of block editors. + // we only want to deserialize the one identified by the concrete block value. + // here's an example of how the layouts collection JSON might look: + // "layout": { + // "Umbraco.BlockGrid": [{ + // "contentUdi": "umb://element/1304E1DDAC87439684FE8A399231CB3D", + // "rowSpan": 1, + // "columnSpan": 12, + // "areas": [] + // } + // ], + // "Umbraco.BlockList": [{ + // "contentUdi": "umb://element/1304E1DDAC87439684FE8A399231CB3D" + // } + // ], + // "Some.Custom.BlockEditor": [{ + // "contentUdi": "umb://element/1304E1DDAC87439684FE8A399231CB3D" + // } + // ] + // } + + // the concrete block editor layout items type + Type layoutItemType = GetLayoutItemType(typeToConvert); + // the type describing a list of concrete block editor layout items + Type layoutItemsType = typeof(List<>).MakeGenericType(layoutItemType); + + while (reader.Read()) + { + if (reader.TokenType is JsonTokenType.EndObject) + { + break; + } + + if (reader.TokenType is JsonTokenType.PropertyName) + { + // grab the block editor alias (e.g. "Umbraco.BlockGrid") + var blockEditorAlias = reader.GetString() + ?? throw new JsonException($"Could bot get the block editor alias from the layout while attempting to deserialize type: {typeToConvert.FullName}."); + + // forward the reader to the next JSON token, which *should* be the array of corresponding layout items + reader.Read(); + if (reader.TokenType is not JsonTokenType.StartArray) + { + throw new JsonException($"Expected to find the beginning of an array of layout items for block editor alias: {blockEditorAlias}, got: {reader.TokenType}. This happened while attempting to deserialize type: {typeToConvert.FullName}."); + } + + // did we encounter the concrete block value? + if (blockEditorAlias == blockValue.PropertyEditorAlias) + { + // yes, deserialize the block layout items as their concrete type (list of layoutItemType) + var layoutItems = JsonSerializer.Deserialize(ref reader, layoutItemsType, options); + blockValue.Layout[blockEditorAlias] = layoutItems as IEnumerable + ?? throw new JsonException($"Could not deserialize block editor layout items as type: {layoutItemType.FullName} while attempting to deserialize layout items for block editor alias: {blockEditorAlias} for type: {typeToConvert.FullName}."); + } + else + { + // ignore this layout - forward the reader to the end of the array and look for the next one + while (reader.TokenType is not JsonTokenType.EndArray) + { + reader.Read(); + } + } + } + } + } +} + diff --git a/src/Umbraco.Infrastructure/Serialization/SystemTextJsonSerializer.cs b/src/Umbraco.Infrastructure/Serialization/SystemTextJsonSerializer.cs index 6d74d0cc87..3edd4d2fc3 100644 --- a/src/Umbraco.Infrastructure/Serialization/SystemTextJsonSerializer.cs +++ b/src/Umbraco.Infrastructure/Serialization/SystemTextJsonSerializer.cs @@ -20,7 +20,8 @@ public sealed class SystemTextJsonSerializer : SystemTextJsonSerializerBase new JsonStringEnumConverter(), new JsonUdiConverter(), new JsonUdiRangeConverter(), - new JsonObjectConverter() // Required for block editor values + new JsonObjectConverter(), // Required for block editor values + new JsonBlockValueConverter() } }; diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/PropertyEditors/PropertyIndexValueFactoryTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/PropertyEditors/PropertyIndexValueFactoryTests.cs new file mode 100644 index 0000000000..3884a43ded --- /dev/null +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/PropertyEditors/PropertyIndexValueFactoryTests.cs @@ -0,0 +1,381 @@ +using NUnit.Framework; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.Blocks; +using Umbraco.Cms.Core.PropertyEditors; +using Umbraco.Cms.Core.Serialization; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Tests.Common.Builders; +using Umbraco.Cms.Tests.Common.Builders.Extensions; +using Umbraco.Cms.Tests.Common.Testing; +using Umbraco.Cms.Tests.Integration.Testing; + +namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.PropertyEditors; + +[TestFixture] +[UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerTest)] +public class PropertyIndexValueFactoryTests : UmbracoIntegrationTest +{ + private IContentTypeService ContentTypeService => GetRequiredService(); + + private IContentService ContentService => GetRequiredService(); + + private IDataTypeService DataTypeService => GetRequiredService(); + + private IJsonSerializer JsonSerializer => GetRequiredService(); + + private PropertyEditorCollection PropertyEditorCollection => GetRequiredService(); + + private IConfigurationEditorJsonSerializer ConfigurationEditorJsonSerializer => GetRequiredService(); + + [Test] + public void Can_Get_Index_Values_From_RichText_With_Blocks() + { + var elementType = ContentTypeBuilder.CreateAllTypesContentType("myElementType", "My Element Type"); + elementType.IsElement = true; + ContentTypeService.Save(elementType); + + var contentType = ContentTypeBuilder.CreateTextPageContentType("myContentType"); + contentType.AllowedTemplates = Enumerable.Empty(); + ContentTypeService.Save(contentType); + + var dataType = DataTypeService.GetDataType(contentType.PropertyTypes.First(propertyType => propertyType.Alias == "bodyText").DataTypeId)!; + var editor = dataType.Editor!; + + var elementId = Guid.NewGuid(); + var propertyValue = RichTextPropertyEditorHelper.SerializeRichTextEditorValue( + new RichTextEditorValue + { + Markup = @$"

This is some markup

", + Blocks = JsonSerializer.Deserialize($$""" + { + "layout": { + "Umbraco.TinyMCE": [{ + "contentUdi": "umb://element/{{elementId:N}}" + } + ] + }, + "contentData": [{ + "contentTypeKey": "{{elementType.Key:D}}", + "udi": "umb://element/{{elementId:N}}", + "singleLineText": "The single line of text in the block", + "bodyText": "

The body text in the block

" + } + ], + "settingsData": [] + } + """) + }, + JsonSerializer); + + var content = ContentBuilder.CreateTextpageContent(contentType, "My Content", -1); + content.Properties["bodyText"]!.SetValue(propertyValue); + ContentService.Save(content); + + var indexValues = editor.PropertyIndexValueFactory.GetIndexValues( + content.Properties["bodyText"]!, + culture: null, + segment: null, + published: false, + availableCultures: Enumerable.Empty(), + contentTypeDictionary: new Dictionary + { + { elementType.Key, elementType }, { contentType.Key, contentType } + }).ToDictionary(); + + Assert.IsTrue(indexValues.TryGetValue("bodyText", out var bodyTextIndexValues)); + + Assert.AreEqual(1, bodyTextIndexValues.Count()); + var bodyTextIndexValue = bodyTextIndexValues.First() as string; + Assert.IsNotNull(bodyTextIndexValue); + + Assert.Multiple(() => + { + Assert.IsTrue(bodyTextIndexValue.Contains("This is some markup")); + Assert.IsTrue(bodyTextIndexValue.Contains("The single line of text in the block")); + Assert.IsTrue(bodyTextIndexValue.Contains("The body text in the block")); + }); + } + + [Test] + public void Can_Get_Index_Values_From_RichText_Without_Blocks() + { + var contentType = ContentTypeBuilder.CreateTextPageContentType("myContentType"); + contentType.AllowedTemplates = Enumerable.Empty(); + ContentTypeService.Save(contentType); + + var dataType = DataTypeService.GetDataType(contentType.PropertyTypes.First(propertyType => propertyType.Alias == "bodyText").DataTypeId)!; + var editor = dataType.Editor!; + + var content = ContentBuilder.CreateTextpageContent(contentType, "My Content", -1); + content.Properties["bodyText"]!.SetValue("

This is some markup

"); + ContentService.Save(content); + + var indexValues = editor.PropertyIndexValueFactory.GetIndexValues( + content.Properties["bodyText"]!, + culture: null, + segment: null, + published: false, + availableCultures: Enumerable.Empty(), + contentTypeDictionary: new Dictionary + { + { contentType.Key, contentType } + }).ToDictionary(); + + Assert.IsTrue(indexValues.TryGetValue("bodyText", out var bodyTextIndexValues)); + + Assert.AreEqual(1, bodyTextIndexValues.Count()); + var bodyTextIndexValue = bodyTextIndexValues.First() as string; + Assert.IsNotNull(bodyTextIndexValue); + Assert.IsTrue(bodyTextIndexValue.Contains("This is some markup")); + } + + [Test] + public async Task Can_Get_Index_Values_From_BlockList() + { + var elementType = ContentTypeBuilder.CreateAllTypesContentType("myElementType", "My Element Type"); + elementType.IsElement = true; + ContentTypeService.Save(elementType); + + var dataType = new DataType(PropertyEditorCollection[Constants.PropertyEditors.Aliases.BlockList], ConfigurationEditorJsonSerializer) + { + ConfigurationData = new Dictionary + { + { + "blocks", + ConfigurationEditorJsonSerializer.Serialize(new BlockListConfiguration.BlockConfiguration[] + { + new() { ContentElementTypeKey = elementType.Key } + }) + } + }, + Name = "My Block List", + DatabaseType = ValueStorageType.Ntext, + ParentId = Constants.System.Root, + CreateDate = DateTime.UtcNow + }; + await DataTypeService.CreateAsync(dataType, Constants.Security.SuperUserKey); + + var builder = new ContentTypeBuilder(); + var contentType = builder + .WithAlias("myPage") + .WithName("My Page") + .AddPropertyType() + .WithAlias("blocks") + .WithName("Blocks") + .WithDataTypeId(dataType.Id) + .Done() + .Build(); + ContentTypeService.Save(contentType); + + var editor = dataType.Editor!; + + var contentElementUdi = new GuidUdi(Constants.UdiEntityType.Element, Guid.NewGuid()); + var blockListValue = new BlockListValue + { + Layout = new Dictionary> + { + { + Constants.PropertyEditors.Aliases.BlockList, + new IBlockLayoutItem[] + { + new BlockListLayoutItem() + { + ContentUdi = contentElementUdi + } + } + } + }, + ContentData = + [ + new() + { + Udi = contentElementUdi, + ContentTypeAlias = elementType.Alias, + ContentTypeKey = elementType.Key, + RawPropertyValues = new Dictionary + { + {"singleLineText", "The single line of text in the block"}, + {"bodyText", "

The body text in the block

"} + } + } + ], + SettingsData = [] + }; + var propertyValue = JsonSerializer.Serialize(blockListValue); + + var content = ContentBuilder.CreateBasicContent(contentType); + content.Properties["blocks"]!.SetValue(propertyValue); + ContentService.Save(content); + + var indexValues = editor.PropertyIndexValueFactory.GetIndexValues( + content.Properties["blocks"]!, + culture: null, + segment: null, + published: false, + availableCultures: Enumerable.Empty(), + contentTypeDictionary: new Dictionary + { + { elementType.Key, elementType }, { contentType.Key, contentType } + }).ToDictionary(); + + Assert.IsTrue(indexValues.TryGetValue("blocks", out var blocksIndexValues)); + + Assert.AreEqual(1, blocksIndexValues.Count()); + var blockIndexValue = blocksIndexValues.First() as string; + Assert.IsNotNull(blockIndexValue); + + Assert.Multiple(() => + { + Assert.IsTrue(blockIndexValue.Contains("The single line of text in the block")); + Assert.IsTrue(blockIndexValue.Contains("The body text in the block")); + }); + } + + [Test] + public async Task Can_Get_Index_Values_From_BlockGrid() + { + var elementType = ContentTypeBuilder.CreateAllTypesContentType("myElementType", "My Element Type"); + elementType.IsElement = true; + ContentTypeService.Save(elementType); + + var dataType = new DataType(PropertyEditorCollection[Constants.PropertyEditors.Aliases.BlockGrid], ConfigurationEditorJsonSerializer) + { + ConfigurationData = new Dictionary + { + { + "blocks", + ConfigurationEditorJsonSerializer.Serialize(new BlockGridConfiguration.BlockGridBlockConfiguration[] + { + new() + { + ContentElementTypeKey = elementType.Key, + Areas = new BlockGridConfiguration.BlockGridAreaConfiguration[] + { + new() + { + Key = Guid.NewGuid(), + Alias = "one", + ColumnSpan = 12, + RowSpan = 1 + } + } + } + }) + } + }, + Name = "My Block Grid", + DatabaseType = ValueStorageType.Ntext, + ParentId = Constants.System.Root, + CreateDate = DateTime.UtcNow + }; + await DataTypeService.CreateAsync(dataType, Constants.Security.SuperUserKey); + + var builder = new ContentTypeBuilder(); + var contentType = builder + .WithAlias("myPage") + .WithName("My Page") + .AddPropertyType() + .WithAlias("blocks") + .WithName("Blocks") + .WithDataTypeId(dataType.Id) + .Done() + .Build(); + ContentTypeService.Save(contentType); + + var editor = dataType.Editor!; + + var contentElementUdi = new GuidUdi(Constants.UdiEntityType.Element, Guid.NewGuid()); + var contentAreaElementUdi = new GuidUdi(Constants.UdiEntityType.Element, Guid.NewGuid()); + var blockGridValue = new BlockGridValue + { + Layout = new Dictionary> + { + { + Constants.PropertyEditors.Aliases.BlockGrid, + new IBlockLayoutItem[] + { + new BlockGridLayoutItem + { + ColumnSpan = 12, + RowSpan = 1, + ContentUdi = contentElementUdi, + Areas = new [] + { + new BlockGridLayoutAreaItem + { + Key = Guid.NewGuid(), + Items = new [] + { + new BlockGridLayoutItem + { + ContentUdi = contentAreaElementUdi, + ColumnSpan = 12, + RowSpan = 1 + } + } + } + } + } + } + } + }, + ContentData = + [ + new() + { + Udi = contentElementUdi, + ContentTypeAlias = elementType.Alias, + ContentTypeKey = elementType.Key, + RawPropertyValues = new Dictionary + { + {"singleLineText", "The single line of text in the grid root"}, + {"bodyText", "

The body text in the grid root

"} + } + }, + new() + { + Udi = contentAreaElementUdi, + ContentTypeAlias = elementType.Alias, + ContentTypeKey = elementType.Key, + RawPropertyValues = new Dictionary + { + {"singleLineText", "The single line of text in the grid area"}, + {"bodyText", "

The body text in the grid area

"} + } + } + ], + SettingsData = [] + }; + var propertyValue = JsonSerializer.Serialize(blockGridValue); + + var content = ContentBuilder.CreateBasicContent(contentType); + content.Properties["blocks"]!.SetValue(propertyValue); + ContentService.Save(content); + + var indexValues = editor.PropertyIndexValueFactory.GetIndexValues( + content.Properties["blocks"]!, + culture: null, + segment: null, + published: false, + availableCultures: Enumerable.Empty(), + contentTypeDictionary: new Dictionary + { + { elementType.Key, elementType }, { contentType.Key, contentType } + }).ToDictionary(); + + Assert.IsTrue(indexValues.TryGetValue("blocks", out var blocksIndexValues)); + + Assert.AreEqual(1, blocksIndexValues.Count()); + var blockIndexValue = blocksIndexValues.First() as string; + Assert.IsNotNull(blockIndexValue); + + Assert.Multiple(() => + { + Assert.IsTrue(blockIndexValue.Contains("The single line of text in the grid root")); + Assert.IsTrue(blockIndexValue.Contains("The body text in the grid root")); + Assert.IsTrue(blockIndexValue.Contains("The single line of text in the grid area")); + Assert.IsTrue(blockIndexValue.Contains("The body text in the grid area")); + }); + } +} diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/BlockGridPropertyValueConverterTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/BlockGridPropertyValueConverterTests.cs index 2187731f5a..5756a05622 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/BlockGridPropertyValueConverterTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/BlockGridPropertyValueConverterTests.cs @@ -29,6 +29,144 @@ public class BlockGridPropertyValueConverterTests : BlockPropertyValueConverterT Assert.AreEqual(typeof(BlockGridModel), valueType); } + [Test] + public void Convert_Valid_Json() + { + var editor = CreateConverter(); + var config = ConfigForSingle(SettingKey1); + var propertyType = GetPropertyType(config); + var publishedElement = Mock.Of(); + + var json = @" +{ + ""layout"": { + """ + Constants.PropertyEditors.Aliases.BlockGrid + @""": [ + { + ""contentUdi"": ""umb://element/1304E1DDAC87439684FE8A399231CB3D"", + ""settingsUdi"": ""umb://element/2D3529EDB47B4B109F6D4B802DD5DFE2"", + ""rowSpan"": 1, + ""columnSpan"": 12, + ""areas"": [] + } + ] + }, + ""contentData"": [ + { + ""contentTypeKey"": """ + ContentKey1 + @""", + ""udi"": ""umb://element/1304E1DDAC87439684FE8A399231CB3D"" + } + ], + ""settingsData"": [ + { + ""contentTypeKey"": """ + SettingKey1 + @""", + ""udi"": ""umb://element/2D3529EDB47B4B109F6D4B802DD5DFE2"" + } + ] +}"; + var converted = + editor.ConvertIntermediateToObject(publishedElement, propertyType, PropertyCacheLevel.None, json, false) as + BlockGridModel; + + Assert.IsNotNull(converted); + Assert.AreEqual(1, converted.Count); + Assert.AreEqual(Guid.Parse("1304E1DD-AC87-4396-84FE-8A399231CB3D"), converted[0].Content.Key); + Assert.AreEqual(UdiParser.Parse("umb://element/1304E1DDAC87439684FE8A399231CB3D"), converted[0].ContentUdi); + Assert.AreEqual(ContentAlias1, converted[0].Content.ContentType.Alias); + Assert.AreEqual(Guid.Parse("2D3529ED-B47B-4B10-9F6D-4B802DD5DFE2"), converted[0].Settings.Key); + Assert.AreEqual(UdiParser.Parse("umb://element/2D3529EDB47B4B109F6D4B802DD5DFE2"), converted[0].SettingsUdi); + Assert.AreEqual(SettingAlias1, converted[0].Settings.ContentType.Alias); + } + + [Test] + public void Can_Convert_Without_Settings() + { + var editor = CreateConverter(); + var config = ConfigForSingle(); + var propertyType = GetPropertyType(config); + var publishedElement = Mock.Of(); + + var json = @" +{ + ""layout"": { + """ + Constants.PropertyEditors.Aliases.BlockGrid + @""": [ + { + ""contentUdi"": ""umb://element/1304E1DDAC87439684FE8A399231CB3D"", + ""rowSpan"": 1, + ""columnSpan"": 12, + ""areas"": [] + } + ] + }, + ""contentData"": [ + { + ""contentTypeKey"": """ + ContentKey1 + @""", + ""udi"": ""umb://element/1304E1DDAC87439684FE8A399231CB3D"" + } + ] +}"; + var converted = + editor.ConvertIntermediateToObject(publishedElement, propertyType, PropertyCacheLevel.None, json, false) as + BlockGridModel; + + Assert.IsNotNull(converted); + Assert.AreEqual(1, converted.Count); + var item0 = converted[0].Content; + Assert.AreEqual(Guid.Parse("1304E1DD-AC87-4396-84FE-8A399231CB3D"), item0.Key); + Assert.AreEqual(UdiParser.Parse("umb://element/1304E1DDAC87439684FE8A399231CB3D"), converted[0].ContentUdi); + Assert.AreEqual("Test1", item0.ContentType.Alias); + Assert.IsNull(converted[0].Settings); + } + + [Test] + public void Ignores_Other_Layouts() + { + var editor = CreateConverter(); + var config = ConfigForSingle(); + var propertyType = GetPropertyType(config); + var publishedElement = Mock.Of(); + + var json = @" +{ + ""layout"": { + """ + Constants.PropertyEditors.Aliases.BlockGrid + @""": [ + { + ""contentUdi"": ""umb://element/1304E1DDAC87439684FE8A399231CB3D"", + ""rowSpan"": 1, + ""columnSpan"": 12, + ""areas"": [] + } + ], + """ + Constants.PropertyEditors.Aliases.BlockList + @""": [ + { + ""contentUdi"": ""umb://element/1304E1DDAC87439684FE8A399231CB3D"" + } + ], + ""Some.Custom.BlockEditor"": [ + { + ""contentUdi"": ""umb://element/1304E1DDAC87439684FE8A399231CB3D"" + } + ] + }, + ""contentData"": [ + { + ""contentTypeKey"": """ + ContentKey1 + @""", + ""udi"": ""umb://element/1304E1DDAC87439684FE8A399231CB3D"" + } + ] +}"; + var converted = + editor.ConvertIntermediateToObject(publishedElement, propertyType, PropertyCacheLevel.None, json, false) as + BlockGridModel; + + Assert.IsNotNull(converted); + Assert.AreEqual(1, converted.Count); + var item0 = converted[0].Content; + Assert.AreEqual(Guid.Parse("1304E1DD-AC87-4396-84FE-8A399231CB3D"), item0.Key); + Assert.AreEqual(UdiParser.Parse("umb://element/1304E1DDAC87439684FE8A399231CB3D"), converted[0].ContentUdi); + Assert.AreEqual("Test1", item0.ContentType.Alias); + Assert.IsNull(converted[0].Settings); + } + private BlockGridPropertyValueConverter CreateConverter() { var publishedSnapshotAccessor = GetPublishedSnapshotAccessor(); @@ -42,8 +180,8 @@ public class BlockGridPropertyValueConverterTests : BlockPropertyValueConverterT return editor; } - private BlockGridConfiguration ConfigForSingle() => new() + private BlockGridConfiguration ConfigForSingle(Guid? settingsElementTypeKey = null) => new() { - Blocks = new[] { new BlockGridConfiguration.BlockGridBlockConfiguration { ContentElementTypeKey = ContentKey1 } }, + Blocks = new[] { new BlockGridConfiguration.BlockGridBlockConfiguration { ContentElementTypeKey = ContentKey1, SettingsElementTypeKey = settingsElementTypeKey} }, }; } diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Serialization/JsonBlockValueConverterTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Serialization/JsonBlockValueConverterTests.cs new file mode 100644 index 0000000000..b109eb51cf --- /dev/null +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Serialization/JsonBlockValueConverterTests.cs @@ -0,0 +1,537 @@ +using NUnit.Framework; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Models.Blocks; +using Umbraco.Cms.Infrastructure.Serialization; + +namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Serialization; + +[TestFixture] +public class JsonBlockValueConverterTests +{ + [Test] + public void Can_Serialize_BlockGrid_With_Blocks() + { + var contentElementUdi1 = Udi.Create(Constants.UdiEntityType.Element, Guid.NewGuid()); + var settingsElementUdi1 = Udi.Create(Constants.UdiEntityType.Element, Guid.NewGuid()); + var contentElementUdi2 = Udi.Create(Constants.UdiEntityType.Element, Guid.NewGuid()); + var settingsElementUdi2 = Udi.Create(Constants.UdiEntityType.Element, Guid.NewGuid()); + var contentElementUdi3 = Udi.Create(Constants.UdiEntityType.Element, Guid.NewGuid()); + var settingsElementUdi3 = Udi.Create(Constants.UdiEntityType.Element, Guid.NewGuid()); + var contentElementUdi4 = Udi.Create(Constants.UdiEntityType.Element, Guid.NewGuid()); + var settingsElementUdi4 = Udi.Create(Constants.UdiEntityType.Element, Guid.NewGuid()); + + var elementType1Key = Guid.NewGuid(); + var elementType2Key = Guid.NewGuid(); + var elementType3Key = Guid.NewGuid(); + var elementType4Key = Guid.NewGuid(); + + var blockGridValue = new BlockGridValue + { + Layout = new Dictionary> + { + { + Constants.PropertyEditors.Aliases.BlockGrid, + new IBlockLayoutItem[] + { + new BlockGridLayoutItem + { + ColumnSpan = 123, + RowSpan = 456, + ContentUdi = contentElementUdi1, + SettingsUdi = settingsElementUdi1, + Areas = new [] + { + new BlockGridLayoutAreaItem + { + Key = Guid.NewGuid(), + Items = new [] + { + new BlockGridLayoutItem + { + ColumnSpan = 12, + RowSpan = 34, + ContentUdi = contentElementUdi3, + SettingsUdi = settingsElementUdi3, + Areas = new [] + { + new BlockGridLayoutAreaItem + { + Key = Guid.NewGuid(), + Items = new [] + { + new BlockGridLayoutItem + { + ColumnSpan = 56, + RowSpan = 78, + ContentUdi = contentElementUdi4, + SettingsUdi = settingsElementUdi4 + } + } + } + } + } + } + } + } + }, + new BlockGridLayoutItem + { + ColumnSpan = 789, + RowSpan = 123, + ContentUdi = contentElementUdi2, + SettingsUdi = settingsElementUdi2 + } + } + } + }, + ContentData = + [ + new() { Udi = contentElementUdi1, ContentTypeAlias = "elementType1", ContentTypeKey = elementType1Key }, + new() { Udi = contentElementUdi2, ContentTypeAlias = "elementType2", ContentTypeKey = elementType2Key }, + new() { Udi = contentElementUdi3, ContentTypeAlias = "elementType3", ContentTypeKey = elementType3Key }, + new() { Udi = contentElementUdi4, ContentTypeAlias = "elementType¤", ContentTypeKey = elementType4Key }, + ], + SettingsData = + [ + new() { Udi = settingsElementUdi1, ContentTypeAlias = "elementType3", ContentTypeKey = elementType3Key }, + new() { Udi = settingsElementUdi2, ContentTypeAlias = "elementType4", ContentTypeKey = elementType4Key }, + new() { Udi = settingsElementUdi3, ContentTypeAlias = "elementType1", ContentTypeKey = elementType1Key }, + new() { Udi = settingsElementUdi4, ContentTypeAlias = "elementType2", ContentTypeKey = elementType2Key } + ] + }; + + var serializer = new SystemTextJsonSerializer(); + var serialized = serializer.Serialize(blockGridValue); + var deserialized = serializer.Deserialize(serialized); + + Assert.IsNotNull(deserialized); + + Assert.AreEqual(1, deserialized.Layout.Count); + Assert.IsTrue(deserialized.Layout.ContainsKey(Constants.PropertyEditors.Aliases.BlockGrid)); + var layoutItems = deserialized.Layout[Constants.PropertyEditors.Aliases.BlockGrid].OfType().ToArray(); + Assert.AreEqual(2, layoutItems.Count()); + Assert.Multiple(() => + { + Assert.AreEqual(123, layoutItems[0].ColumnSpan); + Assert.AreEqual(456, layoutItems[0].RowSpan); + Assert.AreEqual(contentElementUdi1, layoutItems[0].ContentUdi); + Assert.AreEqual(settingsElementUdi1, layoutItems[0].SettingsUdi); + + Assert.AreEqual(789, layoutItems[1].ColumnSpan); + Assert.AreEqual(123, layoutItems[1].RowSpan); + Assert.AreEqual(contentElementUdi2, layoutItems[1].ContentUdi); + Assert.AreEqual(settingsElementUdi2, layoutItems[1].SettingsUdi); + }); + + Assert.AreEqual(1, layoutItems[0].Areas.Length); + Assert.AreEqual(1, layoutItems[0].Areas[0].Items.Length); + + Assert.Multiple(() => + { + Assert.AreEqual(12, layoutItems[0].Areas[0].Items[0].ColumnSpan); + Assert.AreEqual(34, layoutItems[0].Areas[0].Items[0].RowSpan); + Assert.AreEqual(contentElementUdi3, layoutItems[0].Areas[0].Items[0].ContentUdi); + Assert.AreEqual(settingsElementUdi3, layoutItems[0].Areas[0].Items[0].SettingsUdi); + }); + + Assert.AreEqual(1, layoutItems[0].Areas[0].Items[0].Areas.Length); + Assert.AreEqual(1, layoutItems[0].Areas[0].Items[0].Areas[0].Items.Length); + + Assert.Multiple(() => + { + Assert.AreEqual(56, layoutItems[0].Areas[0].Items[0].Areas[0].Items[0].ColumnSpan); + Assert.AreEqual(78, layoutItems[0].Areas[0].Items[0].Areas[0].Items[0].RowSpan); + Assert.AreEqual(contentElementUdi4, layoutItems[0].Areas[0].Items[0].Areas[0].Items[0].ContentUdi); + Assert.AreEqual(settingsElementUdi4, layoutItems[0].Areas[0].Items[0].Areas[0].Items[0].SettingsUdi); + }); + + Assert.AreEqual(4, deserialized.ContentData.Count); + Assert.Multiple(() => + { + Assert.AreEqual(contentElementUdi1, deserialized.ContentData[0].Udi); + Assert.AreEqual(elementType1Key, deserialized.ContentData[0].ContentTypeKey); + Assert.AreEqual(string.Empty, deserialized.ContentData[0].ContentTypeAlias); // explicitly annotated to be ignored by the serializer + + Assert.AreEqual(contentElementUdi2, deserialized.ContentData[1].Udi); + Assert.AreEqual(elementType2Key, deserialized.ContentData[1].ContentTypeKey); + Assert.AreEqual(string.Empty, deserialized.ContentData[1].ContentTypeAlias); + + Assert.AreEqual(contentElementUdi3, deserialized.ContentData[2].Udi); + Assert.AreEqual(elementType3Key, deserialized.ContentData[2].ContentTypeKey); + Assert.AreEqual(string.Empty, deserialized.ContentData[2].ContentTypeAlias); + + Assert.AreEqual(contentElementUdi3, deserialized.ContentData[2].Udi); + Assert.AreEqual(elementType3Key, deserialized.ContentData[2].ContentTypeKey); + Assert.AreEqual(string.Empty, deserialized.ContentData[2].ContentTypeAlias); + }); + + Assert.AreEqual(4, deserialized.SettingsData.Count); + Assert.Multiple(() => + { + Assert.AreEqual(settingsElementUdi1, deserialized.SettingsData[0].Udi); + Assert.AreEqual(elementType3Key, deserialized.SettingsData[0].ContentTypeKey); + Assert.AreEqual(string.Empty, deserialized.SettingsData[0].ContentTypeAlias); + + Assert.AreEqual(settingsElementUdi2, deserialized.SettingsData[1].Udi); + Assert.AreEqual(elementType4Key, deserialized.SettingsData[1].ContentTypeKey); + Assert.AreEqual(string.Empty, deserialized.SettingsData[1].ContentTypeAlias); + + Assert.AreEqual(settingsElementUdi3, deserialized.SettingsData[2].Udi); + Assert.AreEqual(elementType1Key, deserialized.SettingsData[2].ContentTypeKey); + Assert.AreEqual(string.Empty, deserialized.SettingsData[2].ContentTypeAlias); + + Assert.AreEqual(settingsElementUdi4, deserialized.SettingsData[3].Udi); + Assert.AreEqual(elementType2Key, deserialized.SettingsData[3].ContentTypeKey); + Assert.AreEqual(string.Empty, deserialized.SettingsData[3].ContentTypeAlias); + }); + } + + [Test] + public void Can_Serialize_BlockGrid_Without_Blocks() + { + var blockGridValue = new BlockGridValue + { + Layout = new Dictionary>(), + ContentData = [], + SettingsData = [] + }; + + var serializer = new SystemTextJsonSerializer(); + var serialized = serializer.Serialize(blockGridValue); + var deserialized = serializer.Deserialize(serialized); + + Assert.IsNotNull(deserialized); + Assert.Multiple(() => + { + Assert.IsEmpty(deserialized.Layout); + Assert.IsEmpty(deserialized.ContentData); + Assert.IsEmpty(deserialized.SettingsData); + }); + } + + [Test] + public void Can_Serialize_BlockList_With_Blocks() + { + var contentElementUdi1 = Udi.Create(Constants.UdiEntityType.Element, Guid.NewGuid()); + var settingsElementUdi1 = Udi.Create(Constants.UdiEntityType.Element, Guid.NewGuid()); + var contentElementUdi2 = Udi.Create(Constants.UdiEntityType.Element, Guid.NewGuid()); + var settingsElementUdi2 = Udi.Create(Constants.UdiEntityType.Element, Guid.NewGuid()); + + var elementType1Key = Guid.NewGuid(); + var elementType2Key = Guid.NewGuid(); + var elementType3Key = Guid.NewGuid(); + var elementType4Key = Guid.NewGuid(); + + var blockListValue = new BlockListValue + { + Layout = new Dictionary> + { + { + Constants.PropertyEditors.Aliases.BlockList, + new IBlockLayoutItem[] + { + new BlockListLayoutItem() + { + ContentUdi = contentElementUdi1, + SettingsUdi = settingsElementUdi1 + }, + new BlockListLayoutItem + { + ContentUdi = contentElementUdi2, + SettingsUdi = settingsElementUdi2 + } + } + } + }, + ContentData = + [ + new() { Udi = contentElementUdi1, ContentTypeAlias = "elementType1", ContentTypeKey = elementType1Key }, + new() { Udi = contentElementUdi2, ContentTypeAlias = "elementType2", ContentTypeKey = elementType2Key } + ], + SettingsData = + [ + new() { Udi = settingsElementUdi1, ContentTypeAlias = "elementType3", ContentTypeKey = elementType3Key }, + new() { Udi = settingsElementUdi2, ContentTypeAlias = "elementType4", ContentTypeKey = elementType4Key } + ] + }; + + var serializer = new SystemTextJsonSerializer(); + var serialized = serializer.Serialize(blockListValue); + var deserialized = serializer.Deserialize(serialized); + + Assert.IsNotNull(deserialized); + + Assert.AreEqual(1, deserialized.Layout.Count); + Assert.IsTrue(deserialized.Layout.ContainsKey(Constants.PropertyEditors.Aliases.BlockList)); + var layoutItems = deserialized.Layout[Constants.PropertyEditors.Aliases.BlockList].OfType().ToArray(); + Assert.AreEqual(2, layoutItems.Count()); + Assert.Multiple(() => + { + Assert.AreEqual(contentElementUdi1, layoutItems.First().ContentUdi); + Assert.AreEqual(settingsElementUdi1, layoutItems.First().SettingsUdi); + + Assert.AreEqual(contentElementUdi2, layoutItems.Last().ContentUdi); + Assert.AreEqual(settingsElementUdi2, layoutItems.Last().SettingsUdi); + }); + + Assert.AreEqual(2, deserialized.ContentData.Count); + Assert.Multiple(() => + { + Assert.AreEqual(contentElementUdi1, deserialized.ContentData.First().Udi); + Assert.AreEqual(elementType1Key, deserialized.ContentData.First().ContentTypeKey); + Assert.AreEqual(string.Empty, deserialized.ContentData.First().ContentTypeAlias); // explicitly annotated to be ignored by the serializer + + Assert.AreEqual(contentElementUdi2, deserialized.ContentData.Last().Udi); + Assert.AreEqual(elementType2Key, deserialized.ContentData.Last().ContentTypeKey); + Assert.AreEqual(string.Empty, deserialized.ContentData.Last().ContentTypeAlias); + }); + + Assert.AreEqual(2, deserialized.SettingsData.Count); + Assert.Multiple(() => + { + Assert.AreEqual(settingsElementUdi1, deserialized.SettingsData.First().Udi); + Assert.AreEqual(elementType3Key, deserialized.SettingsData.First().ContentTypeKey); + Assert.AreEqual(string.Empty, deserialized.SettingsData.First().ContentTypeAlias); + + Assert.AreEqual(settingsElementUdi2, deserialized.SettingsData.Last().Udi); + Assert.AreEqual(elementType4Key, deserialized.SettingsData.Last().ContentTypeKey); + Assert.AreEqual(string.Empty, deserialized.SettingsData.Last().ContentTypeAlias); + }); + } + + [Test] + public void Can_Serialize_BlockList_Without_Blocks() + { + var blockListValue = new BlockListValue + { + Layout = new Dictionary>(), + ContentData = [], + SettingsData = [] + }; + + var serializer = new SystemTextJsonSerializer(); + var serialized = serializer.Serialize(blockListValue); + var deserialized = serializer.Deserialize(serialized); + + Assert.IsNotNull(deserialized); + Assert.Multiple(() => + { + Assert.IsEmpty(deserialized.Layout); + Assert.IsEmpty(deserialized.ContentData); + Assert.IsEmpty(deserialized.SettingsData); + }); + } + + [Test] + public void Can_Serialize_Richtext_With_Blocks() + { + var contentElementUdi1 = Udi.Create(Constants.UdiEntityType.Element, Guid.NewGuid()); + var settingsElementUdi1 = Udi.Create(Constants.UdiEntityType.Element, Guid.NewGuid()); + var contentElementUdi2 = Udi.Create(Constants.UdiEntityType.Element, Guid.NewGuid()); + var settingsElementUdi2 = Udi.Create(Constants.UdiEntityType.Element, Guid.NewGuid()); + + var elementType1Key = Guid.NewGuid(); + var elementType2Key = Guid.NewGuid(); + var elementType3Key = Guid.NewGuid(); + var elementType4Key = Guid.NewGuid(); + + var richTextBlockValue = new RichTextBlockValue + { + Layout = new Dictionary> + { + { + Constants.PropertyEditors.Aliases.TinyMce, + new IBlockLayoutItem[] + { + new RichTextBlockLayoutItem + { + ContentUdi = contentElementUdi1, + SettingsUdi = settingsElementUdi1 + }, + new RichTextBlockLayoutItem + { + ContentUdi = contentElementUdi2, + SettingsUdi = settingsElementUdi2 + } + } + } + }, + ContentData = + [ + new() { Udi = contentElementUdi1, ContentTypeAlias = "elementType1", ContentTypeKey = elementType1Key }, + new() { Udi = contentElementUdi2, ContentTypeAlias = "elementType2", ContentTypeKey = elementType2Key } + ], + SettingsData = + [ + new() { Udi = settingsElementUdi1, ContentTypeAlias = "elementType3", ContentTypeKey = elementType3Key }, + new() { Udi = settingsElementUdi2, ContentTypeAlias = "elementType4", ContentTypeKey = elementType4Key } + ] + }; + + var richTextEditorValue = new RichTextEditorValue + { + Blocks = richTextBlockValue, + Markup = "

This is some markup

" + }; + + var serializer = new SystemTextJsonSerializer(); + var serialized = serializer.Serialize(richTextEditorValue); + var deserialized = serializer.Deserialize(serialized); + + Assert.IsNotNull(deserialized); + Assert.AreEqual("

This is some markup

", deserialized.Markup); + + var deserializedBlocks = deserialized.Blocks; + Assert.IsNotNull(deserializedBlocks); + Assert.AreEqual(1, deserializedBlocks.Layout.Count); + Assert.IsTrue(deserializedBlocks.Layout.ContainsKey(Constants.PropertyEditors.Aliases.TinyMce)); + var layoutItems = deserializedBlocks.Layout[Constants.PropertyEditors.Aliases.TinyMce].OfType().ToArray(); + Assert.AreEqual(2, layoutItems.Count()); + Assert.Multiple(() => + { + Assert.AreEqual(contentElementUdi1, layoutItems.First().ContentUdi); + Assert.AreEqual(settingsElementUdi1, layoutItems.First().SettingsUdi); + + Assert.AreEqual(contentElementUdi2, layoutItems.Last().ContentUdi); + Assert.AreEqual(settingsElementUdi2, layoutItems.Last().SettingsUdi); + }); + + Assert.AreEqual(2, deserializedBlocks.ContentData.Count); + Assert.Multiple(() => + { + Assert.AreEqual(contentElementUdi1, deserializedBlocks.ContentData.First().Udi); + Assert.AreEqual(elementType1Key, deserializedBlocks.ContentData.First().ContentTypeKey); + Assert.AreEqual(string.Empty, deserializedBlocks.ContentData.First().ContentTypeAlias); // explicitly annotated to be ignored by the serializer + + Assert.AreEqual(contentElementUdi2, deserializedBlocks.ContentData.Last().Udi); + Assert.AreEqual(elementType2Key, deserializedBlocks.ContentData.Last().ContentTypeKey); + Assert.AreEqual(string.Empty, deserializedBlocks.ContentData.Last().ContentTypeAlias); + }); + + Assert.AreEqual(2, deserializedBlocks.SettingsData.Count); + Assert.Multiple(() => + { + Assert.AreEqual(settingsElementUdi1, deserializedBlocks.SettingsData.First().Udi); + Assert.AreEqual(elementType3Key, deserializedBlocks.SettingsData.First().ContentTypeKey); + Assert.AreEqual(string.Empty, deserializedBlocks.SettingsData.First().ContentTypeAlias); + + Assert.AreEqual(settingsElementUdi2, deserializedBlocks.SettingsData.Last().Udi); + Assert.AreEqual(elementType4Key, deserializedBlocks.SettingsData.Last().ContentTypeKey); + Assert.AreEqual(string.Empty, deserializedBlocks.SettingsData.Last().ContentTypeAlias); + }); + } + + [Test] + public void Can_Serialize_Richtext_Without_Blocks() + { + var richTextEditorValue = new RichTextEditorValue + { + Blocks = new RichTextBlockValue + { + Layout = new Dictionary>(), + ContentData = new List(), + SettingsData = new List() + }, + Markup = "

This is some markup

" + }; + + var serializer = new SystemTextJsonSerializer(); + var serialized = serializer.Serialize(richTextEditorValue); + var deserialized = serializer.Deserialize(serialized); + + Assert.IsNotNull(deserialized); + Assert.AreEqual("

This is some markup

", deserialized.Markup); + Assert.IsNotNull(deserialized.Blocks); + Assert.Multiple(() => + { + Assert.IsEmpty(deserialized.Blocks.Layout); + Assert.IsEmpty(deserialized.Blocks.ContentData); + Assert.IsEmpty(deserialized.Blocks.SettingsData); + }); + } + + [Test] + public void Ignores_Other_Layouts() + { + var contentElementUdi1 = Udi.Create(Constants.UdiEntityType.Element, Guid.NewGuid()); + var settingsElementUdi1 = Udi.Create(Constants.UdiEntityType.Element, Guid.NewGuid()); + + var elementType1Key = Guid.NewGuid(); + var elementType2Key = Guid.NewGuid(); + + var blockListValue = new BlockListValue + { + Layout = new Dictionary> + { + { + Constants.PropertyEditors.Aliases.TinyMce, + new IBlockLayoutItem[] + { + new RichTextBlockLayoutItem + { + ContentUdi = contentElementUdi1, + SettingsUdi = settingsElementUdi1 + } + } + }, + { + Constants.PropertyEditors.Aliases.BlockList, + new IBlockLayoutItem[] + { + new BlockListLayoutItem + { + ContentUdi = contentElementUdi1, + SettingsUdi = settingsElementUdi1 + } + } + }, + { + Constants.PropertyEditors.Aliases.BlockGrid, + new IBlockLayoutItem[] + { + new BlockGridLayoutItem + { + ContentUdi = contentElementUdi1, + SettingsUdi = settingsElementUdi1 + } + } + }, + { + "Some.Custom.Block.Editor", + new IBlockLayoutItem[] + { + new BlockListLayoutItem + { + ContentUdi = contentElementUdi1, + SettingsUdi = settingsElementUdi1 + } + } + } + }, + ContentData = + [ + new() { Udi = contentElementUdi1, ContentTypeAlias = "elementType1", ContentTypeKey = elementType1Key }, + ], + SettingsData = + [ + new() { Udi = settingsElementUdi1, ContentTypeAlias = "elementType2", ContentTypeKey = elementType2Key }, + ] + }; + + var serializer = new SystemTextJsonSerializer(); + var serialized = serializer.Serialize(blockListValue); + var deserialized = serializer.Deserialize(serialized); + + Assert.IsNotNull(deserialized); + + Assert.AreEqual(1, deserialized.Layout.Count); + Assert.IsTrue(deserialized.Layout.ContainsKey(Constants.PropertyEditors.Aliases.BlockList)); + var layoutItems = deserialized.Layout[Constants.PropertyEditors.Aliases.BlockList].OfType().ToArray(); + Assert.AreEqual(1, layoutItems.Count()); + Assert.Multiple(() => + { + Assert.AreEqual(contentElementUdi1, layoutItems.First().ContentUdi); + Assert.AreEqual(settingsElementUdi1, layoutItems.First().SettingsUdi); + }); + } +}