Skip to content

Commit dd01a56

Browse files
Migaroezkjac
andauthored
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 <[email protected]>
1 parent eea970a commit dd01a56

File tree

19 files changed

+1488
-5
lines changed

19 files changed

+1488
-5
lines changed

src/Umbraco.Core/Constants-PropertyEditors.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,11 @@ public static class Aliases
4040
/// </summary>
4141
public const string BlockList = "Umbraco.BlockList";
4242

43+
/// <summary>
44+
/// Block List.
45+
/// </summary>
46+
public const string SingleBlock = "Umbraco.SingleBlock";
47+
4348
/// <summary>
4449
/// Block Grid.
4550
/// </summary>
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
using Umbraco.Cms.Core.Serialization;
2+
3+
namespace Umbraco.Cms.Core.Models.Blocks;
4+
5+
/// <summary>
6+
/// Data converter for the single block property editor
7+
/// </summary>
8+
public class SingleBlockEditorDataConverter : BlockEditorDataConverter<SingleBlockValue, SingleBlockLayoutItem>
9+
{
10+
public SingleBlockEditorDataConverter(IJsonSerializer jsonSerializer)
11+
: base(jsonSerializer)
12+
{
13+
}
14+
15+
protected override IEnumerable<ContentAndSettingsReference> GetBlockReferences(IEnumerable<SingleBlockLayoutItem> layout)
16+
=> layout.Select(x => new ContentAndSettingsReference(x.ContentKey, x.SettingsKey)).ToList();
17+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
using Umbraco.Cms.Core.PropertyEditors;
2+
3+
namespace Umbraco.Cms.Core.Models.Blocks;
4+
5+
public class SingleBlockLayoutItem : BlockLayoutItemBase
6+
{
7+
}
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
using System.Text.Json.Serialization;
2+
3+
namespace Umbraco.Cms.Core.Models.Blocks;
4+
5+
/// <summary>
6+
/// Represents a single block value.
7+
/// </summary>
8+
public class SingleBlockValue : BlockValue<SingleBlockLayoutItem>
9+
{
10+
/// <summary>
11+
/// Initializes a new instance of the <see cref="SingleBlockValue" /> class.
12+
/// </summary>
13+
public SingleBlockValue()
14+
{ }
15+
16+
/// <summary>
17+
/// Initializes a new instance of the <see cref="SingleBlockValue" /> class.
18+
/// </summary>
19+
/// <param name="layout">The layout.</param>
20+
public SingleBlockValue(SingleBlockLayoutItem layout)
21+
=> Layout[PropertyEditorAlias] = [layout];
22+
23+
/// <inheritdoc />
24+
[JsonIgnore]
25+
public override string PropertyEditorAlias => Constants.PropertyEditors.Aliases.SingleBlock;
26+
}

src/Umbraco.Core/PropertyEditors/BlockListConfiguration.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ public class BlockListConfiguration
1515
public NumberRange ValidationLimit { get; set; } = new();
1616

1717
[ConfigurationField("useSingleBlockMode")]
18+
[Obsolete("Use SingleBlockPropertyEditor and its configuration instead")]
1819
public bool UseSingleBlockMode { get; set; }
1920

2021
public class BlockConfiguration : IBlockConfiguration
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
// Copyright (c) Umbraco.
2+
// See LICENSE for more details.
3+
4+
namespace Umbraco.Cms.Core.PropertyEditors;
5+
6+
/// <summary>
7+
/// The configuration object for the Single Block editor
8+
/// </summary>
9+
public class SingleBlockConfiguration
10+
{
11+
[ConfigurationField("blocks")]
12+
public BlockListConfiguration.BlockConfiguration[] Blocks { get; set; } = [];
13+
}

src/Umbraco.Infrastructure/PropertyEditors/BlockEditorMinMaxValidatorBase.cs

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,16 @@ internal abstract class BlockEditorMinMaxValidatorBase<TValue, TLayout> : IValue
2929
/// <inheritdoc/>
3030
public abstract IEnumerable<ValidationResult> Validate(object? value, string? valueType, object? dataTypeConfiguration, PropertyValidationContext validationContext);
3131

32+
// internal method so we can test for specific error messages being returned without keeping strings in sync
33+
internal static string BuildErrorMessage(
34+
ILocalizedTextService textService,
35+
int? maxNumberOfBlocks,
36+
int numberOfBlocks)
37+
=> textService.Localize(
38+
"validation",
39+
"entriesExceed",
40+
[maxNumberOfBlocks.ToString(), (numberOfBlocks - maxNumberOfBlocks).ToString(),]);
41+
3242
/// <summary>
3343
/// Validates the number of blocks are within the configured minimum and maximum values.
3444
/// </summary>
@@ -53,10 +63,7 @@ protected IEnumerable<ValidationResult> ValidateNumberOfBlocks(BlockEditorData<T
5363
if (blockEditorData != null && max.HasValue && numberOfBlocks > max)
5464
{
5565
yield return new ValidationResult(
56-
TextService.Localize(
57-
"validation",
58-
"entriesExceed",
59-
[max.ToString(), (numberOfBlocks - max).ToString(),]),
66+
BuildErrorMessage(TextService, max, numberOfBlocks),
6067
["value"]);
6168
}
6269
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
using Umbraco.Cms.Core.IO;
2+
using Umbraco.Cms.Core.PropertyEditors;
3+
4+
namespace Umbraco.Cms.Infrastructure.PropertyEditors;
5+
6+
internal sealed class SingleBlockConfigurationEditor : ConfigurationEditor<SingleBlockConfiguration>
7+
{
8+
public SingleBlockConfigurationEditor(IIOHelper ioHelper)
9+
: base(ioHelper)
10+
{
11+
}
12+
}
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
using System.ComponentModel.DataAnnotations;
2+
using Microsoft.Extensions.Logging;
3+
using Umbraco.Cms.Core;
4+
using Umbraco.Cms.Core.Cache;
5+
using Umbraco.Cms.Core.Cache.PropertyEditors;
6+
using Umbraco.Cms.Core.IO;
7+
using Umbraco.Cms.Core.Models;
8+
using Umbraco.Cms.Core.Models.Blocks;
9+
using Umbraco.Cms.Core.Models.Validation;
10+
using Umbraco.Cms.Core.PropertyEditors;
11+
using Umbraco.Cms.Core.PropertyEditors.ValueConverters;
12+
using Umbraco.Cms.Core.Serialization;
13+
using Umbraco.Cms.Core.Services;
14+
using Umbraco.Cms.Core.Strings;
15+
using Umbraco.Extensions;
16+
17+
namespace Umbraco.Cms.Infrastructure.PropertyEditors;
18+
19+
/// <summary>
20+
/// Represents a single block property editor.
21+
/// </summary>
22+
[DataEditor(
23+
Constants.PropertyEditors.Aliases.SingleBlock,
24+
ValueType = ValueTypes.Json,
25+
ValueEditorIsReusable = false)]
26+
public class SingleBlockPropertyEditor : DataEditor
27+
{
28+
private readonly IJsonSerializer _jsonSerializer;
29+
private readonly IIOHelper _ioHelper;
30+
private readonly IBlockValuePropertyIndexValueFactory _blockValuePropertyIndexValueFactory;
31+
32+
public SingleBlockPropertyEditor(
33+
IDataValueEditorFactory dataValueEditorFactory,
34+
IJsonSerializer jsonSerializer,
35+
IIOHelper ioHelper,
36+
IBlockValuePropertyIndexValueFactory blockValuePropertyIndexValueFactory)
37+
: base(dataValueEditorFactory)
38+
{
39+
_jsonSerializer = jsonSerializer;
40+
_ioHelper = ioHelper;
41+
_blockValuePropertyIndexValueFactory = blockValuePropertyIndexValueFactory;
42+
}
43+
44+
public override IPropertyIndexValueFactory PropertyIndexValueFactory => _blockValuePropertyIndexValueFactory;
45+
46+
/// <inheritdoc/>
47+
public override bool SupportsConfigurableElements => true;
48+
49+
/// <summary>
50+
/// Instantiates a new <see cref="BlockEditorDataConverter{SingleBlockValue, SingleBlockLayoutItem}"/> for use with the single block editor property value editor.
51+
/// </summary>
52+
/// <returns>A new instance of <see cref="SingleBlockEditorDataConverter"/>.</returns>
53+
protected virtual BlockEditorDataConverter<SingleBlockValue, SingleBlockLayoutItem> CreateBlockEditorDataConverter()
54+
=> new SingleBlockEditorDataConverter(_jsonSerializer);
55+
56+
/// <inheritdoc/>
57+
protected override IDataValueEditor CreateValueEditor() =>
58+
DataValueEditorFactory.Create<SingleBlockEditorPropertyValueEditor>(Attribute!, CreateBlockEditorDataConverter());
59+
60+
/// <inheritdoc />
61+
public override bool CanMergePartialPropertyValues(IPropertyType propertyType) => propertyType.VariesByCulture() is false;
62+
63+
/// <inheritdoc />
64+
public override object? MergePartialPropertyValueForCulture(object? sourceValue, object? targetValue, string? culture)
65+
{
66+
var valueEditor = (SingleBlockEditorPropertyValueEditor)GetValueEditor();
67+
return valueEditor.MergePartialPropertyValueForCulture(sourceValue, targetValue, culture);
68+
}
69+
70+
/// <inheritdoc/>
71+
protected override IConfigurationEditor CreateConfigurationEditor() =>
72+
new SingleBlockConfigurationEditor(_ioHelper);
73+
74+
/// <inheritdoc/>
75+
public override object? MergeVariantInvariantPropertyValue(
76+
object? sourceValue,
77+
object? targetValue,
78+
bool canUpdateInvariantData,
79+
HashSet<string> allowedCultures)
80+
{
81+
var valueEditor = (SingleBlockEditorPropertyValueEditor)GetValueEditor();
82+
return valueEditor.MergeVariantInvariantPropertyValue(sourceValue, targetValue, canUpdateInvariantData, allowedCultures);
83+
}
84+
85+
internal sealed class SingleBlockEditorPropertyValueEditor : BlockEditorPropertyValueEditor<SingleBlockValue, SingleBlockLayoutItem>
86+
{
87+
public SingleBlockEditorPropertyValueEditor(
88+
DataEditorAttribute attribute,
89+
BlockEditorDataConverter<SingleBlockValue, SingleBlockLayoutItem> blockEditorDataConverter,
90+
PropertyEditorCollection propertyEditors,
91+
DataValueReferenceFactoryCollection dataValueReferenceFactories,
92+
IDataTypeConfigurationCache dataTypeConfigurationCache,
93+
IShortStringHelper shortStringHelper,
94+
IJsonSerializer jsonSerializer,
95+
BlockEditorVarianceHandler blockEditorVarianceHandler,
96+
ILanguageService languageService,
97+
IIOHelper ioHelper,
98+
IBlockEditorElementTypeCache elementTypeCache,
99+
ILogger<SingleBlockEditorPropertyValueEditor> logger,
100+
ILocalizedTextService textService,
101+
IPropertyValidationService propertyValidationService)
102+
: base(propertyEditors, dataValueReferenceFactories, dataTypeConfigurationCache, shortStringHelper, jsonSerializer, blockEditorVarianceHandler, languageService, ioHelper, attribute)
103+
{
104+
BlockEditorValues = new BlockEditorValues<SingleBlockValue, SingleBlockLayoutItem>(blockEditorDataConverter, elementTypeCache, logger);
105+
Validators.Add(new BlockEditorValidator<SingleBlockValue, SingleBlockLayoutItem>(propertyValidationService, BlockEditorValues, elementTypeCache));
106+
Validators.Add(new SingleBlockValidator(BlockEditorValues, textService));
107+
}
108+
109+
protected override SingleBlockValue CreateWithLayout(IEnumerable<SingleBlockLayoutItem> layout) =>
110+
new(layout.Single());
111+
112+
/// <inheritdoc/>
113+
public override IEnumerable<Guid> ConfiguredElementTypeKeys()
114+
{
115+
var configuration = ConfigurationObject as SingleBlockConfiguration;
116+
return configuration?.Blocks.SelectMany(ConfiguredElementTypeKeys) ?? Enumerable.Empty<Guid>();
117+
}
118+
119+
/// <summary>
120+
/// Validates whether the block editor holds a single value
121+
/// </summary>
122+
internal sealed class SingleBlockValidator : BlockEditorMinMaxValidatorBase<SingleBlockValue, SingleBlockLayoutItem>
123+
{
124+
private readonly BlockEditorValues<SingleBlockValue, SingleBlockLayoutItem> _blockEditorValues;
125+
126+
public SingleBlockValidator(BlockEditorValues<SingleBlockValue, SingleBlockLayoutItem> blockEditorValues, ILocalizedTextService textService)
127+
: base(textService) =>
128+
_blockEditorValues = blockEditorValues;
129+
130+
public override IEnumerable<ValidationResult> Validate(object? value, string? valueType, object? dataTypeConfiguration, PropertyValidationContext validationContext)
131+
{
132+
BlockEditorData<SingleBlockValue, SingleBlockLayoutItem>? blockEditorData = _blockEditorValues.DeserializeAndClean(value);
133+
134+
return ValidateNumberOfBlocks(blockEditorData, 0, 1);
135+
}
136+
}
137+
}
138+
}
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
// Copyright (c) Umbraco.
2+
// See LICENSE for more details.
3+
4+
using Umbraco.Cms.Core;
5+
using Umbraco.Cms.Core.DeliveryApi;
6+
using Umbraco.Cms.Core.Logging;
7+
using Umbraco.Cms.Core.Models.Blocks;
8+
using Umbraco.Cms.Core.Models.DeliveryApi;
9+
using Umbraco.Cms.Core.Models.PublishedContent;
10+
using Umbraco.Cms.Core.PropertyEditors;
11+
using Umbraco.Cms.Core.PropertyEditors.DeliveryApi;
12+
using Umbraco.Cms.Core.PropertyEditors.ValueConverters;
13+
using Umbraco.Cms.Core.Serialization;
14+
using Umbraco.Cms.Infrastructure.Extensions;
15+
using Umbraco.Extensions;
16+
17+
namespace Umbraco.Cms.Infrastructure.PropertyEditors.ValueConverters;
18+
19+
[DefaultPropertyValueConverter(typeof(JsonValueConverter))]
20+
public class SingleBlockPropertyValueConverter : PropertyValueConverterBase, IDeliveryApiPropertyValueConverter
21+
{
22+
private readonly IProfilingLogger _proflog;
23+
private readonly BlockEditorConverter _blockConverter;
24+
private readonly IApiElementBuilder _apiElementBuilder;
25+
private readonly IJsonSerializer _jsonSerializer;
26+
private readonly BlockListPropertyValueConstructorCache _constructorCache;
27+
private readonly IVariationContextAccessor _variationContextAccessor;
28+
private readonly BlockEditorVarianceHandler _blockEditorVarianceHandler;
29+
30+
public SingleBlockPropertyValueConverter(
31+
IProfilingLogger proflog,
32+
BlockEditorConverter blockConverter,
33+
IApiElementBuilder apiElementBuilder,
34+
IJsonSerializer jsonSerializer,
35+
BlockListPropertyValueConstructorCache constructorCache,
36+
IVariationContextAccessor variationContextAccessor,
37+
BlockEditorVarianceHandler blockEditorVarianceHandler)
38+
{
39+
_proflog = proflog;
40+
_blockConverter = blockConverter;
41+
_apiElementBuilder = apiElementBuilder;
42+
_jsonSerializer = jsonSerializer;
43+
_constructorCache = constructorCache;
44+
_variationContextAccessor = variationContextAccessor;
45+
_blockEditorVarianceHandler = blockEditorVarianceHandler;
46+
}
47+
48+
/// <inheritdoc />
49+
public override bool IsConverter(IPublishedPropertyType propertyType)
50+
=> propertyType.EditorAlias.InvariantEquals(Constants.PropertyEditors.Aliases.SingleBlock);
51+
52+
/// <inheritdoc />
53+
public override Type GetPropertyValueType(IPublishedPropertyType propertyType) => typeof( BlockListItem);
54+
55+
/// <inheritdoc />
56+
public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType)
57+
=> PropertyCacheLevel.Element;
58+
59+
/// <inheritdoc />
60+
public override object? ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object? source, bool preview)
61+
=> source?.ToString();
62+
63+
/// <inheritdoc />
64+
public override object? ConvertIntermediateToObject(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object? inter, bool preview)
65+
{
66+
// 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
67+
using (!_proflog.IsEnabled(Core.Logging.LogLevel.Debug) ? null : _proflog.DebugDuration<BlockListPropertyValueConverter>(
68+
$"ConvertPropertyToBlockList ({propertyType.DataType.Id})"))
69+
{
70+
return ConvertIntermediateToBlockListItem(owner, propertyType, referenceCacheLevel, inter, preview);
71+
}
72+
}
73+
74+
/// <inheritdoc />
75+
public PropertyCacheLevel GetDeliveryApiPropertyCacheLevel(IPublishedPropertyType propertyType) => GetPropertyCacheLevel(propertyType);
76+
77+
/// <inheritdoc />
78+
public PropertyCacheLevel GetDeliveryApiPropertyCacheLevelForExpansion(IPublishedPropertyType propertyType) => PropertyCacheLevel.Snapshot;
79+
80+
/// <inheritdoc />
81+
public Type GetDeliveryApiPropertyValueType(IPublishedPropertyType propertyType)
82+
=> typeof(ApiBlockItem);
83+
84+
/// <inheritdoc />
85+
public object? ConvertIntermediateToDeliveryApiObject(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object? inter, bool preview, bool expanding)
86+
{
87+
BlockListItem? model = ConvertIntermediateToBlockListItem(owner, propertyType, referenceCacheLevel, inter, preview);
88+
89+
return
90+
model?.CreateApiBlockItem(_apiElementBuilder);
91+
}
92+
93+
private BlockListItem? ConvertIntermediateToBlockListItem(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object? inter, bool preview)
94+
{
95+
using (!_proflog.IsEnabled(LogLevel.Debug) ? null : _proflog.DebugDuration<SingleBlockPropertyValueConverter>(
96+
$"ConvertPropertyToSingleBlock ({propertyType.DataType.Id})"))
97+
{
98+
// 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
99+
if (inter is not string intermediateBlockModelValue)
100+
{
101+
return null;
102+
}
103+
104+
// Get configuration
105+
SingleBlockConfiguration? configuration = propertyType.DataType.ConfigurationAs<SingleBlockConfiguration>();
106+
if (configuration is null)
107+
{
108+
return null;
109+
}
110+
111+
112+
var creator = new SingleBlockPropertyValueCreator(_blockConverter, _variationContextAccessor, _blockEditorVarianceHandler, _jsonSerializer, _constructorCache);
113+
return creator.CreateBlockModel(owner, referenceCacheLevel, intermediateBlockModelValue, preview, configuration.Blocks);
114+
}
115+
}
116+
}

0 commit comments

Comments
 (0)