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:
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>();
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user