Merge remote-tracking branch 'origin/v12/dev' into v13/dev

# Conflicts:
#	src/Umbraco.Core/PropertyEditors/DataValueReferenceFactoryCollection.cs
#	src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentRepositoryBase.cs
#	src/Umbraco.Infrastructure/PropertyEditors/BlockEditorPropertyValueEditor.cs
#	tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/DataValueEditorReuseTests.cs
#	tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/DataValueReferenceFactoryCollectionTests.cs
This commit is contained in:
Bjarke Berg
2024-01-23 10:15:49 +01:00
17 changed files with 323 additions and 187 deletions

View File

@@ -1082,20 +1082,24 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement
protected void PersistRelations(TEntity entity)
{
// Get all references from our core built in DataEditors/Property Editors
// Along with seeing if developers want to collect additional references from the DataValueReferenceFactories collection
// Get all references and automatic relation type aliases
ISet<UmbracoEntityReference> references = _dataValueReferenceFactories.GetAllReferences(entity.Properties, PropertyEditors);
// First delete all auto-relations for this entity
ISet<string> automaticRelationTypeAliases = _dataValueReferenceFactories.GetAutomaticRelationTypesAliases(entity.Properties, PropertyEditors);
RelationRepository.DeleteByParent(entity.Id, automaticRelationTypeAliases.ToArray());
ISet<string> automaticRelationTypeAliases = _dataValueReferenceFactories.GetAllAutomaticRelationTypesAliases(PropertyEditors);
if (references.Count == 0)
{
// Delete all relations using the automatic relation type aliases
RelationRepository.DeleteByParent(entity.Id, automaticRelationTypeAliases.ToArray());
// No need to add new references/relations
return;
}
// Lookup all relation type IDs
var relationTypeLookup = RelationTypeRepository.GetMany(Array.Empty<int>())
.Where(x => automaticRelationTypeAliases.Contains(x.Alias))
.ToDictionary(x => x.Alias, x => x.Id);
// Lookup node IDs for all GUID based UDIs
IEnumerable<Guid> keys = references.Select(x => x.Udi).OfType<GuidUdi>().Select(x => x.Guid);
var keysLookup = Database.FetchByGroups<NodeIdKey, Guid>(keys, Constants.Sql.MaxParameterCount, guids =>
@@ -1106,15 +1110,17 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement
.WhereIn<NodeDto>(x => x.UniqueId, guids);
}).ToDictionary(x => x.UniqueId, x => x.NodeId);
// Lookup all relation type IDs
var relationTypeLookup = RelationTypeRepository.GetMany(Array.Empty<int>()).ToDictionary(x => x.Alias, x => x.Id);
// Get all valid relations
var relations = new List<ReadOnlyRelation>(references.Count);
var relations = new List<(int ChildId, int RelationTypeId)>(references.Count);
foreach (UmbracoEntityReference reference in references)
{
if (!automaticRelationTypeAliases.Contains(reference.RelationTypeAlias))
if (string.IsNullOrEmpty(reference.RelationTypeAlias))
{
// Reference does not specify a relation type alias, so skip adding a relation
Logger.LogDebug("The reference to {Udi} does not specify a relation type alias, so it will not be saved as relation.", reference.Udi);
}
else if (!automaticRelationTypeAliases.Contains(reference.RelationTypeAlias))
{
// Returning a reference that doesn't use an automatic relation type is an issue that should be fixed in code
Logger.LogError("The reference to {Udi} uses a relation type {RelationTypeAlias} that is not an automatic relation type.", reference.Udi, reference.RelationTypeAlias);
}
@@ -1130,12 +1136,24 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement
}
else
{
relations.Add(new ReadOnlyRelation(entity.Id, id, relationTypeId));
relations.Add((id, relationTypeId));
}
}
// Save bulk relations
RelationRepository.SaveBulk(relations);
// Get all existing relations (optimize for adding new and keeping existing relations)
var query = Query<IRelation>().Where(x => x.ParentId == entity.Id).WhereIn(x => x.RelationTypeId, relationTypeLookup.Values);
var existingRelations = RelationRepository.GetPagedRelationsByQuery(query, 0, int.MaxValue, out _, null)
.ToDictionary(x => (x.ChildId, x.RelationTypeId)); // Relations are unique by parent ID, child ID and relation type ID
// Add relations that don't exist yet
var relationsToAdd = relations.Except(existingRelations.Keys).Select(x => new ReadOnlyRelation(entity.Id, x.ChildId, x.RelationTypeId));
RelationRepository.SaveBulk(relationsToAdd);
// Delete relations that don't exist anymore
foreach (IRelation relation in existingRelations.Where(x => !relations.Contains(x.Key)).Select(x => x.Value))
{
RelationRepository.Delete(relation);
}
}
/// <summary>

