Files
Umbraco-CMS/src/Umbraco.Core/PropertyEditors/ValueConverters/MultiNodeTreePickerValueConverter.cs
Andy Butland 8b4849be05 Fix issue with preview in delivery API for MNTP property editor (#19668)
* Passes the preview flag to the cache retrieval when resolving the delivery API object for the MNTP property editor.

* Added unit test verifying fix and adjusted mocks for tests to acoomodate.

* Provided preview flag for Razor rendering.
2025-07-07 11:33:59 +02:00

262 lines
11 KiB
C#

using System.Globalization;
using Umbraco.Cms.Core.DeliveryApi;
using Umbraco.Cms.Core.DependencyInjection;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.DeliveryApi;
using Umbraco.Cms.Core.Models.PublishedContent;
using Umbraco.Cms.Core.PropertyEditors.DeliveryApi;
using Umbraco.Cms.Core.PublishedCache;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Web;
using Umbraco.Extensions;
namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters;
/// <summary>
/// The multi node tree picker property editor value converter.
/// </summary>
[DefaultPropertyValueConverter(typeof(MustBeStringValueConverter))]
public class MultiNodeTreePickerValueConverter : PropertyValueConverterBase, IDeliveryApiPropertyValueConverter
{
private static readonly List<string> PropertiesToExclude = new()
{
Constants.Conventions.Content.InternalRedirectId.ToLower(CultureInfo.InvariantCulture),
Constants.Conventions.Content.Redirect.ToLower(CultureInfo.InvariantCulture),
};
private readonly IMemberService _memberService;
private readonly IUmbracoContextAccessor _umbracoContextAccessor;
private readonly IApiContentBuilder _apiContentBuilder;
private readonly IApiMediaBuilder _apiMediaBuilder;
private readonly IPublishedContentCache _contentCache;
private readonly IPublishedMediaCache _mediaCache;
private readonly IPublishedMemberCache _memberCache;
public MultiNodeTreePickerValueConverter(
IUmbracoContextAccessor umbracoContextAccessor,
IMemberService memberService,
IApiContentBuilder apiContentBuilder,
IApiMediaBuilder apiMediaBuilder,
IPublishedContentCache contentCache,
IPublishedMediaCache mediaCache,
IPublishedMemberCache memberCache)
{
_umbracoContextAccessor = umbracoContextAccessor;
_memberService = memberService;
_apiContentBuilder = apiContentBuilder;
_apiMediaBuilder = apiMediaBuilder;
_contentCache = contentCache;
_mediaCache = mediaCache;
_memberCache = memberCache;
}
public override bool IsConverter(IPublishedPropertyType propertyType) =>
propertyType.EditorAlias.Equals(Constants.PropertyEditors.Aliases.MultiNodeTreePicker);
public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType)
=> PropertyCacheLevel.Snapshot;
public override Type GetPropertyValueType(IPublishedPropertyType propertyType)
=> IsSingleNodePicker(propertyType)
? typeof(IPublishedContent)
: typeof(IEnumerable<IPublishedContent>);
public override object? ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object? source, bool preview)
{
if (source == null)
{
return null;
}
if (propertyType.EditorAlias.Equals(Constants.PropertyEditors.Aliases.MultiNodeTreePicker))
{
Udi[]? nodeIds = source.ToString()?
.Split(Constants.CharArrays.Comma, StringSplitOptions.RemoveEmptyEntries)
.Select(UdiParser.Parse)
.ToArray();
return nodeIds;
}
return null;
}
public override object? ConvertIntermediateToObject(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel cacheLevel, object? source, bool preview)
{
if (source == null)
{
return null;
}
// TODO: Inject an UmbracoHelper and create a GetUmbracoHelper method based on either injected or singleton
if (_umbracoContextAccessor.TryGetUmbracoContext(out _))
{
if (propertyType.EditorAlias.Equals(Constants.PropertyEditors.Aliases.MultiNodeTreePicker))
{
var udis = (Udi[])source;
var isSingleNodePicker = IsSingleNodePicker(propertyType);
if ((propertyType.Alias != null && PropertiesToExclude.InvariantContains(propertyType.Alias)) == false)
{
var multiNodeTreePicker = new List<IPublishedContent>();
UmbracoObjectTypes objectType = UmbracoObjectTypes.Unknown;
foreach (Udi udi in udis)
{
if (udi is not GuidUdi guidUdi)
{
continue;
}
IPublishedContent? multiNodeTreePickerItem = null;
switch (udi.EntityType)
{
case Constants.UdiEntityType.Document:
multiNodeTreePickerItem = GetPublishedContent(
udi,
ref objectType,
UmbracoObjectTypes.Document,
id => _contentCache.GetById(preview, guidUdi.Guid));
break;
case Constants.UdiEntityType.Media:
multiNodeTreePickerItem = GetPublishedContent(
udi,
ref objectType,
UmbracoObjectTypes.Media,
id => _mediaCache.GetById(guidUdi.Guid));
break;
case Constants.UdiEntityType.Member:
multiNodeTreePickerItem = GetPublishedContent(
udi,
ref objectType,
UmbracoObjectTypes.Member,
id =>
{
IMember? m = _memberService.GetById(guidUdi.Guid);
if (m == null)
{
return null;
}
IPublishedContent? member = _memberCache.Get(m);
return member;
});
break;
}
if (multiNodeTreePickerItem != null &&
multiNodeTreePickerItem.ContentType.ItemType != PublishedItemType.Element)
{
multiNodeTreePicker.Add(multiNodeTreePickerItem);
if (isSingleNodePicker)
{
break;
}
}
}
if (isSingleNodePicker)
{
return multiNodeTreePicker.FirstOrDefault();
}
return multiNodeTreePicker;
}
// return the first nodeId as this is one of the excluded properties that expects a single id
return udis.FirstOrDefault();
}
}
return source;
}
public PropertyCacheLevel GetDeliveryApiPropertyCacheLevel(IPublishedPropertyType propertyType) => PropertyCacheLevel.Snapshot;
public PropertyCacheLevel GetDeliveryApiPropertyCacheLevelForExpansion(IPublishedPropertyType propertyType) => PropertyCacheLevel.Snapshot;
public Type GetDeliveryApiPropertyValueType(IPublishedPropertyType propertyType)
=> GetEntityType(propertyType) switch
{
Constants.UdiEntityType.Media => typeof(IEnumerable<IApiMedia>),
Constants.UdiEntityType.Member => typeof(string), // unsupported
_ => typeof(IEnumerable<IApiContent>)
};
public object? ConvertIntermediateToDeliveryApiObject(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object? inter, bool preview, bool expanding)
{
IEnumerable<IApiContent> DefaultValue() => Array.Empty<IApiContent>();
if (inter is not IEnumerable<Udi> udis)
{
return DefaultValue();
}
var entityType = GetEntityType(propertyType);
if (entityType == "content")
{
// TODO Why do MNTP config saves "content" and not "document"?
entityType = Constants.UdiEntityType.Document;
}
GuidUdi[] entityTypeUdis = udis.Where(udi => udi.EntityType == entityType).OfType<GuidUdi>().ToArray();
return entityType switch
{
Constants.UdiEntityType.Document => entityTypeUdis.Select(udi =>
{
IPublishedContent? content = _contentCache.GetById(preview, udi.Guid);
return content != null
? _apiContentBuilder.Build(content)
: null;
}).WhereNotNull().ToArray(),
Constants.UdiEntityType.Media => entityTypeUdis.Select(udi =>
{
IPublishedContent? media = _mediaCache.GetById(udi.Guid);
return media != null
? _apiMediaBuilder.Build(media)
: null;
}).WhereNotNull().ToArray(),
Constants.UdiEntityType.Member => "(unsupported)",
_ => DefaultValue()
};
}
private static bool IsSingleNodePicker(IPublishedPropertyType propertyType) =>
propertyType.DataType.ConfigurationAs<MultiNodePickerConfiguration>()?.MaxNumber == 1;
private static string GetEntityType(IPublishedPropertyType propertyType) =>
propertyType.DataType.ConfigurationAs<MultiNodePickerConfiguration>()?.TreeSource?.ObjectType ?? Constants.UdiEntityType.Document;
/// <summary>
/// Attempt to get an IPublishedContent instance based on ID and content type
/// </summary>
/// <param name="nodeId">The content node ID</param>
/// <param name="actualType">The type of content being requested</param>
/// <param name="expectedType">The type of content expected/supported by <paramref name="contentFetcher" /></param>
/// <param name="contentFetcher">A function to fetch content of type <paramref name="expectedType" /></param>
/// <returns>
/// The requested content, or null if either it does not exist or <paramref name="actualType" /> does not match
/// <paramref name="expectedType" />
/// </returns>
private IPublishedContent? GetPublishedContent<T>(T nodeId, ref UmbracoObjectTypes actualType, UmbracoObjectTypes expectedType, Func<T, IPublishedContent?> contentFetcher)
{
// is the actual type supported by the content fetcher?
if (actualType != UmbracoObjectTypes.Unknown && actualType != expectedType)
{
// no, return null
return null;
}
// attempt to get the content
IPublishedContent? content = contentFetcher(nodeId);
if (content != null)
{
// if we found the content, assign the expected type to the actual type so we don't have to keep looking for other types of content
actualType = expectedType;
}
return content;
}
}