Simplify creating content from a blueprint programmatically (#19528)

* 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.

* Added integration tests to verify that new block ids are generated when creating content from a blueprint

* Return copy of the blueprint in `ContentBlueprintEditingService.GetScaffoldedAsync` instead of the blueprint itself

* Update CreateContentFromBlueprint xml docs to mention both replacement methods

* Fix tests for rich text blocks

* Small re-organization

* Adjusted tests that were still referencing `ContentService.CreateContentFromBlueprint`

* Add default implementation to new CreateBlueprintFromContent method

* Update tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/ContentBlueprintEditingServiceTests.GetScaffold.cs

Co-authored-by: Andy Butland <abutland73@gmail.com>

---------

Co-authored-by: Andy Butland <abutland73@gmail.com>
This commit is contained in:
Laura Neto
2025-06-24 13:43:34 +02:00
committed by GitHub
parent b41eecf58c
commit 55506bac3a
10 changed files with 386 additions and 136 deletions

View File

@@ -45,11 +45,13 @@ internal sealed class ContentBlueprintEditingService
return Task.FromResult<IContent?>(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<IContent?>(blueprint);
return Task.FromResult<IContent?>(scaffold);
}
public async Task<Attempt<PagedModel<IContent>?, 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)
{

View File

@@ -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;
}
/// <inheritdoc />
[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<IContent> GetBlueprintsForContentTypes(params int[] contentTypeId)
{
using (ScopeProvider.CreateCoreScope(autoComplete: true))

View File

@@ -55,8 +55,18 @@ public interface IContentService : IContentServiceBase<IContent>
void DeleteBlueprint(IContent content, int userId = Constants.Security.SuperUserId);
/// <summary>
/// Creates a new content item from a blueprint.
/// Creates a blueprint from a content item.
/// </summary>
// TODO: Remove the default implementation when CreateContentFromBlueprint is removed.
IContent CreateBlueprintFromContent(IContent blueprint, string name, int userId = Constants.Security.SuperUserId)
=> throw new NotImplementedException();
/// <summary>
/// (Deprecated) Creates a new content item from a blueprint.
/// </summary>
/// <remarks>If creating content from a blueprint, use <see cref="IContentBlueprintEditingService.GetScaffoldedAsync"/>
/// instead. If creating a blueprint from content use <see cref="CreateBlueprintFromContent"/> instead.</remarks>
[Obsolete("Use IContentBlueprintEditingService.GetScaffoldedAsync() instead. Scheduled for removal in V18.")]
IContent CreateContentFromBlueprint(IContent blueprint, string name, int userId = Constants.Security.SuperUserId);
/// <summary>

View File

@@ -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<string, object> 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<string, object> dictionary,
Guid? elementKey,
Guid? elementSettingKey,
bool? allowAtRoot)
{
if (elementKey is null)
{
return;
}
dictionary["blocks"] = new[] { BuildBlockConfiguration(elementKey.Value, elementSettingKey, allowAtRoot) };
}
private static Dictionary<string, object> GetBlockGridBaseConfiguration() => new() { ["gridColumns"] = 12 };
private static Dictionary<string, object> GetRteBaseConfiguration()
{
var dictionary = new Dictionary<string, object>
{
["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<string, object> BuildBlockConfiguration(
Guid? elementKey,
Guid? elementSettingKey,
bool? allowAtRoot)
{
var dictionary = new Dictionary<string, object>();
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;
}
}

View File

@@ -19,6 +19,8 @@ public abstract class UmbracoIntegrationTestWithContent : UmbracoIntegrationTest
protected IContentTypeService ContentTypeService => GetRequiredService<IContentTypeService>();
protected IDataTypeService DataTypeService => GetRequiredService<IDataTypeService>();
protected IFileService FileService => GetRequiredService<IFileService>();
protected ContentService ContentService => (ContentService)GetRequiredService<IContentService>();

View File

@@ -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());
}
}

View File

@@ -286,104 +286,12 @@ internal sealed class ElementSwitchValidatorTests : UmbracoIntegrationTest
Guid elementKey,
Guid? elementSettingKey)
{
Dictionary<string, object> configuration;
switch (editorAlias)
{
case Constants.PropertyEditors.Aliases.BlockGrid:
configuration = GetBlockGridBaseConfiguration();
break;
case Constants.PropertyEditors.Aliases.RichText:
configuration = GetRteBaseConfiguration();
break;
default:
configuration = new Dictionary<string, object>();
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<string, object> dictionary,
Guid? elementKey,
Guid? elementSettingKey,
bool? allowAtRoot)
{
if (elementKey is null)
{
return;
}
dictionary["blocks"] = new[] { BuildBlockConfiguration(elementKey.Value, elementSettingKey, allowAtRoot) };
}
private Dictionary<string, object> GetBlockGridBaseConfiguration()
=> new Dictionary<string, object> { ["gridColumns"] = 12 };
private Dictionary<string, object> GetRteBaseConfiguration()
{
var dictionary = new Dictionary<string, object>
{
["maxImageSize"] = 500,
["mode"] = "Classic",
["toolbar"] = new[]
{
"styles", "bold", "italic", "alignleft", "aligncenter", "alignright", "bullist", "numlist",
"outdent", "indent", "sourcecode", "link", "umbmediapicker", "umbembeddialog"
},
};
return dictionary;
}
private Dictionary<string, object> BuildBlockConfiguration(
Guid? elementKey,
Guid? elementSettingKey,
bool? allowAtRoot)
{
var dictionary = new Dictionary<string, object>();
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;
}
}

