From 2da9d9b0b8480d92de433af6bb80f8f5bf642f41 Mon Sep 17 00:00:00 2001 From: Laura Neto <12862535+lauraneto@users.noreply.github.com> Date: Wed, 11 Jun 2025 10:00:01 +0200 Subject: [PATCH 1/9] Rename `IContentService.CreateContentFromBlueprint` to `CreateBlueprintFromContent` In reality, this method is used by the core to create a blueprint from content, and not the other way around, which doesn't need new ids. This was causing confusion, so the old name has been marked as deprecated in favor of the new name. If developers want to create content from blueprints they should use `IContentBlueprintEditingService.GetScaffoldedAsync()` instead, which is what is used by the management api. --- .../ContentBlueprintEditingService.cs | 2 +- src/Umbraco.Core/Services/ContentService.cs | 27 ++++++++++--------- src/Umbraco.Core/Services/IContentService.cs | 6 +++++ 3 files changed, 22 insertions(+), 13 deletions(-) diff --git a/src/Umbraco.Core/Services/ContentBlueprintEditingService.cs b/src/Umbraco.Core/Services/ContentBlueprintEditingService.cs index 7af5171bc96b..50380e194752 100644 --- a/src/Umbraco.Core/Services/ContentBlueprintEditingService.cs +++ b/src/Umbraco.Core/Services/ContentBlueprintEditingService.cs @@ -112,7 +112,7 @@ public async Task> C // 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 f246b9c3f0aa..34ac8db8ffec 100644 --- a/src/Umbraco.Core/Services/ContentService.cs +++ b/src/Umbraco.Core/Services/ContentService.cs @@ -3654,12 +3654,12 @@ public void DeleteBlueprint(IContent content, int userId = Constants.Security.Su 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 IContent CreateContentFromBlueprint(IContent blueprint, string name, int 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 IContent CreateContentFromBlueprint(IContent blueprint, string name, int 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 ea14e6771a88..6dfd12ed83b4 100644 --- a/src/Umbraco.Core/Services/IContentService.cs +++ b/src/Umbraco.Core/Services/IContentService.cs @@ -54,9 +54,15 @@ public interface IContentService : IContentServiceBase /// void DeleteBlueprint(IContent content, int userId = Constants.Security.SuperUserId); + /// + /// Creates a blueprint from a content item. + /// + IContent CreateBlueprintFromContent(IContent blueprint, string name, int userId = Constants.Security.SuperUserId); + /// /// Creates a new content item from a blueprint. /// + [Obsolete("Use IContentBlueprintEditingService.GetScaffoldedAsync() instead. Scheduled for removal in V18.")] IContent CreateContentFromBlueprint(IContent blueprint, string name, int userId = Constants.Security.SuperUserId); /// From 94659db533e01947ca11786e27353635289ceaa4 Mon Sep 17 00:00:00 2001 From: Laura Neto <12862535+lauraneto@users.noreply.github.com> Date: Wed, 11 Jun 2025 10:15:35 +0200 Subject: [PATCH 2/9] Added integration tests to verify that new block ids are generated when creating content from a blueprint --- .../Builders/DataTypeBuilder.cs | 101 +++++++++++- .../UmbracoIntegrationTestWithContent.cs | 2 + .../Services/ElementSwitchValidatorTests.cs | 100 +----------- ...lueprintEditingServiceTests.GetScaffold.cs | 60 ++++++- .../ContentBlueprintEditingServiceTests.cs | 153 +++++++++++++++++- 5 files changed, 314 insertions(+), 102 deletions(-) diff --git a/tests/Umbraco.Tests.Common/Builders/DataTypeBuilder.cs b/tests/Umbraco.Tests.Common/Builders/DataTypeBuilder.cs index 9c26114e3d0d..9226d42474ef 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 override DataType Build() 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 70957f4c8398..42aba90eb2f3 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/ElementSwitchValidatorTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/ElementSwitchValidatorTests.cs index 916ae02ceae4..794dc81efc3c 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 @@ private async Task SetupDataType( 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.Infrastructure/Services/ContentBlueprintEditingServiceTests.GetScaffold.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentBlueprintEditingServiceTests.GetScaffold.cs index 56ac93ba600a..e049f6bd8c79 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentBlueprintEditingServiceTests.GetScaffold.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentBlueprintEditingServiceTests.GetScaffold.cs @@ -1,6 +1,9 @@ using NUnit.Framework; +using Umbraco.Cms.Core; using Umbraco.Cms.Core.Events; +using Umbraco.Cms.Core.Models.Blocks; using Umbraco.Cms.Core.Notifications; +using Umbraco.Cms.Core.PropertyEditors; using Umbraco.Cms.Tests.Integration.Attributes; namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services; @@ -8,7 +11,10 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services; public partial class ContentBlueprintEditingServiceTests { public static void AddScaffoldedNotificationHandler(IUmbracoBuilder builder) - => builder.AddNotificationHandler(); + => builder + .AddNotificationHandler() + .AddNotificationHandler() + .AddNotificationHandler(); [TestCase(true)] [TestCase(false)] @@ -51,6 +57,58 @@ public async Task Cannot_Get_Non_Existing_Scaffold() 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.IsTrue(!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; } diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentBlueprintEditingServiceTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentBlueprintEditingServiceTests.cs index c15e61940933..1ca904f709fa 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentBlueprintEditingServiceTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentBlueprintEditingServiceTests.cs @@ -1,9 +1,13 @@ using NUnit.Framework; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.Blocks; using Umbraco.Cms.Core.Models.ContentEditing; using Umbraco.Cms.Core.Models.Entities; +using Umbraco.Cms.Core.Serialization; using Umbraco.Cms.Core.Services; +using Umbraco.Cms.Tests.Common.Builders; +using Umbraco.Cms.Tests.Common.Builders.Extensions; namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services; @@ -17,6 +21,8 @@ public partial class ContentBlueprintEditingServiceTests : ContentEditingService private IEntityService EntityService => GetRequiredService(); + private IJsonSerializer JsonSerializer => GetRequiredService(); + private async Task CreateInvariantContentBlueprint() { var contentType = CreateInvariantContentType(); @@ -38,6 +44,144 @@ private async Task CreateInvariantContentBlueprint() return result.Result.Content!; } + + 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(); + 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); + + (object BlockValue, IEnumerable BlockKeys) CreateBlockValue() + { + switch (editorAlias) + { + case Constants.PropertyEditors.Aliases.BlockList: + return CreateBlockValueOfType(); + case Constants.PropertyEditors.Aliases.BlockGrid: + return CreateBlockValueOfType(); + case Constants.PropertyEditors.Aliases.RichText: + var res = CreateBlockValueOfType(); + return (new RichTextEditorValue { Markup = string.Empty, Blocks = res.BlockValue }, res.BlockKeys); + default: + throw new NotSupportedException($"Editor alias '{editorAlias}' is not supported for block blueprints."); + } + } + + (T BlockValue, IEnumerable BlockKeys) CreateBlockValueOfType() + 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)); + } + } + private async Task CreateVariantContentBlueprint() { var contentType = await CreateVariantContentType(); @@ -75,8 +219,8 @@ private ContentBlueprintCreateModel SimpleContentBlueprintCreateModel(Guid bluep 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 +234,12 @@ private ContentBlueprintUpdateModel SimpleContentBlueprintUpdateModel() [ 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(); } + From f7b3de4857471044fb8ce9f1933cad32b7446be5 Mon Sep 17 00:00:00 2001 From: Laura Neto <12862535+lauraneto@users.noreply.github.com> Date: Wed, 11 Jun 2025 10:16:40 +0200 Subject: [PATCH 3/9] Return copy of the blueprint in `ContentBlueprintEditingService.GetScaffoldedAsync` instead of the blueprint itself --- .../Services/ContentBlueprintEditingService.cs | 6 ++++-- .../ContentBlueprintEditingServiceTests.GetScaffold.cs | 10 +++++++++- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/src/Umbraco.Core/Services/ContentBlueprintEditingService.cs b/src/Umbraco.Core/Services/ContentBlueprintEditingService.cs index 50380e194752..5e8357e453de 100644 --- a/src/Umbraco.Core/Services/ContentBlueprintEditingService.cs +++ b/src/Umbraco.Core/Services/ContentBlueprintEditingService.cs @@ -45,11 +45,13 @@ public 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) 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 e049f6bd8c79..6b7031d99efc 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentBlueprintEditingServiceTests.GetScaffold.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentBlueprintEditingServiceTests.GetScaffold.cs @@ -34,7 +34,15 @@ public async Task Can_Get_Scaffold(bool variant) }; 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); From 59fe3208235a00c235bda48318672b5cf88eb183 Mon Sep 17 00:00:00 2001 From: Laura Neto <12862535+lauraneto@users.noreply.github.com> Date: Wed, 11 Jun 2025 11:13:13 +0200 Subject: [PATCH 4/9] Update CreateContentFromBlueprint xml docs to mention both replacement methods --- src/Umbraco.Core/Services/IContentService.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Umbraco.Core/Services/IContentService.cs b/src/Umbraco.Core/Services/IContentService.cs index 6dfd12ed83b4..25a89ec67278 100644 --- a/src/Umbraco.Core/Services/IContentService.cs +++ b/src/Umbraco.Core/Services/IContentService.cs @@ -60,8 +60,10 @@ public interface IContentService : IContentServiceBase IContent CreateBlueprintFromContent(IContent blueprint, string name, int userId = Constants.Security.SuperUserId); /// - /// Creates a new content item from a blueprint. + /// (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); From 5d985ed0f878a47bd59885b86f9550b060cfa4bc Mon Sep 17 00:00:00 2001 From: Laura Neto <12862535+lauraneto@users.noreply.github.com> Date: Wed, 11 Jun 2025 13:04:04 +0200 Subject: [PATCH 5/9] Fix tests for rich text blocks --- .../Services/ContentBlueprintEditingServiceTests.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentBlueprintEditingServiceTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentBlueprintEditingServiceTests.cs index 1ca904f709fa..df312ab81d61 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentBlueprintEditingServiceTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentBlueprintEditingServiceTests.cs @@ -136,7 +136,11 @@ private async Task CreateInvariantContentBlueprint() return CreateBlockValueOfType(); case Constants.PropertyEditors.Aliases.RichText: var res = CreateBlockValueOfType(); - return (new RichTextEditorValue { Markup = string.Empty, Blocks = res.BlockValue }, res.BlockKeys); + 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."); } From c6076956b7652305cc8cf31f82167aba49debdd3 Mon Sep 17 00:00:00 2001 From: Laura Neto <12862535+lauraneto@users.noreply.github.com> Date: Wed, 11 Jun 2025 13:20:48 +0200 Subject: [PATCH 6/9] Small re-organization --- ...lueprintEditingServiceTests.GetScaffold.cs | 155 +++++++++++++++++- .../ContentBlueprintEditingServiceTests.cs | 145 ---------------- 2 files changed, 154 insertions(+), 146 deletions(-) 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 6b7031d99efc..37d1eb324cd9 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentBlueprintEditingServiceTests.GetScaffold.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentBlueprintEditingServiceTests.GetScaffold.cs @@ -1,10 +1,15 @@ 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; @@ -14,7 +19,8 @@ public static void AddScaffoldedNotificationHandler(IUmbracoBuilder builder) => builder .AddNotificationHandler() .AddNotificationHandler() - .AddNotificationHandler(); + .AddNotificationHandler() + .AddNotificationHandler(); [TestCase(true)] [TestCase(false)] @@ -123,4 +129,151 @@ public class ContentScaffoldedNotificationHandler : INotificationHandler 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 df312ab81d61..57b59da68a47 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentBlueprintEditingServiceTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentBlueprintEditingServiceTests.cs @@ -1,13 +1,10 @@ using NUnit.Framework; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Models; -using Umbraco.Cms.Core.Models.Blocks; using Umbraco.Cms.Core.Models.ContentEditing; using Umbraco.Cms.Core.Models.Entities; using Umbraco.Cms.Core.Serialization; using Umbraco.Cms.Core.Services; -using Umbraco.Cms.Tests.Common.Builders; -using Umbraco.Cms.Tests.Common.Builders.Extensions; namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services; @@ -44,148 +41,6 @@ private async Task CreateInvariantContentBlueprint() return result.Result.Content!; } - - 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(); - 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); - - (object BlockValue, IEnumerable BlockKeys) CreateBlockValue() - { - switch (editorAlias) - { - case Constants.PropertyEditors.Aliases.BlockList: - return CreateBlockValueOfType(); - case Constants.PropertyEditors.Aliases.BlockGrid: - return CreateBlockValueOfType(); - case Constants.PropertyEditors.Aliases.RichText: - var res = CreateBlockValueOfType(); - 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."); - } - } - - (T BlockValue, IEnumerable BlockKeys) CreateBlockValueOfType() - 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)); - } - } - private async Task CreateVariantContentBlueprint() { var contentType = await CreateVariantContentType(); From 136fe690c943baad6bcbb672589ef84ecaf61c77 Mon Sep 17 00:00:00 2001 From: Laura Neto <12862535+lauraneto@users.noreply.github.com> Date: Wed, 11 Jun 2025 14:50:57 +0200 Subject: [PATCH 7/9] Adjusted tests that were still referencing `ContentService.CreateContentFromBlueprint` --- .../Services/ContentServiceTests.cs | 33 +++++++++---------- .../Services/TelemetryProviderTests.cs | 8 +++-- 2 files changed, 22 insertions(+), 19 deletions(-) diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/ContentServiceTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/ContentServiceTests.cs index 242be715570b..2c1ea1e2786a 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 @@ public void Delete_Blueprint() } [Test] - public void Create_Content_From_Blueprint() + public void Create_Blueprint_From_Content() { using (var scope = ScopeProvider.CreateScope(autoComplete: true)) { @@ -140,22 +140,21 @@ public void Create_Content_From_Blueprint() 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"); - - ContentService.SaveBlueprint(blueprint); - - 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()); + 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); + + var fromContent = ContentService.CreateBlueprintFromContent(originalPage, "hello world"); + ContentService.SaveBlueprint(fromContent); + + 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/TelemetryProviderTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Core/Services/TelemetryProviderTests.cs index 210d66f28a86..c5d6548677cd 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 @@ await DomainService.UpdateDomainsAsync( } [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 @@ public void SectionService_Can_Get_Allowed_Sections_For_User() 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; From 00f2ba8828496547e50f7682b55f70ff458b83ad Mon Sep 17 00:00:00 2001 From: Laura Neto <12862535+lauraneto@users.noreply.github.com> Date: Thu, 12 Jun 2025 15:06:40 +0200 Subject: [PATCH 8/9] Add default implementation to new CreateBlueprintFromContent method --- src/Umbraco.Core/Services/IContentService.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Umbraco.Core/Services/IContentService.cs b/src/Umbraco.Core/Services/IContentService.cs index 25a89ec67278..423f15787499 100644 --- a/src/Umbraco.Core/Services/IContentService.cs +++ b/src/Umbraco.Core/Services/IContentService.cs @@ -57,7 +57,9 @@ public interface IContentService : IContentServiceBase /// /// Creates a blueprint from a content item. /// - IContent CreateBlueprintFromContent(IContent blueprint, string name, int userId = Constants.Security.SuperUserId); + // 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. From 4c0feedbd30cc7d50f35dbb52df842252a83cf80 Mon Sep 17 00:00:00 2001 From: Laura Neto <12862535+lauraneto@users.noreply.github.com> Date: Mon, 23 Jun 2025 10:09:04 +0200 Subject: [PATCH 9/9] Update tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentBlueprintEditingServiceTests.GetScaffold.cs Co-authored-by: Andy Butland --- .../Services/ContentBlueprintEditingServiceTests.GetScaffold.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 37d1eb324cd9..32b92a20b26d 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentBlueprintEditingServiceTests.GetScaffold.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentBlueprintEditingServiceTests.GetScaffold.cs @@ -106,7 +106,7 @@ public async Task Get_Scaffold_With_Blocks_Generates_New_Block_Ids(bool variant, foreach (var newKey in newKeys) { - Assert.IsTrue(!blueprint.BlockKeys.Contains(newKey), "The blocks in a content item generated from a template should have new keys."); + Assert.IsFalse(blueprint.BlockKeys.Contains(newKey), "The blocks in a content item generated from a template should have new keys."); } return;