Feature: single block property editor (#20098)
* First Go at the single block property editor based on blocklistpropertyeditor * Add simalar tests to the blocklist editor Also check whether either block of configured blocks can be picked and used from a data perspective * WIP singleblock Valiation tests * Finished first full pass off SingleBlock validation testing * Typos, Future test function * Restore accidently removed file * Introduce propertyValueConverter * Comment updates * Add singleBlock renderer * Textual improvements Comment improvements, remove licensing in file * Update DataEditorCount by 1 as we introduced a new one * Align test naming * Add ignored singleblock default renderer * Enable SingleBlock Property Indexing * Enable Partial value merging * Fix indentation --------- Co-authored-by: kjac <kja@umbraco.dk>
This commit is contained in:
@@ -40,6 +40,11 @@ public static partial class Constants
|
||||
/// </summary>
|
||||
public const string BlockList = "Umbraco.BlockList";
|
||||
|
||||
/// <summary>
|
||||
/// Block List.
|
||||
/// </summary>
|
||||
public const string SingleBlock = "Umbraco.SingleBlock";
|
||||
|
||||
/// <summary>
|
||||
/// Block Grid.
|
||||
/// </summary>
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
using Umbraco.Cms.Core.Serialization;
|
||||
|
||||
namespace Umbraco.Cms.Core.Models.Blocks;
|
||||
|
||||
/// <summary>
|
||||
/// Data converter for the single block property editor
|
||||
/// </summary>
|
||||
public class SingleBlockEditorDataConverter : BlockEditorDataConverter<SingleBlockValue, SingleBlockLayoutItem>
|
||||
{
|
||||
public SingleBlockEditorDataConverter(IJsonSerializer jsonSerializer)
|
||||
: base(jsonSerializer)
|
||||
{
|
||||
}
|
||||
|
||||
protected override IEnumerable<ContentAndSettingsReference> GetBlockReferences(IEnumerable<SingleBlockLayoutItem> layout)
|
||||
=> layout.Select(x => new ContentAndSettingsReference(x.ContentKey, x.SettingsKey)).ToList();
|
||||
}
|
||||
7
src/Umbraco.Core/Models/Blocks/SingleBlockLayoutItem.cs
Normal file
7
src/Umbraco.Core/Models/Blocks/SingleBlockLayoutItem.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
using Umbraco.Cms.Core.PropertyEditors;
|
||||
|
||||
namespace Umbraco.Cms.Core.Models.Blocks;
|
||||
|
||||
public class SingleBlockLayoutItem : BlockLayoutItemBase
|
||||
{
|
||||
}
|
||||
26
src/Umbraco.Core/Models/Blocks/SingleBlockValue.cs
Normal file
26
src/Umbraco.Core/Models/Blocks/SingleBlockValue.cs
Normal file
@@ -0,0 +1,26 @@
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Umbraco.Cms.Core.Models.Blocks;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a single block value.
|
||||
/// </summary>
|
||||
public class SingleBlockValue : BlockValue<SingleBlockLayoutItem>
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SingleBlockValue" /> class.
|
||||
/// </summary>
|
||||
public SingleBlockValue()
|
||||
{ }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SingleBlockValue" /> class.
|
||||
/// </summary>
|
||||
/// <param name="layout">The layout.</param>
|
||||
public SingleBlockValue(SingleBlockLayoutItem layout)
|
||||
=> Layout[PropertyEditorAlias] = [layout];
|
||||
|
||||
/// <inheritdoc />
|
||||
[JsonIgnore]
|
||||
public override string PropertyEditorAlias => Constants.PropertyEditors.Aliases.SingleBlock;
|
||||
}
|
||||
@@ -15,6 +15,7 @@ public class BlockListConfiguration
|
||||
public NumberRange ValidationLimit { get; set; } = new();
|
||||
|
||||
[ConfigurationField("useSingleBlockMode")]
|
||||
[Obsolete("Use SingleBlockPropertyEditor and its configuration instead")]
|
||||
public bool UseSingleBlockMode { get; set; }
|
||||
|
||||
public class BlockConfiguration : IBlockConfiguration
|
||||
|
||||
13
src/Umbraco.Core/PropertyEditors/SingleBlockConfiguration.cs
Normal file
13
src/Umbraco.Core/PropertyEditors/SingleBlockConfiguration.cs
Normal file
@@ -0,0 +1,13 @@
|
||||
// Copyright (c) Umbraco.
|
||||
// See LICENSE for more details.
|
||||
|
||||
namespace Umbraco.Cms.Core.PropertyEditors;
|
||||
|
||||
/// <summary>
|
||||
/// The configuration object for the Single Block editor
|
||||
/// </summary>
|
||||
public class SingleBlockConfiguration
|
||||
{
|
||||
[ConfigurationField("blocks")]
|
||||
public BlockListConfiguration.BlockConfiguration[] Blocks { get; set; } = [];
|
||||
}
|
||||
@@ -29,6 +29,16 @@ internal abstract class BlockEditorMinMaxValidatorBase<TValue, TLayout> : IValue
|
||||
/// <inheritdoc/>
|
||||
public abstract IEnumerable<ValidationResult> Validate(object? value, string? valueType, object? dataTypeConfiguration, PropertyValidationContext validationContext);
|
||||
|
||||
// internal method so we can test for specific error messages being returned without keeping strings in sync
|
||||
internal static string BuildErrorMessage(
|
||||
ILocalizedTextService textService,
|
||||
int? maxNumberOfBlocks,
|
||||
int numberOfBlocks)
|
||||
=> textService.Localize(
|
||||
"validation",
|
||||
"entriesExceed",
|
||||
[maxNumberOfBlocks.ToString(), (numberOfBlocks - maxNumberOfBlocks).ToString(),]);
|
||||
|
||||
/// <summary>
|
||||
/// Validates the number of blocks are within the configured minimum and maximum values.
|
||||
/// </summary>
|
||||
@@ -53,10 +63,7 @@ internal abstract class BlockEditorMinMaxValidatorBase<TValue, TLayout> : IValue
|
||||
if (blockEditorData != null && max.HasValue && numberOfBlocks > max)
|
||||
{
|
||||
yield return new ValidationResult(
|
||||
TextService.Localize(
|
||||
"validation",
|
||||
"entriesExceed",
|
||||
[max.ToString(), (numberOfBlocks - max).ToString(),]),
|
||||
BuildErrorMessage(TextService, max, numberOfBlocks),
|
||||
["value"]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
using Umbraco.Cms.Core.IO;
|
||||
using Umbraco.Cms.Core.PropertyEditors;
|
||||
|
||||
namespace Umbraco.Cms.Infrastructure.PropertyEditors;
|
||||
|
||||
internal sealed class SingleBlockConfigurationEditor : ConfigurationEditor<SingleBlockConfiguration>
|
||||
{
|
||||
public SingleBlockConfigurationEditor(IIOHelper ioHelper)
|
||||
: base(ioHelper)
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.Cache;
|
||||
using Umbraco.Cms.Core.Cache.PropertyEditors;
|
||||
using Umbraco.Cms.Core.IO;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Models.Blocks;
|
||||
using Umbraco.Cms.Core.Models.Validation;
|
||||
using Umbraco.Cms.Core.PropertyEditors;
|
||||
using Umbraco.Cms.Core.PropertyEditors.ValueConverters;
|
||||
using Umbraco.Cms.Core.Serialization;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Core.Strings;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Infrastructure.PropertyEditors;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a single block property editor.
|
||||
/// </summary>
|
||||
[DataEditor(
|
||||
Constants.PropertyEditors.Aliases.SingleBlock,
|
||||
ValueType = ValueTypes.Json,
|
||||
ValueEditorIsReusable = false)]
|
||||
public class SingleBlockPropertyEditor : DataEditor
|
||||
{
|
||||
private readonly IJsonSerializer _jsonSerializer;
|
||||
private readonly IIOHelper _ioHelper;
|
||||
private readonly IBlockValuePropertyIndexValueFactory _blockValuePropertyIndexValueFactory;
|
||||
|
||||
public SingleBlockPropertyEditor(
|
||||
IDataValueEditorFactory dataValueEditorFactory,
|
||||
IJsonSerializer jsonSerializer,
|
||||
IIOHelper ioHelper,
|
||||
IBlockValuePropertyIndexValueFactory blockValuePropertyIndexValueFactory)
|
||||
: base(dataValueEditorFactory)
|
||||
{
|
||||
_jsonSerializer = jsonSerializer;
|
||||
_ioHelper = ioHelper;
|
||||
_blockValuePropertyIndexValueFactory = blockValuePropertyIndexValueFactory;
|
||||
}
|
||||
|
||||
public override IPropertyIndexValueFactory PropertyIndexValueFactory => _blockValuePropertyIndexValueFactory;
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override bool SupportsConfigurableElements => true;
|
||||
|
||||
/// <summary>
|
||||
/// Instantiates a new <see cref="BlockEditorDataConverter{SingleBlockValue, SingleBlockLayoutItem}"/> for use with the single block editor property value editor.
|
||||
/// </summary>
|
||||
/// <returns>A new instance of <see cref="SingleBlockEditorDataConverter"/>.</returns>
|
||||
protected virtual BlockEditorDataConverter<SingleBlockValue, SingleBlockLayoutItem> CreateBlockEditorDataConverter()
|
||||
=> new SingleBlockEditorDataConverter(_jsonSerializer);
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override IDataValueEditor CreateValueEditor() =>
|
||||
DataValueEditorFactory.Create<SingleBlockEditorPropertyValueEditor>(Attribute!, CreateBlockEditorDataConverter());
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool CanMergePartialPropertyValues(IPropertyType propertyType) => propertyType.VariesByCulture() is false;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override object? MergePartialPropertyValueForCulture(object? sourceValue, object? targetValue, string? culture)
|
||||
{
|
||||
var valueEditor = (SingleBlockEditorPropertyValueEditor)GetValueEditor();
|
||||
return valueEditor.MergePartialPropertyValueForCulture(sourceValue, targetValue, culture);
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override IConfigurationEditor CreateConfigurationEditor() =>
|
||||
new SingleBlockConfigurationEditor(_ioHelper);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override object? MergeVariantInvariantPropertyValue(
|
||||
object? sourceValue,
|
||||
object? targetValue,
|
||||
bool canUpdateInvariantData,
|
||||
HashSet<string> allowedCultures)
|
||||
{
|
||||
var valueEditor = (SingleBlockEditorPropertyValueEditor)GetValueEditor();
|
||||
return valueEditor.MergeVariantInvariantPropertyValue(sourceValue, targetValue, canUpdateInvariantData, allowedCultures);
|
||||
}
|
||||
|
||||
internal sealed class SingleBlockEditorPropertyValueEditor : BlockEditorPropertyValueEditor<SingleBlockValue, SingleBlockLayoutItem>
|
||||
{
|
||||
public SingleBlockEditorPropertyValueEditor(
|
||||
DataEditorAttribute attribute,
|
||||
BlockEditorDataConverter<SingleBlockValue, SingleBlockLayoutItem> blockEditorDataConverter,
|
||||
PropertyEditorCollection propertyEditors,
|
||||
DataValueReferenceFactoryCollection dataValueReferenceFactories,
|
||||
IDataTypeConfigurationCache dataTypeConfigurationCache,
|
||||
IShortStringHelper shortStringHelper,
|
||||
IJsonSerializer jsonSerializer,
|
||||
BlockEditorVarianceHandler blockEditorVarianceHandler,
|
||||
ILanguageService languageService,
|
||||
IIOHelper ioHelper,
|
||||
IBlockEditorElementTypeCache elementTypeCache,
|
||||
ILogger<SingleBlockEditorPropertyValueEditor> logger,
|
||||
ILocalizedTextService textService,
|
||||
IPropertyValidationService propertyValidationService)
|
||||
: base(propertyEditors, dataValueReferenceFactories, dataTypeConfigurationCache, shortStringHelper, jsonSerializer, blockEditorVarianceHandler, languageService, ioHelper, attribute)
|
||||
{
|
||||
BlockEditorValues = new BlockEditorValues<SingleBlockValue, SingleBlockLayoutItem>(blockEditorDataConverter, elementTypeCache, logger);
|
||||
Validators.Add(new BlockEditorValidator<SingleBlockValue, SingleBlockLayoutItem>(propertyValidationService, BlockEditorValues, elementTypeCache));
|
||||
Validators.Add(new SingleBlockValidator(BlockEditorValues, textService));
|
||||
}
|
||||
|
||||
protected override SingleBlockValue CreateWithLayout(IEnumerable<SingleBlockLayoutItem> layout) =>
|
||||
new(layout.Single());
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override IEnumerable<Guid> ConfiguredElementTypeKeys()
|
||||
{
|
||||
var configuration = ConfigurationObject as SingleBlockConfiguration;
|
||||
return configuration?.Blocks.SelectMany(ConfiguredElementTypeKeys) ?? Enumerable.Empty<Guid>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates whether the block editor holds a single value
|
||||
/// </summary>
|
||||
internal sealed class SingleBlockValidator : BlockEditorMinMaxValidatorBase<SingleBlockValue, SingleBlockLayoutItem>
|
||||
{
|
||||
private readonly BlockEditorValues<SingleBlockValue, SingleBlockLayoutItem> _blockEditorValues;
|
||||
|
||||
public SingleBlockValidator(BlockEditorValues<SingleBlockValue, SingleBlockLayoutItem> blockEditorValues, ILocalizedTextService textService)
|
||||
: base(textService) =>
|
||||
_blockEditorValues = blockEditorValues;
|
||||
|
||||
public override IEnumerable<ValidationResult> Validate(object? value, string? valueType, object? dataTypeConfiguration, PropertyValidationContext validationContext)
|
||||
{
|
||||
BlockEditorData<SingleBlockValue, SingleBlockLayoutItem>? blockEditorData = _blockEditorValues.DeserializeAndClean(value);
|
||||
|
||||
return ValidateNumberOfBlocks(blockEditorData, 0, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
// Copyright (c) Umbraco.
|
||||
// See LICENSE for more details.
|
||||
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.DeliveryApi;
|
||||
using Umbraco.Cms.Core.Logging;
|
||||
using Umbraco.Cms.Core.Models.Blocks;
|
||||
using Umbraco.Cms.Core.Models.DeliveryApi;
|
||||
using Umbraco.Cms.Core.Models.PublishedContent;
|
||||
using Umbraco.Cms.Core.PropertyEditors;
|
||||
using Umbraco.Cms.Core.PropertyEditors.DeliveryApi;
|
||||
using Umbraco.Cms.Core.PropertyEditors.ValueConverters;
|
||||
using Umbraco.Cms.Core.Serialization;
|
||||
using Umbraco.Cms.Infrastructure.Extensions;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Infrastructure.PropertyEditors.ValueConverters;
|
||||
|
||||
[DefaultPropertyValueConverter(typeof(JsonValueConverter))]
|
||||
public class SingleBlockPropertyValueConverter : PropertyValueConverterBase, IDeliveryApiPropertyValueConverter
|
||||
{
|
||||
private readonly IProfilingLogger _proflog;
|
||||
private readonly BlockEditorConverter _blockConverter;
|
||||
private readonly IApiElementBuilder _apiElementBuilder;
|
||||
private readonly IJsonSerializer _jsonSerializer;
|
||||
private readonly BlockListPropertyValueConstructorCache _constructorCache;
|
||||
private readonly IVariationContextAccessor _variationContextAccessor;
|
||||
private readonly BlockEditorVarianceHandler _blockEditorVarianceHandler;
|
||||
|
||||
public SingleBlockPropertyValueConverter(
|
||||
IProfilingLogger proflog,
|
||||
BlockEditorConverter blockConverter,
|
||||
IApiElementBuilder apiElementBuilder,
|
||||
IJsonSerializer jsonSerializer,
|
||||
BlockListPropertyValueConstructorCache constructorCache,
|
||||
IVariationContextAccessor variationContextAccessor,
|
||||
BlockEditorVarianceHandler blockEditorVarianceHandler)
|
||||
{
|
||||
_proflog = proflog;
|
||||
_blockConverter = blockConverter;
|
||||
_apiElementBuilder = apiElementBuilder;
|
||||
_jsonSerializer = jsonSerializer;
|
||||
_constructorCache = constructorCache;
|
||||
_variationContextAccessor = variationContextAccessor;
|
||||
_blockEditorVarianceHandler = blockEditorVarianceHandler;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool IsConverter(IPublishedPropertyType propertyType)
|
||||
=> propertyType.EditorAlias.InvariantEquals(Constants.PropertyEditors.Aliases.SingleBlock);
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Type GetPropertyValueType(IPublishedPropertyType propertyType) => typeof( BlockListItem);
|
||||
|
||||
/// <inheritdoc />
|
||||
public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType)
|
||||
=> PropertyCacheLevel.Element;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override object? ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object? source, bool preview)
|
||||
=> source?.ToString();
|
||||
|
||||
/// <inheritdoc />
|
||||
public override object? ConvertIntermediateToObject(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object? inter, bool preview)
|
||||
{
|
||||
// NOTE: The intermediate object is just a JSON string, we don't actually convert from source -> intermediate since source is always just a JSON string
|
||||
using (!_proflog.IsEnabled(Core.Logging.LogLevel.Debug) ? null : _proflog.DebugDuration<BlockListPropertyValueConverter>(
|
||||
$"ConvertPropertyToBlockList ({propertyType.DataType.Id})"))
|
||||
{
|
||||
return ConvertIntermediateToBlockListItem(owner, propertyType, referenceCacheLevel, inter, preview);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public PropertyCacheLevel GetDeliveryApiPropertyCacheLevel(IPublishedPropertyType propertyType) => GetPropertyCacheLevel(propertyType);
|
||||
|
||||
/// <inheritdoc />
|
||||
public PropertyCacheLevel GetDeliveryApiPropertyCacheLevelForExpansion(IPublishedPropertyType propertyType) => PropertyCacheLevel.Snapshot;
|
||||
|
||||
/// <inheritdoc />
|
||||
public Type GetDeliveryApiPropertyValueType(IPublishedPropertyType propertyType)
|
||||
=> typeof(ApiBlockItem);
|
||||
|
||||
/// <inheritdoc />
|
||||
public object? ConvertIntermediateToDeliveryApiObject(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object? inter, bool preview, bool expanding)
|
||||
{
|
||||
BlockListItem? model = ConvertIntermediateToBlockListItem(owner, propertyType, referenceCacheLevel, inter, preview);
|
||||
|
||||
return
|
||||
model?.CreateApiBlockItem(_apiElementBuilder);
|
||||
}
|
||||
|
||||
private BlockListItem? ConvertIntermediateToBlockListItem(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object? inter, bool preview)
|
||||
{
|
||||
using (!_proflog.IsEnabled(LogLevel.Debug) ? null : _proflog.DebugDuration<SingleBlockPropertyValueConverter>(
|
||||
$"ConvertPropertyToSingleBlock ({propertyType.DataType.Id})"))
|
||||
{
|
||||
// NOTE: The intermediate object is just a JSON string, we don't actually convert from source -> intermediate since source is always just a JSON string
|
||||
if (inter is not string intermediateBlockModelValue)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// Get configuration
|
||||
SingleBlockConfiguration? configuration = propertyType.DataType.ConfigurationAs<SingleBlockConfiguration>();
|
||||
if (configuration is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
var creator = new SingleBlockPropertyValueCreator(_blockConverter, _variationContextAccessor, _blockEditorVarianceHandler, _jsonSerializer, _constructorCache);
|
||||
return creator.CreateBlockModel(owner, referenceCacheLevel, intermediateBlockModelValue, preview, configuration.Blocks);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
using Umbraco.Cms.Core.Models.Blocks;
|
||||
using Umbraco.Cms.Core.Models.PublishedContent;
|
||||
using Umbraco.Cms.Core.Serialization;
|
||||
|
||||
namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters;
|
||||
|
||||
internal sealed class SingleBlockPropertyValueCreator : BlockPropertyValueCreatorBase<BlockListModel, BlockListItem, SingleBlockLayoutItem, BlockListConfiguration.BlockConfiguration, SingleBlockValue>
|
||||
{
|
||||
private readonly IJsonSerializer _jsonSerializer;
|
||||
private readonly BlockListPropertyValueConstructorCache _constructorCache;
|
||||
|
||||
public SingleBlockPropertyValueCreator(
|
||||
BlockEditorConverter blockEditorConverter,
|
||||
IVariationContextAccessor variationContextAccessor,
|
||||
BlockEditorVarianceHandler blockEditorVarianceHandler,
|
||||
IJsonSerializer jsonSerializer,
|
||||
BlockListPropertyValueConstructorCache constructorCache)
|
||||
: base(blockEditorConverter, variationContextAccessor, blockEditorVarianceHandler)
|
||||
{
|
||||
_jsonSerializer = jsonSerializer;
|
||||
_constructorCache = constructorCache;
|
||||
}
|
||||
|
||||
// The underlying Value is still stored as an array to allow for code reuse and easier migration
|
||||
public BlockListItem? CreateBlockModel(IPublishedElement owner, PropertyCacheLevel referenceCacheLevel, string intermediateBlockModelValue, bool preview, BlockListConfiguration.BlockConfiguration[] blockConfigurations)
|
||||
{
|
||||
BlockListModel CreateEmptyModel() => BlockListModel.Empty;
|
||||
|
||||
BlockListModel CreateModel(IList<BlockListItem> items) => new BlockListModel(items);
|
||||
|
||||
BlockListItem? blockModel = CreateBlockModel(owner, referenceCacheLevel, intermediateBlockModelValue, preview, blockConfigurations, CreateEmptyModel, CreateModel).SingleOrDefault();
|
||||
|
||||
return blockModel;
|
||||
}
|
||||
|
||||
protected override BlockEditorDataConverter<SingleBlockValue, SingleBlockLayoutItem> CreateBlockEditorDataConverter() => new SingleBlockEditorDataConverter(_jsonSerializer);
|
||||
|
||||
protected override BlockItemActivator<BlockListItem> CreateBlockItemActivator() => new BlockListItemActivator(BlockEditorConverter, _constructorCache);
|
||||
|
||||
private sealed class BlockListItemActivator : BlockItemActivator<BlockListItem>
|
||||
{
|
||||
public BlockListItemActivator(BlockEditorConverter blockConverter, BlockListPropertyValueConstructorCache constructorCache)
|
||||
: base(blockConverter, constructorCache)
|
||||
{
|
||||
}
|
||||
|
||||
protected override Type GenericItemType => typeof(BlockListItem<,>);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
using Microsoft.AspNetCore.Html;
|
||||
using Microsoft.AspNetCore.Mvc.Rendering;
|
||||
using Microsoft.AspNetCore.Mvc.ViewEngines;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Umbraco.Cms.Core.Models.Blocks;
|
||||
using Umbraco.Cms.Core.Models.PublishedContent;
|
||||
|
||||
namespace Umbraco.Extensions;
|
||||
|
||||
public static class SingleBlockTemplateExtensions
|
||||
{
|
||||
public const string DefaultFolder = "singleblock/";
|
||||
public const string DefaultTemplate = "default";
|
||||
|
||||
#region Async
|
||||
|
||||
public static async Task<IHtmlContent> GetBlockHtmlAsync(this IHtmlHelper html, BlockListItem? model, string template = DefaultTemplate)
|
||||
{
|
||||
if (model is null)
|
||||
{
|
||||
return new HtmlString(string.Empty);
|
||||
}
|
||||
|
||||
return await html.PartialAsync(DefaultFolderTemplate(template), model);
|
||||
}
|
||||
|
||||
public static async Task<IHtmlContent> GetBlockHtmlAsync(this IHtmlHelper html, IPublishedProperty property, string template = DefaultTemplate)
|
||||
=> await GetBlockHtmlAsync(html, property.GetValue() as BlockListItem, template);
|
||||
|
||||
public static async Task<IHtmlContent> GetBlockHtmlAsync(this IHtmlHelper html, IPublishedContent contentItem, string propertyAlias)
|
||||
=> await GetBlockHtmlAsync(html, contentItem, propertyAlias, DefaultTemplate);
|
||||
|
||||
public static async Task<IHtmlContent> GetBlockHtmlAsync(this IHtmlHelper html, IPublishedContent contentItem, string propertyAlias, string template)
|
||||
{
|
||||
IPublishedProperty property = GetRequiredProperty(contentItem, propertyAlias);
|
||||
return await GetBlockHtmlAsync(html, property.GetValue() as BlockListItem, template);
|
||||
}
|
||||
#endregion
|
||||
|
||||
#region Sync
|
||||
|
||||
public static IHtmlContent GetBlockHtml(this IHtmlHelper html, BlockListItem? model, string template = DefaultTemplate)
|
||||
{
|
||||
if (model is null)
|
||||
{
|
||||
return new HtmlString(string.Empty);
|
||||
}
|
||||
|
||||
return html.Partial(DefaultFolderTemplate(template), model);
|
||||
}
|
||||
|
||||
public static IHtmlContent GetBlockHtml(this IHtmlHelper html, IPublishedProperty property, string template = DefaultTemplate)
|
||||
=> GetBlockHtml(html, property.GetValue() as BlockListItem, template);
|
||||
|
||||
public static IHtmlContent GetBlockHtml(this IHtmlHelper html, IPublishedContent contentItem, string propertyAlias)
|
||||
=> GetBlockHtml(html, contentItem, propertyAlias, DefaultTemplate);
|
||||
|
||||
public static IHtmlContent GetBlockHtml(this IHtmlHelper html, IPublishedContent contentItem, string propertyAlias, string template)
|
||||
{
|
||||
IPublishedProperty property = GetRequiredProperty(contentItem, propertyAlias);
|
||||
return GetBlockHtml(html, property.GetValue() as BlockListItem, template);
|
||||
}
|
||||
|
||||
public static string SingleBlockPartialWithFallback(this IHtmlHelper html, string template, string fallbackTemplate)
|
||||
{
|
||||
IServiceProvider requestServices = html.ViewContext.HttpContext.RequestServices;
|
||||
ICompositeViewEngine? viewEngine = requestServices.GetService<ICompositeViewEngine>();
|
||||
if (viewEngine is null)
|
||||
{
|
||||
return template;
|
||||
}
|
||||
|
||||
// .Getview, and likely .FindView, will be invoked when invoking html.Partial
|
||||
// the heavy lifting in the underlying logic seems to be cached so it should be ok to offer this logic
|
||||
// as a DX feature in the default block renderer.
|
||||
return
|
||||
viewEngine.GetView(html.ViewContext.ExecutingFilePath, template, isMainPage: false).Success
|
||||
? template
|
||||
: viewEngine.FindView(html.ViewContext, template, isMainPage: false).Success
|
||||
? template
|
||||
: fallbackTemplate;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
private static string DefaultFolderTemplate(string template) => $"{DefaultFolder}{template}";
|
||||
|
||||
private static IPublishedProperty GetRequiredProperty(IPublishedContent contentItem, string propertyAlias)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(propertyAlias);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(propertyAlias))
|
||||
{
|
||||
throw new ArgumentException(
|
||||
"Value can't be empty or consist only of white-space characters.",
|
||||
nameof(propertyAlias));
|
||||
}
|
||||
|
||||
IPublishedProperty? property = contentItem.GetProperty(propertyAlias);
|
||||
if (property == null)
|
||||
{
|
||||
throw new InvalidOperationException("No property type found with alias " + propertyAlias);
|
||||
}
|
||||
|
||||
return property;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
@inherits Umbraco.Cms.Web.Common.Views.UmbracoViewPage<Umbraco.Cms.Core.Models.Blocks.BlockListItem>
|
||||
@{
|
||||
if (Model.ContentKey == Guid.Empty) { return; }
|
||||
var data = Model.Content;
|
||||
}
|
||||
@await Html.PartialAsync(
|
||||
Html.SingleBlockPartialWithFallback("singleBlock/Components/" + data.ContentType.Alias,
|
||||
"blocklist/Components/" + data.ContentType.Alias )
|
||||
, Model)
|
||||
@@ -56,6 +56,10 @@
|
||||
<Content Include="..\src\Umbraco.Web.UI\Views\Partials\blockgrid\**">
|
||||
<Link>UmbracoProject\Views\Partials\blockgrid\%(RecursiveDir)%(Filename)%(Extension)</Link>
|
||||
<PackagePath>UmbracoProject\Views\Partials\blockgrid</PackagePath>
|
||||
</Content>
|
||||
<Content Include="..\src\Umbraco.Web.UI\Views\Partials\singleblock\**">
|
||||
<Link>UmbracoProject\Views\Partials\singleblock\%(RecursiveDir)%(Filename)%(Extension)</Link>
|
||||
<PackagePath>UmbracoProject\Views\Partials\singleblock</PackagePath>
|
||||
</Content>
|
||||
<Content Include="..\src\Umbraco.Web.UI\Views\_ViewImports.cshtml">
|
||||
<Link>UmbracoProject\Views\_ViewImports.cshtml</Link>
|
||||
|
||||
@@ -5,6 +5,7 @@ using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.IO;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.PropertyEditors;
|
||||
using Umbraco.Cms.Infrastructure.PropertyEditors;
|
||||
using Umbraco.Cms.Infrastructure.Serialization;
|
||||
using Umbraco.Cms.Tests.Common.Builders.Extensions;
|
||||
using Umbraco.Cms.Tests.Common.Builders.Interfaces;
|
||||
@@ -194,6 +195,10 @@ public class DataTypeBuilder
|
||||
dataTypeBuilder.WithConfigurationEditor(
|
||||
new BlockListConfigurationEditor(ioHelper) { DefaultConfiguration = configuration });
|
||||
break;
|
||||
case Constants.PropertyEditors.Aliases.SingleBlock:
|
||||
dataTypeBuilder.WithConfigurationEditor(
|
||||
new SingleBlockConfigurationEditor(ioHelper) { DefaultConfiguration = configuration });
|
||||
break;
|
||||
case Constants.PropertyEditors.Aliases.RichText:
|
||||
dataTypeBuilder.WithConfigurationEditor(
|
||||
new RichTextConfigurationEditor(ioHelper) { DefaultConfiguration = configuration });
|
||||
|
||||
@@ -0,0 +1,187 @@
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Models.ContentEditing;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Core.Services;
|
||||
|
||||
// Disclaimer: Based on code generated by Claude code.
|
||||
// Most likely not complete, but the logic looks sound.
|
||||
public interface IContentEditingModelFactory
|
||||
{
|
||||
Task<ContentUpdateModel> CreateFromAsync(IContent content);
|
||||
}
|
||||
|
||||
public class ContentEditingModelFactory : IContentEditingModelFactory
|
||||
{
|
||||
private readonly ITemplateService _templateService;
|
||||
|
||||
public ContentEditingModelFactory(ITemplateService templateService)
|
||||
{
|
||||
_templateService = templateService;
|
||||
}
|
||||
|
||||
public async Task<ContentUpdateModel> CreateFromAsync(IContent content)
|
||||
{
|
||||
{
|
||||
var templateKey = content.TemplateId.HasValue
|
||||
? (await _templateService.GetAsync(content.TemplateId.Value))?.Key
|
||||
: null;
|
||||
var model = new ContentUpdateModel { TemplateKey = templateKey };
|
||||
var properties = new List<PropertyValueModel>();
|
||||
var variants = new List<VariantModel>();
|
||||
|
||||
MapProperties(content, properties);
|
||||
|
||||
MapNames(content, properties, variants);
|
||||
|
||||
model.Properties = properties;
|
||||
model.Variants = variants;
|
||||
return model;
|
||||
}
|
||||
}
|
||||
|
||||
private static void MapNames(IContent content, List<PropertyValueModel> properties, List<VariantModel> variants)
|
||||
{
|
||||
// Handle variants (content names per culture/segment)
|
||||
var contentVariesByCulture = content.ContentType.VariesByCulture();
|
||||
var contentVariesBySegment = content.ContentType.VariesBySegment();
|
||||
if (contentVariesByCulture || contentVariesBySegment)
|
||||
{
|
||||
// Get all unique culture/segment combinations from CultureInfos
|
||||
var cultureSegmentCombinations = new HashSet<(string? culture, string? segment)>();
|
||||
|
||||
// Add invariant combination
|
||||
cultureSegmentCombinations.Add((null, null));
|
||||
if (contentVariesByCulture)
|
||||
{
|
||||
// Add cultures
|
||||
foreach (var culture in content.AvailableCultures)
|
||||
{
|
||||
cultureSegmentCombinations.Add((culture, null));
|
||||
}
|
||||
}
|
||||
|
||||
// For segment support, we need to extract segments from property values
|
||||
// since content doesn't have "AvailableSegments" like cultures
|
||||
if (contentVariesBySegment)
|
||||
{
|
||||
var segmentsFromProperties = properties
|
||||
.Where(p => !string.IsNullOrEmpty(p.Segment))
|
||||
.Select(p => p.Segment)
|
||||
.Distinct()
|
||||
.ToList();
|
||||
foreach (var segment in segmentsFromProperties)
|
||||
{
|
||||
cultureSegmentCombinations.Add((null, segment));
|
||||
// If content also varies by culture, add culture+segment combinations
|
||||
if (contentVariesByCulture)
|
||||
{
|
||||
foreach (var culture in content.AvailableCultures)
|
||||
{
|
||||
cultureSegmentCombinations.Add((culture, segment));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Create variants for each combination
|
||||
foreach (var (culture, segment) in cultureSegmentCombinations)
|
||||
{
|
||||
string? variantName;
|
||||
if (culture == null && segment == null)
|
||||
{
|
||||
// Invariant
|
||||
variantName = content.Name;
|
||||
}
|
||||
else if (culture != null && segment == null)
|
||||
{
|
||||
// Culture-specific
|
||||
variantName = content.GetCultureName(culture);
|
||||
}
|
||||
else
|
||||
{
|
||||
// For segment-specific or culture+segment combinations,
|
||||
// we'll use the invariant or culture name as segments don't have separate names
|
||||
variantName = culture != null ? content.GetCultureName(culture) : content.Name;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(variantName))
|
||||
{
|
||||
variants.Add(new VariantModel { Culture = culture, Segment = segment, Name = variantName });
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// For invariant content, add single variant
|
||||
variants.Add(new VariantModel { Culture = null, Segment = null, Name = content.Name ?? string.Empty });
|
||||
}
|
||||
}
|
||||
|
||||
private static void MapProperties(IContent content, List<PropertyValueModel> properties)
|
||||
{
|
||||
// Handle properties
|
||||
foreach (var property in content.Properties)
|
||||
{
|
||||
var propertyVariesByCulture = property.PropertyType.VariesByCulture();
|
||||
var propertyVariesBySegment = property.PropertyType.VariesBySegment();
|
||||
|
||||
// Get all property values from the property's Values collection
|
||||
foreach (var propertyValue in property.Values)
|
||||
{
|
||||
if (propertyValue.EditedValue != null)
|
||||
{
|
||||
properties.Add(new PropertyValueModel
|
||||
{
|
||||
Alias = property.Alias,
|
||||
Value = propertyValue.EditedValue,
|
||||
Culture = propertyVariesByCulture ? propertyValue.Culture : null,
|
||||
Segment = propertyVariesBySegment ? propertyValue.Segment : null,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: if no values found in the Values collection, try the traditional approach
|
||||
if (!property.Values.Any())
|
||||
{
|
||||
if (propertyVariesByCulture && content.AvailableCultures.Any())
|
||||
{
|
||||
// Handle culture variants
|
||||
foreach (var culture in content.AvailableCultures)
|
||||
{
|
||||
var cultureValue = property.GetValue(culture);
|
||||
if (cultureValue != null)
|
||||
{
|
||||
properties.Add(new PropertyValueModel
|
||||
{
|
||||
Alias = property.Alias, Value = cultureValue, Culture = culture, Segment = null,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Also add the invariant value if it exists
|
||||
var invariantValue = property.GetValue();
|
||||
if (invariantValue != null)
|
||||
{
|
||||
properties.Add(new PropertyValueModel
|
||||
{
|
||||
Alias = property.Alias, Value = invariantValue, Culture = null, Segment = null,
|
||||
});
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Handle invariant properties
|
||||
var value = property.GetValue();
|
||||
if (value != null)
|
||||
{
|
||||
properties.Add(new PropertyValueModel
|
||||
{
|
||||
Alias = property.Alias, Value = value, Culture = null, Segment = null,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -185,6 +185,9 @@ public abstract class UmbracoIntegrationTest : UmbracoIntegrationTestBase
|
||||
CustomTestSetup(builder);
|
||||
ExecuteBuilderAttributes(builder);
|
||||
|
||||
// custom helper services that might be moved out of tests eventually to benefit the community
|
||||
services.AddSingleton<IContentEditingModelFactory, ContentEditingModelFactory>();
|
||||
|
||||
builder.Build();
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,777 @@
|
||||
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.PropertyEditors;
|
||||
using Umbraco.Cms.Core.Serialization;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Infrastructure.PropertyEditors;
|
||||
using Umbraco.Cms.Tests.Common.Builders;
|
||||
using Umbraco.Cms.Tests.Common.Builders.Extensions;
|
||||
using Umbraco.Cms.Tests.Common.Testing;
|
||||
using Umbraco.Cms.Tests.Integration.Testing;
|
||||
|
||||
namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.PropertyEditors;
|
||||
|
||||
[TestFixture]
|
||||
[UmbracoTest(Database = UmbracoTestOptions.Database.NewSchemaPerTest)]
|
||||
internal sealed class SingleBlockPropertyEditorTests : UmbracoIntegrationTest
|
||||
{
|
||||
private IContentTypeService ContentTypeService => GetRequiredService<IContentTypeService>();
|
||||
|
||||
private IContentService ContentService => GetRequiredService<IContentService>();
|
||||
|
||||
private IDataTypeService DataTypeService => GetRequiredService<IDataTypeService>();
|
||||
|
||||
private IJsonSerializer JsonSerializer => GetRequiredService<IJsonSerializer>();
|
||||
|
||||
private IConfigurationEditorJsonSerializer ConfigurationEditorJsonSerializer => GetRequiredService<IConfigurationEditorJsonSerializer>();
|
||||
|
||||
private PropertyEditorCollection PropertyEditorCollection => GetRequiredService<PropertyEditorCollection>();
|
||||
|
||||
private ILanguageService LanguageService => GetRequiredService<ILanguageService>();
|
||||
|
||||
private IContentValidationService ContentValidationService => GetRequiredService<IContentValidationService>();
|
||||
|
||||
private IContentEditingModelFactory ContentEditingModelFactory => GetRequiredService<IContentEditingModelFactory>();
|
||||
|
||||
private ILocalizedTextService LocalizedTextService => GetRequiredService<ILocalizedTextService>();
|
||||
|
||||
private const string AllTypes = "allTypes";
|
||||
private const string MetaType = "metaType";
|
||||
private const string TextType = "textType";
|
||||
|
||||
[Theory]
|
||||
[TestCase(AllTypes)]
|
||||
[TestCase(MetaType)]
|
||||
public async Task Can_Select_Different_Configured_Block(string elementTypeName)
|
||||
{
|
||||
if (elementTypeName != AllTypes && elementTypeName != MetaType)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(elementTypeName));
|
||||
}
|
||||
|
||||
var textPageContentType = ContentTypeBuilder.CreateTextPageContentType("myContentType");
|
||||
textPageContentType.AllowedTemplates = [];
|
||||
await ContentTypeService.CreateAsync(textPageContentType, Constants.Security.SuperUserKey);
|
||||
|
||||
var textPage = ContentBuilder.CreateTextpageContent(textPageContentType, "My Picked Content", -1);
|
||||
ContentService.Save(textPage);
|
||||
|
||||
var allTypesType = ContentTypeBuilder.CreateAllTypesContentType("allTypesType", "All Types type");
|
||||
allTypesType.IsElement = true;
|
||||
await ContentTypeService.CreateAsync(allTypesType, Constants.Security.SuperUserKey);
|
||||
|
||||
var metaTypesType = ContentTypeBuilder.CreateMetaContentType();
|
||||
metaTypesType.IsElement = true;
|
||||
await ContentTypeService.CreateAsync(metaTypesType, Constants.Security.SuperUserKey);
|
||||
|
||||
var singleBlockContentType = await CreateSingleBlockContentTypePage([allTypesType, metaTypesType]);
|
||||
|
||||
var contentElementKey = Guid.NewGuid();
|
||||
var blockValue = new SingleBlockValue
|
||||
{
|
||||
Layout = new Dictionary<string, IEnumerable<IBlockLayoutItem>>
|
||||
{
|
||||
{
|
||||
Constants.PropertyEditors.Aliases.SingleBlock,
|
||||
[
|
||||
new SingleBlockLayoutItem { ContentKey = contentElementKey }
|
||||
]
|
||||
},
|
||||
},
|
||||
ContentData =
|
||||
[
|
||||
elementTypeName == AllTypes
|
||||
? new BlockItemData
|
||||
{
|
||||
Key = contentElementKey,
|
||||
ContentTypeAlias = allTypesType.Alias,
|
||||
ContentTypeKey = allTypesType.Key,
|
||||
Values =
|
||||
[
|
||||
new BlockPropertyValue
|
||||
{
|
||||
Alias = "contentPicker",
|
||||
Value = textPage.GetUdi(),
|
||||
}
|
||||
],
|
||||
}
|
||||
: new BlockItemData
|
||||
{
|
||||
Key = contentElementKey,
|
||||
ContentTypeAlias = metaTypesType.Alias,
|
||||
ContentTypeKey = metaTypesType.Key,
|
||||
Values =
|
||||
[
|
||||
new BlockPropertyValue
|
||||
{
|
||||
Alias = "metadescription",
|
||||
Value = "something very meta",
|
||||
}
|
||||
],
|
||||
}
|
||||
],
|
||||
};
|
||||
var blocksPropertyValue = JsonSerializer.Serialize(blockValue);
|
||||
|
||||
var content = new ContentBuilder()
|
||||
.WithContentType(singleBlockContentType)
|
||||
.WithName("My Blocks")
|
||||
.WithPropertyValues(new { block = blocksPropertyValue })
|
||||
.Build();
|
||||
ContentService.Save(content);
|
||||
|
||||
var valueEditor = await GetValueEditor(singleBlockContentType);
|
||||
var toEditorValue = valueEditor.ToEditor(content.Properties["block"]!) as SingleBlockValue;
|
||||
Assert.IsNotNull(toEditorValue);
|
||||
Assert.AreEqual(1, toEditorValue.ContentData.Count);
|
||||
|
||||
var properties = toEditorValue.ContentData.First().Values;
|
||||
Assert.AreEqual(1, properties.Count);
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
var property = properties.First();
|
||||
Assert.AreEqual(elementTypeName == AllTypes ? "contentPicker" : "metadescription", property.Alias);
|
||||
Assert.AreEqual(elementTypeName == AllTypes ? textPage.Key : "something very meta", property.Value);
|
||||
});
|
||||
|
||||
// convert to updateModel and run validation
|
||||
var updateModel = await ContentEditingModelFactory.CreateFromAsync(content);
|
||||
var validationResult = await ContentValidationService.ValidatePropertiesAsync(updateModel, singleBlockContentType);
|
||||
|
||||
Assert.AreEqual(0, validationResult.ValidationErrors.Count());
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[TestCase(AllTypes, true)]
|
||||
[TestCase(MetaType, true)]
|
||||
[TestCase(TextType, false)]
|
||||
[Ignore("Reenable when configured block validation is introduced")]
|
||||
public async Task Validates_Configured_Blocks(string elementTypeName, bool shouldPass)
|
||||
{
|
||||
if (elementTypeName != AllTypes && elementTypeName != MetaType && elementTypeName != TextType)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(elementTypeName));
|
||||
}
|
||||
|
||||
var textPageContentType = ContentTypeBuilder.CreateTextPageContentType("myContentType");
|
||||
textPageContentType.AllowedTemplates = [];
|
||||
await ContentTypeService.CreateAsync(textPageContentType, Constants.Security.SuperUserKey);
|
||||
|
||||
var textPage = ContentBuilder.CreateTextpageContent(textPageContentType, "My Picked Content", -1);
|
||||
ContentService.Save(textPage);
|
||||
|
||||
var allTypesType = ContentTypeBuilder.CreateAllTypesContentType("allTypesType", "All Types type");
|
||||
allTypesType.IsElement = true;
|
||||
await ContentTypeService.CreateAsync(allTypesType, Constants.Security.SuperUserKey);
|
||||
|
||||
var metaTypesType = ContentTypeBuilder.CreateMetaContentType();
|
||||
metaTypesType.IsElement = true;
|
||||
await ContentTypeService.CreateAsync(metaTypesType, Constants.Security.SuperUserKey);
|
||||
|
||||
var textType = new ContentTypeBuilder()
|
||||
.WithAlias("TextType")
|
||||
.WithName("Text type")
|
||||
.AddPropertyGroup()
|
||||
.WithAlias("content")
|
||||
.WithName("Content")
|
||||
.WithSortOrder(1)
|
||||
.WithSupportsPublishing(true)
|
||||
.AddPropertyType()
|
||||
.WithAlias("title")
|
||||
.WithName("Title")
|
||||
.WithSortOrder(1)
|
||||
.Done()
|
||||
.Done()
|
||||
.Build();
|
||||
textType.IsElement = true;
|
||||
await ContentTypeService.CreateAsync(textType, Constants.Security.SuperUserKey);
|
||||
|
||||
// do not allow textType to be a valid block
|
||||
var singleBlockContentType = await CreateSingleBlockContentTypePage([allTypesType, metaTypesType]);
|
||||
|
||||
var contentElementKey = Guid.NewGuid();
|
||||
var blockValue = new SingleBlockValue
|
||||
{
|
||||
Layout = new Dictionary<string, IEnumerable<IBlockLayoutItem>>
|
||||
{
|
||||
{
|
||||
Constants.PropertyEditors.Aliases.SingleBlock,
|
||||
[
|
||||
new SingleBlockLayoutItem { ContentKey = contentElementKey }
|
||||
]
|
||||
},
|
||||
},
|
||||
ContentData =
|
||||
[
|
||||
elementTypeName == AllTypes
|
||||
? new BlockItemData
|
||||
{
|
||||
Key = contentElementKey,
|
||||
ContentTypeAlias = allTypesType.Alias,
|
||||
ContentTypeKey = allTypesType.Key,
|
||||
Values =
|
||||
[
|
||||
new BlockPropertyValue
|
||||
{
|
||||
Alias = "contentPicker",
|
||||
Value = textPage.GetUdi(),
|
||||
}
|
||||
],
|
||||
}
|
||||
: elementTypeName == MetaType ?
|
||||
new BlockItemData
|
||||
{
|
||||
Key = contentElementKey,
|
||||
ContentTypeAlias = metaTypesType.Alias,
|
||||
ContentTypeKey = metaTypesType.Key,
|
||||
Values =
|
||||
[
|
||||
new BlockPropertyValue
|
||||
{
|
||||
Alias = "metadescription",
|
||||
Value = "something very meta",
|
||||
}
|
||||
],
|
||||
}
|
||||
: new BlockItemData
|
||||
{
|
||||
Key = contentElementKey,
|
||||
ContentTypeAlias = textType.Alias,
|
||||
ContentTypeKey = textType.Key,
|
||||
Values =
|
||||
[
|
||||
new BlockPropertyValue
|
||||
{
|
||||
Alias = "title",
|
||||
Value = "a random title",
|
||||
}
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
var blocksPropertyValue = JsonSerializer.Serialize(blockValue);
|
||||
|
||||
var content = new ContentBuilder()
|
||||
.WithContentType(singleBlockContentType)
|
||||
.WithName("My Blocks")
|
||||
.WithPropertyValues(new { block = blocksPropertyValue })
|
||||
.Build();
|
||||
ContentService.Save(content);
|
||||
|
||||
// convert to updateModel and run validation
|
||||
var updateModel = await ContentEditingModelFactory.CreateFromAsync(content);
|
||||
var validationResult = await ContentValidationService.ValidatePropertiesAsync(updateModel, singleBlockContentType);
|
||||
|
||||
Assert.AreEqual(shouldPass ? 0 : 1, validationResult.ValidationErrors.Count());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// There should be some validation when publishing through the contentEditingService
|
||||
/// </summary>
|
||||
[Test]
|
||||
public async Task Cannot_Select_Multiple_Configured_Blocks()
|
||||
{
|
||||
var textPageContentType = ContentTypeBuilder.CreateTextPageContentType("myContentType");
|
||||
textPageContentType.AllowedTemplates = [];
|
||||
await ContentTypeService.CreateAsync(textPageContentType, Constants.Security.SuperUserKey);
|
||||
|
||||
var textPage = ContentBuilder.CreateTextpageContent(textPageContentType, "My Picked Content", -1);
|
||||
ContentService.Save(textPage);
|
||||
|
||||
var allTypesType = ContentTypeBuilder.CreateAllTypesContentType("allTypesType", "All Types type");
|
||||
allTypesType.IsElement = true;
|
||||
await ContentTypeService.CreateAsync(allTypesType, Constants.Security.SuperUserKey);
|
||||
|
||||
var metaTypesType = ContentTypeBuilder.CreateMetaContentType();
|
||||
metaTypesType.IsElement = true;
|
||||
await ContentTypeService.CreateAsync(metaTypesType, Constants.Security.SuperUserKey);
|
||||
|
||||
var singleBlockContentType = await CreateSingleBlockContentTypePage([allTypesType, metaTypesType]);
|
||||
|
||||
var firstElementKey = Guid.NewGuid();
|
||||
var secondElementKey = Guid.NewGuid();
|
||||
var blockValue = new SingleBlockValue
|
||||
{
|
||||
Layout = new Dictionary<string, IEnumerable<IBlockLayoutItem>>
|
||||
{
|
||||
{
|
||||
Constants.PropertyEditors.Aliases.SingleBlock,
|
||||
[
|
||||
new SingleBlockLayoutItem { ContentKey = firstElementKey },
|
||||
new SingleBlockLayoutItem { ContentKey = secondElementKey }
|
||||
]
|
||||
},
|
||||
},
|
||||
ContentData =
|
||||
[
|
||||
new BlockItemData
|
||||
{
|
||||
Key = firstElementKey,
|
||||
ContentTypeAlias = allTypesType.Alias,
|
||||
ContentTypeKey = allTypesType.Key,
|
||||
Values =
|
||||
[
|
||||
new BlockPropertyValue
|
||||
{
|
||||
Alias = "contentPicker",
|
||||
Value = textPage.GetUdi(),
|
||||
}
|
||||
],
|
||||
},
|
||||
new BlockItemData
|
||||
{
|
||||
Key = secondElementKey,
|
||||
ContentTypeAlias = metaTypesType.Alias,
|
||||
ContentTypeKey = metaTypesType.Key,
|
||||
Values =
|
||||
[
|
||||
new BlockPropertyValue
|
||||
{
|
||||
Alias = "metadescription",
|
||||
Value = "something very meta",
|
||||
}
|
||||
],
|
||||
}
|
||||
],
|
||||
};
|
||||
var blocksPropertyValue = JsonSerializer.Serialize(blockValue);
|
||||
|
||||
var content = new ContentBuilder()
|
||||
.WithContentType(singleBlockContentType)
|
||||
.WithName("My Blocks")
|
||||
.WithPropertyValues(new { block = blocksPropertyValue })
|
||||
.Build();
|
||||
|
||||
// No validation, should just save
|
||||
ContentService.Save(content);
|
||||
|
||||
// convert to updateModel and run validation
|
||||
var updateModel = await ContentEditingModelFactory.CreateFromAsync(content);
|
||||
var validationResult = await ContentValidationService.ValidatePropertiesAsync(updateModel, singleBlockContentType);
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.AreEqual(1, validationResult.ValidationErrors.Count());
|
||||
var validationError = validationResult.ValidationErrors.Single();
|
||||
var expectedErrorMessage = SingleBlockPropertyEditor.SingleBlockEditorPropertyValueEditor
|
||||
.SingleBlockValidator
|
||||
.BuildErrorMessage(LocalizedTextService, 1, 2);
|
||||
Assert.AreEqual("block", validationError.Alias);
|
||||
Assert.AreEqual(expectedErrorMessage, validationError.ErrorMessages.Single());
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Can_Track_References()
|
||||
{
|
||||
var textPageContentType = ContentTypeBuilder.CreateTextPageContentType("myContentType");
|
||||
textPageContentType.AllowedTemplates = [];
|
||||
await ContentTypeService.CreateAsync(textPageContentType, Constants.Security.SuperUserKey);
|
||||
|
||||
var textPage = ContentBuilder.CreateTextpageContent(textPageContentType, "My Picked Content", -1);
|
||||
ContentService.Save(textPage);
|
||||
|
||||
var elementType = ContentTypeBuilder.CreateAllTypesContentType("allTypesType", "All Types type");
|
||||
elementType.IsElement = true;
|
||||
await ContentTypeService.CreateAsync(elementType, Constants.Security.SuperUserKey);
|
||||
|
||||
var singleBlockContentType = await CreateSingleBlockContentTypePage([elementType]);
|
||||
|
||||
var contentElementKey = Guid.NewGuid();
|
||||
var blockValue = new SingleBlockValue
|
||||
{
|
||||
Layout = new Dictionary<string, IEnumerable<IBlockLayoutItem>>
|
||||
{
|
||||
{
|
||||
Constants.PropertyEditors.Aliases.SingleBlock,
|
||||
[
|
||||
new SingleBlockLayoutItem { ContentKey = contentElementKey }
|
||||
]
|
||||
},
|
||||
},
|
||||
ContentData =
|
||||
[
|
||||
new()
|
||||
{
|
||||
Key = contentElementKey,
|
||||
ContentTypeAlias = elementType.Alias,
|
||||
ContentTypeKey = elementType.Key,
|
||||
Values =
|
||||
[
|
||||
new BlockPropertyValue
|
||||
{
|
||||
Alias = "contentPicker",
|
||||
Value = textPage.GetUdi(),
|
||||
}
|
||||
],
|
||||
}
|
||||
],
|
||||
};
|
||||
var blocksPropertyValue = JsonSerializer.Serialize(blockValue);
|
||||
|
||||
var content = new ContentBuilder()
|
||||
.WithContentType(singleBlockContentType)
|
||||
.WithName("My Blocks")
|
||||
.WithPropertyValues(new { block = blocksPropertyValue })
|
||||
.Build();
|
||||
ContentService.Save(content);
|
||||
|
||||
var valueEditor = await GetValueEditor(singleBlockContentType);
|
||||
|
||||
var references = valueEditor.GetReferences(content.GetValue("block")).ToArray();
|
||||
Assert.AreEqual(1, references.Length);
|
||||
var reference = references.First();
|
||||
Assert.AreEqual(Constants.Conventions.RelationTypes.RelatedDocumentAlias, reference.RelationTypeAlias);
|
||||
Assert.AreEqual(textPage.GetUdi(), reference.Udi);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Can_Track_Tags()
|
||||
{
|
||||
var elementType = ContentTypeBuilder.CreateAllTypesContentType("myElementType", "My Element Type");
|
||||
elementType.IsElement = true;
|
||||
await ContentTypeService.CreateAsync(elementType, Constants.Security.SuperUserKey);
|
||||
|
||||
var singleBlockContentType = await CreateSingleBlockContentTypePage([elementType]);
|
||||
|
||||
var contentElementKey = Guid.NewGuid();
|
||||
var blockValue = new SingleBlockValue
|
||||
{
|
||||
Layout = new Dictionary<string, IEnumerable<IBlockLayoutItem>>
|
||||
{
|
||||
{
|
||||
Constants.PropertyEditors.Aliases.SingleBlock,
|
||||
[
|
||||
new SingleBlockLayoutItem { ContentKey = contentElementKey }
|
||||
]
|
||||
},
|
||||
},
|
||||
ContentData =
|
||||
[
|
||||
new()
|
||||
{
|
||||
Key = contentElementKey,
|
||||
ContentTypeAlias = elementType.Alias,
|
||||
ContentTypeKey = elementType.Key,
|
||||
Values =
|
||||
[
|
||||
new ()
|
||||
{
|
||||
Alias = "tags",
|
||||
// this is a little skewed, but the tags editor expects a serialized array of strings
|
||||
Value = JsonSerializer.Serialize(new[] { "Tag One", "Tag Two", "Tag Three" }),
|
||||
}
|
||||
],
|
||||
}
|
||||
],
|
||||
};
|
||||
var blockPropertyValue = JsonSerializer.Serialize(blockValue);
|
||||
|
||||
var content = new ContentBuilder()
|
||||
.WithContentType(singleBlockContentType)
|
||||
.WithName("My Blocks")
|
||||
.WithPropertyValues(new { block = blockPropertyValue })
|
||||
.Build();
|
||||
ContentService.Save(content);
|
||||
|
||||
var valueEditor = await GetValueEditor(singleBlockContentType);
|
||||
|
||||
var tags = valueEditor.GetTags(content.GetValue("block"), null, null).ToArray();
|
||||
Assert.AreEqual(3, tags.Length);
|
||||
Assert.IsNotNull(tags.Single(tag => tag.Text == "Tag One" && tag.LanguageId == null));
|
||||
Assert.IsNotNull(tags.Single(tag => tag.Text == "Tag Two" && tag.LanguageId == null));
|
||||
Assert.IsNotNull(tags.Single(tag => tag.Text == "Tag Three" && tag.LanguageId == null));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Can_Track_Tags_For_Block_Level_Variance()
|
||||
{
|
||||
var result = await LanguageService.CreateAsync(
|
||||
new Language("da-DK", "Danish"), Constants.Security.SuperUserKey);
|
||||
Assert.IsTrue(result.Success);
|
||||
var daDkId = result.Result.Id;
|
||||
|
||||
var elementType = ContentTypeBuilder.CreateAllTypesContentType("myElementType", "My Element Type");
|
||||
elementType.IsElement = true;
|
||||
elementType.Variations = ContentVariation.Culture;
|
||||
elementType.PropertyTypes.First(p => p.Alias == "tags").Variations = ContentVariation.Culture;
|
||||
await ContentTypeService.CreateAsync(elementType, Constants.Security.SuperUserKey);
|
||||
|
||||
var singleBlockContentType = await CreateSingleBlockContentTypePage([elementType]);
|
||||
singleBlockContentType.Variations = ContentVariation.Culture;
|
||||
await ContentTypeService.CreateAsync(singleBlockContentType, Constants.Security.SuperUserKey);
|
||||
|
||||
var contentElementKey = Guid.NewGuid();
|
||||
var blockValue = new SingleBlockValue
|
||||
{
|
||||
Layout = new Dictionary<string, IEnumerable<IBlockLayoutItem>>
|
||||
{
|
||||
{
|
||||
Constants.PropertyEditors.Aliases.SingleBlock,
|
||||
[
|
||||
new SingleBlockLayoutItem { ContentKey = contentElementKey }
|
||||
]
|
||||
},
|
||||
},
|
||||
ContentData =
|
||||
[
|
||||
new()
|
||||
{
|
||||
Key = contentElementKey,
|
||||
ContentTypeAlias = elementType.Alias,
|
||||
ContentTypeKey = elementType.Key,
|
||||
Values =
|
||||
[
|
||||
new()
|
||||
{
|
||||
Alias = "tags",
|
||||
// this is a little skewed, but the tags editor expects a serialized array of strings
|
||||
Value = JsonSerializer.Serialize(new[] { "Tag One EN", "Tag Two EN", "Tag Three EN" }),
|
||||
Culture = "en-US",
|
||||
},
|
||||
new()
|
||||
{
|
||||
Alias = "tags",
|
||||
// this is a little skewed, but the tags editor expects a serialized array of strings
|
||||
Value = JsonSerializer.Serialize(new[] { "Tag One DA", "Tag Two DA", "Tag Three DA" }),
|
||||
Culture = "da-DK",
|
||||
}
|
||||
],
|
||||
}
|
||||
],
|
||||
};
|
||||
var blockPropertyValue = JsonSerializer.Serialize(blockValue);
|
||||
|
||||
var content = new ContentBuilder()
|
||||
.WithContentType(singleBlockContentType)
|
||||
.WithCultureName("en-US", "My Blocks EN")
|
||||
.WithCultureName("da-DK", "My Blocks DA")
|
||||
.WithPropertyValues(new { block = blockPropertyValue })
|
||||
.Build();
|
||||
ContentService.Save(content);
|
||||
|
||||
var valueEditor = await GetValueEditor(singleBlockContentType);
|
||||
|
||||
var tags = valueEditor.GetTags(content.GetValue("block"), null, null).ToArray();
|
||||
Assert.AreEqual(6, tags.Length);
|
||||
Assert.IsNotNull(tags.Single(tag => tag.Text == "Tag One EN" && tag.LanguageId == 1));
|
||||
Assert.IsNotNull(tags.Single(tag => tag.Text == "Tag Two EN" && tag.LanguageId == 1));
|
||||
Assert.IsNotNull(tags.Single(tag => tag.Text == "Tag Three EN" && tag.LanguageId == 1));
|
||||
Assert.IsNotNull(tags.Single(tag => tag.Text == "Tag One DA" && tag.LanguageId == daDkId));
|
||||
Assert.IsNotNull(tags.Single(tag => tag.Text == "Tag Two DA" && tag.LanguageId == daDkId));
|
||||
Assert.IsNotNull(tags.Single(tag => tag.Text == "Tag Three DA" && tag.LanguageId == daDkId));
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Can_Handle_Culture_Variance_Addition()
|
||||
{
|
||||
var elementType = ContentTypeBuilder.CreateAllTypesContentType("myElementType", "My Element Type");
|
||||
elementType.IsElement = true;
|
||||
await ContentTypeService.CreateAsync(elementType, Constants.Security.SuperUserKey);
|
||||
|
||||
var singleBlockContentType = await CreateSingleBlockContentTypePage([elementType]);
|
||||
|
||||
var contentElementKey = Guid.NewGuid();
|
||||
var blockValue = new SingleBlockValue
|
||||
{
|
||||
Layout = new Dictionary<string, IEnumerable<IBlockLayoutItem>>
|
||||
{
|
||||
{
|
||||
Constants.PropertyEditors.Aliases.SingleBlock,
|
||||
[
|
||||
new SingleBlockLayoutItem { ContentKey = contentElementKey }
|
||||
]
|
||||
},
|
||||
},
|
||||
ContentData =
|
||||
[
|
||||
new()
|
||||
{
|
||||
Key = contentElementKey,
|
||||
ContentTypeAlias = elementType.Alias,
|
||||
ContentTypeKey = elementType.Key,
|
||||
Values =
|
||||
[
|
||||
new ()
|
||||
{
|
||||
Alias = "singleLineText",
|
||||
Value = "The single line text",
|
||||
}
|
||||
],
|
||||
}
|
||||
],
|
||||
Expose =
|
||||
[
|
||||
new BlockItemVariation(contentElementKey, null, null)
|
||||
],
|
||||
};
|
||||
var blockPropertyValue = JsonSerializer.Serialize(blockValue);
|
||||
|
||||
var content = new ContentBuilder()
|
||||
.WithContentType(singleBlockContentType)
|
||||
.WithName("My Blocks")
|
||||
.WithPropertyValues(new { block = blockPropertyValue })
|
||||
.Build();
|
||||
ContentService.Save(content);
|
||||
|
||||
elementType.Variations = ContentVariation.Culture;
|
||||
elementType.PropertyTypes.First(pt => pt.Alias == "singleLineText").Variations = ContentVariation.Culture;
|
||||
ContentTypeService.Save(elementType);
|
||||
|
||||
var valueEditor = await GetValueEditor(singleBlockContentType);
|
||||
var toEditorValue = valueEditor.ToEditor(content.Properties["block"]!) as SingleBlockValue;
|
||||
Assert.IsNotNull(toEditorValue);
|
||||
Assert.AreEqual(1, toEditorValue.ContentData.Count);
|
||||
|
||||
var properties = toEditorValue.ContentData.First().Values;
|
||||
Assert.AreEqual(1, properties.Count);
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
var property = properties.First();
|
||||
Assert.AreEqual("singleLineText", property.Alias);
|
||||
Assert.AreEqual("The single line text", property.Value);
|
||||
Assert.AreEqual("en-US", property.Culture);
|
||||
});
|
||||
|
||||
Assert.AreEqual(1, toEditorValue.Expose.Count);
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
var itemVariation = toEditorValue.Expose[0];
|
||||
Assert.AreEqual(contentElementKey, itemVariation.ContentKey);
|
||||
Assert.AreEqual("en-US", itemVariation.Culture);
|
||||
});
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Can_Handle_Culture_Variance_Removal()
|
||||
{
|
||||
var elementType = ContentTypeBuilder.CreateAllTypesContentType("myElementType", "My Element Type");
|
||||
elementType.IsElement = true;
|
||||
elementType.Variations = ContentVariation.Culture;
|
||||
elementType.PropertyTypes.First(pt => pt.Alias == "singleLineText").Variations = ContentVariation.Culture;
|
||||
await ContentTypeService.CreateAsync(elementType, Constants.Security.SuperUserKey);
|
||||
|
||||
var singleBlockContentType = await CreateSingleBlockContentTypePage([elementType]);
|
||||
|
||||
var contentElementKey = Guid.NewGuid();
|
||||
var blockValue = new SingleBlockValue
|
||||
{
|
||||
Layout = new Dictionary<string, IEnumerable<IBlockLayoutItem>>
|
||||
{
|
||||
{
|
||||
Constants.PropertyEditors.Aliases.SingleBlock,
|
||||
[
|
||||
new SingleBlockLayoutItem { ContentKey = contentElementKey },
|
||||
]
|
||||
},
|
||||
},
|
||||
ContentData =
|
||||
[
|
||||
new()
|
||||
{
|
||||
Key = contentElementKey,
|
||||
ContentTypeAlias = elementType.Alias,
|
||||
ContentTypeKey = elementType.Key,
|
||||
Values =
|
||||
[
|
||||
new ()
|
||||
{
|
||||
Alias = "singleLineText",
|
||||
Value = "The single line text",
|
||||
Culture = "en-US",
|
||||
}
|
||||
],
|
||||
}
|
||||
],
|
||||
Expose =
|
||||
[
|
||||
new BlockItemVariation(contentElementKey, "en-US", null)
|
||||
],
|
||||
};
|
||||
var blockPropertyValue = JsonSerializer.Serialize(blockValue);
|
||||
|
||||
var content = new ContentBuilder()
|
||||
.WithContentType(singleBlockContentType)
|
||||
.WithName("My Blocks")
|
||||
.WithPropertyValues(new { block = blockPropertyValue })
|
||||
.Build();
|
||||
ContentService.Save(content);
|
||||
|
||||
elementType.PropertyTypes.First(pt => pt.Alias == "singleLineText").Variations = ContentVariation.Nothing;
|
||||
elementType.Variations = ContentVariation.Nothing;
|
||||
await ContentTypeService.SaveAsync(elementType, Constants.Security.SuperUserKey);
|
||||
|
||||
var valueEditor = await GetValueEditor(singleBlockContentType);
|
||||
var toEditorValue = valueEditor.ToEditor(content.Properties["block"]!) as SingleBlockValue;
|
||||
Assert.IsNotNull(toEditorValue);
|
||||
Assert.AreEqual(1, toEditorValue.ContentData.Count);
|
||||
|
||||
var properties = toEditorValue.ContentData.First().Values;
|
||||
Assert.AreEqual(1, properties.Count);
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
var property = properties.First();
|
||||
Assert.AreEqual("singleLineText", property.Alias);
|
||||
Assert.AreEqual("The single line text", property.Value);
|
||||
Assert.AreEqual(null, property.Culture);
|
||||
});
|
||||
|
||||
Assert.AreEqual(1, toEditorValue.Expose.Count);
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
var itemVariation = toEditorValue.Expose[0];
|
||||
Assert.AreEqual(contentElementKey, itemVariation.ContentKey);
|
||||
Assert.AreEqual(null, itemVariation.Culture);
|
||||
});
|
||||
}
|
||||
|
||||
private async Task<IContentType> CreateSingleBlockContentTypePage(IContentType[] allowedElementTypes)
|
||||
{
|
||||
var singleBlockDataType = new DataType(PropertyEditorCollection[Constants.PropertyEditors.Aliases.SingleBlock], ConfigurationEditorJsonSerializer)
|
||||
{
|
||||
ConfigurationData = new Dictionary<string, object>
|
||||
{
|
||||
{
|
||||
"blocks",
|
||||
allowedElementTypes.Select(allowedElementType =>
|
||||
new BlockListConfiguration.BlockConfiguration
|
||||
{
|
||||
ContentElementTypeKey = allowedElementType.Key,
|
||||
}).ToArray()
|
||||
},
|
||||
},
|
||||
Name = "My Single Block",
|
||||
DatabaseType = ValueStorageType.Ntext,
|
||||
ParentId = Constants.System.Root,
|
||||
CreateDate = DateTime.UtcNow
|
||||
};
|
||||
|
||||
await DataTypeService.CreateAsync(singleBlockDataType, Constants.Security.SuperUserKey);
|
||||
|
||||
var contentType = new ContentTypeBuilder()
|
||||
.WithAlias("myPage")
|
||||
.WithName("My Page")
|
||||
.AddPropertyType()
|
||||
.WithAlias("block")
|
||||
.WithName("Block")
|
||||
.WithDataTypeId(singleBlockDataType.Id)
|
||||
.Done()
|
||||
.Build();
|
||||
ContentTypeService.Save(contentType);
|
||||
// re-fetch to wire up all key bindings (particularly to the datatype)
|
||||
return await ContentTypeService.GetAsync(contentType.Key);
|
||||
}
|
||||
|
||||
private async Task<SingleBlockPropertyEditor.SingleBlockEditorPropertyValueEditor> GetValueEditor(IContentType contentType)
|
||||
{
|
||||
var dataType = await DataTypeService.GetAsync(contentType.PropertyTypes.First(propertyType => propertyType.Alias == "block").DataTypeKey);
|
||||
Assert.IsNotNull(dataType?.Editor);
|
||||
var valueEditor = dataType.Editor.GetValueEditor() as SingleBlockPropertyEditor.SingleBlockEditorPropertyValueEditor;
|
||||
Assert.IsNotNull(valueEditor);
|
||||
|
||||
return valueEditor;
|
||||
}
|
||||
}
|
||||
@@ -132,7 +132,7 @@ public class TypeLoaderTests
|
||||
public void GetDataEditors()
|
||||
{
|
||||
var types = _typeLoader.GetDataEditors();
|
||||
Assert.AreEqual(36, types.Count());
|
||||
Assert.AreEqual(37, types.Count());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
Reference in New Issue
Block a user