Block level variants - search indexing (#17239)

* Support block level variance for search indexing

* Rename base class

---------

Co-authored-by: Elitsa <elm@umbraco.dk>
This commit is contained in:
Kenn Jacobsen
2024-10-20 15:42:13 +02:00
committed by GitHub
parent 0a0cf73e3c
commit bafcc2b945
15 changed files with 1143 additions and 347 deletions

View File

@@ -1,5 +1,4 @@
using Umbraco.Cms.Core.Models;
using Umbraco.Extensions;
namespace Umbraco.Cms.Core.PropertyEditors;
@@ -9,11 +8,15 @@ namespace Umbraco.Cms.Core.PropertyEditors;
/// </summary>
public class DefaultPropertyIndexValueFactory : IPropertyIndexValueFactory
{
public IEnumerable<KeyValuePair<string, IEnumerable<object?>>> GetIndexValues(IProperty property, string? culture, string? segment, bool published,
public IEnumerable<IndexValue> GetIndexValues(IProperty property, string? culture, string? segment, bool published,
IEnumerable<string> availableCultures, IDictionary<Guid, IContentType> contentTypeDictionary)
{
yield return new KeyValuePair<string, IEnumerable<object?>>(
property.Alias,
property.GetValue(culture, segment, published).Yield());
}
=>
[
new IndexValue
{
Culture = culture,
FieldName = property.Alias,
Values = [property.GetValue(culture, segment, published)]
}
];
}

View File

@@ -12,9 +12,9 @@ public interface IPropertyIndexValueFactory
/// </summary>
/// <remarks>
/// <para>
/// Returns key-value pairs, where keys are indexed field names. By default, that would be the property alias,
/// and there would be only one pair, but some implementations (see for instance the grid one) may return more than
/// one pair, with different indexed field names.
/// Returns index values for a given property. By default, a property uses its alias as index field name,
/// and there would be only one index value, but some implementations (see for instance the grid one) may return more than
/// one value, with different indexed field names.
/// </para>
/// <para>
/// And then, values are an enumerable of objects, because each indexed field can in turn have multiple
@@ -22,7 +22,7 @@ public interface IPropertyIndexValueFactory
/// more than one value for a given field.
/// </para>
/// </remarks>
IEnumerable<KeyValuePair<string, IEnumerable<object?>>> GetIndexValues(
IEnumerable<IndexValue> GetIndexValues(
IProperty property,
string? culture,
string? segment,

View File

@@ -0,0 +1,10 @@
namespace Umbraco.Cms.Core.PropertyEditors;
public sealed class IndexValue
{
public required string? Culture { get; set; }
public required string FieldName { get; set; }
public required IEnumerable<object?> Values { get; set; }
}

View File

@@ -27,7 +27,7 @@ public abstract class JsonPropertyIndexValueFactoryBase<TSerialized> : IProperty
indexingSettings.OnChange(newValue => _indexingSettings = newValue);
}
public virtual IEnumerable<KeyValuePair<string, IEnumerable<object?>>> GetIndexValues(
public virtual IEnumerable<IndexValue> GetIndexValues(
IProperty property,
string? culture,
string? segment,
@@ -35,7 +35,7 @@ public abstract class JsonPropertyIndexValueFactoryBase<TSerialized> : IProperty
IEnumerable<string> availableCultures,
IDictionary<Guid, IContentType> contentTypeDictionary)
{
var result = new List<KeyValuePair<string, IEnumerable<object?>>>();
var result = new List<IndexValue>();
var propertyValue = property.GetValue(culture, segment, published);
@@ -65,7 +65,7 @@ public abstract class JsonPropertyIndexValueFactoryBase<TSerialized> : IProperty
}
}
IEnumerable<KeyValuePair<string, IEnumerable<object?>>> summary = HandleResume(result, property, culture, segment, published);
IEnumerable<IndexValue> summary = HandleResume(result, property, culture, segment, published);
if (_indexingSettings.ExplicitlyIndexEachNestedProperty || ForceExplicitlyIndexEachNestedProperty)
{
result.AddRange(summary);
@@ -78,17 +78,17 @@ public abstract class JsonPropertyIndexValueFactoryBase<TSerialized> : IProperty
/// <summary>
/// Method to return a list of summary of the content. By default this returns an empty list
/// </summary>
protected virtual IEnumerable<KeyValuePair<string, IEnumerable<object?>>> HandleResume(
List<KeyValuePair<string, IEnumerable<object?>>> result,
protected virtual IEnumerable<IndexValue> HandleResume(
List<IndexValue> result,
IProperty property,
string? culture,
string? segment,
bool published) => Array.Empty<KeyValuePair<string, IEnumerable<object?>>>();
bool published) => Array.Empty<IndexValue>();
/// <summary>
/// Method that handle the deserialized object.
/// </summary>
protected abstract IEnumerable<KeyValuePair<string, IEnumerable<object?>>> Handle(
protected abstract IEnumerable<IndexValue> Handle(
TSerialized deserializedPropertyValue,
IProperty property,
string? culture,

View File

@@ -8,7 +8,7 @@ namespace Umbraco.Cms.Core.PropertyEditors;
public class NoopPropertyIndexValueFactory : IPropertyIndexValueFactory
{
/// <inheritdoc />
public IEnumerable<KeyValuePair<string, IEnumerable<object?>>> GetIndexValues(IProperty property, string? culture, string? segment, bool published,
public IEnumerable<IndexValue> GetIndexValues(IProperty property, string? culture, string? segment, bool published,
IEnumerable<string> availableCultures, IDictionary<Guid, IContentType> contentTypeDictionary)
=> Array.Empty<KeyValuePair<string, IEnumerable<object?>>>();
=> [];
}

View File

@@ -19,17 +19,7 @@ public class TagPropertyIndexValueFactory : JsonPropertyIndexValueFactoryBase<st
indexingSettings.OnChange(newValue => _indexingSettings = newValue);
}
[Obsolete("Use the overload with the 'contentTypeDictionary' parameter instead, scheduled for removal in v15")]
protected IEnumerable<KeyValuePair<string, IEnumerable<object?>>> Handle(
string[] deserializedPropertyValue,
IProperty property,
string? culture,
string? segment,
bool published,
IEnumerable<string> availableCultures)
=> Handle(deserializedPropertyValue, property, culture, segment, published, availableCultures, new Dictionary<Guid, IContentType>());
protected override IEnumerable<KeyValuePair<string, IEnumerable<object?>>> Handle(
protected override IEnumerable<IndexValue> Handle(
string[] deserializedPropertyValue,
IProperty property,
string? culture,
@@ -37,11 +27,17 @@ public class TagPropertyIndexValueFactory : JsonPropertyIndexValueFactoryBase<st
bool published,
IEnumerable<string> availableCultures,
IDictionary<Guid, IContentType> contentTypeDictionary)
{
yield return new KeyValuePair<string, IEnumerable<object?>>(property.Alias, deserializedPropertyValue);
}
=>
[
new IndexValue
{
Culture = culture,
FieldName = property.Alias,
Values = deserializedPropertyValue
}
];
public override IEnumerable<KeyValuePair<string, IEnumerable<object?>>> GetIndexValues(
public override IEnumerable<IndexValue> GetIndexValues(
IProperty property,
string? culture,
string? segment,
@@ -49,13 +45,13 @@ public class TagPropertyIndexValueFactory : JsonPropertyIndexValueFactoryBase<st
IEnumerable<string> availableCultures,
IDictionary<Guid, IContentType> contentTypeDictionary)
{
IEnumerable<KeyValuePair<string, IEnumerable<object?>>> jsonValues = base.GetIndexValues(property, culture, segment, published, availableCultures, contentTypeDictionary);
IEnumerable<IndexValue> jsonValues = base.GetIndexValues(property, culture, segment, published, availableCultures, contentTypeDictionary);
if (jsonValues?.Any() is true)
{
return jsonValues;
}
var result = new List<KeyValuePair<string, IEnumerable<object?>>>();
var result = new List<IndexValue>();
var propertyValue = property.GetValue(culture, segment, published);
@@ -67,7 +63,7 @@ public class TagPropertyIndexValueFactory : JsonPropertyIndexValueFactoryBase<st
result.AddRange(Handle(values, property, culture, segment, published, availableCultures, contentTypeDictionary));
}
IEnumerable<KeyValuePair<string, IEnumerable<object?>>> summary = HandleResume(result, property, culture, segment, published);
IEnumerable<IndexValue> summary = HandleResume(result, property, culture, segment, published);
if (_indexingSettings.ExplicitlyIndexEachNestedProperty || ForceExplicitlyIndexEachNestedProperty)
{
result.AddRange(summary);

View File

@@ -30,18 +30,19 @@ public abstract class BaseValueSetBuilder<TContent> : IValueSetBuilder<TContent>
return;
}
IEnumerable<KeyValuePair<string, IEnumerable<object?>>> indexVals =
IEnumerable<IndexValue> indexVals =
editor.PropertyIndexValueFactory.GetIndexValues(property, culture, segment, PublishedValuesOnly, availableCultures, contentTypeDictionary);
foreach (KeyValuePair<string, IEnumerable<object?>> keyVal in indexVals)
foreach (IndexValue indexValue in indexVals)
{
if (keyVal.Key.IsNullOrWhiteSpace())
if (indexValue.FieldName.IsNullOrWhiteSpace())
{
continue;
}
var cultureSuffix = culture == null ? string.Empty : "_" + culture;
var indexValueCulture = indexValue.Culture ?? culture;
var cultureSuffix = indexValueCulture == null ? string.Empty : "_" + indexValueCulture.ToLowerInvariant();
foreach (var val in keyVal.Value)
foreach (var val in indexValue.Values)
{
switch (val)
{
@@ -55,28 +56,28 @@ public abstract class BaseValueSetBuilder<TContent> : IValueSetBuilder<TContent>
continue;
}
var key = $"{keyVal.Key}{cultureSuffix}";
var key = $"{indexValue.FieldName}{cultureSuffix}";
if (values?.TryGetValue(key, out IEnumerable<object?>? v) ?? false)
{
values[key] = new List<object?>(v) { val }.ToArray();
}
else
{
values?.Add($"{keyVal.Key}{cultureSuffix}", val.Yield());
values?.Add($"{indexValue.FieldName}{cultureSuffix}", val.Yield());
}
}
break;
default:
{
var key = $"{keyVal.Key}{cultureSuffix}";
var key = $"{indexValue.FieldName}{cultureSuffix}";
if (values?.TryGetValue(key, out IEnumerable<object?>? v) ?? false)
{
values[key] = new List<object?>(v) { val }.ToArray();
}
else
{
values?.Add($"{keyVal.Key}{cultureSuffix}", val.Yield());
values?.Add($"{indexValue.FieldName}{cultureSuffix}", val.Yield());
}
}

View File

@@ -3,14 +3,13 @@
using Microsoft.Extensions.Options;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.Blocks;
using Umbraco.Cms.Core.Serialization;
namespace Umbraco.Cms.Core.PropertyEditors;
internal sealed class BlockValuePropertyIndexValueFactory :
NestedPropertyIndexValueFactoryBase<BlockValuePropertyIndexValueFactory.IndexValueFactoryBlockValue, BlockItemData>,
internal class BlockValuePropertyIndexValueFactory :
BlockValuePropertyIndexValueFactoryBase<BlockValuePropertyIndexValueFactory.IndexValueFactoryBlockValue>,
IBlockValuePropertyIndexValueFactory
{
public BlockValuePropertyIndexValueFactory(
@@ -21,19 +20,14 @@ internal sealed class BlockValuePropertyIndexValueFactory :
{
}
protected override IContentType? GetContentTypeOfNestedItem(BlockItemData input, IDictionary<Guid, IContentType> contentTypeDictionary)
=> contentTypeDictionary.TryGetValue(input.ContentTypeKey, out var result) ? result : null;
protected override IDictionary<string, object?> GetRawProperty(BlockItemData blockItemData)
=> blockItemData.Values
.Where(p => p.Culture is null && p.Segment is null)
.ToDictionary(p => p.Alias, p => p.Value);
protected override IEnumerable<BlockItemData> GetDataItems(IndexValueFactoryBlockValue input) => input.ContentData;
protected override IEnumerable<RawDataItem> GetDataItems(IndexValueFactoryBlockValue input, bool published)
=> GetDataItems(input.ContentData, input.Expose, published);
// we only care about the content data when extracting values for indexing - not the layouts nor the settings
internal class IndexValueFactoryBlockValue
{
public List<BlockItemData> ContentData { get; set; } = new();
public List<BlockItemVariation> Expose { get; set; } = new();
}
}

View File

@@ -0,0 +1,308 @@
using System.Text;
using Microsoft.Extensions.Options;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.Blocks;
using Umbraco.Cms.Core.Serialization;
using Umbraco.Cms.Infrastructure.Examine;
using Umbraco.Extensions;
namespace Umbraco.Cms.Core.PropertyEditors;
internal abstract class BlockValuePropertyIndexValueFactoryBase<TSerialized> : JsonPropertyIndexValueFactoryBase<TSerialized>
{
private readonly PropertyEditorCollection _propertyEditorCollection;
protected BlockValuePropertyIndexValueFactoryBase(
PropertyEditorCollection propertyEditorCollection,
IJsonSerializer jsonSerializer,
IOptionsMonitor<IndexingSettings> indexingSettings)
: base(jsonSerializer, indexingSettings)
{
_propertyEditorCollection = propertyEditorCollection;
}
protected override IEnumerable<IndexValue> Handle(
TSerialized deserializedPropertyValue,
IProperty property,
string? culture,
string? segment,
bool published,
IEnumerable<string> availableCultures,
IDictionary<Guid, IContentType> contentTypeDictionary)
{
var result = new List<IndexValue>();
var index = 0;
foreach (RawDataItem rawData in GetDataItems(deserializedPropertyValue, published))
{
if (contentTypeDictionary.TryGetValue(rawData.ContentTypeKey, out IContentType? contentType) is false)
{
continue;
}
var propertyTypeDictionary =
contentType
.CompositionPropertyTypes
.Select(propertyType =>
{
// We want to ensure that the nested properties are set vary by culture if the parent is
// This is because it's perfectly valid to have a nested property type that's set to invariant even if the parent varies.
// For instance in a block list, the list it self can vary, but the elements can be invariant, at the same time.
if (culture is not null)
{
propertyType.Variations |= ContentVariation.Culture;
}
if (segment is not null)
{
propertyType.Variations |= ContentVariation.Segment;
}
return propertyType;
})
.ToDictionary(x => x.Alias);
result.AddRange(GetNestedResults(
$"{property.Alias}.items[{index}]",
culture,
segment,
published,
propertyTypeDictionary,
rawData,
availableCultures,
contentTypeDictionary));
index++;
}
return RenameKeysToEnsureRawSegmentsIsAPrefix(result);
}
/// <summary>
/// Rename keys that count the RAW-constant, to ensure the RAW-constant is a prefix.
/// </summary>
private IEnumerable<IndexValue> RenameKeysToEnsureRawSegmentsIsAPrefix(
List<IndexValue> indexContent)
{
foreach (IndexValue indexValue in indexContent)
{
// Tests if key includes the RawFieldPrefix and it is not in the start
if (indexValue.FieldName.Substring(1).Contains(UmbracoExamineFieldNames.RawFieldPrefix))
{
indexValue.FieldName = UmbracoExamineFieldNames.RawFieldPrefix +
indexValue.FieldName.Replace(UmbracoExamineFieldNames.RawFieldPrefix, string.Empty);
}
}
return indexContent;
}
/// <summary>
/// Get the data items of a parent item. E.g. block list have contentData.
/// </summary>
protected abstract IEnumerable<RawDataItem> GetDataItems(TSerialized input, bool published);
/// <summary>
/// Unwraps block item data as data items.
/// </summary>
protected IEnumerable<RawDataItem> GetDataItems(IList<BlockItemData> contentData, IList<BlockItemVariation> expose, bool published)
{
if (published is false)
{
return contentData.Select(ToRawData);
}
var indexData = new List<RawDataItem>();
foreach (BlockItemData blockItemData in contentData)
{
var exposedCultures = expose
.Where(e => e.ContentKey == blockItemData.Key)
.Select(e => e.Culture)
.ToArray();
if (exposedCultures.Any() is false)
{
continue;
}
if (exposedCultures.Contains(null)
|| exposedCultures.ContainsAll(blockItemData.Values.Select(v => v.Culture)))
{
indexData.Add(ToRawData(blockItemData));
continue;
}
indexData.Add(
ToRawData(
blockItemData.ContentTypeKey,
blockItemData.Values.Where(value => value.Culture is null || exposedCultures.Contains(value.Culture))
)
);
}
return indexData;
}
/// <summary>
/// Index a key with the name of the property, using the relevant content of all the children.
/// </summary>
protected override IEnumerable<IndexValue> HandleResume(
List<IndexValue> indexedContent,
IProperty property,
string? culture,
string? segment,
bool published)
{
var indexedCultures = indexedContent
.DistinctBy(v => v.Culture)
.Select(v => v.Culture)
.WhereNotNull()
.ToArray();
var cultures = indexedCultures.Any()
? indexedCultures
: new string?[] { culture };
return cultures.Select(c => new IndexValue
{
Culture = c, FieldName = property.Alias, Values = [GetResumeFromAllContent(indexedContent, c)]
});
}
/// <summary>
/// Gets a resume as string of all the content in this nested type.
/// </summary>
/// <param name="indexedContent">All the indexed content for this property.</param>
/// <returns>the string with all relevant content from </returns>
private static string GetResumeFromAllContent(List<IndexValue> indexedContent, string? culture)
{
var stringBuilder = new StringBuilder();
foreach (IndexValue indexValue in indexedContent.Where(v => v.Culture == culture || v.Culture is null))
{
// Ignore Raw fields
if (indexValue.FieldName.Contains(UmbracoExamineFieldNames.RawFieldPrefix))
{
continue;
}
foreach (var value in indexValue.Values)
{
if (value is not null)
{
stringBuilder.AppendLine(value.ToString());
}
}
}
return stringBuilder.ToString();
}
/// <summary>
/// Gets the content to index for the nested type. E.g. Block list, Nested Content, etc..
/// </summary>
private IEnumerable<IndexValue> GetNestedResults(
string keyPrefix,
string? culture,
string? segment,
bool published,
IDictionary<string, IPropertyType> propertyTypeDictionary,
RawDataItem rawData,
IEnumerable<string> availableCultures,
IDictionary<Guid,IContentType> contentTypeDictionary)
{
foreach (RawPropertyData rawPropertyData in rawData.Properties)
{
if (propertyTypeDictionary.TryGetValue(rawPropertyData.Alias, out IPropertyType? propertyType))
{
IDataEditor? editor = _propertyEditorCollection[propertyType.PropertyEditorAlias];
if (editor is null)
{
continue;
}
IProperty subProperty = new Property(propertyType);
IEnumerable<IndexValue> indexValues = null!;
var propertyCulture = rawPropertyData.Culture ?? culture;
if (propertyType.VariesByCulture() && propertyCulture is null)
{
foreach (var availableCulture in availableCultures)
{
subProperty.SetValue(rawPropertyData.Value, availableCulture, segment);
if (published)
{
subProperty.PublishValues(availableCulture, segment ?? "*");
}
indexValues =
editor.PropertyIndexValueFactory.GetIndexValues(subProperty, availableCulture, segment, published, availableCultures, contentTypeDictionary);
}
}
else
{
subProperty.SetValue(rawPropertyData.Value, propertyCulture, segment);
if (published)
{
subProperty.PublishValues(propertyCulture ?? "*", segment ?? "*");
}
indexValues = editor.PropertyIndexValueFactory.GetIndexValues(subProperty, propertyCulture, segment, published, availableCultures, contentTypeDictionary);
}
var rawDataCultures = rawData.Properties.Select(property => property.Culture).Distinct().WhereNotNull().ToArray();
foreach (IndexValue indexValue in indexValues)
{
indexValue.FieldName = $"{keyPrefix}.{indexValue.FieldName}";
if (indexValue.Culture is null && rawDataCultures.Any())
{
foreach (var rawDataCulture in rawDataCultures)
{
yield return new IndexValue
{
Culture = rawDataCulture,
FieldName = indexValue.FieldName,
Values = indexValue.Values
};
}
}
else
{
indexValue.Culture = rawDataCultures.Any() ? indexValue.Culture : null;
yield return indexValue;
}
}
}
}
}
private RawDataItem ToRawData(BlockItemData blockItemData)
=> ToRawData(blockItemData.ContentTypeKey, blockItemData.Values);
private RawDataItem ToRawData(Guid contentTypeKey, IEnumerable<BlockPropertyValue> values)
=> new()
{
ContentTypeKey = contentTypeKey,
Properties = values.Select(value => new RawPropertyData
{
Alias = value.Alias,
Culture = value.Culture,
Value = value.Value
})
};
protected class RawDataItem
{
public required Guid ContentTypeKey { get; init; }
public required IEnumerable<RawPropertyData> Properties { get; init; }
}
protected class RawPropertyData
{
public required string Alias { get; init; }
public required object? Value { get; init; }
public required string? Culture { get; init; }
}
}

View File

@@ -1,222 +0,0 @@
using System.Text;
using Microsoft.Extensions.Options;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Serialization;
using Umbraco.Cms.Infrastructure.Examine;
using Umbraco.Extensions;
namespace Umbraco.Cms.Core.PropertyEditors;
internal abstract class NestedPropertyIndexValueFactoryBase<TSerialized, TItem> : JsonPropertyIndexValueFactoryBase<TSerialized>
{
private readonly PropertyEditorCollection _propertyEditorCollection;
protected NestedPropertyIndexValueFactoryBase(
PropertyEditorCollection propertyEditorCollection,
IJsonSerializer jsonSerializer,
IOptionsMonitor<IndexingSettings> indexingSettings)
: base(jsonSerializer, indexingSettings)
{
_propertyEditorCollection = propertyEditorCollection;
}
protected override IEnumerable<KeyValuePair<string, IEnumerable<object?>>> Handle(
TSerialized deserializedPropertyValue,
IProperty property,
string? culture,
string? segment,
bool published,
IEnumerable<string> availableCultures,
IDictionary<Guid, IContentType> contentTypeDictionary)
{
var result = new List<KeyValuePair<string, IEnumerable<object?>>>();
var index = 0;
foreach (TItem nestedContentRowValue in GetDataItems(deserializedPropertyValue))
{
IContentType? contentType = GetContentTypeOfNestedItem(nestedContentRowValue, contentTypeDictionary);
if (contentType is null)
{
continue;
}
var propertyTypeDictionary =
contentType
.CompositionPropertyGroups
.SelectMany(x => x.PropertyTypes!)
.Select(propertyType =>
{
// We want to ensure that the nested properties are set vary by culture if the parent is
// This is because it's perfectly valid to have a nested property type that's set to invariant even if the parent varies.
// For instance in a block list, the list it self can vary, but the elements can be invariant, at the same time.
if (culture is not null)
{
propertyType.Variations |= ContentVariation.Culture;
}
if (segment is not null)
{
propertyType.Variations |= ContentVariation.Segment;
}
return propertyType;
})
.ToDictionary(x => x.Alias);
result.AddRange(GetNestedResults(
$"{property.Alias}.items[{index}]",
culture,
segment,
published,
propertyTypeDictionary,
nestedContentRowValue,
availableCultures,
contentTypeDictionary));
index++;
}
return RenameKeysToEnsureRawSegmentsIsAPrefix(result);
}
/// <summary>
/// Rename keys that count the RAW-constant, to ensure the RAW-constant is a prefix.
/// </summary>
private IEnumerable<KeyValuePair<string, IEnumerable<object?>>> RenameKeysToEnsureRawSegmentsIsAPrefix(
List<KeyValuePair<string, IEnumerable<object?>>> indexContent)
{
foreach (KeyValuePair<string, IEnumerable<object?>> indexedKeyValuePair in indexContent)
{
// Tests if key includes the RawFieldPrefix and it is not in the start
if (indexedKeyValuePair.Key.Substring(1).Contains(UmbracoExamineFieldNames.RawFieldPrefix))
{
var newKey = UmbracoExamineFieldNames.RawFieldPrefix +
indexedKeyValuePair.Key.Replace(UmbracoExamineFieldNames.RawFieldPrefix, string.Empty);
yield return new KeyValuePair<string, IEnumerable<object?>>(newKey, indexedKeyValuePair.Value);
}
else
{
yield return indexedKeyValuePair;
}
}
}
/// <summary>
/// Gets the content type using the nested item.
/// </summary>
protected abstract IContentType? GetContentTypeOfNestedItem(TItem nestedItem, IDictionary<Guid, IContentType> contentTypeDictionary);
/// <summary>
/// Gets the raw data from a nested item.
/// </summary>
protected abstract IDictionary<string, object?> GetRawProperty(TItem nestedItem);
/// <summary>
/// Get the data times of a parent item. E.g. block list have contentData.
/// </summary>
protected abstract IEnumerable<TItem> GetDataItems(TSerialized input);
/// <summary>
/// Index a key with the name of the property, using the relevant content of all the children.
/// </summary>
protected override IEnumerable<KeyValuePair<string, IEnumerable<object?>>> HandleResume(
List<KeyValuePair<string, IEnumerable<object?>>> indexedContent,
IProperty property,
string? culture,
string? segment,
bool published)
{
yield return new KeyValuePair<string, IEnumerable<object?>>(
property.Alias,
GetResumeFromAllContent(indexedContent).Yield());
}
/// <summary>
/// Gets a resume as string of all the content in this nested type.
/// </summary>
/// <param name="indexedContent">All the indexed content for this property.</param>
/// <returns>the string with all relevant content from </returns>
private static string GetResumeFromAllContent(List<KeyValuePair<string, IEnumerable<object?>>> indexedContent)
{
var stringBuilder = new StringBuilder();
foreach ((var indexKey, IEnumerable<object?>? indexedValue) in indexedContent)
{
// Ignore Raw fields
if (indexKey.Contains(UmbracoExamineFieldNames.RawFieldPrefix))
{
continue;
}
foreach (var value in indexedValue)
{
if (value is not null)
{
stringBuilder.AppendLine(value.ToString());
}
}
}
return stringBuilder.ToString();
}
/// <summary>
/// Gets the content to index for the nested type. E.g. Block list, Nested Content, etc..
/// </summary>
private IEnumerable<KeyValuePair<string, IEnumerable<object?>>> GetNestedResults(
string keyPrefix,
string? culture,
string? segment,
bool published,
IDictionary<string, IPropertyType> propertyTypeDictionary,
TItem nestedContentRowValue,
IEnumerable<string> availableCultures,
IDictionary<Guid,IContentType> contentTypeDictionary)
{
foreach ((var propertyAlias, var propertyValue) in GetRawProperty(nestedContentRowValue))
{
if (propertyTypeDictionary.TryGetValue(propertyAlias, out IPropertyType? propertyType))
{
IDataEditor? editor = _propertyEditorCollection[propertyType.PropertyEditorAlias];
if (editor is null)
{
continue;
}
IProperty subProperty = new Property(propertyType);
IEnumerable<KeyValuePair<string, IEnumerable<object?>>> indexValues = null!;
if (propertyType.VariesByCulture() && culture is null)
{
foreach (var availableCulture in availableCultures)
{
subProperty.SetValue(propertyValue, availableCulture, segment);
if (published)
{
subProperty.PublishValues(availableCulture, segment ?? "*");
}
indexValues =
editor.PropertyIndexValueFactory.GetIndexValues(subProperty, availableCulture, segment, published, availableCultures, contentTypeDictionary);
}
}
else
{
subProperty.SetValue(propertyValue, culture, segment);
if (published)
{
subProperty.PublishValues(culture ?? "*", segment ?? "*");
}
indexValues = editor.PropertyIndexValueFactory.GetIndexValues(subProperty, culture, segment, published, availableCultures, contentTypeDictionary);
}
foreach ((var nestedAlias, IEnumerable<object?> nestedValue) in indexValues)
{
yield return new KeyValuePair<string, IEnumerable<object?>>(
$"{keyPrefix}.{nestedAlias}", nestedValue!);
}
}
}
}
}

View File

@@ -2,15 +2,13 @@
using Microsoft.Extensions.Options;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.Blocks;
using Umbraco.Cms.Core.Serialization;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Infrastructure.Examine;
using Umbraco.Extensions;
namespace Umbraco.Cms.Core.PropertyEditors;
internal class RichTextPropertyIndexValueFactory : NestedPropertyIndexValueFactoryBase<RichTextEditorValue, BlockItemData>, IRichTextPropertyIndexValueFactory
internal class RichTextPropertyIndexValueFactory : BlockValuePropertyIndexValueFactoryBase<RichTextEditorValue>, IRichTextPropertyIndexValueFactory
{
private readonly IJsonSerializer _jsonSerializer;
private readonly ILogger<RichTextPropertyIndexValueFactory> _logger;
@@ -26,18 +24,7 @@ internal class RichTextPropertyIndexValueFactory : NestedPropertyIndexValueFacto
_logger = logger;
}
[Obsolete("Use constructor that doesn't take IContentTypeService, scheduled for removal in V15")]
public RichTextPropertyIndexValueFactory(
PropertyEditorCollection propertyEditorCollection,
IJsonSerializer jsonSerializer,
IOptionsMonitor<IndexingSettings> indexingSettings,
IContentTypeService contentTypeService,
ILogger<RichTextPropertyIndexValueFactory> logger)
: this(propertyEditorCollection, jsonSerializer, indexingSettings, logger)
{
}
public new IEnumerable<KeyValuePair<string, IEnumerable<object?>>> GetIndexValues(
public override IEnumerable<IndexValue> GetIndexValues(
IProperty property,
string? culture,
string? segment,
@@ -48,33 +35,101 @@ internal class RichTextPropertyIndexValueFactory : NestedPropertyIndexValueFacto
var val = property.GetValue(culture, segment, published);
if (RichTextPropertyEditorHelper.TryParseRichTextEditorValue(val, _jsonSerializer, _logger, out RichTextEditorValue? richTextEditorValue) is false)
{
yield break;
return [];
}
// always index the "raw" value
var indexValues = new List<IndexValue>
{
new IndexValue
{
Culture = culture,
FieldName = $"{UmbracoExamineFieldNames.RawFieldPrefix}{property.Alias}",
Values = [richTextEditorValue.Markup]
}
};
// the actual content (RTE content without markup, i.e. the actual words) must be indexed under the property alias
var richTextWithoutMarkup = richTextEditorValue.Markup.StripHtml();
if (richTextEditorValue.Blocks?.ContentData.Any() is not true)
{
// no blocks; index the content for the culture and be done with it
indexValues.Add(new IndexValue
{
Culture = culture,
FieldName = property.Alias,
Values = [richTextWithoutMarkup]
});
return indexValues;
}
// the "blocks values resume" (the combined searchable text values from all blocks) is stored as a string value under the property alias by the base implementation
var blocksIndexValues = base.GetIndexValues(property, culture, segment, published, availableCultures, contentTypeDictionary).ToDictionary(pair => pair.Key, pair => pair.Value);
var blocksIndexValuesResume = blocksIndexValues.TryGetValue(property.Alias, out IEnumerable<object?>? blocksIndexValuesResumeValue)
? blocksIndexValuesResumeValue.FirstOrDefault() as string
: null;
var blocksIndexValuesResumes = base
.GetIndexValues(property, culture, segment, published, availableCultures, contentTypeDictionary)
.Where(value => value.FieldName == property.Alias)
.GroupBy(value => value.Culture?.ToLowerInvariant())
.Select(group => new
{
Culture = group.Key,
Resume = string.Join(Environment.NewLine, group.Select(v => v.Values.FirstOrDefault() as string))
})
.ToArray();
// index the stripped HTML values combined with "blocks values resume" value
yield return new KeyValuePair<string, IEnumerable<object?>>(
property.Alias,
new object[] { $"{richTextEditorValue.Markup.StripHtml()} {blocksIndexValuesResume}" });
// is this RTE sat on culture variant content?
if (culture is not null)
{
// yes, append the "block values resume" for the specific culture only (if present)
var blocksResume = blocksIndexValuesResumes
.FirstOrDefault(r => r.Culture.InvariantEquals(culture))?
.Resume;
// store the raw value
yield return new KeyValuePair<string, IEnumerable<object?>>(
$"{UmbracoExamineFieldNames.RawFieldPrefix}{property.Alias}", new object[] { richTextEditorValue.Markup });
indexValues.Add(new IndexValue
{
Culture = culture,
FieldName = property.Alias,
Values = [$"{richTextWithoutMarkup}{Environment.NewLine}{blocksResume}"]
});
return indexValues;
}
// is there an invariant "block values resume"? this might happen for purely invariant blocks or in a culture invariant context
var invariantResume = blocksIndexValuesResumes
.FirstOrDefault(r => r.Culture is null)
?.Resume;
if (invariantResume != null)
{
// yes, append the invariant "block values resume"
indexValues.Add(new IndexValue
{
Culture = culture,
FieldName = property.Alias,
Values = [$"{richTextWithoutMarkup}{Environment.NewLine}{invariantResume}"]
});
return indexValues;
}
// at this point we have encountered block level variance - add explicit index values for all "block values resume" cultures found
indexValues.AddRange(blocksIndexValuesResumes.Select(resume =>
new IndexValue
{
Culture = resume.Culture,
FieldName = property.Alias,
Values = [$"{richTextWithoutMarkup}{Environment.NewLine}{resume.Resume}"]
}));
// if one or more cultures did not have any (exposed) blocks, ensure that the RTE content is still indexed for those cultures
IEnumerable<string?> missingBlocksResumeCultures = availableCultures.Except(blocksIndexValuesResumes.Select(r => r.Culture), StringComparer.CurrentCultureIgnoreCase);
indexValues.AddRange(missingBlocksResumeCultures.Select(missingResumeCulture =>
new IndexValue
{
Culture = missingResumeCulture,
FieldName = property.Alias,
Values = [richTextWithoutMarkup]
}));
return indexValues;
}
protected override IContentType? GetContentTypeOfNestedItem(BlockItemData nestedItem, IDictionary<Guid, IContentType> contentTypeDictionary)
=> contentTypeDictionary.TryGetValue(nestedItem.ContentTypeKey, out var result) ? result : null;
protected override IDictionary<string, object?> GetRawProperty(BlockItemData blockItemData)
=> blockItemData.Values
.Where(p => p.Culture is null && p.Segment is null)
.ToDictionary(p => p.Alias, p => p.Value);
protected override IEnumerable<BlockItemData> GetDataItems(RichTextEditorValue input)
=> input.Blocks?.ContentData ?? new List<BlockItemData>();
protected override IEnumerable<RawDataItem> GetDataItems(RichTextEditorValue input, bool published)
=> GetDataItems(input.Blocks?.ContentData ?? [], input.Blocks?.Expose ?? [], published);
}

View File

@@ -0,0 +1,399 @@
using NUnit.Framework;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.Blocks;
using Umbraco.Cms.Tests.Common.Builders;
using Umbraco.Cms.Tests.Common.Builders.Extensions;
namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.PropertyEditors;
public partial class BlockListElementLevelVariationTests
{
[Test]
public async Task Can_Index_Cultures_Independently_Invariant_Blocks()
{
var elementType = CreateElementType(ContentVariation.Culture);
var blockListDataType = await CreateBlockListDataType(elementType);
var contentType = CreateContentType(ContentVariation.Culture, blockListDataType);
var content = CreateContent(
contentType,
elementType,
new List<BlockPropertyValue>
{
new() { Alias = "invariantText", Value = "The invariant content value" },
new() { Alias = "variantText", Value = "The content value (en-US)", Culture = "en-US" },
new() { Alias = "variantText", Value = "The content value (da-DK)", Culture = "da-DK" },
},
new List<BlockPropertyValue>
{
new() { Alias = "invariantText", Value = "The invariant settings value" },
new() { Alias = "variantText", Value = "The settings value (en-US)", Culture = "en-US" },
new() { Alias = "variantText", Value = "The settings value (da-DK)", Culture = "da-DK" },
},
true);
var editor = blockListDataType.Editor!;
var indexValues = editor.PropertyIndexValueFactory.GetIndexValues(
content.Properties["blocks"]!,
culture: null,
segment: null,
published: true,
availableCultures: ["en-US", "da-DK"],
contentTypeDictionary: new Dictionary<Guid, IContentType>
{
{ elementType.Key, elementType }, { contentType.Key, contentType }
});
Assert.AreEqual(2, indexValues.Count());
AssertIndexValues("en-US");
AssertIndexValues("da-DK");
void AssertIndexValues(string culture)
{
var indexValue = indexValues.FirstOrDefault(v => v.Culture == culture);
Assert.IsNotNull(indexValue);
Assert.AreEqual(1, indexValue.Values.Count());
var indexedValue = indexValue.Values.First() as string;
Assert.IsNotNull(indexedValue);
var values = indexedValue.Split(Environment.NewLine, StringSplitOptions.RemoveEmptyEntries);
Assert.AreEqual(2, values.Length);
Assert.Contains($"The content value ({culture})", values);
Assert.Contains("The invariant content value", values);
}
}
[Test]
public async Task Can_Index_Cultures_Independently_Variant_Blocks()
{
var elementType = CreateElementType(ContentVariation.Culture);
var blockListDataType = await CreateBlockListDataType(elementType);
var contentType = CreateContentType(ContentVariation.Culture, blockListDataType, ContentVariation.Culture);
var content = CreateContent(
contentType,
elementType,
new []
{
new BlockProperty(
new List<BlockPropertyValue>
{
new() { Alias = "invariantText", Value = "en-US invariantText content value" },
new() { Alias = "variantText", Value = "en-US variantText content value" }
},
new List<BlockPropertyValue>
{
new() { Alias = "invariantText", Value = "en-US invariantText settings value" },
new() { Alias = "variantText", Value = "en-US variantText settings value" }
},
"en-US",
null),
new BlockProperty(
new List<BlockPropertyValue>
{
new() { Alias = "invariantText", Value = "da-DK invariantText content value" },
new() { Alias = "variantText", Value = "da-DK variantText content value" }
},
new List<BlockPropertyValue>
{
new() { Alias = "invariantText", Value = "da-DK invariantText settings value" },
new() { Alias = "variantText", Value = "da-DK variantText settings value" }
},
"da-DK",
null)
},
true);
AssertIndexValues("en-US");
AssertIndexValues("da-DK");
void AssertIndexValues(string culture)
{
var editor = blockListDataType.Editor!;
var indexValues = editor.PropertyIndexValueFactory.GetIndexValues(
content.Properties["blocks"]!,
culture: culture,
segment: null,
published: true,
availableCultures: ["en-US", "da-DK"],
contentTypeDictionary: new Dictionary<Guid, IContentType>
{
{ elementType.Key, elementType }, { contentType.Key, contentType }
});
Assert.AreEqual(1, indexValues.Count());
var indexValue = indexValues.FirstOrDefault(v => v.Culture == culture);
Assert.IsNotNull(indexValue);
Assert.AreEqual(1, indexValue.Values.Count());
var indexedValue = indexValue.Values.First() as string;
Assert.IsNotNull(indexedValue);
Assert.AreEqual($"{culture} invariantText content value {culture} variantText content value", TrimAndStripNewlines(indexedValue));
}
}
[TestCase(true)]
[TestCase(false)]
public async Task Can_Handle_Unexposed_Blocks(bool published)
{
var elementType = CreateElementType(ContentVariation.Culture);
var blockListDataType = await CreateBlockListDataType(elementType);
var contentType = CreateContentType(ContentVariation.Culture, blockListDataType);
var content = CreateContent(contentType, elementType, [], false);
var blockListValue = BlockListPropertyValue(
elementType,
[
(
Guid.NewGuid(),
Guid.NewGuid(),
new BlockProperty(
new List<BlockPropertyValue> {
new() { Alias = "invariantText", Value = "#1: The invariant content value" },
new() { Alias = "variantText", Value = "#1: The content value in English", Culture = "en-US" },
new() { Alias = "variantText", Value = "#1: The content value in Danish", Culture = "da-DK" }
},
[],
null,
null
)
),
(
Guid.NewGuid(),
Guid.NewGuid(),
new BlockProperty(
new List<BlockPropertyValue> {
new() { Alias = "invariantText", Value = "#2: The invariant content value" },
new() { Alias = "variantText", Value = "#2: The content value in English", Culture = "en-US" },
new() { Alias = "variantText", Value = "#2: The content value in Danish", Culture = "da-DK" }
},
[],
null,
null
)
)
]
);
// only expose the first block in English and the second block in Danish (to make a difference between published and unpublished index values)
blockListValue.Expose =
[
new() { ContentKey = blockListValue.ContentData[0].Key, Culture = "en-US" },
new() { ContentKey = blockListValue.ContentData[1].Key, Culture = "da-DK" },
];
content.Properties["blocks"]!.SetValue(JsonSerializer.Serialize(blockListValue));
ContentService.Save(content);
PublishContent(content, contentType, ["en-US", "da-DK"]);
var editor = blockListDataType.Editor!;
var indexValues = editor.PropertyIndexValueFactory.GetIndexValues(
content.Properties["blocks"]!,
culture: null,
segment: null,
published: published,
availableCultures: ["en-US", "da-DK"],
contentTypeDictionary: new Dictionary<Guid, IContentType>
{
{ elementType.Key, elementType }, { contentType.Key, contentType }
});
Assert.AreEqual(2, indexValues.Count());
var indexValue = indexValues.FirstOrDefault(v => v.Culture == "da-DK");
Assert.IsNotNull(indexValue);
Assert.AreEqual(1, indexValue.Values.Count());
var indexedValue = indexValue.Values.First() as string;
Assert.IsNotNull(indexedValue);
if (published)
{
Assert.AreEqual("#2: The invariant content value #2: The content value in Danish", TrimAndStripNewlines(indexedValue));
}
else
{
Assert.AreEqual("#1: The invariant content value #1: The content value in Danish #2: The invariant content value #2: The content value in Danish", TrimAndStripNewlines(indexedValue));
}
indexValue = indexValues.FirstOrDefault(v => v.Culture == "en-US");
Assert.IsNotNull(indexValue);
Assert.AreEqual(1, indexValue.Values.Count());
indexedValue = indexValue.Values.First() as string;
Assert.IsNotNull(indexedValue);
if (published)
{
Assert.AreEqual("#1: The content value in English #1: The invariant content value", TrimAndStripNewlines(indexedValue));
}
else
{
Assert.AreEqual("#1: The invariant content value #1: The content value in English #2: The invariant content value #2: The content value in English", TrimAndStripNewlines(indexedValue));
}
}
[TestCase(ContentVariation.Nothing)]
[TestCase(ContentVariation.Culture)]
public async Task Can_Index_Invariant(ContentVariation elementTypeVariation)
{
var elementType = CreateElementType(elementTypeVariation);
var blockListDataType = await CreateBlockListDataType(elementType);
var contentType = CreateContentType(ContentVariation.Nothing, blockListDataType);
var content = CreateContent(
contentType,
elementType,
new List<BlockPropertyValue>
{
new() { Alias = "invariantText", Value = "The invariant content value" },
new() { Alias = "variantText", Value = "Another invariant content value" }
},
new List<BlockPropertyValue>
{
new() { Alias = "invariantText", Value = "The invariant settings value" },
new() { Alias = "variantText", Value = "Another invariant settings value" }
},
true);
var editor = blockListDataType.Editor!;
var indexValues = editor.PropertyIndexValueFactory.GetIndexValues(
content.Properties["blocks"]!,
culture: null,
segment: null,
published: true,
availableCultures: ["en-US"],
contentTypeDictionary: new Dictionary<Guid, IContentType>
{
{ elementType.Key, elementType }, { contentType.Key, contentType }
});
Assert.AreEqual(1, indexValues.Count());
var indexValue = indexValues.FirstOrDefault(v => v.Culture is null);
Assert.IsNotNull(indexValue);
Assert.AreEqual(1, indexValue.Values.Count());
var indexedValue = indexValue.Values.First() as string;
Assert.IsNotNull(indexedValue);
var values = indexedValue.Split(Environment.NewLine, StringSplitOptions.RemoveEmptyEntries);
Assert.AreEqual(2, values.Length);
Assert.Contains("The invariant content value", values);
Assert.Contains("Another invariant content value", values);
}
[Test]
public async Task Can_Index_Cultures_Independently_Nested_Invariant_Blocks()
{
var nestedElementType = CreateElementType(ContentVariation.Culture);
var nestedBlockListDataType = await CreateBlockListDataType(nestedElementType);
var rootElementType = new ContentTypeBuilder()
.WithAlias("myRootElementType")
.WithName("My Root Element Type")
.WithIsElement(true)
.WithContentVariation(ContentVariation.Culture)
.AddPropertyType()
.WithAlias("invariantText")
.WithName("Invariant text")
.WithDataTypeId(Constants.DataTypes.Textbox)
.WithPropertyEditorAlias(Constants.PropertyEditors.Aliases.TextBox)
.WithValueStorageType(ValueStorageType.Nvarchar)
.WithVariations(ContentVariation.Nothing)
.Done()
.AddPropertyType()
.WithAlias("variantText")
.WithName("Variant text")
.WithDataTypeId(Constants.DataTypes.Textbox)
.WithPropertyEditorAlias(Constants.PropertyEditors.Aliases.TextBox)
.WithValueStorageType(ValueStorageType.Nvarchar)
.WithVariations(ContentVariation.Culture)
.Done()
.AddPropertyType()
.WithAlias("nestedBlocks")
.WithName("Nested blocks")
.WithDataTypeId(nestedBlockListDataType.Id)
.WithPropertyEditorAlias(Constants.PropertyEditors.Aliases.BlockList)
.WithValueStorageType(ValueStorageType.Ntext)
.WithVariations(ContentVariation.Nothing)
.Done()
.Build();
ContentTypeService.Save(rootElementType);
var rootBlockListDataType = await CreateBlockListDataType(rootElementType);
var contentType = CreateContentType(ContentVariation.Culture, rootBlockListDataType);
var nestedElementContentKey = Guid.NewGuid();
var nestedElementSettingsKey = Guid.NewGuid();
var content = CreateContent(
contentType,
rootElementType,
new List<BlockPropertyValue>
{
new()
{
Alias = "nestedBlocks",
Value = BlockListPropertyValue(
nestedElementType,
nestedElementContentKey,
nestedElementSettingsKey,
new BlockProperty(
new List<BlockPropertyValue>
{
new() { Alias = "invariantText", Value = "The first nested invariant content value" },
new() { Alias = "variantText", Value = "The first nested content value in English", Culture = "en-US" },
new() { Alias = "variantText", Value = "The first nested content value in Danish", Culture = "da-DK" },
},
new List<BlockPropertyValue>
{
new() { Alias = "invariantText", Value = "The first nested invariant settings value" },
new() { Alias = "variantText", Value = "The first nested settings value in English", Culture = "en-US" },
new() { Alias = "variantText", Value = "The first nested settings value in Danish", Culture = "da-DK" },
},
null,
null))
},
new() { Alias = "invariantText", Value = "The first root invariant content value" },
new() { Alias = "variantText", Value = "The first root content value in English", Culture = "en-US" },
new() { Alias = "variantText", Value = "The first root content value in Danish", Culture = "da-DK" },
},
[],
true);
var editor = rootBlockListDataType.Editor!;
var indexValues = editor.PropertyIndexValueFactory.GetIndexValues(
content.Properties["blocks"]!,
culture: null,
segment: null,
published: true,
availableCultures: ["en-US"],
contentTypeDictionary: new Dictionary<Guid, IContentType>
{
{ nestedElementType.Key, nestedElementType }, { rootElementType.Key, rootElementType }, { contentType.Key, contentType }
});
Assert.AreEqual(2, indexValues.Count());
AssertIndexedValues(
"en-US",
"The first root invariant content value",
"The first root content value in English",
"The first nested invariant content value",
"The first nested content value in English");
AssertIndexedValues(
"da-DK",
"The first root invariant content value",
"The first root content value in Danish",
"The first nested invariant content value",
"The first nested content value in Danish");
void AssertIndexedValues(string culture, params string[] expectedIndexedValues)
{
var indexValue = indexValues.FirstOrDefault(v => v.Culture == culture);
Assert.IsNotNull(indexValue);
Assert.AreEqual(1, indexValue.Values.Count());
var indexedValue = indexValue.Values.First() as string;
Assert.IsNotNull(indexedValue);
var values = indexedValue.Split(Environment.NewLine, StringSplitOptions.RemoveEmptyEntries);
Assert.AreEqual(expectedIndexedValues.Length, values.Length);
Assert.IsTrue(values.ContainsAll(expectedIndexedValues));
}
}
private string TrimAndStripNewlines(string value)
=> value.Replace(Environment.NewLine, " ").Trim();
}

View File

@@ -83,12 +83,13 @@ public class PropertyIndexValueFactoryTests : UmbracoIntegrationTest
contentTypeDictionary: new Dictionary<Guid, IContentType>
{
{ elementType.Key, elementType }, { contentType.Key, contentType }
}).ToDictionary();
});
Assert.IsTrue(indexValues.TryGetValue("bodyText", out var bodyTextIndexValues));
var indexValue = indexValues.FirstOrDefault(v => v.FieldName == "bodyText");
Assert.IsNotNull(indexValue);
Assert.AreEqual(1, bodyTextIndexValues.Count());
var bodyTextIndexValue = bodyTextIndexValues.First() as string;
Assert.AreEqual(1, indexValue.Values.Count());
var bodyTextIndexValue = indexValue.Values.First() as string;
Assert.IsNotNull(bodyTextIndexValue);
Assert.Multiple(() =>
@@ -122,12 +123,13 @@ public class PropertyIndexValueFactoryTests : UmbracoIntegrationTest
contentTypeDictionary: new Dictionary<Guid, IContentType>
{
{ contentType.Key, contentType }
}).ToDictionary();
});
Assert.IsTrue(indexValues.TryGetValue("bodyText", out var bodyTextIndexValues));
var indexValue = indexValues.FirstOrDefault(v => v.FieldName == "bodyText");
Assert.IsNotNull(indexValue);
Assert.AreEqual(1, bodyTextIndexValues.Count());
var bodyTextIndexValue = bodyTextIndexValues.First() as string;
Assert.AreEqual(1, indexValue.Values.Count());
var bodyTextIndexValue = indexValue.Values.First() as string;
Assert.IsNotNull(bodyTextIndexValue);
Assert.IsTrue(bodyTextIndexValue.Contains("This is some markup"));
}
@@ -205,12 +207,13 @@ public class PropertyIndexValueFactoryTests : UmbracoIntegrationTest
contentTypeDictionary: new Dictionary<Guid, IContentType>
{
{ elementType.Key, elementType }, { contentType.Key, contentType }
}).ToDictionary();
});
Assert.IsTrue(indexValues.TryGetValue("blocks", out var blocksIndexValues));
var indexValue = indexValues.FirstOrDefault(v => v.FieldName == "blocks");
Assert.IsNotNull(indexValue);
Assert.AreEqual(1, blocksIndexValues.Count());
var blockIndexValue = blocksIndexValues.First() as string;
Assert.AreEqual(1, indexValue.Values.Count());
var blockIndexValue = indexValue.Values.First() as string;
Assert.IsNotNull(blockIndexValue);
Assert.Multiple(() =>
@@ -333,12 +336,13 @@ public class PropertyIndexValueFactoryTests : UmbracoIntegrationTest
contentTypeDictionary: new Dictionary<Guid, IContentType>
{
{ elementType.Key, elementType }, { contentType.Key, contentType }
}).ToDictionary();
});
Assert.IsTrue(indexValues.TryGetValue("blocks", out var blocksIndexValues));
var indexValue = indexValues.FirstOrDefault(v => v.FieldName == "blocks");
Assert.IsNotNull(indexValue);
Assert.AreEqual(1, blocksIndexValues.Count());
var blockIndexValue = blocksIndexValues.First() as string;
Assert.AreEqual(1, indexValue.Values.Count());
var blockIndexValue = indexValue.Values.First() as string;
Assert.IsNotNull(blockIndexValue);
Assert.Multiple(() =>

View File

@@ -5,6 +5,7 @@ using Umbraco.Cms.Core.Models.Blocks;
using Umbraco.Cms.Core.Models.DeliveryApi;
using Umbraco.Cms.Core.PropertyEditors;
using Umbraco.Cms.Core.Serialization;
using Umbraco.Cms.Infrastructure.Examine;
using Umbraco.Cms.Tests.Common.Builders;
using Umbraco.Cms.Tests.Common.Builders.Extensions;
@@ -22,8 +23,8 @@ public class RichTextElementLevelVariationTests : BlockEditorElementVariationTes
{
var elementType = CreateElementType(ContentVariation.Culture);
var blockGridDataType = await CreateRichTextDataType(elementType);
var contentType = CreateContentType(blockGridDataType);
var rteDataType = await CreateRichTextDataType(elementType);
var contentType = CreateContentType(rteDataType);
var richTextValue = CreateRichTextValue(elementType);
var content = CreateContent(contentType, richTextValue);
@@ -180,8 +181,8 @@ public class RichTextElementLevelVariationTests : BlockEditorElementVariationTes
{
var elementType = CreateElementType(ContentVariation.Culture);
var blockGridDataType = await CreateRichTextDataType(elementType);
var contentType = CreateContentType(blockGridDataType);
var rteDataType = await CreateRichTextDataType(elementType);
var contentType = CreateContentType(rteDataType);
var richTextValue = CreateRichTextValue(elementType);
var content = CreateContent(contentType, richTextValue);
@@ -285,8 +286,8 @@ public class RichTextElementLevelVariationTests : BlockEditorElementVariationTes
{
var elementType = CreateElementType(ContentVariation.Culture);
var blockGridDataType = await CreateRichTextDataType(elementType);
var contentType = CreateContentType(blockGridDataType);
var rteDataType = await CreateRichTextDataType(elementType);
var contentType = CreateContentType(rteDataType);
var richTextValue = CreateRichTextValue(elementType);
var content = CreateContent(contentType, richTextValue);
@@ -369,8 +370,8 @@ public class RichTextElementLevelVariationTests : BlockEditorElementVariationTes
{
var elementType = CreateElementType(ContentVariation.Culture);
var blockGridDataType = await CreateRichTextDataType(elementType);
var contentType = CreateContentType(blockGridDataType);
var rteDataType = await CreateRichTextDataType(elementType);
var contentType = CreateContentType(rteDataType);
var richTextValue = new RichTextEditorValue { Markup = "<p>Markup here</p>", Blocks = null };
var content = CreateContent(contentType, richTextValue);
@@ -398,8 +399,8 @@ public class RichTextElementLevelVariationTests : BlockEditorElementVariationTes
{
var elementType = CreateElementType(ContentVariation.Culture);
var blockGridDataType = await CreateRichTextDataType(elementType);
var contentType = CreateContentType(ContentVariation.Nothing, blockGridDataType);
var rteDataType = await CreateRichTextDataType(elementType);
var contentType = CreateContentType(ContentVariation.Nothing, rteDataType);
var richTextValue = new RichTextEditorValue { Markup = "<p>Markup here</p>", Blocks = null };
var content = CreateContent(contentType, richTextValue);
@@ -422,6 +423,247 @@ public class RichTextElementLevelVariationTests : BlockEditorElementVariationTes
}
}
[Test]
public async Task Can_Index_Cultures_Independently_Invariant_Blocks()
{
var elementType = CreateElementType(ContentVariation.Culture);
var rteDataType = await CreateRichTextDataType(elementType);
var contentType = CreateContentType(rteDataType);
var richTextValue = CreateRichTextValue(elementType);
var content = CreateContent(contentType, richTextValue);
PublishContent(content, ["en-US", "da-DK"]);
var editor = rteDataType.Editor!;
var indexValues = editor.PropertyIndexValueFactory.GetIndexValues(
content.Properties["blocks"]!,
culture: null,
segment: null,
published: true,
availableCultures: ["en-US", "da-DK"],
contentTypeDictionary: new Dictionary<Guid, IContentType>
{
{ elementType.Key, elementType }, { contentType.Key, contentType }
});
Assert.AreEqual(3, indexValues.Count());
Assert.NotNull(indexValues.FirstOrDefault(value => value.FieldName.StartsWith(UmbracoExamineFieldNames.RawFieldPrefix)));
AssertIndexedValues(
"en-US",
"Some text.",
"More text.",
"Even more text.",
"The end.",
"#1: The first invariant content value",
"#1: The first content value in English",
"#2: The first invariant content value",
"#2: The first content value in English",
"#3: The first invariant content value",
"#3: The first content value in English");
AssertIndexedValues(
"da-DK",
"Some text.",
"More text.",
"Even more text.",
"The end.",
"#1: The first invariant content value",
"#1: The first content value in Danish",
"#2: The first invariant content value",
"#2: The first content value in Danish",
"#3: The first invariant content value",
"#3: The first content value in Danish");
void AssertIndexedValues(string culture, params string[] expectedIndexedValues)
{
var indexValue = indexValues.FirstOrDefault(v => v.Culture.InvariantEquals(culture));
Assert.IsNotNull(indexValue);
Assert.AreEqual(1, indexValue.Values.Count());
var indexedValue = indexValue.Values.First() as string;
Assert.IsNotNull(indexedValue);
var values = indexedValue.Split(Environment.NewLine, StringSplitOptions.RemoveEmptyEntries).Select(s => s.Trim()).ToArray();
Assert.AreEqual(expectedIndexedValues.Length, values.Length);
Assert.IsTrue(values.ContainsAll(expectedIndexedValues));
}
}
[TestCase(true)]
[TestCase(false)]
public async Task Can_Index_With_Unexposed_Blocks(bool published)
{
var elementType = CreateElementType(ContentVariation.Culture);
var rteDataType = await CreateRichTextDataType(elementType);
var contentType = CreateContentType(rteDataType);
var richTextValue = CreateRichTextValue(elementType);
richTextValue.Blocks!.Expose.RemoveAll(e => e.Culture == "da-DK");
var content = CreateContent(contentType, richTextValue);
PublishContent(content, ["en-US", "da-DK"]);
var editor = rteDataType.Editor!;
var indexValues = editor.PropertyIndexValueFactory.GetIndexValues(
content.Properties["blocks"]!,
culture: null,
segment: null,
published: published,
availableCultures: ["en-US", "da-DK"],
contentTypeDictionary: new Dictionary<Guid, IContentType>
{
{ elementType.Key, elementType }, { contentType.Key, contentType }
});
Assert.AreEqual(3, indexValues.Count());
Assert.NotNull(indexValues.FirstOrDefault(value => value.FieldName.StartsWith(UmbracoExamineFieldNames.RawFieldPrefix)));
if (published)
{
AssertIndexedValues(
"da-DK",
"Some text.",
"More text.",
"Even more text.",
"The end.");
}
else
{
AssertIndexedValues(
"da-DK",
"Some text.",
"More text.",
"Even more text.",
"The end.",
"#1: The first invariant content value",
"#1: The first content value in Danish",
"#2: The first invariant content value",
"#2: The first content value in Danish",
"#3: The first invariant content value",
"#3: The first content value in Danish");
}
AssertIndexedValues(
"en-US",
"Some text.",
"More text.",
"Even more text.",
"The end.",
"#1: The first invariant content value",
"#1: The first content value in English",
"#2: The first invariant content value",
"#2: The first content value in English",
"#3: The first invariant content value",
"#3: The first content value in English");
void AssertIndexedValues(string culture, params string[] expectedIndexedValues)
{
var indexValue = indexValues.FirstOrDefault(v => v.Culture.InvariantEquals(culture));
Assert.IsNotNull(indexValue);
Assert.AreEqual(1, indexValue.Values.Count());
var indexedValue = indexValue.Values.First() as string;
Assert.IsNotNull(indexedValue);
var values = indexedValue.Split(Environment.NewLine, StringSplitOptions.RemoveEmptyEntries).Select(s => s.Trim()).ToArray();
Assert.AreEqual(expectedIndexedValues.Length, values.Length);
Assert.IsTrue(values.ContainsAll(expectedIndexedValues));
}
}
[TestCase(ContentVariation.Culture)]
[TestCase(ContentVariation.Nothing)]
public async Task Can_Index_Cultures_Independently_Variant_Blocks(ContentVariation elementTypeVariation)
{
var elementType = CreateElementType(elementTypeVariation);
var rteDataType = await CreateRichTextDataType(elementType);
var contentType = CreateContentType(ContentVariation.Culture, rteDataType, ContentVariation.Culture);
var englishRichTextValue = CreateInvariantRichTextValue("en-US");
var danishRichTextValue = CreateInvariantRichTextValue("da-DK");
var content = CreateContent(contentType);
content.Properties["blocks"]!.SetValue(JsonSerializer.Serialize(englishRichTextValue), "en-US");
content.Properties["blocks"]!.SetValue(JsonSerializer.Serialize(danishRichTextValue), "da-DK");
ContentService.Save(content);
PublishContent(content, ["en-US", "da-DK"]);
var editor = rteDataType.Editor!;
AssertIndexedValues(
"en-US",
"Some text for en-US.",
"More text for en-US.",
"invariantText value for en-US",
"variantText value for en-US");
AssertIndexedValues(
"da-DK",
"Some text for da-DK.",
"More text for da-DK.",
"invariantText value for da-DK",
"variantText value for da-DK");
void AssertIndexedValues(string culture, params string[] expectedIndexedValues)
{
var indexValues = editor.PropertyIndexValueFactory.GetIndexValues(
content.Properties["blocks"]!,
culture: culture,
segment: null,
published: true,
availableCultures: ["en-US", "da-DK"],
contentTypeDictionary: new Dictionary<Guid, IContentType>
{
{ elementType.Key, elementType }, { contentType.Key, contentType }
});
Assert.AreEqual(2, indexValues.Count());
Assert.NotNull(indexValues.FirstOrDefault(value => value.FieldName.StartsWith(UmbracoExamineFieldNames.RawFieldPrefix)));
var indexValue = indexValues.FirstOrDefault(v => v.Culture.InvariantEquals(culture) && v.FieldName == "blocks");
Assert.IsNotNull(indexValue);
Assert.AreEqual(1, indexValue.Values.Count());
var indexedValue = indexValue.Values.First() as string;
Assert.IsNotNull(indexedValue);
var values = indexedValue.Split(Environment.NewLine, StringSplitOptions.RemoveEmptyEntries).Select(s => s.Trim()).ToArray();
Assert.AreEqual(expectedIndexedValues.Length, values.Length);
Assert.IsTrue(values.ContainsAll(expectedIndexedValues));
}
RichTextEditorValue CreateInvariantRichTextValue(string culture)
{
var contentElementKey = Guid.NewGuid();
return new RichTextEditorValue
{
Markup = $"""
<p>Some text for {culture}.</p>
<umb-rte-block data-content-key="{contentElementKey:D}"><!--Umbraco-Block--></umb-rte-block>
<p>More text for {culture}.</p>
""",
Blocks = new RichTextBlockValue([
new RichTextBlockLayoutItem(contentElementKey)
])
{
ContentData =
[
new(contentElementKey, elementType.Key, elementType.Alias)
{
Values =
[
new() { Alias = "invariantText", Value = $"invariantText value for {culture}" },
new() { Alias = "variantText", Value = $"variantText value for {culture}" }
]
}
],
SettingsData = [],
Expose =
[
new(contentElementKey, culture, null),
]
}
};
}
}
private async Task<IDataType> CreateRichTextDataType(IContentType elementType)
=> await CreateBlockEditorDataType(
Constants.PropertyEditors.Aliases.RichText,
@@ -536,7 +778,7 @@ public class RichTextElementLevelVariationTests : BlockEditorElementVariationTes
};
}
private IContent CreateContent(IContentType contentType, RichTextEditorValue richTextValue)
private IContent CreateContent(IContentType contentType, RichTextEditorValue? richTextValue = null)
{
var contentBuilder = new ContentBuilder()
.WithContentType(contentType);
@@ -554,8 +796,11 @@ public class RichTextElementLevelVariationTests : BlockEditorElementVariationTes
var content = contentBuilder.Build();
var propertyValue = JsonSerializer.Serialize(richTextValue);
content.Properties["blocks"]!.SetValue(propertyValue);
if (richTextValue is not null)
{
var propertyValue = JsonSerializer.Serialize(richTextValue);
content.Properties["blocks"]!.SetValue(propertyValue);
}
ContentService.Save(content);
return content;

View File

@@ -158,6 +158,9 @@
<Compile Update="Umbraco.Core\Services\MediaTypeEditingServiceTests.GetFolderMediaTypes.cs">
<DependentUpon>MediaTypeEditingServiceTests.cs</DependentUpon>
</Compile>
<Compile Update="Umbraco.Infrastructure\PropertyEditors\BlockListElementLevelVariationTests.Indexing.cs">
<DependentUpon>BlockListElementLevelVariationTests.cs</DependentUpon>
</Compile>
<Compile Update="Umbraco.Infrastructure\PropertyEditors\BlockListElementLevelVariationTests.Parsing.cs">
<DependentUpon>BlockListElementLevelVariationTests.cs</DependentUpon>
</Compile>