Backport relation tracking fixes and get references from recursive (nested/block) properties (#15593)
* Include automatic relation type aliases from factory and fix SQL parameter overflow (#15141)
* Include automatic relation type aliases from factory
* Remove unnessecary distinct and fix SQL parameter overflow issue
* Fixed assertions and test distinct aliases
* Simplified collection assertions
* Improve logging of invalid reference relations (#15160)
* Include automatic relation type aliases from factory
* Remove unnessecary distinct and fix SQL parameter overflow issue
* Fixed assertions and test distinct aliases
* Simplified collection assertions
* Improve logging of invalid reference relations
* Always get all automatic relation type aliases
* Do not set relation type alias for unknown entity types
* Get references from recursive (nested/block) properties
(cherry picked from commit 5198e7c52d)
This commit is contained in:
committed by
Bjarke Berg
parent
a70c606c27
commit
ce22315520
@@ -3,7 +3,7 @@ using System.Collections;
|
||||
namespace Umbraco.Cms.Core.Composing;
|
||||
|
||||
/// <summary>
|
||||
/// Provides a base class for builder collections.
|
||||
/// Provides a base class for builder collections.
|
||||
/// </summary>
|
||||
/// <typeparam name="TItem">The type of the items.</typeparam>
|
||||
public abstract class BuilderCollectionBase<TItem> : IBuilderCollection<TItem>
|
||||
@@ -11,21 +11,18 @@ public abstract class BuilderCollectionBase<TItem> : IBuilderCollection<TItem>
|
||||
private readonly LazyReadOnlyCollection<TItem> _items;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="BuilderCollectionBase{TItem}" /> with items.
|
||||
/// Initializes a new instance of the <see cref="BuilderCollectionBase{TItem}" /> with items.
|
||||
/// </summary>
|
||||
/// <param name="items">The items.</param>
|
||||
public BuilderCollectionBase(Func<IEnumerable<TItem>> items) => _items = new LazyReadOnlyCollection<TItem>(items);
|
||||
public BuilderCollectionBase(Func<IEnumerable<TItem>> items)
|
||||
=> _items = new LazyReadOnlyCollection<TItem>(items);
|
||||
|
||||
/// <inheritdoc />
|
||||
public int Count => _items.Count;
|
||||
|
||||
/// <summary>
|
||||
/// Gets an enumerator.
|
||||
/// </summary>
|
||||
/// <inheritdoc />
|
||||
public IEnumerator<TItem> GetEnumerator() => _items.GetEnumerator();
|
||||
|
||||
/// <summary>
|
||||
/// Gets an enumerator.
|
||||
/// </summary>
|
||||
/// <inheritdoc />
|
||||
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
|
||||
}
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
namespace Umbraco.Cms.Core.Composing;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a builder collection, ie an immutable enumeration of items.
|
||||
/// Represents a builder collection, ie an immutable enumeration of items.
|
||||
/// </summary>
|
||||
/// <typeparam name="TItem">The type of the items.</typeparam>
|
||||
public interface IBuilderCollection<out TItem> : IEnumerable<TItem>
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets the number of items in the collection.
|
||||
/// Gets the number of items in the collection.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The count.
|
||||
/// </value>
|
||||
int Count { get; }
|
||||
}
|
||||
|
||||
@@ -1,49 +1,88 @@
|
||||
namespace Umbraco.Cms.Core.Models.Editors;
|
||||
|
||||
/// <summary>
|
||||
/// Used to track reference to other entities in a property value
|
||||
/// Used to track a reference to another entity in a property value.
|
||||
/// </summary>
|
||||
public struct UmbracoEntityReference : IEquatable<UmbracoEntityReference>
|
||||
{
|
||||
private static readonly UmbracoEntityReference _empty = new(UnknownTypeUdi.Instance, string.Empty);
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="UmbracoEntityReference" /> struct.
|
||||
/// </summary>
|
||||
/// <param name="udi">The UDI.</param>
|
||||
/// <param name="relationTypeAlias">The relation type alias.</param>
|
||||
public UmbracoEntityReference(Udi udi, string relationTypeAlias)
|
||||
{
|
||||
Udi = udi ?? throw new ArgumentNullException(nameof(udi));
|
||||
RelationTypeAlias = relationTypeAlias ?? throw new ArgumentNullException(nameof(relationTypeAlias));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="UmbracoEntityReference" /> struct for a document or media item.
|
||||
/// </summary>
|
||||
/// <param name="udi">The UDI.</param>
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the UDI.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The UDI.
|
||||
/// </value>
|
||||
public Udi Udi { get; }
|
||||
|
||||
public static UmbracoEntityReference Empty() => _empty;
|
||||
|
||||
public static bool IsEmpty(UmbracoEntityReference reference) => reference == Empty();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the relation type alias.
|
||||
/// </summary>
|
||||
/// <value>
|
||||
/// The relation type alias.
|
||||
/// </value>
|
||||
public string RelationTypeAlias { get; }
|
||||
|
||||
public static bool operator ==(UmbracoEntityReference left, UmbracoEntityReference right) => left.Equals(right);
|
||||
/// <summary>
|
||||
/// Gets an empty reference.
|
||||
/// </summary>
|
||||
/// <returns>
|
||||
/// An empty reference.
|
||||
/// </returns>
|
||||
public static UmbracoEntityReference Empty() => _empty;
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the specified reference is empty.
|
||||
/// </summary>
|
||||
/// <param name="reference">The reference.</param>
|
||||
/// <returns>
|
||||
/// <c>true</c> if the specified reference is empty; otherwise, <c>false</c>.
|
||||
/// </returns>
|
||||
public static bool IsEmpty(UmbracoEntityReference reference) => reference == Empty();
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool Equals(object? obj) => obj is UmbracoEntityReference reference && Equals(reference);
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool Equals(UmbracoEntityReference other) =>
|
||||
EqualityComparer<Udi>.Default.Equals(Udi, other.Udi) &&
|
||||
RelationTypeAlias == other.RelationTypeAlias;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override int GetHashCode()
|
||||
{
|
||||
var hashCode = -487348478;
|
||||
@@ -52,5 +91,9 @@ public struct UmbracoEntityReference : IEquatable<UmbracoEntityReference>
|
||||
return hashCode;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public static bool operator ==(UmbracoEntityReference left, UmbracoEntityReference right) => left.Equals(right);
|
||||
|
||||
/// <inheritdoc />
|
||||
public static bool operator !=(UmbracoEntityReference left, UmbracoEntityReference right) => !(left == right);
|
||||
}
|
||||
|
||||
@@ -4,64 +4,148 @@ using Umbraco.Cms.Core.Models.Editors;
|
||||
|
||||
namespace Umbraco.Cms.Core.PropertyEditors;
|
||||
|
||||
/// <summary>
|
||||
/// Provides a builder collection for <see cref="IDataValueReferenceFactory" /> items.
|
||||
/// </summary>
|
||||
public class DataValueReferenceFactoryCollection : BuilderCollectionBase<IDataValueReferenceFactory>
|
||||
{
|
||||
public DataValueReferenceFactoryCollection(Func<IEnumerable<IDataValueReferenceFactory>> 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<UmbracoEntityReference> GetAllReferences(
|
||||
IPropertyCollection properties,
|
||||
PropertyEditorCollection propertyEditors)
|
||||
{
|
||||
var trackedRelations = new HashSet<UmbracoEntityReference>();
|
||||
|
||||
foreach (IProperty p in properties)
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DataValueReferenceFactoryCollection" /> class.
|
||||
/// </summary>
|
||||
/// <param name="items">The items.</param>
|
||||
public DataValueReferenceFactoryCollection(Func<IEnumerable<IDataValueReferenceFactory>> items)
|
||||
: base(items)
|
||||
{ }
|
||||
|
||||
/// <summary>
|
||||
/// Gets all unique references from the specified properties.
|
||||
/// </summary>
|
||||
/// <param name="properties">The properties.</param>
|
||||
/// <param name="propertyEditors">The property editors.</param>
|
||||
/// <returns>
|
||||
/// The unique references from the specified properties.
|
||||
/// </returns>
|
||||
public ISet<UmbracoEntityReference> GetAllReferences(IPropertyCollection properties, PropertyEditorCollection propertyEditors)
|
||||
{
|
||||
var references = new HashSet<UmbracoEntityReference>();
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the references.
|
||||
/// </summary>
|
||||
/// <param name="dataEditor">The data editor.</param>
|
||||
/// <param name="values">The values.</param>
|
||||
/// <returns>
|
||||
/// The references.
|
||||
/// </returns>
|
||||
public IEnumerable<UmbracoEntityReference> GetReferences(IDataEditor dataEditor, params object?[] values)
|
||||
=> GetReferences(dataEditor, (IEnumerable<object?>)values);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the references.
|
||||
/// </summary>
|
||||
/// <param name="dataEditor">The data editor.</param>
|
||||
/// <param name="values">The values.</param>
|
||||
/// <returns>
|
||||
/// The references.
|
||||
/// </returns>
|
||||
public IEnumerable<UmbracoEntityReference> GetReferences(IDataEditor dataEditor, IEnumerable<object?> 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<UmbracoEntityReference> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets all relation type aliases that are automatically tracked.
|
||||
/// </summary>
|
||||
/// <param name="propertyEditors">The property editors.</param>
|
||||
/// <returns>
|
||||
/// All relation type aliases that are automatically tracked.
|
||||
/// </returns>
|
||||
public ISet<string> GetAllAutomaticRelationTypesAliases(PropertyEditorCollection propertyEditors)
|
||||
{
|
||||
// Always add default automatic relation types
|
||||
var automaticRelationTypeAliases = new HashSet<string>(Constants.Conventions.RelationTypes.AutomaticRelationTypes);
|
||||
|
||||
// Add relation types for all property editors
|
||||
foreach (IDataEditor dataEditor in propertyEditors)
|
||||
{
|
||||
automaticRelationTypeAliases.UnionWith(GetAutomaticRelationTypesAliases(dataEditor));
|
||||
}
|
||||
|
||||
return automaticRelationTypeAliases;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the automatic relation types aliases.
|
||||
/// </summary>
|
||||
/// <param name="dataEditor">The data editor.</param>
|
||||
/// <returns>
|
||||
/// The automatic relation types aliases.
|
||||
/// </returns>
|
||||
public IEnumerable<string> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<UmbracoEntityReference>();
|
||||
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<UmbracoEntityReference> references = _dataValueReferenceFactories.GetAllReferences(entity.Properties, PropertyEditors);
|
||||
|
||||
// First delete all auto-relations for this entity
|
||||
RelationRepository.DeleteByParent(entity.Id, relationTypeAliases.ToArray());
|
||||
ISet<string> 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<NodeIdKey>(Sql()
|
||||
.Select<NodeDto>(x => x.NodeId, x => x.UniqueId)
|
||||
.From<NodeDto>()
|
||||
.WhereIn<NodeDto>(x => x.UniqueId, udiToGuids.Values))
|
||||
.ToDictionary(x => x.UniqueId, x => x.NodeId);
|
||||
|
||||
var allRelationTypes = RelationTypeRepository.GetMany(Array.Empty<int>())?
|
||||
.ToDictionary(x => x.Alias, x => x);
|
||||
|
||||
IEnumerable<ReadOnlyRelation> 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<string> GetAutomaticRelationTypesAliases(
|
||||
IPropertyCollection properties,
|
||||
PropertyEditorCollection propertyEditors)
|
||||
{
|
||||
var automaticRelationTypesAliases = new HashSet<string>(Constants.Conventions.RelationTypes.AutomaticRelationTypes);
|
||||
|
||||
foreach (IProperty property in properties)
|
||||
// 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 =>
|
||||
{
|
||||
if (propertyEditors.TryGet(property.PropertyType.PropertyEditorAlias, out IDataEditor? editor) is false )
|
||||
{
|
||||
continue;
|
||||
}
|
||||
return Sql()
|
||||
.Select<NodeDto>(x => x.NodeId, x => x.UniqueId)
|
||||
.From<NodeDto>()
|
||||
.WhereIn<NodeDto>(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<int>()).ToDictionary(x => x.Alias, x => x.Id);
|
||||
|
||||
// Get all valid relations
|
||||
var relations = new List<ReadOnlyRelation>(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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
|
||||
@@ -17,12 +17,14 @@ internal abstract class BlockEditorPropertyValueEditor : DataValueEditor, IDataV
|
||||
{
|
||||
private BlockEditorValues? _blockEditorValues;
|
||||
private readonly IDataTypeService _dataTypeService;
|
||||
private readonly ILogger<BlockEditorPropertyValueEditor> _logger;
|
||||
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,
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
/// <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>();
|
||||
BlockEditorData? blockEditorData = BlockEditorValues.DeserializeAndClean(rawJson);
|
||||
if (blockEditorData == null)
|
||||
foreach (BlockItemData.BlockPropertyValue propertyValue in GetAllPropertyValues(value))
|
||||
{
|
||||
return Enumerable.Empty<UmbracoEntityReference>();
|
||||
}
|
||||
|
||||
// loop through all content and settings data
|
||||
foreach (BlockItemData row in blockEditorData.BlockValue.ContentData.Concat(blockEditorData.BlockValue.SettingsData))
|
||||
{
|
||||
foreach (KeyValuePair<string, BlockItemData.BlockPropertyValue> 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<UmbracoEntityReference> refs = reference.GetReferences(val);
|
||||
|
||||
result.AddRange(refs);
|
||||
foreach (UmbracoEntityReference reference in _dataValueReferenceFactories.GetReferences(dataEditor, propertyValue.Value))
|
||||
{
|
||||
yield return reference;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public 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;
|
||||
}
|
||||
|
||||
var result = new List<ITag>();
|
||||
// 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<BlockItemData> data = blockEditorData.BlockValue.ContentData.Concat(blockEditorData.BlockValue.SettingsData);
|
||||
foreach (BlockItemData.BlockPropertyValue propertyValue in data.SelectMany(x => x.PropertyValues.Select(x => x.Value)))
|
||||
{
|
||||
foreach (KeyValuePair<string, BlockItemData.BlockPropertyValue> 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
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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,45 @@ 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))
|
||||
foreach (NestedContentValues.NestedContentPropertyValue propertyValue in GetAllPropertyValues(value))
|
||||
{
|
||||
foreach (KeyValuePair<string, NestedContentValues.NestedContentPropertyValue> 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<UmbracoEntityReference> refs = reference.GetReferences(val);
|
||||
|
||||
result.AddRange(refs);
|
||||
foreach (UmbracoEntityReference reference in _dataValueReferenceFactories.GetReferences(dataEditor, propertyValue.Value))
|
||||
{
|
||||
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 +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;
|
||||
}
|
||||
|
||||
@@ -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<IDataValueEditorFactory> _dataValueEditorFactoryMock;
|
||||
private PropertyEditorCollection _propertyEditorCollection;
|
||||
private DataValueReferenceFactoryCollection _dataValueReferenceFactories;
|
||||
|
||||
[SetUp]
|
||||
public void SetUp()
|
||||
@@ -31,6 +31,7 @@ public class DataValueEditorReuseTests
|
||||
Mock.Of<IIOHelper>()));
|
||||
|
||||
_propertyEditorCollection = new PropertyEditorCollection(new DataEditorCollection(Enumerable.Empty<IDataEditor>));
|
||||
_dataValueReferenceFactories = new DataValueReferenceFactoryCollection(Enumerable.Empty<IDataValueReferenceFactory>);
|
||||
|
||||
_dataValueEditorFactoryMock
|
||||
.Setup(m =>
|
||||
@@ -38,6 +39,7 @@ public class DataValueEditorReuseTests
|
||||
.Returns(() => new BlockListPropertyEditorBase.BlockListEditorPropertyValueEditor(
|
||||
new DataEditorAttribute("a", "b", "c"),
|
||||
_propertyEditorCollection,
|
||||
_dataValueReferenceFactories,
|
||||
Mock.Of<IDataTypeService>(),
|
||||
Mock.Of<IContentTypeService>(),
|
||||
Mock.Of<ILocalizedTextService>(),
|
||||
@@ -93,7 +95,7 @@ public class DataValueEditorReuseTests
|
||||
{
|
||||
var blockListPropertyEditor = new BlockListPropertyEditor(
|
||||
_dataValueEditorFactoryMock.Object,
|
||||
new PropertyEditorCollection(new DataEditorCollection(Enumerable.Empty<IDataEditor>)),
|
||||
_propertyEditorCollection,
|
||||
Mock.Of<IIOHelper>(),
|
||||
Mock.Of<IEditorConfigurationParser>(),
|
||||
Mock.Of<IBlockValuePropertyIndexValueFactory>());
|
||||
@@ -114,7 +116,7 @@ public class DataValueEditorReuseTests
|
||||
{
|
||||
var blockListPropertyEditor = new BlockListPropertyEditor(
|
||||
_dataValueEditorFactoryMock.Object,
|
||||
new PropertyEditorCollection(new DataEditorCollection(Enumerable.Empty<IDataEditor>)),
|
||||
_propertyEditorCollection,
|
||||
Mock.Of<IIOHelper>(),
|
||||
Mock.Of<IEditorConfigurationParser>(),
|
||||
Mock.Of<IBlockValuePropertyIndexValueFactory>());
|
||||
|
||||
@@ -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<IDataValueReferenceFactory>);
|
||||
var propertyEditors = new PropertyEditorCollection(new DataEditorCollection(Enumerable.Empty<IDataEditor>));
|
||||
|
||||
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<string> GetAutomaticRelationTypesAliases() => new[]
|
||||
{
|
||||
"umbTest",
|
||||
"umbTest", // Duplicate on purpose to test distinct aliases
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user