View File

@@ -54,6 +54,8 @@ internal sealed class TelemetryProviderTests : UmbracoIntegrationTest
private IMediaTypeService MediaTypeService => GetRequiredService<IMediaTypeService>();
private IContentBlueprintEditingService ContentBlueprintEditingService => GetRequiredService<IContentBlueprintEditingService>();
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<UsageInformation> result = null;

View File

@@ -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<ContentScaffoldedNotification, ContentScaffoldedNotificationHandler>();
=> builder
.AddNotificationHandler<ContentScaffoldedNotification, ContentScaffoldedNotificationHandler>()
.AddNotificationHandler<ContentScaffoldedNotification, BlockListPropertyNotificationHandler>()
.AddNotificationHandler<ContentScaffoldedNotification, BlockGridPropertyNotificationHandler>()
.AddNotificationHandler<ContentScaffoldedNotification, RichTextPropertyNotificationHandler>();
[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<Guid> newKeys = [];
var newInvariantBlocklist = GetBlockValue("invariantBlocks");
newKeys.AddRange(
newInvariantBlocklist.Layout
.SelectMany(x => x.Value)
.SelectMany(v => new List<Guid> { 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<Guid> { 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<BlockListValue>(result.GetValue<string>(propertyAlias, culture)),
Constants.PropertyEditors.Aliases.BlockGrid => JsonSerializer.Deserialize<BlockGridValue>(result.GetValue<string>(propertyAlias, culture)),
Constants.PropertyEditors.Aliases.RichText => JsonSerializer.Deserialize<RichTextEditorValue>(result.GetValue<string>(propertyAlias, culture)).Blocks!,
_ => throw new NotSupportedException($"Editor alias '{editorAlias}' is not supported for block blueprints."),
};
}
}
public class ContentScaffoldedNotificationHandler : INotificationHandler<ContentScaffoldedNotification>
{
public static Action<ContentScaffoldedNotification>? ContentScaffolded { get; set; }
public void Handle(ContentScaffoldedNotification notification) => ContentScaffolded?.Invoke(notification);
}
private async Task<(IContent Content, List<Guid> 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<ContentTypeBuilder>(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<ContentTypeBuilder>(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<Guid> 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<Guid> BlockKeys) CreateBlockValue(
string editorAlias,
IContentType elementContentType,
IContentType settingsContentType)
{
switch (editorAlias)
{
case Constants.PropertyEditors.Aliases.BlockList:
return CreateBlockValueOfType<BlockListValue, BlockListLayoutItem>(editorAlias, elementContentType, settingsContentType);
case Constants.PropertyEditors.Aliases.BlockGrid:
return CreateBlockValueOfType<BlockGridValue, BlockGridLayoutItem>(editorAlias, elementContentType, settingsContentType);
case Constants.PropertyEditors.Aliases.RichText:
var res = CreateBlockValueOfType<RichTextBlockValue, RichTextBlockLayoutItem>(editorAlias, elementContentType, settingsContentType);
return (new RichTextEditorValue
{
Markup = string.Join(string.Empty, res.BlockKeys.Chunk(2).Select(c => $"<umb-rte-block data-content-key=\"{c.First()}\"></umb-rte-block>")),
Blocks = res.BlockValue,
}, res.BlockKeys);
default:
throw new NotSupportedException($"Editor alias '{editorAlias}' is not supported for block blueprints.");
}
}
private static (T BlockValue, IEnumerable<Guid> BlockKeys) CreateBlockValueOfType<T, TLayout>(
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<string, IEnumerable<IBlockLayoutItem>>
{
[editorAlias] = blockKeys.Select(blockKeyGroup =>
new TLayout
{
ContentKey = blockKeyGroup[0],
SettingsKey = blockKeyGroup[1],
}).OfType<IBlockLayoutItem>(),
},
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));
}
}

View File

@@ -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<IEntityService>();
private IJsonSerializer JsonSerializer => GetRequiredService<IJsonSerializer>();
private async Task<IContent> 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();
}