diff --git a/src/Umbraco.Core/Services/ContentBlueprintEditingService.cs b/src/Umbraco.Core/Services/ContentBlueprintEditingService.cs index 7af5171bc9..5e8357e453 100644 --- a/src/Umbraco.Core/Services/ContentBlueprintEditingService.cs +++ b/src/Umbraco.Core/Services/ContentBlueprintEditingService.cs @@ -45,11 +45,13 @@ internal sealed class ContentBlueprintEditingService return Task.FromResult(null); } + IContent scaffold = blueprint.DeepCloneWithResetIdentities(); + using ICoreScope scope = CoreScopeProvider.CreateCoreScope(); - scope.Notifications.Publish(new ContentScaffoldedNotification(blueprint, blueprint, Constants.System.Root, new EventMessages())); + scope.Notifications.Publish(new ContentScaffoldedNotification(blueprint, scaffold, Constants.System.Root, new EventMessages())); scope.Complete(); - return Task.FromResult(blueprint); + return Task.FromResult(scaffold); } public async Task?, ContentEditingOperationStatus>> GetPagedByContentTypeAsync(Guid contentTypeKey, int skip, int take) @@ -112,7 +114,7 @@ internal sealed class ContentBlueprintEditingService // Create Blueprint var currentUserId = await GetUserIdAsync(userKey); - IContent blueprint = ContentService.CreateContentFromBlueprint(content, name, currentUserId); + IContent blueprint = ContentService.CreateBlueprintFromContent(content, name, currentUserId); if (key.HasValue) { diff --git a/src/Umbraco.Core/Services/ContentService.cs b/src/Umbraco.Core/Services/ContentService.cs index f246b9c3f0..34ac8db8ff 100644 --- a/src/Umbraco.Core/Services/ContentService.cs +++ b/src/Umbraco.Core/Services/ContentService.cs @@ -3654,12 +3654,12 @@ public class ContentService : RepositoryService, IContentService private static readonly string?[] ArrayOfOneNullString = { null }; - public IContent CreateContentFromBlueprint(IContent blueprint, string name, int userId = Constants.Security.SuperUserId) + public IContent CreateBlueprintFromContent( + IContent blueprint, + string name, + int userId = Constants.Security.SuperUserId) { - if (blueprint == null) - { - throw new ArgumentNullException(nameof(blueprint)); - } + ArgumentNullException.ThrowIfNull(blueprint); IContentType contentType = GetContentType(blueprint.ContentType.Alias); var content = new Content(name, -1, contentType); @@ -3672,15 +3672,13 @@ public class ContentService : RepositoryService, IContentService if (blueprint.CultureInfos?.Count > 0) { cultures = blueprint.CultureInfos.Values.Select(x => x.Culture); - using (ICoreScope scope = ScopeProvider.CreateCoreScope()) + using ICoreScope scope = ScopeProvider.CreateCoreScope(); + if (blueprint.CultureInfos.TryGetValue(_languageRepository.GetDefaultIsoCode(), out ContentCultureInfos defaultCulture)) { - if (blueprint.CultureInfos.TryGetValue(_languageRepository.GetDefaultIsoCode(), out ContentCultureInfos defaultCulture)) - { - defaultCulture.Name = name; - } - - scope.Complete(); + defaultCulture.Name = name; } + + scope.Complete(); } DateTime now = DateTime.Now; @@ -3701,6 +3699,11 @@ public class ContentService : RepositoryService, IContentService return content; } + /// + [Obsolete("Use IContentBlueprintEditingService.GetScaffoldedAsync() instead. Scheduled for removal in V18.")] + public IContent CreateContentFromBlueprint(IContent blueprint, string name, int userId = Constants.Security.SuperUserId) + => CreateBlueprintFromContent(blueprint, name, userId); + public IEnumerable GetBlueprintsForContentTypes(params int[] contentTypeId) { using (ScopeProvider.CreateCoreScope(autoComplete: true)) diff --git a/src/Umbraco.Core/Services/IContentService.cs b/src/Umbraco.Core/Services/IContentService.cs index ea14e6771a..423f157874 100644 --- a/src/Umbraco.Core/Services/IContentService.cs +++ b/src/Umbraco.Core/Services/IContentService.cs @@ -55,8 +55,18 @@ public interface IContentService : IContentServiceBase void DeleteBlueprint(IContent content, int userId = Constants.Security.SuperUserId); /// - /// Creates a new content item from a blueprint. + /// Creates a blueprint from a content item. /// + // TODO: Remove the default implementation when CreateContentFromBlueprint is removed. + IContent CreateBlueprintFromContent(IContent blueprint, string name, int userId = Constants.Security.SuperUserId) + => throw new NotImplementedException(); + + /// + /// (Deprecated) Creates a new content item from a blueprint. + /// + /// If creating content from a blueprint, use + /// instead. If creating a blueprint from content use instead. + [Obsolete("Use IContentBlueprintEditingService.GetScaffoldedAsync() instead. Scheduled for removal in V18.")] IContent CreateContentFromBlueprint(IContent blueprint, string name, int userId = Constants.Security.SuperUserId); /// diff --git a/tests/Umbraco.Tests.Common/Builders/DataTypeBuilder.cs b/tests/Umbraco.Tests.Common/Builders/DataTypeBuilder.cs index 9c26114e3d..9226d42474 100644 --- a/tests/Umbraco.Tests.Common/Builders/DataTypeBuilder.cs +++ b/tests/Umbraco.Tests.Common/Builders/DataTypeBuilder.cs @@ -1,9 +1,12 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.IO; using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.PropertyEditors; using Umbraco.Cms.Infrastructure.Serialization; +using Umbraco.Cms.Tests.Common.Builders.Extensions; using Umbraco.Cms.Tests.Common.Builders.Interfaces; namespace Umbraco.Cms.Tests.Common.Builders; @@ -155,4 +158,100 @@ public class DataTypeBuilder return dataType; } + + public static DataType CreateSimpleElementDataType( + IIOHelper ioHelper, + string editorAlias, + Guid elementKey, + Guid? elementSettingKey) + { + Dictionary configuration = editorAlias switch + { + Constants.PropertyEditors.Aliases.BlockGrid => GetBlockGridBaseConfiguration(), + Constants.PropertyEditors.Aliases.RichText => GetRteBaseConfiguration(), + _ => [], + }; + + SetBlockConfiguration( + configuration, + elementKey, + elementSettingKey, + editorAlias == Constants.PropertyEditors.Aliases.BlockGrid ? true : null); + + + var dataTypeBuilder = new DataTypeBuilder() + .WithId(0) + .WithDatabaseType(ValueStorageType.Nvarchar) + .AddEditor() + .WithAlias(editorAlias); + + switch (editorAlias) + { + case Constants.PropertyEditors.Aliases.BlockGrid: + dataTypeBuilder.WithConfigurationEditor( + new BlockGridConfigurationEditor(ioHelper) { DefaultConfiguration = configuration }); + break; + case Constants.PropertyEditors.Aliases.BlockList: + dataTypeBuilder.WithConfigurationEditor( + new BlockListConfigurationEditor(ioHelper) { DefaultConfiguration = configuration }); + break; + case Constants.PropertyEditors.Aliases.RichText: + dataTypeBuilder.WithConfigurationEditor( + new RichTextConfigurationEditor(ioHelper) { DefaultConfiguration = configuration }); + break; + } + + return dataTypeBuilder.Done().Build(); + } + + private static void SetBlockConfiguration( + Dictionary dictionary, + Guid? elementKey, + Guid? elementSettingKey, + bool? allowAtRoot) + { + if (elementKey is null) + { + return; + } + + dictionary["blocks"] = new[] { BuildBlockConfiguration(elementKey.Value, elementSettingKey, allowAtRoot) }; + } + + private static Dictionary GetBlockGridBaseConfiguration() => new() { ["gridColumns"] = 12 }; + + private static Dictionary GetRteBaseConfiguration() + { + var dictionary = new Dictionary + { + ["maxImageSize"] = 500, + ["mode"] = "Classic", + ["toolbar"] = new[] + { + "styles", "bold", "italic", "alignleft", "aligncenter", "alignright", "bullist", "numlist", "outdent", + "indent", "sourcecode", "link", "umbmediapicker", "umbembeddialog" + }, + }; + return dictionary; + } + + private static Dictionary BuildBlockConfiguration( + Guid? elementKey, + Guid? elementSettingKey, + bool? allowAtRoot) + { + var dictionary = new Dictionary(); + if (allowAtRoot is not null) + { + dictionary.Add("allowAtRoot", allowAtRoot.Value); + } + + dictionary.Add("contentElementTypeKey", elementKey.ToString()); + if (elementSettingKey is not null) + { + dictionary.Add("settingsElementTypeKey", elementSettingKey.ToString()); + } + + return dictionary; + } } diff --git a/tests/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTestWithContent.cs b/tests/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTestWithContent.cs index 70957f4c83..42aba90eb2 100644 --- a/tests/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTestWithContent.cs +++ b/tests/Umbraco.Tests.Integration/Testing/UmbracoIntegrationTestWithContent.cs @@ -19,6 +19,8 @@ public abstract class UmbracoIntegrationTestWithContent : UmbracoIntegrationTest protected IContentTypeService ContentTypeService => GetRequiredService(); + protected IDataTypeService DataTypeService => GetRequiredService(); + protected IFileService FileService => GetRequiredService(); protected ContentService ContentService => (ContentService)GetRequiredService(); diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/ContentServiceTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/ContentServiceTests.cs index 242be71557..2c1ea1e278 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/ContentServiceTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/ContentServiceTests.cs @@ -130,7 +130,7 @@ internal sealed class ContentServiceTests : UmbracoIntegrationTestWithContent } [Test] - public void Create_Content_From_Blueprint() + public void Create_Blueprint_From_Content() { using (var scope = ScopeProvider.CreateScope(autoComplete: true)) { @@ -140,22 +140,21 @@ internal sealed class ContentServiceTests : UmbracoIntegrationTestWithContent var contentType = ContentTypeBuilder.CreateTextPageContentType(defaultTemplateId: template.Id); ContentTypeService.Save(contentType); - var blueprint = ContentBuilder.CreateTextpageContent(contentType, "hello", Constants.System.Root); - blueprint.SetValue("title", "blueprint 1"); - blueprint.SetValue("bodyText", "blueprint 2"); - blueprint.SetValue("keywords", "blueprint 3"); - blueprint.SetValue("description", "blueprint 4"); + var originalPage = ContentBuilder.CreateTextpageContent(contentType, "hello", Constants.System.Root); + originalPage.SetValue("title", "blueprint 1"); + originalPage.SetValue("bodyText", "blueprint 2"); + originalPage.SetValue("keywords", "blueprint 3"); + originalPage.SetValue("description", "blueprint 4"); + ContentService.Save(originalPage); - ContentService.SaveBlueprint(blueprint); + var fromContent = ContentService.CreateBlueprintFromContent(originalPage, "hello world"); + ContentService.SaveBlueprint(fromContent); - var fromBlueprint = ContentService.CreateContentFromBlueprint(blueprint, "hello world"); - ContentService.Save(fromBlueprint); - - Assert.IsTrue(fromBlueprint.HasIdentity); - Assert.AreEqual("blueprint 1", fromBlueprint.Properties["title"].GetValue()); - Assert.AreEqual("blueprint 2", fromBlueprint.Properties["bodyText"].GetValue()); - Assert.AreEqual("blueprint 3", fromBlueprint.Properties["keywords"].GetValue()); - Assert.AreEqual("blueprint 4", fromBlueprint.Properties["description"].GetValue()); + Assert.IsTrue(fromContent.HasIdentity); + Assert.AreEqual("blueprint 1", fromContent.Properties["title"]?.GetValue()); + Assert.AreEqual("blueprint 2", fromContent.Properties["bodyText"]?.GetValue()); + Assert.AreEqual("blueprint 3", fromContent.Properties["keywords"]?.GetValue()); + Assert.AreEqual("blueprint 4", fromContent.Properties["description"]?.GetValue()); } } diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/ElementSwitchValidatorTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/ElementSwitchValidatorTests.cs index 916ae02cea..794dc81efc 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/ElementSwitchValidatorTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/ElementSwitchValidatorTests.cs @@ -286,104 +286,12 @@ internal sealed class ElementSwitchValidatorTests : UmbracoIntegrationTest Guid elementKey, Guid? elementSettingKey) { - Dictionary configuration; - switch (editorAlias) - { - case Constants.PropertyEditors.Aliases.BlockGrid: - configuration = GetBlockGridBaseConfiguration(); - break; - case Constants.PropertyEditors.Aliases.RichText: - configuration = GetRteBaseConfiguration(); - break; - default: - configuration = new Dictionary(); - break; - } - - SetBlockConfiguration( - configuration, + var dataType = DataTypeBuilder.CreateSimpleElementDataType( + IOHelper, + editorAlias, elementKey, - elementSettingKey, - editorAlias == Constants.PropertyEditors.Aliases.BlockGrid ? true : null); - - - var dataTypeBuilder = new DataTypeBuilder() - .WithId(0) - .WithDatabaseType(ValueStorageType.Nvarchar) - .AddEditor() - .WithAlias(editorAlias); - - switch (editorAlias) - { - case Constants.PropertyEditors.Aliases.BlockGrid: - dataTypeBuilder.WithConfigurationEditor( - new BlockGridConfigurationEditor(IOHelper) { DefaultConfiguration = configuration }); - break; - case Constants.PropertyEditors.Aliases.BlockList: - dataTypeBuilder.WithConfigurationEditor( - new BlockListConfigurationEditor(IOHelper) { DefaultConfiguration = configuration }); - break; - case Constants.PropertyEditors.Aliases.RichText: - dataTypeBuilder.WithConfigurationEditor( - new RichTextConfigurationEditor(IOHelper) { DefaultConfiguration = configuration }); - break; - } - - var dataType = dataTypeBuilder.Done() - .Build(); + elementSettingKey); await DataTypeService.CreateAsync(dataType, Constants.Security.SuperUserKey); } - - private void SetBlockConfiguration( - Dictionary dictionary, - Guid? elementKey, - Guid? elementSettingKey, - bool? allowAtRoot) - { - if (elementKey is null) - { - return; - } - - dictionary["blocks"] = new[] { BuildBlockConfiguration(elementKey.Value, elementSettingKey, allowAtRoot) }; - } - - private Dictionary GetBlockGridBaseConfiguration() - => new Dictionary { ["gridColumns"] = 12 }; - - private Dictionary GetRteBaseConfiguration() - { - var dictionary = new Dictionary - { - ["maxImageSize"] = 500, - ["mode"] = "Classic", - ["toolbar"] = new[] - { - "styles", "bold", "italic", "alignleft", "aligncenter", "alignright", "bullist", "numlist", - "outdent", "indent", "sourcecode", "link", "umbmediapicker", "umbembeddialog" - }, - }; - return dictionary; - } - - private Dictionary BuildBlockConfiguration( - Guid? elementKey, - Guid? elementSettingKey, - bool? allowAtRoot) - { - var dictionary = new Dictionary(); - if (allowAtRoot is not null) - { - dictionary.Add("allowAtRoot", allowAtRoot.Value); - } - - dictionary.Add("contentElementTypeKey", elementKey.ToString()); - if (elementSettingKey is not null) - { - dictionary.Add("settingsElementTypeKey", elementSettingKey.ToString()); - } - - return dictionary; - } } diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/TelemetryProviderTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/TelemetryProviderTests.cs index 210d66f28a..c5d6548677 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/TelemetryProviderTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/TelemetryProviderTests.cs @@ -54,6 +54,8 @@ internal sealed class TelemetryProviderTests : UmbracoIntegrationTest private IMediaTypeService MediaTypeService => GetRequiredService(); + private IContentBlueprintEditingService ContentBlueprintEditingService => GetRequiredService(); + private readonly LanguageBuilder _languageBuilder = new(); private readonly UserBuilder _userBuilder = new(); @@ -99,7 +101,7 @@ internal sealed class TelemetryProviderTests : UmbracoIntegrationTest } [Test] - public void SectionService_Can_Get_Allowed_Sections_For_User() + public async Task SectionService_Can_Get_Allowed_Sections_For_User() { // Arrange var template = TemplateBuilder.CreateTextPageTemplate(); @@ -116,7 +118,9 @@ internal sealed class TelemetryProviderTests : UmbracoIntegrationTest ContentService.SaveBlueprint(blueprint); - var fromBlueprint = ContentService.CreateContentFromBlueprint(blueprint, "My test content"); + var fromBlueprint = await ContentBlueprintEditingService.GetScaffoldedAsync(blueprint.Key); + Assert.IsNotNull(fromBlueprint); + fromBlueprint.Name = "My test content"; ContentService.Save(fromBlueprint); IEnumerable result = null; diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentBlueprintEditingServiceTests.GetScaffold.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentBlueprintEditingServiceTests.GetScaffold.cs index 56ac93ba60..32b92a20b2 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentBlueprintEditingServiceTests.GetScaffold.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentBlueprintEditingServiceTests.GetScaffold.cs @@ -1,14 +1,26 @@ using NUnit.Framework; +using Umbraco.Cms.Core; using Umbraco.Cms.Core.Events; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.Blocks; +using Umbraco.Cms.Core.Models.ContentEditing; using Umbraco.Cms.Core.Notifications; +using Umbraco.Cms.Core.PropertyEditors; +using Umbraco.Cms.Tests.Common.Builders; +using Umbraco.Cms.Tests.Common.Builders.Extensions; using Umbraco.Cms.Tests.Integration.Attributes; +using IContent = Umbraco.Cms.Core.Models.IContent; namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services; public partial class ContentBlueprintEditingServiceTests { public static void AddScaffoldedNotificationHandler(IUmbracoBuilder builder) - => builder.AddNotificationHandler(); + => builder + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler(); [TestCase(true)] [TestCase(false)] @@ -28,7 +40,15 @@ public partial class ContentBlueprintEditingServiceTests }; var result = await ContentBlueprintEditingService.GetScaffoldedAsync(blueprint.Key); Assert.IsNotNull(result); - Assert.AreEqual(blueprint.Key, result.Key); + Assert.AreNotEqual(blueprint.Key, result.Key); + Assert.AreEqual( + blueprint.ContentType.Key, + result.ContentType.Key, + "The content type of the scaffolded content should match the original blueprint content type."); + Assert.AreEqual( + blueprint.Properties.Select(p => (p.Alias, p.PropertyType.Key)), + result.Properties.Select(p => (p.Alias, p.PropertyType.Key)), + "The properties of the scaffolded content should match the original blueprint properties."); var propertyValues = result.Properties.SelectMany(property => property.Values).ToArray(); Assert.IsNotEmpty(propertyValues); @@ -51,10 +71,209 @@ public partial class ContentBlueprintEditingServiceTests Assert.IsNull(result); } + [TestCase(false, Constants.PropertyEditors.Aliases.BlockList)] + [TestCase(false, Constants.PropertyEditors.Aliases.BlockGrid)] + [TestCase(false, Constants.PropertyEditors.Aliases.RichText)] + [TestCase(true, Constants.PropertyEditors.Aliases.BlockList)] + [TestCase(true, Constants.PropertyEditors.Aliases.BlockGrid)] + [TestCase(true, Constants.PropertyEditors.Aliases.RichText)] + [ConfigureBuilder(ActionName = nameof(AddScaffoldedNotificationHandler))] + public async Task Get_Scaffold_With_Blocks_Generates_New_Block_Ids(bool variant, string editorAlias) + { + var blueprint = await CreateBlueprintWithBlocksEditor(variant, editorAlias); + var result = await ContentBlueprintEditingService.GetScaffoldedAsync(blueprint.Content.Key); + Assert.IsNotNull(result); + Assert.AreNotEqual(blueprint.Content.Key, result.Key); + + List newKeys = []; + var newInvariantBlocklist = GetBlockValue("invariantBlocks"); + newKeys.AddRange( + newInvariantBlocklist.Layout + .SelectMany(x => x.Value) + .SelectMany(v => new List { v.ContentKey, v.SettingsKey!.Value })); + + if (variant) + { + foreach (var culture in result.AvailableCultures) + { + var newVariantBlocklist = GetBlockValue("blocks", culture); + newKeys.AddRange( + newVariantBlocklist.Layout + .SelectMany(x => x.Value) + .SelectMany(v => new List { v.ContentKey, v.SettingsKey!.Value })); + } + } + + foreach (var newKey in newKeys) + { + Assert.IsFalse(blueprint.BlockKeys.Contains(newKey), "The blocks in a content item generated from a template should have new keys."); + } + + return; + + BlockValue GetBlockValue(string propertyAlias, string? culture = null) + { + return editorAlias switch + { + Constants.PropertyEditors.Aliases.BlockList => JsonSerializer.Deserialize(result.GetValue(propertyAlias, culture)), + Constants.PropertyEditors.Aliases.BlockGrid => JsonSerializer.Deserialize(result.GetValue(propertyAlias, culture)), + Constants.PropertyEditors.Aliases.RichText => JsonSerializer.Deserialize(result.GetValue(propertyAlias, culture)).Blocks!, + _ => throw new NotSupportedException($"Editor alias '{editorAlias}' is not supported for block blueprints."), + }; + } + } + public class ContentScaffoldedNotificationHandler : INotificationHandler { public static Action? ContentScaffolded { get; set; } public void Handle(ContentScaffoldedNotification notification) => ContentScaffolded?.Invoke(notification); } + + private async Task<(IContent Content, List BlockKeys)> CreateBlueprintWithBlocksEditor(bool variant, string editorAlias) + { + var contentType = variant ? await CreateVariantContentType() : CreateInvariantContentType(); + + // Create element type + var elementContentType = new ContentTypeBuilder() + .WithAlias("elementType") + .WithName("Element") + .WithIsElement(true) + .Build(); + await ContentTypeService.CreateAsync(elementContentType, Constants.Security.SuperUserKey); + + // Create settings element type + var settingsContentType = new ContentTypeBuilder() + .WithAlias("settingsType") + .WithName("Settings") + .WithIsElement(true) + .Build(); + await ContentTypeService.CreateAsync(settingsContentType, Constants.Security.SuperUserKey); + + // Create blocks datatype using the created elements + var dataType = DataTypeBuilder.CreateSimpleElementDataType(IOHelper, editorAlias, elementContentType.Key, settingsContentType.Key); + var dataTypeAttempt = await DataTypeService.CreateAsync(dataType, Constants.Security.SuperUserKey); + Assert.True(dataTypeAttempt.Success, $"Failed to create data type: {dataTypeAttempt.Exception?.Message}"); + + // Create new blocks property types + var invariantPropertyType = new PropertyTypeBuilder(new ContentTypeBuilder()) + .WithPropertyEditorAlias(editorAlias) + .WithValueStorageType(ValueStorageType.Ntext) + .WithAlias("invariantBlocks") + .WithName("Invariant Blocks") + .WithDataTypeId(dataType.Id) + .WithVariations(ContentVariation.Nothing) + .Build(); + contentType.AddPropertyType(invariantPropertyType); + + if (contentType.VariesByCulture()) + { + var propertyType = new PropertyTypeBuilder(new ContentTypeBuilder()) + .WithPropertyEditorAlias(editorAlias) + .WithValueStorageType(ValueStorageType.Ntext) + .WithAlias("blocks") + .WithName("Blocks") + .WithDataTypeId(dataType.Id) + .WithVariations(contentType.Variations) + .Build(); + contentType.AddPropertyType(propertyType); + } + + // Update the content type with the new blocks property type + await ContentTypeService.UpdateAsync(contentType, Constants.Security.SuperUserKey); + + string?[] cultures = contentType.VariesByCulture() + ? [null, "en-US", "da-DK"] + : [null]; + + var createModel = new ContentBlueprintCreateModel + { + ContentTypeKey = contentType.Key, + ParentKey = Constants.System.RootKey, + Variants = cultures.Where(c => variant ? c != null : c == null).Select(c => new VariantModel { Culture = c, Name = $"Initial Blueprint {c}" }), + }; + + List allBlockKeys = []; + foreach (var culture in cultures) + { + var (blockValue, blockKeys) = CreateBlockValue(editorAlias, elementContentType, settingsContentType); + createModel.Properties = createModel.Properties.Append( + new PropertyValueModel + { + Alias = culture == null ? "invariantBlocks" : "blocks", + Value = JsonSerializer.Serialize(blockValue), + Culture = culture, + }); + allBlockKeys.AddRange(blockKeys); + } + + var result = await ContentBlueprintEditingService.CreateAsync(createModel, Constants.Security.SuperUserKey); + Assert.IsTrue(result.Success); + return (result.Result.Content, allBlockKeys); + } + + private static (object BlockValue, IEnumerable BlockKeys) CreateBlockValue( + string editorAlias, + IContentType elementContentType, + IContentType settingsContentType) + { + switch (editorAlias) + { + case Constants.PropertyEditors.Aliases.BlockList: + return CreateBlockValueOfType(editorAlias, elementContentType, settingsContentType); + case Constants.PropertyEditors.Aliases.BlockGrid: + return CreateBlockValueOfType(editorAlias, elementContentType, settingsContentType); + case Constants.PropertyEditors.Aliases.RichText: + var res = CreateBlockValueOfType(editorAlias, elementContentType, settingsContentType); + return (new RichTextEditorValue + { + Markup = string.Join(string.Empty, res.BlockKeys.Chunk(2).Select(c => $"")), + Blocks = res.BlockValue, + }, res.BlockKeys); + default: + throw new NotSupportedException($"Editor alias '{editorAlias}' is not supported for block blueprints."); + } + } + + private static (T BlockValue, IEnumerable BlockKeys) CreateBlockValueOfType( + string editorAlias, + IContentType elementContentType, + IContentType settingsContentType) + where T : BlockValue, new() + where TLayout : IBlockLayoutItem, new() + { + // Generate two pairs of Guids as a list of tuples + const int numberOfBlocks = 2; + var blockKeys = Enumerable.Range(0, numberOfBlocks) + .Select(_ => Enumerable.Range(0, 2).Select(_ => Guid.NewGuid()).ToList()) + .ToList(); + return (new T + { + Layout = new Dictionary> + { + [editorAlias] = blockKeys.Select(blockKeyGroup => + new TLayout + { + ContentKey = blockKeyGroup[0], + SettingsKey = blockKeyGroup[1], + }).OfType(), + }, + ContentData = blockKeys.Select(blockKeyGroup => new BlockItemData + { + Key = blockKeyGroup[0], + ContentTypeAlias = elementContentType.Alias, + ContentTypeKey = elementContentType.Key, + Values = [], + }) + .ToList(), + SettingsData = blockKeys.Select(blockKeyGroup => new BlockItemData + { + Key = blockKeyGroup[1], + ContentTypeAlias = settingsContentType.Alias, + ContentTypeKey = settingsContentType.Key, + Values = [], + }) + .ToList(), + }, blockKeys.SelectMany(l => l)); + } } diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentBlueprintEditingServiceTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentBlueprintEditingServiceTests.cs index c15e619409..57b59da68a 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentBlueprintEditingServiceTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentBlueprintEditingServiceTests.cs @@ -3,6 +3,7 @@ using Umbraco.Cms.Core; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.ContentEditing; using Umbraco.Cms.Core.Models.Entities; +using Umbraco.Cms.Core.Serialization; using Umbraco.Cms.Core.Services; namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services; @@ -17,6 +18,8 @@ public partial class ContentBlueprintEditingServiceTests : ContentEditingService private IEntityService EntityService => GetRequiredService(); + private IJsonSerializer JsonSerializer => GetRequiredService(); + private async Task CreateInvariantContentBlueprint() { var contentType = CreateInvariantContentType(); @@ -75,8 +78,8 @@ public partial class ContentBlueprintEditingServiceTests : ContentEditingService Properties = [ new PropertyValueModel { Alias = "title", Value = "The title value" }, - new PropertyValueModel { Alias = "author", Value = "The author value" } - ] + new PropertyValueModel { Alias = "author", Value = "The author value" }, + ], }; return createModel; } @@ -90,11 +93,12 @@ public partial class ContentBlueprintEditingServiceTests : ContentEditingService [ new PropertyValueModel { Alias = "title", Value = "The title value updated" }, new PropertyValueModel { Alias = "author", Value = "The author value updated" } - ] + ], }; return createModel; } private IEntitySlim[] GetBlueprintChildren(Guid? containerKey) - => EntityService.GetPagedChildren(containerKey, new[] { UmbracoObjectTypes.DocumentBlueprintContainer }, UmbracoObjectTypes.DocumentBlueprint, 0, 100, out _).ToArray(); + => EntityService.GetPagedChildren(containerKey, [UmbracoObjectTypes.DocumentBlueprintContainer], UmbracoObjectTypes.DocumentBlueprint, 0, 100, out _).ToArray(); } +