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