View File

@@ -16,18 +16,27 @@ namespace Umbraco.Cms.Core.PropertyEditors;
internal abstract class BlockEditorPropertyValueEditor : BlockValuePropertyValueEditorBase
{
private BlockEditorValues? _blockEditorValues;
private readonly IDataTypeService _dataTypeService;
private readonly PropertyEditorCollection _propertyEditors;
private readonly DataValueReferenceFactoryCollection _dataValueReferenceFactories;
private readonly ILogger<BlockEditorPropertyValueEditor> _logger;
protected BlockEditorPropertyValueEditor(
DataEditorAttribute attribute,
PropertyEditorCollection propertyEditors,
DataValueReferenceFactoryCollection dataValueReferenceFactories,
IDataTypeService dataTypeService,
ILocalizedTextService textService,
ILogger<BlockEditorPropertyValueEditor> logger,
IShortStringHelper shortStringHelper,
IJsonSerializer jsonSerializer,
IIOHelper ioHelper)
: base(attribute, propertyEditors, dataTypeService, textService, logger, shortStringHelper, jsonSerializer, ioHelper)
: base(attribute, propertyEditors, dataTypeService, textService, logger, shortStringHelper, jsonSerializer, ioHelper, dataValueReferenceFactories)
{
_propertyEditors = propertyEditors;
_dataValueReferenceFactories = dataValueReferenceFactories;
_dataTypeService = dataTypeService;
_logger = logger;
}
protected BlockEditorValues BlockEditorValues
@@ -39,30 +48,57 @@ internal abstract class BlockEditorPropertyValueEditor : BlockValuePropertyValue
/// <inheritdoc />
public override IEnumerable<UmbracoEntityReference> GetReferences(object? value)
{
var rawJson = value == null ? string.Empty : value is string str ? str : value.ToString();
var result = new List<UmbracoEntityReference>();
BlockEditorData? blockEditorData = BlockEditorValues.DeserializeAndClean(rawJson);
if (blockEditorData == null)
// Group by property editor alias to avoid duplicate lookups and optimize value parsing
foreach (var valuesByPropertyEditorAlias in GetAllPropertyValues(value).GroupBy(x => x.PropertyType.PropertyEditorAlias, x => x.Value))
{
return Enumerable.Empty<UmbracoEntityReference>();
}
if (!_propertyEditors.TryGet(valuesByPropertyEditorAlias.Key, out IDataEditor? dataEditor))
{
continue;
}
return GetBlockValueReferences(blockEditorData.BlockValue);
// Use distinct values to avoid duplicate parsing of the same value
foreach (UmbracoEntityReference reference in _dataValueReferenceFactories.GetReferences(dataEditor, valuesByPropertyEditorAlias.Distinct()))
{
yield return reference;
}
}
}
/// <inheritdoc />
public override IEnumerable<ITag> GetTags(object? value, object? dataTypeConfiguration, int? languageId)
{
foreach (BlockItemData.BlockPropertyValue propertyValue in GetAllPropertyValues(value))
{
if (!_propertyEditors.TryGet(propertyValue.PropertyType.PropertyEditorAlias, out IDataEditor? dataEditor) ||
dataEditor.GetValueEditor() is not IDataValueTags dataValueTags)
{
continue;
}
object? configuration = _dataTypeService.GetDataType(propertyValue.PropertyType.DataTypeKey)?.Configuration;
foreach (ITag tag in dataValueTags.GetTags(propertyValue.Value, configuration, languageId))
{
yield return tag;
}
}
}
private IEnumerable<BlockItemData.BlockPropertyValue> GetAllPropertyValues(object? value)
{
var rawJson = value == null ? string.Empty : value is string str ? str : value.ToString();
BlockEditorData? blockEditorData = BlockEditorValues.DeserializeAndClean(rawJson);
if (blockEditorData == null)
if (blockEditorData is null)
{
return Enumerable.Empty<ITag>();
yield break;
}
return GetBlockValueTags(blockEditorData.BlockValue, languageId);
// Return all property values from the content and settings data
IEnumerable<BlockItemData> data = blockEditorData.BlockValue.ContentData.Concat(blockEditorData.BlockValue.SettingsData);
foreach (BlockItemData.BlockPropertyValue propertyValue in data.SelectMany(x => x.PropertyValues.Select(x => x.Value)))
{
yield return propertyValue;
}
}
// note: there is NO variant support here

