diff --git a/src/Umbraco.Core/Composing/BuilderCollectionBase.cs b/src/Umbraco.Core/Composing/BuilderCollectionBase.cs index 722a6efca0..e92790ab4e 100644 --- a/src/Umbraco.Core/Composing/BuilderCollectionBase.cs +++ b/src/Umbraco.Core/Composing/BuilderCollectionBase.cs @@ -3,7 +3,7 @@ using System.Collections; namespace Umbraco.Cms.Core.Composing; /// -/// Provides a base class for builder collections. +/// Provides a base class for builder collections. /// /// The type of the items. public abstract class BuilderCollectionBase : IBuilderCollection @@ -11,21 +11,18 @@ public abstract class BuilderCollectionBase : IBuilderCollection private readonly LazyReadOnlyCollection _items; /// - /// Initializes a new instance of the with items. + /// Initializes a new instance of the with items. /// /// The items. - public BuilderCollectionBase(Func> items) => _items = new LazyReadOnlyCollection(items); + public BuilderCollectionBase(Func> items) + => _items = new LazyReadOnlyCollection(items); /// public int Count => _items.Count; - /// - /// Gets an enumerator. - /// + /// public IEnumerator GetEnumerator() => _items.GetEnumerator(); - /// - /// Gets an enumerator. - /// + /// IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); } diff --git a/src/Umbraco.Core/Composing/IBuilderCollection.cs b/src/Umbraco.Core/Composing/IBuilderCollection.cs index 56036997bc..c8920149c5 100644 --- a/src/Umbraco.Core/Composing/IBuilderCollection.cs +++ b/src/Umbraco.Core/Composing/IBuilderCollection.cs @@ -1,13 +1,16 @@ namespace Umbraco.Cms.Core.Composing; /// -/// Represents a builder collection, ie an immutable enumeration of items. +/// Represents a builder collection, ie an immutable enumeration of items. /// /// The type of the items. public interface IBuilderCollection : IEnumerable { /// - /// Gets the number of items in the collection. + /// Gets the number of items in the collection. /// + /// + /// The count. + /// int Count { get; } } diff --git a/src/Umbraco.Core/Models/Editors/UmbracoEntityReference.cs b/src/Umbraco.Core/Models/Editors/UmbracoEntityReference.cs index c093962408..3f02be10b1 100644 --- a/src/Umbraco.Core/Models/Editors/UmbracoEntityReference.cs +++ b/src/Umbraco.Core/Models/Editors/UmbracoEntityReference.cs @@ -1,49 +1,88 @@ namespace Umbraco.Cms.Core.Models.Editors; /// -/// Used to track reference to other entities in a property value +/// Used to track a reference to another entity in a property value. /// public struct UmbracoEntityReference : IEquatable { private static readonly UmbracoEntityReference _empty = new(UnknownTypeUdi.Instance, string.Empty); + /// + /// Initializes a new instance of the struct. + /// + /// The UDI. + /// The relation type alias. public UmbracoEntityReference(Udi udi, string relationTypeAlias) { Udi = udi ?? throw new ArgumentNullException(nameof(udi)); RelationTypeAlias = relationTypeAlias ?? throw new ArgumentNullException(nameof(relationTypeAlias)); } + /// + /// Initializes a new instance of the struct for a document or media item. + /// + /// The UDI. public UmbracoEntityReference(Udi udi) { Udi = udi ?? throw new ArgumentNullException(nameof(udi)); switch (udi.EntityType) { + case Constants.UdiEntityType.Document: + RelationTypeAlias = Constants.Conventions.RelationTypes.RelatedDocumentAlias; + break; case Constants.UdiEntityType.Media: RelationTypeAlias = Constants.Conventions.RelationTypes.RelatedMediaAlias; break; default: - RelationTypeAlias = Constants.Conventions.RelationTypes.RelatedDocumentAlias; + // No relation type alias convention for this entity type, so leave it empty + RelationTypeAlias = string.Empty; break; } } + /// + /// Gets the UDI. + /// + /// + /// The UDI. + /// public Udi Udi { get; } - public static UmbracoEntityReference Empty() => _empty; - - public static bool IsEmpty(UmbracoEntityReference reference) => reference == Empty(); - + /// + /// Gets the relation type alias. + /// + /// + /// The relation type alias. + /// public string RelationTypeAlias { get; } - public static bool operator ==(UmbracoEntityReference left, UmbracoEntityReference right) => left.Equals(right); + /// + /// Gets an empty reference. + /// + /// + /// An empty reference. + /// + public static UmbracoEntityReference Empty() => _empty; + /// + /// Determines whether the specified reference is empty. + /// + /// The reference. + /// + /// true if the specified reference is empty; otherwise, false. + /// + public static bool IsEmpty(UmbracoEntityReference reference) => reference == Empty(); + + /// public override bool Equals(object? obj) => obj is UmbracoEntityReference reference && Equals(reference); + /// public bool Equals(UmbracoEntityReference other) => EqualityComparer.Default.Equals(Udi, other.Udi) && RelationTypeAlias == other.RelationTypeAlias; + /// public override int GetHashCode() { var hashCode = -487348478; @@ -52,5 +91,9 @@ public struct UmbracoEntityReference : IEquatable return hashCode; } + /// + public static bool operator ==(UmbracoEntityReference left, UmbracoEntityReference right) => left.Equals(right); + + /// public static bool operator !=(UmbracoEntityReference left, UmbracoEntityReference right) => !(left == right); } diff --git a/src/Umbraco.Core/PropertyEditors/DataValueReferenceFactoryCollection.cs b/src/Umbraco.Core/PropertyEditors/DataValueReferenceFactoryCollection.cs index 24d6f17eb0..c173e766e0 100644 --- a/src/Umbraco.Core/PropertyEditors/DataValueReferenceFactoryCollection.cs +++ b/src/Umbraco.Core/PropertyEditors/DataValueReferenceFactoryCollection.cs @@ -4,64 +4,148 @@ using Umbraco.Cms.Core.Models.Editors; namespace Umbraco.Cms.Core.PropertyEditors; +/// +/// Provides a builder collection for items. +/// public class DataValueReferenceFactoryCollection : BuilderCollectionBase { - public DataValueReferenceFactoryCollection(Func> items) - : base(items) - { - } - // TODO: We could further reduce circular dependencies with PropertyEditorCollection by not having IDataValueReference implemented // by property editors and instead just use the already built in IDataValueReferenceFactory and/or refactor that into a more normal collection - public IEnumerable GetAllReferences( - IPropertyCollection properties, - PropertyEditorCollection propertyEditors) - { - var trackedRelations = new HashSet(); - foreach (IProperty p in properties) + /// + /// Initializes a new instance of the class. + /// + /// The items. + public DataValueReferenceFactoryCollection(Func> items) + : base(items) + { } + + /// + /// Gets all unique references from the specified properties. + /// + /// The properties. + /// The property editors. + /// + /// The unique references from the specified properties. + /// + public ISet GetAllReferences(IPropertyCollection properties, PropertyEditorCollection propertyEditors) + { + var references = new HashSet(); + + foreach (IProperty property in properties) { - if (!propertyEditors.TryGet(p.PropertyType.PropertyEditorAlias, out IDataEditor? editor)) + if (!propertyEditors.TryGet(property.PropertyType.PropertyEditorAlias, out IDataEditor? dataEditor)) { continue; } - // TODO: We will need to change this once we support tracking via variants/segments - // for now, we are tracking values from ALL variants - foreach (IPropertyValue propertyVal in p.Values) + // Only use edited value for now + references.UnionWith(GetReferences(dataEditor, property.Values.Select(x => x.EditedValue))); + } + + return references; + } + + /// + /// Gets the references. + /// + /// The data editor. + /// The values. + /// + /// The references. + /// + public IEnumerable GetReferences(IDataEditor dataEditor, params object?[] values) + => GetReferences(dataEditor, (IEnumerable)values); + + /// + /// Gets the references. + /// + /// The data editor. + /// The values. + /// + /// The references. + /// + public IEnumerable GetReferences(IDataEditor dataEditor, IEnumerable values) + { + // TODO: We will need to change this once we support tracking via variants/segments + // for now, we are tracking values from ALL variants + if (dataEditor.GetValueEditor() is IDataValueReference dataValueReference) + { + foreach (UmbracoEntityReference reference in values.SelectMany(dataValueReference.GetReferences)) { - var val = propertyVal.EditedValue; - - IDataValueEditor? valueEditor = editor?.GetValueEditor(); - if (valueEditor is IDataValueReference reference) - { - IEnumerable refs = reference.GetReferences(val); - foreach (UmbracoEntityReference r in refs) - { - trackedRelations.Add(r); - } - } - - // Loop over collection that may be add to existing property editors - // implementation of GetReferences in IDataValueReference. - // Allows developers to add support for references by a - // package /property editor that did not implement IDataValueReference themselves - foreach (IDataValueReferenceFactory item in this) - { - // Check if this value reference is for this datatype/editor - // Then call it's GetReferences method - to see if the value stored - // in the dataeditor/property has referecnes to media/content items - if (item.IsForEditor(editor)) - { - foreach (UmbracoEntityReference r in item.GetDataValueReference().GetReferences(val)) - { - trackedRelations.Add(r); - } - } - } + yield return reference; } } - return trackedRelations; + // Loop over collection that may be add to existing property editors + // implementation of GetReferences in IDataValueReference. + // Allows developers to add support for references by a + // package /property editor that did not implement IDataValueReference themselves + foreach (IDataValueReferenceFactory dataValueReferenceFactory in this) + { + // Check if this value reference is for this datatype/editor + // Then call it's GetReferences method - to see if the value stored + // in the dataeditor/property has references to media/content items + if (dataValueReferenceFactory.IsForEditor(dataEditor)) + { + IDataValueReference factoryDataValueReference = dataValueReferenceFactory.GetDataValueReference(); + foreach (UmbracoEntityReference reference in values.SelectMany(factoryDataValueReference.GetReferences)) + { + yield return reference; + } + } + } + } + + /// + /// Gets all relation type aliases that are automatically tracked. + /// + /// The property editors. + /// + /// All relation type aliases that are automatically tracked. + /// + public ISet GetAllAutomaticRelationTypesAliases(PropertyEditorCollection propertyEditors) + { + // Always add default automatic relation types + var automaticRelationTypeAliases = new HashSet(Constants.Conventions.RelationTypes.AutomaticRelationTypes); + + // Add relation types for all property editors + foreach (IDataEditor dataEditor in propertyEditors) + { + automaticRelationTypeAliases.UnionWith(GetAutomaticRelationTypesAliases(dataEditor)); + } + + return automaticRelationTypeAliases; + } + + /// + /// Gets the automatic relation types aliases. + /// + /// The data editor. + /// + /// The automatic relation types aliases. + /// + public IEnumerable GetAutomaticRelationTypesAliases(IDataEditor dataEditor) + { + if (dataEditor.GetValueEditor() is IDataValueReference dataValueReference) + { + // Return custom relation types from value editor implementation + foreach (var alias in dataValueReference.GetAutomaticRelationTypesAliases()) + { + yield return alias; + } + } + + foreach (IDataValueReferenceFactory dataValueReferenceFactory in this) + { + if (dataValueReferenceFactory.IsForEditor(dataEditor)) + { + // Return custom relation types from factory + foreach (var alias in dataValueReferenceFactory.GetDataValueReference().GetAutomaticRelationTypesAliases()) + { + yield return alias; + } + } + } } } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentRepositoryBase.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentRepositoryBase.cs index 4f6aa228d0..6569ad4d89 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentRepositoryBase.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentRepositoryBase.cs @@ -1083,91 +1083,59 @@ 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 deverlopers want to collect additional references from the DataValueReferenceFactories collection - var trackedRelations = new List(); - trackedRelations.AddRange(_dataValueReferenceFactories.GetAllReferences(entity.Properties, PropertyEditors)); - - var relationTypeAliases = GetAutomaticRelationTypesAliases(entity.Properties, PropertyEditors).ToList(); - - // At this point we potentially have a problem (see for example: https://github.com/umbraco/Umbraco.Forms.Issues/issues/1129). - // If we have a custom relation type (i.e. not document or media) and use of this within a block grid/list property editor, - // we'll get an error when saving the relations. - // It happens because the block grid doesn't expose the automatic relation type aliases for all of it's nested properties, and - // as such relationTypeAliases does not contain the custom relation type alias. - // Auto-relations of that type are then then not deleted, and we get duplicate error on insert. - // To resolve we can look at the relations we are going to be saving, and if they include any relation type aliases we haven't - // already identified, we'll add them in, so they will also be removed along with the other auto-relations. - relationTypeAliases.AddRange(trackedRelations.Select(x => x.RelationTypeAlias).Distinct().Except(relationTypeAliases)); + // Along with seeing if developers want to collect additional references from the DataValueReferenceFactories collection + ISet references = _dataValueReferenceFactories.GetAllReferences(entity.Properties, PropertyEditors); // First delete all auto-relations for this entity - RelationRepository.DeleteByParent(entity.Id, relationTypeAliases.ToArray()); + ISet automaticRelationTypeAliases = _dataValueReferenceFactories.GetAllAutomaticRelationTypesAliases(PropertyEditors); + RelationRepository.DeleteByParent(entity.Id, automaticRelationTypeAliases.ToArray()); - if (trackedRelations.Count == 0) + if (references.Count == 0) { + // No need to add new references/relations return; } - trackedRelations = trackedRelations.Distinct().ToList(); - var udiToGuids = trackedRelations.Select(x => x.Udi as GuidUdi) - .ToDictionary(x => (Udi)x!, x => x!.Guid); - - // lookup in the DB all INT ids for the GUIDs and chuck into a dictionary - var keyToIds = Database.Fetch(Sql() - .Select(x => x.NodeId, x => x.UniqueId) - .From() - .WhereIn(x => x.UniqueId, udiToGuids.Values)) - .ToDictionary(x => x.UniqueId, x => x.NodeId); - - var allRelationTypes = RelationTypeRepository.GetMany(Array.Empty())? - .ToDictionary(x => x.Alias, x => x); - - IEnumerable toSave = trackedRelations.Select(rel => - { - if (allRelationTypes is null || !allRelationTypes.TryGetValue(rel.RelationTypeAlias, out IRelationType? relationType)) - { - throw new InvalidOperationException($"The relation type {rel.RelationTypeAlias} does not exist"); - } - - if (!udiToGuids.TryGetValue(rel.Udi, out Guid guid)) - { - return null; // This shouldn't happen! - } - - if (!keyToIds.TryGetValue(guid, out var id)) - { - return null; // This shouldn't happen! - } - - return new ReadOnlyRelation(entity.Id, id, relationType.Id); - }).WhereNotNull(); - - // Save bulk relations - RelationRepository.SaveBulk(toSave); - } - - private IEnumerable GetAutomaticRelationTypesAliases( - IPropertyCollection properties, - PropertyEditorCollection propertyEditors) - { - var automaticRelationTypesAliases = new HashSet(Constants.Conventions.RelationTypes.AutomaticRelationTypes); - - foreach (IProperty property in properties) + // Lookup node IDs for all GUID based UDIs + IEnumerable keys = references.Select(x => x.Udi).OfType().Select(x => x.Guid); + var keysLookup = Database.FetchByGroups(keys, Constants.Sql.MaxParameterCount, guids => { - if (propertyEditors.TryGet(property.PropertyType.PropertyEditorAlias, out IDataEditor? editor) is false ) - { - continue; - } + return Sql() + .Select(x => x.NodeId, x => x.UniqueId) + .From() + .WhereIn(x => x.UniqueId, guids); + }).ToDictionary(x => x.UniqueId, x => x.NodeId); - if (editor.GetValueEditor() is IDataValueReference reference) + // Lookup all relation type IDs + var relationTypeLookup = RelationTypeRepository.GetMany(Array.Empty()).ToDictionary(x => x.Alias, x => x.Id); + + // Get all valid relations + var relations = new List(references.Count); + foreach (UmbracoEntityReference reference in references) + { + if (!automaticRelationTypeAliases.Contains(reference.RelationTypeAlias)) { - foreach (var alias in reference.GetAutomaticRelationTypesAliases()) - { - automaticRelationTypesAliases.Add(alias); - } + // 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); + } + else if (!relationTypeLookup.TryGetValue(reference.RelationTypeAlias, out int relationTypeId)) + { + // A non-existent relation type could be caused by an environment issue (e.g. it was manually removed) + Logger.LogWarning("The reference to {Udi} uses a relation type {RelationTypeAlias} that does not exist.", reference.Udi, reference.RelationTypeAlias); + } + else if (reference.Udi is not GuidUdi udi || !keysLookup.TryGetValue(udi.Guid, out var id)) + { + // Relations only support references to items that are stored in the NodeDto table (because of foreign key constraints) + Logger.LogInformation("The reference to {Udi} can not be saved as relation, because doesn't have a node ID.", reference.Udi); + } + else + { + relations.Add(new ReadOnlyRelation(entity.Id, id, relationTypeId)); } } - return automaticRelationTypesAliases; + // Save bulk relations + RelationRepository.SaveBulk(relations); } /// diff --git a/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorPropertyValueEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorPropertyValueEditor.cs index c524c2c39b..9739553b99 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorPropertyValueEditor.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorPropertyValueEditor.cs @@ -17,12 +17,14 @@ internal abstract class BlockEditorPropertyValueEditor : DataValueEditor, IDataV { private BlockEditorValues? _blockEditorValues; private readonly IDataTypeService _dataTypeService; - private readonly ILogger _logger; private readonly PropertyEditorCollection _propertyEditors; + private readonly DataValueReferenceFactoryCollection _dataValueReferenceFactories; + private readonly ILogger _logger; protected BlockEditorPropertyValueEditor( DataEditorAttribute attribute, PropertyEditorCollection propertyEditors, + DataValueReferenceFactoryCollection dataValueReferenceFactories, IDataTypeService dataTypeService, ILocalizedTextService textService, ILogger logger, @@ -32,6 +34,7 @@ internal abstract class BlockEditorPropertyValueEditor : DataValueEditor, IDataV : base(textService, shortStringHelper, jsonSerializer, ioHelper, attribute) { _propertyEditors = propertyEditors; + _dataValueReferenceFactories = dataValueReferenceFactories; _dataTypeService = dataTypeService; _logger = logger; } @@ -42,73 +45,58 @@ internal abstract class BlockEditorPropertyValueEditor : DataValueEditor, IDataV set => _blockEditorValues = value; } + /// public IEnumerable GetReferences(object? value) { - var rawJson = value == null ? string.Empty : value is string str ? str : value.ToString(); - - var result = new List(); - BlockEditorData? blockEditorData = BlockEditorValues.DeserializeAndClean(rawJson); - if (blockEditorData == null) + foreach (BlockItemData.BlockPropertyValue propertyValue in GetAllPropertyValues(value)) { - return Enumerable.Empty(); - } - - // loop through all content and settings data - foreach (BlockItemData row in blockEditorData.BlockValue.ContentData.Concat(blockEditorData.BlockValue.SettingsData)) - { - foreach (KeyValuePair prop in row.PropertyValues) + if (!_propertyEditors.TryGet(propertyValue.PropertyType.PropertyEditorAlias, 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 refs = reference.GetReferences(val); - - result.AddRange(refs); + foreach (UmbracoEntityReference reference in _dataValueReferenceFactories.GetReferences(dataEditor, propertyValue.Value)) + { + yield return reference; } } - - return result; } /// public IEnumerable 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 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(); + yield break; } - var result = new List(); - // loop through all content and settings data - foreach (BlockItemData row in blockEditorData.BlockValue.ContentData.Concat(blockEditorData.BlockValue.SettingsData)) + // Return all property values from the content and settings data + IEnumerable data = blockEditorData.BlockValue.ContentData.Concat(blockEditorData.BlockValue.SettingsData); + foreach (BlockItemData.BlockPropertyValue propertyValue in data.SelectMany(x => x.PropertyValues.Select(x => x.Value))) { - foreach (KeyValuePair prop in row.PropertyValues) - { - IDataEditor? propEditor = _propertyEditors[prop.Value.PropertyType.PropertyEditorAlias]; - - 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)); - } + yield return propertyValue; } - - return result; } #region Convert database // editor diff --git a/src/Umbraco.Infrastructure/PropertyEditors/BlockGridPropertyEditorBase.cs b/src/Umbraco.Infrastructure/PropertyEditors/BlockGridPropertyEditorBase.cs index 73b767bd4f..7e67c2d8a7 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/BlockGridPropertyEditorBase.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/BlockGridPropertyEditorBase.cs @@ -50,6 +50,7 @@ public abstract class BlockGridPropertyEditorBase : DataEditor public BlockGridEditorPropertyValueEditor( DataEditorAttribute attribute, PropertyEditorCollection propertyEditors, + DataValueReferenceFactoryCollection dataValueReferenceFactories, IDataTypeService dataTypeService, ILocalizedTextService textService, ILogger 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)); diff --git a/src/Umbraco.Infrastructure/PropertyEditors/BlockListPropertyEditorBase.cs b/src/Umbraco.Infrastructure/PropertyEditors/BlockListPropertyEditorBase.cs index 194383560e..d59af3817c 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/BlockListPropertyEditorBase.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/BlockListPropertyEditorBase.cs @@ -46,6 +46,7 @@ public abstract class BlockListPropertyEditorBase : DataEditor public BlockListEditorPropertyValueEditor( DataEditorAttribute attribute, PropertyEditorCollection propertyEditors, + DataValueReferenceFactoryCollection dataValueReferenceFactories, IDataTypeService dataTypeService, IContentTypeService contentTypeService, ILocalizedTextService textService, @@ -54,7 +55,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(new BlockListEditorDataConverter(), contentTypeService, logger); Validators.Add(new BlockEditorValidator(propertyValidationService, BlockEditorValues, contentTypeService)); diff --git a/src/Umbraco.Infrastructure/PropertyEditors/NestedContentPropertyEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/NestedContentPropertyEditor.cs index bf5781079a..87d80e3972 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/NestedContentPropertyEditor.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/NestedContentPropertyEditor.cs @@ -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 _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 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,45 @@ public class NestedContentPropertyEditor : DataEditor } } + /// public IEnumerable GetReferences(object? value) { - var rawJson = value == null ? string.Empty : value is string str ? str : value.ToString(); - - var result = new List(); - - foreach (NestedContentValues.NestedContentRowValue row in _nestedContentValues.GetPropertyValues(rawJson)) + foreach (NestedContentValues.NestedContentPropertyValue propertyValue in GetAllPropertyValues(value)) { - foreach (KeyValuePair prop in - row.PropertyValues) + if (!_propertyEditors.TryGet(propertyValue.PropertyType.PropertyEditorAlias, 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 refs = reference.GetReferences(val); - - result.AddRange(refs); + foreach (UmbracoEntityReference reference in _dataValueReferenceFactories.GetReferences(dataEditor, propertyValue.Value)) + { + yield return reference; } } - - return result; } /// public IEnumerable GetTags(object? value, object? dataTypeConfiguration, int? languageId) { - IReadOnlyList rows = - _nestedContentValues.GetPropertyValues(value); - - var result = new List(); - - foreach (NestedContentValues.NestedContentRowValue row in rows.ToList()) + foreach (NestedContentValues.NestedContentPropertyValue propertyValue in GetAllPropertyValues(value)) { - foreach (KeyValuePair 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 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 +407,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; } diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/DataValueEditorReuseTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/DataValueEditorReuseTests.cs index d88a9689ab..75d4f0d509 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/DataValueEditorReuseTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/DataValueEditorReuseTests.cs @@ -1,4 +1,3 @@ -using System.Linq; using Microsoft.Extensions.Logging; using Moq; using NUnit.Framework; @@ -15,6 +14,7 @@ public class DataValueEditorReuseTests { private Mock _dataValueEditorFactoryMock; private PropertyEditorCollection _propertyEditorCollection; + private DataValueReferenceFactoryCollection _dataValueReferenceFactories; [SetUp] public void SetUp() @@ -31,6 +31,7 @@ public class DataValueEditorReuseTests Mock.Of())); _propertyEditorCollection = new PropertyEditorCollection(new DataEditorCollection(Enumerable.Empty)); + _dataValueReferenceFactories = new DataValueReferenceFactoryCollection(Enumerable.Empty); _dataValueEditorFactoryMock .Setup(m => @@ -38,6 +39,7 @@ public class DataValueEditorReuseTests .Returns(() => new BlockListPropertyEditorBase.BlockListEditorPropertyValueEditor( new DataEditorAttribute("a", "b", "c"), _propertyEditorCollection, + _dataValueReferenceFactories, Mock.Of(), Mock.Of(), Mock.Of(), @@ -93,7 +95,7 @@ public class DataValueEditorReuseTests { var blockListPropertyEditor = new BlockListPropertyEditor( _dataValueEditorFactoryMock.Object, - new PropertyEditorCollection(new DataEditorCollection(Enumerable.Empty)), + _propertyEditorCollection, Mock.Of(), Mock.Of(), Mock.Of()); @@ -114,7 +116,7 @@ public class DataValueEditorReuseTests { var blockListPropertyEditor = new BlockListPropertyEditor( _dataValueEditorFactoryMock.Object, - new PropertyEditorCollection(new DataEditorCollection(Enumerable.Empty)), + _propertyEditorCollection, Mock.Of(), Mock.Of(), Mock.Of()); diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/DataValueReferenceFactoryCollectionTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/DataValueReferenceFactoryCollectionTests.cs index 6108f59e2e..38fc5125dc 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/DataValueReferenceFactoryCollectionTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/DataValueReferenceFactoryCollectionTests.cs @@ -1,8 +1,6 @@ // Copyright (c) Umbraco. // See LICENSE for more details. -using System.Collections.Generic; -using System.Linq; using Moq; using NUnit.Framework; using Umbraco.Cms.Core; @@ -173,6 +171,33 @@ public class DataValueReferenceFactoryCollectionTests Assert.AreEqual(trackedUdi4, result.ElementAt(1).Udi.ToString()); } + [Test] + public void GetAutomaticRelationTypesAliases_ContainsDefault() + { + var collection = new DataValueReferenceFactoryCollection(Enumerable.Empty); + var propertyEditors = new PropertyEditorCollection(new DataEditorCollection(Enumerable.Empty)); + + var result = collection.GetAllAutomaticRelationTypesAliases(propertyEditors).ToArray(); + + var expected = Constants.Conventions.RelationTypes.AutomaticRelationTypes; + CollectionAssert.AreEquivalent(expected, result, "Result does not contain the expected relation type aliases."); + } + + [Test] + public void GetAutomaticRelationTypesAliases_ContainsCustom() + { + var collection = new DataValueReferenceFactoryCollection(() => new TestDataValueReferenceFactory().Yield()); + + var labelPropertyEditor = new LabelPropertyEditor(DataValueEditorFactory, IOHelper, EditorConfigurationParser); + var propertyEditors = new PropertyEditorCollection(new DataEditorCollection(() => labelPropertyEditor.Yield())); + var serializer = new ConfigurationEditorJsonSerializer(); + + var result = collection.GetAllAutomaticRelationTypesAliases(propertyEditors).ToArray(); + + var expected = Constants.Conventions.RelationTypes.AutomaticRelationTypes.Append("umbTest"); + CollectionAssert.AreEquivalent(expected, result, "Result does not contain the expected relation type aliases."); + } + private class TestDataValueReferenceFactory : IDataValueReferenceFactory { public IDataValueReference GetDataValueReference() => new TestMediaDataValueReference(); @@ -196,6 +221,12 @@ public class DataValueReferenceFactoryCollectionTests yield return new UmbracoEntityReference(udi); } } + + public IEnumerable GetAutomaticRelationTypesAliases() => new[] + { + "umbTest", + "umbTest", // Duplicate on purpose to test distinct aliases + }; } } }