From 1be503e71f5e01384c773b8e63a2e7a73cf6f7ee Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Mon, 30 Sep 2024 07:01:18 +0200 Subject: [PATCH] Block level variance (#17120) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Block level variance - initial commit * Remove TODOs * Only convert RTEs with blocks * Fix JSON paths for block level property validation * Rename Properties to Values * Correct the JSON path of block level validation errors * Make it possible to skip content migration + ensure backwards compat for the new block format * Partial culture variance publishing at property level * UDI to key conversion for block editors - draft, WIP, do NOT merge 😄 (#16970) * Convert block UDIs to GUIDs * Fix merge * Fix merge issues * Rework nested layout item key parsing for backwards compatibility * Clean-up * Reverse block layout item key calculation * Review * Use IOptions to skip content migrations * Remove "published" from data editor feature naming, as it can be used in other contexts too * Parallel migration * Don't use deprecated constructor * Ensure that layout follows structure for partial publishing * Block Grid element level variance + tests (incl. refactor of element level variation tests) * Rollback unintended changes to Program.cs * Fix bad casing * Minor formatting * RTE element level variance + tests * Remove obsoleted constructors * Use Umbraco.RichText instead of Umbraco.TinyMCE as layout alias for blocks in the RTE * Fix bad merge * Temporary fix for new cache in integration tests * Add EditorAlias to block level properties * Remove the unintended PropertyEditorAlias output for block values * Add EditorAlias to Datatype Item model * Update OpenApi.json * Introduce "expose" for blocks * Strict (explicit) handling for Expose * Improve handling of document and element level variance changes * Refactor variance alignment for published rendering * Block UDI to Key conversion should also register as a conversion * Convert newly added RTE unit test to new RTE blocks format * Minor review changes * Run memory intensive tests on Linux only * Add tests proving that AllowEditInvariantFromNonDefault has effect for block level variance too * Fix the Platform annotations * Removed Platform annotations for tests. * Fix merge * Obsolete old PublishCulture extension * More fixing bad merge --------- Co-authored-by: Niels Lyngsø Co-authored-by: nikolajlauridsen --- .../Mapping/Item/ItemTypeMapDefinition.cs | 1 + src/Umbraco.Cms.Api.Management/OpenApi.json | 4 + .../Item/DataTypeItemResponseModel.cs | 2 + .../Models/Blocks/BlockEditorDataConverter.cs | 54 +- .../Blocks/BlockGridEditorDataConverter.cs | 10 +- .../Models/Blocks/BlockGridItem.cs | 59 +- .../Models/Blocks/BlockGridLayoutItem.cs | 27 +- .../Models/Blocks/BlockGridValue.cs | 3 + .../Models/Blocks/BlockItemData.cs | 51 +- .../Models/Blocks/BlockItemVariation.cs | 21 + .../Models/Blocks/BlockLayoutItemBase.cs | 84 + .../Blocks/BlockListEditorDataConverter.cs | 12 +- .../Models/Blocks/BlockListItem.cs | 50 +- .../Models/Blocks/BlockListLayoutItem.cs | 27 +- .../Models/Blocks/BlockListValue.cs | 3 + .../Models/Blocks/BlockPropertyValue.cs | 13 + src/Umbraco.Core/Models/Blocks/BlockValue.cs | 11 + .../Blocks/ContentAndSettingsReference.cs | 22 +- .../Models/Blocks/IBlockLayoutItem.cs | 6 + .../Models/Blocks/IBlockReference.cs | 2 +- .../Models/Blocks/RichTextBlockItem.cs | 44 +- .../Models/Blocks/RichTextBlockLayoutItem.cs | 27 +- .../Models/Blocks/RichTextBlockValue.cs | 10 +- .../RichTextEditorBlockDataConverter.cs | 8 +- .../Models/ContentRepositoryExtensions.cs | 36 +- src/Umbraco.Core/Models/IProperty.cs | 3 + src/Umbraco.Core/Models/Property.cs | 28 +- .../PropertyEditors/DataEditor.cs | 6 + .../PropertyEditors/IDataEditor.cs | 14 + src/Umbraco.Core/Services/ContentService.cs | 24 +- .../DeliveryApi/ApiRichTextElementParser.cs | 8 +- .../DeliveryApi/ApiRichTextMarkupParser.cs | 8 +- .../DeliveryApi/ApiRichTextParserBase.cs | 2 + .../UmbracoBuilder.CoreServices.cs | 1 + .../Migrations/Upgrade/UmbracoPlan.cs | 3 + .../ConvertBlockEditorPropertiesBase.cs | 281 +++ .../ConvertBlockEditorPropertiesOptions.cs | 29 + .../ConvertBlockGridEditorProperties.cs | 47 + .../ConvertBlockListEditorProperties.cs | 47 + .../ConvertRichTextEditorProperties.cs | 74 + ...ckEditorPropertyNotificationHandlerBase.cs | 44 +- .../BlockEditorPropertyValueEditor.cs | 77 +- .../BlockEditorValidatorBase.cs | 23 +- .../PropertyEditors/BlockEditorValues.cs | 32 +- .../BlockGridPropertyEditor.cs | 12 + .../BlockGridPropertyEditorBase.cs | 10 +- .../BlockListPropertyEditor.cs | 12 + .../BlockListPropertyEditorBase.cs | 8 +- .../BlockValuePropertyIndexValueFactory.cs | 6 +- .../BlockValuePropertyValueEditorBase.cs | 220 ++- .../MediaPicker3PropertyEditor.cs | 8 +- .../PropertyEditors/RichTextPropertyEditor.cs | 93 +- .../RichTextPropertyIndexValueFactory.cs | 4 +- .../ValueConverters/BlockEditorConverter.cs | 51 +- ...EditorPropertyValueConstructorCacheBase.cs | 6 +- .../BlockEditorVarianceHandler.cs | 137 ++ .../BlockGridPropertyValueConverter.cs | 27 +- .../BlockGridPropertyValueCreator.cs | 13 +- .../BlockListPropertyValueConverter.cs | 31 +- .../BlockListPropertyValueCreator.cs | 9 +- .../BlockPropertyValueConverterBase.cs | 255 --- .../BlockPropertyValueCreatorBase.cs | 61 +- .../RichTextBlockPropertyValueCreator.cs | 9 +- .../ValueConverters/RichTextParsingRegexes.cs | 2 +- .../RteBlockRenderingValueConverter.cs | 30 +- .../Serialization/JsonBlockValueConverter.cs | 20 +- .../Services/ContentServiceTests.cs | 4 +- .../Repositories/DocumentRepositoryTest.cs | 10 +- .../BlockEditorBackwardsCompatibilityTests.cs | 522 ++++++ .../BlockEditorElementVariationTestBase.cs | 172 ++ .../BlockGridElementLevelVariationTests.cs | 533 ++++++ ...kListElementLevelVariationTests.Parsing.cs | 662 +++++++ ...stElementLevelVariationTests.Publishing.cs | 1551 +++++++++++++++++ .../BlockListElementLevelVariationTests.cs | 167 ++ .../BlockListPropertyEditorTests.cs | 365 ++++ .../PropertyIndexValueFactoryTests.cs | 55 +- .../RichTextElementLevelVariationTests.cs | 495 ++++++ .../RichTextPropertyEditorTests.cs | 20 +- .../Services/ContentValidationServiceTests.cs | 141 +- .../Umbraco.Tests.Integration.csproj | 6 + .../DeliveryApi/RichTextParserTests.cs | 10 +- .../Umbraco.Core/Models/ContentTests.cs | 11 +- .../Umbraco.Core/Models/VariationTests.cs | 24 +- .../BlockEditorComponentTests.cs | 141 +- .../BlockGridPropertyValueConverterTests.cs | 59 +- .../BlockListPropertyValueConverterTests.cs | 122 +- .../BlockPropertyValueConverterTestsBase.cs | 4 + .../DataValueEditorReuseTests.cs | 5 +- .../RichTextPropertyEditorHelperTests.cs | 110 +- .../BlockEditorVarianceHandlerTests.cs | 75 + .../JsonBlockValueConverterTests.cs | 170 +- 91 files changed, 6762 insertions(+), 1034 deletions(-) create mode 100644 src/Umbraco.Core/Models/Blocks/BlockItemVariation.cs create mode 100644 src/Umbraco.Core/Models/Blocks/BlockLayoutItemBase.cs create mode 100644 src/Umbraco.Core/Models/Blocks/BlockPropertyValue.cs create mode 100644 src/Umbraco.Infrastructure/Migrations/Upgrade/V_15_0_0/ConvertBlockEditorPropertiesBase.cs create mode 100644 src/Umbraco.Infrastructure/Migrations/Upgrade/V_15_0_0/ConvertBlockEditorPropertiesOptions.cs create mode 100644 src/Umbraco.Infrastructure/Migrations/Upgrade/V_15_0_0/ConvertBlockGridEditorProperties.cs create mode 100644 src/Umbraco.Infrastructure/Migrations/Upgrade/V_15_0_0/ConvertBlockListEditorProperties.cs create mode 100644 src/Umbraco.Infrastructure/Migrations/Upgrade/V_15_0_0/ConvertRichTextEditorProperties.cs create mode 100644 src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/BlockEditorVarianceHandler.cs delete mode 100644 src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/BlockPropertyValueConverterBase.cs create mode 100644 tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/PropertyEditors/BlockEditorBackwardsCompatibilityTests.cs create mode 100644 tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/PropertyEditors/BlockEditorElementVariationTestBase.cs create mode 100644 tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/PropertyEditors/BlockGridElementLevelVariationTests.cs create mode 100644 tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/PropertyEditors/BlockListElementLevelVariationTests.Parsing.cs create mode 100644 tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/PropertyEditors/BlockListElementLevelVariationTests.Publishing.cs create mode 100644 tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/PropertyEditors/BlockListElementLevelVariationTests.cs create mode 100644 tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/PropertyEditors/BlockListPropertyEditorTests.cs create mode 100644 tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/PropertyEditors/RichTextElementLevelVariationTests.cs create mode 100644 tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/PropertyEditors/BlockEditorVarianceHandlerTests.cs diff --git a/src/Umbraco.Cms.Api.Management/Mapping/Item/ItemTypeMapDefinition.cs b/src/Umbraco.Cms.Api.Management/Mapping/Item/ItemTypeMapDefinition.cs index 9152970b36fc..5b588623776f 100644 --- a/src/Umbraco.Cms.Api.Management/Mapping/Item/ItemTypeMapDefinition.cs +++ b/src/Umbraco.Cms.Api.Management/Mapping/Item/ItemTypeMapDefinition.cs @@ -48,6 +48,7 @@ private static void Map(IDataType source, DataTypeItemResponseModel target, Mapp target.Name = source.Name ?? string.Empty; target.Id = source.Key; target.EditorUiAlias = source.EditorUiAlias; + target.EditorAlias = source.EditorAlias; target.IsDeletable = source.IsDeletableDataType(); } diff --git a/src/Umbraco.Cms.Api.Management/OpenApi.json b/src/Umbraco.Cms.Api.Management/OpenApi.json index 29acbdd16a76..eb912bf85690 100644 --- a/src/Umbraco.Cms.Api.Management/OpenApi.json +++ b/src/Umbraco.Cms.Api.Management/OpenApi.json @@ -35987,6 +35987,7 @@ }, "DataTypeItemResponseModel": { "required": [ + "editorAlias", "id", "isDeletable", "name" @@ -36004,6 +36005,9 @@ "type": "string", "nullable": true }, + "editorAlias": { + "type": "string" + }, "isDeletable": { "type": "boolean" } diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/DataType/Item/DataTypeItemResponseModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/DataType/Item/DataTypeItemResponseModel.cs index b753aa22b27a..947abe388d59 100644 --- a/src/Umbraco.Cms.Api.Management/ViewModels/DataType/Item/DataTypeItemResponseModel.cs +++ b/src/Umbraco.Cms.Api.Management/ViewModels/DataType/Item/DataTypeItemResponseModel.cs @@ -6,5 +6,7 @@ public class DataTypeItemResponseModel : NamedItemResponseModelBase { public string? EditorUiAlias { get; set; } + public string EditorAlias { get; set; } = string.Empty; + public bool IsDeletable { get; set; } } diff --git a/src/Umbraco.Core/Models/Blocks/BlockEditorDataConverter.cs b/src/Umbraco.Core/Models/Blocks/BlockEditorDataConverter.cs index e197f1d59e21..5e263cf4c00d 100644 --- a/src/Umbraco.Core/Models/Blocks/BlockEditorDataConverter.cs +++ b/src/Umbraco.Core/Models/Blocks/BlockEditorDataConverter.cs @@ -62,13 +62,63 @@ public BlockEditorData Deserialize(string json) public BlockEditorData Convert(TValue? value) { - if (value?.GetLayouts() is not IEnumerable layouts) + if (value is not null) + { + var converted = ConvertOriginalBlockFormat(value.ContentData); + if (converted) + { + ConvertOriginalBlockFormat(value.SettingsData); + AmendExpose(value); + } + } + + TLayout[]? layouts = value?.GetLayouts()?.ToArray(); + if (layouts is null) { return BlockEditorData.Empty; } IEnumerable references = GetBlockReferences(layouts); - return new BlockEditorData(references, value); + return new BlockEditorData(references, value!); + } + + // this method is only meant to have any effect when migrating block editor values + // from the original format to the new, variant enabled format + private void AmendExpose(TValue value) + => value.Expose = value.ContentData.Select(cd => new BlockItemVariation(cd.Key, null, null)).ToList(); + + // this method is only meant to have any effect when migrating block editor values + // from the original format to the new, variant enabled format + private bool ConvertOriginalBlockFormat(List blockItemDatas) + { + var converted = false; + foreach (BlockItemData blockItemData in blockItemDatas) + { + // only overwrite the Properties collection if none have been added at this point + if (blockItemData.Values.Any() is false && blockItemData.RawPropertyValues.Any()) + { + blockItemData.Values = blockItemData + .RawPropertyValues + .Select(item => new BlockPropertyValue { Alias = item.Key, Value = item.Value }) + .ToList(); + converted = true; + } + + // no matter what, clear the RawPropertyValues collection so it is not saved back to the DB + blockItemData.RawPropertyValues.Clear(); + + // assign the correct Key if only a UDI is set + if (blockItemData.Key == Guid.Empty && blockItemData.Udi is GuidUdi guidUdi) + { + blockItemData.Key = guidUdi.Guid; + converted = true; + } + + // no matter what, clear the UDI value so it's not saved back to the DB + blockItemData.Udi = null; + } + + return converted; } } diff --git a/src/Umbraco.Core/Models/Blocks/BlockGridEditorDataConverter.cs b/src/Umbraco.Core/Models/Blocks/BlockGridEditorDataConverter.cs index b771ed1e3cee..b2e3a1337dee 100644 --- a/src/Umbraco.Core/Models/Blocks/BlockGridEditorDataConverter.cs +++ b/src/Umbraco.Core/Models/Blocks/BlockGridEditorDataConverter.cs @@ -1,8 +1,6 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using Microsoft.Extensions.DependencyInjection; -using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.Serialization; namespace Umbraco.Cms.Core.Models.Blocks; @@ -12,12 +10,6 @@ namespace Umbraco.Cms.Core.Models.Blocks; /// public class BlockGridEditorDataConverter : BlockEditorDataConverter { - [Obsolete("Use the constructor that takes IJsonSerializer. Will be removed in V15.")] - public BlockGridEditorDataConverter() - : this(StaticServiceProvider.Instance.GetRequiredService()) - { - } - public BlockGridEditorDataConverter(IJsonSerializer jsonSerializer) : base(jsonSerializer) { @@ -27,7 +19,7 @@ protected override IEnumerable GetBlockReferences(I { IList ExtractContentAndSettingsReferences(BlockGridLayoutItem item) { - var references = new List { new(item.ContentUdi, item.SettingsUdi) }; + var references = new List { new(item.ContentKey, item.SettingsKey) }; references.AddRange(item.Areas.SelectMany(area => area.Items.SelectMany(ExtractContentAndSettingsReferences))); return references; } diff --git a/src/Umbraco.Core/Models/Blocks/BlockGridItem.cs b/src/Umbraco.Core/Models/Blocks/BlockGridItem.cs index abe8cc89a0d6..8c463187e32a 100644 --- a/src/Umbraco.Core/Models/Blocks/BlockGridItem.cs +++ b/src/Umbraco.Core/Models/Blocks/BlockGridItem.cs @@ -23,21 +23,45 @@ public class BlockGridItem : IBlockReferencecontentUdi /// or /// content + [Obsolete("Use constructor that accepts GUIDs instead. Will be removed in V18.")] public BlockGridItem(Udi contentUdi, IPublishedElement content, Udi settingsUdi, IPublishedElement settings) + : this( + (contentUdi as GuidUdi)?.Guid ?? throw new ArgumentException(nameof(contentUdi)), + content, + (settingsUdi as GuidUdi)?.Guid, + settings) { - ContentUdi = contentUdi ?? throw new ArgumentNullException(nameof(contentUdi)); + } + + public BlockGridItem(Guid contentKey, IPublishedElement content, Guid? settingsKey, IPublishedElement? settings) + { + ContentKey = contentKey; + ContentUdi = new GuidUdi(Constants.UdiEntityType.Element, contentKey); Content = content ?? throw new ArgumentNullException(nameof(content)); - SettingsUdi = settingsUdi; + SettingsKey = settingsKey; + SettingsUdi = settingsKey.HasValue + ? new GuidUdi(Constants.UdiEntityType.Element, settingsKey.Value) + : null; Settings = settings; } + /// + /// Gets the content key. + /// + public Guid ContentKey { get; set; } + + /// + /// Gets the settings key. + /// + public Guid? SettingsKey { get; set; } + /// /// Gets the content UDI. /// /// /// The content UDI. /// - [DataMember(Name = "contentUdi")] + [Obsolete("Use ContentKey instead. Will be removed in V18.")] public Udi ContentUdi { get; } /// @@ -46,7 +70,6 @@ public BlockGridItem(Udi contentUdi, IPublishedElement content, Udi settingsUdi, /// /// The content. /// - [DataMember(Name = "content")] public IPublishedElement Content { get; } /// @@ -55,8 +78,8 @@ public BlockGridItem(Udi contentUdi, IPublishedElement content, Udi settingsUdi, /// /// The settings UDI. /// - [DataMember(Name = "settingsUdi")] - public Udi SettingsUdi { get; } + [Obsolete("Use SettingsKey instead. Will be removed in V18.")] + public Udi? SettingsUdi { get; } /// /// Gets the settings. @@ -64,37 +87,31 @@ public BlockGridItem(Udi contentUdi, IPublishedElement content, Udi settingsUdi, /// /// The settings. /// - [DataMember(Name = "settings")] - public IPublishedElement Settings { get; } + public IPublishedElement? Settings { get; } /// /// The number of rows this item should span /// - [DataMember(Name = "rowSpan")] public int RowSpan { get; set; } /// /// The number of columns this item should span /// - [DataMember(Name = "columnSpan")] public int ColumnSpan { get; set; } /// /// The grid areas within this item /// - [DataMember(Name = "areas")] public IEnumerable Areas { get; set; } = Array.Empty(); /// /// The number of columns available for the areas to span /// - [DataMember(Name = "areaGridColumns")] public int? AreaGridColumns { get; set; } /// /// The number of columns in the root grid /// - [DataMember(Name = "gridColumns")] public int? GridColumns { get; set; } } @@ -112,12 +129,19 @@ public class BlockGridItem : BlockGridItem /// The content. /// The settings UDI. /// The settings. + [Obsolete("Use constructor that accepts GUIDs instead. Will be removed in V18.")] public BlockGridItem(Udi contentUdi, T content, Udi settingsUdi, IPublishedElement settings) : base(contentUdi, content, settingsUdi, settings) { Content = content; } + public BlockGridItem(Guid contentKey, T content, Guid? settingsKey, IPublishedElement? settings) + : base(contentKey, content, settingsKey, settings) + { + Content = content; + } + /// /// Gets the content. /// @@ -143,18 +167,25 @@ public class BlockGridItem : BlockGridItem /// The content. /// The settings udi. /// The settings. + [Obsolete("Use constructor that accepts GUIDs instead. Will be removed in V18.")] public BlockGridItem(Udi contentUdi, TContent content, Udi settingsUdi, TSettings settings) : base(contentUdi, content, settingsUdi, settings) { Settings = settings; } + public BlockGridItem(Guid contentKey, TContent content, Guid? settingsKey, TSettings? settings) + : base(contentKey, content, settingsKey, settings) + { + Settings = settings; + } + /// /// Gets the settings. /// /// /// The settings. /// - public new TSettings Settings { get; } + public new TSettings? Settings { get; } } } diff --git a/src/Umbraco.Core/Models/Blocks/BlockGridLayoutItem.cs b/src/Umbraco.Core/Models/Blocks/BlockGridLayoutItem.cs index bd24a0d5f564..ff977acf9836 100644 --- a/src/Umbraco.Core/Models/Blocks/BlockGridLayoutItem.cs +++ b/src/Umbraco.Core/Models/Blocks/BlockGridLayoutItem.cs @@ -6,12 +6,8 @@ namespace Umbraco.Cms.Core.Models.Blocks; /// /// Used for deserializing the block grid layout /// -public class BlockGridLayoutItem : IBlockLayoutItem +public class BlockGridLayoutItem : BlockLayoutItemBase { - public Udi? ContentUdi { get; set; } - - public Udi? SettingsUdi { get; set; } - public int? ColumnSpan { get; set; } public int? RowSpan { get; set; } @@ -21,10 +17,25 @@ public class BlockGridLayoutItem : IBlockLayoutItem public BlockGridLayoutItem() { } + [Obsolete("Use constructor that accepts GUIDs instead. Will be removed in V18.")] public BlockGridLayoutItem(Udi contentUdi) - => ContentUdi = contentUdi; + : base(contentUdi) + { + } + [Obsolete("Use constructor that accepts GUIDs instead. Will be removed in V18.")] public BlockGridLayoutItem(Udi contentUdi, Udi settingsUdi) - : this(contentUdi) - => SettingsUdi = settingsUdi; + : base(contentUdi, settingsUdi) + { + } + + public BlockGridLayoutItem(Guid contentKey) + : base(contentKey) + { + } + + public BlockGridLayoutItem(Guid contentKey, Guid settingsKey) + : base(contentKey, settingsKey) + { + } } diff --git a/src/Umbraco.Core/Models/Blocks/BlockGridValue.cs b/src/Umbraco.Core/Models/Blocks/BlockGridValue.cs index 650f4e47542d..e7f7c1ca17b7 100644 --- a/src/Umbraco.Core/Models/Blocks/BlockGridValue.cs +++ b/src/Umbraco.Core/Models/Blocks/BlockGridValue.cs @@ -1,3 +1,5 @@ +using System.Text.Json.Serialization; + namespace Umbraco.Cms.Core.Models.Blocks; /// @@ -19,5 +21,6 @@ public BlockGridValue(IEnumerable layouts) => Layout[PropertyEditorAlias] = layouts; /// + [JsonIgnore] public override string PropertyEditorAlias => Constants.PropertyEditors.Aliases.BlockGrid; } diff --git a/src/Umbraco.Core/Models/Blocks/BlockItemData.cs b/src/Umbraco.Core/Models/Blocks/BlockItemData.cs index 75d34ba8c6fb..e99e8010e7e2 100644 --- a/src/Umbraco.Core/Models/Blocks/BlockItemData.cs +++ b/src/Umbraco.Core/Models/Blocks/BlockItemData.cs @@ -14,10 +14,20 @@ public BlockItemData() { } + [Obsolete("Use constructor that accepts GUID key instead. Will be removed in V18.")] public BlockItemData(Udi udi, Guid contentTypeKey, string contentTypeAlias) + : this( + (udi as GuidUdi)?.Guid ?? throw new ArgumentException(nameof(udi)), + contentTypeKey, + contentTypeAlias) + { + } + + public BlockItemData(Guid key, Guid contentTypeKey, string contentTypeAlias) { ContentTypeAlias = contentTypeAlias; - Udi = udi; + Key = key; + Udi = new GuidUdi(Constants.UdiEntityType.Element, key); ContentTypeKey = contentTypeKey; } @@ -29,43 +39,14 @@ public BlockItemData(Udi udi, Guid contentTypeKey, string contentTypeAlias) [JsonIgnore] public string ContentTypeAlias { get; set; } = string.Empty; + [Obsolete("Use Key instead. Will be removed in V18.")] public Udi? Udi { get; set; } - [JsonIgnore] - public Guid Key => Udi is not null ? ((GuidUdi)Udi).Guid : throw new InvalidOperationException("No Udi assigned"); + public Guid Key { get; set; } - /// - /// The remaining properties will be serialized to a dictionary - /// - /// - /// The JsonExtensionDataAttribute is used to put the non-typed properties into a bucket - /// https://docs.microsoft.com/en-us/dotnet/api/system.text.json.serialization.jsonextensiondataattribute - /// NestedContent serializes to string, int, whatever eg - /// "stringValue":"Some String","numericValue":125,"otherNumeric":null - /// + public IList Values { get; set; } = new List(); + + [Obsolete("Use Properties instead. Will be removed in V18.")] [JsonExtensionData] public Dictionary RawPropertyValues { get; set; } = new(); - - /// - /// Used during deserialization to convert the raw property data into data with a property type context - /// - [JsonIgnore] - public IDictionary PropertyValues { get; set; } = - new Dictionary(); - - /// - /// Used during deserialization to populate the property value/property type of a block item content property - /// - public class BlockPropertyValue - { - public BlockPropertyValue(object? value, IPropertyType propertyType) - { - Value = value; - PropertyType = propertyType ?? throw new ArgumentNullException(nameof(propertyType)); - } - - public object? Value { get; } - - public IPropertyType PropertyType { get; } - } } diff --git a/src/Umbraco.Core/Models/Blocks/BlockItemVariation.cs b/src/Umbraco.Core/Models/Blocks/BlockItemVariation.cs new file mode 100644 index 000000000000..bb8dfcff2298 --- /dev/null +++ b/src/Umbraco.Core/Models/Blocks/BlockItemVariation.cs @@ -0,0 +1,21 @@ +namespace Umbraco.Cms.Core.Models.Blocks; + +public class BlockItemVariation +{ + public BlockItemVariation() + { + } + + public BlockItemVariation(Guid contentKey, string? culture, string? segment) + { + ContentKey = contentKey; + Culture = culture; + Segment = segment; + } + + public Guid ContentKey { get; set; } + + public string? Culture { get; set; } + + public string? Segment { get; set; } +} diff --git a/src/Umbraco.Core/Models/Blocks/BlockLayoutItemBase.cs b/src/Umbraco.Core/Models/Blocks/BlockLayoutItemBase.cs new file mode 100644 index 000000000000..4f4ff9e22acc --- /dev/null +++ b/src/Umbraco.Core/Models/Blocks/BlockLayoutItemBase.cs @@ -0,0 +1,84 @@ +namespace Umbraco.Cms.Core.Models.Blocks; + +public abstract class BlockLayoutItemBase : IBlockLayoutItem +{ + private Guid? _contentKey; + private Guid? _settingsKey; + + private Udi? _contentUdi; + private Udi? _settingsUdi; + + [Obsolete("Use ContentKey instead. Will be removed in V18.")] + public Udi? ContentUdi + { + get => _contentUdi; + set + { + if (_contentKey is not null) + { + return; + } + + _contentUdi = value; + _contentKey = (value as GuidUdi)?.Guid; + } + } + + [Obsolete("Use SettingsKey instead. Will be removed in V18.")] + public Udi? SettingsUdi + { + get => _settingsUdi; + set + { + if (_settingsKey is not null) + { + return; + } + + _settingsUdi = value; + _settingsKey = (value as GuidUdi)?.Guid; + } + } + + public Guid ContentKey + { + get => _contentKey ?? throw new InvalidOperationException("ContentKey has not yet been initialized"); + set => _contentKey = value; + } + + public Guid? SettingsKey + { + get => _settingsKey; + set => _settingsKey = value; + } + + protected BlockLayoutItemBase() + { } + + [Obsolete("Use constructor that accepts GUIDs instead. Will be removed in V18.")] + protected BlockLayoutItemBase(Udi contentUdi) + : this((contentUdi as GuidUdi)?.Guid ?? throw new ArgumentException(nameof(contentUdi))) + { + } + + [Obsolete("Use constructor that accepts GUIDs instead. Will be removed in V18.")] + protected BlockLayoutItemBase(Udi contentUdi, Udi settingsUdi) + : this( + (contentUdi as GuidUdi)?.Guid ?? throw new ArgumentException(nameof(contentUdi)), + (settingsUdi as GuidUdi)?.Guid ?? throw new ArgumentException(nameof(settingsUdi))) + { + } + + protected BlockLayoutItemBase(Guid contentKey) + { + ContentKey = contentKey; + ContentUdi = new GuidUdi(Constants.UdiEntityType.Element, contentKey); + } + + protected BlockLayoutItemBase(Guid contentKey, Guid settingsKey) + : this(contentKey) + { + SettingsKey = settingsKey; + SettingsUdi = new GuidUdi(Constants.UdiEntityType.Element, settingsKey); + } +} diff --git a/src/Umbraco.Core/Models/Blocks/BlockListEditorDataConverter.cs b/src/Umbraco.Core/Models/Blocks/BlockListEditorDataConverter.cs index 7842c66a2867..e213be5f127e 100644 --- a/src/Umbraco.Core/Models/Blocks/BlockListEditorDataConverter.cs +++ b/src/Umbraco.Core/Models/Blocks/BlockListEditorDataConverter.cs @@ -1,8 +1,6 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using Microsoft.Extensions.DependencyInjection; -using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.Serialization; namespace Umbraco.Cms.Core.Models.Blocks; @@ -12,17 +10,11 @@ namespace Umbraco.Cms.Core.Models.Blocks; /// public class BlockListEditorDataConverter : BlockEditorDataConverter { - [Obsolete("Use the constructor that takes IJsonSerializer. Will be removed in V15.")] - public BlockListEditorDataConverter() - : this(StaticServiceProvider.Instance.GetRequiredService()) - { - } - public BlockListEditorDataConverter(IJsonSerializer jsonSerializer) - : base(Constants.PropertyEditors.Aliases.BlockList, jsonSerializer) + : base(jsonSerializer) { } protected override IEnumerable GetBlockReferences(IEnumerable layout) - => layout.Select(x => new ContentAndSettingsReference(x.ContentUdi, x.SettingsUdi)).ToList(); + => layout.Select(x => new ContentAndSettingsReference(x.ContentKey, x.SettingsKey)).ToList(); } diff --git a/src/Umbraco.Core/Models/Blocks/BlockListItem.cs b/src/Umbraco.Core/Models/Blocks/BlockListItem.cs index 6ccc4080e216..7e14a1b1e8c5 100644 --- a/src/Umbraco.Core/Models/Blocks/BlockListItem.cs +++ b/src/Umbraco.Core/Models/Blocks/BlockListItem.cs @@ -25,11 +25,25 @@ public class BlockListItem : IBlockReference + [Obsolete("Use constructor that accepts GUIDs instead. Will be removed in V18.")] public BlockListItem(Udi contentUdi, IPublishedElement content, Udi settingsUdi, IPublishedElement settings) + : this( + (contentUdi as GuidUdi)?.Guid ?? throw new ArgumentException(nameof(contentUdi)), + content, + (settingsUdi as GuidUdi)?.Guid, + settings) { - ContentUdi = contentUdi ?? throw new ArgumentNullException(nameof(contentUdi)); + } + + public BlockListItem(Guid contentKey, IPublishedElement content, Guid? settingsKey, IPublishedElement? settings) + { + ContentKey = contentKey; + ContentUdi = new GuidUdi(Constants.UdiEntityType.Element, contentKey); Content = content ?? throw new ArgumentNullException(nameof(content)); - SettingsUdi = settingsUdi; + SettingsKey = settingsKey; + SettingsUdi = settingsKey.HasValue + ? new GuidUdi(Constants.UdiEntityType.Element, settingsKey.Value) + : null; Settings = settings; } @@ -39,7 +53,6 @@ public BlockListItem(Udi contentUdi, IPublishedElement content, Udi settingsUdi, /// /// The content. /// - [DataMember(Name = "content")] public IPublishedElement Content { get; } /// @@ -48,8 +61,8 @@ public BlockListItem(Udi contentUdi, IPublishedElement content, Udi settingsUdi, /// /// The settings UDI. /// - [DataMember(Name = "settingsUdi")] - public Udi SettingsUdi { get; } + [Obsolete("Use SettingsKey instead. Will be removed in V18.")] + public Udi? SettingsUdi { get; } /// /// Gets the content UDI. @@ -57,17 +70,26 @@ public BlockListItem(Udi contentUdi, IPublishedElement content, Udi settingsUdi, /// /// The content UDI. /// - [DataMember(Name = "contentUdi")] + [Obsolete("Use ContentKey instead. Will be removed in V18.")] public Udi ContentUdi { get; } + /// + /// Gets the content key. + /// + public Guid ContentKey { get; set; } + + /// + /// Gets the settings key. + /// + public Guid? SettingsKey { get; set; } + /// /// Gets the settings. /// /// /// The settings. /// - [DataMember(Name = "settings")] - public IPublishedElement Settings { get; } + public IPublishedElement? Settings { get; } } /// @@ -85,10 +107,15 @@ public class BlockListItem : BlockListItem /// The content. /// The settings UDI. /// The settings. + [Obsolete("Use constructor that accepts GUIDs instead. Will be removed in V18.")] public BlockListItem(Udi contentUdi, T content, Udi settingsUdi, IPublishedElement settings) : base(contentUdi, content, settingsUdi, settings) => Content = content; + public BlockListItem(Guid contentKey, T content, Guid? settingsKey, IPublishedElement? settings) + : base(contentKey, content, settingsKey, settings) => + Content = content; + /// /// Gets the content. /// @@ -115,15 +142,20 @@ public class BlockListItem : BlockListItem /// The content. /// The settings udi. /// The settings. + [Obsolete("Use constructor that accepts GUIDs instead. Will be removed in V18.")] public BlockListItem(Udi contentUdi, TContent content, Udi settingsUdi, TSettings settings) : base(contentUdi, content, settingsUdi, settings) => Settings = settings; + public BlockListItem(Guid contentKey, TContent content, Guid? settingsKey, TSettings? settings) + : base(contentKey, content, settingsKey, settings) => + Settings = settings; + /// /// Gets the settings. /// /// /// The settings. /// - public new TSettings Settings { get; } + public new TSettings? Settings { get; } } diff --git a/src/Umbraco.Core/Models/Blocks/BlockListLayoutItem.cs b/src/Umbraco.Core/Models/Blocks/BlockListLayoutItem.cs index 4412257add3b..cffa234156aa 100644 --- a/src/Umbraco.Core/Models/Blocks/BlockListLayoutItem.cs +++ b/src/Umbraco.Core/Models/Blocks/BlockListLayoutItem.cs @@ -6,19 +6,30 @@ namespace Umbraco.Cms.Core.Models.Blocks; /// /// Used for deserializing the block list layout /// -public class BlockListLayoutItem : IBlockLayoutItem +public class BlockListLayoutItem : BlockLayoutItemBase { - public Udi? ContentUdi { get; set; } - - public Udi? SettingsUdi { get; set; } - public BlockListLayoutItem() { } + [Obsolete("Use constructor that accepts GUIDs instead. Will be removed in V18.")] public BlockListLayoutItem(Udi contentUdi) - => ContentUdi = contentUdi; + : base(contentUdi) + { + } + [Obsolete("Use constructor that accepts GUIDs instead. Will be removed in V18.")] public BlockListLayoutItem(Udi contentUdi, Udi settingsUdi) - : this(contentUdi) - => SettingsUdi = settingsUdi; + : base(contentUdi, settingsUdi) + { + } + + public BlockListLayoutItem(Guid contentKey) + : base(contentKey) + { + } + + public BlockListLayoutItem(Guid contentKey, Guid settingsKey) + : base(contentKey, settingsKey) + { + } } diff --git a/src/Umbraco.Core/Models/Blocks/BlockListValue.cs b/src/Umbraco.Core/Models/Blocks/BlockListValue.cs index d06277f71f02..fe6524f88d12 100644 --- a/src/Umbraco.Core/Models/Blocks/BlockListValue.cs +++ b/src/Umbraco.Core/Models/Blocks/BlockListValue.cs @@ -1,3 +1,5 @@ +using System.Text.Json.Serialization; + namespace Umbraco.Cms.Core.Models.Blocks; /// @@ -19,5 +21,6 @@ public BlockListValue(IEnumerable layouts) => Layout[PropertyEditorAlias] = layouts; /// + [JsonIgnore] public override string PropertyEditorAlias => Constants.PropertyEditors.Aliases.BlockList; } diff --git a/src/Umbraco.Core/Models/Blocks/BlockPropertyValue.cs b/src/Umbraco.Core/Models/Blocks/BlockPropertyValue.cs new file mode 100644 index 000000000000..0b27f1668181 --- /dev/null +++ b/src/Umbraco.Core/Models/Blocks/BlockPropertyValue.cs @@ -0,0 +1,13 @@ +using System.Text.Json.Serialization; +using Umbraco.Cms.Core.Models.ContentEditing; + +namespace Umbraco.Cms.Core.Models.Blocks; + +public sealed class BlockPropertyValue : ValueModelBase +{ + // Used during deserialization to populate the property value/property type of a block item content property + [JsonIgnore] + public IPropertyType? PropertyType { get; set; } + + public string? EditorAlias => PropertyType?.PropertyEditorAlias; +} diff --git a/src/Umbraco.Core/Models/Blocks/BlockValue.cs b/src/Umbraco.Core/Models/Blocks/BlockValue.cs index bfe84430f8f3..1c25c8d434e1 100644 --- a/src/Umbraco.Core/Models/Blocks/BlockValue.cs +++ b/src/Umbraco.Core/Models/Blocks/BlockValue.cs @@ -32,6 +32,14 @@ public abstract class BlockValue /// public List SettingsData { get; set; } = []; + /// + /// Gets or sets the availability of blocks per variation. + /// + /// + /// Only applicable for block level variance. + /// + public IList Expose { get; set; } = new List(); + /// /// Gets the property editor alias of the current layout. /// @@ -39,6 +47,9 @@ public abstract class BlockValue /// The property editor alias of the current layout. /// public abstract string PropertyEditorAlias { get; } + + [Obsolete("Will be removed in V18.")] + public virtual bool SupportsBlockLayoutAlias(string alias) => alias.Equals(PropertyEditorAlias); } /// diff --git a/src/Umbraco.Core/Models/Blocks/ContentAndSettingsReference.cs b/src/Umbraco.Core/Models/Blocks/ContentAndSettingsReference.cs index 61b95235cd6c..f26db3b46d56 100644 --- a/src/Umbraco.Core/Models/Blocks/ContentAndSettingsReference.cs +++ b/src/Umbraco.Core/Models/Blocks/ContentAndSettingsReference.cs @@ -5,16 +5,34 @@ namespace Umbraco.Cms.Core.Models.Blocks; public struct ContentAndSettingsReference : IEquatable { + [Obsolete("Use constructor that accepts GUIDs instead. Will be removed in V18.")] public ContentAndSettingsReference(Udi? contentUdi, Udi? settingsUdi) + : this( + (contentUdi as GuidUdi)?.Guid ?? throw new ArgumentException(nameof(contentUdi)), + (settingsUdi as GuidUdi)?.Guid) { - ContentUdi = contentUdi ?? throw new ArgumentNullException(nameof(contentUdi)); - SettingsUdi = settingsUdi; } + public ContentAndSettingsReference(Guid contentKey, Guid? settingsKey) + { + ContentKey = contentKey; + SettingsKey = settingsKey; + ContentUdi = new GuidUdi(Constants.UdiEntityType.Element, contentKey); + SettingsUdi = settingsKey.HasValue + ? new GuidUdi(Constants.UdiEntityType.Element, settingsKey.Value) + : null; + } + + [Obsolete("Use ContentKey instead. Will be removed in V18.")] public Udi ContentUdi { get; } + [Obsolete("Use SettingsKey instead. Will be removed in V18.")] public Udi? SettingsUdi { get; } + public Guid ContentKey { get; set; } + + public Guid? SettingsKey { get; set; } + public static bool operator ==(ContentAndSettingsReference left, ContentAndSettingsReference right) => left.Equals(right); diff --git a/src/Umbraco.Core/Models/Blocks/IBlockLayoutItem.cs b/src/Umbraco.Core/Models/Blocks/IBlockLayoutItem.cs index eb5d3b0553ae..3974bfc1a0b2 100644 --- a/src/Umbraco.Core/Models/Blocks/IBlockLayoutItem.cs +++ b/src/Umbraco.Core/Models/Blocks/IBlockLayoutItem.cs @@ -5,7 +5,13 @@ namespace Umbraco.Cms.Core.Models.Blocks; public interface IBlockLayoutItem { + [Obsolete("Use ContentKey instead. Will be removed in V18.")] public Udi? ContentUdi { get; set; } + [Obsolete("Use SettingsKey instead. Will be removed in V18.")] public Udi? SettingsUdi { get; set; } + + public Guid ContentKey { get; set; } + + public Guid? SettingsKey { get; set; } } diff --git a/src/Umbraco.Core/Models/Blocks/IBlockReference.cs b/src/Umbraco.Core/Models/Blocks/IBlockReference.cs index 647d1f5b2fe9..2505183efa6d 100644 --- a/src/Umbraco.Core/Models/Blocks/IBlockReference.cs +++ b/src/Umbraco.Core/Models/Blocks/IBlockReference.cs @@ -37,7 +37,7 @@ public interface IBlockReference : IBlockReference /// /// The settings. /// - TSettings Settings { get; } + TSettings? Settings { get; } } diff --git a/src/Umbraco.Core/Models/Blocks/RichTextBlockItem.cs b/src/Umbraco.Core/Models/Blocks/RichTextBlockItem.cs index f5be6f9e2312..17abbe1d90c8 100644 --- a/src/Umbraco.Core/Models/Blocks/RichTextBlockItem.cs +++ b/src/Umbraco.Core/Models/Blocks/RichTextBlockItem.cs @@ -25,21 +25,38 @@ public class RichTextBlockItem : IBlockReference + [Obsolete("Use constructor that accepts GUIDs instead. Will be removed in V18.")] public RichTextBlockItem(Udi contentUdi, IPublishedElement content, Udi settingsUdi, IPublishedElement settings) + : this( + (contentUdi as GuidUdi)?.Guid ?? throw new ArgumentException(nameof(contentUdi)), + content, + (settingsUdi as GuidUdi)?.Guid, + settings) { - ContentUdi = contentUdi ?? throw new ArgumentNullException(nameof(contentUdi)); + } + + public RichTextBlockItem(Guid contentKey, IPublishedElement content, Guid? settingsKey, IPublishedElement? settings) + { + ContentKey = contentKey; + ContentUdi = new GuidUdi(Constants.UdiEntityType.Element, contentKey); Content = content ?? throw new ArgumentNullException(nameof(content)); - SettingsUdi = settingsUdi; + SettingsKey = settingsKey; + SettingsUdi = settingsKey.HasValue + ? new GuidUdi(Constants.UdiEntityType.Element, settingsKey.Value) + : null; Settings = settings; } + public Guid ContentKey { get; set; } + + public Guid? SettingsKey { get; set; } + /// /// Gets the content. /// /// /// The content. /// - [DataMember(Name = "content")] public IPublishedElement Content { get; } /// @@ -48,8 +65,8 @@ public RichTextBlockItem(Udi contentUdi, IPublishedElement content, Udi settings /// /// The settings UDI. /// - [DataMember(Name = "settingsUdi")] - public Udi SettingsUdi { get; } + [Obsolete("Use SettingsKey instead. Will be removed in V18.")] + public Udi? SettingsUdi { get; } /// /// Gets the content UDI. @@ -57,7 +74,7 @@ public RichTextBlockItem(Udi contentUdi, IPublishedElement content, Udi settings /// /// The content UDI. /// - [DataMember(Name = "contentUdi")] + [Obsolete("Use ContentKey instead. Will be removed in V18.")] public Udi ContentUdi { get; } /// @@ -66,8 +83,7 @@ public RichTextBlockItem(Udi contentUdi, IPublishedElement content, Udi settings /// /// The settings. /// - [DataMember(Name = "settings")] - public IPublishedElement Settings { get; } + public IPublishedElement? Settings { get; } } /// @@ -85,10 +101,15 @@ public class RichTextBlockItem : RichTextBlockItem /// The content. /// The settings UDI. /// The settings. + [Obsolete("Use constructor that accepts GUIDs instead. Will be removed in V18.")] public RichTextBlockItem(Udi contentUdi, T content, Udi settingsUdi, IPublishedElement settings) : base(contentUdi, content, settingsUdi, settings) => Content = content; + public RichTextBlockItem(Guid contentKey, T content, Guid? settingsKey, IPublishedElement? settings) + : base(contentKey, content, settingsKey, settings) => + Content = content; + /// /// Gets the content. /// @@ -115,15 +136,20 @@ public class RichTextBlockItem : RichTextBlockItemThe content. /// The settings udi. /// The settings. + [Obsolete("Use constructor that accepts GUIDs instead. Will be removed in V18.")] public RichTextBlockItem(Udi contentUdi, TContent content, Udi settingsUdi, TSettings settings) : base(contentUdi, content, settingsUdi, settings) => Settings = settings; + public RichTextBlockItem(Guid contentKey, TContent content, Guid? settingsKey, TSettings? settings) + : base(contentKey, content, settingsKey, settings) => + Settings = settings; + /// /// Gets the settings. /// /// /// The settings. /// - public new TSettings Settings { get; } + public new TSettings? Settings { get; } } diff --git a/src/Umbraco.Core/Models/Blocks/RichTextBlockLayoutItem.cs b/src/Umbraco.Core/Models/Blocks/RichTextBlockLayoutItem.cs index 0cd721044393..57d69003a184 100644 --- a/src/Umbraco.Core/Models/Blocks/RichTextBlockLayoutItem.cs +++ b/src/Umbraco.Core/Models/Blocks/RichTextBlockLayoutItem.cs @@ -6,19 +6,30 @@ namespace Umbraco.Cms.Core.Models.Blocks; /// /// Used for deserializing the rich text block layouts /// -public class RichTextBlockLayoutItem : IBlockLayoutItem +public class RichTextBlockLayoutItem : BlockLayoutItemBase { - public Udi? ContentUdi { get; set; } - - public Udi? SettingsUdi { get; set; } - public RichTextBlockLayoutItem() { } + [Obsolete("Use constructor that accepts GUIDs instead. Will be removed in V18.")] public RichTextBlockLayoutItem(Udi contentUdi) - => ContentUdi = contentUdi; + : base(contentUdi) + { + } + [Obsolete("Use constructor that accepts GUIDs instead. Will be removed in V18.")] public RichTextBlockLayoutItem(Udi contentUdi, Udi settingsUdi) - : this(contentUdi) - => SettingsUdi = settingsUdi; + : base(contentUdi, settingsUdi) + { + } + + public RichTextBlockLayoutItem(Guid contentKey) + : base(contentKey) + { + } + + public RichTextBlockLayoutItem(Guid contentKey, Guid settingsKey) + : base(contentKey, settingsKey) + { + } } diff --git a/src/Umbraco.Core/Models/Blocks/RichTextBlockValue.cs b/src/Umbraco.Core/Models/Blocks/RichTextBlockValue.cs index 728c06152e05..efae15d0a117 100644 --- a/src/Umbraco.Core/Models/Blocks/RichTextBlockValue.cs +++ b/src/Umbraco.Core/Models/Blocks/RichTextBlockValue.cs @@ -1,3 +1,5 @@ +using System.Text.Json.Serialization; + namespace Umbraco.Cms.Core.Models.Blocks; /// @@ -19,5 +21,11 @@ public RichTextBlockValue(IEnumerable layouts) => Layout[PropertyEditorAlias] = layouts; /// - public override string PropertyEditorAlias => Constants.PropertyEditors.Aliases.TinyMce; + [JsonIgnore] + public override string PropertyEditorAlias => Constants.PropertyEditors.Aliases.RichText; + + // RTE block layouts uses "Umbraco.TinyMCE" in V14 and below, but should use "Umbraco.RichText" for V15+ + [Obsolete("Will be removed in V18.")] + public override bool SupportsBlockLayoutAlias(string alias) + => base.SupportsBlockLayoutAlias(alias) || alias.Equals(Constants.PropertyEditors.Aliases.TinyMce); } diff --git a/src/Umbraco.Core/Models/Blocks/RichTextEditorBlockDataConverter.cs b/src/Umbraco.Core/Models/Blocks/RichTextEditorBlockDataConverter.cs index 183dabe420c9..508b55202c03 100644 --- a/src/Umbraco.Core/Models/Blocks/RichTextEditorBlockDataConverter.cs +++ b/src/Umbraco.Core/Models/Blocks/RichTextEditorBlockDataConverter.cs @@ -7,17 +7,11 @@ namespace Umbraco.Cms.Core.Models.Blocks; /// 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(); + => layout.Select(x => new ContentAndSettingsReference(x.ContentKey, x.SettingsKey)).ToList(); } diff --git a/src/Umbraco.Core/Models/ContentRepositoryExtensions.cs b/src/Umbraco.Core/Models/ContentRepositoryExtensions.cs index 5d67a4a974bd..22bbfae7c749 100644 --- a/src/Umbraco.Core/Models/ContentRepositoryExtensions.cs +++ b/src/Umbraco.Core/Models/ContentRepositoryExtensions.cs @@ -1,4 +1,7 @@ +using Microsoft.Extensions.DependencyInjection; +using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.PropertyEditors; namespace Umbraco.Extensions; @@ -287,21 +290,22 @@ public static void SetCultureEdited(this IContent content, IEnumerable? } } + [Obsolete("Please use the overload that accepts all parameters. Will be removed in V16.")] + public static bool PublishCulture(this IContent content, CultureImpact? impact) + => PublishCulture(content, impact, DateTime.Now, StaticServiceProvider.Instance.GetRequiredService()); + /// /// Sets the publishing values for names and properties. /// /// /// + /// + /// /// /// A value indicating whether it was possible to publish the names and values for the specified /// culture(s). The method may fail if required names are not set, but it does NOT validate property data /// - /// - public static bool PublishCulture(this IContent content, CultureImpact? impact) - { - return PublishCulture(content, impact, DateTime.Now); - } - public static bool PublishCulture(this IContent content, CultureImpact? impact, DateTime publishTime) + public static bool PublishCulture(this IContent content, CultureImpact? impact, DateTime publishTime, PropertyEditorCollection propertyEditorCollection) { if (impact == null) { @@ -356,13 +360,13 @@ public static bool PublishCulture(this IContent content, CultureImpact? impact, foreach (IProperty property in content.Properties) { // for the specified culture (null or all or specific) - property.PublishValues(impact.Culture); + PublishPropertyValues(content, property, impact.Culture, propertyEditorCollection); // maybe the specified culture did not impact the invariant culture, so PublishValues // above would skip it, yet it *also* impacts invariant properties if (impact.ImpactsAlsoInvariantProperties && (property.PropertyType.VariesByCulture() is false || impact.ImpactsOnlyDefaultCulture)) { - property.PublishValues(null); + PublishPropertyValues(content, property, null, propertyEditorCollection); } } @@ -370,6 +374,22 @@ public static bool PublishCulture(this IContent content, CultureImpact? impact, return true; } + private static void PublishPropertyValues(IContent content, IProperty property, string? culture, PropertyEditorCollection propertyEditorCollection) + { + // if the content varies by culture, let data editor opt-in to perform partial property publishing (per culture) + if (content.ContentType.VariesByCulture() + && propertyEditorCollection.TryGet(property.PropertyType.PropertyEditorAlias, out IDataEditor? dataEditor) + && dataEditor.CanMergePartialPropertyValues(property.PropertyType)) + { + // perform partial publishing for the current culture + property.PublishPartialValues(dataEditor, culture); + return; + } + + // for the specified culture (null or all or specific) + property.PublishValues(culture); + } + /// /// Returns false if the culture is already unpublished /// diff --git a/src/Umbraco.Core/Models/IProperty.cs b/src/Umbraco.Core/Models/IProperty.cs index 54f1e8581fc4..d9a57e255844 100644 --- a/src/Umbraco.Core/Models/IProperty.cs +++ b/src/Umbraco.Core/Models/IProperty.cs @@ -1,4 +1,5 @@ using Umbraco.Cms.Core.Models.Entities; +using Umbraco.Cms.Core.PropertyEditors; namespace Umbraco.Cms.Core.Models; @@ -35,5 +36,7 @@ public interface IProperty : IEntity, IRememberBeingDirty void PublishValues(string? culture = "*", string segment = "*"); + void PublishPartialValues(IDataEditor dataEditor, string? culture); + void UnpublishValues(string? culture = "*", string segment = "*"); } diff --git a/src/Umbraco.Core/Models/Property.cs b/src/Umbraco.Core/Models/Property.cs index a4e8eb056c9a..b756b143ad1a 100644 --- a/src/Umbraco.Core/Models/Property.cs +++ b/src/Umbraco.Core/Models/Property.cs @@ -2,6 +2,7 @@ using System.Runtime.Serialization; using Umbraco.Cms.Core.Collections; using Umbraco.Cms.Core.Models.Entities; +using Umbraco.Cms.Core.PropertyEditors; using Umbraco.Extensions; namespace Umbraco.Cms.Core.Models; @@ -174,6 +175,21 @@ public static Property CreateWithValues(int id, IPropertyType propertyType, para : null; } + // internal - must be invoked by the content item + // does *not* validate the value - content item must validate first + public void PublishPartialValues(IDataEditor dataEditor, string? culture) + { + if (PropertyType.VariesByCulture()) + { + throw new NotSupportedException("Cannot publish merged culture values for culture variant properties"); + } + + culture = culture?.NullOrWhiteSpaceAsNull(); + + var value = dataEditor.MergePartialPropertyValueForCulture(_pvalue?.EditedValue, _pvalue?.PublishedValue, culture); + PublishValue(_pvalue, value); + } + // internal - must be invoked by the content item // does *not* validate the value - content item must validate first public void PublishValues(string? culture = "*", string? segment = "*") @@ -300,13 +316,23 @@ private void PublishValue(IPropertyValue? pvalue) return; } + PublishValue(pvalue, ConvertAssignedValue(pvalue.EditedValue)); + } + + private void PublishValue(IPropertyValue? pvalue, object? newPublishedValue) + { + if (pvalue == null) + { + return; + } + if (!PropertyType.SupportsPublishing) { throw new NotSupportedException("Property type does not support publishing."); } var origValue = pvalue.PublishedValue; - pvalue.PublishedValue = ConvertAssignedValue(pvalue.EditedValue); + pvalue.PublishedValue = newPublishedValue; DetectChanges(pvalue.EditedValue, origValue, nameof(Values), PropertyValueComparer, false); } diff --git a/src/Umbraco.Core/PropertyEditors/DataEditor.cs b/src/Umbraco.Core/PropertyEditors/DataEditor.cs index 304816289173..cb7a43ba78b4 100644 --- a/src/Umbraco.Core/PropertyEditors/DataEditor.cs +++ b/src/Umbraco.Core/PropertyEditors/DataEditor.cs @@ -196,4 +196,10 @@ protected virtual IConfigurationEditor CreateConfigurationEditor() /// Provides a summary of the PropertyEditor for use with the . /// protected virtual string DebuggerDisplay() => $"Alias: {Alias}"; + + /// + public virtual bool CanMergePartialPropertyValues(IPropertyType propertyType) => false; + + /// + public virtual object? MergePartialPropertyValueForCulture(object? sourceValue, object? targetValue, string? culture) => sourceValue; } diff --git a/src/Umbraco.Core/PropertyEditors/IDataEditor.cs b/src/Umbraco.Core/PropertyEditors/IDataEditor.cs index 5a95445fcecf..a9d3b896e7c7 100644 --- a/src/Umbraco.Core/PropertyEditors/IDataEditor.cs +++ b/src/Umbraco.Core/PropertyEditors/IDataEditor.cs @@ -51,4 +51,18 @@ public interface IDataEditor : IDiscoverable /// Is expected to throw if the editor does not support being configured, e.g. for most parameter editors. /// IConfigurationEditor GetConfigurationEditor(); + + /// + /// Determines if the value editor needs to perform for a given property type. + /// + bool CanMergePartialPropertyValues(IPropertyType propertyType) => false; + + /// + /// Partially merges a source property value into a target property value for a given culture. + /// + /// The source property value. + /// The target property value. + /// The culture (or null for invariant). + /// The result of the merge operation. + object? MergePartialPropertyValueForCulture(object? sourceValue, object? targetValue, string? culture) => sourceValue; } diff --git a/src/Umbraco.Core/Services/ContentService.cs b/src/Umbraco.Core/Services/ContentService.cs index c667ea0b3591..d73528984a1e 100644 --- a/src/Umbraco.Core/Services/ContentService.cs +++ b/src/Umbraco.Core/Services/ContentService.cs @@ -10,6 +10,7 @@ using Umbraco.Cms.Core.Persistence; using Umbraco.Cms.Core.Persistence.Querying; using Umbraco.Cms.Core.Persistence.Repositories; +using Umbraco.Cms.Core.PropertyEditors; using Umbraco.Cms.Core.Scoping; using Umbraco.Cms.Core.Services.Changes; using Umbraco.Cms.Core.Services.Navigation; @@ -35,6 +36,7 @@ public class ContentService : RepositoryService, IContentService private readonly ICultureImpactFactory _cultureImpactFactory; private readonly IUserIdKeyResolver _userIdKeyResolver; private readonly IDocumentNavigationManagementService _documentNavigationManagementService; + private readonly PropertyEditorCollection _propertyEditorCollection; private IQuery? _queryNotTrashed; #region Constructors @@ -53,7 +55,8 @@ public ContentService( IShortStringHelper shortStringHelper, ICultureImpactFactory cultureImpactFactory, IUserIdKeyResolver userIdKeyResolver, - IDocumentNavigationManagementService documentNavigationManagementService) + IDocumentNavigationManagementService documentNavigationManagementService, + PropertyEditorCollection propertyEditorCollection) : base(provider, loggerFactory, eventMessagesFactory) { _documentRepository = documentRepository; @@ -67,6 +70,7 @@ public ContentService( _cultureImpactFactory = cultureImpactFactory; _userIdKeyResolver = userIdKeyResolver; _documentNavigationManagementService = documentNavigationManagementService; + _propertyEditorCollection = propertyEditorCollection; _logger = loggerFactory.CreateLogger(); } @@ -99,11 +103,12 @@ public ContentService( shortStringHelper, cultureImpactFactory, userIdKeyResolver, - StaticServiceProvider.Instance.GetRequiredService()) + StaticServiceProvider.Instance.GetRequiredService(), + StaticServiceProvider.Instance.GetRequiredService()) { } - [Obsolete("Use constructor that takes IUserIdKeyResolver as a parameter, scheduled for removal in V15")] + [Obsolete("Use non-obsolete constructor. Scheduled for removal in V16.")] public ContentService( ICoreScopeProvider provider, ILoggerFactory loggerFactory, @@ -131,7 +136,8 @@ public ContentService( shortStringHelper, cultureImpactFactory, StaticServiceProvider.Instance.GetRequiredService(), - StaticServiceProvider.Instance.GetRequiredService()) + StaticServiceProvider.Instance.GetRequiredService(), + StaticServiceProvider.Instance.GetRequiredService()) { } @@ -1244,7 +1250,7 @@ public PublishResult Publish(IContent content, string[] cultures, int userId = C var publishTime = DateTime.Now; foreach (CultureImpact? impact in impacts) { - content.PublishCulture(impact, publishTime); + content.PublishCulture(impact, publishTime, _propertyEditorCollection); } // Change state to publishing @@ -1881,7 +1887,7 @@ private void PerformScheduledPublishingRelease(DateTime date, List 0) { @@ -1965,12 +1971,12 @@ private bool PublishBranch_PublishCultures(IContent content, HashSet cul return culturesToPublish.All(culture => { CultureImpact? impact = _cultureImpactFactory.Create(culture, IsDefaultCulture(allLangs, culture), content); - return content.PublishCulture(impact, publishTime) && + return content.PublishCulture(impact, publishTime, _propertyEditorCollection) && _propertyValidationService.Value.IsPropertyDataValid(content, out _, impact); }); } - return content.PublishCulture(_cultureImpactFactory.ImpactInvariant(), publishTime) + return content.PublishCulture(_cultureImpactFactory.ImpactInvariant(), publishTime, _propertyEditorCollection) && _propertyValidationService.Value.IsPropertyDataValid(content, out _, _cultureImpactFactory.ImpactInvariant()); } @@ -3197,7 +3203,7 @@ private PublishResult StrategyCanPublish( // publish the culture(s) var publishTime = DateTime.Now; - if (!impactsToPublish.All(impact => content.PublishCulture(impact, publishTime))) + if (!impactsToPublish.All(impact => content.PublishCulture(impact, publishTime, _propertyEditorCollection))) { return new PublishResult(PublishResultType.FailedPublishContentInvalid, evtMsgs, content); } diff --git a/src/Umbraco.Infrastructure/DeliveryApi/ApiRichTextElementParser.cs b/src/Umbraco.Infrastructure/DeliveryApi/ApiRichTextElementParser.cs index 3860d3c32e80..ec2d71008721 100644 --- a/src/Umbraco.Infrastructure/DeliveryApi/ApiRichTextElementParser.cs +++ b/src/Umbraco.Infrastructure/DeliveryApi/ApiRichTextElementParser.cs @@ -166,18 +166,18 @@ private void ReplaceLocalImages(IPublishedSnapshot publishedSnapshot, string tag private void CleanUpBlocks(string tag, Dictionary attributes) { - if (tag.StartsWith("umb-rte-block") is false || attributes.ContainsKey("data-content-udi") is false || attributes["data-content-udi"] is not string dataUdi) + if (tag.StartsWith("umb-rte-block") is false || attributes.ContainsKey(BlockContentKeyAttribute) is false || attributes[BlockContentKeyAttribute] is not string dataKey) { return; } - if (UdiParser.TryParse(dataUdi, out GuidUdi? guidUdi) is false) + if (Guid.TryParse(dataKey, out Guid key) is false) { return; } - attributes["content-id"] = guidUdi.Guid; - attributes.Remove("data-content-udi"); + attributes["content-id"] = key; + attributes.Remove(BlockContentKeyAttribute); } private static void SanitizeAttributes(Dictionary attributes) diff --git a/src/Umbraco.Infrastructure/DeliveryApi/ApiRichTextMarkupParser.cs b/src/Umbraco.Infrastructure/DeliveryApi/ApiRichTextMarkupParser.cs index c6a4a3c956e9..fcb55258d065 100644 --- a/src/Umbraco.Infrastructure/DeliveryApi/ApiRichTextMarkupParser.cs +++ b/src/Umbraco.Infrastructure/DeliveryApi/ApiRichTextMarkupParser.cs @@ -109,15 +109,15 @@ private void CleanUpBlocks(HtmlDocument doc) HtmlNode[] blocks = doc.DocumentNode.SelectNodes("//*[starts-with(local-name(),'umb-rte-block')]")?.ToArray() ?? Array.Empty(); foreach (HtmlNode block in blocks) { - var dataUdi = block.GetAttributeValue("data-content-udi", string.Empty); - if (UdiParser.TryParse(dataUdi, out GuidUdi? guidUdi) is false) + var dataKey = block.GetAttributeValue(BlockContentKeyAttribute, string.Empty); + if (Guid.TryParse(dataKey, out Guid key) is false) { continue; } // swap the content UDI for the content ID - block.Attributes.Remove("data-content-udi"); - block.SetAttributeValue("data-content-id", guidUdi.Guid.ToString("D")); + block.Attributes.Remove(BlockContentKeyAttribute); + block.SetAttributeValue("data-content-id", key.ToString("D")); // remove the inner comment placed by the RTE block.RemoveAllChildren(); diff --git a/src/Umbraco.Infrastructure/DeliveryApi/ApiRichTextParserBase.cs b/src/Umbraco.Infrastructure/DeliveryApi/ApiRichTextParserBase.cs index 407bc1a022f0..dd88453fabdc 100644 --- a/src/Umbraco.Infrastructure/DeliveryApi/ApiRichTextParserBase.cs +++ b/src/Umbraco.Infrastructure/DeliveryApi/ApiRichTextParserBase.cs @@ -13,6 +13,8 @@ internal abstract partial class ApiRichTextParserBase private readonly IApiContentRouteBuilder _apiContentRouteBuilder; private readonly IApiMediaUrlProvider _apiMediaUrlProvider; + protected const string BlockContentKeyAttribute = "data-content-key"; + protected ApiRichTextParserBase(IApiContentRouteBuilder apiContentRouteBuilder, IApiMediaUrlProvider apiMediaUrlProvider) { _apiContentRouteBuilder = apiContentRouteBuilder; diff --git a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs index 6d99898cd600..cdb00380b994 100644 --- a/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs +++ b/src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs @@ -162,6 +162,7 @@ public static IUmbracoBuilder AddCoreInitialServices(this IUmbracoBuilder builde builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); + builder.Services.AddSingleton(); // both SimpleTinyMceValueConverter (in Core) and RteBlockRenderingValueConverter (in Infrastructure) will be // discovered when CoreBootManager configures the converters. We will remove the basic one defined diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs index 649190f5f02f..459af0304e4b 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/UmbracoPlan.cs @@ -99,5 +99,8 @@ protected virtual void DefinePlan() // To 15.0.0 To("{7F4F31D8-DD71-4F0D-93FC-2690A924D84B}"); To("{1A8835EF-F8AB-4472-B4D8-D75B7C164022}"); + To("{6C04B137-0097-4938-8C6A-276DF1A0ECA8}"); + To("{9D3CE7D4-4884-41D4-98E8-302EB6CB0CF6}"); + To("{37875E80-5CDD-42FF-A21A-7D4E3E23E0ED}"); } } diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_15_0_0/ConvertBlockEditorPropertiesBase.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_15_0_0/ConvertBlockEditorPropertiesBase.cs new file mode 100644 index 000000000000..f60c7f049f77 --- /dev/null +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_15_0_0/ConvertBlockEditorPropertiesBase.cs @@ -0,0 +1,281 @@ +using System.Collections.Concurrent; +using Microsoft.Extensions.Logging; +using NPoco; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.Blocks; +using Umbraco.Cms.Core.Models.Editors; +using Umbraco.Cms.Core.Serialization; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Web; +using Umbraco.Cms.Infrastructure.Persistence; +using Umbraco.Cms.Infrastructure.Persistence.Dtos; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_15_0_0; + +[Obsolete("Will be removed in V18")] +public abstract class ConvertBlockEditorPropertiesBase : MigrationBase +{ + private readonly ILogger _logger; + private readonly IContentTypeService _contentTypeService; + private readonly IDataTypeService _dataTypeService; + private readonly IJsonSerializer _jsonSerializer; + private readonly IUmbracoContextFactory _umbracoContextFactory; + private readonly ILanguageService _languageService; + + protected abstract IEnumerable PropertyEditorAliases { get; } + + protected abstract EditorValueHandling DetermineEditorValueHandling(object editorValue); + + protected bool SkipMigration { get; init; } + + protected enum EditorValueHandling + { + IgnoreConversion, + ProceedConversion, + HandleAsError + } + + public ConvertBlockEditorPropertiesBase( + IMigrationContext context, + ILogger logger, + IContentTypeService contentTypeService, + IDataTypeService dataTypeService, + IJsonSerializer jsonSerializer, + IUmbracoContextFactory umbracoContextFactory, + ILanguageService languageService) + : base(context) + { + _logger = logger; + _contentTypeService = contentTypeService; + _dataTypeService = dataTypeService; + _jsonSerializer = jsonSerializer; + _umbracoContextFactory = umbracoContextFactory; + _languageService = languageService; + } + + protected override void Migrate() + { + if (SkipMigration) + { + _logger.LogInformation("Migration was skipped due to configuration."); + return; + } + + using UmbracoContextReference umbracoContextReference = _umbracoContextFactory.EnsureUmbracoContext(); + var languagesById = _languageService.GetAllAsync().GetAwaiter().GetResult().ToDictionary(language => language.Id); + IContentType[] allContentTypes = _contentTypeService.GetAll().ToArray(); + var allPropertyTypesByEditor = allContentTypes + .SelectMany(ct => ct.PropertyTypes) + .GroupBy(pt => pt.PropertyEditorAlias) + .ToDictionary(group => group.Key, group => group.ToArray()); + + foreach (var propertyEditorAlias in PropertyEditorAliases) + { + if (allPropertyTypesByEditor.TryGetValue(propertyEditorAlias, out IPropertyType[]? propertyTypes) is false) + { + continue; + } + + _logger.LogInformation("Migration starting for all properties of type: {propertyEditorAlias}", propertyEditorAlias); + if (Handle(propertyTypes, languagesById)) + { + _logger.LogInformation("Migration succeeded for all properties of type: {propertyEditorAlias}", propertyEditorAlias); + } + else + { + _logger.LogError("Migration failed for one or more properties of type: {propertyEditorAlias}", propertyEditorAlias); + } + } + } + + protected virtual object UpdateEditorValue(object editorValue) => editorValue; + + protected virtual string UpdateDatabaseValue(string dbValue) => dbValue; + + private bool Handle(IPropertyType[] propertyTypes, IDictionary languagesById) + { + var success = true; + + foreach (IPropertyType propertyType in propertyTypes) + { + try + { + _logger.LogInformation("- starting property type: {propertyTypeName} (id: {propertyTypeId}, alias: {propertyTypeAlias})...", propertyType.Name, propertyType.Id, propertyType.Alias); + IDataType dataType = _dataTypeService.GetAsync(propertyType.DataTypeKey).GetAwaiter().GetResult() + ?? throw new InvalidOperationException("The data type could not be fetched."); + + if (IsCandidateForMigration(propertyType, dataType) is false) + { + _logger.LogInformation(" - skipped property type migration because it was not a applicable."); + continue; + } + + IDataValueEditor valueEditor = dataType.Editor?.GetValueEditor() + ?? throw new InvalidOperationException("The data type value editor could not be fetched."); + + Sql sql = Sql() + .Select() + .From() + .Where(dto => dto.PropertyTypeId == propertyType.Id); + List propertyDataDtos = Database.Fetch(sql); + if (propertyDataDtos.Any() is false) + { + continue; + } + + var updateBatch = propertyDataDtos.Select(propertyDataDto => + UpdateBatch.For(propertyDataDto, Database.StartSnapshot(propertyDataDto))).ToList(); + + var updatesToSkip = new ConcurrentBag>(); + + var progress = 0; + + ExecutionContext.SuppressFlow(); + Parallel.ForEach(updateBatch, update => + { + using UmbracoContextReference umbracoContextReference = _umbracoContextFactory.EnsureUmbracoContext(); + + progress++; + if (progress % 100 == 0) + { + _logger.LogInformation(" - finíshed {progress} of {total} properties", progress, updateBatch.Count); + } + + PropertyDataDto propertyDataDto = update.Poco; + + // NOTE: some old property data DTOs can have variance defined, even if the property type no longer varies + var culture = propertyType.VariesByCulture() + && propertyDataDto.LanguageId.HasValue + && languagesById.TryGetValue(propertyDataDto.LanguageId.Value, out ILanguage? language) + ? language.IsoCode + : null; + + if (culture is null && propertyType.VariesByCulture()) + { + // if we end up here, the property DTO is bound to a language that no longer exists. this is an error scenario, + // and we can't really handle it in any other way than logging; in all likelihood this is an old property version, + // and it won't cause any runtime issues + _logger.LogWarning( + " - property data with id: {propertyDataId} references a language that does not exist - language id: {languageId} (property type: {propertyTypeName}, id: {propertyTypeId}, alias: {propertyTypeAlias})", + propertyDataDto.Id, + propertyDataDto.LanguageId, + propertyType.Name, + propertyType.Id, + propertyType.Alias); + return; + } + + var segment = propertyType.VariesBySegment() ? propertyDataDto.Segment : null; + var property = new Property(propertyType); + property.SetValue(propertyDataDto.Value, culture, segment); + var toEditorValue = valueEditor.ToEditor(property, culture, segment); + switch (toEditorValue) + { + case null: + _logger.LogWarning( + " - value editor yielded a null value for property data with id: {propertyDataId} (property type: {propertyTypeName}, id: {propertyTypeId}, alias: {propertyTypeAlias})", + propertyDataDto.Id, + propertyType.Name, + propertyType.Id, + propertyType.Alias); + updatesToSkip.Add(update); + return; + + case string str when str.IsNullOrWhiteSpace(): + // indicates either an empty block editor or corrupt block editor data - we can't do anything about either here + updatesToSkip.Add(update); + return; + + default: + switch (DetermineEditorValueHandling(toEditorValue)) + { + case EditorValueHandling.IgnoreConversion: + // nothing to convert, continue + updatesToSkip.Add(update); + return; + case EditorValueHandling.ProceedConversion: + // continue the conversion + break; + case EditorValueHandling.HandleAsError: + _logger.LogError( + " - value editor did not yield a valid ToEditor value for property data with id: {propertyDataId} - the value type was {valueType} (property type: {propertyTypeName}, id: {propertyTypeId}, alias: {propertyTypeAlias})", + propertyDataDto.Id, + toEditorValue.GetType(), + propertyType.Name, + propertyType.Id, + propertyType.Alias); + updatesToSkip.Add(update); + return; + default: + throw new ArgumentOutOfRangeException(); + } + break; + } + + toEditorValue = UpdateEditorValue(toEditorValue); + + var editorValue = _jsonSerializer.Serialize(toEditorValue); + var dbValue = valueEditor.FromEditor(new ContentPropertyData(editorValue, null), null); + if (dbValue is not string stringValue || stringValue.DetectIsJson() is false) + { + _logger.LogError( + " - value editor did not yield a valid JSON string as FromEditor value property data with id: {propertyDataId} (property type: {propertyTypeName}, id: {propertyTypeId}, alias: {propertyTypeAlias})", + propertyDataDto.Id, + propertyType.Name, + propertyType.Id, + propertyType.Alias); + updatesToSkip.Add(update); + return; + } + + stringValue = UpdateDatabaseValue(stringValue); + + propertyDataDto.TextValue = stringValue; + }); + ExecutionContext.RestoreFlow(); + + updateBatch.RemoveAll(updatesToSkip.Contains); + + if (updateBatch.Any() is false) + { + _logger.LogInformation(" - no properties to convert, continuing"); + continue; + } + + _logger.LogInformation(" - {totalConverted} properties converted, saving...", updateBatch.Count); + var result = Database.UpdateBatch(updateBatch, new BatchOptions { BatchSize = 100 }); + if (result != updateBatch.Count) + { + throw new InvalidOperationException($"The database batch update was supposed to update {updateBatch.Count} property DTO entries, but it updated {result} entries."); + } + + _logger.LogDebug( + "Migration completed for property type: {propertyTypeName} (id: {propertyTypeId}, alias: {propertyTypeAlias}, editor alias: {propertyTypeEditorAlias}) - {updateCount} property DTO entries updated.", + propertyType.Name, + propertyType.Id, + propertyType.Alias, + propertyType.PropertyEditorAlias, + result); + } + catch (Exception ex) + { + _logger.LogError( + ex, + "Migration failed for property type: {propertyTypeName} (id: {propertyTypeId}, alias: {propertyTypeAlias}, editor alias: {propertyTypeEditorAlias})", + propertyType.Name, + propertyType.Id, + propertyType.Alias, + propertyType.PropertyEditorAlias); + + success = false; + } + } + + return success; + } + + protected virtual bool IsCandidateForMigration(IPropertyType propertyType, IDataType dataType) + => true; +} diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_15_0_0/ConvertBlockEditorPropertiesOptions.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_15_0_0/ConvertBlockEditorPropertiesOptions.cs new file mode 100644 index 000000000000..348b8e8e2147 --- /dev/null +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_15_0_0/ConvertBlockEditorPropertiesOptions.cs @@ -0,0 +1,29 @@ +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_15_0_0; + +[Obsolete("Will be removed in V18")] +public class ConvertBlockEditorPropertiesOptions +{ + /// + /// Setting this property to true will cause the migration of Block List editors to be skipped. + /// + /// + /// If you choose to skip the migration, you're responsible for performing the content migration for Block Lists after the V15 upgrade has completed. + /// + public bool SkipBlockListEditors { get; set; } = false; + + /// + /// Setting this property to true will cause the migration of Block Grid editors to be skipped. + /// + /// + /// If you choose to skip the migration, you're responsible for performing the content migration for Block Grids after the V15 upgrade has completed. + /// + public bool SkipBlockGridEditors { get; set; } = false; + + /// + /// Setting this property to true will cause the migration of Rich Text editors to be skipped. + /// + /// + /// If you choose to skip the migration, you're responsible for performing the content migration for Rich Texts after the V15 upgrade has completed. + /// + public bool SkipRichTextEditors { get; set; } = false; +} diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_15_0_0/ConvertBlockGridEditorProperties.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_15_0_0/ConvertBlockGridEditorProperties.cs new file mode 100644 index 000000000000..cc80a77ea9c0 --- /dev/null +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_15_0_0/ConvertBlockGridEditorProperties.cs @@ -0,0 +1,47 @@ +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Models.Blocks; +using Umbraco.Cms.Core.Serialization; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Web; + +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_15_0_0; + +[Obsolete("Will be removed in V18")] +public class ConvertBlockGridEditorProperties : ConvertBlockEditorPropertiesBase +{ + public ConvertBlockGridEditorProperties( + IMigrationContext context, + ILogger logger, + IContentTypeService contentTypeService, + IDataTypeService dataTypeService, + IJsonSerializer jsonSerializer, + IUmbracoContextFactory umbracoContextFactory, + ILanguageService languageService, + IOptions options) + : base(context, logger, contentTypeService, dataTypeService, jsonSerializer, umbracoContextFactory, languageService) + => SkipMigration = options.Value.SkipBlockGridEditors; + + protected override IEnumerable PropertyEditorAliases + => new[] { Constants.PropertyEditors.Aliases.BlockGrid }; + + protected override EditorValueHandling DetermineEditorValueHandling(object editorValue) + => editorValue is BlockValue blockValue + ? blockValue.ContentData.Any() + ? EditorValueHandling.ProceedConversion + : EditorValueHandling.IgnoreConversion + : EditorValueHandling.HandleAsError; + + public ConvertBlockGridEditorProperties( + IMigrationContext context, + ILogger logger, + IContentTypeService contentTypeService, + IDataTypeService dataTypeService, + IJsonSerializer jsonSerializer, + IUmbracoContextFactory umbracoContextFactory, + ILanguageService languageService) + : base(context, logger, contentTypeService, dataTypeService, jsonSerializer, umbracoContextFactory, languageService) + { + } +} diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_15_0_0/ConvertBlockListEditorProperties.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_15_0_0/ConvertBlockListEditorProperties.cs new file mode 100644 index 000000000000..e920a3b6d852 --- /dev/null +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_15_0_0/ConvertBlockListEditorProperties.cs @@ -0,0 +1,47 @@ +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Models.Blocks; +using Umbraco.Cms.Core.Serialization; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Web; + +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_15_0_0; + +[Obsolete("Will be removed in V18")] +public class ConvertBlockListEditorProperties : ConvertBlockEditorPropertiesBase +{ + public ConvertBlockListEditorProperties( + IMigrationContext context, + ILogger logger, + IContentTypeService contentTypeService, + IDataTypeService dataTypeService, + IJsonSerializer jsonSerializer, + IUmbracoContextFactory umbracoContextFactory, + ILanguageService languageService, + IOptions options) + : base(context, logger, contentTypeService, dataTypeService, jsonSerializer, umbracoContextFactory, languageService) + => SkipMigration = options.Value.SkipBlockListEditors; + + protected override IEnumerable PropertyEditorAliases + => new[] { Constants.PropertyEditors.Aliases.BlockList }; + + protected override EditorValueHandling DetermineEditorValueHandling(object editorValue) + => editorValue is BlockValue blockValue + ? blockValue.ContentData.Any() + ? EditorValueHandling.ProceedConversion + : EditorValueHandling.IgnoreConversion + : EditorValueHandling.HandleAsError; + + public ConvertBlockListEditorProperties( + IMigrationContext context, + ILogger logger, + IContentTypeService contentTypeService, + IDataTypeService dataTypeService, + IJsonSerializer jsonSerializer, + IUmbracoContextFactory umbracoContextFactory, + ILanguageService languageService) + : base(context, logger, contentTypeService, dataTypeService, jsonSerializer, umbracoContextFactory, languageService) + { + } +} diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_15_0_0/ConvertRichTextEditorProperties.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_15_0_0/ConvertRichTextEditorProperties.cs new file mode 100644 index 000000000000..6023917b4cd1 --- /dev/null +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_15_0_0/ConvertRichTextEditorProperties.cs @@ -0,0 +1,74 @@ +using System.Text.RegularExpressions; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.PropertyEditors; +using Umbraco.Cms.Core.Serialization; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Web; + +namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_15_0_0; + +[Obsolete("Will be removed in V18")] +public partial class ConvertRichTextEditorProperties : ConvertBlockEditorPropertiesBase +{ + public ConvertRichTextEditorProperties( + IMigrationContext context, + ILogger logger, + IContentTypeService contentTypeService, + IDataTypeService dataTypeService, + IJsonSerializer jsonSerializer, + IUmbracoContextFactory umbracoContextFactory, + ILanguageService languageService, + IOptions options) + : base(context, logger, contentTypeService, dataTypeService, jsonSerializer, umbracoContextFactory, languageService) + => SkipMigration = options.Value.SkipRichTextEditors; + + protected override IEnumerable PropertyEditorAliases + => new[] { Constants.PropertyEditors.Aliases.TinyMce, Constants.PropertyEditors.Aliases.RichText }; + + protected override EditorValueHandling DetermineEditorValueHandling(object editorValue) + => editorValue is RichTextEditorValue richTextEditorValue + ? richTextEditorValue.Blocks?.ContentData.Any() is true + ? EditorValueHandling.ProceedConversion + : EditorValueHandling.IgnoreConversion + : EditorValueHandling.HandleAsError; + + protected override object UpdateEditorValue(object editorValue) + { + if (editorValue is not RichTextEditorValue richTextEditorValue) + { + return base.UpdateEditorValue(editorValue); + } + + richTextEditorValue.Markup = BlockRegex().Replace( + richTextEditorValue.Markup, + match => UdiParser.TryParse(match.Groups["udi"].Value, out GuidUdi? guidUdi) + ? match.Value + .Replace(match.Groups["attribute"].Value, "data-content-key") + .Replace(match.Groups["udi"].Value, guidUdi.Guid.ToString("D")) + : string.Empty); + + return richTextEditorValue; + } + + public ConvertRichTextEditorProperties( + IMigrationContext context, + ILogger logger, + IContentTypeService contentTypeService, + IDataTypeService dataTypeService, + IJsonSerializer jsonSerializer, + IUmbracoContextFactory umbracoContextFactory, + ILanguageService languageService) + : base(context, logger, contentTypeService, dataTypeService, jsonSerializer, umbracoContextFactory, languageService) + { + } + + protected override bool IsCandidateForMigration(IPropertyType propertyType, IDataType dataType) + => dataType.ConfigurationObject is RichTextConfiguration richTextConfiguration + && richTextConfiguration.Blocks?.Any() is true; + + [GeneratedRegex("data-content-udi)=\"(?.[^\"]*)\".*<\\/umb-rte-block")] + private static partial Regex BlockRegex(); +} diff --git a/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorPropertyNotificationHandlerBase.cs b/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorPropertyNotificationHandlerBase.cs index a2f5a3564ea5..bb1bf3bc642e 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorPropertyNotificationHandlerBase.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorPropertyNotificationHandlerBase.cs @@ -11,26 +11,26 @@ public abstract class BlockEditorPropertyNotificationHandlerBase> _logger; - private readonly List _udisToReplace = new List(); + private readonly List _keysToReplace = new List(); protected BlockEditorPropertyNotificationHandlerBase(ILogger> logger) => _logger = logger; protected override string FormatPropertyValue(string rawJson, bool onlyMissingKeys) { - // the block editor doesn't ever have missing UDIs so when this is true there's nothing to process + // the block editor doesn't ever have missing keys so when this is true there's nothing to process if (onlyMissingKeys) { return rawJson; } - return ReplaceBlockEditorUdis(rawJson); + return ReplaceBlockEditorKeys(rawJson); } // internal for tests - // the purpose of this method is to replace the content and settings UDIs throughout the JSON structure of a block editor value. - // the challenge is nested block editor values, which must also have their UDIs replaced. this becomes particularly tricky because - // other nested property values could also contain UDIs, which should *not* be replaced (i.e. a content picker value). - internal string ReplaceBlockEditorUdis(string rawJson, Func? createGuid = null) + // the purpose of this method is to replace the content and settings keys throughout the JSON structure of a block editor value. + // the challenge is nested block editor values, which must also have their keys replaced. this becomes particularly tricky because + // other nested property values could also contain keys, which should *not* be replaced (i.e. a content picker value). + internal string ReplaceBlockEditorKeys(string rawJson, Func? createGuid = null) { // used so we can test nicely createGuid ??= _ => Guid.NewGuid(); @@ -52,21 +52,21 @@ internal string ReplaceBlockEditorUdis(string rawJson, Func? createG rawJson = Regex.Replace( rawJson, - @"(umb:\/\/\w*\/)(\w*)", + @"\w{8}-\w{4}-\w{4}-\w{4}-\w{12}", match => { - if (_udisToReplace.Contains(match.Value) == false) + if (_keysToReplace.Contains(match.Value) is false) { return match.Value; } - var oldKey = Guid.Parse(match.Groups[2].Value); + var oldKey = Guid.Parse(match.Value); if (oldToNewKeys.ContainsKey(oldKey) == false) { oldToNewKeys[oldKey] = createGuid(oldKey); } - return $"{match.Groups[1]}{oldToNewKeys[oldKey].ToString("N")}"; + return match.Value.Replace(match.Value, oldToNewKeys[oldKey].ToString("D")); }); return rawJson; @@ -109,7 +109,7 @@ private void TraverseObject(JsonObject obj) // we'll assume that the object is a data representation of a block based editor if it contains "contentData" and "settingsData". if (obj["contentData"] is JsonArray contentData && obj["settingsData"] is JsonArray settingsData) { - ParseUdis(contentData, settingsData); + ParseKeys(contentData, settingsData); return; } @@ -119,28 +119,28 @@ private void TraverseObject(JsonObject obj) } } - private void ParseUdis(JsonArray contentData, JsonArray settingsData) + private void ParseKeys(JsonArray contentData, JsonArray settingsData) { - // grab all UDIs from the objects of contentData and settingsData - var udis = contentData.Select(c => c?["udi"]) - .Union(settingsData.Select(s => s?["udi"])) - .Select(udiToken => udiToken?.GetValue().NullOrWhiteSpaceAsNull()) + // grab all keys from the objects of contentData and settingsData + var keys = contentData.Select(c => c?["key"]) + .Union(settingsData.Select(s => s?["key"])) + .Select(keyToken => keyToken?.GetValue().NullOrWhiteSpaceAsNull()) .ToArray(); // the following is solely for avoiding functionality wise breakage. we should consider removing it eventually, but for the time being it's harmless. - foreach (var udiToReplace in udis) + foreach (var keyToReplace in keys) { - if (UdiParser.TryParse(udiToReplace ?? string.Empty, out Udi? udi) == false || udi is not GuidUdi) + if (Guid.TryParse(keyToReplace ?? string.Empty, out Guid _) is false) { - throw new FormatException($"Could not parse a valid {nameof(GuidUdi)} from the string: \"{udiToReplace}\""); + throw new FormatException($"Could not parse a valid {nameof(Guid)} from the string: \"{keyToReplace}\""); } } - _udisToReplace.AddRange(udis.WhereNotNull()); + _keysToReplace.AddRange(keys.WhereNotNull()); foreach (JsonObject item in contentData.Union(settingsData).WhereNotNull().OfType()) { - foreach (JsonNode property in item.Where(p => p.Key != "contentTypeKey" && p.Key != "udi").Select(p => p.Value).WhereNotNull()) + foreach (JsonNode property in item.Where(p => p.Key != "contentTypeKey" && p.Key != "key").Select(p => p.Value).WhereNotNull()) { TraverseProperty(property); } diff --git a/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorPropertyValueEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorPropertyValueEditor.cs index 98c394d584eb..97fcd17aa980 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorPropertyValueEditor.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorPropertyValueEditor.cs @@ -7,6 +7,7 @@ using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.Blocks; using Umbraco.Cms.Core.Models.Editors; +using Umbraco.Cms.Core.PropertyEditors.ValueConverters; using Umbraco.Cms.Core.Serialization; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Strings; @@ -18,10 +19,6 @@ public abstract class BlockEditorPropertyValueEditor : BlockVal where TLayout : class, IBlockLayoutItem, new() { private readonly IJsonSerializer _jsonSerializer; - private BlockEditorValues? _blockEditorValues; - private readonly IDataTypeConfigurationCache _dataTypeConfigurationCache; - private readonly PropertyEditorCollection _propertyEditors; - private readonly DataValueReferenceFactoryCollection _dataValueReferenceFactories; protected BlockEditorPropertyValueEditor( DataEditorAttribute attribute, @@ -32,79 +29,35 @@ protected BlockEditorPropertyValueEditor( ILogger> logger, IShortStringHelper shortStringHelper, IJsonSerializer jsonSerializer, - IIOHelper ioHelper) - : base(attribute, propertyEditors, dataTypeConfigurationCache, textService, logger, shortStringHelper, jsonSerializer, ioHelper, dataValueReferenceFactories) - { - _propertyEditors = propertyEditors; - _dataValueReferenceFactories = dataValueReferenceFactories; - _dataTypeConfigurationCache = dataTypeConfigurationCache; + IIOHelper ioHelper, + BlockEditorVarianceHandler blockEditorVarianceHandler) + : base(attribute, propertyEditors, dataTypeConfigurationCache, textService, logger, shortStringHelper, jsonSerializer, ioHelper, dataValueReferenceFactories, blockEditorVarianceHandler) => _jsonSerializer = jsonSerializer; - } - - protected BlockEditorValues BlockEditorValues - { - get => _blockEditorValues ?? throw new NullReferenceException($"The property {nameof(BlockEditorValues)} must be initialized at value editor construction"); - set => _blockEditorValues = value; - } /// public override IEnumerable GetReferences(object? value) { - // Group by property editor alias to avoid duplicate lookups and optimize value parsing - foreach (var valuesByPropertyEditorAlias in GetAllPropertyValues(value).GroupBy(x => x.PropertyType.PropertyEditorAlias, x => x.Value)) - { - if (!_propertyEditors.TryGet(valuesByPropertyEditorAlias.Key, out IDataEditor? dataEditor)) - { - continue; - } - - // Use distinct values to avoid duplicate parsing of the same value - foreach (UmbracoEntityReference reference in _dataValueReferenceFactories.GetReferences(dataEditor, valuesByPropertyEditorAlias.Distinct())) - { - yield return reference; - } - } + TValue? blockValue = ParseBlockValue(value); + return blockValue is not null + ? GetBlockValueReferences(blockValue) + : Enumerable.Empty(); } /// public override IEnumerable GetTags(object? value, object? dataTypeConfiguration, int? languageId) { - foreach (BlockItemData.BlockPropertyValue propertyValue in GetAllPropertyValues(value)) - { - if (!_propertyEditors.TryGet(propertyValue.PropertyType.PropertyEditorAlias, out IDataEditor? dataEditor) || - dataEditor.GetValueEditor() is not IDataValueTags dataValueTags) - { - continue; - } - - object? configuration = _dataTypeConfigurationCache.GetConfiguration(propertyValue.PropertyType.DataTypeKey); - foreach (ITag tag in dataValueTags.GetTags(propertyValue.Value, configuration, languageId)) - { - yield return tag; - } - } + TValue? blockValue = ParseBlockValue(value); + return blockValue is not null + ? GetBlockValueTags(blockValue, languageId) + : Enumerable.Empty(); } - private IEnumerable GetAllPropertyValues(object? value) + private TValue? ParseBlockValue(object? value) { var rawJson = value == null ? string.Empty : value is string str ? str : value.ToString(); - - BlockEditorData? blockEditorData = BlockEditorValues.DeserializeAndClean(rawJson); - if (blockEditorData is null) - { - yield break; - } - - // Return all property values from the content and settings data - IEnumerable data = blockEditorData.BlockValue.ContentData.Concat(blockEditorData.BlockValue.SettingsData); - foreach (BlockItemData.BlockPropertyValue propertyValue in data.SelectMany(x => x.PropertyValues.Select(x => x.Value))) - { - yield return propertyValue; - } + return BlockEditorValues.DeserializeAndClean(rawJson)?.BlockValue; } - // note: there is NO variant support here - /// /// Ensure that sub-editor values are translated through their ToEditor methods /// @@ -132,7 +85,7 @@ public override object ToEditor(IProperty property, string? culture = null, stri return string.Empty; } - MapBlockValueToEditor(property, blockEditorData.BlockValue); + MapBlockValueToEditor(property, blockEditorData.BlockValue, culture, segment); // return json convertable object return blockEditorData.BlockValue; diff --git a/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorValidatorBase.cs b/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorValidatorBase.cs index cb8a69e446cf..e77ff3d9c0bb 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorValidatorBase.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorValidatorBase.cs @@ -28,6 +28,8 @@ protected IEnumerable GetBlockEditorDataValidation(B new { Path = nameof(BlockValue.SettingsData).ToFirstLowerInvariant(), Items = blockEditorData.BlockValue.SettingsData } }; + var valuesJsonPathPart = nameof(BlockItemData.Values).ToFirstLowerInvariant(); + foreach (var group in itemDataGroups) { var allElementTypes = _elementTypeCache.GetAll(group.Items.Select(x => x.ContentTypeKey).ToArray()).ToDictionary(x => x.Key); @@ -40,22 +42,21 @@ protected IEnumerable GetBlockEditorDataValidation(B throw new InvalidOperationException($"No element type found with key {item.ContentTypeKey}"); } - // now ensure missing properties - foreach (IPropertyType elementTypeProp in elementType.CompositionPropertyTypes) + // NOTE: for now this only validates the property data actually sent by the client, not all element properties. + // we need to ensure that all properties for all languages have a matching "item" entry here, to handle validation of + // required properties (see comment in the top of this method). a separate task has been created, get in touch with KJA. + var elementValidation = new ElementTypeValidationModel(item.ContentTypeAlias, item.Key); + for (var j = 0; j < item.Values.Count; j++) { - if (!item.PropertyValues.ContainsKey(elementTypeProp.Alias)) + BlockPropertyValue blockPropertyValue = item.Values[j]; + IPropertyType? propertyType = blockPropertyValue.PropertyType; + if (propertyType is null) { - // set values to null - item.PropertyValues[elementTypeProp.Alias] = new BlockItemData.BlockPropertyValue(null, elementTypeProp); - item.RawPropertyValues[elementTypeProp.Alias] = null; + throw new ArgumentException("One or more block properties did not have a resolved property type. Block editor values must be resolved before attempting to validate them.", nameof(blockEditorData)); } - } - var elementValidation = new ElementTypeValidationModel(item.ContentTypeAlias, item.Key); - foreach (KeyValuePair prop in item.PropertyValues) - { elementValidation.AddPropertyTypeValidation( - new PropertyTypeValidationModel(prop.Value.PropertyType, prop.Value.Value, $"{group.Path}[{i}].{prop.Value.PropertyType.Alias}")); + new PropertyTypeValidationModel(propertyType, blockPropertyValue.Value, $"{group.Path}[{i}].{valuesJsonPathPart}[{j}].value")); } yield return elementValidation; diff --git a/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorValues.cs b/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorValues.cs index 87c78fa119d7..5fcb8d4dd965 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorValues.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorValues.cs @@ -62,7 +62,7 @@ public BlockEditorValues(BlockEditorDataConverter dataConverter IDictionary contentTypesDictionary = _elementTypeCache.GetAll(contentTypeKeys).ToDictionary(x=>x.Key); foreach (BlockItemData block in blockEditorData.BlockValue.ContentData.Where(x => - blockEditorData.References.Any(r => x.Udi is not null && r.ContentUdi == x.Udi))) + blockEditorData.References.Any(r => r.ContentKey == x.Key))) { ResolveBlockItemData(block, contentTypePropertyTypes, contentTypesDictionary); } @@ -70,7 +70,7 @@ public BlockEditorValues(BlockEditorDataConverter dataConverter // filter out any settings that isn't referenced in the layout references foreach (BlockItemData block in blockEditorData.BlockValue.SettingsData.Where(x => blockEditorData.References.Any(r => - r.SettingsUdi is not null && x.Udi is not null && r.SettingsUdi == x.Udi))) + r.SettingsKey.HasValue && r.SettingsKey.Value == x.Key))) { ResolveBlockItemData(block, contentTypePropertyTypes, contentTypesDictionary); } @@ -82,7 +82,6 @@ public BlockEditorValues(BlockEditorDataConverter dataConverter return blockEditorData; } - private bool ResolveBlockItemData(BlockItemData block, Dictionary> contentTypePropertyTypes, IDictionary contentTypesDictionary) { if (contentTypesDictionary.TryGetValue(block.ContentTypeKey, out IContentType? contentType) is false) @@ -97,31 +96,26 @@ private bool ResolveBlockItemData(BlockItemData block, Dictionary x.Alias, x => x); } - var propValues = new Dictionary(); - - // find any keys that are not real property types and remove them - foreach (KeyValuePair prop in block.RawPropertyValues.ToList()) + // resolve the actual property types for all block properties + foreach (BlockPropertyValue property in block.Values) { - // doesn't exist so remove it - if (!propertyTypes.TryGetValue(prop.Key, out IPropertyType? propType)) + if (!propertyTypes.TryGetValue(property.Alias, out IPropertyType? propertyType)) { - block.RawPropertyValues.Remove(prop.Key); _logger.LogWarning( - "The property {PropertyKey} for block {BlockKey} was removed because the property type {PropertyTypeAlias} was not found on {ContentTypeAlias}", - prop.Key, + "The property {PropertyAlias} for block {BlockKey} was removed because the property type was not found on {ContentTypeAlias}", + property.Alias, block.Key, - prop.Key, contentType.Alias); + continue; } - else - { - // set the value to include the resolved property type - propValues[prop.Key] = new BlockItemData.BlockPropertyValue(prop.Value, propType); - } + + property.PropertyType = propertyType; } + // remove all block properties that did not resolve a property type + block.Values.RemoveAll(blockProperty => blockProperty.PropertyType is null); + block.ContentTypeAlias = contentType.Alias; - block.PropertyValues = propValues; return true; } diff --git a/src/Umbraco.Infrastructure/PropertyEditors/BlockGridPropertyEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/BlockGridPropertyEditor.cs index 6fe0e3882d00..ba1188198adb 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/BlockGridPropertyEditor.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/BlockGridPropertyEditor.cs @@ -2,6 +2,8 @@ // See LICENSE for more details. using Umbraco.Cms.Core.IO; +using Umbraco.Cms.Core.Models; +using Umbraco.Extensions; namespace Umbraco.Cms.Core.PropertyEditors; @@ -24,6 +26,16 @@ public BlockGridPropertyEditor( public override bool SupportsConfigurableElements => true; + /// + public override bool CanMergePartialPropertyValues(IPropertyType propertyType) => propertyType.VariesByCulture() is false; + + /// + public override object? MergePartialPropertyValueForCulture(object? sourceValue, object? targetValue, string? culture) + { + var valueEditor = (BlockGridEditorPropertyValueEditor)GetValueEditor(); + return valueEditor.MergePartialPropertyValueForCulture(sourceValue, targetValue, culture); + } + #region Pre Value Editor protected override IConfigurationEditor CreateConfigurationEditor() => new BlockGridConfigurationEditor(_ioHelper); diff --git a/src/Umbraco.Infrastructure/PropertyEditors/BlockGridPropertyEditorBase.cs b/src/Umbraco.Infrastructure/PropertyEditors/BlockGridPropertyEditorBase.cs index 254933eb8ffe..2b7364b1aa74 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/BlockGridPropertyEditorBase.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/BlockGridPropertyEditorBase.cs @@ -8,6 +8,7 @@ using Umbraco.Cms.Core.IO; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.Blocks; +using Umbraco.Cms.Core.PropertyEditors.ValueConverters; using Umbraco.Cms.Core.Serialization; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Strings; @@ -38,7 +39,7 @@ protected BlockGridPropertyEditorBase(IDataValueEditorFactory dataValueEditorFac protected override IDataValueEditor CreateValueEditor() => DataValueEditorFactory.Create(Attribute!); - private class BlockGridEditorPropertyValueEditor : BlockEditorPropertyValueEditor + internal class BlockGridEditorPropertyValueEditor : BlockEditorPropertyValueEditor { public BlockGridEditorPropertyValueEditor( DataEditorAttribute attribute, @@ -51,14 +52,17 @@ public BlockGridEditorPropertyValueEditor( IJsonSerializer jsonSerializer, IIOHelper ioHelper, IBlockEditorElementTypeCache elementTypeCache, - IPropertyValidationService propertyValidationService) - : base(attribute, propertyEditors, dataValueReferenceFactories, dataTypeConfigurationCache, textService, logger, shortStringHelper, jsonSerializer, ioHelper) + IPropertyValidationService propertyValidationService, + BlockEditorVarianceHandler blockEditorVarianceHandler) + : base(attribute, propertyEditors, dataValueReferenceFactories, dataTypeConfigurationCache, textService, logger, shortStringHelper, jsonSerializer, ioHelper, blockEditorVarianceHandler) { BlockEditorValues = new BlockEditorValues(new BlockGridEditorDataConverter(jsonSerializer), elementTypeCache, logger); Validators.Add(new BlockEditorValidator(propertyValidationService, BlockEditorValues, elementTypeCache)); Validators.Add(new MinMaxValidator(BlockEditorValues, textService)); } + protected override BlockGridValue CreateWithLayout(IEnumerable layout) => new(layout); + private class MinMaxValidator : BlockEditorMinMaxValidatorBase { private readonly BlockEditorValues _blockEditorValues; diff --git a/src/Umbraco.Infrastructure/PropertyEditors/BlockListPropertyEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/BlockListPropertyEditor.cs index bbb368dec1be..c1a2caacb8a2 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/BlockListPropertyEditor.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/BlockListPropertyEditor.cs @@ -2,7 +2,9 @@ // See LICENSE for more details. using Umbraco.Cms.Core.IO; +using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Serialization; +using Umbraco.Extensions; namespace Umbraco.Cms.Core.PropertyEditors; @@ -38,6 +40,16 @@ public BlockListPropertyEditor( public override bool SupportsConfigurableElements => true; + /// + public override bool CanMergePartialPropertyValues(IPropertyType propertyType) => propertyType.VariesByCulture() is false; + + /// + public override object? MergePartialPropertyValueForCulture(object? sourceValue, object? targetValue, string? culture) + { + var valueEditor = (BlockListEditorPropertyValueEditor)GetValueEditor(); + return valueEditor.MergePartialPropertyValueForCulture(sourceValue, targetValue, culture); + } + #region Pre Value Editor protected override IConfigurationEditor CreateConfigurationEditor() => diff --git a/src/Umbraco.Infrastructure/PropertyEditors/BlockListPropertyEditorBase.cs b/src/Umbraco.Infrastructure/PropertyEditors/BlockListPropertyEditorBase.cs index 2e37056cd233..59fa980d2b59 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/BlockListPropertyEditorBase.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/BlockListPropertyEditorBase.cs @@ -6,6 +6,7 @@ using Umbraco.Cms.Core.IO; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.Blocks; +using Umbraco.Cms.Core.PropertyEditors.ValueConverters; using Umbraco.Cms.Core.Serialization; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Strings; @@ -62,14 +63,17 @@ public BlockListEditorPropertyValueEditor( IShortStringHelper shortStringHelper, IJsonSerializer jsonSerializer, IIOHelper ioHelper, - IPropertyValidationService propertyValidationService) : - base(attribute, propertyEditors, dataValueReferenceFactories, dataTypeConfigurationCache, textService, logger, shortStringHelper, jsonSerializer, ioHelper) + IPropertyValidationService propertyValidationService, + BlockEditorVarianceHandler blockEditorVarianceHandler) + : base(attribute, propertyEditors, dataValueReferenceFactories, dataTypeConfigurationCache, textService, logger, shortStringHelper, jsonSerializer, ioHelper, blockEditorVarianceHandler) { BlockEditorValues = new BlockEditorValues(blockEditorDataConverter, elementTypeCache, logger); Validators.Add(new BlockEditorValidator(propertyValidationService, BlockEditorValues, elementTypeCache)); Validators.Add(new MinMaxValidator(BlockEditorValues, textService)); } + protected override BlockListValue CreateWithLayout(IEnumerable layout) => new(layout); + private class MinMaxValidator : BlockEditorMinMaxValidatorBase { private readonly BlockEditorValues _blockEditorValues; diff --git a/src/Umbraco.Infrastructure/PropertyEditors/BlockValuePropertyIndexValueFactory.cs b/src/Umbraco.Infrastructure/PropertyEditors/BlockValuePropertyIndexValueFactory.cs index c5cbb8d79e87..b4521d3bbd5e 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/BlockValuePropertyIndexValueFactory.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/BlockValuePropertyIndexValueFactory.cs @@ -24,8 +24,10 @@ public BlockValuePropertyIndexValueFactory( protected override IContentType? GetContentTypeOfNestedItem(BlockItemData input, IDictionary contentTypeDictionary) => contentTypeDictionary.TryGetValue(input.ContentTypeKey, out var result) ? result : null; - protected override IDictionary GetRawProperty(BlockItemData blockItemData) => - blockItemData.RawPropertyValues; + protected override IDictionary GetRawProperty(BlockItemData blockItemData) + => blockItemData.Values + .Where(p => p.Culture is null && p.Segment is null) + .ToDictionary(p => p.Alias, p => p.Value); protected override IEnumerable GetDataItems(IndexValueFactoryBlockValue input) => input.ContentData; diff --git a/src/Umbraco.Infrastructure/PropertyEditors/BlockValuePropertyValueEditorBase.cs b/src/Umbraco.Infrastructure/PropertyEditors/BlockValuePropertyValueEditorBase.cs index f2178fd71ae0..82d66a3bd073 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/BlockValuePropertyValueEditorBase.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/BlockValuePropertyValueEditorBase.cs @@ -4,9 +4,11 @@ using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.Blocks; using Umbraco.Cms.Core.Models.Editors; +using Umbraco.Cms.Core.PropertyEditors.ValueConverters; using Umbraco.Cms.Core.Serialization; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Strings; +using Umbraco.Extensions; namespace Umbraco.Cms.Core.PropertyEditors; @@ -17,7 +19,10 @@ public abstract class BlockValuePropertyValueEditorBase : DataV private readonly IDataTypeConfigurationCache _dataTypeConfigurationCache; private readonly PropertyEditorCollection _propertyEditors; private readonly ILogger _logger; + private readonly IJsonSerializer _jsonSerializer; private readonly DataValueReferenceFactoryCollection _dataValueReferenceFactoryCollection; + private readonly BlockEditorVarianceHandler _blockEditorVarianceHandler; + private BlockEditorValues? _blockEditorValues; protected BlockValuePropertyValueEditorBase( DataEditorAttribute attribute, @@ -28,24 +33,40 @@ protected BlockValuePropertyValueEditorBase( IShortStringHelper shortStringHelper, IJsonSerializer jsonSerializer, IIOHelper ioHelper, - DataValueReferenceFactoryCollection dataValueReferenceFactoryCollection) + DataValueReferenceFactoryCollection dataValueReferenceFactoryCollection, + BlockEditorVarianceHandler blockEditorVarianceHandler) : base(textService, shortStringHelper, jsonSerializer, ioHelper, attribute) { _propertyEditors = propertyEditors; _dataTypeConfigurationCache = dataTypeConfigurationCache; _logger = logger; + _jsonSerializer = jsonSerializer; _dataValueReferenceFactoryCollection = dataValueReferenceFactoryCollection; + _blockEditorVarianceHandler = blockEditorVarianceHandler; } /// public abstract IEnumerable GetReferences(object? value); + protected abstract TValue CreateWithLayout(IEnumerable layout); + + protected BlockEditorValues BlockEditorValues + { + get => _blockEditorValues ?? throw new NullReferenceException($"The property {nameof(BlockEditorValues)} must be initialized at value editor construction"); + set => _blockEditorValues = value; + } + protected IEnumerable GetBlockValueReferences(TValue blockValue) { var result = new HashSet(); - BlockItemData.BlockPropertyValue[] propertyValues = blockValue.ContentData.Concat(blockValue.SettingsData) - .SelectMany(x => x.PropertyValues.Values).ToArray(); - foreach (IGrouping valuesByPropertyEditorAlias in propertyValues.GroupBy(x => x.PropertyType.PropertyEditorAlias, x => x.Value)) + BlockPropertyValue[] blockPropertyValues = blockValue.ContentData.Concat(blockValue.SettingsData) + .SelectMany(x => x.Values).ToArray(); + if (blockPropertyValues.Any(p => p.PropertyType is null)) + { + throw new ArgumentException("One or more block properties did not have a resolved property type. Block editor values must be resolved before attempting to find references within them.", nameof(blockValue)); + } + + foreach (IGrouping valuesByPropertyEditorAlias in blockPropertyValues.GroupBy(x => x.PropertyType!.PropertyEditorAlias, x => x.Value)) { if (!_propertyEditors.TryGet(valuesByPropertyEditorAlias.Key, out IDataEditor? dataEditor)) { @@ -83,9 +104,14 @@ protected IEnumerable GetBlockValueTags(TValue blockValue, int? languageId // loop through all content and settings data foreach (BlockItemData row in blockValue.ContentData.Concat(blockValue.SettingsData)) { - foreach (KeyValuePair prop in row.PropertyValues) + foreach (BlockPropertyValue blockPropertyValue in row.Values) { - IDataEditor? propEditor = _propertyEditors[prop.Value.PropertyType.PropertyEditorAlias]; + if (blockPropertyValue.PropertyType is null) + { + throw new ArgumentException("One or more block properties did not have a resolved property type. Block editor values must be resolved before attempting to find tags within them.", nameof(blockValue)); + } + + IDataEditor? propEditor = _propertyEditors[blockPropertyValue.PropertyType.PropertyEditorAlias]; IDataValueEditor? valueEditor = propEditor?.GetValueEditor(); if (valueEditor is not IDataValueTags tagsProvider) @@ -93,9 +119,9 @@ protected IEnumerable GetBlockValueTags(TValue blockValue, int? languageId continue; } - object? configuration = _dataTypeConfigurationCache.GetConfiguration(prop.Value.PropertyType.DataTypeKey); + object? configuration = _dataTypeConfigurationCache.GetConfiguration(blockPropertyValue.PropertyType.DataTypeKey); - result.AddRange(tagsProvider.GetTags(prop.Value.Value, configuration, languageId)); + result.AddRange(tagsProvider.GetTags(blockPropertyValue.Value, configuration, languageId)); } } @@ -108,10 +134,11 @@ protected void MapBlockValueFromEditor(TValue blockValue) MapBlockItemDataFromEditor(blockValue.SettingsData); } - protected void MapBlockValueToEditor(IProperty property, TValue blockValue) + protected void MapBlockValueToEditor(IProperty property, TValue blockValue, string? culture, string? segment) { - MapBlockItemDataToEditor(property, blockValue.ContentData); - MapBlockItemDataToEditor(property, blockValue.SettingsData); + MapBlockItemDataToEditor(property, blockValue.ContentData, culture, segment); + MapBlockItemDataToEditor(property, blockValue.SettingsData, culture, segment); + _blockEditorVarianceHandler.AlignExposeVariance(blockValue); } protected IEnumerable ConfiguredElementTypeKeys(IBlockConfiguration configuration) @@ -123,71 +150,178 @@ protected IEnumerable ConfiguredElementTypeKeys(IBlockConfiguration config } } - private void MapBlockItemDataToEditor(IProperty property, List items) + private void MapBlockItemDataToEditor(IProperty property, List items, string? culture, string? segment) { - var valEditors = new Dictionary(); + var valueEditorsByKey = new Dictionary(); - foreach (BlockItemData row in items) + foreach (BlockItemData item in items) { - foreach (KeyValuePair prop in row.PropertyValues) + foreach (BlockPropertyValue blockPropertyValue in item.Values) { - // create a temp property with the value - // - force it to be culture invariant as the block editor can't handle culture variant element properties - prop.Value.PropertyType.Variations = ContentVariation.Nothing; - var tempProp = new Property(prop.Value.PropertyType); - tempProp.SetValue(prop.Value.Value); - - IDataEditor? propEditor = _propertyEditors[prop.Value.PropertyType.PropertyEditorAlias]; - if (propEditor == null) + IPropertyType? propertyType = blockPropertyValue.PropertyType; + if (propertyType is null) { - // NOTE: This logic was borrowed from Nested Content and I'm unsure why it exists. - // if the property editor doesn't exist I think everything will break anyways? - // update the raw value since this is what will get serialized out - row.RawPropertyValues[prop.Key] = tempProp.GetValue()?.ToString(); + throw new ArgumentException("One or more block properties did not have a resolved property type. Block editor values must be resolved before attempting to map them to editor.", nameof(items)); + } + + IDataEditor? propertyEditor = _propertyEditors[propertyType.PropertyEditorAlias]; + if (propertyEditor is null) + { + // leave the current block property value as-is - will be used to render a fallback output in the client continue; } - Guid dataTypeKey = prop.Value.PropertyType.DataTypeKey; - if (!valEditors.TryGetValue(dataTypeKey, out IDataValueEditor? valEditor)) + // if changes were made to the element type variation, we need those changes reflected in the block property values. + // for regular content this happens when a content type is saved (copies of property values are created in the DB), + // but for local block level properties we don't have that kind of handling, so we to do it manually. + // to be friendly we'll map "formerly invariant properties" to the default language ISO code instead of performing a + // hard reset of the property values (which would likely be the most correct thing to do from a data point of view). + _blockEditorVarianceHandler.AlignPropertyVarianceAsync(blockPropertyValue, propertyType, culture).GetAwaiter().GetResult(); + + if (!valueEditorsByKey.TryGetValue(propertyType.DataTypeKey, out IDataValueEditor? valueEditor)) { - var configuration = _dataTypeConfigurationCache.GetConfiguration(dataTypeKey); - valEditor = propEditor.GetValueEditor(configuration); + var configuration = _dataTypeConfigurationCache.GetConfiguration(propertyType.DataTypeKey); + valueEditor = propertyEditor.GetValueEditor(configuration); - valEditors.Add(dataTypeKey, valEditor); + valueEditorsByKey.Add(propertyType.DataTypeKey, valueEditor); } - var convValue = valEditor.ToEditor(tempProp); + var tempProp = new Property(propertyType); + tempProp.SetValue(blockPropertyValue.Value, blockPropertyValue.Culture, blockPropertyValue.Segment); + + var editorValue = valueEditor.ToEditor(tempProp, blockPropertyValue.Culture, blockPropertyValue.Segment); // update the raw value since this is what will get serialized out - row.RawPropertyValues[prop.Key] = convValue; + blockPropertyValue.Value = editorValue; } } } private void MapBlockItemDataFromEditor(List items) { - foreach (BlockItemData row in items) + foreach (BlockItemData item in items) { - foreach (KeyValuePair prop in row.PropertyValues) + foreach (BlockPropertyValue blockPropertyValue in item.Values) { - // Fetch the property types prevalue - var configuration = _dataTypeConfigurationCache.GetConfiguration(prop.Value.PropertyType.DataTypeKey); + IPropertyType? propertyType = blockPropertyValue.PropertyType; + if (propertyType is null) + { + throw new ArgumentException("One or more block properties did not have a resolved property type. Block editor values must be resolved before attempting to map them from editor.", nameof(items)); + } // Lookup the property editor - IDataEditor? propEditor = _propertyEditors[prop.Value.PropertyType.PropertyEditorAlias]; - if (propEditor == null) + IDataEditor? propertyEditor = _propertyEditors[propertyType.PropertyEditorAlias]; + if (propertyEditor is null) { continue; } + // Fetch the property types prevalue + var configuration = _dataTypeConfigurationCache.GetConfiguration(propertyType.DataTypeKey); + // Create a fake content property data object - var contentPropData = new ContentPropertyData(prop.Value.Value, configuration); + var propertyData = new ContentPropertyData(blockPropertyValue.Value, configuration); // Get the property editor to do it's conversion - var newValue = propEditor.GetValueEditor().FromEditor(contentPropData, prop.Value.Value); + var newValue = propertyEditor.GetValueEditor().FromEditor(propertyData, blockPropertyValue.Value); // update the raw value since this is what will get serialized out - row.RawPropertyValues[prop.Key] = newValue; + blockPropertyValue.Value = newValue; + } + } + } + + internal virtual object? MergePartialPropertyValueForCulture(object? sourceValue, object? targetValue, string? culture) + { + if (sourceValue is null) + { + return null; + } + + // parse the source value as block editor data + BlockEditorData? sourceBlockEditorValues = BlockEditorValues.DeserializeAndClean(sourceValue); + if (sourceBlockEditorValues?.Layout is null) + { + return null; + } + + // parse the target value as block editor data (fallback to an empty set of block editor data) + BlockEditorData targetBlockEditorValues = + (targetValue is not null ? BlockEditorValues.DeserializeAndClean(targetValue) : null) + ?? new BlockEditorData([], CreateWithLayout(sourceBlockEditorValues.Layout)); + + TValue mergeResult = MergeBlockEditorDataForCulture(sourceBlockEditorValues.BlockValue, targetBlockEditorValues.BlockValue, culture); + return _jsonSerializer.Serialize(mergeResult); + } + + protected TValue MergeBlockEditorDataForCulture(TValue sourceBlockValue, TValue targetBlockValue, string? culture) + { + // structure is global, layout and expose follows structure + targetBlockValue.Layout = sourceBlockValue.Layout; + targetBlockValue.Expose = sourceBlockValue.Expose; + + MergePartialPropertyValueForCulture(sourceBlockValue.ContentData, targetBlockValue.ContentData, culture); + MergePartialPropertyValueForCulture(sourceBlockValue.SettingsData, targetBlockValue.SettingsData, culture); + + return targetBlockValue; + } + + private void MergePartialPropertyValueForCulture(List sourceBlockItems, List targetBlockItems, string? culture) + { + // remove all target blocks that are not part of the source blocks (structure is global) + targetBlockItems.RemoveAll(pb => sourceBlockItems.Any(eb => eb.Key == pb.Key) is false); + + // merge the source values into the target values for culture + foreach (BlockItemData sourceBlockItem in sourceBlockItems) + { + BlockItemData? targetBlockItem = targetBlockItems.FirstOrDefault(i => i.Key == sourceBlockItem.Key); + if (targetBlockItem is null) + { + targetBlockItem = new BlockItemData( + sourceBlockItem.Key, + sourceBlockItem.ContentTypeKey, + sourceBlockItem.ContentTypeAlias); + + // NOTE: this only works because targetBlockItem is by ref! + targetBlockItems.Add(targetBlockItem); + } + + foreach (BlockPropertyValue sourceBlockPropertyValue in sourceBlockItem.Values) + { + // is this another editor that supports partial merging? i.e. blocks within blocks. + IDataEditor? mergingDataEditor = null; + var shouldPerformPartialMerge = sourceBlockPropertyValue.PropertyType is not null + && _propertyEditors.TryGet(sourceBlockPropertyValue.PropertyType.PropertyEditorAlias, out mergingDataEditor) + && mergingDataEditor.CanMergePartialPropertyValues(sourceBlockPropertyValue.PropertyType); + + if (shouldPerformPartialMerge is false && sourceBlockPropertyValue.Culture != culture) + { + // skip for now (irrelevant for the current culture, but might be included in the next pass) + continue; + } + + BlockPropertyValue? targetBlockPropertyValue = targetBlockItem + .Values + .FirstOrDefault(v => + v.Alias == sourceBlockPropertyValue.Alias && + v.Culture == sourceBlockPropertyValue.Culture && + v.Segment == sourceBlockPropertyValue.Segment); + + if (targetBlockPropertyValue is null) + { + targetBlockPropertyValue = new BlockPropertyValue + { + Alias = sourceBlockPropertyValue.Alias, + Culture = sourceBlockPropertyValue.Culture, + Segment = sourceBlockPropertyValue.Segment + }; + targetBlockItem.Values.Add(targetBlockPropertyValue); + } + + // assign source value to target value (or perform partial merge, depending on context) + targetBlockPropertyValue.Value = shouldPerformPartialMerge is false + ? sourceBlockPropertyValue.Value + : mergingDataEditor!.MergePartialPropertyValueForCulture(sourceBlockPropertyValue.Value, targetBlockPropertyValue.Value, culture); } } } diff --git a/src/Umbraco.Infrastructure/PropertyEditors/MediaPicker3PropertyEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/MediaPicker3PropertyEditor.cs index e3abf0035f17..d99a742c309a 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/MediaPicker3PropertyEditor.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/MediaPicker3PropertyEditor.cs @@ -186,9 +186,6 @@ private List UpdateMediaTypeAliases(List m private List HandleTemporaryMediaUploads(List mediaWithCropsDtos, MediaPicker3Configuration configuration) { - Guid userKey = _backOfficeSecurityAccessor.BackOfficeSecurity?.CurrentUser?.Key - ?? throw new InvalidOperationException("Could not obtain the current backoffice user"); - var invalidDtos = new List(); foreach (MediaWithCropsDto mediaWithCropsDto in mediaWithCropsDtos) @@ -218,7 +215,7 @@ private List HandleTemporaryMediaUploads(List HandleTemporaryMediaUploads(List _backOfficeSecurityAccessor.BackOfficeSecurity?.CurrentUser?.Key + ?? throw new InvalidOperationException("Could not obtain the current backoffice user"); + /// /// Model/DTO that represents the JSON that the MediaPicker3 stores. /// diff --git a/src/Umbraco.Infrastructure/PropertyEditors/RichTextPropertyEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/RichTextPropertyEditor.cs index ef442b768fb5..b28a99472b3f 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/RichTextPropertyEditor.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/RichTextPropertyEditor.cs @@ -6,12 +6,12 @@ using Microsoft.Extensions.Logging; using Umbraco.Cms.Core.Cache; using Umbraco.Cms.Core.Cache.PropertyEditors; -using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.IO; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.Blocks; using Umbraco.Cms.Core.Models.Editors; using Umbraco.Cms.Core.PropertyEditors.Validators; +using Umbraco.Cms.Core.PropertyEditors.ValueConverters; using Umbraco.Cms.Core.Security; using Umbraco.Cms.Core.Serialization; using Umbraco.Cms.Core.Services; @@ -51,6 +51,16 @@ public RichTextPropertyEditor( public override bool SupportsConfigurableElements => true; + /// + public override bool CanMergePartialPropertyValues(IPropertyType propertyType) => propertyType.VariesByCulture() is false; + + /// + public override object? MergePartialPropertyValueForCulture(object? sourceValue, object? targetValue, string? culture) + { + var valueEditor = (RichTextPropertyValueEditor)GetValueEditor(); + return valueEditor.MergePartialPropertyValueForCulture(sourceValue, targetValue, culture); + } + /// /// Create a custom value editor /// @@ -78,46 +88,6 @@ internal class RichTextPropertyValueEditor : BlockValuePropertyValueEditorBase _logger; - [Obsolete("Use non-obsolete constructor. This is schedules for removal in v16.")] - public RichTextPropertyValueEditor( - DataEditorAttribute attribute, - PropertyEditorCollection propertyEditors, - IDataTypeConfigurationCache dataTypeReadCache, - ILogger logger, - IBackOfficeSecurityAccessor backOfficeSecurityAccessor, - ILocalizedTextService localizedTextService, - IShortStringHelper shortStringHelper, - HtmlImageSourceParser imageSourceParser, - HtmlLocalLinkParser localLinkParser, - RichTextEditorPastedImages pastedImages, - IJsonSerializer jsonSerializer, - IIOHelper ioHelper, - IHtmlSanitizer htmlSanitizer, - IBlockEditorElementTypeCache elementTypeCache, - IPropertyValidationService propertyValidationService, - DataValueReferenceFactoryCollection dataValueReferenceFactoryCollection) - : this( - attribute, - propertyEditors, - dataTypeReadCache, - logger, - backOfficeSecurityAccessor, - localizedTextService, - shortStringHelper, - imageSourceParser, - localLinkParser, - pastedImages, - jsonSerializer, - ioHelper, - htmlSanitizer, - elementTypeCache, - propertyValidationService, - dataValueReferenceFactoryCollection, - StaticServiceProvider.Instance.GetRequiredService()) - { - - } - public RichTextPropertyValueEditor( DataEditorAttribute attribute, PropertyEditorCollection propertyEditors, @@ -135,8 +105,9 @@ public RichTextPropertyValueEditor( IBlockEditorElementTypeCache elementTypeCache, IPropertyValidationService propertyValidationService, DataValueReferenceFactoryCollection dataValueReferenceFactoryCollection, - IRichTextRequiredValidator richTextRequiredValidator) - : base(attribute, propertyEditors, dataTypeReadCache, localizedTextService, logger, shortStringHelper, jsonSerializer, ioHelper, dataValueReferenceFactoryCollection) + IRichTextRequiredValidator richTextRequiredValidator, + BlockEditorVarianceHandler blockEditorVarianceHandler) + : base(attribute, propertyEditors, dataTypeReadCache, localizedTextService, logger, shortStringHelper, jsonSerializer, ioHelper, dataValueReferenceFactoryCollection, blockEditorVarianceHandler) { _backOfficeSecurityAccessor = backOfficeSecurityAccessor; _localizedTextService = localizedTextService; @@ -154,6 +125,8 @@ public RichTextPropertyValueEditor( public override IValueRequiredValidator RequiredValidator => _richTextRequiredValidator; + protected override RichTextBlockValue CreateWithLayout(IEnumerable layout) => new(layout); + /// public override object? ConfigurationObject { @@ -247,7 +220,7 @@ public override IEnumerable GetTags(object? value, object? dataTypeConfigu richTextEditorValue.Markup = _imageSourceParser.EnsureImageSources(richTextEditorValue.Markup); // return json convertable object - return CleanAndMapBlocks(richTextEditorValue, blockValue => MapBlockValueToEditor(property, blockValue)); + return CleanAndMapBlocks(richTextEditorValue, blockValue => MapBlockValueToEditor(property, blockValue, culture, segment)); } /// @@ -295,6 +268,38 @@ public override IEnumerable ConfiguredElementTypeKeys() return configuration?.Blocks?.SelectMany(ConfiguredElementTypeKeys) ?? Enumerable.Empty(); } + internal override object? MergePartialPropertyValueForCulture(object? sourceValue, object? targetValue, string? culture) + { + if (sourceValue is null) + { + return null; + } + + if (TryParseEditorValue(sourceValue, out RichTextEditorValue? sourceRichTextEditorValue) is false + || sourceRichTextEditorValue.Blocks is null) + { + return null; + } + + BlockEditorData? sourceBlockEditorData = ConvertAndClean(sourceRichTextEditorValue.Blocks); + if (sourceBlockEditorData?.Layout is null) + { + return null; + } + + TryParseEditorValue(targetValue, out RichTextEditorValue? targetRichTextEditorValue); + + BlockEditorData targetBlockEditorData = + (targetRichTextEditorValue?.Blocks is not null ? ConvertAndClean(targetRichTextEditorValue.Blocks) : null) + ?? new BlockEditorData([], CreateWithLayout(sourceBlockEditorData.Layout)); + + RichTextBlockValue blocksMergeResult = MergeBlockEditorDataForCulture(sourceBlockEditorData.BlockValue, targetBlockEditorData.BlockValue, culture); + + // structure is global, and markup follows structure + var mergedEditorValue = new RichTextEditorValue { Markup = sourceRichTextEditorValue.Markup, Blocks = blocksMergeResult }; + return RichTextPropertyEditorHelper.SerializeRichTextEditorValue(mergedEditorValue, _jsonSerializer); + } + private bool TryParseEditorValue(object? value, [NotNullWhen(true)] out RichTextEditorValue? richTextEditorValue) => RichTextPropertyEditorHelper.TryParseRichTextEditorValue(value, _jsonSerializer, _logger, out richTextEditorValue); diff --git a/src/Umbraco.Infrastructure/PropertyEditors/RichTextPropertyIndexValueFactory.cs b/src/Umbraco.Infrastructure/PropertyEditors/RichTextPropertyIndexValueFactory.cs index b89b36f99ecc..e2236e2976ec 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/RichTextPropertyIndexValueFactory.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/RichTextPropertyIndexValueFactory.cs @@ -71,7 +71,9 @@ public RichTextPropertyIndexValueFactory( => contentTypeDictionary.TryGetValue(nestedItem.ContentTypeKey, out var result) ? result : null; protected override IDictionary GetRawProperty(BlockItemData blockItemData) - => blockItemData.RawPropertyValues; + => blockItemData.Values + .Where(p => p.Culture is null && p.Segment is null) + .ToDictionary(p => p.Alias, p => p.Value); protected override IEnumerable GetDataItems(RichTextEditorValue input) => input.Blocks?.ContentData ?? new List(); diff --git a/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/BlockEditorConverter.cs b/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/BlockEditorConverter.cs index f84e1e31b213..2dc70eea0d90 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/BlockEditorConverter.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/BlockEditorConverter.cs @@ -15,16 +15,22 @@ public sealed class BlockEditorConverter { private readonly IPublishedModelFactory _publishedModelFactory; private readonly IPublishedSnapshotAccessor _publishedSnapshotAccessor; + private readonly IVariationContextAccessor _variationContextAccessor; + private readonly BlockEditorVarianceHandler _blockEditorVarianceHandler; public BlockEditorConverter( IPublishedSnapshotAccessor publishedSnapshotAccessor, - IPublishedModelFactory publishedModelFactory) + IPublishedModelFactory publishedModelFactory, + IVariationContextAccessor variationContextAccessor, + BlockEditorVarianceHandler blockEditorVarianceHandler) { _publishedSnapshotAccessor = publishedSnapshotAccessor; _publishedModelFactory = publishedModelFactory; + _variationContextAccessor = variationContextAccessor; + _blockEditorVarianceHandler = blockEditorVarianceHandler; } - public IPublishedElement? ConvertToElement(BlockItemData data, PropertyCacheLevel referenceCacheLevel, bool preview) + public IPublishedElement? ConvertToElement(IPublishedElement owner, BlockItemData data, PropertyCacheLevel referenceCacheLevel, bool preview) { IPublishedContentCache? publishedContentCache = _publishedSnapshotAccessor.GetRequiredPublishedSnapshot().Content; @@ -36,10 +42,45 @@ public BlockEditorConverter( return null; } - Dictionary propertyValues = data.RawPropertyValues; + VariationContext variationContext = _variationContextAccessor.VariationContext ?? new VariationContext(); - // Get the UDI from the deserialized object. If this is empty, we can fallback to checking the 'key' if there is one - Guid key = data.Udi is GuidUdi gudi ? gudi.Guid : Guid.Empty; + var propertyTypesByAlias = publishedContentType + .PropertyTypes + .ToDictionary(propertyType => propertyType.Alias); + + var propertyValues = new Dictionary(); + foreach (BlockPropertyValue property in data.Values) + { + if (!propertyTypesByAlias.TryGetValue(property.Alias, out IPublishedPropertyType? propertyType)) + { + continue; + } + + // if case changes have been made to the content or element type variation since the parent content was published, + // we need to align those changes for the block properties - unlike for root level properties, where these + // things are handled when a content type is saved. + BlockPropertyValue? alignedProperty = _blockEditorVarianceHandler.AlignedPropertyVarianceAsync(property, propertyType, owner).GetAwaiter().GetResult(); + if (alignedProperty is null) + { + continue; + } + + var expectedCulture = owner.ContentType.VariesByCulture() && publishedContentType.VariesByCulture() && propertyType.VariesByCulture() + ? variationContext.Culture + : null; + var expectedSegment = owner.ContentType.VariesBySegment() && publishedContentType.VariesBySegment() && propertyType.VariesBySegment() + ? variationContext.Segment + : null; + + if (alignedProperty.Culture.NullOrWhiteSpaceAsNull().InvariantEquals(expectedCulture.NullOrWhiteSpaceAsNull()) + && alignedProperty.Segment.NullOrWhiteSpaceAsNull().InvariantEquals(expectedSegment.NullOrWhiteSpaceAsNull())) + { + propertyValues[alignedProperty.Alias] = alignedProperty.Value; + } + } + + // Get the key from the deserialized object. If this is empty, we can fallback to checking the 'key' if there is one + Guid key = data.Key; if (key == Guid.Empty && propertyValues.TryGetValue("key", out var keyo)) { Guid.TryParse(keyo!.ToString(), out key); diff --git a/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/BlockEditorPropertyValueConstructorCacheBase.cs b/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/BlockEditorPropertyValueConstructorCacheBase.cs index 3af7b4bebaab..df722c611aca 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/BlockEditorPropertyValueConstructorCacheBase.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/BlockEditorPropertyValueConstructorCacheBase.cs @@ -9,13 +9,13 @@ public abstract class BlockEditorPropertyValueConstructorCacheBase where T : IBlockReference { private readonly - ConcurrentDictionary<(Guid, Guid?), Func> + ConcurrentDictionary<(Guid, Guid?), Func> _constructorCache = new(); - public bool TryGetValue((Guid ContentTypeKey, Guid? SettingsTypeKey) key, [MaybeNullWhen(false)] out Func value) + public bool TryGetValue((Guid ContentTypeKey, Guid? SettingsTypeKey) key, [MaybeNullWhen(false)] out Func value) => _constructorCache.TryGetValue(key, out value); - public void SetValue((Guid ContentTypeKey, Guid? SettingsTypeKey) key, Func value) + public void SetValue((Guid ContentTypeKey, Guid? SettingsTypeKey) key, Func value) => _constructorCache[key] = value; public void Clear() diff --git a/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/BlockEditorVarianceHandler.cs b/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/BlockEditorVarianceHandler.cs new file mode 100644 index 000000000000..003464819122 --- /dev/null +++ b/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/BlockEditorVarianceHandler.cs @@ -0,0 +1,137 @@ +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.Blocks; +using Umbraco.Cms.Core.Models.PublishedContent; +using Umbraco.Cms.Core.Services; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters; + +public sealed class BlockEditorVarianceHandler +{ + private readonly ILanguageService _languageService; + + public BlockEditorVarianceHandler(ILanguageService languageService) + => _languageService = languageService; + + public async Task AlignPropertyVarianceAsync(BlockPropertyValue blockPropertyValue, IPropertyType propertyType, string? culture) + { + culture ??= await _languageService.GetDefaultIsoCodeAsync(); + if (propertyType.VariesByCulture() != VariesByCulture(blockPropertyValue)) + { + blockPropertyValue.Culture = propertyType.VariesByCulture() + ? culture + : null; + } + } + + public async Task AlignedPropertyVarianceAsync(BlockPropertyValue blockPropertyValue, IPublishedPropertyType propertyType, IPublishedElement owner) + { + ContentVariation propertyTypeVariation = owner.ContentType.Variations & propertyType.Variations; + if (propertyTypeVariation.VariesByCulture() == VariesByCulture(blockPropertyValue)) + { + return blockPropertyValue; + } + + // mismatch in culture variation for published content: + // - if the property type varies by culture, assign the default culture + // - if the property type does not vary by culture: + // - if the property value culture equals the default culture, assign a null value for it to be rendered as the invariant value + // - otherwise return null (not applicable for rendering) + var defaultCulture = await _languageService.GetDefaultIsoCodeAsync(); + if (propertyTypeVariation.VariesByCulture()) + { + return new BlockPropertyValue + { + Alias = blockPropertyValue.Alias, + Culture = defaultCulture, + Segment = blockPropertyValue.Segment, + Value = blockPropertyValue.Value, + PropertyType = blockPropertyValue.PropertyType + }; + } + + if (defaultCulture.Equals(blockPropertyValue.Culture)) + { + return new BlockPropertyValue + { + Alias = blockPropertyValue.Alias, + Culture = null, + Segment = blockPropertyValue.Segment, + Value = blockPropertyValue.Value, + PropertyType = blockPropertyValue.PropertyType + }; + } + + return null; + } + + public async Task> AlignedExposeVarianceAsync(BlockValue blockValue, IPublishedElement owner, IPublishedElement element) + { + BlockItemVariation[] blockVariations = blockValue.Expose.Where(v => v.ContentKey == element.Key).ToArray(); + if (blockVariations.Any() is false) + { + return blockVariations; + } + + // in case of mismatch in culture variation for block value variation: + // - if the expected variation is by culture, assign the default culture to all block variation + // - if the expected variation is not by culture, use all in block variation from the default culture as invariant + + ContentVariation exposeVariation = owner.ContentType.Variations & element.ContentType.Variations; + if (exposeVariation.VariesByCulture() && blockVariations.All(v => v.Culture is null)) + { + var defaultCulture = await _languageService.GetDefaultIsoCodeAsync(); + return blockVariations.Select(v => new BlockItemVariation(v.ContentKey, defaultCulture, v.Segment)); + } + + if (exposeVariation.VariesByCulture() is false && blockVariations.All(v => v.Culture is not null)) + { + var defaultCulture = await _languageService.GetDefaultIsoCodeAsync(); + return blockVariations + .Where(v => v.Culture == defaultCulture) + .Select(v => new BlockItemVariation(v.ContentKey, null, v.Segment)) + .ToList(); + } + + return blockVariations; + } + + public void AlignExposeVariance(BlockValue blockValue) + { + var contentDataToAlign = new List(); + foreach (BlockItemVariation variation in blockValue.Expose) + { + BlockItemData? contentData = blockValue.ContentData.FirstOrDefault(cd => cd.Key == variation.ContentKey); + if (contentData is null) + { + continue; + } + + if((variation.Culture is null && contentData.Values.Any(v => v.Culture is not null)) + || (variation.Culture is not null && contentData.Values.All(v => v.Culture is null))) + { + contentDataToAlign.Add(contentData); + } + } + + if (contentDataToAlign.Any() is false) + { + return; + } + + blockValue.Expose.RemoveAll(v => contentDataToAlign.Any(cd => cd.Key == v.ContentKey)); + foreach (BlockItemData contentData in contentDataToAlign) + { + var omitNullCulture = contentData.Values.Any(v => v.Culture is not null); + foreach (BlockPropertyValue value in contentData.Values + .Where(v => omitNullCulture is false || v.Culture is not null) + .DistinctBy(v => v.Culture + v.Segment)) + { + blockValue.Expose.Add(new BlockItemVariation(contentData.Key, value.Culture, value.Segment)); + } + } + } + + private static bool VariesByCulture(BlockPropertyValue blockPropertyValue) + => blockPropertyValue.Culture.IsNullOrWhiteSpace() is false; +} diff --git a/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/BlockGridPropertyValueConverter.cs b/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/BlockGridPropertyValueConverter.cs index 1e00b5ee6f2c..822632aaf3dd 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/BlockGridPropertyValueConverter.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/BlockGridPropertyValueConverter.cs @@ -22,14 +22,19 @@ public class BlockGridPropertyValueConverter : PropertyValueConverterBase, IDeli private readonly IJsonSerializer _jsonSerializer; private readonly IApiElementBuilder _apiElementBuilder; private readonly BlockGridPropertyValueConstructorCache _constructorCache; + private readonly IVariationContextAccessor _variationContextAccessor; + private readonly BlockEditorVarianceHandler _blockEditorVarianceHandler; - [Obsolete("Please use non-obsolete construtor. This will be removed in Umbraco 15.")] + [Obsolete("Use the constructor that takes all parameters, scheduled for removal in V16")] public BlockGridPropertyValueConverter( IProfilingLogger proflog, BlockEditorConverter blockConverter, IJsonSerializer jsonSerializer, - IApiElementBuilder apiElementBuilder) - : this(proflog, blockConverter, jsonSerializer, apiElementBuilder, StaticServiceProvider.Instance.GetRequiredService()) + IApiElementBuilder apiElementBuilder, + BlockGridPropertyValueConstructorCache constructorCache) + : this(proflog, blockConverter, jsonSerializer, apiElementBuilder, constructorCache, + StaticServiceProvider.Instance.GetRequiredService(), + StaticServiceProvider.Instance.GetRequiredService()) { } @@ -38,13 +43,17 @@ public BlockGridPropertyValueConverter( BlockEditorConverter blockConverter, IJsonSerializer jsonSerializer, IApiElementBuilder apiElementBuilder, - BlockGridPropertyValueConstructorCache constructorCache) + BlockGridPropertyValueConstructorCache constructorCache, + IVariationContextAccessor variationContextAccessor, + BlockEditorVarianceHandler blockEditorVarianceHandler) { _proflog = proflog; _blockConverter = blockConverter; _jsonSerializer = jsonSerializer; _apiElementBuilder = apiElementBuilder; _constructorCache = constructorCache; + _variationContextAccessor = variationContextAccessor; + _blockEditorVarianceHandler = blockEditorVarianceHandler; } /// @@ -61,7 +70,7 @@ public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType /// public override object? ConvertIntermediateToObject(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object? inter, bool preview) - => ConvertIntermediateToBlockGridModel(propertyType, referenceCacheLevel, inter, preview); + => ConvertIntermediateToBlockGridModel(owner, propertyType, referenceCacheLevel, inter, preview); /// public PropertyCacheLevel GetDeliveryApiPropertyCacheLevel(IPublishedPropertyType propertyType) => GetPropertyCacheLevel(propertyType); @@ -78,7 +87,7 @@ public Type GetDeliveryApiPropertyValueType(IPublishedPropertyType propertyType) { const int defaultColumns = 12; - BlockGridModel? blockGridModel = ConvertIntermediateToBlockGridModel(propertyType, referenceCacheLevel, inter, preview); + BlockGridModel? blockGridModel = ConvertIntermediateToBlockGridModel(owner, propertyType, referenceCacheLevel, inter, preview); if (blockGridModel == null) { return new ApiBlockGridModel(defaultColumns, Array.Empty()); @@ -109,7 +118,7 @@ ApiBlockGridArea CreateApiBlockGridArea(BlockGridArea area) return model; } - private BlockGridModel? ConvertIntermediateToBlockGridModel(IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object? inter, bool preview) + private BlockGridModel? ConvertIntermediateToBlockGridModel(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object? inter, bool preview) { using (!_proflog.IsEnabled(LogLevel.Debug) ? null : _proflog.DebugDuration($"ConvertPropertyToBlockGrid ({propertyType.DataType.Id})")) { @@ -132,8 +141,8 @@ ApiBlockGridArea CreateApiBlockGridArea(BlockGridArea area) return null; } - var creator = new BlockGridPropertyValueCreator(_blockConverter, _jsonSerializer, _constructorCache); - return creator.CreateBlockModel(referenceCacheLevel, intermediateBlockModelValue, preview, configuration.Blocks, configuration.GridColumns); + var creator = new BlockGridPropertyValueCreator(_blockConverter, _variationContextAccessor, _blockEditorVarianceHandler, _jsonSerializer, _constructorCache); + return creator.CreateBlockModel(owner, referenceCacheLevel, intermediateBlockModelValue, preview, configuration.Blocks, configuration.GridColumns); } } } diff --git a/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/BlockGridPropertyValueCreator.cs b/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/BlockGridPropertyValueCreator.cs index 6b1252f751f4..1cfef58b23f9 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/BlockGridPropertyValueCreator.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/BlockGridPropertyValueCreator.cs @@ -1,4 +1,5 @@ using Umbraco.Cms.Core.Models.Blocks; +using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Cms.Core.Serialization; using Umbraco.Extensions; @@ -9,14 +10,19 @@ internal class BlockGridPropertyValueCreator : BlockPropertyValueCreatorBase BlockGridModel.Empty; @@ -46,6 +52,7 @@ public BlockGridModel CreateBlockModel(PropertyCacheLevel referenceCacheLevel, s } BlockGridModel blockModel = CreateBlockModel( + owner, referenceCacheLevel, intermediateBlockModelValue, preview, diff --git a/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/BlockListPropertyValueConverter.cs b/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/BlockListPropertyValueConverter.cs index 9863e4135b9a..ccd637f0078f 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/BlockListPropertyValueConverter.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/BlockListPropertyValueConverter.cs @@ -26,21 +26,24 @@ public class BlockListPropertyValueConverter : PropertyValueConverterBase, IDeli private readonly IApiElementBuilder _apiElementBuilder; private readonly IJsonSerializer _jsonSerializer; private readonly BlockListPropertyValueConstructorCache _constructorCache; + private readonly IVariationContextAccessor _variationContextAccessor; + private readonly BlockEditorVarianceHandler _blockEditorVarianceHandler; - - [Obsolete("Use the constructor that takes all parameters, scheduled for removal in V15")] - public BlockListPropertyValueConverter(IProfilingLogger proflog, BlockEditorConverter blockConverter, IContentTypeService contentTypeService, IApiElementBuilder apiElementBuilder) - : this(proflog, blockConverter, contentTypeService, apiElementBuilder, StaticServiceProvider.Instance.GetRequiredService(), StaticServiceProvider.Instance.GetRequiredService()) - { - } - - [Obsolete("Use the constructor that takes all parameters, scheduled for removal in V15")] - public BlockListPropertyValueConverter(IProfilingLogger proflog, BlockEditorConverter blockConverter, IContentTypeService contentTypeService, IApiElementBuilder apiElementBuilder, BlockListPropertyValueConstructorCache constructorCache) - : this(proflog, blockConverter, contentTypeService, apiElementBuilder, StaticServiceProvider.Instance.GetRequiredService(), constructorCache) + [Obsolete("Use the constructor that takes all parameters, scheduled for removal in V16")] + public BlockListPropertyValueConverter(IProfilingLogger proflog, BlockEditorConverter blockConverter, IContentTypeService contentTypeService, IApiElementBuilder apiElementBuilder, IJsonSerializer jsonSerializer, BlockListPropertyValueConstructorCache constructorCache) + : this(proflog, blockConverter, contentTypeService, apiElementBuilder, jsonSerializer, constructorCache, StaticServiceProvider.Instance.GetRequiredService(), StaticServiceProvider.Instance.GetRequiredService()) { } - public BlockListPropertyValueConverter(IProfilingLogger proflog, BlockEditorConverter blockConverter, IContentTypeService contentTypeService, IApiElementBuilder apiElementBuilder, IJsonSerializer jsonSerializer, BlockListPropertyValueConstructorCache constructorCache) + public BlockListPropertyValueConverter( + IProfilingLogger proflog, + BlockEditorConverter blockConverter, + IContentTypeService contentTypeService, + IApiElementBuilder apiElementBuilder, + IJsonSerializer jsonSerializer, + BlockListPropertyValueConstructorCache constructorCache, + IVariationContextAccessor variationContextAccessor, + BlockEditorVarianceHandler blockEditorVarianceHandler) { _proflog = proflog; _blockConverter = blockConverter; @@ -48,6 +51,8 @@ public BlockListPropertyValueConverter(IProfilingLogger proflog, BlockEditorConv _apiElementBuilder = apiElementBuilder; _jsonSerializer = jsonSerializer; _constructorCache = constructorCache; + _variationContextAccessor = variationContextAccessor; + _blockEditorVarianceHandler = blockEditorVarianceHandler; } /// @@ -159,8 +164,8 @@ public Type GetDeliveryApiPropertyValueType(IPublishedPropertyType propertyType) return null; } - var creator = new BlockListPropertyValueCreator(_blockConverter, _jsonSerializer, _constructorCache); - return creator.CreateBlockModel(referenceCacheLevel, intermediateBlockModelValue, preview, configuration.Blocks); + var creator = new BlockListPropertyValueCreator(_blockConverter, _variationContextAccessor, _blockEditorVarianceHandler, _jsonSerializer, _constructorCache); + return creator.CreateBlockModel(owner, referenceCacheLevel, intermediateBlockModelValue, preview, configuration.Blocks); } } } diff --git a/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/BlockListPropertyValueCreator.cs b/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/BlockListPropertyValueCreator.cs index 853dc1027f0e..5714256759d6 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/BlockListPropertyValueCreator.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/BlockListPropertyValueCreator.cs @@ -1,4 +1,5 @@ using Umbraco.Cms.Core.Models.Blocks; +using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Cms.Core.Serialization; namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters; @@ -10,21 +11,23 @@ internal class BlockListPropertyValueCreator : BlockPropertyValueCreatorBase BlockListModel.Empty; BlockListModel CreateModel(IList items) => new BlockListModel(items); - BlockListModel blockModel = CreateBlockModel(referenceCacheLevel, intermediateBlockModelValue, preview, blockConfigurations, CreateEmptyModel, CreateModel); + BlockListModel blockModel = CreateBlockModel(owner, referenceCacheLevel, intermediateBlockModelValue, preview, blockConfigurations, CreateEmptyModel, CreateModel); return blockModel; } diff --git a/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/BlockPropertyValueConverterBase.cs b/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/BlockPropertyValueConverterBase.cs deleted file mode 100644 index dd0d206a7884..000000000000 --- a/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/BlockPropertyValueConverterBase.cs +++ /dev/null @@ -1,255 +0,0 @@ -// Copyright (c) Umbraco. -// See LICENSE for more details. - -using System.Reflection; -using Umbraco.Cms.Core.Models.Blocks; -using Umbraco.Cms.Core.Models.PublishedContent; -using Umbraco.Extensions; - -namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters; - -[Obsolete("Please use implementations of BlockPropertyValueCreatorBase instead of this. See BlockListPropertyValueConverter for inspiration.. Will be removed in V15.")] -public abstract class BlockPropertyValueConverterBase : PropertyValueConverterBase - where TBlockItemModel : class, IBlockReference - where TBlockLayoutItem : class, IBlockLayoutItem, new() - where TBlockConfiguration : IBlockConfiguration - where TBlockValue : BlockValue, new() -{ - /// - /// Creates a specific data converter for the block property implementation. - /// - /// - protected abstract BlockEditorDataConverter CreateBlockEditorDataConverter(); - - /// - /// Creates a specific block item activator for the block property implementation. - /// - /// - protected abstract BlockItemActivator CreateBlockItemActivator(); - - /// - /// Creates an empty block model, i.e. for uninitialized or invalid property values. - /// - /// - protected delegate TBlockModel CreateEmptyBlockModel(); - - /// - /// Creates a block model for a list of unwrapped block items. - /// - /// The unwrapped block items to base the block model on. - /// - protected delegate TBlockModel CreateBlockModel(IList blockItems); - - /// - /// Creates a block item from a block layout item. - /// - /// The block layout item to base the block item on. - /// - protected delegate TBlockItemModel? CreateBlockItemModelFromLayout(TBlockLayoutItem layoutItem); - - /// - /// Enriches a block item after it has been created by the block item activator. Use this to set block item data based on concrete block layout and configuration. - /// - /// The block item to enrich. - /// The block layout item for the block item being enriched. - /// The configuration of the block. - /// Delegate for creating new block items from block layout items. - /// - protected delegate TBlockItemModel? EnrichBlockItemModelFromConfiguration(TBlockItemModel item, TBlockLayoutItem layoutItem, TBlockConfiguration configuration, CreateBlockItemModelFromLayout blockItemModelCreator); - - protected BlockPropertyValueConverterBase(BlockEditorConverter blockBlockEditorConverter) => BlockEditorConverter = blockBlockEditorConverter; - - protected BlockEditorConverter BlockEditorConverter { get; } - - /// - public override object? ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object? source, bool preview) => source?.ToString(); - - /// - public override Type GetPropertyValueType(IPublishedPropertyType propertyType) => typeof(TBlockModel); - - /// - public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) - => PropertyCacheLevel.Element; - - protected TBlockModel UnwrapBlockModel( - PropertyCacheLevel referenceCacheLevel, - object? inter, - bool preview, - IEnumerable blockConfigurations, - CreateEmptyBlockModel createEmptyModel, - CreateBlockModel createModel, - EnrichBlockItemModelFromConfiguration? enrichBlockItem = null) - { - // NOTE: The intermediate object is just a json string, we don't actually convert from source -> intermediate since source is always just a json string - - var value = (string?)inter; - - // Short-circuit on empty values - if (string.IsNullOrWhiteSpace(value)) - { - return createEmptyModel(); - } - - BlockEditorDataConverter blockEditorDataConverter = CreateBlockEditorDataConverter(); - BlockEditorData converted = blockEditorDataConverter.Deserialize(value); - if (converted.BlockValue.ContentData.Count == 0) - { - return createEmptyModel(); - } - - IEnumerable? layout = converted.Layout; - if (layout is null) - { - return createEmptyModel(); - } - - var blockConfigMap = blockConfigurations.ToDictionary(bc => bc.ContentElementTypeKey); - - // Convert the content data - var contentPublishedElements = new Dictionary(); - foreach (BlockItemData data in converted.BlockValue.ContentData) - { - if (!blockConfigMap.ContainsKey(data.ContentTypeKey)) - { - continue; - } - - IPublishedElement? element = BlockEditorConverter.ConvertToElement(data, referenceCacheLevel, preview); - if (element == null) - { - continue; - } - - contentPublishedElements[element.Key] = element; - } - - // If there are no content elements, it doesn't matter what is stored in layout - if (contentPublishedElements.Count == 0) - { - return createEmptyModel(); - } - - // Convert the settings data - var settingsPublishedElements = new Dictionary(); - var validSettingsElementTypes = blockConfigMap.Values.Select(x => x.SettingsElementTypeKey) - .Where(x => x.HasValue).Distinct().ToList(); - foreach (BlockItemData data in converted.BlockValue.SettingsData) - { - if (!validSettingsElementTypes.Contains(data.ContentTypeKey)) - { - continue; - } - - IPublishedElement? element = BlockEditorConverter.ConvertToElement(data, referenceCacheLevel, preview); - if (element is null) - { - continue; - } - - settingsPublishedElements[element.Key] = element; - } - - BlockItemActivator blockItemActivator = CreateBlockItemActivator(); - - TBlockItemModel? CreateBlockItem(TBlockLayoutItem layoutItem) - { - // Get the content reference - var contentGuidUdi = (GuidUdi?)layoutItem.ContentUdi; - if (contentGuidUdi is null || - !contentPublishedElements.TryGetValue(contentGuidUdi.Guid, out IPublishedElement? contentData)) - { - return null; - } - - if (!blockConfigMap.TryGetValue( - contentData.ContentType.Key, - out TBlockConfiguration? blockConfig)) - { - return null; - } - - // Get the setting reference - IPublishedElement? settingsData = null; - var settingGuidUdi = (GuidUdi?)layoutItem.SettingsUdi; - if (settingGuidUdi is not null) - { - settingsPublishedElements.TryGetValue(settingGuidUdi.Guid, out settingsData); - } - - // This can happen if they have a settings type, save content, remove the settings type, and display the front-end page before saving the content again - // We also ensure that the content types match, since maybe the settings type has been changed after this has been persisted - if (settingsData is not null && (!blockConfig.SettingsElementTypeKey.HasValue || - settingsData.ContentType.Key != blockConfig.SettingsElementTypeKey)) - { - settingsData = null; - } - - // Create instance (use content/settings type from configuration) - TBlockItemModel? blockItem = blockItemActivator.CreateInstance(blockConfig.ContentElementTypeKey, blockConfig.SettingsElementTypeKey, contentGuidUdi, contentData, settingGuidUdi, settingsData); - if (blockItem == null) - { - return null; - } - - if (enrichBlockItem != null) - { - blockItem = enrichBlockItem(blockItem, layoutItem, blockConfig, CreateBlockItem); - } - - return blockItem; - } - - var blockItems = layout.Select(CreateBlockItem).WhereNotNull().ToList(); - return createModel(blockItems); - } - - // Cache constructors locally (it's tied to the current IPublishedSnapshot and IPublishedModelFactory) - protected abstract class BlockItemActivator - { - protected abstract Type GenericItemType { get; } - - private readonly BlockEditorConverter _blockConverter; - - private readonly - Dictionary<(Guid, Guid?), Func> - _constructorCache = new(); - - public BlockItemActivator(BlockEditorConverter blockConverter) - => _blockConverter = blockConverter; - - public T CreateInstance(Guid contentTypeKey, Guid? settingsTypeKey, Udi contentUdi, IPublishedElement contentData, Udi? settingsUdi, IPublishedElement? settingsData) - { - if (!_constructorCache.TryGetValue( - (contentTypeKey, settingsTypeKey), - out Func? constructor)) - { - constructor = _constructorCache[(contentTypeKey, settingsTypeKey)] = - EmitConstructor(contentTypeKey, settingsTypeKey); - } - - return constructor(contentUdi, contentData, settingsUdi, settingsData); - } - - private Func EmitConstructor( - Guid contentTypeKey, Guid? settingsTypeKey) - { - Type contentType = _blockConverter.GetModelType(contentTypeKey); - Type settingsType = settingsTypeKey.HasValue - ? _blockConverter.GetModelType(settingsTypeKey.Value) - : typeof(IPublishedElement); - Type type = GenericItemType.MakeGenericType(contentType, settingsType); - - ConstructorInfo? constructor = - type.GetConstructor(new[] { typeof(Udi), contentType, typeof(Udi), settingsType }); - if (constructor == null) - { - throw new InvalidOperationException($"Could not find the required public constructor on {type}."); - } - - // We use unsafe here, because we know the constructor parameter count and types match - return ReflectionUtilities - .EmitConstructorUnsafe>( - constructor); - } - } -} diff --git a/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/BlockPropertyValueCreatorBase.cs b/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/BlockPropertyValueCreatorBase.cs index 45d4cdff2b4a..b7dca9151527 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/BlockPropertyValueCreatorBase.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/BlockPropertyValueCreatorBase.cs @@ -15,6 +15,9 @@ internal abstract class BlockPropertyValueCreatorBase, new() { + private readonly IVariationContextAccessor _variationContextAccessor; + private readonly BlockEditorVarianceHandler _blockEditorVarianceHandler; + /// /// Creates a specific data converter for the block property implementation. /// @@ -57,11 +60,17 @@ internal abstract class BlockPropertyValueCreatorBase protected delegate TBlockItemModel? EnrichBlockItemModelFromConfiguration(TBlockItemModel item, TBlockLayoutItem layoutItem, TBlockConfiguration configuration, CreateBlockItemModelFromLayout blockItemModelCreator); - protected BlockPropertyValueCreatorBase(BlockEditorConverter blockEditorConverter) => BlockEditorConverter = blockEditorConverter; + protected BlockPropertyValueCreatorBase(BlockEditorConverter blockEditorConverter, IVariationContextAccessor variationContextAccessor, BlockEditorVarianceHandler blockEditorVarianceHandler) + { + BlockEditorConverter = blockEditorConverter; + _variationContextAccessor = variationContextAccessor; + _blockEditorVarianceHandler = blockEditorVarianceHandler; + } protected BlockEditorConverter BlockEditorConverter { get; } protected TBlockModel CreateBlockModel( + IPublishedElement owner, PropertyCacheLevel referenceCacheLevel, string intermediateBlockModelValue, bool preview, @@ -78,10 +87,11 @@ protected TBlockModel CreateBlockModel( BlockEditorDataConverter blockEditorDataConverter = CreateBlockEditorDataConverter(); BlockEditorData converted = blockEditorDataConverter.Deserialize(intermediateBlockModelValue); - return CreateBlockModel(referenceCacheLevel, converted, preview, blockConfigurations, createEmptyModel, createModelFromItems, enrichBlockItem); + return CreateBlockModel(owner, referenceCacheLevel, converted, preview, blockConfigurations, createEmptyModel, createModelFromItems, enrichBlockItem); } protected TBlockModel CreateBlockModel( + IPublishedElement owner, PropertyCacheLevel referenceCacheLevel, TBlockValue blockValue, bool preview, @@ -92,10 +102,11 @@ protected TBlockModel CreateBlockModel( { BlockEditorDataConverter blockEditorDataConverter = CreateBlockEditorDataConverter(); BlockEditorData converted = blockEditorDataConverter.Convert(blockValue); - return CreateBlockModel(referenceCacheLevel, converted, preview, blockConfigurations, createEmptyModel, createModelFromItems, enrichBlockItem); + return CreateBlockModel(owner, referenceCacheLevel, converted, preview, blockConfigurations, createEmptyModel, createModelFromItems, enrichBlockItem); } private TBlockModel CreateBlockModel( + IPublishedElement owner, PropertyCacheLevel referenceCacheLevel, BlockEditorData converted, bool preview, @@ -115,6 +126,7 @@ private TBlockModel CreateBlockModel( } var blockConfigMap = blockConfigurations.ToDictionary(bc => bc.ContentElementTypeKey); + VariationContext variationContext = _variationContextAccessor.VariationContext ?? new VariationContext(); // Convert the content data var contentPublishedElements = new Dictionary(); @@ -125,12 +137,28 @@ private TBlockModel CreateBlockModel( continue; } - IPublishedElement? element = BlockEditorConverter.ConvertToElement(data, referenceCacheLevel, preview); + IPublishedElement? element = BlockEditorConverter.ConvertToElement(owner, data, referenceCacheLevel, preview); if (element == null) { continue; } + // if case changes have been made to the content or element type variation since the content was published, + // we need to align those changes for the exposed blocks. + IEnumerable expose = _blockEditorVarianceHandler.AlignedExposeVarianceAsync(converted.BlockValue, owner, element).GetAwaiter().GetResult(); + var expectedBlockVariationCulture = owner.ContentType.VariesByCulture() && element.ContentType.VariesByCulture() + ? variationContext.Culture.NullOrWhiteSpaceAsNull() + : null; + var expectedBlockVariationSegment = owner.ContentType.VariesBySegment() && element.ContentType.VariesBySegment() + ? variationContext.Segment.NullOrWhiteSpaceAsNull() + : null; + if (expose.Any(v => + v.ContentKey == element.Key && v.Culture == expectedBlockVariationCulture && + v.Segment == expectedBlockVariationSegment) is false) + { + continue; + } + contentPublishedElements[element.Key] = element; } @@ -151,7 +179,7 @@ private TBlockModel CreateBlockModel( continue; } - IPublishedElement? element = BlockEditorConverter.ConvertToElement(data, referenceCacheLevel, preview); + IPublishedElement? element = BlockEditorConverter.ConvertToElement(owner, data, referenceCacheLevel, preview); if (element is null) { continue; @@ -165,9 +193,7 @@ private TBlockModel CreateBlockModel( TBlockItemModel? CreateBlockItem(TBlockLayoutItem layoutItem) { // Get the content reference - var contentGuidUdi = (GuidUdi?)layoutItem.ContentUdi; - if (contentGuidUdi is null || - !contentPublishedElements.TryGetValue(contentGuidUdi.Guid, out IPublishedElement? contentData)) + if (!contentPublishedElements.TryGetValue(layoutItem.ContentKey, out IPublishedElement? contentData)) { return null; } @@ -181,10 +207,9 @@ private TBlockModel CreateBlockModel( // Get the setting reference IPublishedElement? settingsData = null; - var settingGuidUdi = (GuidUdi?)layoutItem.SettingsUdi; - if (settingGuidUdi is not null) + if (layoutItem.SettingsKey.HasValue) { - settingsPublishedElements.TryGetValue(settingGuidUdi.Guid, out settingsData); + settingsPublishedElements.TryGetValue(layoutItem.SettingsKey.Value, out settingsData); } // This can happen if they have a settings type, save content, remove the settings type, and display the front-end page before saving the content again @@ -196,7 +221,7 @@ private TBlockModel CreateBlockModel( } // Create instance (use content/settings type from configuration) - var blockItem = blockItemActivator.CreateInstance(blockConfig.ContentElementTypeKey, blockConfig.SettingsElementTypeKey, contentGuidUdi, contentData, settingGuidUdi, settingsData); + var blockItem = blockItemActivator.CreateInstance(blockConfig.ContentElementTypeKey, blockConfig.SettingsElementTypeKey, layoutItem.ContentKey, contentData, layoutItem.SettingsKey, settingsData); if (blockItem == null) { return null; @@ -230,20 +255,20 @@ public BlockItemActivator(BlockEditorConverter blockConverter, BlockEditorProper _constructorCache = constructorCache; } - public T CreateInstance(Guid contentTypeKey, Guid? settingsTypeKey, Udi contentUdi, IPublishedElement contentData, Udi? settingsUdi, IPublishedElement? settingsData) + public T CreateInstance(Guid contentTypeKey, Guid? settingsTypeKey, Guid contentKey, IPublishedElement contentData, Guid? settingsKey, IPublishedElement? settingsData) { if (!_constructorCache.TryGetValue( (contentTypeKey, settingsTypeKey), - out Func? constructor)) + out Func? constructor)) { constructor = EmitConstructor(contentTypeKey, settingsTypeKey); _constructorCache.SetValue((contentTypeKey, settingsTypeKey), constructor); } - return constructor(contentUdi, contentData, settingsUdi, settingsData); + return constructor(contentKey, contentData, settingsKey, settingsData); } - private Func EmitConstructor( + private Func EmitConstructor( Guid contentTypeKey, Guid? settingsTypeKey) { Type contentType = _blockConverter.GetModelType(contentTypeKey); @@ -253,7 +278,7 @@ public T CreateInstance(Guid contentTypeKey, Guid? settingsTypeKey, Udi contentU Type type = GenericItemType.MakeGenericType(contentType, settingsType); ConstructorInfo? constructor = - type.GetConstructor(new[] { typeof(Udi), contentType, typeof(Udi), settingsType }); + type.GetConstructor(new[] { typeof(Guid), contentType, typeof(Guid?), settingsType }); if (constructor == null) { throw new InvalidOperationException($"Could not find the required public constructor on {type}."); @@ -261,7 +286,7 @@ public T CreateInstance(Guid contentTypeKey, Guid? settingsTypeKey, Udi contentU // We use unsafe here, because we know the constructor parameter count and types match return ReflectionUtilities - .EmitConstructorUnsafe>( + .EmitConstructorUnsafe>( constructor); } } diff --git a/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/RichTextBlockPropertyValueCreator.cs b/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/RichTextBlockPropertyValueCreator.cs index 9ec91fa0deb4..241b5825e2fb 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/RichTextBlockPropertyValueCreator.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/RichTextBlockPropertyValueCreator.cs @@ -2,6 +2,7 @@ // See LICENSE for more details. using Umbraco.Cms.Core.Models.Blocks; +using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Cms.Core.Serialization; namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters; @@ -13,21 +14,23 @@ internal class RichTextBlockPropertyValueCreator : BlockPropertyValueCreatorBase public RichTextBlockPropertyValueCreator( BlockEditorConverter blockEditorConverter, + IVariationContextAccessor variationContextAccessor, + BlockEditorVarianceHandler blockEditorVarianceHandler, IJsonSerializer jsonSerializer, RichTextBlockPropertyValueConstructorCache constructorCache) - : base(blockEditorConverter) + : base(blockEditorConverter, variationContextAccessor, blockEditorVarianceHandler) { _jsonSerializer = jsonSerializer; _constructorCache = constructorCache; } - public RichTextBlockModel CreateBlockModel(PropertyCacheLevel referenceCacheLevel, RichTextBlockValue blockValue, bool preview, RichTextConfiguration.RichTextBlockConfiguration[] blockConfigurations) + public RichTextBlockModel CreateBlockModel(IPublishedElement owner, PropertyCacheLevel referenceCacheLevel, RichTextBlockValue blockValue, bool preview, RichTextConfiguration.RichTextBlockConfiguration[] blockConfigurations) { RichTextBlockModel CreateEmptyModel() => RichTextBlockModel.Empty; RichTextBlockModel CreateModel(IList items) => new RichTextBlockModel(items); - RichTextBlockModel blockModel = CreateBlockModel(referenceCacheLevel, blockValue, preview, blockConfigurations, CreateEmptyModel, CreateModel); + RichTextBlockModel blockModel = CreateBlockModel(owner, referenceCacheLevel, blockValue, preview, blockConfigurations, CreateEmptyModel, CreateModel); return blockModel; } diff --git a/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/RichTextParsingRegexes.cs b/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/RichTextParsingRegexes.cs index 2d4c19c17a49..1041b6922d5a 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/RichTextParsingRegexes.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/RichTextParsingRegexes.cs @@ -4,6 +4,6 @@ namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters; internal static partial class RichTextParsingRegexes { - [GeneratedRegex(".[^\"]*)\">(?:)?<\\/umb-rte-block(?:-inline)?>")] + [GeneratedRegex(".[^\"]*)\">(?:)?<\\/umb-rte-block(?:-inline)?>")] public static partial Regex BlockRegex(); } diff --git a/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/RteBlockRenderingValueConverter.cs b/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/RteBlockRenderingValueConverter.cs index 8a0ec3ba4aef..58bf1bf05b50 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/RteBlockRenderingValueConverter.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/RteBlockRenderingValueConverter.cs @@ -4,6 +4,7 @@ using System.Globalization; using System.Text.RegularExpressions; using HtmlAgilityPack; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Umbraco.Cms.Core.Blocks; @@ -13,6 +14,7 @@ using Umbraco.Cms.Core.Strings; using Umbraco.Cms.Core.Templates; using Umbraco.Cms.Core.DeliveryApi; +using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.Models.Blocks; using Umbraco.Cms.Core.Models.DeliveryApi; using Umbraco.Cms.Core.Serialization; @@ -39,13 +41,27 @@ public class RteBlockRenderingValueConverter : SimpleTinyMceValueConverter, IDel private readonly ILogger _logger; private readonly IApiElementBuilder _apiElementBuilder; private readonly RichTextBlockPropertyValueConstructorCache _constructorCache; + private readonly IVariationContextAccessor _variationContextAccessor; + private readonly BlockEditorVarianceHandler _blockEditorVarianceHandler; private DeliveryApiSettings _deliveryApiSettings; + [Obsolete("Use the constructor that takes all parameters, scheduled for removal in V16")] public RteBlockRenderingValueConverter(HtmlLocalLinkParser linkParser, HtmlUrlParser urlParser, HtmlImageSourceParser imageSourceParser, IApiRichTextElementParser apiRichTextElementParser, IApiRichTextMarkupParser apiRichTextMarkupParser, IPartialViewBlockEngine partialViewBlockEngine, BlockEditorConverter blockEditorConverter, IJsonSerializer jsonSerializer, IApiElementBuilder apiElementBuilder, RichTextBlockPropertyValueConstructorCache constructorCache, ILogger logger, IOptionsMonitor deliveryApiSettingsMonitor) + : this(linkParser, urlParser, imageSourceParser, apiRichTextElementParser, apiRichTextMarkupParser, partialViewBlockEngine, blockEditorConverter, jsonSerializer, + apiElementBuilder, constructorCache, logger, StaticServiceProvider.Instance.GetRequiredService(), + StaticServiceProvider.Instance.GetRequiredService(), deliveryApiSettingsMonitor) + { + } + + public RteBlockRenderingValueConverter(HtmlLocalLinkParser linkParser, HtmlUrlParser urlParser, HtmlImageSourceParser imageSourceParser, + IApiRichTextElementParser apiRichTextElementParser, IApiRichTextMarkupParser apiRichTextMarkupParser, + IPartialViewBlockEngine partialViewBlockEngine, BlockEditorConverter blockEditorConverter, IJsonSerializer jsonSerializer, + IApiElementBuilder apiElementBuilder, RichTextBlockPropertyValueConstructorCache constructorCache, ILogger logger, + IVariationContextAccessor variationContextAccessor, BlockEditorVarianceHandler blockEditorVarianceHandler, IOptionsMonitor deliveryApiSettingsMonitor) { _linkParser = linkParser; _urlParser = urlParser; @@ -58,6 +74,8 @@ public RteBlockRenderingValueConverter(HtmlLocalLinkParser linkParser, HtmlUrlPa _apiElementBuilder = apiElementBuilder; _constructorCache = constructorCache; _logger = logger; + _variationContextAccessor = variationContextAccessor; + _blockEditorVarianceHandler = blockEditorVarianceHandler; _deliveryApiSettings = deliveryApiSettingsMonitor.CurrentValue; deliveryApiSettingsMonitor.OnChange(settings => _deliveryApiSettings = settings); } @@ -78,7 +96,7 @@ public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType // the reference cache level is .Element here, as is also the case when rendering at property level. RichTextBlockModel? richTextBlockModel = richTextEditorValue.Blocks is not null - ? ParseRichTextBlockModel(richTextEditorValue.Blocks, propertyType, PropertyCacheLevel.Element, preview) + ? ParseRichTextBlockModel(owner, richTextEditorValue.Blocks, propertyType, PropertyCacheLevel.Element, preview) : null; return new RichTextEditorIntermediateValue @@ -181,7 +199,7 @@ public Type GetDeliveryApiPropertyValueType(IPublishedPropertyType propertyType) return sourceString; } - private RichTextBlockModel? ParseRichTextBlockModel(RichTextBlockValue blocks, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, bool preview) + private RichTextBlockModel? ParseRichTextBlockModel(IPublishedElement owner, RichTextBlockValue blocks, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, bool preview) { RichTextConfiguration? configuration = propertyType.DataType.ConfigurationAs(); if (configuration?.Blocks?.Any() is not true) @@ -189,8 +207,8 @@ public Type GetDeliveryApiPropertyValueType(IPublishedPropertyType propertyType) return null; } - var creator = new RichTextBlockPropertyValueCreator(_blockEditorConverter, _jsonSerializer, _constructorCache); - return creator.CreateBlockModel(referenceCacheLevel, blocks, preview, configuration.Blocks); + var creator = new RichTextBlockPropertyValueCreator(_blockEditorConverter, _variationContextAccessor, _blockEditorVarianceHandler, _jsonSerializer, _constructorCache); + return creator.CreateBlockModel(owner, referenceCacheLevel, blocks, preview, configuration.Blocks); } private string RenderRichTextBlockModel(string source, RichTextBlockModel? richTextBlockModel) @@ -200,10 +218,10 @@ private string RenderRichTextBlockModel(string source, RichTextBlockModel? richT return source; } - var blocksByUdi = richTextBlockModel.ToDictionary(block => block.ContentUdi); + var blocksByKey = richTextBlockModel.ToDictionary(block => block.ContentKey); string RenderBlock(Match match) => - UdiParser.TryParse(match.Groups["udi"].Value, out Udi? udi) && blocksByUdi.TryGetValue(udi, out RichTextBlockItem? richTextBlockItem) + Guid.TryParse(match.Groups["key"].Value, out Guid key) && blocksByKey.TryGetValue(key, out RichTextBlockItem? richTextBlockItem) ? _partialViewBlockEngine.ExecuteAsync(richTextBlockItem).GetAwaiter().GetResult() : string.Empty; diff --git a/src/Umbraco.Infrastructure/Serialization/JsonBlockValueConverter.cs b/src/Umbraco.Infrastructure/Serialization/JsonBlockValueConverter.cs index 1e6f9e6898fe..6716dfae7108 100644 --- a/src/Umbraco.Infrastructure/Serialization/JsonBlockValueConverter.cs +++ b/src/Umbraco.Infrastructure/Serialization/JsonBlockValueConverter.cs @@ -61,6 +61,9 @@ public override BlockValue Read(ref Utf8JsonReader reader, Type typeToConvert, J case nameof(BlockValue.Layout): DeserializeAndSetLayout(ref reader, options, typeToConvert, blockValue); break; + case nameof(BlockValue.Expose): + blockValue.Expose = DeserializeBlockVariation(ref reader, options, typeToConvert, nameof(BlockValue.Expose)); + break; } } } @@ -84,6 +87,9 @@ public override void Write(Utf8JsonWriter writer, BlockValue value, JsonSerializ JsonSerializer.Serialize(writer, value.SettingsData, options); } + writer.WritePropertyName(nameof(BlockValue.Expose).ToFirstLowerInvariant()); + JsonSerializer.Serialize(writer, value.Expose, options); + Type layoutItemType = GetLayoutItemType(value.GetType()); writer.WriteStartObject(nameof(BlockValue.Layout)); @@ -115,7 +121,13 @@ private static Type GetLayoutItemType(Type blockValueType) } private List DeserializeBlockItemData(ref Utf8JsonReader reader, JsonSerializerOptions options, Type typeToConvert, string propertyName) - => JsonSerializer.Deserialize>(ref reader, options) + => DeserializeListOf(ref reader, options, typeToConvert, propertyName); + + private List DeserializeBlockVariation(ref Utf8JsonReader reader, JsonSerializerOptions options, Type typeToConvert, string propertyName) + => DeserializeListOf(ref reader, options, typeToConvert, propertyName); + + private List DeserializeListOf(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) @@ -167,12 +179,12 @@ private void DeserializeAndSetLayout(ref Utf8JsonReader reader, JsonSerializerOp } // did we encounter the concrete block value? - if (blockEditorAlias == blockValue.PropertyEditorAlias) + if (blockValue.SupportsBlockLayoutAlias(blockEditorAlias)) { // 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}."); + blockValue.Layout[blockValue.PropertyEditorAlias] = 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 { diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/ContentServiceTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/ContentServiceTests.cs index 8e4b836fee61..67612b575683 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/ContentServiceTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/ContentServiceTests.cs @@ -1290,7 +1290,7 @@ public async Task Can_Publish_And_Unpublish_Cultures_In_Single_Operation() content.SetCultureName("name-fr", langFr.IsoCode); content.SetCultureName("name-da", langDa.IsoCode); - content.PublishCulture(CultureImpact.Explicit(langFr.IsoCode, langFr.IsDefault)); + content.PublishCulture(CultureImpact.Explicit(langFr.IsoCode, langFr.IsDefault), DateTime.Now, PropertyEditorCollection); var result = ContentService.CommitDocumentChanges(content); Assert.IsTrue(result.Success); content = ContentService.GetById(content.Id); @@ -1298,7 +1298,7 @@ public async Task Can_Publish_And_Unpublish_Cultures_In_Single_Operation() Assert.IsFalse(content.IsCulturePublished(langDa.IsoCode)); content.UnpublishCulture(langFr.IsoCode); - content.PublishCulture(CultureImpact.Explicit(langDa.IsoCode, langDa.IsDefault)); + content.PublishCulture(CultureImpact.Explicit(langDa.IsoCode, langDa.IsDefault), DateTime.Now, PropertyEditorCollection); result = ContentService.CommitDocumentChanges(content); Assert.IsTrue(result.Success); diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/DocumentRepositoryTest.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/DocumentRepositoryTest.cs index f4cee3997894..e6ce8d467ece 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/DocumentRepositoryTest.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/DocumentRepositoryTest.cs @@ -60,6 +60,8 @@ public void SetUpData() private FileSystems FileSystems => GetRequiredService(); + private PropertyEditorCollection PropertyEditorCollection => GetRequiredService(); + private IConfigurationEditorJsonSerializer ConfigurationEditorJsonSerializer => GetRequiredService(); @@ -224,7 +226,7 @@ public void CreateVersions() // publish = new edit version content1.SetValue("title", "title"); - content1.PublishCulture(CultureImpact.Invariant); + content1.PublishCulture(CultureImpact.Invariant, DateTime.Now, PropertyEditorCollection); content1.PublishedState = PublishedState.Publishing; repository.Save(content1); @@ -300,7 +302,7 @@ public void CreateVersions() new { id = content1.Id })); // publish = version - content1.PublishCulture(CultureImpact.Invariant); + content1.PublishCulture(CultureImpact.Invariant, DateTime.Now, PropertyEditorCollection); content1.PublishedState = PublishedState.Publishing; repository.Save(content1); @@ -344,7 +346,7 @@ public void CreateVersions() // publish = new version content1.Name = "name-4"; content1.SetValue("title", "title-4"); - content1.PublishCulture(CultureImpact.Invariant); + content1.PublishCulture(CultureImpact.Invariant, DateTime.Now, PropertyEditorCollection); content1.PublishedState = PublishedState.Publishing; repository.Save(content1); @@ -764,7 +766,7 @@ public void GetAllContentManyVersions() // publish them all foreach (var content in result) { - content.PublishCulture(CultureImpact.Invariant); + content.PublishCulture(CultureImpact.Invariant, DateTime.Now, PropertyEditorCollection); repository.Save(content); } diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/PropertyEditors/BlockEditorBackwardsCompatibilityTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/PropertyEditors/BlockEditorBackwardsCompatibilityTests.cs new file mode 100644 index 000000000000..c4acaf7b01f2 --- /dev/null +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/PropertyEditors/BlockEditorBackwardsCompatibilityTests.cs @@ -0,0 +1,522 @@ +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 BlockEditorBackwardsCompatibilityTests : UmbracoIntegrationTest +{ + private IContentTypeService ContentTypeService => GetRequiredService(); + + private IContentService ContentService => GetRequiredService(); + + private IDataTypeService DataTypeService => GetRequiredService(); + + private PropertyEditorCollection PropertyEditorCollection => GetRequiredService(); + + private IConfigurationEditorJsonSerializer ConfigurationEditorJsonSerializer => GetRequiredService(); + + [TestCase] + public async Task BlockListIsBackwardsCompatible() + { + var elementType = await CreateElementType(); + var blockListDataType = await CreateBlockListDataType(elementType); + var contentType = await CreateContentType(blockListDataType); + + var json = $$""" + { + "layout": { + "{{Constants.PropertyEditors.Aliases.BlockList}}": [ + { + "contentUdi": "umb://element/1304e1ddac87439684fe8a399231cb3d", + "settingsUdi": "umb://element/1f613e26ce274898908a561437af5100" + }, + { + "contentUdi": "umb://element/0a4a416e547d464fabcc6f345c17809a", + "settingsUdi": "umb://element/63027539b0db45e7b70459762d4e83dd" + } + ] + }, + "contentData": [ + { + "contentTypeKey": "{{elementType.Key}}", + "udi": "umb://element/1304e1ddac87439684fe8a399231cb3d", + "title": "Content Title One", + "text": "Content Text One" + }, + { + "contentTypeKey": "{{elementType.Key}}", + "udi": "umb://element/0a4a416e547d464fabcc6f345c17809a", + "title": "Content Title Two", + "text": "Content Text Two" + } + ], + "settingsData": [ + { + "contentTypeKey": "{{elementType.Key}}", + "udi": "umb://element/1f613e26ce274898908a561437af5100", + "title": "Settings Title One", + "text": "Settings Text One" + }, + { + "contentTypeKey": "{{elementType.Key}}", + "udi": "umb://element/63027539b0db45e7b70459762d4e83dd", + "title": "Settings Title Two", + "text": "Settings Text Two" + } + ] + } + """; + + var contentBuilder = new ContentBuilder() + .WithContentType(contentType) + .WithName("Home"); + + var content = contentBuilder.Build(); + content.Properties["blocks"]!.SetValue(json); + ContentService.Save(content); + + var toEditor = blockListDataType.Editor!.GetValueEditor().ToEditor(content.Properties["blocks"]!) as BlockListValue; + Assert.IsNotNull(toEditor); + + Assert.Multiple(() => + { + Assert.AreEqual(2, toEditor.ContentData.Count); + + Assert.AreEqual("1304e1ddac87439684fe8a399231cb3d", toEditor.ContentData[0].Key.ToString("N")); + Assert.AreEqual("0a4a416e547d464fabcc6f345c17809a", toEditor.ContentData[1].Key.ToString("N")); + + AssertValueEquals(toEditor.ContentData[0], "title", "Content Title One"); + AssertValueEquals(toEditor.ContentData[0], "text", "Content Text One"); + AssertValueEquals(toEditor.ContentData[1], "title", "Content Title Two"); + AssertValueEquals(toEditor.ContentData[1], "text", "Content Text Two"); + + Assert.IsFalse(toEditor.ContentData[0].RawPropertyValues.Any()); + Assert.IsFalse(toEditor.ContentData[1].RawPropertyValues.Any()); + }); + + Assert.Multiple(() => + { + Assert.AreEqual(2, toEditor.SettingsData.Count); + + Assert.AreEqual("1f613e26ce274898908a561437af5100", toEditor.SettingsData[0].Key.ToString("N")); + Assert.AreEqual("63027539b0db45e7b70459762d4e83dd", toEditor.SettingsData[1].Key.ToString("N")); + + AssertValueEquals(toEditor.SettingsData[0], "title", "Settings Title One"); + AssertValueEquals(toEditor.SettingsData[0], "text", "Settings Text One"); + AssertValueEquals(toEditor.SettingsData[1], "title", "Settings Title Two"); + AssertValueEquals(toEditor.SettingsData[1], "text", "Settings Text Two"); + + Assert.IsFalse(toEditor.SettingsData[0].RawPropertyValues.Any()); + Assert.IsFalse(toEditor.SettingsData[1].RawPropertyValues.Any()); + }); + + Assert.Multiple(() => + { + Assert.AreEqual(2, toEditor.Expose.Count); + + Assert.AreEqual("1304e1ddac87439684fe8a399231cb3d", toEditor.Expose[0].ContentKey.ToString("N")); + Assert.AreEqual("0a4a416e547d464fabcc6f345c17809a", toEditor.Expose[1].ContentKey.ToString("N")); + }); + } + + [TestCase] + public async Task BlockGridIsBackwardsCompatible() + { + var elementType = await CreateElementType(); + var gridAreaKey = Guid.NewGuid(); + var blockGridDataType = await CreateBlockGridDataType(elementType, gridAreaKey); + var contentType = await CreateContentType(blockGridDataType); + + var json = $$""" + { + "layout": { + "{{Constants.PropertyEditors.Aliases.BlockGrid}}": [ + { + "contentUdi": "umb://element/1304e1ddac87439684fe8a399231cb3d", + "settingsUdi": "umb://element/1f613e26ce274898908a561437af5100", + "columnSpan": 12, + "rowSpan": 1, + "areas": [{ + "key": "{{gridAreaKey}}", + "items": [{ + "contentUdi": "umb://element/5fc866c590be4d01a28a979472a1ffee", + "areas": [], + "columnSpan": 12, + "rowSpan": 1 + }] + }] + }, + { + "contentUdi": "umb://element/0a4a416e547d464fabcc6f345c17809a", + "settingsUdi": "umb://element/63027539b0db45e7b70459762d4e83dd", + "columnSpan": 12, + "rowSpan": 1, + "areas": [{ + "key": "{{gridAreaKey}}", + "items": [{ + "contentUdi": "umb://element/264536b65b0f4641aa43d4bfb515831d", + "areas": [], + "columnSpan": 12, + "rowSpan": 1 + }] + }] + } + ] + }, + "contentData": [ + { + "contentTypeKey": "{{elementType.Key}}", + "udi": "umb://element/1304e1ddac87439684fe8a399231cb3d", + "title": "Content Title One", + "text": "Content Text One" + }, + { + "contentTypeKey": "{{elementType.Key}}", + "udi": "umb://element/0a4a416e547d464fabcc6f345c17809a", + "title": "Content Title Two", + "text": "Content Text Two" + }, + { + "contentTypeKey": "{{elementType.Key}}", + "udi": "umb://element/5fc866c590be4d01a28a979472a1ffee", + "title": "Content Area Title One", + "text": "Content Area Text One" + }, + { + "contentTypeKey": "{{elementType.Key}}", + "udi": "umb://element/264536b65b0f4641aa43d4bfb515831d", + "title": "Content Area Title Two", + "text": "Content Area Text Two" + } + ], + "settingsData": [ + { + "contentTypeKey": "{{elementType.Key}}", + "udi": "umb://element/1f613e26ce274898908a561437af5100", + "title": "Settings Title One", + "text": "Settings Text One" + }, + { + "contentTypeKey": "{{elementType.Key}}", + "udi": "umb://element/63027539b0db45e7b70459762d4e83dd", + "title": "Settings Title Two", + "text": "Settings Text Two" + } + ] + } + """; + + var contentBuilder = new ContentBuilder() + .WithContentType(contentType) + .WithName("Home"); + + var content = contentBuilder.Build(); + content.Properties["blocks"]!.SetValue(json); + ContentService.Save(content); + + var toEditor = blockGridDataType.Editor!.GetValueEditor().ToEditor(content.Properties["blocks"]!) as BlockGridValue; + Assert.IsNotNull(toEditor); + + Assert.AreEqual(4, toEditor.ContentData.Count); + Assert.Multiple(() => + { + Assert.AreEqual("1304e1ddac87439684fe8a399231cb3d", toEditor.ContentData[0].Key.ToString("N")); + Assert.AreEqual("0a4a416e547d464fabcc6f345c17809a", toEditor.ContentData[1].Key.ToString("N")); + Assert.AreEqual("5fc866c590be4d01a28a979472a1ffee", toEditor.ContentData[2].Key.ToString("N")); + Assert.AreEqual("264536b65b0f4641aa43d4bfb515831d", toEditor.ContentData[3].Key.ToString("N")); + + AssertValueEquals(toEditor.ContentData[0], "title", "Content Title One"); + AssertValueEquals(toEditor.ContentData[0], "text", "Content Text One"); + AssertValueEquals(toEditor.ContentData[1], "title", "Content Title Two"); + AssertValueEquals(toEditor.ContentData[1], "text", "Content Text Two"); + AssertValueEquals(toEditor.ContentData[2], "title", "Content Area Title One"); + AssertValueEquals(toEditor.ContentData[2], "text", "Content Area Text One"); + AssertValueEquals(toEditor.ContentData[3], "title", "Content Area Title Two"); + AssertValueEquals(toEditor.ContentData[3], "text", "Content Area Text Two"); + + Assert.IsFalse(toEditor.ContentData[0].RawPropertyValues.Any()); + Assert.IsFalse(toEditor.ContentData[1].RawPropertyValues.Any()); + Assert.IsFalse(toEditor.ContentData[2].RawPropertyValues.Any()); + Assert.IsFalse(toEditor.ContentData[3].RawPropertyValues.Any()); + }); + + Assert.AreEqual(2, toEditor.SettingsData.Count); + Assert.Multiple(() => + { + Assert.AreEqual("1f613e26ce274898908a561437af5100", toEditor.SettingsData[0].Key.ToString("N")); + Assert.AreEqual("63027539b0db45e7b70459762d4e83dd", toEditor.SettingsData[1].Key.ToString("N")); + + AssertValueEquals(toEditor.SettingsData[0], "title", "Settings Title One"); + AssertValueEquals(toEditor.SettingsData[0], "text", "Settings Text One"); + AssertValueEquals(toEditor.SettingsData[1], "title", "Settings Title Two"); + AssertValueEquals(toEditor.SettingsData[1], "text", "Settings Text Two"); + + Assert.IsFalse(toEditor.SettingsData[0].RawPropertyValues.Any()); + Assert.IsFalse(toEditor.SettingsData[1].RawPropertyValues.Any()); + }); + + Assert.Multiple(() => + { + Assert.AreEqual(4, toEditor.Expose.Count); + + Assert.AreEqual("1304e1ddac87439684fe8a399231cb3d", toEditor.Expose[0].ContentKey.ToString("N")); + Assert.AreEqual("0a4a416e547d464fabcc6f345c17809a", toEditor.Expose[1].ContentKey.ToString("N")); + Assert.AreEqual("5fc866c590be4d01a28a979472a1ffee", toEditor.Expose[2].ContentKey.ToString("N")); + Assert.AreEqual("264536b65b0f4641aa43d4bfb515831d", toEditor.Expose[3].ContentKey.ToString("N")); + }); + } + + [TestCase] + public async Task RichTextIsBackwardsCompatible() + { + var elementType = await CreateElementType(); + var richTextDataType = await CreateRichTextDataType(elementType); + var contentType = await CreateContentType(richTextDataType); + + var json = $$""" + { + "markup": "

huh?

", + "blocks": { + "layout": { + "{{Constants.PropertyEditors.Aliases.TinyMce}}": [ + { + "contentUdi": "umb://element/1304e1ddac87439684fe8a399231cb3d", + "settingsUdi": "umb://element/1f613e26ce274898908a561437af5100" + }, + { + "contentUdi": "umb://element/0a4a416e547d464fabcc6f345c17809a", + "settingsUdi": "umb://element/63027539b0db45e7b70459762d4e83dd" + } + ] + }, + "contentData": [ + { + "contentTypeKey": "{{elementType.Key}}", + "udi": "umb://element/1304e1ddac87439684fe8a399231cb3d", + "title": "Content Title One", + "text": "Content Text One" + }, + { + "contentTypeKey": "{{elementType.Key}}", + "udi": "umb://element/0a4a416e547d464fabcc6f345c17809a", + "title": "Content Title Two", + "text": "Content Text Two" + } + ], + "settingsData": [ + { + "contentTypeKey": "{{elementType.Key}}", + "udi": "umb://element/1f613e26ce274898908a561437af5100", + "title": "Settings Title One", + "text": "Settings Text One" + }, + { + "contentTypeKey": "{{elementType.Key}}", + "udi": "umb://element/63027539b0db45e7b70459762d4e83dd", + "title": "Settings Title Two", + "text": "Settings Text Two" + } + ] + } + } + """; + + var contentBuilder = new ContentBuilder() + .WithContentType(contentType) + .WithName("Home"); + + var content = contentBuilder.Build(); + content.Properties["blocks"]!.SetValue(json); + ContentService.Save(content); + + var toEditor = richTextDataType.Editor!.GetValueEditor().ToEditor(content.Properties["blocks"]!) as RichTextEditorValue; + Assert.IsNotNull(toEditor); + Assert.IsNotNull(toEditor.Blocks); + + Assert.Multiple(() => + { + Assert.AreEqual(2, toEditor.Blocks.ContentData.Count); + + Assert.AreEqual("1304e1ddac87439684fe8a399231cb3d", toEditor.Blocks.ContentData[0].Key.ToString("N")); + Assert.AreEqual("0a4a416e547d464fabcc6f345c17809a", toEditor.Blocks.ContentData[1].Key.ToString("N")); + + AssertValueEquals(toEditor.Blocks.ContentData[0], "title", "Content Title One"); + AssertValueEquals(toEditor.Blocks.ContentData[0], "text", "Content Text One"); + AssertValueEquals(toEditor.Blocks.ContentData[1], "title", "Content Title Two"); + AssertValueEquals(toEditor.Blocks.ContentData[1], "text", "Content Text Two"); + + Assert.IsFalse(toEditor.Blocks.ContentData[0].RawPropertyValues.Any()); + Assert.IsFalse(toEditor.Blocks.ContentData[1].RawPropertyValues.Any()); + }); + + Assert.Multiple(() => + { + Assert.AreEqual(2, toEditor.Blocks.SettingsData.Count); + + Assert.AreEqual("1f613e26ce274898908a561437af5100", toEditor.Blocks.SettingsData[0].Key.ToString("N")); + Assert.AreEqual("63027539b0db45e7b70459762d4e83dd", toEditor.Blocks.SettingsData[1].Key.ToString("N")); + + AssertValueEquals(toEditor.Blocks.SettingsData[0], "title", "Settings Title One"); + AssertValueEquals(toEditor.Blocks.SettingsData[0], "text", "Settings Text One"); + AssertValueEquals(toEditor.Blocks.SettingsData[1], "title", "Settings Title Two"); + AssertValueEquals(toEditor.Blocks.SettingsData[1], "text", "Settings Text Two"); + + Assert.IsFalse(toEditor.Blocks.SettingsData[0].RawPropertyValues.Any()); + Assert.IsFalse(toEditor.Blocks.SettingsData[1].RawPropertyValues.Any()); + }); + + Assert.Multiple(() => + { + Assert.AreEqual(2, toEditor.Blocks.Expose.Count); + + Assert.AreEqual("1304e1ddac87439684fe8a399231cb3d", toEditor.Blocks.Expose[0].ContentKey.ToString("N")); + Assert.AreEqual("0a4a416e547d464fabcc6f345c17809a", toEditor.Blocks.Expose[1].ContentKey.ToString("N")); + }); + } + + private static void AssertValueEquals(BlockItemData blockItemData, string propertyAlias, string expectedValue) + { + var blockPropertyValue = blockItemData.Values.FirstOrDefault(v => v.Alias == propertyAlias); + Assert.IsNotNull(blockPropertyValue); + Assert.AreEqual(expectedValue, blockPropertyValue.Value); + } + + private async Task CreateElementType() + { + var elementType = new ContentTypeBuilder() + .WithAlias("myElementType") + .WithName("My Element Type") + .WithIsElement(true) + .AddPropertyType() + .WithAlias("title") + .WithName("Title") + .WithDataTypeId(Constants.DataTypes.Textbox) + .WithPropertyEditorAlias(Constants.PropertyEditors.Aliases.TextBox) + .WithValueStorageType(ValueStorageType.Nvarchar) + .Done() + .AddPropertyType() + .WithAlias("text") + .WithName("Text") + .WithDataTypeId(Constants.DataTypes.Textbox) + .WithPropertyEditorAlias(Constants.PropertyEditors.Aliases.TextBox) + .WithValueStorageType(ValueStorageType.Nvarchar) + .Done() + .Build(); + + await ContentTypeService.CreateAsync(elementType, Constants.Security.SuperUserKey); + return elementType; + } + + private async Task CreateBlockListDataType(IContentType elementType) + { + var dataType = new DataType(PropertyEditorCollection[Constants.PropertyEditors.Aliases.BlockList], ConfigurationEditorJsonSerializer) + { + ConfigurationData = new Dictionary + { + { + "blocks", + new BlockListConfiguration.BlockConfiguration[] + { + new() { ContentElementTypeKey = elementType.Key, SettingsElementTypeKey = elementType.Key } + } + } + }, + Name = "My Block List", + DatabaseType = ValueStorageType.Ntext, + ParentId = Constants.System.Root, + CreateDate = DateTime.UtcNow + }; + + await DataTypeService.CreateAsync(dataType, Constants.Security.SuperUserKey); + return dataType; + } + + private async Task CreateBlockGridDataType(IContentType elementType, Guid gridAreaKey) + { + var dataType = new DataType(PropertyEditorCollection[Constants.PropertyEditors.Aliases.BlockGrid], ConfigurationEditorJsonSerializer) + { + ConfigurationData = new Dictionary + { + { + "blocks", + new BlockGridConfiguration.BlockGridBlockConfiguration[] + { + new() + { + ContentElementTypeKey = elementType.Key, + SettingsElementTypeKey = elementType.Key, + AllowInAreas = true, + AllowAtRoot = true, + Areas = + [ + new BlockGridConfiguration.BlockGridAreaConfiguration + { + Key = gridAreaKey, + Alias = "areaOne" + } + ] + } + } + } + }, + Name = "My Block Grid", + DatabaseType = ValueStorageType.Ntext, + ParentId = Constants.System.Root, + CreateDate = DateTime.UtcNow + }; + + await DataTypeService.CreateAsync(dataType, Constants.Security.SuperUserKey); + return dataType; + } + + private async Task CreateRichTextDataType(IContentType elementType) + { + var dataType = new DataType(PropertyEditorCollection[Constants.PropertyEditors.Aliases.RichText], ConfigurationEditorJsonSerializer) + { + ConfigurationData = new Dictionary + { + { + "blocks", + new RichTextConfiguration.RichTextBlockConfiguration[] + { + new() { ContentElementTypeKey = elementType.Key, SettingsElementTypeKey = elementType.Key } + } + } + }, + Name = "My Rich Text", + DatabaseType = ValueStorageType.Ntext, + ParentId = Constants.System.Root, + CreateDate = DateTime.UtcNow + }; + + await DataTypeService.CreateAsync(dataType, Constants.Security.SuperUserKey); + return dataType; + } + + private async Task CreateContentType(IDataType blockEditorDataType) + { + var contentType = new ContentTypeBuilder() + .WithAlias("myPage") + .WithName("My Page") + .AddPropertyType() + .WithAlias("blocks") + .WithName("Blocks") + .WithDataTypeId(blockEditorDataType.Id) + .Done() + .Build(); + + await ContentTypeService.CreateAsync(contentType, Constants.Security.SuperUserKey); + return contentType; + } +} diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/PropertyEditors/BlockEditorElementVariationTestBase.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/PropertyEditors/BlockEditorElementVariationTestBase.cs new file mode 100644 index 000000000000..2c1f69de8a42 --- /dev/null +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/PropertyEditors/BlockEditorElementVariationTestBase.cs @@ -0,0 +1,172 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.Extensions.DependencyInjection; +using Moq; +using NUnit.Framework; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Cache; +using Umbraco.Cms.Core.Configuration.Models; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.PublishedContent; +using Umbraco.Cms.Core.PropertyEditors; +using Umbraco.Cms.Core.PublishedCache; +using Umbraco.Cms.Core.Serialization; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Core.Services.Changes; +using Umbraco.Cms.Core.Web; +using Umbraco.Cms.Tests.Common; +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 abstract class BlockEditorElementVariationTestBase : UmbracoIntegrationTest +{ + protected List TestsRequiringAllowEditInvariantFromNonDefault { get; set; } = new(); + + protected ILanguageService LanguageService => GetRequiredService(); + + protected IContentService ContentService => GetRequiredService(); + + protected IContentTypeService ContentTypeService => GetRequiredService(); + + protected PropertyEditorCollection PropertyEditorCollection => GetRequiredService(); + + private IPublishedSnapshotService PublishedSnapshotService => GetRequiredService(); + + private IUmbracoContextAccessor UmbracoContextAccessor => GetRequiredService(); + + private IUmbracoContextFactory UmbracoContextFactory => GetRequiredService(); + + private IVariationContextAccessor VariationContextAccessor => GetRequiredService(); + + private IDataTypeService DataTypeService => GetRequiredService(); + + private IConfigurationEditorJsonSerializer ConfigurationEditorJsonSerializer => GetRequiredService(); + + protected override void CustomTestSetup(IUmbracoBuilder builder) + { + var mockHttpContextAccessor = new Mock(); + var httpContext = new DefaultHttpContext(); + httpContext.Request.Scheme = "http"; + httpContext.Request.Host = new HostString("localhost"); + + mockHttpContextAccessor.SetupGet(x => x.HttpContext).Returns(httpContext); + + builder.Services.AddUnique(); + builder.Services.AddUnique(mockHttpContextAccessor.Object); + builder.AddUmbracoHybridCache(); + builder.AddNuCache(); + + builder.Services.Configure(config => + config.AllowEditInvariantFromNonDefault = TestsRequiringAllowEditInvariantFromNonDefault.Contains(TestContext.CurrentContext.Test.Name)); + } + + [SetUp] + public async Task SetUp() => await LanguageService.CreateAsync( + new Language("da-DK", "Danish"), Constants.Security.SuperUserKey); + + protected void PublishContent(IContent content, string[] culturesToPublish) + { + var publishResult = ContentService.Publish(content, culturesToPublish); + Assert.IsTrue(publishResult.Success); + + ContentCacheRefresher.JsonPayload[] payloads = + [ + new ContentCacheRefresher.JsonPayload + { + ChangeTypes = TreeChangeTypes.RefreshNode, + Key = content.Key, + Id = content.Id, + Blueprint = false + } + ]; + + PublishedSnapshotService.Notify(payloads, out _, out _); + } + + protected IContentType CreateElementType(ContentVariation variation, string alias = "myElementType") + { + var elementType = new ContentTypeBuilder() + .WithAlias(alias) + .WithName("My Element Type") + .WithIsElement(true) + .WithContentVariation(variation) + .AddPropertyType() + .WithAlias("invariantText") + .WithName("Invariant text") + .WithDataTypeId(Constants.DataTypes.Textbox) + .WithPropertyEditorAlias(Constants.PropertyEditors.Aliases.TextBox) + .WithValueStorageType(ValueStorageType.Nvarchar) + .WithVariations(ContentVariation.Nothing) + .Done() + .AddPropertyType() + .WithAlias("variantText") + .WithName("Variant text") + .WithDataTypeId(Constants.DataTypes.Textbox) + .WithPropertyEditorAlias(Constants.PropertyEditors.Aliases.TextBox) + .WithValueStorageType(ValueStorageType.Nvarchar) + .WithVariations(variation) + .Done() + .Build(); + ContentTypeService.Save(elementType); + return elementType; + } + + protected IContentType CreateContentType(ContentVariation contentTypeVariation, IDataType blocksEditorDataType, ContentVariation blocksPropertyVariation = ContentVariation.Nothing) + { + var contentType = new ContentTypeBuilder() + .WithAlias("myPage") + .WithName("My Page") + .WithContentVariation(contentTypeVariation) + .AddPropertyType() + .WithAlias("blocks") + .WithName("Blocks") + .WithDataTypeId(blocksEditorDataType.Id) + .WithVariations(blocksPropertyVariation) + .Done() + .Build(); + ContentTypeService.Save(contentType); + return contentType; + } + + protected IPublishedContent GetPublishedContent(Guid key) + { + UmbracoContextAccessor.Clear(); + var umbracoContext = UmbracoContextFactory.EnsureUmbracoContext().UmbracoContext; + var publishedContent = umbracoContext.Content?.GetById(key); + Assert.IsNotNull(publishedContent); + + return publishedContent; + } + + protected void SetVariationContext(string? culture, string? segment) + => VariationContextAccessor.VariationContext = new VariationContext(culture: culture, segment: segment); + + protected async Task CreateBlockEditorDataType(string propertyEditorAlias, T blocksConfiguration) + { + var dataType = new DataType(PropertyEditorCollection[propertyEditorAlias], ConfigurationEditorJsonSerializer) + { + ConfigurationData = new Dictionary { { "blocks", blocksConfiguration } }, + Name = "My Block Editor", + DatabaseType = ValueStorageType.Ntext, + ParentId = Constants.System.Root, + CreateDate = DateTime.UtcNow + }; + + await DataTypeService.CreateAsync(dataType, Constants.Security.SuperUserKey); + return dataType; + } + + protected void RefreshContentTypeCache(params IContentType[] contentTypes) + { + ContentTypeCacheRefresher.JsonPayload[] payloads = contentTypes + .Select(contentType => new ContentTypeCacheRefresher.JsonPayload(nameof(IContentType), contentType.Id, ContentTypeChangeTypes.RefreshMain)) + .ToArray(); + + PublishedSnapshotService.Notify(payloads); + } +} diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/PropertyEditors/BlockGridElementLevelVariationTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/PropertyEditors/BlockGridElementLevelVariationTests.cs new file mode 100644 index 000000000000..0122eaee67d1 --- /dev/null +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/PropertyEditors/BlockGridElementLevelVariationTests.cs @@ -0,0 +1,533 @@ +using NUnit.Framework; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.Blocks; +using Umbraco.Cms.Core.Models.PublishedContent; +using Umbraco.Cms.Core.PropertyEditors; +using Umbraco.Cms.Core.Serialization; +using Umbraco.Cms.Tests.Common.Builders; + +namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.PropertyEditors; + +// NOTE: These tests are in place to ensure that element level variation works for Block Grid. Element level variation +// is tested more in-depth for Block List (see BlockListElementLevelVariationTests), but since the actual +// implementation is shared between Block List and Block Grid, we won't repeat all those tests here. +public class BlockGridElementLevelVariationTests : BlockEditorElementVariationTestBase +{ + private IJsonSerializer JsonSerializer => GetRequiredService(); + + [Test] + public async Task Can_Publish_Cultures_Independently() + { + var elementType = CreateElementType(ContentVariation.Culture); + + var areaKey = Guid.NewGuid(); + var blockGridDataType = await CreateBlockGridDataType(elementType, areaKey); + var contentType = CreateContentType(blockGridDataType); + var blockGridValue = CreateBlockGridValue(elementType, areaKey); + var content = CreateContent(contentType, blockGridValue); + + PublishContent(content, ["en-US", "da-DK"]); + + AssertPropertyValues( + "en-US", + (element1, element2, element3) => + { + Assert.AreEqual("#1: The first invariant content value", element1.Value("invariantText")); + Assert.AreEqual("#1: The first content value in English", element1.Value("variantText")); + Assert.AreEqual("#2: The first invariant content value", element2.Value("invariantText")); + Assert.AreEqual("#2: The first content value in English", element2.Value("variantText")); + Assert.AreEqual("#3: The first invariant content value", element3.Value("invariantText")); + Assert.AreEqual("#3: The first content value in English", element3.Value("variantText")); + }, + (element1, element2, element3) => + { + Assert.AreEqual("#1: The first invariant settings value", element1.Value("invariantText")); + Assert.AreEqual("#1: The first settings value in English", element1.Value("variantText")); + Assert.AreEqual("#2: The first invariant settings value", element2.Value("invariantText")); + Assert.AreEqual("#2: The first settings value in English", element2.Value("variantText")); + Assert.AreEqual("#3: The first invariant settings value", element3.Value("invariantText")); + Assert.AreEqual("#3: The first settings value in English", element3.Value("variantText")); + }); + + AssertPropertyValues( + "da-DK", + (element1, element2, element3) => + { + Assert.AreEqual("#1: The first invariant content value", element1.Value("invariantText")); + Assert.AreEqual("#1: The first content value in Danish", element1.Value("variantText")); + Assert.AreEqual("#2: The first invariant content value", element2.Value("invariantText")); + Assert.AreEqual("#2: The first content value in Danish", element2.Value("variantText")); + Assert.AreEqual("#3: The first invariant content value", element3.Value("invariantText")); + Assert.AreEqual("#3: The first content value in Danish", element3.Value("variantText")); + }, + (element1, element2, element3) => + { + Assert.AreEqual("#1: The first invariant settings value", element1.Value("invariantText")); + Assert.AreEqual("#1: The first settings value in Danish", element1.Value("variantText")); + Assert.AreEqual("#2: The first invariant settings value", element2.Value("invariantText")); + Assert.AreEqual("#2: The first settings value in Danish", element2.Value("variantText")); + Assert.AreEqual("#3: The first invariant settings value", element3.Value("invariantText")); + Assert.AreEqual("#3: The first settings value in Danish", element3.Value("variantText")); + }); + + blockGridValue = JsonSerializer.Deserialize((string)content.Properties["blocks"]!.GetValue()!); + for (var i = 0; i < 3; i++) + { + blockGridValue.ContentData[i].Values[0].Value = $"#{i + 1}: The second invariant content value"; + blockGridValue.ContentData[i].Values[1].Value = $"#{i + 1}: The second content value in English"; + blockGridValue.ContentData[i].Values[2].Value = $"#{i + 1}: The second content value in Danish"; + blockGridValue.SettingsData[i].Values[0].Value = $"#{i + 1}: The second invariant settings value"; + blockGridValue.SettingsData[i].Values[1].Value = $"#{i + 1}: The second settings value in English"; + blockGridValue.SettingsData[i].Values[2].Value = $"#{i + 1}: The second settings value in Danish"; + } + + content.Properties["blocks"]!.SetValue(JsonSerializer.Serialize(blockGridValue)); + ContentService.Save(content); + PublishContent(content, ["en-US"]); + + AssertPropertyValues( + "en-US", + (element1, element2, element3) => + { + Assert.AreEqual("#1: The second invariant content value", element1.Value("invariantText")); + Assert.AreEqual("#1: The second content value in English", element1.Value("variantText")); + Assert.AreEqual("#2: The second invariant content value", element2.Value("invariantText")); + Assert.AreEqual("#2: The second content value in English", element2.Value("variantText")); + Assert.AreEqual("#3: The second invariant content value", element3.Value("invariantText")); + Assert.AreEqual("#3: The second content value in English", element3.Value("variantText")); + }, + (element1, element2, element3) => + { + Assert.AreEqual("#1: The second invariant settings value", element1.Value("invariantText")); + Assert.AreEqual("#1: The second settings value in English", element1.Value("variantText")); + Assert.AreEqual("#2: The second invariant settings value", element2.Value("invariantText")); + Assert.AreEqual("#2: The second settings value in English", element2.Value("variantText")); + Assert.AreEqual("#3: The second invariant settings value", element3.Value("invariantText")); + Assert.AreEqual("#3: The second settings value in English", element3.Value("variantText")); + }); + + AssertPropertyValues( + "da-DK", + (element1, element2, element3) => + { + Assert.AreEqual("#1: The second invariant content value", element1.Value("invariantText")); + Assert.AreEqual("#1: The first content value in Danish", element1.Value("variantText")); + Assert.AreEqual("#2: The second invariant content value", element2.Value("invariantText")); + Assert.AreEqual("#2: The first content value in Danish", element2.Value("variantText")); + Assert.AreEqual("#3: The second invariant content value", element3.Value("invariantText")); + Assert.AreEqual("#3: The first content value in Danish", element3.Value("variantText")); + }, + (element1, element2, element3) => + { + Assert.AreEqual("#1: The second invariant settings value", element1.Value("invariantText")); + Assert.AreEqual("#1: The first settings value in Danish", element1.Value("variantText")); + Assert.AreEqual("#2: The second invariant settings value", element2.Value("invariantText")); + Assert.AreEqual("#2: The first settings value in Danish", element2.Value("variantText")); + Assert.AreEqual("#3: The second invariant settings value", element3.Value("invariantText")); + Assert.AreEqual("#3: The first settings value in Danish", element3.Value("variantText")); + }); + + PublishContent(content, ["da-DK"]); + + AssertPropertyValues( + "da-DK", + (element1, element2, element3) => + { + Assert.AreEqual("#1: The second invariant content value", element1.Value("invariantText")); + Assert.AreEqual("#1: The second content value in Danish", element1.Value("variantText")); + Assert.AreEqual("#2: The second invariant content value", element2.Value("invariantText")); + Assert.AreEqual("#2: The second content value in Danish", element2.Value("variantText")); + Assert.AreEqual("#3: The second invariant content value", element3.Value("invariantText")); + Assert.AreEqual("#3: The second content value in Danish", element3.Value("variantText")); + }, + (element1, element2, element3) => + { + Assert.AreEqual("#1: The second invariant settings value", element1.Value("invariantText")); + Assert.AreEqual("#1: The second settings value in Danish", element1.Value("variantText")); + Assert.AreEqual("#2: The second invariant settings value", element2.Value("invariantText")); + Assert.AreEqual("#2: The second settings value in Danish", element2.Value("variantText")); + Assert.AreEqual("#3: The second invariant settings value", element3.Value("invariantText")); + Assert.AreEqual("#3: The second settings value in Danish", element3.Value("variantText")); + }); + + void AssertPropertyValues( + string culture, + Action validateBlockContentValues, + Action validateBlockSettingsValues) + { + SetVariationContext(culture, null); + var publishedContent = GetPublishedContent(content.Key); + + var blocks = publishedContent.Value("blocks"); + Assert.IsNotNull(blocks); + Assert.AreEqual(2, blocks.Count); + var area = blocks[0].Areas.FirstOrDefault(); + Assert.IsNotNull(area); + Assert.AreEqual(1, area.Count); + Assert.Multiple(() => + { + validateBlockContentValues(blocks[0].Content, area[0].Content, blocks[1].Content); + validateBlockSettingsValues(blocks[0].Settings, area[0].Settings, blocks[1].Settings); + }); + } + } + + [Test] + public async Task Can_Publish_With_Blocks_Removed_At_Root() + { + var elementType = CreateElementType(ContentVariation.Culture); + + var areaKey = Guid.NewGuid(); + var blockGridDataType = await CreateBlockGridDataType(elementType, areaKey); + var contentType = CreateContentType(blockGridDataType); + var blockGridValue = CreateBlockGridValue(elementType, areaKey); + var content = CreateContent(contentType, blockGridValue); + + PublishContent(content, ["en-US", "da-DK"]); + + AssertPropertyValues("en-US", 2, blocks => { }); + AssertPropertyValues("da-DK", 2, blocks => { }); + + blockGridValue = JsonSerializer.Deserialize((string)content.Properties["blocks"]!.GetValue()!); + + // remove block #3 (second at root level) + blockGridValue.Layout[blockGridValue.Layout.First().Key] = + [ + blockGridValue.Layout.First().Value.First(), + ]; + var contentKey = blockGridValue.ContentData[2].Key; + blockGridValue.ContentData.RemoveAt(2); + blockGridValue.SettingsData.RemoveAt(2); + blockGridValue.Expose.RemoveAll(v => v.ContentKey == contentKey); + Assert.AreEqual(4, blockGridValue.Expose.Count); + + for (var i = 0; i < 2; i++) + { + blockGridValue.ContentData[i].Values[0].Value = $"#{i + 1}: The second invariant content value"; + blockGridValue.ContentData[i].Values[1].Value = $"#{i + 1}: The second content value in English"; + blockGridValue.ContentData[i].Values[2].Value = $"#{i + 1}: The second content value in Danish"; + blockGridValue.SettingsData[i].Values[0].Value = $"#{i + 1}: The second invariant settings value"; + blockGridValue.SettingsData[i].Values[1].Value = $"#{i + 1}: The second settings value in English"; + blockGridValue.SettingsData[i].Values[2].Value = $"#{i + 1}: The second settings value in Danish"; + } + + content.Properties["blocks"]!.SetValue(JsonSerializer.Serialize(blockGridValue)); + ContentService.Save(content); + PublishContent(content, ["en-US"]); + + AssertPropertyValues("en-US", 1, blocks => + { + var areaItem = blocks[0].Areas.FirstOrDefault()?.FirstOrDefault(); + Assert.IsNotNull(areaItem); + + Assert.AreEqual("#1: The second invariant content value", blocks[0].Content.Value("invariantText")); + Assert.AreEqual("#1: The second content value in English", blocks[0].Content.Value("variantText")); + Assert.AreEqual("#2: The second invariant content value", areaItem.Content.Value("invariantText")); + Assert.AreEqual("#2: The second content value in English", areaItem.Content.Value("variantText")); + + Assert.AreEqual("#1: The second invariant settings value", blocks[0].Settings!.Value("invariantText")); + Assert.AreEqual("#1: The second settings value in English", blocks[0].Settings.Value("variantText")); + Assert.AreEqual("#2: The second invariant settings value", areaItem.Settings!.Value("invariantText")); + Assert.AreEqual("#2: The second settings value in English", areaItem.Settings.Value("variantText")); + }); + + AssertPropertyValues("da-DK", 1, blocks => + { + var areaItem = blocks[0].Areas.FirstOrDefault()?.FirstOrDefault(); + Assert.IsNotNull(areaItem); + + Assert.AreEqual("#1: The second invariant content value", blocks[0].Content.Value("invariantText")); + Assert.AreEqual("#1: The first content value in Danish", blocks[0].Content.Value("variantText")); + Assert.AreEqual("#2: The second invariant content value", areaItem.Content.Value("invariantText")); + Assert.AreEqual("#2: The first content value in Danish", areaItem.Content.Value("variantText")); + + Assert.AreEqual("#1: The second invariant settings value", blocks[0].Settings!.Value("invariantText")); + Assert.AreEqual("#1: The first settings value in Danish", blocks[0].Settings.Value("variantText")); + Assert.AreEqual("#2: The second invariant settings value", areaItem.Settings!.Value("invariantText")); + Assert.AreEqual("#2: The first settings value in Danish", areaItem.Settings.Value("variantText")); + }); + + PublishContent(content, ["da-DK"]); + + AssertPropertyValues("da-DK", 1, blocks => + { + var areaItem = blocks[0].Areas.FirstOrDefault()?.FirstOrDefault(); + Assert.IsNotNull(areaItem); + + Assert.AreEqual("#1: The second invariant content value", blocks[0].Content.Value("invariantText")); + Assert.AreEqual("#1: The second content value in Danish", blocks[0].Content.Value("variantText")); + Assert.AreEqual("#2: The second invariant content value", areaItem.Content.Value("invariantText")); + Assert.AreEqual("#2: The second content value in Danish", areaItem.Content.Value("variantText")); + + Assert.AreEqual("#1: The second invariant settings value", blocks[0].Settings!.Value("invariantText")); + Assert.AreEqual("#1: The second settings value in Danish", blocks[0].Settings.Value("variantText")); + Assert.AreEqual("#2: The second invariant settings value", areaItem.Settings!.Value("invariantText")); + Assert.AreEqual("#2: The second settings value in Danish", areaItem.Settings.Value("variantText")); + }); + + void AssertPropertyValues(string culture, int numberOfExpectedBlocks, Action validateBlocks) + { + SetVariationContext(culture, null); + var publishedContent = GetPublishedContent(content.Key); + + var value = publishedContent.Value("blocks"); + Assert.IsNotNull(value); + Assert.AreEqual(numberOfExpectedBlocks, value.Count); + + validateBlocks(value); + } + } + + [Test] + public async Task Can_Publish_With_Blocks_Removed_In_Area() + { + var elementType = CreateElementType(ContentVariation.Culture); + + var areaKey = Guid.NewGuid(); + var blockGridDataType = await CreateBlockGridDataType(elementType, areaKey); + var contentType = CreateContentType(blockGridDataType); + var blockGridValue = CreateBlockGridValue(elementType, areaKey); + var content = CreateContent(contentType, blockGridValue); + + PublishContent(content, ["en-US", "da-DK"]); + + AssertPropertyValues("en-US", 2, blocks => + { + Assert.IsNotEmpty(blocks[0].Areas); + + // no need to validate the content/settings values here, the same thing is validated in another test + }); + + AssertPropertyValues("da-DK", 2, blocks => + { + Assert.IsNotEmpty(blocks[0].Areas); + + // no need to validate the content/settings values here, the same thing is validated in another test + }); + + blockGridValue = JsonSerializer.Deserialize((string)content.Properties["blocks"]!.GetValue()!); + + // remove block #2 (inside the area of the first block at root level) + ((BlockGridLayoutItem)blockGridValue.Layout[blockGridValue.Layout.First().Key].First()).Areas = []; + var contentKey = blockGridValue.ContentData[1].Key; + blockGridValue.ContentData.RemoveAt(1); + blockGridValue.SettingsData.RemoveAt(1); + blockGridValue.Expose.RemoveAll(v => v.ContentKey == contentKey); + Assert.AreEqual(4, blockGridValue.Expose.Count); + + blockGridValue.ContentData[0].Values[0].Value = "#1: The second invariant content value"; + blockGridValue.ContentData[0].Values[1].Value = "#1: The second content value in English"; + blockGridValue.ContentData[0].Values[2].Value = "#1: The second content value in Danish"; + blockGridValue.ContentData[1].Values[0].Value = "#3: The second invariant content value"; + blockGridValue.ContentData[1].Values[1].Value = "#3: The second content value in English"; + blockGridValue.ContentData[1].Values[2].Value = "#3: The second content value in Danish"; + blockGridValue.SettingsData[0].Values[0].Value = "#1: The second invariant settings value"; + blockGridValue.SettingsData[0].Values[1].Value = "#1: The second settings value in English"; + blockGridValue.SettingsData[0].Values[2].Value = "#1: The second settings value in Danish"; + blockGridValue.SettingsData[1].Values[0].Value = "#3: The second invariant settings value"; + blockGridValue.SettingsData[1].Values[1].Value = "#3: The second settings value in English"; + blockGridValue.SettingsData[1].Values[2].Value = "#3: The second settings value in Danish"; + + content.Properties["blocks"]!.SetValue(JsonSerializer.Serialize(blockGridValue)); + ContentService.Save(content); + PublishContent(content, ["en-US"]); + + AssertPropertyValues("en-US", 2, blocks => + { + Assert.IsEmpty(blocks[0].Areas); + + Assert.AreEqual("#1: The second invariant content value", blocks[0].Content.Value("invariantText")); + Assert.AreEqual("#1: The second content value in English", blocks[0].Content.Value("variantText")); + Assert.AreEqual("#3: The second invariant content value", blocks[1].Content.Value("invariantText")); + Assert.AreEqual("#3: The second content value in English", blocks[1].Content.Value("variantText")); + + Assert.AreEqual("#1: The second invariant settings value", blocks[0].Settings!.Value("invariantText")); + Assert.AreEqual("#1: The second settings value in English", blocks[0].Settings.Value("variantText")); + Assert.AreEqual("#3: The second invariant settings value", blocks[1].Settings!.Value("invariantText")); + Assert.AreEqual("#3: The second settings value in English", blocks[1].Settings.Value("variantText")); + }); + + AssertPropertyValues("da-DK", 2, blocks => + { + Assert.IsEmpty(blocks[0].Areas); + + Assert.AreEqual("#1: The second invariant content value", blocks[0].Content.Value("invariantText")); + Assert.AreEqual("#1: The first content value in Danish", blocks[0].Content.Value("variantText")); + Assert.AreEqual("#3: The second invariant content value", blocks[1].Content.Value("invariantText")); + Assert.AreEqual("#3: The first content value in Danish", blocks[1].Content.Value("variantText")); + + Assert.AreEqual("#1: The second invariant settings value", blocks[0].Settings!.Value("invariantText")); + Assert.AreEqual("#1: The first settings value in Danish", blocks[0].Settings.Value("variantText")); + Assert.AreEqual("#3: The second invariant settings value", blocks[1].Settings!.Value("invariantText")); + Assert.AreEqual("#3: The first settings value in Danish", blocks[1].Settings.Value("variantText")); + }); + + PublishContent(content, ["da-DK"]); + + AssertPropertyValues("da-DK", 2, blocks => + { + Assert.IsEmpty(blocks[0].Areas); + + Assert.AreEqual("#1: The second invariant content value", blocks[0].Content.Value("invariantText")); + Assert.AreEqual("#1: The second content value in Danish", blocks[0].Content.Value("variantText")); + Assert.AreEqual("#3: The second invariant content value", blocks[1].Content.Value("invariantText")); + Assert.AreEqual("#3: The second content value in Danish", blocks[1].Content.Value("variantText")); + + Assert.AreEqual("#1: The second invariant settings value", blocks[0].Settings!.Value("invariantText")); + Assert.AreEqual("#1: The second settings value in Danish", blocks[0].Settings.Value("variantText")); + Assert.AreEqual("#3: The second invariant settings value", blocks[1].Settings!.Value("invariantText")); + Assert.AreEqual("#3: The second settings value in Danish", blocks[1].Settings.Value("variantText")); + }); + + void AssertPropertyValues(string culture, int numberOfExpectedBlocks, Action validateBlocks) + { + SetVariationContext(culture, null); + var publishedContent = GetPublishedContent(content.Key); + + var value = publishedContent.Value("blocks"); + Assert.IsNotNull(value); + Assert.AreEqual(numberOfExpectedBlocks, value.Count); + + validateBlocks(value); + } + } + + private async Task CreateBlockGridDataType(IContentType elementType, Guid areaKey) + => await CreateBlockEditorDataType( + Constants.PropertyEditors.Aliases.BlockGrid, + new BlockGridConfiguration.BlockGridBlockConfiguration[] + { + new() + { + ContentElementTypeKey = elementType.Key, + SettingsElementTypeKey = elementType.Key, + AreaGridColumns = 12, + Areas = + [ + new() { Alias = "one", Key = areaKey, ColumnSpan = 12, RowSpan = 1 } + ] + } + }); + + private IContentType CreateContentType(IDataType blockListDataType) + => CreateContentType(ContentVariation.Culture, blockListDataType); + + private BlockGridValue CreateBlockGridValue(IContentType elementType, Guid areaKey) + { + var contentElementKey1 = Guid.NewGuid(); + var settingsElementKey1 = Guid.NewGuid(); + var contentElementKey2 = Guid.NewGuid(); + var settingsElementKey2 = Guid.NewGuid(); + var contentElementKey3 = Guid.NewGuid(); + var settingsElementKey3 = Guid.NewGuid(); + return new BlockGridValue( + [ + new BlockGridLayoutItem(contentElementKey1, settingsElementKey1) + { + ColumnSpan = 12, + RowSpan = 1, + Areas = + [ + new BlockGridLayoutAreaItem(areaKey) + { + Items = + [ + new BlockGridLayoutItem(contentElementKey2, settingsElementKey2) + { + ColumnSpan = 12, + RowSpan = 1 + }, + ], + }, + ], + }, + new BlockGridLayoutItem(contentElementKey3, settingsElementKey3) + { + ColumnSpan = 12, + RowSpan = 1, + } + ]) + { + ContentData = + [ + new(contentElementKey1, elementType.Key, elementType.Alias) + { + Values = [ + new() { Alias = "invariantText", Value = "#1: The first invariant content value" }, + new() { Alias = "variantText", Value = "#1: The first content value in English", Culture = "en-US" }, + new() { Alias = "variantText", Value = "#1: The first content value in Danish", Culture = "da-DK" } + ] + }, + new(contentElementKey2, elementType.Key, elementType.Alias) + { + Values = [ + new() { Alias = "invariantText", Value = "#2: The first invariant content value" }, + new() { Alias = "variantText", Value = "#2: The first content value in English", Culture = "en-US" }, + new() { Alias = "variantText", Value = "#2: The first content value in Danish", Culture = "da-DK" } + ] + }, + new(contentElementKey3, elementType.Key, elementType.Alias) + { + Values = [ + new() { Alias = "invariantText", Value = "#3: The first invariant content value" }, + new() { Alias = "variantText", Value = "#3: The first content value in English", Culture = "en-US" }, + new() { Alias = "variantText", Value = "#3: The first content value in Danish", Culture = "da-DK" } + ] + }, + ], + SettingsData = + [ + new(settingsElementKey1, elementType.Key, elementType.Alias) + { + Values = [ + new() { Alias = "invariantText", Value = "#1: The first invariant settings value" }, + new() { Alias = "variantText", Value = "#1: The first settings value in English", Culture = "en-US" }, + new() { Alias = "variantText", Value = "#1: The first settings value in Danish", Culture = "da-DK" } + ] + }, + new(settingsElementKey2, elementType.Key, elementType.Alias) + { + Values = [ + new() { Alias = "invariantText", Value = "#2: The first invariant settings value" }, + new() { Alias = "variantText", Value = "#2: The first settings value in English", Culture = "en-US" }, + new() { Alias = "variantText", Value = "#2: The first settings value in Danish", Culture = "da-DK" } + ] + }, + new(settingsElementKey3, elementType.Key, elementType.Alias) + { + Values = [ + new() { Alias = "invariantText", Value = "#3: The first invariant settings value" }, + new() { Alias = "variantText", Value = "#3: The first settings value in English", Culture = "en-US" }, + new() { Alias = "variantText", Value = "#3: The first settings value in Danish", Culture = "da-DK" } + ] + }, + ], + Expose = + [ + new (contentElementKey1, "en-US", null), + new (contentElementKey1, "da-DK", null), + new (contentElementKey2, "en-US", null), + new (contentElementKey2, "da-DK", null), + new (contentElementKey3, "en-US", null), + new (contentElementKey3, "da-DK", null), + ] + }; + } + + private IContent CreateContent(IContentType contentType, BlockGridValue blockGridValue) + { + var contentBuilder = new ContentBuilder() + .WithContentType(contentType) + .WithCultureName("en-US", "Home (en)") + .WithCultureName("da-DK", "Home (da)"); + + var content = contentBuilder.Build(); + + var propertyValue = JsonSerializer.Serialize(blockGridValue); + content.Properties["blocks"]!.SetValue(propertyValue); + + ContentService.Save(content); + return content; + } +} diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/PropertyEditors/BlockListElementLevelVariationTests.Parsing.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/PropertyEditors/BlockListElementLevelVariationTests.Parsing.cs new file mode 100644 index 000000000000..7433417df7ff --- /dev/null +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/PropertyEditors/BlockListElementLevelVariationTests.Parsing.cs @@ -0,0 +1,662 @@ +using NUnit.Framework; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.Blocks; + +namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.PropertyEditors; + +public partial class BlockListElementLevelVariationTests +{ + [TestCase("en-US", "The culture variant content value in English", "The culture variant settings value in English")] + [TestCase("da-DK", "The culture variant content value in Danish", "The culture variant settings value in Danish")] + public async Task Can_Parse_Element_Level_Culture_Variations(string culture, string expectedVariantContentValue, string expectedVariantSettingsValue) + { + var publishedContent = await CreatePublishedContent( + ContentVariation.Culture, + new List + { + new() { Alias = "invariantText", Value = "The invariant content value" }, + new() { Alias = "variantText", Value = "The culture variant content value in English", Culture = "en-US" }, + new() { Alias = "variantText", Value = "The culture variant content value in Danish", Culture = "da-DK" }, + }, + new List + { + new() { Alias = "invariantText", Value = "The invariant settings value" }, + new() { Alias = "variantText", Value = "The culture variant settings value in English", Culture = "en-US" }, + new() { Alias = "variantText", Value = "The culture variant settings value in Danish", Culture = "da-DK" }, + }); + + SetVariationContext(culture, null); + + var value = publishedContent.GetProperty("blocks")!.GetValue() as BlockListModel; + Assert.IsNotNull(value); + Assert.AreEqual(1, value.Count); + + var blockListItem = value.First(); + Assert.AreEqual(2, blockListItem.Content.Properties.Count()); + Assert.Multiple(() => + { + var invariantProperty = blockListItem.Content.Properties.First(); + Assert.AreEqual("invariantText", invariantProperty.Alias); + Assert.AreEqual("The invariant content value", invariantProperty.GetValue()); + + var variantProperty = blockListItem.Content.Properties.Last(); + Assert.AreEqual("variantText", variantProperty.Alias); + Assert.AreEqual(expectedVariantContentValue, variantProperty.GetValue()); + }); + + Assert.AreEqual(2, blockListItem.Settings.Properties.Count()); + Assert.Multiple(() => + { + var invariantProperty = blockListItem.Settings.Properties.First(); + Assert.AreEqual("invariantText", invariantProperty.Alias); + Assert.AreEqual("The invariant settings value", invariantProperty.GetValue()); + + var variantProperty = blockListItem.Settings.Properties.Last(); + Assert.AreEqual("variantText", variantProperty.Alias); + Assert.AreEqual(expectedVariantSettingsValue, variantProperty.GetValue()); + }); + } + + [TestCase("segment1", "The segment variant content value for Segment1", "The segment variant settings value for Segment1")] + [TestCase("segment2", "The segment variant content value for Segment2", "The segment variant settings value for Segment2")] + public async Task Can_Parse_Element_Level_Segment_Variations(string segment, string expectedVariantContentValue, string expectedVariantSettingsValue) + { + var publishedContent = await CreatePublishedContent( + ContentVariation.Segment, + new List + { + new() { Alias = "invariantText", Value = "The invariant content value" }, + new() { Alias = "variantText", Value = "The segment variant content value for Segment1", Segment = "segment1" }, + new() { Alias = "variantText", Value = "The segment variant content value for Segment2", Segment = "segment2" }, + }, + new List + { + new() { Alias = "invariantText", Value = "The invariant settings value" }, + new() { Alias = "variantText", Value = "The segment variant settings value for Segment1", Segment = "segment1" }, + new() { Alias = "variantText", Value = "The segment variant settings value for Segment2", Segment = "segment2" }, + }); + + SetVariationContext(null, segment); + + var value = publishedContent.GetProperty("blocks")!.GetValue() as BlockListModel; + Assert.IsNotNull(value); + Assert.AreEqual(1, value.Count); + + var blockListItem = value.First(); + Assert.AreEqual(2, blockListItem.Content.Properties.Count()); + Assert.Multiple(() => + { + var invariantProperty = blockListItem.Content.Properties.First(); + Assert.AreEqual("invariantText", invariantProperty.Alias); + Assert.AreEqual("The invariant content value", invariantProperty.GetValue()); + + var variantProperty = blockListItem.Content.Properties.Last(); + Assert.AreEqual("variantText", variantProperty.Alias); + Assert.AreEqual(expectedVariantContentValue, variantProperty.GetValue()); + }); + + Assert.AreEqual(2, blockListItem.Settings.Properties.Count()); + Assert.Multiple(() => + { + var invariantProperty = blockListItem.Settings.Properties.First(); + Assert.AreEqual("invariantText", invariantProperty.Alias); + Assert.AreEqual("The invariant settings value", invariantProperty.GetValue()); + + var variantProperty = blockListItem.Settings.Properties.Last(); + Assert.AreEqual("variantText", variantProperty.Alias); + Assert.AreEqual(expectedVariantSettingsValue, variantProperty.GetValue()); + }); + } + + [TestCase( + "en-US", + "segment1", + "The variant content value in English for Segment1", + "The variant settings value in English for Segment1")] + [TestCase( + "en-US", + "segment2", + "The variant content value in English for Segment2", + "The variant settings value in English for Segment2")] + [TestCase( + "da-DK", + "segment1", + "The variant content value in Danish for Segment1", + "The variant settings value in Danish for Segment1")] + [TestCase( + "da-DK", + "segment2", + "The variant content value in Danish for Segment2", + "The variant settings value in Danish for Segment2")] + public async Task Can_Parse_Element_Level_Culture_And_Segment_Variations( + string culture, + string segment, + string expectedVariantContentValue, + string expectedVariantSettingsValue) + { + var publishedContent = await CreatePublishedContent( + ContentVariation.CultureAndSegment, + new List + { + new() { Alias = "invariantText", Value = "The invariant content value" }, + new() { Alias = "variantText", Value = "The variant content value in English for Segment1", Culture = "en-US", Segment = "segment1" }, + new() { Alias = "variantText", Value = "The variant content value in English for Segment2", Culture = "en-US", Segment = "segment2" }, + new() { Alias = "variantText", Value = "The variant content value in Danish for Segment1", Culture = "da-DK", Segment = "segment1" }, + new() { Alias = "variantText", Value = "The variant content value in Danish for Segment2", Culture = "da-DK", Segment = "segment2" } + }, + new List + { + new() { Alias = "invariantText", Value = "The invariant settings value" }, + new() { Alias = "variantText", Value = "The variant settings value in English for Segment1", Culture = "en-US", Segment = "segment1" }, + new() { Alias = "variantText", Value = "The variant settings value in English for Segment2", Culture = "en-US", Segment = "segment2" }, + new() { Alias = "variantText", Value = "The variant settings value in Danish for Segment1", Culture = "da-DK", Segment = "segment1" }, + new() { Alias = "variantText", Value = "The variant settings value in Danish for Segment2", Culture = "da-DK", Segment = "segment2" } + }); + + SetVariationContext(culture, segment); + + var value = publishedContent.GetProperty("blocks")!.GetValue() as BlockListModel; + Assert.IsNotNull(value); + Assert.AreEqual(1, value.Count); + + var blockListItem = value.First(); + Assert.AreEqual(2, blockListItem.Content.Properties.Count()); + Assert.Multiple(() => + { + var invariantProperty = blockListItem.Content.Properties.First(); + Assert.AreEqual("invariantText", invariantProperty.Alias); + Assert.AreEqual("The invariant content value", invariantProperty.GetValue()); + + var variantProperty = blockListItem.Content.Properties.Last(); + Assert.AreEqual("variantText", variantProperty.Alias); + Assert.AreEqual(expectedVariantContentValue, variantProperty.GetValue()); + }); + + Assert.AreEqual(2, blockListItem.Settings.Properties.Count()); + Assert.Multiple(() => + { + var invariantProperty = blockListItem.Settings.Properties.First(); + Assert.AreEqual("invariantText", invariantProperty.Alias); + Assert.AreEqual("The invariant settings value", invariantProperty.GetValue()); + + var variantProperty = blockListItem.Settings.Properties.Last(); + Assert.AreEqual("variantText", variantProperty.Alias); + Assert.AreEqual(expectedVariantSettingsValue, variantProperty.GetValue()); + }); + } + + [TestCase("en-US")] + [TestCase("da-DK")] + public async Task Can_Be_Invariant(string culture) + { + var publishedContent = await CreatePublishedContent( + ContentVariation.Nothing, + new List + { + new() { Alias = "invariantText", Value = "The invariant content value" }, + new() { Alias = "variantText", Value = "Another invariant content value" } + }, + new List + { + new() { Alias = "invariantText", Value = "The invariant settings value" }, + new() { Alias = "variantText", Value = "Another invariant settings value" } + }); + + SetVariationContext(culture, null); + + var value = publishedContent.GetProperty("blocks")!.GetValue() as BlockListModel; + Assert.IsNotNull(value); + Assert.AreEqual(1, value.Count); + + var blockListItem = value.First(); + Assert.AreEqual(2, blockListItem.Content.Properties.Count()); + Assert.Multiple(() => + { + var invariantProperty = blockListItem.Content.Properties.First(); + Assert.AreEqual("invariantText", invariantProperty.Alias); + Assert.AreEqual("The invariant content value", invariantProperty.GetValue()); + + var variantProperty = blockListItem.Content.Properties.Last(); + Assert.AreEqual("variantText", variantProperty.Alias); + Assert.AreEqual("Another invariant content value", variantProperty.GetValue()); + }); + + Assert.AreEqual(2, blockListItem.Settings.Properties.Count()); + Assert.Multiple(() => + { + var invariantProperty = blockListItem.Settings.Properties.First(); + Assert.AreEqual("invariantText", invariantProperty.Alias); + Assert.AreEqual("The invariant settings value", invariantProperty.GetValue()); + + var variantProperty = blockListItem.Settings.Properties.Last(); + Assert.AreEqual("variantText", variantProperty.Alias); + Assert.AreEqual("Another invariant settings value", variantProperty.GetValue()); + }); + } + + [TestCase("en-US", true)] + [TestCase("da-DK", false)] + public async Task Can_Become_Variant_After_Publish(string culture, bool expectExposedBlocks) + { + var elementType = CreateElementType(ContentVariation.Nothing); + var blockListDataType = await CreateBlockListDataType(elementType); + var contentType = CreateContentType(ContentVariation.Nothing, blockListDataType); + + var content = CreateContent( + contentType, + elementType, + new List + { + new() { Alias = "invariantText", Value = "The invariant content value" }, + new() { Alias = "variantText", Value = "Another invariant content value" } + }, + new List + { + new() { Alias = "invariantText", Value = "The invariant settings value" }, + new() { Alias = "variantText", Value = "Another invariant settings value" } + }, + true); + + // the content and element types are created invariant; now make them culture variant, and enable culture variance on the "variantText" property + elementType.Variations = ContentVariation.Culture; + elementType.PropertyTypes.First(pt => pt.Alias == "variantText").Variations = ContentVariation.Culture; + ContentTypeService.Save(elementType); + + contentType.Variations = ContentVariation.Culture; + ContentTypeService.Save(contentType); + + RefreshContentTypeCache(elementType, contentType); + + // to re-publish the content in both cultures we need to set the culture names + content = ContentService.GetById(content.Key)!; + content.SetCultureName("Home (en)", "en-US"); + content.SetCultureName("Home (da)", "da-DK"); + ContentService.Save(content); + PublishContent(content, contentType); + + var publishedContent = GetPublishedContent(content.Key); + + SetVariationContext(culture, null); + + // the "blocks" property is invariant (at content level), and the block data currently stored is also invariant. + // however, the content and element types both vary by culture at this point, so the blocks should be parsed + // accordingly. this means that the block is exposed only in the default culture, and the "variantText" property + // should perform a fallback to the default language (which is en-US). + var value = publishedContent.GetProperty("blocks")!.GetValue() as BlockListModel; + Assert.IsNotNull(value); + Assert.AreEqual(expectExposedBlocks ? 1 : 0, value.Count); + + if (expectExposedBlocks) + { + var blockListItem = value.First(); + Assert.AreEqual(2, blockListItem.Content.Properties.Count()); + Assert.Multiple(() => + { + var invariantProperty = blockListItem.Content.Properties.First(); + Assert.IsFalse(invariantProperty.PropertyType.VariesByCulture()); + Assert.AreEqual("invariantText", invariantProperty.Alias); + Assert.AreEqual("The invariant content value", invariantProperty.GetValue()); + + var variantProperty = blockListItem.Content.Properties.Last(); + Assert.IsTrue(variantProperty.PropertyType.VariesByCulture()); + Assert.AreEqual("variantText", variantProperty.Alias); + Assert.AreEqual("Another invariant content value", variantProperty.GetValue()); + }); + + Assert.AreEqual(2, blockListItem.Settings.Properties.Count()); + Assert.Multiple(() => + { + var invariantProperty = blockListItem.Settings.Properties.First(); + Assert.AreEqual("invariantText", invariantProperty.Alias); + Assert.AreEqual("The invariant settings value", invariantProperty.GetValue()); + + var variantProperty = blockListItem.Settings.Properties.Last(); + Assert.AreEqual("variantText", variantProperty.Alias); + Assert.AreEqual("Another invariant settings value", variantProperty.GetValue()); + }); + } + } + + [TestCase("en-US", "en-US", ContentVariation.Nothing)] + [TestCase("en-US", "en-US", ContentVariation.Culture)] + [TestCase("en-US", "da-DK", ContentVariation.Nothing)] + [TestCase("en-US", "da-DK", ContentVariation.Culture)] + [TestCase("da-DK", "en-US", ContentVariation.Nothing)] + [TestCase("da-DK", "en-US", ContentVariation.Culture)] + [TestCase("da-DK", "da-DK", ContentVariation.Nothing)] + [TestCase("da-DK", "da-DK", ContentVariation.Culture)] + public async Task Can_Become_Invariant_After_Publish(string requestCulture, string defaultCulture, ContentVariation elementVariationAfterPublish) + { + var language = await LanguageService.GetAsync(defaultCulture); + language!.IsDefault = true; + await LanguageService.UpdateAsync(language, Constants.Security.SuperUserKey); + + var elementType = CreateElementType(ContentVariation.Culture); + var blockListDataType = await CreateBlockListDataType(elementType); + var contentType = CreateContentType(ContentVariation.Culture, blockListDataType); + + var content = CreateContent( + contentType, + elementType, + new List + { + new() { Alias = "invariantText", Value = "The invariant content value" }, + new() { Alias = "variantText", Value = "The en-US content value", Culture = "en-US" }, + new() { Alias = "variantText", Value = "The da-DK content value", Culture = "da-DK" }, + }, + new List + { + new() { Alias = "invariantText", Value = "The invariant settings value" }, + new() { Alias = "variantText", Value = "The en-US settings value", Culture = "en-US" }, + new() { Alias = "variantText", Value = "The da-DK settings value", Culture = "da-DK" }, + }, + true); + + // the content and element types are created as variant; now update the element type according to the test case + elementType.Variations = elementVariationAfterPublish; + elementType.PropertyTypes.First(pt => pt.Alias == "variantText").Variations = elementVariationAfterPublish; + ContentTypeService.Save(elementType); + + // ...and make the content type invariant + contentType.Variations = ContentVariation.Nothing; + ContentTypeService.Save(contentType); + + RefreshContentTypeCache(elementType, contentType); + + // to re-publish the content we need to set the invariant name + content = ContentService.GetById(content.Key)!; + content.Name = "Home"; + ContentService.Save(content); + PublishContent(content, contentType); + + var publishedContent = GetPublishedContent(content.Key); + + SetVariationContext(requestCulture, null); + + // the "blocks" property is invariant (at content level), but the block data currently stored is variant because the + // content type was originally variant. however, as the content type has changed to invariant, we expect no variance + // in the rendered block output, despite the variance of the element (which may or may not vary by culture, depending + // on the test case). this means that the "variantText" property should now always output the value set for the + // default language (which is also depending on the test case). + var value = publishedContent.GetProperty("blocks")!.GetValue() as BlockListModel; + Assert.IsNotNull(value); + Assert.AreEqual(1, value.Count); + + var blockListItem = value.First(); + Assert.AreEqual(2, blockListItem.Content.Properties.Count()); + Assert.Multiple(() => + { + var invariantProperty = blockListItem.Content.Properties.First(); + Assert.AreEqual("invariantText", invariantProperty.Alias); + Assert.AreEqual("The invariant content value", invariantProperty.GetValue()); + + var variantProperty = blockListItem.Content.Properties.Last(); + Assert.AreEqual("variantText", variantProperty.Alias); + Assert.AreEqual($"The {defaultCulture} content value", variantProperty.GetValue()); + }); + + Assert.AreEqual(2, blockListItem.Settings.Properties.Count()); + Assert.Multiple(() => + { + var invariantProperty = blockListItem.Settings.Properties.First(); + Assert.AreEqual("invariantText", invariantProperty.Alias); + Assert.AreEqual("The invariant settings value", invariantProperty.GetValue()); + + var variantProperty = blockListItem.Settings.Properties.Last(); + Assert.AreEqual("variantText", variantProperty.Alias); + Assert.AreEqual($"The {defaultCulture} settings value", variantProperty.GetValue()); + }); + } + + [TestCase(ContentVariation.Nothing, "en-US", "English")] + [TestCase(ContentVariation.Nothing, "da-DK", "Danish")] + [TestCase(ContentVariation.Culture, "en-US", "English")] + [TestCase(ContentVariation.Culture, "da-DK", "Danish")] + [TestCase(ContentVariation.CultureAndSegment, "en-US", "English")] + [TestCase(ContentVariation.CultureAndSegment, "da-DK", "Danish")] + [TestCase(ContentVariation.Segment, "en-US", "English")] + [TestCase(ContentVariation.Segment, "da-DK", "Danish")] + public async Task Can_Handle_Both_Content_And_Element_Level_Variation(ContentVariation elementVariation, string culture, string expectedStartsWith) + { + var elementType = CreateElementType(elementVariation); + var blockListDataType = await CreateBlockListDataType(elementType); + var contentType = CreateContentType(ContentVariation.Culture, blockListDataType, ContentVariation.Culture); + + var content = CreateContent( + contentType, + elementType, + new [] + { + new BlockProperty( + new List + { + new() { Alias = "invariantText", Value = "English invariant content value" }, + new() { Alias = "variantText", Value = "English variant content value", Culture = elementVariation.VariesByCulture() ? "en-US" : null } + }, + new List + { + new() { Alias = "invariantText", Value = "English invariant settings value" }, + new() { Alias = "variantText", Value = "English variant settings value", Culture = elementVariation.VariesByCulture() ? "en-US" : null } + }, + "en-US", + null), + new BlockProperty( + new List + { + new() { Alias = "invariantText", Value = "Danish invariant content value" }, + new() { Alias = "variantText", Value = "Danish variant content value", Culture = elementVariation.VariesByCulture() ? "da-DK" : null } + }, + new List + { + new() { Alias = "invariantText", Value = "Danish invariant settings value" }, + new() { Alias = "variantText", Value = "Danish variant settings value", Culture = elementVariation.VariesByCulture() ? "da-DK" : null } + }, + "da-DK", + null) + }, + true); + + SetVariationContext(culture, null); + + var publishedContent = GetPublishedContent(content.Key); + + var value = publishedContent.GetProperty("blocks")!.GetValue() as BlockListModel; + Assert.IsNotNull(value); + Assert.AreEqual(1, value.Count); + + var blockListItem = value.First(); + Assert.AreEqual(2, blockListItem.Content.Properties.Count()); + Assert.Multiple(() => + { + var invariantProperty = blockListItem.Content.Properties.First(); + Assert.AreEqual("invariantText", invariantProperty.Alias); + Assert.IsTrue(invariantProperty.GetValue()!.ToString()!.StartsWith(expectedStartsWith)); + + var variantProperty = blockListItem.Content.Properties.Last(); + Assert.AreEqual("variantText", variantProperty.Alias); + Assert.IsTrue(variantProperty.GetValue()!.ToString()!.StartsWith(expectedStartsWith)); + }); + + Assert.AreEqual(2, blockListItem.Settings.Properties.Count()); + Assert.Multiple(() => + { + var invariantProperty = blockListItem.Settings.Properties.First(); + Assert.AreEqual("invariantText", invariantProperty.Alias); + Assert.IsTrue(invariantProperty.GetValue()!.ToString()!.StartsWith(expectedStartsWith)); + + var variantProperty = blockListItem.Settings.Properties.Last(); + Assert.AreEqual("variantText", variantProperty.Alias); + Assert.IsTrue(variantProperty.GetValue()!.ToString()!.StartsWith(expectedStartsWith)); + }); + } + + [TestCase(ContentVariation.Culture, "en-US", "Segment1")] + [TestCase(ContentVariation.Culture, "en-US", "Segment2")] + [TestCase(ContentVariation.Culture, "da-DK", "Segment1")] + [TestCase(ContentVariation.Culture, "da-DK", "Segment2")] + [TestCase(ContentVariation.CultureAndSegment, "en-US", "Segment1")] + [TestCase(ContentVariation.CultureAndSegment, "en-US", "Segment2")] + [TestCase(ContentVariation.CultureAndSegment, "da-DK", "Segment1")] + [TestCase(ContentVariation.CultureAndSegment, "da-DK", "Segment2")] + [TestCase(ContentVariation.Segment, "en-US", "Segment1")] + [TestCase(ContentVariation.Segment, "en-US", "Segment2")] + [TestCase(ContentVariation.Segment, "da-DK", "Segment1")] + [TestCase(ContentVariation.Segment, "da-DK", "Segment2")] + public async Task Can_Handle_Variant_Element_For_Invariant_Content(ContentVariation elementVariation, string culture, string segment) + { + var elementType = CreateElementType(elementVariation); + var blockListDataType = await CreateBlockListDataType(elementType); + var contentType = CreateContentType(ContentVariation.Nothing, blockListDataType); + + var content = CreateContent( + contentType, + elementType, + new [] + { + new BlockProperty( + new List + { + new() { Alias = "invariantText", Value = "This is invariant content text" }, + new() { Alias = "variantText", Value = "This is also invariant content text" } + }, + new List + { + new() { Alias = "invariantText", Value = "This is invariant settings text" }, + new() { Alias = "variantText", Value = "This is also invariant settings text" } + }, + null, + null) + }, + true); + + SetVariationContext(culture, segment); + + var publishedContent = GetPublishedContent(content.Key); + + var value = publishedContent.GetProperty("blocks")!.GetValue() as BlockListModel; + Assert.IsNotNull(value); + Assert.AreEqual(1, value.Count); + + var blockListItem = value.First(); + Assert.AreEqual(2, blockListItem.Content.Properties.Count()); + Assert.Multiple(() => + { + var invariantProperty = blockListItem.Content.Properties.First(); + Assert.AreEqual("invariantText", invariantProperty.Alias); + Assert.AreEqual("This is invariant content text", invariantProperty.GetValue()); + + var variantProperty = blockListItem.Content.Properties.Last(); + Assert.AreEqual("variantText", variantProperty.Alias); + Assert.AreEqual("This is also invariant content text", variantProperty.GetValue()); + }); + + Assert.AreEqual(2, blockListItem.Settings.Properties.Count()); + Assert.Multiple(() => + { + var invariantProperty = blockListItem.Settings.Properties.First(); + Assert.AreEqual("invariantText", invariantProperty.Alias); + Assert.AreEqual("This is invariant settings text", invariantProperty.GetValue()); + + var variantProperty = blockListItem.Settings.Properties.Last(); + Assert.AreEqual("variantText", variantProperty.Alias); + Assert.AreEqual("This is also invariant settings text", variantProperty.GetValue()); + }); + } + + [TestCase("en-US", null)] + [TestCase("en-US", "segment1")] + [TestCase("en-US", "segment2")] + [TestCase("da-DK", null)] + [TestCase("da-DK", "segment1")] + [TestCase("da-DK", "segment2")] + public async Task Can_Combine_Element_Level_Segment_Variation_With_Document_Level_Language_Variation(string culture, string? segment) + { + var elementType = CreateElementType(ContentVariation.Segment); + var blockListDataType = await CreateBlockListDataType(elementType); + var contentType = CreateContentType(ContentVariation.CultureAndSegment, blockListDataType, ContentVariation.Culture); + + var content = CreateContent( + contentType, + elementType, + new [] + { + new BlockProperty( + new List + { + new() { Alias = "invariantText", Value = "This is invariant content text for en-US" }, + new() { Alias = "variantText", Value = "This is the default segment content text for en-US", Segment = null }, + new() { Alias = "variantText", Value = "This is the segment1 segment content text for en-US", Segment = "segment1" }, + new() { Alias = "variantText", Value = "This is the segment2 segment content text for en-US", Segment = "segment2" }, + }, + new List + { + new() { Alias = "invariantText", Value = "This is invariant settings text for en-US" }, + new() { Alias = "variantText", Value = "This is the default segment settings text for en-US", Segment = null }, + new() { Alias = "variantText", Value = "This is the segment1 segment settings text for en-US", Segment = "segment1" }, + new() { Alias = "variantText", Value = "This is the segment2 segment settings text for en-US", Segment = "segment2" } + }, + "en-US", + null), + new BlockProperty( + new List + { + new() { Alias = "invariantText", Value = "This is invariant content text for da-DK" }, + new() { Alias = "variantText", Value = "This is the default segment content text for da-DK", Segment = null }, + new() { Alias = "variantText", Value = "This is the segment1 segment content text for da-DK", Segment = "segment1" }, + new() { Alias = "variantText", Value = "This is the segment2 segment content text for da-DK", Segment = "segment2" }, + }, + new List + { + new() { Alias = "invariantText", Value = "This is invariant settings text for da-DK" }, + new() { Alias = "variantText", Value = "This is the default segment settings text for da-DK", Segment = null }, + new() { Alias = "variantText", Value = "This is the segment1 segment settings text for da-DK", Segment = "segment1" }, + new() { Alias = "variantText", Value = "This is the segment2 segment settings text for da-DK", Segment = "segment2" } + }, + "da-DK", + null) + }, + true); + + SetVariationContext(culture, segment); + + var publishedContent = GetPublishedContent(content.Key); + + var value = publishedContent.GetProperty("blocks")!.GetValue() as BlockListModel; + Assert.IsNotNull(value); + Assert.AreEqual(1, value.Count); + + var blockListItem = value.First(); + Assert.AreEqual(2, blockListItem.Content.Properties.Count()); + Assert.Multiple(() => + { + var invariantProperty = blockListItem.Content.Properties.First(); + Assert.AreEqual(ContentVariation.Nothing, invariantProperty.PropertyType.Variations); + Assert.AreEqual("invariantText", invariantProperty.Alias); + Assert.AreEqual($"This is invariant content text for {culture}", invariantProperty.GetValue()); + + var variantProperty = blockListItem.Content.Properties.Last(); + Assert.AreEqual(ContentVariation.Segment, variantProperty.PropertyType.Variations); + Assert.AreEqual("variantText", variantProperty.Alias); + Assert.AreEqual( + segment is null ? $"This is the default segment content text for {culture}" : $"This is the {segment} segment content text for {culture}", + variantProperty.GetValue()); + }); + + Assert.AreEqual(2, blockListItem.Settings.Properties.Count()); + Assert.Multiple(() => + { + var invariantProperty = blockListItem.Settings.Properties.First(); + Assert.AreEqual(ContentVariation.Nothing, invariantProperty.PropertyType.Variations); + Assert.AreEqual("invariantText", invariantProperty.Alias); + Assert.AreEqual($"This is invariant settings text for {culture}", invariantProperty.GetValue()); + + var variantProperty = blockListItem.Settings.Properties.Last(); + Assert.AreEqual(ContentVariation.Segment, variantProperty.PropertyType.Variations); + Assert.AreEqual("variantText", variantProperty.Alias); + Assert.AreEqual( + segment is null ? $"This is the default segment settings text for {culture}" : $"This is the {segment} segment settings text for {culture}", + variantProperty.GetValue()); + }); + } +} diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/PropertyEditors/BlockListElementLevelVariationTests.Publishing.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/PropertyEditors/BlockListElementLevelVariationTests.Publishing.cs new file mode 100644 index 000000000000..29131e4fa6c3 --- /dev/null +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/PropertyEditors/BlockListElementLevelVariationTests.Publishing.cs @@ -0,0 +1,1551 @@ +using NUnit.Framework; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.Blocks; +using Umbraco.Cms.Core.Models.PublishedContent; +using Umbraco.Cms.Core.PropertyEditors; +using Umbraco.Cms.Tests.Common.Builders; +using Umbraco.Cms.Tests.Common.Builders.Extensions; + +namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.PropertyEditors; + +/* +If the content type varies, then: + +1. Variant element types ALWAYS adds variation to the block property Expose, as well as to any variant element properties. +2. Invariant element types NEVER adds variation to the block property Expose, nor to any variant element properties (because there are none). + +If the content type does NOT vary, then variation is NEVER added to Expose, nor to any variant properties - regardless of the element type variation. + +This means that an invariant element cannot be "turned off" for a single variation - it's all or nothing. + +It also means that in a variant setting, the parent property variance has no effect for the variance notation for any nested blocks. +*/ +public partial class BlockListElementLevelVariationTests +{ + [Test] + public async Task Can_Publish_Cultures_Independently_Invariant_Blocks() + { + var elementType = CreateElementType(ContentVariation.Culture); + var blockListDataType = await CreateBlockListDataType(elementType); + var contentType = CreateContentType(ContentVariation.Culture, blockListDataType); + + var content = CreateContent( + contentType, + elementType, + new List + { + new() { Alias = "invariantText", Value = "The first invariant content value" }, + new() { Alias = "variantText", Value = "The first content value in English", Culture = "en-US" }, + new() { Alias = "variantText", Value = "The first content value in Danish", Culture = "da-DK" }, + }, + new List + { + new() { Alias = "invariantText", Value = "The first invariant settings value" }, + new() { Alias = "variantText", Value = "The first settings value in English", Culture = "en-US" }, + new() { Alias = "variantText", Value = "The first settings value in Danish", Culture = "da-DK" }, + }, + true); + + AssertPropertyValues("en-US", + "The first invariant content value", "The first content value in English", + "The first invariant settings value", "The first settings value in English"); + + AssertPropertyValues("da-DK", + "The first invariant content value", "The first content value in Danish", + "The first invariant settings value", "The first settings value in Danish"); + + var blockListValue = JsonSerializer.Deserialize((string)content.Properties["blocks"]!.GetValue()!); + blockListValue.ContentData[0].Values[0].Value = "The second invariant content value"; + blockListValue.ContentData[0].Values[1].Value = "The second content value in English"; + blockListValue.ContentData[0].Values[2].Value = "The second content value in Danish"; + blockListValue.SettingsData[0].Values[0].Value = "The second invariant settings value"; + blockListValue.SettingsData[0].Values[1].Value = "The second settings value in English"; + blockListValue.SettingsData[0].Values[2].Value = "The second settings value in Danish"; + + content.Properties["blocks"]!.SetValue(JsonSerializer.Serialize(blockListValue)); + ContentService.Save(content); + PublishContent(content, contentType, ["en-US"]); + + AssertPropertyValues("en-US", + "The second invariant content value", "The second content value in English", + "The second invariant settings value", "The second settings value in English"); + + AssertPropertyValues("da-DK", + "The second invariant content value", "The first content value in Danish", + "The second invariant settings value", "The first settings value in Danish"); + + PublishContent(content, contentType, ["da-DK"]); + + AssertPropertyValues("da-DK", + "The second invariant content value", "The second content value in Danish", + "The second invariant settings value", "The second settings value in Danish"); + + void AssertPropertyValues(string culture, + string expectedInvariantContentValue, string expectedVariantContentValue, + string expectedInvariantSettingsValue, string expectedVariantSettingsValue) + { + SetVariationContext(culture, null); + var publishedContent = GetPublishedContent(content.Key); + + var value = publishedContent.Value("blocks"); + Assert.IsNotNull(value); + Assert.AreEqual(1, value.Count); + + var blockListItem = value.First(); + Assert.AreEqual(2, blockListItem.Content.Properties.Count()); + Assert.Multiple(() => + { + Assert.AreEqual(expectedInvariantContentValue, blockListItem.Content.Value("invariantText")); + Assert.AreEqual(expectedVariantContentValue, blockListItem.Content.Value("variantText")); + }); + + Assert.AreEqual(2, blockListItem.Settings.Properties.Count()); + Assert.Multiple(() => + { + Assert.AreEqual(expectedInvariantSettingsValue, blockListItem.Settings.Value("invariantText")); + Assert.AreEqual(expectedVariantSettingsValue, blockListItem.Settings.Value("variantText")); + }); + } + } + + [Test] + public async Task Can_Publish_Cultures_Independently_Variant_Blocks() + { + var elementType = CreateElementType(ContentVariation.Nothing); + var blockListDataType = await CreateBlockListDataType(elementType); + var contentType = CreateContentType(ContentVariation.Culture, blockListDataType, ContentVariation.Culture); + + var content = CreateContent( + contentType, + elementType, + new [] + { + new BlockProperty( + new List + { + new() { Alias = "invariantText", Value = "English invariantText content value" }, + new() { Alias = "variantText", Value = "English variantText content value" } + }, + new List + { + new() { Alias = "invariantText", Value = "English invariantText settings value" }, + new() { Alias = "variantText", Value = "English variantText settings value" } + }, + "en-US", + null), + new BlockProperty( + new List + { + new() { Alias = "invariantText", Value = "Danish invariantText content value" }, + new() { Alias = "variantText", Value = "Danish variantText content value" } + }, + new List + { + new() { Alias = "invariantText", Value = "Danish invariantText settings value" }, + new() { Alias = "variantText", Value = "Danish variantText settings value" } + }, + "da-DK", + null) + }, + true); + + AssertPropertyValues("en-US", + "English invariantText content value", "English variantText content value", + "English invariantText settings value", "English variantText settings value"); + + AssertPropertyValues("da-DK", + "Danish invariantText content value", "Danish variantText content value", + "Danish invariantText settings value", "Danish variantText settings value"); + + var blockListValue = JsonSerializer.Deserialize((string)content.Properties["blocks"]!.GetValue("en-US")!); + blockListValue.ContentData[0].Values[0].Value = "English invariantText content value (updated)"; + blockListValue.ContentData[0].Values[1].Value = "English variantText content value (updated)"; + blockListValue.SettingsData[0].Values[0].Value = "English invariantText settings value (updated)"; + blockListValue.SettingsData[0].Values[1].Value = "English variantText settings value (updated)"; + content.Properties["blocks"]!.SetValue(JsonSerializer.Serialize(blockListValue), "en-US"); + + blockListValue = JsonSerializer.Deserialize((string)content.Properties["blocks"]!.GetValue("da-DK")!); + blockListValue.ContentData[0].Values[0].Value = "Danish invariantText content value (updated)"; + blockListValue.ContentData[0].Values[1].Value = "Danish variantText content value (updated)"; + blockListValue.SettingsData[0].Values[0].Value = "Danish invariantText settings value (updated)"; + blockListValue.SettingsData[0].Values[1].Value = "Danish variantText settings value (updated)"; + content.Properties["blocks"]!.SetValue(JsonSerializer.Serialize(blockListValue), "da-DK"); + + ContentService.Save(content); + PublishContent(content, contentType, ["en-US"]); + + AssertPropertyValues("en-US", + "English invariantText content value (updated)", "English variantText content value (updated)", + "English invariantText settings value (updated)", "English variantText settings value (updated)"); + + AssertPropertyValues("da-DK", + "Danish invariantText content value", "Danish variantText content value", + "Danish invariantText settings value", "Danish variantText settings value"); + + PublishContent(content, contentType, ["da-DK"]); + + AssertPropertyValues("da-DK", + "Danish invariantText content value (updated)", "Danish variantText content value (updated)", + "Danish invariantText settings value (updated)", "Danish variantText settings value (updated)"); + + void AssertPropertyValues(string culture, + string expectedInvariantContentValue, string expectedVariantContentValue, + string expectedInvariantSettingsValue, string expectedVariantSettingsValue) + { + SetVariationContext(culture, null); + var publishedContent = GetPublishedContent(content.Key); + + var value = publishedContent.Value("blocks"); + Assert.IsNotNull(value); + Assert.AreEqual(1, value.Count); + + var blockListItem = value.First(); + Assert.AreEqual(2, blockListItem.Content.Properties.Count()); + Assert.Multiple(() => + { + Assert.AreEqual(expectedInvariantContentValue, blockListItem.Content.Value("invariantText")); + Assert.AreEqual(expectedVariantContentValue, blockListItem.Content.Value("variantText")); + }); + + Assert.AreEqual(2, blockListItem.Settings.Properties.Count()); + Assert.Multiple(() => + { + Assert.AreEqual(expectedInvariantSettingsValue, blockListItem.Settings.Value("invariantText")); + Assert.AreEqual(expectedVariantSettingsValue, blockListItem.Settings.Value("variantText")); + }); + } + } + + [Test] + public async Task Can_Publish_Cultures_Independently_Nested_Invariant_Blocks() + { + var nestedElementType = CreateElementType(ContentVariation.Culture); + var nestedBlockListDataType = await CreateBlockListDataType(nestedElementType); + + var rootElementType = new ContentTypeBuilder() + .WithAlias("myRootElementType") + .WithName("My Root Element Type") + .WithIsElement(true) + .WithContentVariation(ContentVariation.Culture) + .AddPropertyType() + .WithAlias("invariantText") + .WithName("Invariant text") + .WithDataTypeId(Constants.DataTypes.Textbox) + .WithPropertyEditorAlias(Constants.PropertyEditors.Aliases.TextBox) + .WithValueStorageType(ValueStorageType.Nvarchar) + .WithVariations(ContentVariation.Nothing) + .Done() + .AddPropertyType() + .WithAlias("variantText") + .WithName("Variant text") + .WithDataTypeId(Constants.DataTypes.Textbox) + .WithPropertyEditorAlias(Constants.PropertyEditors.Aliases.TextBox) + .WithValueStorageType(ValueStorageType.Nvarchar) + .WithVariations(ContentVariation.Culture) + .Done() + .AddPropertyType() + .WithAlias("nestedBlocks") + .WithName("Nested blocks") + .WithDataTypeId(nestedBlockListDataType.Id) + .WithPropertyEditorAlias(Constants.PropertyEditors.Aliases.BlockList) + .WithValueStorageType(ValueStorageType.Ntext) + .WithVariations(ContentVariation.Nothing) + .Done() + .Build(); + ContentTypeService.Save(rootElementType); + var rootBlockListDataType = await CreateBlockListDataType(rootElementType); + var contentType = CreateContentType(ContentVariation.Culture, rootBlockListDataType); + + var nestedElementContentKey = Guid.NewGuid(); + var nestedElementSettingsKey = Guid.NewGuid(); + var content = CreateContent( + contentType, + rootElementType, + new List + { + new() + { + Alias = "nestedBlocks", + Value = BlockListPropertyValue( + nestedElementType, + nestedElementContentKey, + nestedElementSettingsKey, + new BlockProperty( + new List + { + new() { Alias = "invariantText", Value = "The first nested invariant content value" }, + new() { Alias = "variantText", Value = "The first nested content value in English", Culture = "en-US" }, + new() { Alias = "variantText", Value = "The first nested content value in Danish", Culture = "da-DK" }, + }, + new List + { + new() { Alias = "invariantText", Value = "The first nested invariant settings value" }, + new() { Alias = "variantText", Value = "The first nested settings value in English", Culture = "en-US" }, + new() { Alias = "variantText", Value = "The first nested settings value in Danish", Culture = "da-DK" }, + }, + null, + null)) + }, + new() { Alias = "invariantText", Value = "The first root invariant content value" }, + new() { Alias = "variantText", Value = "The first root content value in English", Culture = "en-US" }, + new() { Alias = "variantText", Value = "The first root content value in Danish", Culture = "da-DK" }, + }, + [], + true); + + AssertPropertyValues( + "en-US", + (rootBlockContent, nestedBlockContent, nestedBlockSetting) => + { + Assert.AreEqual("The first root invariant content value", rootBlockContent.Value("invariantText")); + Assert.AreEqual("The first root content value in English", rootBlockContent.Value("variantText")); + + Assert.AreEqual("The first nested invariant content value", nestedBlockContent.Value("invariantText")); + Assert.AreEqual("The first nested content value in English", nestedBlockContent.Value("variantText")); + + Assert.AreEqual("The first nested invariant settings value", nestedBlockSetting.Value("invariantText")); + Assert.AreEqual("The first nested settings value in English", nestedBlockSetting.Value("variantText")); + }); + + AssertPropertyValues( + "da-DK", + (rootBlockContent, nestedBlockContent, nestedBlockSetting) => + { + Assert.AreEqual("The first root invariant content value", rootBlockContent.Value("invariantText")); + Assert.AreEqual("The first root content value in Danish", rootBlockContent.Value("variantText")); + + Assert.AreEqual("The first nested invariant content value", nestedBlockContent.Value("invariantText")); + Assert.AreEqual("The first nested content value in Danish", nestedBlockContent.Value("variantText")); + + Assert.AreEqual("The first nested invariant settings value", nestedBlockSetting.Value("invariantText")); + Assert.AreEqual("The first nested settings value in Danish", nestedBlockSetting.Value("variantText")); + }); + + var blockListValue = JsonSerializer.Deserialize((string)content.Properties["blocks"]!.GetValue()!); + blockListValue.ContentData[0].Values[0].Value = BlockListPropertyValue( + nestedElementType, + nestedElementContentKey, + nestedElementSettingsKey, + new BlockProperty( + new List + { + new() { Alias = "invariantText", Value = "The second nested invariant content value" }, + new() { Alias = "variantText", Value = "The second nested content value in English", Culture = "en-US" }, + new() { Alias = "variantText", Value = "The second nested content value in Danish", Culture = "da-DK" }, + }, + new List + { + new() { Alias = "invariantText", Value = "The second nested invariant settings value" }, + new() { Alias = "variantText", Value = "The second nested settings value in English", Culture = "en-US" }, + new() { Alias = "variantText", Value = "The second nested settings value in Danish", Culture = "da-DK" }, + }, + null, + null)); + blockListValue.ContentData[0].Values[1].Value = "The second root invariant content value"; + blockListValue.ContentData[0].Values[2].Value = "The second root content value in English"; + blockListValue.ContentData[0].Values[3].Value = "The second root content value in Danish"; + + content.Properties["blocks"]!.SetValue(JsonSerializer.Serialize(blockListValue)); + ContentService.Save(content); + PublishContent(content, contentType, ["en-US"]); + + AssertPropertyValues( + "en-US", + (rootBlockContent, nestedBlockContent, nestedBlockSetting) => + { + Assert.AreEqual("The second root invariant content value", rootBlockContent.Value("invariantText")); + Assert.AreEqual("The second root content value in English", rootBlockContent.Value("variantText")); + + Assert.AreEqual("The second nested invariant content value", nestedBlockContent.Value("invariantText")); + Assert.AreEqual("The second nested content value in English", nestedBlockContent.Value("variantText")); + + Assert.AreEqual("The second nested invariant settings value", nestedBlockSetting.Value("invariantText")); + Assert.AreEqual("The second nested settings value in English", nestedBlockSetting.Value("variantText")); + }); + + AssertPropertyValues( + "da-DK", + (rootBlockContent, nestedBlockContent, nestedBlockSetting) => + { + Assert.AreEqual("The second root invariant content value", rootBlockContent.Value("invariantText")); + Assert.AreEqual("The first root content value in Danish", rootBlockContent.Value("variantText")); + + Assert.AreEqual("The second nested invariant content value", nestedBlockContent.Value("invariantText")); + Assert.AreEqual("The first nested content value in Danish", nestedBlockContent.Value("variantText")); + + Assert.AreEqual("The second nested invariant settings value", nestedBlockSetting.Value("invariantText")); + Assert.AreEqual("The first nested settings value in Danish", nestedBlockSetting.Value("variantText")); + }); + + PublishContent(content, contentType, ["da-DK"]); + + AssertPropertyValues( + "da-DK", + (rootBlockContent, nestedBlockContent, nestedBlockSetting) => + { + Assert.AreEqual("The second root invariant content value", rootBlockContent.Value("invariantText")); + Assert.AreEqual("The second root content value in Danish", rootBlockContent.Value("variantText")); + + Assert.AreEqual("The second nested invariant content value", nestedBlockContent.Value("invariantText")); + Assert.AreEqual("The second nested content value in Danish", nestedBlockContent.Value("variantText")); + + Assert.AreEqual("The second nested invariant settings value", nestedBlockSetting.Value("invariantText")); + Assert.AreEqual("The second nested settings value in Danish", nestedBlockSetting.Value("variantText")); + }); + + void AssertPropertyValues(string culture, Action validateBlockValues) + { + SetVariationContext(culture, null); + var publishedContent = GetPublishedContent(content.Key); + + var rootBlock = publishedContent.Value("blocks"); + Assert.IsNotNull(rootBlock); + Assert.AreEqual(1, rootBlock.Count); + Assert.Multiple(() => + { + var rootBlockContent = rootBlock.First().Content; + + var nestedBlock = rootBlockContent.Value("nestedBlocks"); + Assert.IsNotNull(nestedBlock); + Assert.AreEqual(1, nestedBlock.Count); + + var nestedBlockContent = nestedBlock.First().Content; + var nestedBlockSettings = nestedBlock.First().Settings; + + validateBlockValues(rootBlockContent, nestedBlockContent, nestedBlockSettings); + }); + } + } + + [Test] + public async Task Can_Publish_Cultures_Independently_Nested_Variant_Blocks() + { + var nestedElementType = CreateElementType(ContentVariation.Nothing); + var nestedBlockListDataType = await CreateBlockListDataType(nestedElementType); + + var rootElementType = new ContentTypeBuilder() + .WithAlias("myRootElementType") + .WithName("My Root Element Type") + .WithIsElement(true) + .WithContentVariation(ContentVariation.Culture) + .AddPropertyType() + .WithAlias("invariantText") + .WithName("Invariant text") + .WithDataTypeId(Constants.DataTypes.Textbox) + .WithPropertyEditorAlias(Constants.PropertyEditors.Aliases.TextBox) + .WithValueStorageType(ValueStorageType.Nvarchar) + .WithVariations(ContentVariation.Nothing) + .Done() + .AddPropertyType() + .WithAlias("variantText") + .WithName("Variant text") + .WithDataTypeId(Constants.DataTypes.Textbox) + .WithPropertyEditorAlias(Constants.PropertyEditors.Aliases.TextBox) + .WithValueStorageType(ValueStorageType.Nvarchar) + .WithVariations(ContentVariation.Culture) + .Done() + .AddPropertyType() + .WithAlias("nestedBlocks") + .WithName("Nested blocks") + .WithDataTypeId(nestedBlockListDataType.Id) + .WithPropertyEditorAlias(Constants.PropertyEditors.Aliases.BlockList) + .WithValueStorageType(ValueStorageType.Ntext) + .WithVariations(ContentVariation.Culture) + .Done() + .Build(); + ContentTypeService.Save(rootElementType); + var rootBlockListDataType = await CreateBlockListDataType(rootElementType); + var contentType = CreateContentType(ContentVariation.Culture, rootBlockListDataType); + + var nestedElementContentKeyEnUs = Guid.NewGuid(); + var nestedElementSettingsKeyEnUs = Guid.NewGuid(); + var nestedElementContentKeyDaDk = Guid.NewGuid(); + var nestedElementSettingsKeyDaDk = Guid.NewGuid(); + var content = CreateContent( + contentType, + rootElementType, + new List + { + new() + { + Alias = "nestedBlocks", + Value = BlockListPropertyValue( + nestedElementType, + nestedElementContentKeyEnUs, + nestedElementSettingsKeyEnUs, + new BlockProperty( + new List + { + new() { Alias = "invariantText", Value = "The first nested invariant content value" }, + new() { Alias = "variantText", Value = "The first nested content value in English" } + }, + new List + { + new() { Alias = "invariantText", Value = "The first nested invariant settings value" }, + new() { Alias = "variantText", Value = "The first nested settings value in English" } + }, + null, + null)), + Culture = "en-US" + }, + new() + { + Alias = "nestedBlocks", + Value = BlockListPropertyValue( + nestedElementType, + nestedElementContentKeyDaDk, + nestedElementSettingsKeyDaDk, + new BlockProperty( + new List + { + new() { Alias = "invariantText", Value = "The first nested invariant content value" }, + new() { Alias = "variantText", Value = "The first nested content value in Danish" } + }, + new List + { + new() { Alias = "invariantText", Value = "The first nested invariant settings value" }, + new() { Alias = "variantText", Value = "The first nested settings value in Danish" } + }, + null, + null)), + Culture = "da-DK" + }, + new() { Alias = "invariantText", Value = "The first root invariant content value" }, + new() { Alias = "variantText", Value = "The first root content value in English", Culture = "en-US" }, + new() { Alias = "variantText", Value = "The first root content value in Danish", Culture = "da-DK" }, + }, + [], + true); + + AssertPropertyValues( + "en-US", + (rootBlockContent, nestedBlockContent, nestedBlockSetting) => + { + Assert.AreEqual("The first root invariant content value", rootBlockContent.Value("invariantText")); + Assert.AreEqual("The first root content value in English", rootBlockContent.Value("variantText")); + + Assert.AreEqual("The first nested invariant content value", nestedBlockContent.Value("invariantText")); + Assert.AreEqual("The first nested content value in English", nestedBlockContent.Value("variantText")); + + Assert.AreEqual("The first nested invariant settings value", nestedBlockSetting.Value("invariantText")); + Assert.AreEqual("The first nested settings value in English", nestedBlockSetting.Value("variantText")); + }); + + AssertPropertyValues( + "da-DK", + (rootBlockContent, nestedBlockContent, nestedBlockSetting) => + { + Assert.AreEqual("The first root invariant content value", rootBlockContent.Value("invariantText")); + Assert.AreEqual("The first root content value in Danish", rootBlockContent.Value("variantText")); + + Assert.AreEqual("The first nested invariant content value", nestedBlockContent.Value("invariantText")); + Assert.AreEqual("The first nested content value in Danish", nestedBlockContent.Value("variantText")); + + Assert.AreEqual("The first nested invariant settings value", nestedBlockSetting.Value("invariantText")); + Assert.AreEqual("The first nested settings value in Danish", nestedBlockSetting.Value("variantText")); + }); + + var blockListValue = JsonSerializer.Deserialize((string)content.Properties["blocks"]!.GetValue()!); + blockListValue.ContentData[0].Values[0].Value = BlockListPropertyValue( + nestedElementType, + nestedElementContentKeyEnUs, + nestedElementSettingsKeyEnUs, + new BlockProperty( + new List + { + new() { Alias = "invariantText", Value = "The second nested invariant content value" }, + new() { Alias = "variantText", Value = "The second nested content value in English" } + }, + new List + { + new() { Alias = "invariantText", Value = "The second nested invariant settings value" }, + new() { Alias = "variantText", Value = "The second nested settings value in English" } + }, + null, + null)); + blockListValue.ContentData[0].Values[1].Value = BlockListPropertyValue( + nestedElementType, + nestedElementContentKeyDaDk, + nestedElementSettingsKeyDaDk, + new BlockProperty( + new List + { + new() { Alias = "invariantText", Value = "The second nested invariant content value" }, + new() { Alias = "variantText", Value = "The second nested content value in Danish" } + }, + new List + { + new() { Alias = "invariantText", Value = "The second nested invariant settings value" }, + new() { Alias = "variantText", Value = "The second nested settings value in Danish" } + }, + null, + null)); + blockListValue.ContentData[0].Values[2].Value = "The second root invariant content value"; + blockListValue.ContentData[0].Values[3].Value = "The second root content value in English"; + blockListValue.ContentData[0].Values[4].Value = "The second root content value in Danish"; + + content.Properties["blocks"]!.SetValue(JsonSerializer.Serialize(blockListValue)); + ContentService.Save(content); + PublishContent(content, contentType, ["en-US"]); + + AssertPropertyValues( + "en-US", + (rootBlockContent, nestedBlockContent, nestedBlockSetting) => + { + Assert.AreEqual("The second root invariant content value", rootBlockContent.Value("invariantText")); + Assert.AreEqual("The second root content value in English", rootBlockContent.Value("variantText")); + + Assert.AreEqual("The second nested invariant content value", nestedBlockContent.Value("invariantText")); + Assert.AreEqual("The second nested content value in English", nestedBlockContent.Value("variantText")); + + Assert.AreEqual("The second nested invariant settings value", nestedBlockSetting.Value("invariantText")); + Assert.AreEqual("The second nested settings value in English", nestedBlockSetting.Value("variantText")); + }); + + AssertPropertyValues( + "da-DK", + (rootBlockContent, nestedBlockContent, nestedBlockSetting) => + { + Assert.AreEqual("The second root invariant content value", rootBlockContent.Value("invariantText")); + Assert.AreEqual("The first root content value in Danish", rootBlockContent.Value("variantText")); + + Assert.AreEqual("The first nested invariant content value", nestedBlockContent.Value("invariantText")); + Assert.AreEqual("The first nested content value in Danish", nestedBlockContent.Value("variantText")); + + Assert.AreEqual("The first nested invariant settings value", nestedBlockSetting.Value("invariantText")); + Assert.AreEqual("The first nested settings value in Danish", nestedBlockSetting.Value("variantText")); + }); + + PublishContent(content, contentType, ["da-DK"]); + + AssertPropertyValues( + "da-DK", + (rootBlockContent, nestedBlockContent, nestedBlockSetting) => + { + Assert.AreEqual("The second root invariant content value", rootBlockContent.Value("invariantText")); + Assert.AreEqual("The second root content value in Danish", rootBlockContent.Value("variantText")); + + Assert.AreEqual("The second nested invariant content value", nestedBlockContent.Value("invariantText")); + Assert.AreEqual("The second nested content value in Danish", nestedBlockContent.Value("variantText")); + + Assert.AreEqual("The second nested invariant settings value", nestedBlockSetting.Value("invariantText")); + Assert.AreEqual("The second nested settings value in Danish", nestedBlockSetting.Value("variantText")); + }); + + void AssertPropertyValues(string culture, Action validateBlockValues) + { + SetVariationContext(culture, null); + var publishedContent = GetPublishedContent(content.Key); + + var rootBlock = publishedContent.Value("blocks"); + Assert.IsNotNull(rootBlock); + Assert.AreEqual(1, rootBlock.Count); + Assert.Multiple(() => + { + var rootBlockContent = rootBlock.First().Content; + + var nestedBlock = rootBlockContent.Value("nestedBlocks"); + Assert.IsNotNull(nestedBlock); + Assert.AreEqual(1, nestedBlock.Count); + + var nestedBlockContent = nestedBlock.First().Content; + var nestedBlockSettings = nestedBlock.First().Settings; + + validateBlockValues(rootBlockContent, nestedBlockContent, nestedBlockSettings); + }); + } + } + + [Test] + public async Task Can_Publish_Cultures_Independently_With_Segments() + { + var elementType = CreateElementType(ContentVariation.CultureAndSegment); + var blockListDataType = await CreateBlockListDataType(elementType); + var contentType = CreateContentType(ContentVariation.CultureAndSegment, blockListDataType); + + var content = CreateContent( + contentType, + elementType, + new List + { + new() { Alias = "invariantText", Value = "The first invariant content value" }, + new() { Alias = "variantText", Value = "The first content value in English", Culture = "en-US" }, + new() { Alias = "variantText", Value = "The first content value in English (Segment 1)", Culture = "en-US", Segment = "s1" }, + new() { Alias = "variantText", Value = "The first content value in English (Segment 2)", Culture = "en-US", Segment = "s2" }, + new() { Alias = "variantText", Value = "The first content value in Danish", Culture = "da-DK" }, + new() { Alias = "variantText", Value = "The first content value in Danish (Segment 1)", Culture = "da-DK", Segment = "s1" }, + new() { Alias = "variantText", Value = "The first content value in Danish (Segment 2)", Culture = "da-DK", Segment = "s2" }, + }, + [], + true); + + AssertPropertyValues("en-US", null, + "The first invariant content value", "The first content value in English"); + + AssertPropertyValues("en-US", "s1", + "The first invariant content value", "The first content value in English (Segment 1)"); + + AssertPropertyValues("en-US", "s2", + "The first invariant content value", "The first content value in English (Segment 2)"); + + AssertPropertyValues("da-DK", null, + "The first invariant content value", "The first content value in Danish"); + + AssertPropertyValues("da-DK", "s1", + "The first invariant content value", "The first content value in Danish (Segment 1)"); + + AssertPropertyValues("da-DK", "s2", + "The first invariant content value", "The first content value in Danish (Segment 2)"); + + var blockListValue = JsonSerializer.Deserialize((string)content.Properties["blocks"]!.GetValue()!); + blockListValue.ContentData[0].Values[0].Value = "The second invariant content value"; + blockListValue.ContentData[0].Values[1].Value = "The second content value in English"; + blockListValue.ContentData[0].Values[2].Value = "The second content value in English (Segment 1)"; + blockListValue.ContentData[0].Values[3].Value = "The second content value in English (Segment 2)"; + blockListValue.ContentData[0].Values[4].Value = "The second content value in Danish"; + blockListValue.ContentData[0].Values[5].Value = "The second content value in Danish (Segment 1)"; + blockListValue.ContentData[0].Values[6].Value = "The second content value in Danish (Segment 2)"; + + content.Properties["blocks"]!.SetValue(JsonSerializer.Serialize(blockListValue)); + ContentService.Save(content); + PublishContent(content, contentType, ["en-US"]); + + AssertPropertyValues("en-US", null, + "The second invariant content value", "The second content value in English"); + + AssertPropertyValues("en-US", "s1", + "The second invariant content value", "The second content value in English (Segment 1)"); + + AssertPropertyValues("en-US", "s2", + "The second invariant content value", "The second content value in English (Segment 2)"); + + AssertPropertyValues("da-DK", null, + "The second invariant content value", "The first content value in Danish"); + + AssertPropertyValues("da-DK", "s1", + "The second invariant content value", "The first content value in Danish (Segment 1)"); + + AssertPropertyValues("da-DK", "s2", + "The second invariant content value", "The first content value in Danish (Segment 2)"); + + PublishContent(content, contentType, ["da-DK"]); + + AssertPropertyValues("da-DK", null, + "The second invariant content value", "The second content value in Danish"); + + AssertPropertyValues("da-DK", "s1", + "The second invariant content value", "The second content value in Danish (Segment 1)"); + + AssertPropertyValues("da-DK", "s2", + "The second invariant content value", "The second content value in Danish (Segment 2)"); + + void AssertPropertyValues(string culture, string? segment, string expectedInvariantContentValue, string expectedVariantContentValue) + { + SetVariationContext(culture, segment); + var publishedContent = GetPublishedContent(content.Key); + + var value = publishedContent.Value("blocks"); + Assert.IsNotNull(value); + Assert.AreEqual(1, value.Count); + + var blockListItem = value.First(); + Assert.AreEqual(2, blockListItem.Content.Properties.Count()); + Assert.Multiple(() => + { + Assert.AreEqual(expectedInvariantContentValue, blockListItem.Content.Value("invariantText")); + Assert.AreEqual(expectedVariantContentValue, blockListItem.Content.Value("variantText")); + }); + } + } + + [Test] + public async Task Can_Publish_With_Segments() + { + var elementType = CreateElementType(ContentVariation.Segment); + var blockListDataType = await CreateBlockListDataType(elementType); + var contentType = CreateContentType(ContentVariation.Segment, blockListDataType); + + var content = CreateContent( + contentType, + elementType, + new List + { + new() { Alias = "invariantText", Value = "The first invariant content value" }, + new() { Alias = "variantText", Value = "The first content value" }, + new() { Alias = "variantText", Value = "The first content value (Segment 1)", Segment = "s1" }, + new() { Alias = "variantText", Value = "The first content value (Segment 2)", Segment = "s2" } + }, + [], + true); + + AssertPropertyValues(null, "The first invariant content value", "The first content value"); + + AssertPropertyValues("s1", "The first invariant content value", "The first content value (Segment 1)"); + + AssertPropertyValues("s2", "The first invariant content value", "The first content value (Segment 2)"); + + var blockListValue = JsonSerializer.Deserialize((string)content.Properties["blocks"]!.GetValue()!); + blockListValue.ContentData[0].Values[0].Value = "The second invariant content value"; + blockListValue.ContentData[0].Values[1].Value = "The second content value"; + blockListValue.ContentData[0].Values[2].Value = "The second content value (Segment 1)"; + blockListValue.ContentData[0].Values[3].Value = "The second content value (Segment 2)"; + + content.Properties["blocks"]!.SetValue(JsonSerializer.Serialize(blockListValue)); + ContentService.Save(content); + PublishContent(content, contentType); + + AssertPropertyValues(null, "The second invariant content value", "The second content value"); + + AssertPropertyValues("s1", "The second invariant content value", "The second content value (Segment 1)"); + + AssertPropertyValues("s2", "The second invariant content value", "The second content value (Segment 2)"); + + void AssertPropertyValues(string? segment, string expectedInvariantContentValue, string expectedVariantContentValue) + { + SetVariationContext(null, segment); + var publishedContent = GetPublishedContent(content.Key); + + var value = publishedContent.Value("blocks"); + Assert.IsNotNull(value); + Assert.AreEqual(1, value.Count); + + var blockListItem = value.First(); + Assert.AreEqual(2, blockListItem.Content.Properties.Count()); + Assert.Multiple(() => + { + Assert.AreEqual(expectedInvariantContentValue, blockListItem.Content.Value("invariantText")); + Assert.AreEqual(expectedVariantContentValue, blockListItem.Content.Value("variantText")); + }); + } + } + + [Test] + public async Task Can_Publish_With_Blocks_Removed() + { + var elementType = CreateElementType(ContentVariation.Culture); + var blockListDataType = await CreateBlockListDataType(elementType); + var contentType = CreateContentType(ContentVariation.Culture, blockListDataType); + + var content = CreateContent(contentType, elementType, [], false); + var blockListValue = BlockListPropertyValue( + elementType, + [ + ( + Guid.NewGuid(), + Guid.NewGuid(), + new BlockProperty( + new List { + new() { Alias = "invariantText", Value = "#1: The first invariant content value" }, + new() { Alias = "variantText", Value = "#1: The first content value in English", Culture = "en-US" }, + new() { Alias = "variantText", Value = "#1: The first content value in Danish", Culture = "da-DK" } + }, + [], + null, + null + ) + ), + ( + Guid.NewGuid(), + Guid.NewGuid(), + new BlockProperty( + new List { + new() { Alias = "invariantText", Value = "#2: The first invariant content value" }, + new() { Alias = "variantText", Value = "#2: The first content value in English", Culture = "en-US" }, + new() { Alias = "variantText", Value = "#2: The first content value in Danish", Culture = "da-DK" } + }, + [], + null, + null + ) + ), + ( + Guid.NewGuid(), + Guid.NewGuid(), + new BlockProperty( + new List { + new() { Alias = "invariantText", Value = "#3: The first invariant content value" }, + new() { Alias = "variantText", Value = "#3: The first content value in English", Culture = "en-US" }, + new() { Alias = "variantText", Value = "#3: The first content value in Danish", Culture = "da-DK" } + }, + [], + null, + null + ) + ), + ] + ); + + content.Properties["blocks"]!.SetValue(JsonSerializer.Serialize(blockListValue)); + ContentService.Save(content); + PublishContent(content, contentType, ["en-US", "da-DK"]); + + AssertPropertyValues("en-US", 3, blocks => + { + Assert.AreEqual("#1: The first invariant content value", blocks[0].Content.Value("invariantText")); + Assert.AreEqual("#1: The first content value in English", blocks[0].Content.Value("variantText")); + + Assert.AreEqual("#2: The first invariant content value", blocks[1].Content.Value("invariantText")); + Assert.AreEqual("#2: The first content value in English", blocks[1].Content.Value("variantText")); + + Assert.AreEqual("#3: The first invariant content value", blocks[2].Content.Value("invariantText")); + Assert.AreEqual("#3: The first content value in English", blocks[2].Content.Value("variantText")); + }); + + AssertPropertyValues("da-DK", 3, blocks => + { + Assert.AreEqual("#1: The first invariant content value", blocks[0].Content.Value("invariantText")); + Assert.AreEqual("#1: The first content value in Danish", blocks[0].Content.Value("variantText")); + + Assert.AreEqual("#2: The first invariant content value", blocks[1].Content.Value("invariantText")); + Assert.AreEqual("#2: The first content value in Danish", blocks[1].Content.Value("variantText")); + + Assert.AreEqual("#3: The first invariant content value", blocks[2].Content.Value("invariantText")); + Assert.AreEqual("#3: The first content value in Danish", blocks[2].Content.Value("variantText")); + }); + + // remove block #2 + blockListValue.Layout[blockListValue.Layout.First().Key] = + [ + blockListValue.Layout.First().Value.First(), + blockListValue.Layout.First().Value.Last() + ]; + blockListValue.ContentData.RemoveAt(1); + blockListValue.SettingsData.RemoveAt(1); + + blockListValue.ContentData[0].Values[0].Value = "#1: The second invariant content value"; + blockListValue.ContentData[0].Values[1].Value = "#1: The second content value in English"; + blockListValue.ContentData[0].Values[2].Value = "#1: The second content value in Danish"; + blockListValue.ContentData[1].Values[0].Value = "#3: The second invariant content value"; + blockListValue.ContentData[1].Values[1].Value = "#3: The second content value in English"; + blockListValue.ContentData[1].Values[2].Value = "#3: The second content value in Danish"; + + content.Properties["blocks"]!.SetValue(JsonSerializer.Serialize(blockListValue)); + + ContentService.Save(content); + PublishContent(content, contentType, ["en-US"]); + + AssertPropertyValues("en-US", 2, blocks => + { + Assert.AreEqual("#1: The second invariant content value", blocks[0].Content.Value("invariantText")); + Assert.AreEqual("#1: The second content value in English", blocks[0].Content.Value("variantText")); + + Assert.AreEqual("#3: The second invariant content value", blocks[1].Content.Value("invariantText")); + Assert.AreEqual("#3: The second content value in English", blocks[1].Content.Value("variantText")); + }); + + AssertPropertyValues("da-DK", 2, blocks => + { + Assert.AreEqual("#1: The second invariant content value", blocks[0].Content.Value("invariantText")); + Assert.AreEqual("#1: The first content value in Danish", blocks[0].Content.Value("variantText")); + + Assert.AreEqual("#3: The second invariant content value", blocks[1].Content.Value("invariantText")); + Assert.AreEqual("#3: The first content value in Danish", blocks[1].Content.Value("variantText")); + }); + + PublishContent(content, contentType, ["da-DK"]); + + AssertPropertyValues("da-DK", 2, blocks => + { + Assert.AreEqual("#1: The second invariant content value", blocks[0].Content.Value("invariantText")); + Assert.AreEqual("#1: The second content value in Danish", blocks[0].Content.Value("variantText")); + + Assert.AreEqual("#3: The second invariant content value", blocks[1].Content.Value("invariantText")); + Assert.AreEqual("#3: The second content value in Danish", blocks[1].Content.Value("variantText")); + }); + + void AssertPropertyValues(string culture, int numberOfExpectedBlocks, Action validateBlocks) + { + SetVariationContext(culture, null); + var publishedContent = GetPublishedContent(content.Key); + + var value = publishedContent.Value("blocks"); + Assert.IsNotNull(value); + Assert.AreEqual(numberOfExpectedBlocks, value.Count); + + validateBlocks(value); + } + } + + [Test] + public async Task Can_Publish_With_Blocks_In_One_Language() + { + var elementType = CreateElementType(ContentVariation.Culture); + var blockListDataType = await CreateBlockListDataType(elementType); + var contentType = CreateContentType(ContentVariation.Culture, blockListDataType); + + var content = CreateContent(contentType, elementType, [], false); + var firstBlockContentElementKey = Guid.NewGuid(); + var firstBlockSettingsElementKey = Guid.NewGuid(); + var blockListValue = BlockListPropertyValue( + elementType, + [ + ( + firstBlockContentElementKey, + firstBlockSettingsElementKey, + new BlockProperty( + new List { + new() { Alias = "invariantText", Value = "#1: The first invariant content value" }, + new() { Alias = "variantText", Value = "#1: The first content value in English", Culture = "en-US" }, + new() { Alias = "variantText", Value = "#1: The first content value in Danish", Culture = "da-DK" } + }, + [], + null, + null + ) + ) + ] + ); + + content.Properties["blocks"]!.SetValue(JsonSerializer.Serialize(blockListValue)); + ContentService.Save(content); + PublishContent(content, contentType, ["en-US", "da-DK"]); + + AssertPropertyValues("en-US", 1, blocks => + { + Assert.AreEqual("#1: The first invariant content value", blocks[0].Content.Value("invariantText")); + Assert.AreEqual("#1: The first content value in English", blocks[0].Content.Value("variantText")); + }); + + AssertPropertyValues("da-DK", 1, blocks => + { + Assert.AreEqual("#1: The first invariant content value", blocks[0].Content.Value("invariantText")); + Assert.AreEqual("#1: The first content value in Danish", blocks[0].Content.Value("variantText")); + }); + + // Add one more block + blockListValue = BlockListPropertyValue( + elementType, + [ + ( + firstBlockContentElementKey, + firstBlockSettingsElementKey, + new BlockProperty( + new List { + new() { Alias = "invariantText", Value = "#1: The second invariant content value" }, + new() { Alias = "variantText", Value = "#1: The second content value in English", Culture = "en-US" }, + new() { Alias = "variantText", Value = "#1: The second content value in Danish", Culture = "da-DK" } + }, + [], + null, + null + ) + ), + ( + Guid.NewGuid(), + Guid.NewGuid(), + new BlockProperty( + new List { + new() { Alias = "invariantText", Value = "#2: The second invariant content value" }, + new() { Alias = "variantText", Value = "#2: The second content value in English", Culture = "en-US" } + }, + [], + null, + null + ) + ) + ] + ); + + content.Properties["blocks"]!.SetValue(JsonSerializer.Serialize(blockListValue)); + + ContentService.Save(content); + PublishContent(content, contentType, ["en-US"]); + + AssertPropertyValues("en-US", 2, blocks => + { + Assert.AreEqual("#1: The second invariant content value", blocks[0].Content.Value("invariantText")); + Assert.AreEqual("#1: The second content value in English", blocks[0].Content.Value("variantText")); + + Assert.AreEqual("#2: The second invariant content value", blocks[1].Content.Value("invariantText")); + Assert.AreEqual("#2: The second content value in English", blocks[1].Content.Value("variantText")); + }); + + AssertPropertyValues("da-DK", 1, blocks => + { + Assert.AreEqual("#1: The second invariant content value", blocks[0].Content.Value("invariantText")); + Assert.AreEqual("#1: The first content value in Danish", blocks[0].Content.Value("variantText")); + }); + + PublishContent(content, contentType, ["da-DK"]); + + AssertPropertyValues("da-DK", 1, blocks => + { + Assert.AreEqual("#1: The second invariant content value", blocks[0].Content.Value("invariantText")); + Assert.AreEqual("#1: The second content value in Danish", blocks[0].Content.Value("variantText")); + }); + + void AssertPropertyValues(string culture, int numberOfExpectedBlocks, Action validateBlocks) + { + SetVariationContext(culture, null); + var publishedContent = GetPublishedContent(content.Key); + + var value = publishedContent.Value("blocks"); + Assert.IsNotNull(value); + Assert.AreEqual(numberOfExpectedBlocks, value.Count); + + validateBlocks(value); + } + } + + [Test] + public async Task Can_Publish_With_Blocks_Exposed() + { + var elementType = CreateElementType(ContentVariation.Culture); + var blockListDataType = await CreateBlockListDataType(elementType); + var contentType = CreateContentType(ContentVariation.Culture, blockListDataType); + + var content = CreateContent(contentType, elementType, [], false); + var blockListValue = BlockListPropertyValue( + elementType, + [ + ( + Guid.NewGuid(), + Guid.NewGuid(), + new BlockProperty( + new List { + new() { Alias = "invariantText", Value = "#1: The invariant content value" }, + new() { Alias = "variantText", Value = "#1: The content value in English", Culture = "en-US" }, + new() { Alias = "variantText", Value = "#1: The content value in Danish", Culture = "da-DK" } + }, + [], + null, + null + ) + ), + ( + Guid.NewGuid(), + Guid.NewGuid(), + new BlockProperty( + new List { + new() { Alias = "invariantText", Value = "#2: The invariant content value" }, + new() { Alias = "variantText", Value = "#2: The content value in English", Culture = "en-US" }, + new() { Alias = "variantText", Value = "#2: The content value in Danish", Culture = "da-DK" } + }, + [], + null, + null + ) + ) + ] + ); + + blockListValue.Expose = + [ + new() { ContentKey = blockListValue.ContentData[0].Key, Culture = "en-US" }, + new() { ContentKey = blockListValue.ContentData[1].Key, Culture = "en-US" }, + new() { ContentKey = blockListValue.ContentData[1].Key, Culture = "da-DK" }, + ]; + + content.Properties["blocks"]!.SetValue(JsonSerializer.Serialize(blockListValue)); + ContentService.Save(content); + PublishContent(content, contentType, ["en-US", "da-DK"]); + + AssertPropertyValues("en-US", 2, blocks => + { + Assert.AreEqual("#1: The invariant content value", blocks[0].Content.Value("invariantText")); + Assert.AreEqual("#1: The content value in English", blocks[0].Content.Value("variantText")); + + Assert.AreEqual("#2: The invariant content value", blocks[1].Content.Value("invariantText")); + Assert.AreEqual("#2: The content value in English", blocks[1].Content.Value("variantText")); + }); + + AssertPropertyValues("da-DK", 1, blocks => + { + Assert.AreEqual("#2: The invariant content value", blocks[0].Content.Value("invariantText")); + Assert.AreEqual("#2: The content value in Danish", blocks[0].Content.Value("variantText")); + }); + + blockListValue.Expose = + [ + new() { ContentKey = blockListValue.ContentData[0].Key, Culture = "en-US" }, + new() { ContentKey = blockListValue.ContentData[1].Key, Culture = "en-US" }, + new() { ContentKey = blockListValue.ContentData[0].Key, Culture = "da-DK" }, + new() { ContentKey = blockListValue.ContentData[1].Key, Culture = "da-DK" }, + ]; + + content.Properties["blocks"]!.SetValue(JsonSerializer.Serialize(blockListValue)); + ContentService.Save(content); + PublishContent(content, contentType, ["da-DK"]); + + AssertPropertyValues("da-DK", 2, blocks => + { + Assert.AreEqual("#1: The invariant content value", blocks[0].Content.Value("invariantText")); + Assert.AreEqual("#1: The content value in Danish", blocks[0].Content.Value("variantText")); + + Assert.AreEqual("#2: The invariant content value", blocks[1].Content.Value("invariantText")); + Assert.AreEqual("#2: The content value in Danish", blocks[1].Content.Value("variantText")); + }); + + void AssertPropertyValues(string culture, int numberOfExpectedBlocks, Action validateBlocks) + { + SetVariationContext(culture, null); + var publishedContent = GetPublishedContent(content.Key); + + var value = publishedContent.Value("blocks"); + Assert.IsNotNull(value); + Assert.AreEqual(numberOfExpectedBlocks, value.Count); + + validateBlocks(value); + } + } + + [Test] + public async Task Can_Expose_Invariant_Blocks_Across_Cultures() + { + var elementType = CreateElementType(ContentVariation.Nothing); + var blockListDataType = await CreateBlockListDataType(elementType); + var contentType = CreateContentType(ContentVariation.Culture, blockListDataType); + + var content = CreateContent(contentType, elementType, [], false); + var blockListValue = BlockListPropertyValue( + elementType, + [ + ( + Guid.NewGuid(), + Guid.NewGuid(), + new BlockProperty( + new List { + new() { Alias = "invariantText", Value = "#1: The invariant content value" }, + new() { Alias = "variantText", Value = "#1: The other invariant content value" } + }, + [], + null, + null + ) + ), + ( + Guid.NewGuid(), + Guid.NewGuid(), + new BlockProperty( + new List { + new() { Alias = "invariantText", Value = "#2: The invariant content value" }, + new() { Alias = "variantText", Value = "#2: The other invariant content value" } + }, + [], + null, + null + ) + ) + ] + ); + + blockListValue.Expose = + [ + new() { ContentKey = blockListValue.ContentData[0].Key }, + new() { ContentKey = blockListValue.ContentData[1].Key }, + ]; + + content.Properties["blocks"]!.SetValue(JsonSerializer.Serialize(blockListValue)); + ContentService.Save(content); + PublishContent(content, contentType, ["en-US", "da-DK"]); + + foreach (var culture in new[] { "en-US", "da-DK" }) + { + AssertPropertyValues(culture, 2, blocks => + { + Assert.AreEqual("#1: The invariant content value", blocks[0].Content.Value("invariantText")); + Assert.AreEqual("#1: The other invariant content value", blocks[0].Content.Value("variantText")); + + Assert.AreEqual("#2: The invariant content value", blocks[1].Content.Value("invariantText")); + Assert.AreEqual("#2: The other invariant content value", blocks[1].Content.Value("variantText")); + }); + } + + blockListValue.Expose = + [ + new() { ContentKey = blockListValue.ContentData[1].Key }, + ]; + + content.Properties["blocks"]!.SetValue(JsonSerializer.Serialize(blockListValue)); + ContentService.Save(content); + + // note how publishing in one language affects both due to the invariance of the block element type + PublishContent(content, contentType, ["en-US"]); + + foreach (var culture in new[] { "en-US", "da-DK" }) + { + AssertPropertyValues(culture, 1, blocks => + { + Assert.AreEqual("#2: The invariant content value", blocks[0].Content.Value("invariantText")); + Assert.AreEqual("#2: The other invariant content value", blocks[0].Content.Value("variantText")); + }); + } + + void AssertPropertyValues(string culture, int numberOfExpectedBlocks, Action validateBlocks) + { + SetVariationContext(culture, null); + var publishedContent = GetPublishedContent(content.Key); + + var value = publishedContent.Value("blocks"); + Assert.IsNotNull(value); + Assert.AreEqual(numberOfExpectedBlocks, value.Count); + + validateBlocks(value); + } + } + + [Test] + public async Task Can_Expose_Both_Variant_And_Invariant_Blocks() + { + var invariantElementType = CreateElementType(ContentVariation.Nothing); + var variantElementType = CreateElementType(ContentVariation.Culture, "myVariantElementType"); + var blockListDataType = await CreateBlockEditorDataType( + Constants.PropertyEditors.Aliases.BlockList, + new BlockListConfiguration.BlockConfiguration[] + { + new() { ContentElementTypeKey = invariantElementType.Key }, + new() { ContentElementTypeKey = variantElementType.Key } + }); + var contentType = CreateContentType(ContentVariation.Culture, blockListDataType); + + var content = CreateContent(contentType, invariantElementType, [], false); + var blockListValue = BlockListPropertyValue( + invariantElementType, + [ + ( + Guid.NewGuid(), + Guid.NewGuid(), + new BlockProperty( + new List { + new() { Alias = "invariantText", Value = "#1: The invariant content value" }, + new() { Alias = "variantText", Value = "#1: The other invariant content value" } + }, + [], + null, + null + ) + ) + ] + ); + + var variantElementKey = Guid.NewGuid(); + blockListValue.Layout[Constants.PropertyEditors.Aliases.BlockList] = blockListValue + .Layout[Constants.PropertyEditors.Aliases.BlockList] + .Union(new[] { new BlockListLayoutItem(variantElementKey) }); + blockListValue.ContentData.Add( + new BlockItemData(variantElementKey, variantElementType.Key, variantElementType.Alias) + { + Values = [ + new() { Alias = "invariantText", Value = "#2: The invariant content value" }, + new() { Alias = "variantText", Value = "#2: The variant content value in English", Culture = "en-US" }, + new() { Alias = "variantText", Value = "#2: The variant content value in Danish", Culture = "da-DK" }, + ] + } + ); + + blockListValue.Expose = + [ + new() { ContentKey = blockListValue.ContentData[0].Key }, + new() { ContentKey = blockListValue.ContentData[1].Key, Culture = "en-US" }, + ]; + + content.Properties["blocks"]!.SetValue(JsonSerializer.Serialize(blockListValue)); + ContentService.Save(content); + PublishContent(content, contentType, ["en-US", "da-DK"]); + + AssertPropertyValues("en-US", 2, blocks => + { + Assert.AreEqual("#1: The invariant content value", blocks[0].Content.Value("invariantText")); + Assert.AreEqual("#1: The other invariant content value", blocks[0].Content.Value("variantText")); + + Assert.AreEqual("#2: The invariant content value", blocks[1].Content.Value("invariantText")); + Assert.AreEqual("#2: The variant content value in English", blocks[1].Content.Value("variantText")); + }); + + AssertPropertyValues("da-DK", 1, blocks => + { + Assert.AreEqual("#1: The invariant content value", blocks[0].Content.Value("invariantText")); + Assert.AreEqual("#1: The other invariant content value", blocks[0].Content.Value("variantText")); + }); + + blockListValue.Expose = + [ + new() { ContentKey = blockListValue.ContentData[0].Key }, + new() { ContentKey = blockListValue.ContentData[1].Key, Culture = "en-US" }, + new() { ContentKey = blockListValue.ContentData[1].Key, Culture = "da-DK" }, + ]; + + content.Properties["blocks"]!.SetValue(JsonSerializer.Serialize(blockListValue)); + ContentService.Save(content); + + PublishContent(content, contentType, ["da-DK"]); + + AssertPropertyValues("da-DK", 2, blocks => + { + Assert.AreEqual("#1: The invariant content value", blocks[0].Content.Value("invariantText")); + Assert.AreEqual("#1: The other invariant content value", blocks[0].Content.Value("variantText")); + + Assert.AreEqual("#2: The invariant content value", blocks[1].Content.Value("invariantText")); + Assert.AreEqual("#2: The variant content value in Danish", blocks[1].Content.Value("variantText")); + }); + + void AssertPropertyValues(string culture, int numberOfExpectedBlocks, Action validateBlocks) + { + SetVariationContext(culture, null); + var publishedContent = GetPublishedContent(content.Key); + + var value = publishedContent.Value("blocks"); + Assert.IsNotNull(value); + Assert.AreEqual(numberOfExpectedBlocks, value.Count); + + validateBlocks(value); + } + } + + [Test] + public async Task Can_Publish_Invariant_Properties_Without_Default_Culture_With_AllowEditInvariantFromNonDefault() + { + var elementType = CreateElementType(ContentVariation.Culture); + var blockListDataType = await CreateBlockListDataType(elementType); + var contentType = CreateContentType(ContentVariation.Culture, blockListDataType); + + var content = CreateContent(contentType, elementType, [], false); + var blockListValue = BlockListPropertyValue( + elementType, + [ + ( + Guid.NewGuid(), + Guid.NewGuid(), + new BlockProperty( + new List { + new() { Alias = "invariantText", Value = "The first invariant content value" }, + new() { Alias = "variantText", Value = "The first content value in English", Culture = "en-US" }, + new() { Alias = "variantText", Value = "The first content value in Danish", Culture = "da-DK" } + }, + new List + { + new() { Alias = "invariantText", Value = "The first invariant settings value" }, + new() { Alias = "variantText", Value = "The first settings value in English", Culture = "en-US" }, + new() { Alias = "variantText", Value = "The first settings value in Danish", Culture = "da-DK" }, + }, + null, + null + ) + ) + ] + ); + + blockListValue.Expose = + [ + new() { ContentKey = blockListValue.ContentData[0].Key, Culture = "da-DK" }, + ]; + + content.Properties["blocks"]!.SetValue(JsonSerializer.Serialize(blockListValue)); + ContentService.Save(content); + + PublishContent(content, contentType, ["da-DK"]); + + AssertPropertyValues("en-US", 0); + + AssertPropertyValues("da-DK", 1, blocks => + { + Assert.AreEqual("The first invariant content value", blocks[0].Content.Value("invariantText")); + Assert.AreEqual("The first content value in Danish", blocks[0].Content.Value("variantText")); + Assert.IsNotNull(blocks[0].Settings); + Assert.AreEqual("The first invariant settings value", blocks[0].Settings.Value("invariantText")); + Assert.AreEqual("The first settings value in Danish", blocks[0].Settings.Value("variantText")); + }); + + blockListValue.ContentData[0].Values[0].Value = "The second invariant content value"; + blockListValue.ContentData[0].Values[1].Value = "The second content value in English"; + blockListValue.ContentData[0].Values[2].Value = "The second content value in Danish"; + blockListValue.SettingsData[0].Values[0].Value = "The second invariant settings value"; + blockListValue.SettingsData[0].Values[1].Value = "The second settings value in English"; + blockListValue.SettingsData[0].Values[2].Value = "The second settings value in Danish"; + + content.Properties["blocks"]!.SetValue(JsonSerializer.Serialize(blockListValue)); + ContentService.Save(content); + PublishContent(content, contentType, ["da-DK"]); + + AssertPropertyValues("en-US", 0); + + AssertPropertyValues("da-DK", 1, blocks => + { + Assert.AreEqual("The second invariant content value", blocks[0].Content.Value("invariantText")); + Assert.AreEqual("The second content value in Danish", blocks[0].Content.Value("variantText")); + Assert.IsNotNull(blocks[0].Settings); + Assert.AreEqual("The second invariant settings value", blocks[0].Settings.Value("invariantText")); + Assert.AreEqual("The second settings value in Danish", blocks[0].Settings.Value("variantText")); + }); + + void AssertPropertyValues(string culture, int numberOfExpectedBlocks, Action? validateBlocks = null) + { + SetVariationContext(culture, null); + var publishedContent = GetPublishedContent(content.Key); + + var value = publishedContent.Value("blocks"); + Assert.IsNotNull(value); + Assert.AreEqual(numberOfExpectedBlocks, value.Count); + + validateBlocks?.Invoke(value); + } + } + + [Test] + public async Task Cannot_Publish_Invariant_Properties_Without_Default_Culture_Without_AllowEditInvariantFromNonDefault() + { + var elementType = CreateElementType(ContentVariation.Culture); + var blockListDataType = await CreateBlockListDataType(elementType); + var contentType = CreateContentType(ContentVariation.Culture, blockListDataType); + + var content = CreateContent(contentType, elementType, [], false); + var blockListValue = BlockListPropertyValue( + elementType, + [ + ( + Guid.NewGuid(), + Guid.NewGuid(), + new BlockProperty( + new List { + new() { Alias = "invariantText", Value = "The first invariant content value" }, + new() { Alias = "variantText", Value = "The first content value in English", Culture = "en-US" }, + new() { Alias = "variantText", Value = "The first content value in Danish", Culture = "da-DK" } + }, + new List + { + new() { Alias = "invariantText", Value = "The first invariant settings value" }, + new() { Alias = "variantText", Value = "The first settings value in English", Culture = "en-US" }, + new() { Alias = "variantText", Value = "The first settings value in Danish", Culture = "da-DK" }, + }, + null, + null + ) + ) + ] + ); + + blockListValue.Expose = + [ + new() { ContentKey = blockListValue.ContentData[0].Key, Culture = "da-DK" }, + ]; + + content.Properties["blocks"]!.SetValue(JsonSerializer.Serialize(blockListValue)); + ContentService.Save(content); + + PublishContent(content, contentType, ["da-DK"]); + + AssertPropertyValues("en-US", 0); + + AssertPropertyValues("da-DK", 1, blocks => + { + Assert.AreEqual(string.Empty, blocks[0].Content.Value("invariantText")); + Assert.AreEqual("The first content value in Danish", blocks[0].Content.Value("variantText")); + Assert.IsNotNull(blocks[0].Settings); + Assert.AreEqual(string.Empty, blocks[0].Settings.Value("invariantText")); + Assert.AreEqual("The first settings value in Danish", blocks[0].Settings.Value("variantText")); + }); + + void AssertPropertyValues(string culture, int numberOfExpectedBlocks, Action? validateBlocks = null) + { + SetVariationContext(culture, null); + var publishedContent = GetPublishedContent(content.Key); + + var value = publishedContent.Value("blocks"); + Assert.IsNotNull(value); + Assert.AreEqual(numberOfExpectedBlocks, value.Count); + + validateBlocks?.Invoke(value); + } + } +} diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/PropertyEditors/BlockListElementLevelVariationTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/PropertyEditors/BlockListElementLevelVariationTests.cs new file mode 100644 index 000000000000..8a798a603c5c --- /dev/null +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/PropertyEditors/BlockListElementLevelVariationTests.cs @@ -0,0 +1,167 @@ +using NUnit.Framework; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.Blocks; +using Umbraco.Cms.Core.Models.PublishedContent; +using Umbraco.Cms.Core.PropertyEditors; +using Umbraco.Cms.Core.Serialization; +using Umbraco.Cms.Tests.Common.Builders; +using Umbraco.Cms.Tests.Common.Builders.Extensions; + +namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.PropertyEditors; + +public partial class BlockListElementLevelVariationTests : BlockEditorElementVariationTestBase +{ + [OneTimeSetUp] + public void OneTimeSetUp() + { + TestsRequiringAllowEditInvariantFromNonDefault.Add(nameof(Can_Publish_Invariant_Properties_Without_Default_Culture_With_AllowEditInvariantFromNonDefault)); + } + + private IJsonSerializer JsonSerializer => GetRequiredService(); + + private async Task CreateBlockListDataType(IContentType elementType) + => await CreateBlockEditorDataType( + Constants.PropertyEditors.Aliases.BlockList, + new BlockListConfiguration.BlockConfiguration[] + { + new() { ContentElementTypeKey = elementType.Key, SettingsElementTypeKey = elementType.Key } + }); + + private IContent CreateContent(IContentType contentType, IContentType elementType, IList blockContentValues, IList blockSettingsValues, bool publishContent) + => CreateContent( + contentType, + elementType, + new[] { new BlockProperty(blockContentValues, blockSettingsValues, null, null) }, + publishContent); + + private IContent CreateContent(IContentType contentType, IContentType elementType, IEnumerable blocksProperties, bool publishContent) + { + var contentBuilder = new ContentBuilder() + .WithContentType(contentType); + contentBuilder = contentType.VariesByCulture() + ? contentBuilder + .WithCultureName("en-US", "Home (en)") + .WithCultureName("da-DK", "Home (da)") + : contentBuilder.WithName("Home"); + + var content = contentBuilder.Build(); + + var contentElementKey = Guid.NewGuid(); + var settingsElementKey = Guid.NewGuid(); + foreach (var blocksProperty in blocksProperties) + { + var blockListValue = BlockListPropertyValue(elementType, contentElementKey, settingsElementKey, blocksProperty); + var propertyValue = JsonSerializer.Serialize(blockListValue); + content.Properties["blocks"]!.SetValue(propertyValue, blocksProperty.Culture, blocksProperty.Segment); + } + + ContentService.Save(content); + + if (publishContent) + { + PublishContent(content, contentType); + } + + return content; + } + + private BlockListValue BlockListPropertyValue(IContentType elementType, Guid contentElementKey, Guid settingsElementKey, BlockProperty blocksProperty) + => BlockListPropertyValue(elementType, [(contentElementKey, settingsElementKey, blocksProperty)]); + + private BlockListValue BlockListPropertyValue(IContentType elementType, List<(Guid contentElementKey, Guid settingsElementKey, BlockProperty BlocksProperty)> blocks) + { + var expose = new List(); + foreach (var block in blocks) + { + var cultures = elementType.VariesByCulture() + ? new[] { block.BlocksProperty.Culture } + .Union(block.BlocksProperty.BlockContentValues.Select(value => value.Culture)) + .WhereNotNull() + .Distinct() + .ToArray() + : [null]; + if (cultures.Any() is false) + { + cultures = [null]; + } + + var segments = elementType.VariesBySegment() + ? new[] { block.BlocksProperty.Segment } + .Union(block.BlocksProperty.BlockContentValues.Select(value => value.Segment)) + .Distinct() + .ToArray() + : [null]; + + expose.AddRange(cultures.SelectMany(culture => segments.Select(segment => + new BlockItemVariation(block.contentElementKey, culture, segment)))); + } + + return new BlockListValue + { + Layout = new Dictionary> + { + { + Constants.PropertyEditors.Aliases.BlockList, + blocks.Select(block => new BlockListLayoutItem + { + ContentKey = block.contentElementKey, SettingsKey = block.settingsElementKey + }).ToArray() + } + }, + ContentData = + blocks.Select(block => new BlockItemData + { + Key = block.contentElementKey, + ContentTypeAlias = elementType.Alias, + ContentTypeKey = elementType.Key, + Values = block.BlocksProperty.BlockContentValues + }).ToList(), + SettingsData = blocks.Select(block => new BlockItemData + { + Key = block.settingsElementKey, + ContentTypeAlias = elementType.Alias, + ContentTypeKey = elementType.Key, + Values = block.BlocksProperty.BlockSettingsValues + }).ToList(), + Expose = expose + }; + } + + private void PublishContent(IContent content, IContentType contentType, string[]? culturesToPublish = null) + { + culturesToPublish ??= contentType.VariesByCulture() + ? ["en-US", "da-DK"] + : ["*"]; + PublishContent(content, culturesToPublish); + } + + private async Task CreatePublishedContent(ContentVariation variation, IList blockContentValues, IList blockSettingsValues) + { + var elementType = CreateElementType(variation); + var blockListDataType = await CreateBlockListDataType(elementType); + var contentType = CreateContentType(variation, blockListDataType); + + var content = CreateContent(contentType, elementType, blockContentValues, blockSettingsValues, true); + return GetPublishedContent(content.Key); + } + + private class BlockProperty + { + public BlockProperty(IList blockContentValues, IList blockSettingsValues, string? culture, string? segment) + { + BlockContentValues = blockContentValues; + BlockSettingsValues = blockSettingsValues; + Culture = culture; + Segment = segment; + } + + public IList BlockContentValues { get; } + + public IList BlockSettingsValues { get; } + + public string? Culture { get; } + + public string? Segment { get; } + } +} diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/PropertyEditors/BlockListPropertyEditorTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/PropertyEditors/BlockListPropertyEditorTests.cs new file mode 100644 index 000000000000..0b10332352d7 --- /dev/null +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/PropertyEditors/BlockListPropertyEditorTests.cs @@ -0,0 +1,365 @@ +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 BlockListPropertyEditorTests : UmbracoIntegrationTest +{ + private IContentTypeService ContentTypeService => GetRequiredService(); + + private IContentService ContentService => GetRequiredService(); + + private IDataTypeService DataTypeService => GetRequiredService(); + + private IJsonSerializer JsonSerializer => GetRequiredService(); + + private IConfigurationEditorJsonSerializer ConfigurationEditorJsonSerializer => GetRequiredService(); + + private PropertyEditorCollection PropertyEditorCollection => GetRequiredService(); + + [Test] + public async Task Can_Track_References() + { + var textPageContentType = ContentTypeBuilder.CreateTextPageContentType("myContentType"); + textPageContentType.AllowedTemplates = Enumerable.Empty(); + ContentTypeService.Save(textPageContentType); + + var textPage = ContentBuilder.CreateTextpageContent(textPageContentType, "My Picked Content", -1); + ContentService.Save(textPage); + + var elementType = ContentTypeBuilder.CreateAllTypesContentType("myElementType", "My Element Type"); + elementType.IsElement = true; + ContentTypeService.Save(elementType); + + var blockListContentType = await CreateBlockListContentType(elementType); + + var contentElementKey = Guid.NewGuid(); + var blockListValue = new BlockListValue + { + Layout = new Dictionary> + { + { + Constants.PropertyEditors.Aliases.BlockList, + new IBlockLayoutItem[] + { + new BlockListLayoutItem { ContentKey = contentElementKey } + } + } + }, + ContentData = + [ + new() + { + Key = contentElementKey, + ContentTypeAlias = elementType.Alias, + ContentTypeKey = elementType.Key, + Values = + [ + new () + { + Alias = "contentPicker", + Value = textPage.GetUdi() + } + ] + } + ] + }; + var blocksPropertyValue = JsonSerializer.Serialize(blockListValue); + + var content = new ContentBuilder() + .WithContentType(blockListContentType) + .WithName("My Blocks") + .WithPropertyValues(new { blocks = blocksPropertyValue }) + .Build(); + ContentService.Save(content); + + var valueEditor = await GetValueEditor(blockListContentType); + + var references = valueEditor.GetReferences(content.GetValue("blocks")).ToArray(); + Assert.AreEqual(1, references.Length); + var reference = references.First(); + Assert.AreEqual(Constants.Conventions.RelationTypes.RelatedDocumentAlias, reference.RelationTypeAlias); + Assert.AreEqual(textPage.GetUdi(), reference.Udi); + } + + [Test] + public async Task Can_Track_Tags() + { + var elementType = ContentTypeBuilder.CreateAllTypesContentType("myElementType", "My Element Type"); + elementType.IsElement = true; + ContentTypeService.Save(elementType); + + var blockListContentType = await CreateBlockListContentType(elementType); + + var contentElementKey = Guid.NewGuid(); + var blockListValue = new BlockListValue + { + Layout = new Dictionary> + { + { + Constants.PropertyEditors.Aliases.BlockList, + new IBlockLayoutItem[] + { + new BlockListLayoutItem { ContentKey = contentElementKey } + } + } + }, + ContentData = + [ + new() + { + Key = contentElementKey, + ContentTypeAlias = elementType.Alias, + ContentTypeKey = elementType.Key, + Values = + [ + new () + { + Alias = "tags", + // this is a little skewed, but the tags editor expects a serialized array of strings + Value = JsonSerializer.Serialize(new[] { "Tag One", "Tag Two", "Tag Three" }) + } + ] + } + ] + }; + var blocksPropertyValue = JsonSerializer.Serialize(blockListValue); + + var content = new ContentBuilder() + .WithContentType(blockListContentType) + .WithName("My Blocks") + .WithPropertyValues(new { blocks = blocksPropertyValue }) + .Build(); + ContentService.Save(content); + + var valueEditor = await GetValueEditor(blockListContentType); + + var tags = valueEditor.GetTags(content.GetValue("blocks"), null, null).ToArray(); + Assert.AreEqual(3, tags.Length); + Assert.IsNotNull(tags.Single(tag => tag.Text == "Tag One")); + Assert.IsNotNull(tags.Single(tag => tag.Text == "Tag Two")); + Assert.IsNotNull(tags.Single(tag => tag.Text == "Tag Three")); + } + + [Test] + public async Task Can_Handle_Culture_Variance_Addition() + { + var elementType = ContentTypeBuilder.CreateAllTypesContentType("myElementType", "My Element Type"); + elementType.IsElement = true; + ContentTypeService.Save(elementType); + + var blockListContentType = await CreateBlockListContentType(elementType); + + var contentElementKey = Guid.NewGuid(); + var blockListValue = new BlockListValue + { + Layout = new Dictionary> + { + { + Constants.PropertyEditors.Aliases.BlockList, + new IBlockLayoutItem[] + { + new BlockListLayoutItem { ContentKey = contentElementKey } + } + } + }, + ContentData = + [ + new() + { + Key = contentElementKey, + ContentTypeAlias = elementType.Alias, + ContentTypeKey = elementType.Key, + Values = + [ + new () + { + Alias = "singleLineText", + Value = "The single line text" + } + ] + } + ], + Expose = + [ + new (contentElementKey, null, null) + ] + }; + var blocksPropertyValue = JsonSerializer.Serialize(blockListValue); + + var content = new ContentBuilder() + .WithContentType(blockListContentType) + .WithName("My Blocks") + .WithPropertyValues(new { blocks = blocksPropertyValue }) + .Build(); + ContentService.Save(content); + + elementType.Variations = ContentVariation.Culture; + elementType.PropertyTypes.First(pt => pt.Alias == "singleLineText").Variations = ContentVariation.Culture; + ContentTypeService.Save(elementType); + + var valueEditor = await GetValueEditor(blockListContentType); + var toEditorValue = valueEditor.ToEditor(content.Properties["blocks"]!) as BlockListValue; + Assert.IsNotNull(toEditorValue); + Assert.AreEqual(1, toEditorValue.ContentData.Count); + + var properties = toEditorValue.ContentData.First().Values; + Assert.AreEqual(1, properties.Count); + Assert.Multiple(() => + { + var property = properties.First(); + Assert.AreEqual("singleLineText", property.Alias); + Assert.AreEqual("The single line text", property.Value); + Assert.AreEqual("en-US", property.Culture); + }); + + Assert.AreEqual(1, toEditorValue.Expose.Count); + Assert.Multiple(() => + { + var itemVariation = toEditorValue.Expose[0]; + Assert.AreEqual(contentElementKey, itemVariation.ContentKey); + Assert.AreEqual("en-US", itemVariation.Culture); + }); + } + + [Test] + public async Task Can_Handle_Culture_Variance_Removal() + { + var elementType = ContentTypeBuilder.CreateAllTypesContentType("myElementType", "My Element Type"); + elementType.IsElement = true; + elementType.Variations = ContentVariation.Culture; + elementType.PropertyTypes.First(pt => pt.Alias == "singleLineText").Variations = ContentVariation.Culture; + ContentTypeService.Save(elementType); + + var blockListContentType = await CreateBlockListContentType(elementType); + + var contentElementKey = Guid.NewGuid(); + var blockListValue = new BlockListValue + { + Layout = new Dictionary> + { + { + Constants.PropertyEditors.Aliases.BlockList, + new IBlockLayoutItem[] + { + new BlockListLayoutItem { ContentKey = contentElementKey } + } + } + }, + ContentData = + [ + new() + { + Key = contentElementKey, + ContentTypeAlias = elementType.Alias, + ContentTypeKey = elementType.Key, + Values = + [ + new () + { + Alias = "singleLineText", + Value = "The single line text", + Culture = "en-US" + } + ] + } + ], + Expose = + [ + new (contentElementKey, "en-US", null) + ] + }; + var blocksPropertyValue = JsonSerializer.Serialize(blockListValue); + + var content = new ContentBuilder() + .WithContentType(blockListContentType) + .WithName("My Blocks") + .WithPropertyValues(new { blocks = blocksPropertyValue }) + .Build(); + ContentService.Save(content); + + elementType.PropertyTypes.First(pt => pt.Alias == "singleLineText").Variations = ContentVariation.Nothing; + elementType.Variations = ContentVariation.Nothing; + ContentTypeService.Save(elementType); + + var valueEditor = await GetValueEditor(blockListContentType); + var toEditorValue = valueEditor.ToEditor(content.Properties["blocks"]!) as BlockListValue; + Assert.IsNotNull(toEditorValue); + Assert.AreEqual(1, toEditorValue.ContentData.Count); + + var properties = toEditorValue.ContentData.First().Values; + Assert.AreEqual(1, properties.Count); + Assert.Multiple(() => + { + var property = properties.First(); + Assert.AreEqual("singleLineText", property.Alias); + Assert.AreEqual("The single line text", property.Value); + Assert.AreEqual(null, property.Culture); + }); + + Assert.AreEqual(1, toEditorValue.Expose.Count); + Assert.Multiple(() => + { + var itemVariation = toEditorValue.Expose[0]; + Assert.AreEqual(contentElementKey, itemVariation.ContentKey); + Assert.AreEqual(null, itemVariation.Culture); + }); + } + + private async Task CreateBlockListContentType(IContentType elementType) + { + var blockListDataType = new DataType(PropertyEditorCollection[Constants.PropertyEditors.Aliases.BlockList], ConfigurationEditorJsonSerializer) + { + ConfigurationData = new Dictionary + { + { + "blocks", + new BlockListConfiguration.BlockConfiguration[] + { + new() { ContentElementTypeKey = elementType.Key } + } + } + }, + Name = "My Block List", + DatabaseType = ValueStorageType.Ntext, + ParentId = Constants.System.Root, + CreateDate = DateTime.UtcNow + }; + + await DataTypeService.CreateAsync(blockListDataType, Constants.Security.SuperUserKey); + + var contentType = new ContentTypeBuilder() + .WithAlias("myPage") + .WithName("My Page") + .AddPropertyType() + .WithAlias("blocks") + .WithName("Blocks") + .WithDataTypeId(blockListDataType.Id) + .Done() + .Build(); + ContentTypeService.Save(contentType); + // re-fetch to wire up all key bindings (particularly to the datatype) + return await ContentTypeService.GetAsync(contentType.Key); + } + + private async Task GetValueEditor(IContentType contentType) + { + var dataType = await DataTypeService.GetAsync(contentType.PropertyTypes.First(propertyType => propertyType.Alias == "blocks").DataTypeKey); + Assert.IsNotNull(dataType?.Editor); + var valueEditor = dataType.Editor.GetValueEditor() as BlockListPropertyEditorBase.BlockListEditorPropertyValueEditor; + Assert.IsNotNull(valueEditor); + + return valueEditor; + } +} diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/PropertyEditors/PropertyIndexValueFactoryTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/PropertyEditors/PropertyIndexValueFactoryTests.cs index f5a9d51bbfcc..ac386f10d2f6 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/PropertyEditors/PropertyIndexValueFactoryTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/PropertyEditors/PropertyIndexValueFactoryTests.cs @@ -46,20 +46,22 @@ public void Can_Get_Index_Values_From_RichText_With_Blocks() var propertyValue = RichTextPropertyEditorHelper.SerializeRichTextEditorValue( new RichTextEditorValue { - Markup = @$"

This is some markup

", + Markup = @$"

This is some markup

", Blocks = JsonSerializer.Deserialize($$""" { "layout": { "Umbraco.TinyMCE": [{ - "contentUdi": "umb://element/{{elementId:N}}" + "contentKey": "{{elementId:D}}" } ] }, "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

" + "key": "{{elementId:D}}", + "values": [ + { "alias": "singleLineText", "value": "The single line of text in the block" }, + { "alias": "bodyText", "value": "

The body text in the block

" } + ] } ], "settingsData": [] @@ -170,24 +172,23 @@ public async Task Can_Get_Index_Values_From_BlockList() var editor = dataType.Editor!; - var contentElementUdi = new GuidUdi(Constants.UdiEntityType.Element, Guid.NewGuid()); + var contentElementKey = Guid.NewGuid(); var blockListValue = new BlockListValue( [ - new BlockListLayoutItem(contentElementUdi) + new BlockListLayoutItem(contentElementKey) ]) { ContentData = [ - new(contentElementUdi, elementType.Key, elementType.Alias) + new(contentElementKey, elementType.Key, elementType.Alias) { - RawPropertyValues = new Dictionary + Values = new List { - {"singleLineText", "The single line of text in the block"}, - {"bodyText", "

The body text in the block

"} + new() { Alias = "singleLineText", Value = "The single line of text in the block" }, + new() { Alias = "bodyText", Value = "

The body text in the block

" }, } } ], - SettingsData = [] }; var propertyValue = JsonSerializer.Serialize(blockListValue); @@ -272,11 +273,11 @@ public async Task Can_Get_Index_Values_From_BlockGrid() var editor = dataType.Editor!; - var contentElementUdi = new GuidUdi(Constants.UdiEntityType.Element, Guid.NewGuid()); - var contentAreaElementUdi = new GuidUdi(Constants.UdiEntityType.Element, Guid.NewGuid()); + var contentElementKey = Guid.NewGuid(); + var contentAreaElementKey = Guid.NewGuid(); var blockGridValue = new BlockGridValue( [ - new BlockGridLayoutItem(contentElementUdi) + new BlockGridLayoutItem(contentElementKey) { ColumnSpan = 12, RowSpan = 1, @@ -286,7 +287,7 @@ public async Task Can_Get_Index_Values_From_BlockGrid() { Items = [ - new BlockGridLayoutItem(contentAreaElementUdi) + new BlockGridLayoutItem(contentAreaElementKey) { ColumnSpan = 12, RowSpan = 1, @@ -299,22 +300,22 @@ public async Task Can_Get_Index_Values_From_BlockGrid() { ContentData = [ - new(contentElementUdi, elementType.Key, elementType.Alias) + new(contentElementKey, elementType.Key, elementType.Alias) { - RawPropertyValues = new() + Values = new List { - { "singleLineText", "The single line of text in the grid root" }, - { "bodyText", "

The body text in the grid root

" }, - }, + new() { Alias = "singleLineText", Value = "The single line of text in the grid root" }, + new() { Alias = "bodyText", Value = "

The body text in the grid root

" }, + } }, - new(contentAreaElementUdi, elementType.Key, elementType.Alias) + new(contentAreaElementKey, elementType.Key, elementType.Alias) { - RawPropertyValues = new() + Values = new List { - { "singleLineText", "The single line of text in the grid area" }, - { "bodyText", "

The body text in the grid area

" }, - }, - }, + new() { Alias = "singleLineText", Value = "The single line of text in the grid area" }, + new() { Alias = "bodyText", Value = "

The body text in the grid area

" }, + } + } ], }; var propertyValue = JsonSerializer.Serialize(blockGridValue); diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/PropertyEditors/RichTextElementLevelVariationTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/PropertyEditors/RichTextElementLevelVariationTests.cs new file mode 100644 index 000000000000..934ceea083dd --- /dev/null +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/PropertyEditors/RichTextElementLevelVariationTests.cs @@ -0,0 +1,495 @@ +using NUnit.Framework; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.Blocks; +using Umbraco.Cms.Core.Models.DeliveryApi; +using Umbraco.Cms.Core.PropertyEditors; +using Umbraco.Cms.Core.Serialization; +using Umbraco.Cms.Tests.Common.Builders; + +namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.PropertyEditors; + +// NOTE: These tests are in place to ensure that element level variation works for Rich Text. Element level variation +// is tested more in-depth for Block List (see BlockListElementLevelVariationTests), but since the actual +// implementation is shared between Block List and Rich Text, we won't repeat all those tests here. +public class RichTextElementLevelVariationTests : BlockEditorElementVariationTestBase +{ + private IJsonSerializer JsonSerializer => GetRequiredService(); + + [Test] + public async Task Can_Publish_Cultures_Independently() + { + var elementType = CreateElementType(ContentVariation.Culture); + + var blockGridDataType = await CreateRichTextDataType(elementType); + var contentType = CreateContentType(blockGridDataType); + var richTextValue = CreateRichTextValue(elementType); + var content = CreateContent(contentType, richTextValue); + + PublishContent(content, ["en-US", "da-DK"]); + + AssertPropertyValues( + "en-US", + (element1, element2, element3) => + { + Assert.AreEqual("#1: The first invariant content value", element1.Properties["invariantText"]); + Assert.AreEqual("#1: The first content value in English", element1.Properties["variantText"]); + Assert.AreEqual("#2: The first invariant content value", element2.Properties["invariantText"]); + Assert.AreEqual("#2: The first content value in English", element2.Properties["variantText"]); + Assert.AreEqual("#3: The first invariant content value", element3.Properties["invariantText"]); + Assert.AreEqual("#3: The first content value in English", element3.Properties["variantText"]); + }, + (element1, element2, element3) => + { + Assert.AreEqual("#1: The first invariant settings value", element1.Properties["invariantText"]); + Assert.AreEqual("#1: The first settings value in English", element1.Properties["variantText"]); + Assert.AreEqual("#2: The first invariant settings value", element2.Properties["invariantText"]); + Assert.AreEqual("#2: The first settings value in English", element2.Properties["variantText"]); + Assert.AreEqual("#3: The first invariant settings value", element3.Properties["invariantText"]); + Assert.AreEqual("#3: The first settings value in English", element3.Properties["variantText"]); + }); + + AssertPropertyValues( + "da-DK", + (element1, element2, element3) => + { + Assert.AreEqual("#1: The first invariant content value", element1.Properties["invariantText"]); + Assert.AreEqual("#1: The first content value in Danish", element1.Properties["variantText"]); + Assert.AreEqual("#2: The first invariant content value", element2.Properties["invariantText"]); + Assert.AreEqual("#2: The first content value in Danish", element2.Properties["variantText"]); + Assert.AreEqual("#3: The first invariant content value", element3.Properties["invariantText"]); + Assert.AreEqual("#3: The first content value in Danish", element3.Properties["variantText"]); + }, + (element1, element2, element3) => + { + Assert.AreEqual("#1: The first invariant settings value", element1.Properties["invariantText"]); + Assert.AreEqual("#1: The first settings value in Danish", element1.Properties["variantText"]); + Assert.AreEqual("#2: The first invariant settings value", element2.Properties["invariantText"]); + Assert.AreEqual("#2: The first settings value in Danish", element2.Properties["variantText"]); + Assert.AreEqual("#3: The first invariant settings value", element3.Properties["invariantText"]); + Assert.AreEqual("#3: The first settings value in Danish", element3.Properties["variantText"]); + }); + + richTextValue = JsonSerializer.Deserialize((string)content.Properties["blocks"]!.GetValue()!); + for (var i = 0; i < 3; i++) + { + richTextValue.Blocks.ContentData[i].Values[0].Value = $"#{i + 1}: The second invariant content value"; + richTextValue.Blocks.ContentData[i].Values[1].Value = $"#{i + 1}: The second content value in English"; + richTextValue.Blocks.ContentData[i].Values[2].Value = $"#{i + 1}: The second content value in Danish"; + richTextValue.Blocks.SettingsData[i].Values[0].Value = $"#{i + 1}: The second invariant settings value"; + richTextValue.Blocks.SettingsData[i].Values[1].Value = $"#{i + 1}: The second settings value in English"; + richTextValue.Blocks.SettingsData[i].Values[2].Value = $"#{i + 1}: The second settings value in Danish"; + } + + content.Properties["blocks"]!.SetValue(JsonSerializer.Serialize(richTextValue)); + ContentService.Save(content); + PublishContent(content, ["en-US"]); + + AssertPropertyValues( + "en-US", + (element1, element2, element3) => + { + Assert.AreEqual("#1: The second invariant content value", element1.Properties["invariantText"]); + Assert.AreEqual("#1: The second content value in English", element1.Properties["variantText"]); + Assert.AreEqual("#2: The second invariant content value", element2.Properties["invariantText"]); + Assert.AreEqual("#2: The second content value in English", element2.Properties["variantText"]); + Assert.AreEqual("#3: The second invariant content value", element3.Properties["invariantText"]); + Assert.AreEqual("#3: The second content value in English", element3.Properties["variantText"]); + }, + (element1, element2, element3) => + { + Assert.AreEqual("#1: The second invariant settings value", element1.Properties["invariantText"]); + Assert.AreEqual("#1: The second settings value in English", element1.Properties["variantText"]); + Assert.AreEqual("#2: The second invariant settings value", element2.Properties["invariantText"]); + Assert.AreEqual("#2: The second settings value in English", element2.Properties["variantText"]); + Assert.AreEqual("#3: The second invariant settings value", element3.Properties["invariantText"]); + Assert.AreEqual("#3: The second settings value in English", element3.Properties["variantText"]); + }); + + AssertPropertyValues( + "da-DK", + (element1, element2, element3) => + { + Assert.AreEqual("#1: The second invariant content value", element1.Properties["invariantText"]); + Assert.AreEqual("#1: The first content value in Danish", element1.Properties["variantText"]); + Assert.AreEqual("#2: The second invariant content value", element2.Properties["invariantText"]); + Assert.AreEqual("#2: The first content value in Danish", element2.Properties["variantText"]); + Assert.AreEqual("#3: The second invariant content value", element3.Properties["invariantText"]); + Assert.AreEqual("#3: The first content value in Danish", element3.Properties["variantText"]); + }, + (element1, element2, element3) => + { + Assert.AreEqual("#1: The second invariant settings value", element1.Properties["invariantText"]); + Assert.AreEqual("#1: The first settings value in Danish", element1.Properties["variantText"]); + Assert.AreEqual("#2: The second invariant settings value", element2.Properties["invariantText"]); + Assert.AreEqual("#2: The first settings value in Danish", element2.Properties["variantText"]); + Assert.AreEqual("#3: The second invariant settings value", element3.Properties["invariantText"]); + Assert.AreEqual("#3: The first settings value in Danish", element3.Properties["variantText"]); + }); + + PublishContent(content, ["da-DK"]); + + AssertPropertyValues( + "da-DK", + (element1, element2, element3) => + { + Assert.AreEqual("#1: The second invariant content value", element1.Properties["invariantText"]); + Assert.AreEqual("#1: The second content value in Danish", element1.Properties["variantText"]); + Assert.AreEqual("#2: The second invariant content value", element2.Properties["invariantText"]); + Assert.AreEqual("#2: The second content value in Danish", element2.Properties["variantText"]); + Assert.AreEqual("#3: The second invariant content value", element3.Properties["invariantText"]); + Assert.AreEqual("#3: The second content value in Danish", element3.Properties["variantText"]); + }, + (element1, element2, element3) => + { + Assert.AreEqual("#1: The second invariant settings value", element1.Properties["invariantText"]); + Assert.AreEqual("#1: The second settings value in Danish", element1.Properties["variantText"]); + Assert.AreEqual("#2: The second invariant settings value", element2.Properties["invariantText"]); + Assert.AreEqual("#2: The second settings value in Danish", element2.Properties["variantText"]); + Assert.AreEqual("#3: The second invariant settings value", element3.Properties["invariantText"]); + Assert.AreEqual("#3: The second settings value in Danish", element3.Properties["variantText"]); + }); + + void AssertPropertyValues( + string culture, + Action validateBlockContentValues, + Action validateBlockSettingsValues) + { + SetVariationContext(culture, null); + var publishedContent = GetPublishedContent(content.Key); + var property = publishedContent.GetProperty("blocks"); + Assert.IsNotNull(property); + + var propertyValue = property.GetDeliveryApiValue(false, culture) as RichTextModel; + Assert.IsNotNull(propertyValue); + + var blocks = propertyValue.Blocks.ToArray(); + Assert.AreEqual(3, blocks.Length); + + Assert.Multiple(() => + { + validateBlockContentValues(blocks[0].Content, blocks[1].Content, blocks[2].Content); + validateBlockSettingsValues(blocks[0].Settings, blocks[1].Settings, blocks[2].Settings); + }); + } + } + + [Test] + public async Task Can_Publish_With_Blocks_Removed() + { + var elementType = CreateElementType(ContentVariation.Culture); + + var blockGridDataType = await CreateRichTextDataType(elementType); + var contentType = CreateContentType(blockGridDataType); + var richTextValue = CreateRichTextValue(elementType); + var content = CreateContent(contentType, richTextValue); + + PublishContent(content, ["en-US", "da-DK"]); + + AssertPropertyValues("en-US", 3, blocks => { }); + AssertPropertyValues("da-DK", 3, blocks => { }); + + richTextValue = JsonSerializer.Deserialize((string)content.Properties["blocks"]!.GetValue()!); + + // remove block #2 + var richTextBlockLayout = richTextValue.Blocks!.Layout.First(); + richTextValue.Blocks.Layout[richTextBlockLayout.Key] = + [ + richTextBlockLayout.Value.First(), + richTextBlockLayout.Value.Last() + ]; + var contentKey = richTextValue.Blocks.ContentData[1].Key; + richTextValue.Blocks.ContentData.RemoveAt(1); + richTextValue.Blocks.SettingsData.RemoveAt(1); + richTextValue.Blocks.Expose.RemoveAll(v => v.ContentKey == contentKey); + Assert.AreEqual(4, richTextValue.Blocks.Expose.Count); + + richTextValue.Blocks.ContentData[0].Values[0].Value = "#1: The second invariant content value"; + richTextValue.Blocks.ContentData[0].Values[1].Value = "#1: The second content value in English"; + richTextValue.Blocks.ContentData[0].Values[2].Value = "#1: The second content value in Danish"; + richTextValue.Blocks.ContentData[1].Values[0].Value = "#3: The second invariant content value"; + richTextValue.Blocks.ContentData[1].Values[1].Value = "#3: The second content value in English"; + richTextValue.Blocks.ContentData[1].Values[2].Value = "#3: The second content value in Danish"; + richTextValue.Blocks.SettingsData[0].Values[0].Value = "#1: The second invariant settings value"; + richTextValue.Blocks.SettingsData[0].Values[1].Value = "#1: The second settings value in English"; + richTextValue.Blocks.SettingsData[0].Values[2].Value = "#1: The second settings value in Danish"; + richTextValue.Blocks.SettingsData[1].Values[0].Value = "#3: The second invariant settings value"; + richTextValue.Blocks.SettingsData[1].Values[1].Value = "#3: The second settings value in English"; + richTextValue.Blocks.SettingsData[1].Values[2].Value = "#3: The second settings value in Danish"; + + content.Properties["blocks"]!.SetValue(JsonSerializer.Serialize(richTextValue)); + ContentService.Save(content); + PublishContent(content, ["en-US"]); + + AssertPropertyValues("en-US", 2, blocks => + { + Assert.AreEqual("#1: The second invariant content value", blocks[0].Content.Properties["invariantText"]); + Assert.AreEqual("#1: The second content value in English", blocks[0].Content.Properties["variantText"]); + Assert.AreEqual("#3: The second invariant content value", blocks[1].Content.Properties["invariantText"]); + Assert.AreEqual("#3: The second content value in English", blocks[1].Content.Properties["variantText"]); + + Assert.AreEqual("#1: The second invariant settings value", blocks[0].Settings!.Properties["invariantText"]); + Assert.AreEqual("#1: The second settings value in English", blocks[0].Settings.Properties["variantText"]); + Assert.AreEqual("#3: The second invariant settings value", blocks[1].Settings!.Properties["invariantText"]); + Assert.AreEqual("#3: The second settings value in English", blocks[1].Settings.Properties["variantText"]); + }); + + AssertPropertyValues("da-DK", 2, blocks => + { + Assert.AreEqual("#1: The second invariant content value", blocks[0].Content.Properties["invariantText"]); + Assert.AreEqual("#1: The first content value in Danish", blocks[0].Content.Properties["variantText"]); + Assert.AreEqual("#3: The second invariant content value", blocks[1].Content.Properties["invariantText"]); + Assert.AreEqual("#3: The first content value in Danish", blocks[1].Content.Properties["variantText"]); + + Assert.AreEqual("#1: The second invariant settings value", blocks[0].Settings!.Properties["invariantText"]); + Assert.AreEqual("#1: The first settings value in Danish", blocks[0].Settings.Properties["variantText"]); + Assert.AreEqual("#3: The second invariant settings value", blocks[1].Settings!.Properties["invariantText"]); + Assert.AreEqual("#3: The first settings value in Danish", blocks[1].Settings.Properties["variantText"]); + }); + + PublishContent(content, ["da-DK"]); + + AssertPropertyValues("da-DK", 2, blocks => + { + Assert.AreEqual("#1: The second invariant content value", blocks[0].Content.Properties["invariantText"]); + Assert.AreEqual("#1: The second content value in Danish", blocks[0].Content.Properties["variantText"]); + Assert.AreEqual("#3: The second invariant content value", blocks[1].Content.Properties["invariantText"]); + Assert.AreEqual("#3: The second content value in Danish", blocks[1].Content.Properties["variantText"]); + + Assert.AreEqual("#1: The second invariant settings value", blocks[0].Settings!.Properties["invariantText"]); + Assert.AreEqual("#1: The second settings value in Danish", blocks[0].Settings.Properties["variantText"]); + Assert.AreEqual("#3: The second invariant settings value", blocks[1].Settings!.Properties["invariantText"]); + Assert.AreEqual("#3: The second settings value in Danish", blocks[1].Settings.Properties["variantText"]); + }); + + void AssertPropertyValues(string culture, int numberOfExpectedBlocks, Action validateBlocks) + { + SetVariationContext(culture, null); + var publishedContent = GetPublishedContent(content.Key); + var property = publishedContent.GetProperty("blocks"); + Assert.IsNotNull(property); + + var propertyValue = property.GetDeliveryApiValue(false, culture) as RichTextModel; + Assert.IsNotNull(propertyValue); + + var blocks = propertyValue.Blocks.ToArray(); + Assert.AreEqual(numberOfExpectedBlocks, blocks.Length); + + validateBlocks(blocks); + } + } + + [Test] + public async Task Markup_Follows_Invariance() + { + var elementType = CreateElementType(ContentVariation.Culture); + + var blockGridDataType = await CreateRichTextDataType(elementType); + var contentType = CreateContentType(blockGridDataType); + var richTextValue = CreateRichTextValue(elementType); + var content = CreateContent(contentType, richTextValue); + + PublishContent(content, ["en-US", "da-DK"]); + + AssertPropertyValuesForAllCultures(markup => + { + Assert.Multiple(() => + { + Assert.IsTrue(markup.Contains("

Some text.

")); + Assert.IsTrue(markup.Contains("

More text.

")); + Assert.IsTrue(markup.Contains("

Even more text.

")); + Assert.IsTrue(markup.Contains("

The end.

")); + }); + }); + + richTextValue = JsonSerializer.Deserialize((string)content.Properties["blocks"]!.GetValue()!); + richTextValue.Markup = richTextValue.Markup + .Replace("Some text", "Some text updated") + .Replace("More text", "More text updated") + .Replace("Even more text", "Even more text updated") + .Replace("The end", "The end updated"); + + content.Properties["blocks"]!.SetValue(JsonSerializer.Serialize(richTextValue)); + ContentService.Save(content); + PublishContent(content, ["en-US"]); + + AssertPropertyValuesForAllCultures(markup => + { + Assert.Multiple(() => + { + Assert.IsFalse(markup.Contains("

Some text.

")); + Assert.IsFalse(markup.Contains("

More text.

")); + Assert.IsFalse(markup.Contains("

Even more text.

")); + Assert.IsFalse(markup.Contains("

The end.

")); + Assert.IsTrue(markup.Contains("

Some text updated.

")); + Assert.IsTrue(markup.Contains("

More text updated.

")); + Assert.IsTrue(markup.Contains("

Even more text updated.

")); + Assert.IsTrue(markup.Contains("

The end updated.

")); + }); + }); + + PublishContent(content, ["da-DK"]); + + AssertPropertyValuesForAllCultures(markup => + { + Assert.Multiple(() => + { + Assert.IsFalse(markup.Contains("

Some text.

")); + Assert.IsFalse(markup.Contains("

More text.

")); + Assert.IsFalse(markup.Contains("

Even more text.

")); + Assert.IsFalse(markup.Contains("

The end.

")); + Assert.IsTrue(markup.Contains("

Some text updated.

")); + Assert.IsTrue(markup.Contains("

More text updated.

")); + Assert.IsTrue(markup.Contains("

Even more text updated.

")); + Assert.IsTrue(markup.Contains("

The end updated.

")); + }); + }); + + void AssertPropertyValuesForAllCultures(Action validateMarkup) + { + foreach (var culture in new[] { "en-US", "da-DK" }) + { + SetVariationContext(culture, null); + var publishedContent = GetPublishedContent(content.Key); + var property = publishedContent.GetProperty("blocks"); + Assert.IsNotNull(property); + + var propertyValue = property.GetDeliveryApiValue(false, culture) as RichTextModel; + Assert.IsNotNull(propertyValue); + + Assert.IsNotEmpty(propertyValue.Markup); + validateMarkup(propertyValue.Markup); + } + } + } + + private async Task CreateRichTextDataType(IContentType elementType) + => await CreateBlockEditorDataType( + Constants.PropertyEditors.Aliases.RichText, + new RichTextConfiguration.RichTextBlockConfiguration[] + { + new() + { + ContentElementTypeKey = elementType.Key, + SettingsElementTypeKey = elementType.Key, + } + }); + + private IContentType CreateContentType(IDataType blockListDataType) + => CreateContentType(ContentVariation.Culture, blockListDataType); + + private RichTextEditorValue CreateRichTextValue(IContentType elementType) + { + var contentElementKey1 = Guid.NewGuid(); + var settingsElementKey1 = Guid.NewGuid(); + var contentElementKey2 = Guid.NewGuid(); + var settingsElementKey2 = Guid.NewGuid(); + var contentElementKey3 = Guid.NewGuid(); + var settingsElementKey3 = Guid.NewGuid(); + + return new RichTextEditorValue + { + Markup = $""" +

Some text.

+ +

More text.

+ +

Even more text.

+ +

The end.

+ """, + Blocks = new RichTextBlockValue([ + new RichTextBlockLayoutItem(contentElementKey1, settingsElementKey1), + new RichTextBlockLayoutItem(contentElementKey2, settingsElementKey2), + new RichTextBlockLayoutItem(contentElementKey3, settingsElementKey3), + ]) + { + ContentData = + [ + new(contentElementKey1, elementType.Key, elementType.Alias) + { + Values = + [ + new() { Alias = "invariantText", Value = "#1: The first invariant content value" }, + new() { Alias = "variantText", Value = "#1: The first content value in English", Culture = "en-US" }, + new() { Alias = "variantText", Value = "#1: The first content value in Danish", Culture = "da-DK" } + ] + }, + new(contentElementKey2, elementType.Key, elementType.Alias) + { + Values = + [ + new() { Alias = "invariantText", Value = "#2: The first invariant content value" }, + new() { Alias = "variantText", Value = "#2: The first content value in English", Culture = "en-US" }, + new() { Alias = "variantText", Value = "#2: The first content value in Danish", Culture = "da-DK" } + ] + }, + new(contentElementKey3, elementType.Key, elementType.Alias) + { + Values = + [ + new() { Alias = "invariantText", Value = "#3: The first invariant content value" }, + new() { Alias = "variantText", Value = "#3: The first content value in English", Culture = "en-US" }, + new() { Alias = "variantText", Value = "#3: The first content value in Danish", Culture = "da-DK" } + ] + }, + ], + SettingsData = + [ + new(settingsElementKey1, elementType.Key, elementType.Alias) + { + Values = + [ + new() { Alias = "invariantText", Value = "#1: The first invariant settings value" }, + new() { Alias = "variantText", Value = "#1: The first settings value in English", Culture = "en-US" }, + new() { Alias = "variantText", Value = "#1: The first settings value in Danish", Culture = "da-DK" } + ] + }, + new(settingsElementKey2, elementType.Key, elementType.Alias) + { + Values = + [ + new() { Alias = "invariantText", Value = "#2: The first invariant settings value" }, + new() { Alias = "variantText", Value = "#2: The first settings value in English", Culture = "en-US" }, + new() { Alias = "variantText", Value = "#2: The first settings value in Danish", Culture = "da-DK" } + ] + }, + new(settingsElementKey3, elementType.Key, elementType.Alias) + { + Values = + [ + new() { Alias = "invariantText", Value = "#3: The first invariant settings value" }, + new() { Alias = "variantText", Value = "#3: The first settings value in English", Culture = "en-US" }, + new() { Alias = "variantText", Value = "#3: The first settings value in Danish", Culture = "da-DK" } + ] + }, + ], + Expose = + [ + new (contentElementKey1, "en-US", null), + new (contentElementKey1, "da-DK", null), + new (contentElementKey2, "en-US", null), + new (contentElementKey2, "da-DK", null), + new (contentElementKey3, "en-US", null), + new (contentElementKey3, "da-DK", null), + ] + } + }; + } + + private IContent CreateContent(IContentType contentType, RichTextEditorValue richTextValue) + { + var contentBuilder = new ContentBuilder() + .WithContentType(contentType) + .WithCultureName("en-US", "Home (en)") + .WithCultureName("da-DK", "Home (da)"); + + var content = contentBuilder.Build(); + + var propertyValue = JsonSerializer.Serialize(richTextValue); + content.Properties["blocks"]!.SetValue(propertyValue); + + ContentService.Save(content); + return content; + } +} diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/PropertyEditors/RichTextPropertyEditorTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/PropertyEditors/RichTextPropertyEditorTests.cs index 84717ac203ee..7e93230765a8 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/PropertyEditors/RichTextPropertyEditorTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/PropertyEditors/RichTextPropertyEditorTests.cs @@ -94,19 +94,21 @@ public void Can_Track_Block_References() var propertyValue = RichTextPropertyEditorHelper.SerializeRichTextEditorValue( new RichTextEditorValue { - Markup = @$"

This is some markup

", + Markup = @$"

This is some markup

", Blocks = JsonSerializer.Deserialize($$""" { "layout": { "Umbraco.TinyMCE": [{ - "contentUdi": "umb://element/{{elementId:N}}" + "contentKey": "{{elementId:D}}" } ] }, "contentData": [{ "contentTypeKey": "{{elementType.Key:D}}", - "udi": "umb://element/{{elementId:N}}", - "contentPicker": "umb://document/{{pickedContent.Key:N}}" + "key": "{{elementId:D}}", + "values": [ + { "alias": "contentPicker", "value": "umb://document/{{pickedContent.Key:N}}" } + ] } ], "settingsData": [] @@ -145,19 +147,21 @@ public void Can_Track_Block_Tags() var propertyValue = RichTextPropertyEditorHelper.SerializeRichTextEditorValue( new RichTextEditorValue { - Markup = @$"

This is some markup

", + Markup = @$"

This is some markup

", Blocks = JsonSerializer.Deserialize($$""" { "layout": { "Umbraco.TinyMCE": [{ - "contentUdi": "umb://element/{{elementId:N}}" + "contentKey": "{{elementId:D}}" } ] }, "contentData": [{ "contentTypeKey": "{{elementType.Key:D}}", - "udi": "umb://element/{{elementId:N}}", - "tags": "[\"Tag One\", \"Tag Two\", \"Tag Three\"]" + "key": "{{elementId:D}}", + "values": [ + { "alias": "tags", "value": "[\"Tag One\", \"Tag Two\", \"Tag Three\"]" } + ] } ], "settingsData": [] diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentValidationServiceTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentValidationServiceTests.cs index ef8d46a07196..00e8284135b7 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentValidationServiceTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentValidationServiceTests.cs @@ -42,64 +42,93 @@ public async Task Can_Validate_Block_List_Nested_In_Block_List() { "layout": { "Umbraco.BlockList": [{ - "contentUdi": "umb://element/9addc377c02c4db088c273b933704f7b", - "settingsUdi": "umb://element/65db1ecd78e041a584f07296123a0a73" + "contentKey": "9addc377-c02c-4db0-88c2-73b933704f7b", + "settingsKey": "65db1ecd-78e0-41a5-84f0-7296123a0a73" }, { - "contentUdi": "umb://element/3af93b5b5e404c64b1422564309fc4c7", - "settingsUdi": "umb://element/efb9583ce67043f282fb2a0cb0f3e736" + "contentKey": "3af93b5b-5e40-4c64-b142-2564309fc4c7", + "settingsKey": "efb9583c-e670-43f2-82fb-2a0cb0f3e736" } ] }, "contentData": [{ "contentTypeKey": "{{setup.ElementType.Key}}", - "udi": "umb://element/9addc377c02c4db088c273b933704f7b", - "title": "Valid root content", - "blocks": { - "layout": { - "Umbraco.BlockList": [{ - "contentUdi": "umb://element/f36cebfad03b44519e604bf32c5b1e2f", - "settingsUdi": "umb://element/c9129a4671bb4b4e8f0ad525ad4a5de3" - }, { - "contentUdi": "umb://element/b8173e4a0618475c8277c3c6af68bee6", - "settingsUdi": "umb://element/77f7ea3507664395bf7f0c9df04530f7" - } - ] - }, - "contentData": [{ - "contentTypeKey": "{{setup.ElementType.Key}}", - "udi": "umb://element/f36cebfad03b44519e604bf32c5b1e2f", - "title": "Invalid nested content" - }, { - "contentTypeKey": "{{setup.ElementType.Key}}", - "udi": "umb://element/b8173e4a0618475c8277c3c6af68bee6", - "title": "Valid nested content" + "key": "9addc377-c02c-4db0-88c2-73b933704f7b", + "values": [ + { + "alias": "title", + "value": "Valid root content title" + }, + { + "alias": "blocks", + "value": { + "layout": { + "Umbraco.BlockList": [{ + "contentKey": "f36cebfa-d03b-4451-9e60-4bf32c5b1e2f", + "settingsKey": "c9129a46-71bb-4b4e-8f0a-d525ad4a5de3" + }, { + "contentKey": "b8173e4a-0618-475c-8277-c3c6af68bee6", + "settingsKey": "77f7ea35-0766-4395-bf7f-0c9df04530f7" + } + ] + }, + "contentData": [{ + "contentTypeKey": "{{setup.ElementType.Key}}", + "key": "f36cebfa-d03b-4451-9e60-4bf32c5b1e2f", + "values": [ + { "alias": "title", "value": "Invalid nested content title (ref #4)" }, + { "alias": "text", "value": "Valid nested content text" } + ] + }, { + "contentTypeKey": "{{setup.ElementType.Key}}", + "key": "b8173e4a-0618-475c-8277-c3c6af68bee6", + "values": [ + { "alias": "title", "value": "Valid nested content title" }, + { "alias": "text", "value": "Invalid nested content text (ref #5)" } + ] + } + ], + "settingsData": [{ + "contentTypeKey": "{{setup.ElementType.Key}}", + "key": "c9129a46-71bb-4b4e-8f0a-d525ad4a5de3", + "values": [ + { "alias": "title", "value": "Valid nested setting title" }, + { "alias": "text", "value": "Invalid nested setting text (ref #6)" } + ] + }, { + "contentTypeKey": "{{setup.ElementType.Key}}", + "key": "77f7ea35-0766-4395-bf7f-0c9df04530f7", + "values": [ + { "alias": "title", "value": "Invalid nested setting title (ref #7)" }, + { "alias": "text", "value": "Valid nested setting text)" } + ] + } + ] + } } - ], - "settingsData": [{ - "contentTypeKey": "{{setup.ElementType.Key}}", - "udi": "umb://element/c9129a4671bb4b4e8f0ad525ad4a5de3", - "title": "Valid nested setting" - }, { - "contentTypeKey": "{{setup.ElementType.Key}}", - "udi": "umb://element/77f7ea3507664395bf7f0c9df04530f7", - "title": "Invalid nested setting" - } - ] - } + ] }, { "contentTypeKey": "{{setup.ElementType.Key}}", - "udi": "umb://element/3af93b5b5e404c64b1422564309fc4c7", - "title": "Invalid root content" + "key": "3af93b5b-5e40-4c64-b142-2564309fc4c7", + "values": [ + { "alias": "title", "value": "Invalid root content title (ref #1)" }, + { "alias": "text", "value": "Valid root content text" } + ] } ], "settingsData": [{ "contentTypeKey": "{{setup.ElementType.Key}}", - "udi": "umb://element/65db1ecd78e041a584f07296123a0a73", - "title": "Invalid root setting" + "key": "65db1ecd-78e0-41a5-84f0-7296123a0a73", + "values": [ + { "alias": "title", "value": "Invalid root setting title (ref #2)" }, + { "alias": "text", "value": "Valid root setting text" } + ] }, { "contentTypeKey": "{{setup.ElementType.Key}}", - "udi": "umb://element/efb9583ce67043f282fb2a0cb0f3e736", - "title": "Valid root setting" + "key": "efb9583c-e670-43f2-82fb-2a0cb0f3e736", + "values": [ + { "alias": "title", "value": "Valid root setting title" }, + { "alias": "text", "value": "Invalid root setting text (ref #3)" } + ] } ] } @@ -109,11 +138,23 @@ public async Task Can_Validate_Block_List_Nested_In_Block_List() }, setup.DocumentType); - Assert.AreEqual(4, validationResult.ValidationErrors.Count()); - Assert.IsNotNull(validationResult.ValidationErrors.SingleOrDefault(r => r.Alias == "blocks" && r.JsonPath == ".contentData[0].blocks.contentData[0].title")); - Assert.IsNotNull(validationResult.ValidationErrors.SingleOrDefault(r => r.Alias == "blocks" && r.JsonPath == ".contentData[0].blocks.settingsData[1].title")); - Assert.IsNotNull(validationResult.ValidationErrors.SingleOrDefault(r => r.Alias == "blocks" && r.JsonPath == ".contentData[1].title")); - Assert.IsNotNull(validationResult.ValidationErrors.SingleOrDefault(r => r.Alias == "blocks" && r.JsonPath == ".settingsData[0].title")); + Assert.AreEqual(7, validationResult.ValidationErrors.Count()); + + // ref #1 + Assert.IsNotNull(validationResult.ValidationErrors.SingleOrDefault(r => r.Alias == "blocks" && r.JsonPath == ".contentData[1].values[0].value")); + // ref #2 + Assert.IsNotNull(validationResult.ValidationErrors.SingleOrDefault(r => r.Alias == "blocks" && r.JsonPath == ".settingsData[0].values[0].value")); + // ref #3 + Assert.IsNotNull(validationResult.ValidationErrors.SingleOrDefault(r => r.Alias == "blocks" && r.JsonPath == ".settingsData[1].values[1].value")); + + // ref #4 + Assert.IsNotNull(validationResult.ValidationErrors.SingleOrDefault(r => r.Alias == "blocks" && r.JsonPath == ".contentData[0].values[1].value.contentData[0].values[0].value")); + // ref #5 + Assert.IsNotNull(validationResult.ValidationErrors.SingleOrDefault(r => r.Alias == "blocks" && r.JsonPath == ".contentData[0].values[1].value.contentData[1].values[1].value")); + // ref #6 + Assert.IsNotNull(validationResult.ValidationErrors.SingleOrDefault(r => r.Alias == "blocks" && r.JsonPath == ".contentData[0].values[1].value.settingsData[0].values[1].value")); + // ref #7 + Assert.IsNotNull(validationResult.ValidationErrors.SingleOrDefault(r => r.Alias == "blocks" && r.JsonPath == ".contentData[0].values[1].value.settingsData[1].values[0].value")); } [TestCase(true)] @@ -461,6 +502,10 @@ public async Task Can_Validate_For_Specific_Language(string culture) { ValidationRegExp = "^Valid.*$" }); + elementType.AddPropertyType(new PropertyType(ShortStringHelper, textBoxDataType, "text") + { + ValidationRegExp = "^Valid.*$" + }); await ContentTypeService.SaveAsync(elementType, Constants.Security.SuperUserKey); // create a document type with the block list and a regex validated text box diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Tests.Integration.csproj b/tests/Umbraco.Tests.Integration/Umbraco.Tests.Integration.csproj index 6dfb33f112ea..7f930c61449f 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Tests.Integration.csproj +++ b/tests/Umbraco.Tests.Integration/Umbraco.Tests.Integration.csproj @@ -158,6 +158,12 @@ MediaTypeEditingServiceTests.cs + + BlockListElementLevelVariationTests.cs + + + BlockListElementLevelVariationTests.cs + DocumentNavigationServiceTests.cs diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/RichTextParserTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/RichTextParserTests.cs index a2522a5ecd9b..88f76dc7f379 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/RichTextParserTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/DeliveryApi/RichTextParserTests.cs @@ -259,7 +259,7 @@ public void ParseElement_CleansUpBlocks(bool inlineBlock) var id = Guid.NewGuid(); var tagName = $"umb-rte-block{(inlineBlock ? "-inline" : string.Empty)}"; - var element = parser.Parse($"

<{tagName} data-content-udi=\"umb://element/{id:N}\">

") as RichTextRootElement; + var element = parser.Parse($"

<{tagName} data-content-key=\"{id:N}\">

") as RichTextRootElement; Assert.IsNotNull(element); var paragraph = element.Elements.Single() as RichTextGenericElement; Assert.IsNotNull(paragraph); @@ -296,7 +296,7 @@ public void ParseElement_AppendsBlocks(bool inlineBlock) }); var tagName = $"umb-rte-block{(inlineBlock ? "-inline" : string.Empty)}"; - var element = parser.Parse($"

<{tagName} data-content-udi=\"umb://element/{block1ContentId:N}\"><{tagName} data-content-udi=\"umb://element/{block2ContentId:N}\">

", richTextBlockModel) as RichTextRootElement; + var element = parser.Parse($"

<{tagName} data-content-key=\"{block1ContentId:N}\"><{tagName} data-content-key=\"{block2ContentId:N}\">

", richTextBlockModel) as RichTextRootElement; Assert.IsNotNull(element); var paragraph = element.Elements.Single() as RichTextGenericElement; Assert.IsNotNull(paragraph); @@ -333,7 +333,7 @@ public void ParseElement_CanHandleMixedInlineAndBlockLevelBlocks() var id1 = Guid.NewGuid(); var id2 = Guid.NewGuid(); - var element = parser.Parse($"

") as RichTextRootElement; + var element = parser.Parse($"

") as RichTextRootElement; Assert.IsNotNull(element); Assert.AreEqual(2, element.Elements.Count()); @@ -435,7 +435,7 @@ public void ParseMarkup_CleansUpBlocks(bool inlineBlock) var id = Guid.NewGuid(); var tagName = $"umb-rte-block{(inlineBlock ? "-inline" : string.Empty)}"; - var result = parser.Parse($"

<{tagName} data-content-udi=\"umb://element/{id:N}\">

"); + var result = parser.Parse($"

<{tagName} data-content-key=\"{id:N}\">

"); Assert.AreEqual($"

<{tagName} data-content-id=\"{id:D}\">

", result); } @@ -446,7 +446,7 @@ public void ParseMarkup_CanHandleMixedInlineAndBlockLevelBlocks() var id1 = Guid.NewGuid(); var id2 = Guid.NewGuid(); - var result = parser.Parse($"

"); + var result = parser.Parse($"

"); Assert.AreEqual($"

", result); } diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Models/ContentTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Models/ContentTests.cs index b91861382014..d7a5e372a2e3 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Models/ContentTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Models/ContentTests.cs @@ -12,6 +12,7 @@ using Umbraco.Cms.Core.Logging; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.Entities; +using Umbraco.Cms.Core.PropertyEditors; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Tests.Common.Builders; using Umbraco.Cms.Tests.Common.Builders.Extensions; @@ -26,6 +27,8 @@ public class ContentTests { private readonly IContentTypeService _contentTypeService = Mock.Of(); + private readonly PropertyEditorCollection _propertyEditorCollection = new (new DataEditorCollection(() => [])); + [Test] public void Variant_Culture_Names_Track_Dirty_Changes() { @@ -87,7 +90,7 @@ public void Variant_Published_Culture_Names_Track_Dirty_Changes() Thread.Sleep(500); // The "Date" wont be dirty if the test runs too fast since it will be the same date content.SetCultureName("name-fr", langFr); - content.PublishCulture(CultureImpact.Explicit(langFr, false)); // we've set the name, now we're publishing it + content.PublishCulture(CultureImpact.Explicit(langFr, false), DateTime.Now, _propertyEditorCollection); // we've set the name, now we're publishing it Assert.IsTrue( content.IsPropertyDirty("PublishCultureInfos")); // now it will be changed since the collection has changed var frCultureName = content.PublishCultureInfos[langFr]; @@ -100,7 +103,7 @@ public void Variant_Published_Culture_Names_Track_Dirty_Changes() Thread.Sleep(500); // The "Date" wont be dirty if the test runs too fast since it will be the same date content.SetCultureName("name-fr", langFr); - content.PublishCulture(CultureImpact.Explicit(langFr, false)); // we've set the name, now we're publishing it + content.PublishCulture(CultureImpact.Explicit(langFr, false), DateTime.Now, _propertyEditorCollection); // we've set the name, now we're publishing it Assert.IsTrue(frCultureName.IsPropertyDirty("Date")); Assert.IsTrue(content.IsPropertyDirty("PublishCultureInfos")); // it's true now since we've updated a name } @@ -300,7 +303,7 @@ public void Can_Deep_Clone() content.SetCultureName("Hello", "en-US"); content.SetCultureName("World", "es-ES"); - content.PublishCulture(CultureImpact.All); + content.PublishCulture(CultureImpact.All, DateTime.Now, _propertyEditorCollection); // should not try to clone something that's not Published or Unpublished // (and in fact it will not work) @@ -413,7 +416,7 @@ public void Remember_Dirty_Properties() content.SetCultureName("Hello", "en-US"); content.SetCultureName("World", "es-ES"); - content.PublishCulture(CultureImpact.All); + content.PublishCulture(CultureImpact.All, DateTime.Now, _propertyEditorCollection); var i = 200; foreach (var property in content.Properties) diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Models/VariationTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Models/VariationTests.cs index c68f3f5b6531..a9ff943921f0 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Models/VariationTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Models/VariationTests.cs @@ -22,6 +22,8 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Models; [TestFixture] public class VariationTests { + private readonly PropertyEditorCollection _propertyEditorCollection = new (new DataEditorCollection(() => [])); + [Test] public void ValidateVariationTests() { @@ -315,7 +317,7 @@ public void ContentPublishValues() // can publish value // and get edited and published values - Assert.IsTrue(content.PublishCulture(CultureImpact.All)); + Assert.IsTrue(content.PublishCulture(CultureImpact.All, DateTime.Now, _propertyEditorCollection)); Assert.AreEqual("a", content.GetValue("prop")); Assert.AreEqual("a", content.GetValue("prop", published: true)); @@ -345,9 +347,9 @@ public void ContentPublishValues() // can publish value // and get edited and published values - Assert.IsFalse(content.PublishCulture(CultureImpact.Explicit(langFr, false))); // no name + Assert.IsFalse(content.PublishCulture(CultureImpact.Explicit(langFr, false), DateTime.Now, _propertyEditorCollection)); // no name content.SetCultureName("name-fr", langFr); - Assert.IsTrue(content.PublishCulture(CultureImpact.Explicit(langFr, false))); + Assert.IsTrue(content.PublishCulture(CultureImpact.Explicit(langFr, false), DateTime.Now, _propertyEditorCollection)); Assert.IsNull(content.GetValue("prop")); Assert.IsNull(content.GetValue("prop", published: true)); Assert.AreEqual("c", content.GetValue("prop", langFr)); @@ -361,7 +363,7 @@ public void ContentPublishValues() Assert.IsNull(content.GetValue("prop", langFr, published: true)); // can publish all - Assert.IsTrue(content.PublishCulture(CultureImpact.All)); + Assert.IsTrue(content.PublishCulture(CultureImpact.All, DateTime.Now, _propertyEditorCollection)); Assert.IsNull(content.GetValue("prop")); Assert.IsNull(content.GetValue("prop", published: true)); Assert.AreEqual("c", content.GetValue("prop", langFr)); @@ -371,14 +373,14 @@ public void ContentPublishValues() content.UnpublishCulture(langFr); Assert.AreEqual("c", content.GetValue("prop", langFr)); Assert.IsNull(content.GetValue("prop", langFr, published: true)); - Assert.IsTrue(content.PublishCulture(CultureImpact.Explicit(langFr, false))); + Assert.IsTrue(content.PublishCulture(CultureImpact.Explicit(langFr, false), DateTime.Now, _propertyEditorCollection)); Assert.AreEqual("c", content.GetValue("prop", langFr)); Assert.AreEqual("c", content.GetValue("prop", langFr, published: true)); content.UnpublishCulture(); // clears invariant props if any Assert.IsNull(content.GetValue("prop")); Assert.IsNull(content.GetValue("prop", published: true)); - Assert.IsTrue(content.PublishCulture(CultureImpact.All)); // publishes invariant props if any + Assert.IsTrue(content.PublishCulture(CultureImpact.All, DateTime.Now, _propertyEditorCollection)); // publishes invariant props if any Assert.IsNull(content.GetValue("prop")); Assert.IsNull(content.GetValue("prop", published: true)); @@ -437,19 +439,19 @@ public void ContentPublishValuesWithMixedPropertyTypeVariations() var langFrImpact = CultureImpact.Explicit(langFr, true); Assert.IsTrue( - content.PublishCulture(langFrImpact)); // succeeds because names are ok (not validating properties here) + content.PublishCulture(langFrImpact, DateTime.Now, _propertyEditorCollection)); // succeeds because names are ok (not validating properties here) Assert.IsFalse( propertyValidationService.IsPropertyDataValid(content, out _, langFrImpact)); // fails because prop1 is mandatory content.SetValue("prop1", "a", langFr); Assert.IsTrue( - content.PublishCulture(langFrImpact)); // succeeds because names are ok (not validating properties here) + content.PublishCulture(langFrImpact, DateTime.Now, _propertyEditorCollection)); // succeeds because names are ok (not validating properties here) // Fails because prop2 is mandatory and invariant and the item isn't published. // Invariant is validated against the default language except when there isn't a published version, in that case it's always validated. Assert.IsFalse(propertyValidationService.IsPropertyDataValid(content, out _, langFrImpact)); content.SetValue("prop2", "x"); - Assert.IsTrue(content.PublishCulture(langFrImpact)); // still ok... + Assert.IsTrue(content.PublishCulture(langFrImpact, DateTime.Now, _propertyEditorCollection)); // still ok... Assert.IsTrue(propertyValidationService.IsPropertyDataValid(content, out _, langFrImpact)); // now it's ok Assert.AreEqual("a", content.GetValue("prop1", langFr, published: true)); @@ -485,12 +487,12 @@ public void ContentPublishVariations() content.SetValue("prop", "a-es", langEs); // cannot publish without a name - Assert.IsFalse(content.PublishCulture(CultureImpact.Explicit(langFr, false))); + Assert.IsFalse(content.PublishCulture(CultureImpact.Explicit(langFr, false), DateTime.Now, _propertyEditorCollection)); // works with a name // and then FR is available, and published content.SetCultureName("name-fr", langFr); - Assert.IsTrue(content.PublishCulture(CultureImpact.Explicit(langFr, false))); + Assert.IsTrue(content.PublishCulture(CultureImpact.Explicit(langFr, false), DateTime.Now, _propertyEditorCollection)); // now UK is available too content.SetCultureName("name-uk", langUk); diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/BlockEditorComponentTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/BlockEditorComponentTests.cs index 607a39a7f0e5..719c0885c893 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/BlockEditorComponentTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/BlockEditorComponentTests.cs @@ -4,7 +4,6 @@ using Microsoft.Extensions.Logging; using Moq; using NUnit.Framework; -using Umbraco.Cms.Core; using Umbraco.Cms.Core.Models.Blocks; using Umbraco.Cms.Core.PropertyEditors; using Umbraco.Cms.Core.Serialization; @@ -16,12 +15,12 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.PropertyEditors; [TestFixture] public class BlockEditorComponentTests { - private const string ContentGuid1 = "036ce82586a64dfba2d523a99ed80f58"; - private const string ContentGuid2 = "48288c21a38a40ef82deb3eda90a58f6"; - private const string SettingsGuid1 = "ffd35c4e2eea4900abfa5611b67b2492"; - private const string SubContentGuid1 = "4c44ce6b3a5c4f5f8f15e3dc24819a9e"; - private const string SubContentGuid2 = "a062c06d6b0b44ac892b35d90309c7f8"; - private const string SubSettingsGuid1 = "4d998d980ffa4eee8afdc23c4abd6d29"; + private const string ContentGuid1 = "709b857e-6f00-45c6-bf65-f7da028c361f"; + private const string ContentGuid2 = "823dc755-28ec-4198-b050-514d91b7994e"; + private const string SettingsGuid1 = "4d2e18fe-f030-4ea9-aed9-10e7aee265fd"; + private const string SubContentGuid1 = "b5698cf9-bf26-4c1c-8b1c-db30a1b5c56a"; + private const string SubContentGuid2 = "68606a64-a03a-4b78-bcb1-39daee0c590d"; + private const string SubSettingsGuid1 = "5ce1b7da-7c9f-491e-9b95-5510fd28c50c"; private readonly IJsonSerializer _jsonSerializer = new SystemTextJsonSerializer(); @@ -30,7 +29,7 @@ public void Cannot_Have_Null_Udi() { var component = new BlockListPropertyNotificationHandler(Mock.Of>()); var json = GetBlockListJson(null, string.Empty); - Assert.Throws(() => component.ReplaceBlockEditorUdis(json)); + Assert.Throws(() => component.ReplaceBlockEditorKeys(json)); } [Test] @@ -46,7 +45,7 @@ Guid GuidFactory(Guid oldKey) var json = GetBlockListJson(null); var component = new BlockListPropertyNotificationHandler(Mock.Of>()); - var result = component.ReplaceBlockEditorUdis(json, GuidFactory); + var result = component.ReplaceBlockEditorKeys(json, GuidFactory); Assert.AreEqual(3, guidMap.Count); var expected = ReplaceGuids(json, guidMap); @@ -76,7 +75,7 @@ Guid GuidFactory(Guid oldKey) var json = GetBlockListJson(innerJsonEscaped); var component = new BlockListPropertyNotificationHandler(Mock.Of>()); - var result = component.ReplaceBlockEditorUdis(json, GuidFactory); + var result = component.ReplaceBlockEditorKeys(json, GuidFactory); // the expected result is that the subFeatures data remains escaped Assert.AreEqual(6, guidMap.Count); @@ -105,7 +104,7 @@ Guid GuidFactory(Guid oldKey) var json = GetBlockListJson(innerJson); var component = new BlockListPropertyNotificationHandler(Mock.Of>()); - var result = component.ReplaceBlockEditorUdis(json, GuidFactory); + var result = component.ReplaceBlockEditorKeys(json, GuidFactory); Assert.AreEqual(6, guidMap.Count); var expected = ReplaceGuids(GetBlockListJson(innerJson), guidMap); @@ -137,7 +136,7 @@ Guid GuidFactory(Guid oldKey) var json = GetBlockListJson(complexEditorJsonEscaped); var component = new BlockListPropertyNotificationHandler(Mock.Of>()); - var result = component.ReplaceBlockEditorUdis(json, GuidFactory); + var result = component.ReplaceBlockEditorKeys(json, GuidFactory); // the expected result is that the subFeatures remains escaped Assert.AreEqual(6, guidMap.Count); @@ -168,7 +167,7 @@ Guid GuidFactory(Guid oldKey) var json = GetBlockGridJson(innerJsonEscaped); var component = new BlockGridPropertyNotificationHandler(Mock.Of>()); - var result = component.ReplaceBlockEditorUdis(json, GuidFactory); + var result = component.ReplaceBlockEditorKeys(json, GuidFactory); // the expected result is that the subFeatures remains escaped Assert.AreEqual(13, guidMap.Count); @@ -195,7 +194,7 @@ Guid GuidFactory(Guid oldKey) var json = GetBlockGridJson(innerJson); var component = new BlockGridPropertyNotificationHandler(Mock.Of>()); - var result = component.ReplaceBlockEditorUdis(json, GuidFactory); + var result = component.ReplaceBlockEditorKeys(json, GuidFactory); // the expected result is that the subFeatures remains unescaped Assert.AreEqual(13, guidMap.Count); @@ -228,7 +227,7 @@ Guid GuidFactory(Guid oldKey) var json = GetBlockGridJson(innerJson); var component = new BlockGridPropertyNotificationHandler(Mock.Of>()); - var result = component.ReplaceBlockEditorUdis(json, GuidFactory); + var result = component.ReplaceBlockEditorKeys(json, GuidFactory); // the expected result is that the subFeatures remains unaltered - the UDIs within should still exist Assert.AreEqual(10, guidMap.Count); @@ -253,39 +252,41 @@ private string GetBlockListJson( { ""Umbraco.BlockList"": [ { - ""contentUdi"": """ + (contentGuid1.IsNullOrWhiteSpace() - ? string.Empty - : Udi.Create(Constants.UdiEntityType.Element, Guid.Parse(contentGuid1)).ToString()) + @""" + ""contentKey"": """ + (contentGuid1.IsNullOrWhiteSpace() ? string.Empty : contentGuid1) + @""" }, { - ""contentUdi"": ""umb://element/" + contentGuid2 + @""", - ""settingsUdi"": ""umb://element/" + settingsGuid1 + @""" + ""contentKey"": """ + contentGuid2 + @""", + ""settingsKey"": """ + settingsGuid1 + @""" } ] }, ""contentData"": [ { ""contentTypeKey"": ""d6ce4a86-91a2-45b3-a99c-8691fc1fb020"", - ""udi"": """ + (contentGuid1.IsNullOrWhiteSpace() - ? string.Empty - : Udi.Create(Constants.UdiEntityType.Element, Guid.Parse(contentGuid1)).ToString()) + @""", - ""featureName"": ""Hello"", - ""featureDetails"": ""World"" + ""key"": """ + (contentGuid1.IsNullOrWhiteSpace() ? string.Empty : contentGuid1) + @""", + ""values"": [ + { ""alias"": ""featureName"", ""value"": ""Hello"" }, + { ""alias"": ""featureDetails"", ""value"": ""World"" } + ] }, { ""contentTypeKey"": ""d6ce4a86-91a2-45b3-a99c-8691fc1fb020"", - ""udi"": ""umb://element/" + contentGuid2 + @""", - ""featureName"": ""Another"", - ""featureDetails"": ""Feature""" + - (subFeatures == null ? string.Empty : @", ""subFeatures"": " + subFeatures) + @" + ""key"": """ + contentGuid2 + @""", + ""values"": [ + { ""alias"": ""featureName"", ""value"": ""Another"" }, + { ""alias"": ""featureDetails"", ""value"": ""Feature"" }, + { ""alias"": ""subFeatures"", ""value"": " + subFeatures.OrIfNullOrWhiteSpace(@"""""") + @" } + ] } ], ""settingsData"": [ { ""contentTypeKey"": ""d6ce4a86-91a2-45b3-a99c-8691fc1fb020"", - ""udi"": ""umb://element/" + settingsGuid1 + @""", - ""featureName"": ""Setting 1"", - ""featureDetails"": ""Setting 2"" + ""key"": """ + settingsGuid1 + @""", + ""values"": [ + { ""alias"": ""featureName"", ""value"": ""Setting 1"" }, + { ""alias"": ""featureDetails"", ""value"": ""Setting 2"" } + ] } ] }"; @@ -350,7 +351,7 @@ private string GetBlockGridJson(string subFeatures) => @"{ ""layout"": { ""Umbraco.BlockGrid"": [{ - ""contentUdi"": ""umb://element/d05861169d124582a7c2826e52a51b47"", + ""contentKey"": ""fb0595b1-26e7-493f-86c7-bf2c42326850"", ""areas"": [{ ""key"": ""b17663f0-c1f4-4bee-97cd-290fbc7b9a2c"", ""items"": [] @@ -361,13 +362,13 @@ private string GetBlockGridJson(string subFeatures) => ], ""columnSpan"": 12, ""rowSpan"": 1, - ""settingsUdi"": ""umb://element/262d5efd2eeb43ed95e95c094c45ce1c"" + ""settingsKey"": ""0183ae81-2b62-49b5-8ac6-88d66c33068c"" }, { - ""contentUdi"": ""umb://element/5abad9f1b4e24d7aa269fbd1b50033ac"", + ""contentKey"": ""4852d9ef-ac8d-4d44-87c9-82d282aa0e7f"", ""areas"": [{ ""key"": ""b17663f0-c1f4-4bee-97cd-290fbc7b9a2c"", ""items"": [{ - ""contentUdi"": ""umb://element/5fc866c590be4d01a28a979472a1ffee"", + ""contentKey"": ""96a15ca9-3970-4e0a-9c66-18433bc23274"", ""areas"": [], ""columnSpan"": 3, ""rowSpan"": 1 @@ -376,25 +377,25 @@ private string GetBlockGridJson(string subFeatures) => }, { ""key"": ""2bdcdadd-f609-4acc-b840-01970b9ced1d"", ""items"": [{ - ""contentUdi"": ""umb://element/264536b65b0f4641aa43d4bfb515831d"", + ""contentKey"": ""3093f7f1-c931-4325-ba71-638eb2746c8d"", ""areas"": [], ""columnSpan"": 3, ""rowSpan"": 1, - ""settingsUdi"": ""umb://element/20d735c7c57b40229ed845375cf22d1f"" + ""settingsKey"": ""bef9eb67-56de-4fec-9fbc-1c7c02f5a5a7"" } ] } ], ""columnSpan"": 6, ""rowSpan"": 1, - ""settingsUdi"": ""umb://element/4d121eaba49c4e09a7460069d1bee600"" + ""settingsKey"": ""6eed3662-6ad1-4cba-805b-352f28599b0d"" }, { - ""contentUdi"": ""umb://element/76e24aeb6eeb4370892ca521932a96df"", + ""contentKey"": ""1f778485-933e-40b4-91e2-9926857a5c81"", ""areas"": [], ""columnSpan"": 6, ""rowSpan"": 1 }, { - ""contentUdi"": ""umb://element/90549d94555647fdbe4d111c7178ada4"", + ""contentKey"": ""2d5c6555-0dd8-4db2-b0c9-2d2eba29026d"", ""areas"": [{ ""key"": ""b17663f0-c1f4-4bee-97cd-290fbc7b9a2c"", ""items"": [] @@ -405,51 +406,67 @@ private string GetBlockGridJson(string subFeatures) => ], ""columnSpan"": 12, ""rowSpan"": 3, - ""settingsUdi"": ""umb://element/3dfabc96584c4c35ac2e6bf06ffa20de"" + ""settingsKey"": ""48a7b7da-673f-44d5-8bad-7d71d157fb3e"" } ] }, ""contentData"": [{ ""contentTypeKey"": ""36ccf44a-aac8-40a6-8685-73ab03bc9709"", - ""udi"": ""umb://element/d05861169d124582a7c2826e52a51b47"", - ""title"": ""Element one - 12 cols"" + ""key"": ""fb0595b1-26e7-493f-86c7-bf2c42326850"", + ""values"": [ + { ""alias"": ""title"", ""value"": ""Element one - 12 cols"" } + ] }, { ""contentTypeKey"": ""36ccf44a-aac8-40a6-8685-73ab03bc9709"", - ""udi"": ""umb://element/5abad9f1b4e24d7aa269fbd1b50033ac"", - ""title"": ""Element one - 6 cols, left side"" + ""key"": ""4852d9ef-ac8d-4d44-87c9-82d282aa0e7f"", + ""values"": [ + { ""alias"": ""title"", ""value"": ""Element one - 6 cols, left side"" } + ] }, { ""contentTypeKey"": ""5cc488aa-ba24-41f2-a01e-8f2d1982f865"", - ""udi"": ""umb://element/76e24aeb6eeb4370892ca521932a96df"", - ""text"": ""Element two - 6 cols, right side"" + ""key"": ""1f778485-933e-40b4-91e2-9926857a5c81"", + ""values"": [ + { ""alias"": ""title"", ""value"": ""Element one - 6 cols, right side"" } + ] }, { ""contentTypeKey"": ""36ccf44a-aac8-40a6-8685-73ab03bc9709"", - ""udi"": ""umb://element/90549d94555647fdbe4d111c7178ada4"", - ""title"": ""One more element one - 12 cols"", - ""subFeatures"": " + subFeatures.OrIfNullOrWhiteSpace(@"""""") + @" + ""key"": ""2d5c6555-0dd8-4db2-b0c9-2d2eba29026d"", + ""values"": [ + { ""alias"": ""title"", ""value"": ""One more element one - 12 cols"" }, + { ""alias"": ""subFeatures"", ""value"": " + subFeatures.OrIfNullOrWhiteSpace(@"""""") + @" } + ] }, { ""contentTypeKey"": ""5cc488aa-ba24-41f2-a01e-8f2d1982f865"", - ""udi"": ""umb://element/5fc866c590be4d01a28a979472a1ffee"", - ""text"": ""Nested element two - left side"" + ""key"": ""96a15ca9-3970-4e0a-9c66-18433bc23274"", + ""values"": [ + { ""alias"": ""title"", ""value"": ""Nested element two - left side"" } + ] }, { ""contentTypeKey"": ""36ccf44a-aac8-40a6-8685-73ab03bc9709"", - ""udi"": ""umb://element/264536b65b0f4641aa43d4bfb515831d"", - ""title"": ""Nested element one - right side"" + ""key"": ""3093f7f1-c931-4325-ba71-638eb2746c8d"", + ""values"": [ + { ""alias"": ""title"", ""value"": ""Nested element one - right side"" } + ] } ], ""settingsData"": [{ ""contentTypeKey"": ""ef150524-7145-469e-8d99-166aad69a7ad"", - ""udi"": ""umb://element/262d5efd2eeb43ed95e95c094c45ce1c"", - ""enabled"": 1 + ""key"": ""0183ae81-2b62-49b5-8ac6-88d66c33068c"", + ""values"": [ + { ""alias"": ""enabled"", ""value"": 1 } + ] }, { ""contentTypeKey"": ""ef150524-7145-469e-8d99-166aad69a7ad"", - ""udi"": ""umb://element/4d121eaba49c4e09a7460069d1bee600"" + ""key"": ""6eed3662-6ad1-4cba-805b-352f28599b0d"" }, { ""contentTypeKey"": ""ef150524-7145-469e-8d99-166aad69a7ad"", - ""udi"": ""umb://element/20d735c7c57b40229ed845375cf22d1f"" + ""key"": ""bef9eb67-56de-4fec-9fbc-1c7c02f5a5a7"" }, { ""contentTypeKey"": ""ef150524-7145-469e-8d99-166aad69a7ad"", - ""udi"": ""umb://element/3dfabc96584c4c35ac2e6bf06ffa20de"", - ""enabled"": 1 + ""key"": ""48a7b7da-673f-44d5-8bad-7d71d157fb3e"", + ""values"": [ + { ""alias"": ""enabled"", ""value"": 1 } + ] } ] }"; @@ -458,7 +475,7 @@ private string ReplaceGuids(string json, Dictionary guidMap) { foreach ((Guid oldKey, Guid newKey) in guidMap) { - json = json.Replace(oldKey.ToString("N"), newKey.ToString("N")); + json = json.Replace(oldKey.ToString("D"), newKey.ToString("D")); } return json; diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/BlockGridPropertyValueConverterTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/BlockGridPropertyValueConverterTests.cs index 5756a05622df..6b090ed7c8c7 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/BlockGridPropertyValueConverterTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/BlockGridPropertyValueConverterTests.cs @@ -7,6 +7,7 @@ using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Cms.Core.PropertyEditors; using Umbraco.Cms.Core.PropertyEditors.ValueConverters; +using Umbraco.Cms.Core.Services; using Umbraco.Cms.Infrastructure.Serialization; namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.PropertyEditors; @@ -35,15 +36,15 @@ public void Convert_Valid_Json() var editor = CreateConverter(); var config = ConfigForSingle(SettingKey1); var propertyType = GetPropertyType(config); - var publishedElement = Mock.Of(); + var publishedElement = GetPublishedElement(); var json = @" { ""layout"": { """ + Constants.PropertyEditors.Aliases.BlockGrid + @""": [ { - ""contentUdi"": ""umb://element/1304E1DDAC87439684FE8A399231CB3D"", - ""settingsUdi"": ""umb://element/2D3529EDB47B4B109F6D4B802DD5DFE2"", + ""contentKey"": ""1304E1DD-AC87-4396-84FE-8A399231CB3D"", + ""settingsKey"": ""2D3529ED-B47B-4B10-9F6D-4B802DD5DFE2"", ""rowSpan"": 1, ""columnSpan"": 12, ""areas"": [] @@ -53,13 +54,18 @@ public void Convert_Valid_Json() ""contentData"": [ { ""contentTypeKey"": """ + ContentKey1 + @""", - ""udi"": ""umb://element/1304E1DDAC87439684FE8A399231CB3D"" + ""key"": ""1304E1DD-AC87-4396-84FE-8A399231CB3D"" } ], ""settingsData"": [ { ""contentTypeKey"": """ + SettingKey1 + @""", - ""udi"": ""umb://element/2D3529EDB47B4B109F6D4B802DD5DFE2"" + ""key"": ""2D3529ED-B47B-4B10-9F6D-4B802DD5DFE2"" + } + ], + ""expose"": [ + { + ""contentKey"": ""1304E1DD-AC87-4396-84FE-8A399231CB3D"" } ] }"; @@ -70,10 +76,10 @@ public void Convert_Valid_Json() 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(Guid.Parse("1304E1DD-AC87-4396-84FE-8A399231CB3D"), converted[0].ContentKey); 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(Guid.Parse("2D3529ED-B47B-4B10-9F6D-4B802DD5DFE2"), converted[0].Settings!.Key); + Assert.AreEqual(Guid.Parse("2D3529ED-B47B-4B10-9F6D-4B802DD5DFE2"), converted[0].SettingsKey); Assert.AreEqual(SettingAlias1, converted[0].Settings.ContentType.Alias); } @@ -83,14 +89,14 @@ public void Can_Convert_Without_Settings() var editor = CreateConverter(); var config = ConfigForSingle(); var propertyType = GetPropertyType(config); - var publishedElement = Mock.Of(); + var publishedElement = GetPublishedElement(); var json = @" { ""layout"": { """ + Constants.PropertyEditors.Aliases.BlockGrid + @""": [ { - ""contentUdi"": ""umb://element/1304E1DDAC87439684FE8A399231CB3D"", + ""contentKey"": ""1304E1DD-AC87-4396-84FE-8A399231CB3D"", ""rowSpan"": 1, ""columnSpan"": 12, ""areas"": [] @@ -100,7 +106,12 @@ public void Can_Convert_Without_Settings() ""contentData"": [ { ""contentTypeKey"": """ + ContentKey1 + @""", - ""udi"": ""umb://element/1304E1DDAC87439684FE8A399231CB3D"" + ""key"": ""1304E1DD-AC87-4396-84FE-8A399231CB3D"" + } + ], + ""expose"": [ + { + ""contentKey"": ""1304E1DD-AC87-4396-84FE-8A399231CB3D"" } ] }"; @@ -112,7 +123,7 @@ public void Can_Convert_Without_Settings() 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(Guid.Parse("1304E1DD-AC87-4396-84FE-8A399231CB3D"), converted[0].ContentKey); Assert.AreEqual("Test1", item0.ContentType.Alias); Assert.IsNull(converted[0].Settings); } @@ -123,14 +134,14 @@ public void Ignores_Other_Layouts() var editor = CreateConverter(); var config = ConfigForSingle(); var propertyType = GetPropertyType(config); - var publishedElement = Mock.Of(); + var publishedElement = GetPublishedElement(); var json = @" { ""layout"": { """ + Constants.PropertyEditors.Aliases.BlockGrid + @""": [ { - ""contentUdi"": ""umb://element/1304E1DDAC87439684FE8A399231CB3D"", + ""contentKey"": ""1304E1DD-AC87-4396-84FE-8A399231CB3D"", ""rowSpan"": 1, ""columnSpan"": 12, ""areas"": [] @@ -138,19 +149,24 @@ public void Ignores_Other_Layouts() ], """ + Constants.PropertyEditors.Aliases.BlockList + @""": [ { - ""contentUdi"": ""umb://element/1304E1DDAC87439684FE8A399231CB3D"" + ""contentKey"": ""1304E1DD-AC87-4396-84FE-8A399231CB3D"" } ], ""Some.Custom.BlockEditor"": [ { - ""contentUdi"": ""umb://element/1304E1DDAC87439684FE8A399231CB3D"" + ""contentKey"": ""1304E1DD-AC87-4396-84FE-8A399231CB3D"" } ] }, ""contentData"": [ { ""contentTypeKey"": """ + ContentKey1 + @""", - ""udi"": ""umb://element/1304E1DDAC87439684FE8A399231CB3D"" + ""key"": ""1304E1DD-AC87-4396-84FE-8A399231CB3D"" + } + ], + ""expose"": [ + { + ""contentKey"": ""1304E1DD-AC87-4396-84FE-8A399231CB3D"" } ] }"; @@ -162,7 +178,7 @@ public void Ignores_Other_Layouts() 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(Guid.Parse("1304E1DD-AC87-4396-84FE-8A399231CB3D"), converted[0].ContentKey); Assert.AreEqual("Test1", item0.ContentType.Alias); Assert.IsNull(converted[0].Settings); } @@ -171,12 +187,15 @@ private BlockGridPropertyValueConverter CreateConverter() { var publishedSnapshotAccessor = GetPublishedSnapshotAccessor(); var publishedModelFactory = new NoopPublishedModelFactory(); + var blockVarianceHandler = new BlockEditorVarianceHandler(Mock.Of()); var editor = new BlockGridPropertyValueConverter( Mock.Of(), - new BlockEditorConverter(publishedSnapshotAccessor, publishedModelFactory), + new BlockEditorConverter(publishedSnapshotAccessor, publishedModelFactory, Mock.Of(), blockVarianceHandler), new SystemTextJsonSerializer(), new ApiElementBuilder(Mock.Of()), - new BlockGridPropertyValueConstructorCache()); + new BlockGridPropertyValueConstructorCache(), + Mock.Of(), + blockVarianceHandler); return editor; } diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/BlockListPropertyValueConverterTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/BlockListPropertyValueConverterTests.cs index 8c4634a4e20d..db720fca4654 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/BlockListPropertyValueConverterTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/BlockListPropertyValueConverterTests.cs @@ -24,13 +24,16 @@ private BlockListPropertyValueConverter CreateConverter() { var publishedSnapshotAccessor = GetPublishedSnapshotAccessor(); var publishedModelFactory = new NoopPublishedModelFactory(); + var blockVarianceHandler = new BlockEditorVarianceHandler(Mock.Of()); var editor = new BlockListPropertyValueConverter( Mock.Of(), - new BlockEditorConverter(publishedSnapshotAccessor, publishedModelFactory), + new BlockEditorConverter(publishedSnapshotAccessor, publishedModelFactory, Mock.Of(), blockVarianceHandler), Mock.Of(), new ApiElementBuilder(Mock.Of()), new SystemTextJsonSerializer(), - new BlockListPropertyValueConstructorCache()); + new BlockListPropertyValueConstructorCache(), + Mock.Of(), + blockVarianceHandler); return editor; } @@ -123,7 +126,7 @@ public void Convert_Null_Empty() var editor = CreateConverter(); var config = ConfigForMany(); var propertyType = GetPropertyType(config); - var publishedElement = Mock.Of(); + var publishedElement = GetPublishedElement(); string json = null; var converted = @@ -146,7 +149,7 @@ public void Convert_Valid_Empty_Json() var editor = CreateConverter(); var config = ConfigForMany(); var propertyType = GetPropertyType(config); - var publishedElement = Mock.Of(); + var publishedElement = GetPublishedElement(); var json = "{}"; var converted = @@ -170,11 +173,16 @@ public void Convert_Valid_Empty_Json() ""layout"": { """ + Constants.PropertyEditors.Aliases.BlockList + @""": [ { - ""contentUdi"": ""umb://element/e7dba547615b4e9ab4ab2a7674845bc9"" + ""contentKey"": ""e7dba547-615b-4e9a-b4ab-2a7674845bc9"" } ] }, - ""contentData"": [] + ""contentData"": [], + ""expose"": [ + { + ""contentKey"": ""e7dba547-615b-4e9a-b4ab-2a7674845bc9"" + } + ] }"; converted = editor.ConvertIntermediateToObject(publishedElement, propertyType, PropertyCacheLevel.None, json, false) as BlockListModel; @@ -188,13 +196,18 @@ public void Convert_Valid_Empty_Json() ""layout"": { """ + Constants.PropertyEditors.Aliases.BlockList + @""": [ { - ""contentUdi"": ""umb://element/e7dba547615b4e9ab4ab2a7674845bc9"" + ""contentKey"": ""e7dba547-615b-4e9a-b4ab-2a7674845bc9"" } ] }, ""contentData"": [ { - ""udi"": ""umb://element/e7dba547615b4e9ab4ab2a7674845bc9"" + ""key"": ""e7dba547-615b-4e9a-b4ab-2a7674845bc9"" + } + ], + ""expose"": [ + { + ""contentKey"": ""e7dba547-615b-4e9a-b4ab-2a7674845bc9"" } ] }"; @@ -210,7 +223,7 @@ public void Convert_Valid_Empty_Json() ""layout"": { """ + Constants.PropertyEditors.Aliases.BlockList + @""": [ { - ""contentUdi"": ""umb://element/1304E1DDAC87439684FE8A399231CB3D"" + ""contentKey"": ""1304E1DD-AC87-4396-84FE-8A399231CB3D"" } ] }, @@ -219,6 +232,11 @@ public void Convert_Valid_Empty_Json() ""contentTypeKey"": """ + ContentKey1 + @""", ""key"": ""1304E1DD-0000-4396-84FE-8A399231CB3D"" } + ], + ""expose"": [ + { + ""contentKey"": ""1304E1DD-0000-4396-84FE-8A399231CB3D"" + } ] }"; @@ -234,21 +252,26 @@ public void Convert_Valid_Json() var editor = CreateConverter(); var config = ConfigForMany(); var propertyType = GetPropertyType(config); - var publishedElement = Mock.Of(); + var publishedElement = GetPublishedElement(); var json = @" { ""layout"": { """ + Constants.PropertyEditors.Aliases.BlockList + @""": [ { - ""contentUdi"": ""umb://element/1304E1DDAC87439684FE8A399231CB3D"" + ""contentKey"": ""1304E1DD-AC87-4396-84FE-8A399231CB3D"" } ] }, ""contentData"": [ { ""contentTypeKey"": """ + ContentKey1 + @""", - ""udi"": ""umb://element/1304E1DDAC87439684FE8A399231CB3D"" + ""key"": ""1304E1DD-AC87-4396-84FE-8A399231CB3D"" + } + ], + ""expose"": [ + { + ""contentKey"": ""1304E1DD-AC87-4396-84FE-8A399231CB3D"" } ] }"; @@ -262,7 +285,7 @@ public void Convert_Valid_Json() Assert.AreEqual(Guid.Parse("1304E1DD-AC87-4396-84FE-8A399231CB3D"), item0.Key); Assert.AreEqual("Test1", item0.ContentType.Alias); Assert.IsNull(converted[0].Settings); - Assert.AreEqual(UdiParser.Parse("umb://element/1304E1DDAC87439684FE8A399231CB3D"), converted[0].ContentUdi); + Assert.AreEqual(Guid.Parse("1304E1DD-AC87-4396-84FE-8A399231CB3D"), converted[0].ContentKey); } [Test] @@ -271,51 +294,61 @@ public void Get_Data_From_Layout_Item() var editor = CreateConverter(); var config = ConfigForMany(); var propertyType = GetPropertyType(config); - var publishedElement = Mock.Of(); + var publishedElement = GetPublishedElement(); var json = @" { ""layout"": { """ + Constants.PropertyEditors.Aliases.BlockList + @""": [ { - ""contentUdi"": ""umb://element/1304E1DDAC87439684FE8A399231CB3D"", - ""settingsUdi"": ""umb://element/1F613E26CE274898908A561437AF5100"" + ""contentKey"": ""1304E1DD-AC87-4396-84FE-8A399231CB3D"", + ""settingsKey"": ""1F613E26-CE27-4898-908A-561437AF5100"" }, { - ""contentUdi"": ""umb://element/0A4A416E547D464FABCC6F345C17809A"", - ""settingsUdi"": ""umb://element/63027539B0DB45E7B70459762D4E83DD"" + ""contentKey"": ""0A4A416E-547D-464F-ABCC-6F345C17809A"", + ""settingsKey"": ""63027539-B0DB-45E7-B704-59762D4E83DD"" } ] }, ""contentData"": [ { ""contentTypeKey"": """ + ContentKey1 + @""", - ""udi"": ""umb://element/1304E1DDAC87439684FE8A399231CB3D"" + ""key"": ""1304E1DD-AC87-4396-84FE-8A399231CB3D"" }, { ""contentTypeKey"": """ + ContentKey2 + @""", - ""udi"": ""umb://element/E05A034704424AB3A520E048E6197E79"" + ""key"": ""E05A0347-0442-4AB3-A520-E048E6197E79"" }, { ""contentTypeKey"": """ + ContentKey2 + @""", - ""udi"": ""umb://element/0A4A416E547D464FABCC6F345C17809A"" + ""key"": ""0A4A416E-547D-464F-ABCC-6F345C17809A"" } ], ""settingsData"": [ { ""contentTypeKey"": """ + SettingKey1 + @""", - ""udi"": ""umb://element/63027539B0DB45E7B70459762D4E83DD"" + ""key"": ""63027539-B0DB-45E7-B704-59762D4E83DD"" }, { ""contentTypeKey"": """ + SettingKey2 + @""", - ""udi"": ""umb://element/1F613E26CE274898908A561437AF5100"" + ""key"": ""1F613E26-CE27-4898-908A-561437AF5100"" }, { ""contentTypeKey"": """ + SettingKey2 + @""", - ""udi"": ""umb://element/BCF4BA3DA40C496C93EC58FAC85F18B9"" + ""key"": ""BCF4BA3D-A40C-496C-93EC-58FAC85F18B9"" } - ] -}"; + ], + ""expose"": [ + { + ""contentKey"": ""1304E1DD-AC87-4396-84FE-8A399231CB3D"" + }, + { + ""contentKey"": ""E05A0347-0442-4AB3-A520-E048E6197E79"" + }, + { + ""contentKey"": ""0A4A416E-547D-464F-ABCC-6F345C17809A"" + } + ]}"; var converted = editor.ConvertIntermediateToObject(publishedElement, propertyType, PropertyCacheLevel.None, json, false) as @@ -327,13 +360,13 @@ public void Get_Data_From_Layout_Item() var item0 = converted[0]; Assert.AreEqual(Guid.Parse("1304E1DD-AC87-4396-84FE-8A399231CB3D"), item0.Content.Key); Assert.AreEqual("Test1", item0.Content.ContentType.Alias); - Assert.AreEqual(Guid.Parse("1F613E26CE274898908A561437AF5100"), item0.Settings.Key); + Assert.AreEqual(Guid.Parse("1F613E26-CE27-4898-908A-561437AF5100"), item0.Settings!.Key); Assert.AreEqual("Setting2", item0.Settings.ContentType.Alias); var item1 = converted[1]; Assert.AreEqual(Guid.Parse("0A4A416E-547D-464F-ABCC-6F345C17809A"), item1.Content.Key); Assert.AreEqual("Test2", item1.Content.ContentType.Alias); - Assert.AreEqual(Guid.Parse("63027539B0DB45E7B70459762D4E83DD"), item1.Settings.Key); + Assert.AreEqual(Guid.Parse("63027539-B0DB-45E7-B704-59762D4E83DD"), item1.Settings!.Key); Assert.AreEqual("Setting1", item1.Settings.ContentType.Alias); } @@ -357,48 +390,59 @@ public void Data_Item_Removed_If_Removed_FromConfig() }; var propertyType = GetPropertyType(config); - var publishedElement = Mock.Of(); + var publishedElement = GetPublishedElement(); var json = @" { ""layout"": { """ + Constants.PropertyEditors.Aliases.BlockList + @""": [ { - ""contentUdi"": ""umb://element/1304E1DDAC87439684FE8A399231CB3D"", - ""settingsUdi"": ""umb://element/1F613E26CE274898908A561437AF5100"" + ""contentKey"": ""1304E1DD-AC87-4396-84FE-8A399231CB3D"", + ""settingsKey"": ""1F613E26-CE27-4898-908A-561437AF5100"" }, { - ""contentUdi"": ""umb://element/0A4A416E547D464FABCC6F345C17809A"", - ""settingsUdi"": ""umb://element/63027539B0DB45E7B70459762D4E83DD"" + ""contentKey"": ""0A4A416E-547D-464F-ABCC-6F345C17809A"", + ""settingsKey"": ""63027539-B0DB-45E7-B704-59762D4E83DD"" } ] }, ""contentData"": [ { ""contentTypeKey"": """ + ContentKey1 + @""", - ""udi"": ""umb://element/1304E1DDAC87439684FE8A399231CB3D"" + ""key"": ""1304E1DD-AC87-4396-84FE-8A399231CB3D"" }, { ""contentTypeKey"": """ + ContentKey2 + @""", - ""udi"": ""umb://element/E05A034704424AB3A520E048E6197E79"" + ""key"": ""E05A0347-0442-4AB3-A520-E048E6197E79"" }, { ""contentTypeKey"": """ + ContentKey2 + @""", - ""udi"": ""umb://element/0A4A416E547D464FABCC6F345C17809A"" + ""key"": ""0A4A416E-547D-464F-ABCC-6F345C17809A"" } ], ""settingsData"": [ { ""contentTypeKey"": """ + SettingKey1 + @""", - ""udi"": ""umb://element/63027539B0DB45E7B70459762D4E83DD"" + ""key"": ""63027539-B0DB-45E7-B704-59762D4E83DD"" }, { ""contentTypeKey"": """ + SettingKey2 + @""", - ""udi"": ""umb://element/1F613E26CE274898908A561437AF5100"" + ""key"": ""1F613E26-CE27-4898-908A-561437AF5100"" }, { ""contentTypeKey"": """ + SettingKey2 + @""", - ""udi"": ""umb://element/BCF4BA3DA40C496C93EC58FAC85F18B9"" + ""key"": ""BCF4BA3D-A40C-496C-93EC-58FAC85F18B9"" + } + ], + ""expose"": [ + { + ""contentKey"": ""1304E1DD-AC87-4396-84FE-8A399231CB3D"" + }, + { + ""contentKey"": ""E05A0347-0442-4AB3-A520-E048E6197E79"" + }, + { + ""contentKey"": ""0A4A416E-547D-464F-ABCC-6F345C17809A"" } ] }"; diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/BlockPropertyValueConverterTestsBase.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/BlockPropertyValueConverterTestsBase.cs index 232b30da26dc..1e2739c7ac38 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/BlockPropertyValueConverterTestsBase.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/BlockPropertyValueConverterTestsBase.cs @@ -1,4 +1,5 @@ using Moq; +using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.PublishedContent; using Umbraco.Cms.Core.PublishedCache; @@ -61,4 +62,7 @@ protected IPublishedPropertyType GetPropertyType(TPropertyEditorConfig config) && x.DataType == dataType); return propertyType; } + + protected IPublishedElement GetPublishedElement() + => Mock.Of(m => m.ContentType == Mock.Of()); } diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/DataValueEditorReuseTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/DataValueEditorReuseTests.cs index 5c3c76176524..48d5c3e0c646 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/DataValueEditorReuseTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/DataValueEditorReuseTests.cs @@ -6,6 +6,7 @@ using Umbraco.Cms.Core.IO; using Umbraco.Cms.Core.Models.Blocks; using Umbraco.Cms.Core.PropertyEditors; +using Umbraco.Cms.Core.PropertyEditors.ValueConverters; using Umbraco.Cms.Core.Serialization; using Umbraco.Cms.Core.Services; using Umbraco.Cms.Core.Strings; @@ -35,6 +36,7 @@ public void SetUp() _propertyEditorCollection = new PropertyEditorCollection(new DataEditorCollection(Enumerable.Empty)); _dataValueReferenceFactories = new DataValueReferenceFactoryCollection(Enumerable.Empty); + var blockVarianceHandler = new BlockEditorVarianceHandler(Mock.Of()); _dataValueEditorFactoryMock .Setup(m => m.Create(It.IsAny(), It.IsAny>())) @@ -50,7 +52,8 @@ public void SetUp() Mock.Of(), Mock.Of(), Mock.Of(), - Mock.Of())); + Mock.Of(), + blockVarianceHandler)); } [Test] diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/RichTextPropertyEditorHelperTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/RichTextPropertyEditorHelperTests.cs index 71fa5f085c97..72e2d77fa0ba 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/RichTextPropertyEditorHelperTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/RichTextPropertyEditorHelperTests.cs @@ -27,25 +27,29 @@ public void Can_Parse_JObject() { var input = JsonNode.Parse("""" { - "markup": "

this is some markup

", + "markup": "

this is some markup

", "blocks": { "layout": { "Umbraco.TinyMCE": [{ - "contentUdi": "umb://element/36cc710ad8a645d0a07f7bbd8742cf02", - "settingsUdi": "umb://element/d2eeef66411142f4a1647a523eaffbc2" + "contentKey": "36cc710a-d8a6-45d0-a07f-7bbd8742cf02", + "settingsKey": "d2eeef66-4111-42f4-a164-7a523eaffbc2" } ] }, "contentData": [{ "contentTypeKey": "b2f0806c-d231-4c78-88b2-3c97d26e1123", - "udi": "umb://element/36cc710ad8a645d0a07f7bbd8742cf02", - "contentPropertyAlias": "A content property value" + "key": "36cc710a-d8a6-45d0-a07f-7bbd8742cf02", + "values": [ + { "alias": "contentPropertyAlias", "value": "A content property value" } + ] } ], "settingsData": [{ "contentTypeKey": "e7a9447f-e14d-44dd-9ae8-e68c3c3da598", - "udi": "umb://element/d2eeef66411142f4a1647a523eaffbc2", - "settingsPropertyAlias": "A settings property value" + "key": "d2eeef66-4111-42f4-a164-7a523eaffbc2", + "values": [ + { "alias": "settingsPropertyAlias", "value": "A settings property value" } + ] } ] } @@ -55,7 +59,7 @@ public void Can_Parse_JObject() var result = RichTextPropertyEditorHelper.TryParseRichTextEditorValue(input, JsonSerializer(), Logger(), out RichTextEditorValue? value); Assert.IsTrue(result); Assert.IsNotNull(value); - Assert.AreEqual("

this is some markup

", value.Markup); + Assert.AreEqual("

this is some markup

", value.Markup); Assert.IsNotNull(value.Blocks); @@ -64,16 +68,30 @@ public void Can_Parse_JObject() var contentTypeGuid = Guid.Parse("b2f0806c-d231-4c78-88b2-3c97d26e1123"); var itemGuid = Guid.Parse("36cc710a-d8a6-45d0-a07f-7bbd8742cf02"); Assert.AreEqual(contentTypeGuid, item.ContentTypeKey); - Assert.AreEqual(new GuidUdi(Constants.UdiEntityType.Element, itemGuid), item.Udi); Assert.AreEqual(itemGuid, item.Key); + Assert.AreEqual(itemGuid, item.Key); + var contentProperties = value.Blocks.ContentData.First().Values; + Assert.AreEqual(1, contentProperties.Count); + Assert.Multiple(() => + { + Assert.AreEqual("contentPropertyAlias", contentProperties.First().Alias); + Assert.AreEqual("A content property value", contentProperties.First().Value); + }); Assert.AreEqual(1, value.Blocks.SettingsData.Count); item = value.Blocks.SettingsData.Single(); contentTypeGuid = Guid.Parse("e7a9447f-e14d-44dd-9ae8-e68c3c3da598"); itemGuid = Guid.Parse("d2eeef66-4111-42f4-a164-7a523eaffbc2"); Assert.AreEqual(contentTypeGuid, item.ContentTypeKey); - Assert.AreEqual(new GuidUdi(Constants.UdiEntityType.Element, itemGuid), item.Udi); Assert.AreEqual(itemGuid, item.Key); + Assert.AreEqual(itemGuid, item.Key); + var settingsProperties = value.Blocks.SettingsData.First().Values; + Assert.AreEqual(1, settingsProperties.Count); + Assert.Multiple(() => + { + Assert.AreEqual("settingsPropertyAlias", settingsProperties.First().Alias); + Assert.AreEqual("A settings property value", settingsProperties.First().Value); + }); } [Test] @@ -81,25 +99,29 @@ public void Can_Parse_Blocks_With_Both_Content_And_Settings() { const string input = """ { - "markup": "

this is some markup

", + "markup": "

this is some markup

", "blocks": { "layout": { "Umbraco.TinyMCE": [{ - "contentUdi": "umb://element/36cc710ad8a645d0a07f7bbd8742cf02", - "settingsUdi": "umb://element/d2eeef66411142f4a1647a523eaffbc2" + "contentKey": "36cc710a-d8a6-45d0-a07f-7bbd8742cf02", + "settingsKey": "d2eeef66-4111-42f4-a164-7a523eaffbc2" } ] }, "contentData": [{ "contentTypeKey": "b2f0806c-d231-4c78-88b2-3c97d26e1123", - "udi": "umb://element/36cc710ad8a645d0a07f7bbd8742cf02", - "contentPropertyAlias": "A content property value" + "key": "36cc710a-d8a6-45d0-a07f-7bbd8742cf02", + "values": [ + { "alias": "contentPropertyAlias", "value": "A content property value" } + ] } ], "settingsData": [{ "contentTypeKey": "e7a9447f-e14d-44dd-9ae8-e68c3c3da598", - "udi": "umb://element/d2eeef66411142f4a1647a523eaffbc2", - "settingsPropertyAlias": "A settings property value" + "key": "d2eeef66-4111-42f4-a164-7a523eaffbc2", + "values": [ + { "alias": "settingsPropertyAlias", "value": "A settings property value" } + ] } ] } @@ -109,7 +131,7 @@ public void Can_Parse_Blocks_With_Both_Content_And_Settings() var result = RichTextPropertyEditorHelper.TryParseRichTextEditorValue(input, JsonSerializer(), Logger(), out RichTextEditorValue? value); Assert.IsTrue(result); Assert.IsNotNull(value); - Assert.AreEqual("

this is some markup

", value.Markup); + Assert.AreEqual("

this is some markup

", value.Markup); Assert.IsNotNull(value.Blocks); @@ -118,16 +140,30 @@ public void Can_Parse_Blocks_With_Both_Content_And_Settings() var contentTypeGuid = Guid.Parse("b2f0806c-d231-4c78-88b2-3c97d26e1123"); var itemGuid = Guid.Parse("36cc710a-d8a6-45d0-a07f-7bbd8742cf02"); Assert.AreEqual(contentTypeGuid, item.ContentTypeKey); - Assert.AreEqual(new GuidUdi(Constants.UdiEntityType.Element, itemGuid), item.Udi); Assert.AreEqual(itemGuid, item.Key); + Assert.AreEqual(itemGuid, item.Key); + var contentProperties = value.Blocks.ContentData.First().Values; + Assert.AreEqual(1, contentProperties.Count); + Assert.Multiple(() => + { + Assert.AreEqual("contentPropertyAlias", contentProperties.First().Alias); + Assert.AreEqual("A content property value", contentProperties.First().Value); + }); Assert.AreEqual(1, value.Blocks.SettingsData.Count); item = value.Blocks.SettingsData.Single(); contentTypeGuid = Guid.Parse("e7a9447f-e14d-44dd-9ae8-e68c3c3da598"); itemGuid = Guid.Parse("d2eeef66-4111-42f4-a164-7a523eaffbc2"); Assert.AreEqual(contentTypeGuid, item.ContentTypeKey); - Assert.AreEqual(new GuidUdi(Constants.UdiEntityType.Element, itemGuid), item.Udi); Assert.AreEqual(itemGuid, item.Key); + Assert.AreEqual(itemGuid, item.Key); + var settingsProperties = value.Blocks.SettingsData.First().Values; + Assert.AreEqual(1, settingsProperties.Count); + Assert.Multiple(() => + { + Assert.AreEqual("settingsPropertyAlias", settingsProperties.First().Alias); + Assert.AreEqual("A settings property value", settingsProperties.First().Value); + }); } [Test] @@ -135,18 +171,20 @@ public void Can_Parse_Blocks_With_Content_Only() { const string input = """ { - "markup": "

this is some markup

", + "markup": "

this is some markup

", "blocks": { "layout": { "Umbraco.TinyMCE": [{ - "contentUdi": "umb://element/36cc710ad8a645d0a07f7bbd8742cf02" + "contentKey": "36cc710a-d8a6-45d0-a07f-7bbd8742cf02" } ] }, "contentData": [{ "contentTypeKey": "b2f0806c-d231-4c78-88b2-3c97d26e1123", - "udi": "umb://element/36cc710ad8a645d0a07f7bbd8742cf02", - "contentPropertyAlias": "A content property value" + "key": "36cc710a-d8a6-45d0-a07f-7bbd8742cf02", + "values": [ + { "alias": "contentPropertyAlias", "value": "A content property value" } + ] } ], "settingsData": [] @@ -157,7 +195,7 @@ public void Can_Parse_Blocks_With_Content_Only() var result = RichTextPropertyEditorHelper.TryParseRichTextEditorValue(input, JsonSerializer(), Logger(), out RichTextEditorValue? value); Assert.IsTrue(result); Assert.IsNotNull(value); - Assert.AreEqual("

this is some markup

", value.Markup); + Assert.AreEqual("

this is some markup

", value.Markup); Assert.IsNotNull(value.Blocks); @@ -166,8 +204,15 @@ public void Can_Parse_Blocks_With_Content_Only() var contentTypeGuid = Guid.Parse("b2f0806c-d231-4c78-88b2-3c97d26e1123"); var itemGuid = Guid.Parse("36cc710a-d8a6-45d0-a07f-7bbd8742cf02"); Assert.AreEqual(contentTypeGuid, item.ContentTypeKey); - Assert.AreEqual(new GuidUdi(Constants.UdiEntityType.Element, itemGuid), item.Udi); Assert.AreEqual(itemGuid, item.Key); + Assert.AreEqual(itemGuid, item.Key); + var contentProperties = value.Blocks.ContentData.First().Values; + Assert.AreEqual(1, contentProperties.Count); + Assert.Multiple(() => + { + Assert.AreEqual("contentPropertyAlias", contentProperties.First().Alias); + Assert.AreEqual("A content property value", contentProperties.First().Value); + }); Assert.AreEqual(0, value.Blocks.SettingsData.Count); } @@ -177,23 +222,23 @@ public void Can_Parse_Mixed_Blocks_And_Inline_Blocks() { const string input = """ { - "markup": "

this is some markup

", + "markup": "

this is some markup

", "blocks": { "layout": { "Umbraco.TinyMCE": [{ - "contentUdi": "umb://element/36cc710ad8a645d0a07f7bbd8742cf02" + "contentKey": "36cc710a-d8a6-45d0-a07f-7bbd8742cf02" }, { - "contentUdi": "umb://element/36cc710ad8a645d0a07f7bbd8742cf03" + "contentKey": "36cc710a-d8a6-45d0-a07f-7bbd8742cf03" } ] }, "contentData": [{ "contentTypeKey": "b2f0806c-d231-4c78-88b2-3c97d26e1123", - "udi": "umb://element/36cc710ad8a645d0a07f7bbd8742cf02", + "key": "36cc710a-d8a6-45d0-a07f-7bbd8742cf02", "contentPropertyAlias": "A content property value" }, { "contentTypeKey": "b2f0806c-d231-4c78-88b2-3c97d26e1124", - "udi": "umb://element/36cc710ad8a645d0a07f7bbd8742cf03", + "key": "36cc710a-d8a6-45d0-a07f-7bbd8742cf03", "contentPropertyAlias": "A content property value" } ], @@ -205,7 +250,7 @@ public void Can_Parse_Mixed_Blocks_And_Inline_Blocks() var result = RichTextPropertyEditorHelper.TryParseRichTextEditorValue(input, JsonSerializer(), Logger(), out RichTextEditorValue? value); Assert.IsTrue(result); Assert.IsNotNull(value); - Assert.AreEqual("

this is some markup

", value.Markup); + Assert.AreEqual("

this is some markup

", value.Markup); Assert.IsNotNull(value.Blocks); @@ -216,7 +261,6 @@ public void Can_Parse_Mixed_Blocks_And_Inline_Blocks() for (var i = 0; i < value.Blocks.ContentData.Count; i++) { var item = value.Blocks.ContentData[i]; Assert.AreEqual(contentTypeGuids[i], item.ContentTypeKey); - Assert.AreEqual(new GuidUdi(Constants.UdiEntityType.Element, itemGuids[i]), item.Udi); Assert.AreEqual(itemGuids[i], item.Key); } diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/PropertyEditors/BlockEditorVarianceHandlerTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/PropertyEditors/BlockEditorVarianceHandlerTests.cs new file mode 100644 index 000000000000..664478f16b5e --- /dev/null +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/PropertyEditors/BlockEditorVarianceHandlerTests.cs @@ -0,0 +1,75 @@ +using Moq; +using NUnit.Framework; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.Blocks; +using Umbraco.Cms.Core.Models.PublishedContent; +using Umbraco.Cms.Core.PropertyEditors.ValueConverters; +using Umbraco.Cms.Core.Services; + +namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.PropertyEditors; + +// TODO KJA: more tests for BlockEditorVarianceHandler +[TestFixture] +public class BlockEditorVarianceHandlerTests +{ + [Test] + public async Task Assigns_Default_Culture_When_Culture_Variance_Is_Enabled() + { + var propertyValue = new BlockPropertyValue { Culture = null }; + var subject = BlockEditorVarianceHandler("da-DK"); + var result = await subject.AlignedPropertyVarianceAsync( + propertyValue, + PublishedPropertyType(ContentVariation.Culture), + PublishedElement(ContentVariation.Culture)); + Assert.IsNotNull(result); + Assert.AreEqual("da-DK", result.Culture); + } + + [Test] + public async Task Removes_Default_Culture_When_Culture_Variance_Is_Disabled() + { + var propertyValue = new BlockPropertyValue { Culture = "da-DK" }; + var subject = BlockEditorVarianceHandler("da-DK"); + var result = await subject.AlignedPropertyVarianceAsync( + propertyValue, + PublishedPropertyType(ContentVariation.Nothing), + PublishedElement(ContentVariation.Nothing)); + Assert.IsNotNull(result); + Assert.AreEqual(null, result.Culture); + } + + [Test] + public async Task Ignores_NonDefault_Culture_When_Culture_Variance_Is_Disabled() + { + var propertyValue = new BlockPropertyValue { Culture = "en-US" }; + var subject = BlockEditorVarianceHandler("da-DK"); + var result = await subject.AlignedPropertyVarianceAsync( + propertyValue, + PublishedPropertyType(ContentVariation.Nothing), + PublishedElement(ContentVariation.Nothing)); + Assert.IsNull(result); + } + + private static IPublishedPropertyType PublishedPropertyType(ContentVariation variation) + { + var propertyTypeMock = new Mock(); + propertyTypeMock.SetupGet(m => m.Variations).Returns(variation); + return propertyTypeMock.Object; + } + + private static IPublishedElement PublishedElement(ContentVariation variation) + { + var contentTypeMock = new Mock(); + contentTypeMock.SetupGet(m => m.Variations).Returns(variation); + var elementMock = new Mock(); + elementMock.SetupGet(m => m.ContentType).Returns(contentTypeMock.Object); + return elementMock.Object; + } + + private static BlockEditorVarianceHandler BlockEditorVarianceHandler(string defaultLanguageIsoCode) + { + var languageServiceMock = new Mock(); + languageServiceMock.Setup(m => m.GetDefaultIsoCodeAsync()).ReturnsAsync(defaultLanguageIsoCode); + return new BlockEditorVarianceHandler(languageServiceMock.Object); + } +} diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Serialization/JsonBlockValueConverterTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Serialization/JsonBlockValueConverterTests.cs index 09000d474eaa..7377efdf3572 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Serialization/JsonBlockValueConverterTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Serialization/JsonBlockValueConverterTests.cs @@ -11,14 +11,14 @@ 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 contentElementKey1 = Guid.NewGuid(); + var settingsElementKey1 = Guid.NewGuid(); + var contentElementKey2 = Guid.NewGuid(); + var settingsElementKey2 = Guid.NewGuid(); + var contentElementKey3 = Guid.NewGuid(); + var settingsElementKey3 = Guid.NewGuid(); + var contentElementKey4 = Guid.NewGuid(); + var settingsElementKey4 = Guid.NewGuid(); var elementType1Key = Guid.NewGuid(); var elementType2Key = Guid.NewGuid(); @@ -27,7 +27,7 @@ public void Can_Serialize_BlockGrid_With_Blocks() var blockGridValue = new BlockGridValue( [ - new BlockGridLayoutItem(contentElementUdi1, settingsElementUdi1) + new BlockGridLayoutItem(contentElementKey1, settingsElementKey1) { ColumnSpan = 123, RowSpan = 456, @@ -37,7 +37,7 @@ public void Can_Serialize_BlockGrid_With_Blocks() { Items = [ - new BlockGridLayoutItem(contentElementUdi3, settingsElementUdi3) + new BlockGridLayoutItem(contentElementKey3, settingsElementKey3) { ColumnSpan = 12, RowSpan = 34, @@ -47,7 +47,7 @@ public void Can_Serialize_BlockGrid_With_Blocks() { Items = [ - new BlockGridLayoutItem(contentElementUdi4, settingsElementUdi4) + new BlockGridLayoutItem(contentElementKey4, settingsElementKey4) { ColumnSpan = 56, RowSpan = 78, @@ -60,7 +60,7 @@ public void Can_Serialize_BlockGrid_With_Blocks() }, ], }, - new BlockGridLayoutItem(contentElementUdi2, settingsElementUdi2) + new BlockGridLayoutItem(contentElementKey2, settingsElementKey2) { ColumnSpan = 789, RowSpan = 123, @@ -69,17 +69,17 @@ public void Can_Serialize_BlockGrid_With_Blocks() { ContentData = [ - new(contentElementUdi1, elementType1Key, "elementType1"), - new(contentElementUdi2, elementType2Key, "elementType2"), - new(contentElementUdi3, elementType3Key, "elementType3"), - new(contentElementUdi4, elementType4Key, "elementType4"), + new(contentElementKey1, elementType1Key, "elementType1"), + new(contentElementKey2, elementType2Key, "elementType2"), + new(contentElementKey3, elementType3Key, "elementType3"), + new(contentElementKey4, elementType4Key, "elementType4"), ], SettingsData = [ - new(settingsElementUdi1, elementType3Key, "elementType3"), - new(settingsElementUdi2, elementType4Key, "elementType4"), - new(settingsElementUdi3, elementType1Key, "elementType1"), - new(settingsElementUdi4, elementType2Key, "elementType2") + new(settingsElementKey1, elementType3Key, "elementType3"), + new(settingsElementKey2, elementType4Key, "elementType4"), + new(settingsElementKey3, elementType1Key, "elementType1"), + new(settingsElementKey4, elementType2Key, "elementType2") ] }; @@ -97,13 +97,13 @@ public void Can_Serialize_BlockGrid_With_Blocks() { 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(contentElementKey1, layoutItems[0].ContentKey); + Assert.AreEqual(settingsElementKey1, layoutItems[0].SettingsKey); 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(contentElementKey2, layoutItems[1].ContentKey); + Assert.AreEqual(settingsElementKey2, layoutItems[1].SettingsKey); }); Assert.AreEqual(1, layoutItems[0].Areas.Length); @@ -113,8 +113,8 @@ public void Can_Serialize_BlockGrid_With_Blocks() { 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(contentElementKey3, layoutItems[0].Areas[0].Items[0].ContentKey); + Assert.AreEqual(settingsElementKey3, layoutItems[0].Areas[0].Items[0].SettingsKey); }); Assert.AreEqual(1, layoutItems[0].Areas[0].Items[0].Areas.Length); @@ -124,26 +124,26 @@ public void Can_Serialize_BlockGrid_With_Blocks() { 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(contentElementKey4, layoutItems[0].Areas[0].Items[0].Areas[0].Items[0].ContentKey); + Assert.AreEqual(settingsElementKey4, layoutItems[0].Areas[0].Items[0].Areas[0].Items[0].SettingsKey); }); Assert.AreEqual(4, deserialized.ContentData.Count); Assert.Multiple(() => { - Assert.AreEqual(contentElementUdi1, deserialized.ContentData[0].Udi); + Assert.AreEqual(contentElementKey1, deserialized.ContentData[0].Key); 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(contentElementKey2, deserialized.ContentData[1].Key); Assert.AreEqual(elementType2Key, deserialized.ContentData[1].ContentTypeKey); Assert.AreEqual(string.Empty, deserialized.ContentData[1].ContentTypeAlias); - Assert.AreEqual(contentElementUdi3, deserialized.ContentData[2].Udi); + Assert.AreEqual(contentElementKey3, deserialized.ContentData[2].Key); Assert.AreEqual(elementType3Key, deserialized.ContentData[2].ContentTypeKey); Assert.AreEqual(string.Empty, deserialized.ContentData[2].ContentTypeAlias); - Assert.AreEqual(contentElementUdi3, deserialized.ContentData[2].Udi); + Assert.AreEqual(contentElementKey3, deserialized.ContentData[2].Key); Assert.AreEqual(elementType3Key, deserialized.ContentData[2].ContentTypeKey); Assert.AreEqual(string.Empty, deserialized.ContentData[2].ContentTypeAlias); }); @@ -151,19 +151,19 @@ public void Can_Serialize_BlockGrid_With_Blocks() Assert.AreEqual(4, deserialized.SettingsData.Count); Assert.Multiple(() => { - Assert.AreEqual(settingsElementUdi1, deserialized.SettingsData[0].Udi); + Assert.AreEqual(settingsElementKey1, deserialized.SettingsData[0].Key); Assert.AreEqual(elementType3Key, deserialized.SettingsData[0].ContentTypeKey); Assert.AreEqual(string.Empty, deserialized.SettingsData[0].ContentTypeAlias); - Assert.AreEqual(settingsElementUdi2, deserialized.SettingsData[1].Udi); + Assert.AreEqual(settingsElementKey2, deserialized.SettingsData[1].Key); Assert.AreEqual(elementType4Key, deserialized.SettingsData[1].ContentTypeKey); Assert.AreEqual(string.Empty, deserialized.SettingsData[1].ContentTypeAlias); - Assert.AreEqual(settingsElementUdi3, deserialized.SettingsData[2].Udi); + Assert.AreEqual(settingsElementKey3, deserialized.SettingsData[2].Key); Assert.AreEqual(elementType1Key, deserialized.SettingsData[2].ContentTypeKey); Assert.AreEqual(string.Empty, deserialized.SettingsData[2].ContentTypeAlias); - Assert.AreEqual(settingsElementUdi4, deserialized.SettingsData[3].Udi); + Assert.AreEqual(settingsElementKey4, deserialized.SettingsData[3].Key); Assert.AreEqual(elementType2Key, deserialized.SettingsData[3].ContentTypeKey); Assert.AreEqual(string.Empty, deserialized.SettingsData[3].ContentTypeAlias); }); @@ -189,10 +189,10 @@ public void Can_Serialize_BlockGrid_Without_Blocks() [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 contentElementKey1 = Guid.NewGuid(); + var settingsElementKey1 = Guid.NewGuid(); + var contentElementKey2 = Guid.NewGuid(); + var settingsElementKey2 = Guid.NewGuid(); var elementType1Key = Guid.NewGuid(); var elementType2Key = Guid.NewGuid(); @@ -201,19 +201,19 @@ public void Can_Serialize_BlockList_With_Blocks() var blockListValue = new BlockListValue( [ - new BlockListLayoutItem(contentElementUdi1, settingsElementUdi1), - new BlockListLayoutItem(contentElementUdi2, settingsElementUdi2), + new BlockListLayoutItem(contentElementKey1, settingsElementKey1), + new BlockListLayoutItem(contentElementKey2, settingsElementKey2), ]) { ContentData = [ - new(contentElementUdi1, elementType1Key, "elementType1"), - new(contentElementUdi2, elementType2Key, "elementType2") + new(contentElementKey1, elementType1Key, "elementType1"), + new(contentElementKey2, elementType2Key, "elementType2") ], SettingsData = [ - new(settingsElementUdi1, elementType3Key, "elementType3"), - new(settingsElementUdi2, elementType4Key, "elementType4") + new(settingsElementKey1, elementType3Key, "elementType3"), + new(settingsElementKey2, elementType4Key, "elementType4") ] }; @@ -229,21 +229,21 @@ public void Can_Serialize_BlockList_With_Blocks() Assert.AreEqual(2, layoutItems.Count()); Assert.Multiple(() => { - Assert.AreEqual(contentElementUdi1, layoutItems.First().ContentUdi); - Assert.AreEqual(settingsElementUdi1, layoutItems.First().SettingsUdi); + Assert.AreEqual(contentElementKey1, layoutItems.First().ContentKey); + Assert.AreEqual(settingsElementKey1, layoutItems.First().SettingsKey); - Assert.AreEqual(contentElementUdi2, layoutItems.Last().ContentUdi); - Assert.AreEqual(settingsElementUdi2, layoutItems.Last().SettingsUdi); + Assert.AreEqual(contentElementKey2, layoutItems.Last().ContentKey); + Assert.AreEqual(settingsElementKey2, layoutItems.Last().SettingsKey); }); Assert.AreEqual(2, deserialized.ContentData.Count); Assert.Multiple(() => { - Assert.AreEqual(contentElementUdi1, deserialized.ContentData.First().Udi); + Assert.AreEqual(contentElementKey1, deserialized.ContentData.First().Key); 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(contentElementKey2, deserialized.ContentData.Last().Key); Assert.AreEqual(elementType2Key, deserialized.ContentData.Last().ContentTypeKey); Assert.AreEqual(string.Empty, deserialized.ContentData.Last().ContentTypeAlias); }); @@ -251,11 +251,11 @@ public void Can_Serialize_BlockList_With_Blocks() Assert.AreEqual(2, deserialized.SettingsData.Count); Assert.Multiple(() => { - Assert.AreEqual(settingsElementUdi1, deserialized.SettingsData.First().Udi); + Assert.AreEqual(settingsElementKey1, deserialized.SettingsData.First().Key); Assert.AreEqual(elementType3Key, deserialized.SettingsData.First().ContentTypeKey); Assert.AreEqual(string.Empty, deserialized.SettingsData.First().ContentTypeAlias); - Assert.AreEqual(settingsElementUdi2, deserialized.SettingsData.Last().Udi); + Assert.AreEqual(settingsElementKey2, deserialized.SettingsData.Last().Key); Assert.AreEqual(elementType4Key, deserialized.SettingsData.Last().ContentTypeKey); Assert.AreEqual(string.Empty, deserialized.SettingsData.Last().ContentTypeAlias); }); @@ -281,10 +281,10 @@ public void Can_Serialize_BlockList_Without_Blocks() [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 contentElementKey1 = Guid.NewGuid(); + var settingsElementKey1 = Guid.NewGuid(); + var contentElementKey2 = Guid.NewGuid(); + var settingsElementKey2 = Guid.NewGuid(); var elementType1Key = Guid.NewGuid(); var elementType2Key = Guid.NewGuid(); @@ -293,19 +293,19 @@ public void Can_Serialize_Richtext_With_Blocks() var richTextBlockValue = new RichTextBlockValue( [ - new RichTextBlockLayoutItem(contentElementUdi1, settingsElementUdi1), - new RichTextBlockLayoutItem(contentElementUdi2, settingsElementUdi2), + new RichTextBlockLayoutItem(contentElementKey1, settingsElementKey1), + new RichTextBlockLayoutItem(contentElementKey2, settingsElementKey2), ]) { ContentData = [ - new(contentElementUdi1, elementType1Key, "elementType1"), - new(contentElementUdi2, elementType2Key, "elementType2") + new(contentElementKey1, elementType1Key, "elementType1"), + new(contentElementKey2, elementType2Key, "elementType2") ], SettingsData = [ - new(settingsElementUdi1, elementType3Key, "elementType3"), - new(settingsElementUdi2, elementType4Key, "elementType4") + new(settingsElementKey1, elementType3Key, "elementType3"), + new(settingsElementKey2, elementType4Key, "elementType4") ] }; @@ -325,26 +325,26 @@ public void Can_Serialize_Richtext_With_Blocks() 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.IsTrue(deserializedBlocks.Layout.ContainsKey(Constants.PropertyEditors.Aliases.RichText)); + var layoutItems = deserializedBlocks.Layout[Constants.PropertyEditors.Aliases.RichText].OfType().ToArray(); Assert.AreEqual(2, layoutItems.Count()); Assert.Multiple(() => { - Assert.AreEqual(contentElementUdi1, layoutItems.First().ContentUdi); - Assert.AreEqual(settingsElementUdi1, layoutItems.First().SettingsUdi); + Assert.AreEqual(contentElementKey1, layoutItems.First().ContentKey); + Assert.AreEqual(settingsElementKey1, layoutItems.First().SettingsKey); - Assert.AreEqual(contentElementUdi2, layoutItems.Last().ContentUdi); - Assert.AreEqual(settingsElementUdi2, layoutItems.Last().SettingsUdi); + Assert.AreEqual(contentElementKey2, layoutItems.Last().ContentKey); + Assert.AreEqual(settingsElementKey2, layoutItems.Last().SettingsKey); }); Assert.AreEqual(2, deserializedBlocks.ContentData.Count); Assert.Multiple(() => { - Assert.AreEqual(contentElementUdi1, deserializedBlocks.ContentData.First().Udi); + Assert.AreEqual(contentElementKey1, deserializedBlocks.ContentData.First().Key); 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(contentElementKey2, deserializedBlocks.ContentData.Last().Key); Assert.AreEqual(elementType2Key, deserializedBlocks.ContentData.Last().ContentTypeKey); Assert.AreEqual(string.Empty, deserializedBlocks.ContentData.Last().ContentTypeAlias); }); @@ -352,11 +352,11 @@ public void Can_Serialize_Richtext_With_Blocks() Assert.AreEqual(2, deserializedBlocks.SettingsData.Count); Assert.Multiple(() => { - Assert.AreEqual(settingsElementUdi1, deserializedBlocks.SettingsData.First().Udi); + Assert.AreEqual(settingsElementKey1, deserializedBlocks.SettingsData.First().Key); Assert.AreEqual(elementType3Key, deserializedBlocks.SettingsData.First().ContentTypeKey); Assert.AreEqual(string.Empty, deserializedBlocks.SettingsData.First().ContentTypeAlias); - Assert.AreEqual(settingsElementUdi2, deserializedBlocks.SettingsData.Last().Udi); + Assert.AreEqual(settingsElementKey2, deserializedBlocks.SettingsData.Last().Key); Assert.AreEqual(elementType4Key, deserializedBlocks.SettingsData.Last().ContentTypeKey); Assert.AreEqual(string.Empty, deserializedBlocks.SettingsData.Last().ContentTypeAlias); }); @@ -389,39 +389,39 @@ public void Can_Serialize_Richtext_Without_Blocks() [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 contentElementKey1 = Guid.NewGuid(); + var settingsElementKey1 = Guid.NewGuid(); var elementType1Key = Guid.NewGuid(); var elementType2Key = Guid.NewGuid(); var blockListValue = new BlockListValue( [ - new BlockListLayoutItem(contentElementUdi1, settingsElementUdi1), + new BlockListLayoutItem(contentElementKey1, settingsElementKey1), ]) { Layout = { - [Constants.PropertyEditors.Aliases.TinyMce] = + [Constants.PropertyEditors.Aliases.RichText] = [ - new RichTextBlockLayoutItem(contentElementUdi1, settingsElementUdi1) + new RichTextBlockLayoutItem(contentElementKey1, settingsElementKey1) ], [Constants.PropertyEditors.Aliases.BlockGrid] = [ - new BlockGridLayoutItem(contentElementUdi1, settingsElementUdi1), + new BlockGridLayoutItem(contentElementKey1, settingsElementKey1), ], ["Some.Custom.Block.Editor"] = [ - new BlockListLayoutItem(contentElementUdi1, settingsElementUdi1), + new BlockListLayoutItem(contentElementKey1, settingsElementKey1), ] }, ContentData = [ - new(contentElementUdi1, elementType1Key, "elementType1"), + new(contentElementKey1, elementType1Key, "elementType1"), ], SettingsData = [ - new(settingsElementUdi1, elementType2Key, "elementType2") + new(settingsElementKey1, elementType2Key, "elementType2") ] }; @@ -437,8 +437,8 @@ public void Ignores_Other_Layouts() Assert.AreEqual(1, layoutItems.Count()); Assert.Multiple(() => { - Assert.AreEqual(contentElementUdi1, layoutItems.First().ContentUdi); - Assert.AreEqual(settingsElementUdi1, layoutItems.First().SettingsUdi); + Assert.AreEqual(contentElementKey1, layoutItems.First().ContentKey); + Assert.AreEqual(settingsElementKey1, layoutItems.First().SettingsKey); }); } }