View File

@@ -50,6 +50,7 @@ public abstract class BlockGridPropertyEditorBase : DataEditor
public BlockGridEditorPropertyValueEditor(
DataEditorAttribute attribute,
PropertyEditorCollection propertyEditors,
DataValueReferenceFactoryCollection dataValueReferenceFactories,
IDataTypeService dataTypeService,
ILocalizedTextService textService,
ILogger<BlockEditorPropertyValueEditor> logger,
@@ -58,7 +59,7 @@ public abstract class BlockGridPropertyEditorBase : DataEditor
IIOHelper ioHelper,
IContentTypeService contentTypeService,
IPropertyValidationService propertyValidationService)
: base(attribute, propertyEditors, dataTypeService, textService, logger, shortStringHelper, jsonSerializer, ioHelper)
: base(attribute, propertyEditors, dataValueReferenceFactories, dataTypeService, textService, logger, shortStringHelper, jsonSerializer, ioHelper)
{
BlockEditorValues = new BlockEditorValues(new BlockGridEditorDataConverter(jsonSerializer), contentTypeService, logger);
Validators.Add(new BlockEditorValidator(propertyValidationService, BlockEditorValues, contentTypeService));

View File

@@ -52,6 +52,7 @@ public abstract class BlockListPropertyEditorBase : DataEditor
DataEditorAttribute attribute,
BlockEditorDataConverter blockEditorDataConverter,
PropertyEditorCollection propertyEditors,
DataValueReferenceFactoryCollection dataValueReferenceFactories,
IDataTypeService dataTypeService,
IContentTypeService contentTypeService,
ILocalizedTextService textService,
@@ -60,7 +61,7 @@ public abstract class BlockListPropertyEditorBase : DataEditor
IJsonSerializer jsonSerializer,
IIOHelper ioHelper,
IPropertyValidationService propertyValidationService) :
base(attribute, propertyEditors, dataTypeService, textService, logger, shortStringHelper, jsonSerializer, ioHelper)
base(attribute, propertyEditors, dataValueReferenceFactories,dataTypeService, textService, logger, shortStringHelper, jsonSerializer, ioHelper)
{
BlockEditorValues = new BlockEditorValues(blockEditorDataConverter, contentTypeService, logger);
Validators.Add(new BlockEditorValidator(propertyValidationService, BlockEditorValues, contentTypeService));

View File

@@ -14,6 +14,7 @@ internal abstract class BlockValuePropertyValueEditorBase : DataValueEditor, IDa
private readonly IDataTypeService _dataTypeService;
private readonly PropertyEditorCollection _propertyEditors;
private readonly ILogger _logger;
private readonly DataValueReferenceFactoryCollection _dataValueReferenceFactoryCollection;
protected BlockValuePropertyValueEditorBase(
DataEditorAttribute attribute,
@@ -23,12 +24,14 @@ internal abstract class BlockValuePropertyValueEditorBase : DataValueEditor, IDa
ILogger logger,
IShortStringHelper shortStringHelper,
IJsonSerializer jsonSerializer,
IIOHelper ioHelper)
IIOHelper ioHelper,
DataValueReferenceFactoryCollection dataValueReferenceFactoryCollection)
: base(textService, shortStringHelper, jsonSerializer, ioHelper, attribute)
{
_propertyEditors = propertyEditors;
_dataTypeService = dataTypeService;
_logger = logger;
_dataValueReferenceFactoryCollection = dataValueReferenceFactoryCollection;
}
/// <inheritdoc />
@@ -36,30 +39,33 @@ internal abstract class BlockValuePropertyValueEditorBase : DataValueEditor, IDa
protected IEnumerable<UmbracoEntityReference> GetBlockValueReferences(BlockValue blockValue)
{
var result = new List<UmbracoEntityReference>();
// loop through all content and settings data
foreach (BlockItemData row in blockValue.ContentData.Concat(blockValue.SettingsData))
BlockItemData.BlockPropertyValue[] propertyValues = blockValue.ContentData.Concat(blockValue.SettingsData)
.SelectMany(x => x.PropertyValues.Values).ToArray();
foreach (IGrouping<string, object?> valuesByPropertyEditorAlias in propertyValues.GroupBy(x => x.PropertyType.PropertyEditorAlias, x => x.Value))
{
foreach (KeyValuePair<string, BlockItemData.BlockPropertyValue> prop in row.PropertyValues)
if (!_propertyEditors.TryGet(valuesByPropertyEditorAlias.Key, out IDataEditor? dataEditor))
{
IDataEditor? propEditor = _propertyEditors[prop.Value.PropertyType.PropertyEditorAlias];
IDataValueEditor? valueEditor = propEditor?.GetValueEditor();
if (!(valueEditor is IDataValueReference reference))
{
continue;
}
var val = prop.Value.Value?.ToString();
IEnumerable<UmbracoEntityReference> refs = reference.GetReferences(val);
result.AddRange(refs);
continue;
}
}
return result;
var districtValues = valuesByPropertyEditorAlias.Distinct().ToArray();
if (dataEditor.GetValueEditor() is IDataValueReference reference)
{
foreach (UmbracoEntityReference value in districtValues.SelectMany(reference.GetReferences))
{
yield return value;
}
}
IEnumerable<UmbracoEntityReference> references = _dataValueReferenceFactoryCollection.GetReferences(dataEditor, districtValues);
foreach (UmbracoEntityReference value in references)
{
yield return value;
}
}
}
/// <inheritdoc />

View File

@@ -91,9 +91,10 @@ public class NestedContentPropertyEditor : DataEditor
internal class NestedContentPropertyValueEditor : DataValueEditor, IDataValueReference, IDataValueTags
{
private readonly IDataTypeService _dataTypeService;
private readonly PropertyEditorCollection _propertyEditors;
private readonly DataValueReferenceFactoryCollection _dataValueReferenceFactories;
private readonly ILogger<NestedContentPropertyEditor> _logger;
private readonly NestedContentValues _nestedContentValues;
private readonly PropertyEditorCollection _propertyEditors;
public NestedContentPropertyValueEditor(
IDataTypeService dataTypeService,
@@ -102,16 +103,19 @@ public class NestedContentPropertyEditor : DataEditor
IShortStringHelper shortStringHelper,
DataEditorAttribute attribute,
PropertyEditorCollection propertyEditors,
DataValueReferenceFactoryCollection dataValueReferenceFactories,
ILogger<NestedContentPropertyEditor> logger,
IJsonSerializer jsonSerializer,
IIOHelper ioHelper,
IPropertyValidationService propertyValidationService)
: base(localizedTextService, shortStringHelper, jsonSerializer, ioHelper, attribute)
{
_propertyEditors = propertyEditors;
_dataTypeService = dataTypeService;
_propertyEditors = propertyEditors;
_dataValueReferenceFactories = dataValueReferenceFactories;
_logger = logger;
_nestedContentValues = new NestedContentValues(contentTypeService);
Validators.Add(new NestedContentValidator(propertyValidationService, _nestedContentValues, contentTypeService));
}
@@ -139,66 +143,47 @@ public class NestedContentPropertyEditor : DataEditor
}
}
/// <inheritdoc />
public IEnumerable<UmbracoEntityReference> GetReferences(object? value)
{
var rawJson = value == null ? string.Empty : value is string str ? str : value.ToString();
var result = new List<UmbracoEntityReference>();
foreach (NestedContentValues.NestedContentRowValue row in _nestedContentValues.GetPropertyValues(rawJson))
// Group by property editor alias to avoid duplicate lookups and optimize value parsing
foreach (var valuesByPropertyEditorAlias in GetAllPropertyValues(value).GroupBy(x => x.PropertyType.PropertyEditorAlias, x => x.Value))
{
foreach (KeyValuePair<string, NestedContentValues.NestedContentPropertyValue> prop in
row.PropertyValues)
if (!_propertyEditors.TryGet(valuesByPropertyEditorAlias.Key, out IDataEditor? dataEditor))
{
IDataEditor? propEditor = _propertyEditors[prop.Value.PropertyType.PropertyEditorAlias];
continue;
}
IDataValueEditor? valueEditor = propEditor?.GetValueEditor();
if (!(valueEditor is IDataValueReference reference))
{
continue;
}
var val = prop.Value.Value?.ToString();
IEnumerable<UmbracoEntityReference> refs = reference.GetReferences(val);
result.AddRange(refs);
// Use distinct values to avoid duplicate parsing of the same value
foreach (UmbracoEntityReference reference in _dataValueReferenceFactories.GetReferences(dataEditor, valuesByPropertyEditorAlias.Distinct()))
{
yield return reference;
}
}
return result;
}
/// <inheritdoc />
public IEnumerable<ITag> GetTags(object? value, object? dataTypeConfiguration, int? languageId)
{
IReadOnlyList<NestedContentValues.NestedContentRowValue> rows =
_nestedContentValues.GetPropertyValues(value);
var result = new List<ITag>();
foreach (NestedContentValues.NestedContentRowValue row in rows.ToList())
foreach (NestedContentValues.NestedContentPropertyValue propertyValue in GetAllPropertyValues(value))
{
foreach (KeyValuePair<string, NestedContentValues.NestedContentPropertyValue> prop in row.PropertyValues
.ToList())
if (!_propertyEditors.TryGet(propertyValue.PropertyType.PropertyEditorAlias, out IDataEditor? dataEditor) ||
dataEditor.GetValueEditor() is not IDataValueTags dataValueTags)
{
IDataEditor? propEditor = _propertyEditors[prop.Value.PropertyType.PropertyEditorAlias];
continue;
}
IDataValueEditor? valueEditor = propEditor?.GetValueEditor();
if (valueEditor is not IDataValueTags tagsProvider)
{
continue;
}
object? configuration = _dataTypeService.GetDataType(prop.Value.PropertyType.DataTypeKey)?.Configuration;
result.AddRange(tagsProvider.GetTags(prop.Value.Value, configuration, languageId));
object? configuration = _dataTypeService.GetDataType(propertyValue.PropertyType.DataTypeKey)?.Configuration;
foreach (ITag tag in dataValueTags.GetTags(propertyValue.Value, configuration, languageId))
{
yield return tag;
}
}
return result;
}
private IEnumerable<NestedContentValues.NestedContentPropertyValue> GetAllPropertyValues(object? value)
=> _nestedContentValues.GetPropertyValues(value).SelectMany(x => x.PropertyValues.Values);
#region DB to String
public override string ConvertDbToString(IPropertyType propertyType, object? propertyValue)
@@ -424,7 +409,8 @@ public class NestedContentPropertyEditor : DataEditor
// set values to null
row.PropertyValues[elementTypeProp.Alias] = new NestedContentValues.NestedContentPropertyValue
{
PropertyType = elementTypeProp, Value = null,
PropertyType = elementTypeProp,
Value = null,
};
row.RawPropertyValues[elementTypeProp.Alias] = null;
}

View File

@@ -189,8 +189,9 @@ public class RichTextPropertyEditor : DataEditor
IHtmlSanitizer htmlSanitizer,
IHtmlMacroParameterParser macroParameterParser,
IContentTypeService contentTypeService,
IPropertyValidationService propertyValidationService)
: base(attribute, propertyEditors, dataTypeService, localizedTextService, logger, shortStringHelper, jsonSerializer, ioHelper)
IPropertyValidationService propertyValidationService,
DataValueReferenceFactoryCollection dataValueReferenceFactoryCollection)
: base(attribute, propertyEditors, dataTypeService, localizedTextService, logger, shortStringHelper, jsonSerializer, ioHelper, dataValueReferenceFactoryCollection)
{
_backOfficeSecurityAccessor = backOfficeSecurityAccessor;
_imageSourceParser = imageSourceParser;