Add support for file upload property editor within the block list and grid (#18976)
* Fix for https://github.com/umbraco/Umbraco-CMS/issues/18872 * Parsing added for current value * Build fix. * Cyclomatic complexity fix * Resolved breaking change. * Pass content key. * Simplified collections. * Added unit tests to verify behaviour. * Allow file upload on block list. * Added unit test verifying added property. * Added unit test verifying removed property. * Restored null return for null value fixing failing integration tests. * Logic has been updated according edge cases * Logic to copy files from block list items has been added. * Logic to delete files from block list items on content deletion has been added * Test fix. * Refactoring. * WIP: Resolved breaking changes, minor refactoring. * Consistently return null over empty, resolving failure in integration test. * Removed unnecessary code nesting. * Handle distinct paths. * Handles clean up of files added via file upload in rich text blocks on delete of the content. * Update src/Umbraco.Infrastructure/PropertyEditors/FileUploadPropertyEditor.cs Co-authored-by: Sven Geusens <geusens@gmail.com> * Fixed build of integration tests project. * Handled delete of file uploads when deleting a block from an RTE using a file upload property. * Refactored ensure of property type property populated on rich text values to a common helper extension method. * Fixed integration tests build. * Handle create of new file from file upload block in an RTE when the document is copied. * Fixed failing integration tests. * Refactored notification handlers relating to file uploads into separate classes. * Handle nested rich text editor block with file upload when copying content. * Handle nested rich text editor block with file upload when deleting content. * Minor refactor. * Integration test compatibility supressions. --------- Co-authored-by: Andy Butland <abutland73@gmail.com> Co-authored-by: Sven Geusens <geusens@gmail.com>
This commit is contained in:
@@ -52,6 +52,7 @@ using Umbraco.Cms.Infrastructure.Migrations;
|
||||
using Umbraco.Cms.Infrastructure.Migrations.Install;
|
||||
using Umbraco.Cms.Infrastructure.Persistence;
|
||||
using Umbraco.Cms.Infrastructure.Persistence.Mappers;
|
||||
using Umbraco.Cms.Infrastructure.PropertyEditors.NotificationHandlers;
|
||||
using Umbraco.Cms.Infrastructure.Routing;
|
||||
using Umbraco.Cms.Infrastructure.Runtime;
|
||||
using Umbraco.Cms.Infrastructure.Runtime.RuntimeModeValidators;
|
||||
@@ -355,11 +356,11 @@ public static partial class UmbracoBuilderExtensions
|
||||
.AddNotificationHandler<ContentSavingNotification, RichTextPropertyNotificationHandler>()
|
||||
.AddNotificationHandler<ContentCopyingNotification, RichTextPropertyNotificationHandler>()
|
||||
.AddNotificationHandler<ContentScaffoldedNotification, RichTextPropertyNotificationHandler>()
|
||||
.AddNotificationHandler<ContentCopiedNotification, FileUploadPropertyEditor>()
|
||||
.AddNotificationHandler<ContentDeletedNotification, FileUploadPropertyEditor>()
|
||||
.AddNotificationHandler<MediaDeletedNotification, FileUploadPropertyEditor>()
|
||||
.AddNotificationHandler<MediaSavingNotification, FileUploadPropertyEditor>()
|
||||
.AddNotificationHandler<MemberDeletedNotification, FileUploadPropertyEditor>()
|
||||
.AddNotificationHandler<ContentCopiedNotification, FileUploadContentCopiedNotificationHandler>()
|
||||
.AddNotificationHandler<ContentDeletedNotification, FileUploadContentDeletedNotificationHandler>()
|
||||
.AddNotificationHandler<MediaDeletedNotification, FileUploadMediaDeletedNotificationHandler>()
|
||||
.AddNotificationHandler<MediaSavingNotification, FileUploadMediaSavingNotificationHandler>()
|
||||
.AddNotificationHandler<MemberDeletedNotification, FileUploadMemberDeletedNotificationHandler>()
|
||||
.AddNotificationHandler<ContentCopiedNotification, ImageCropperPropertyEditor>()
|
||||
.AddNotificationHandler<ContentDeletedNotification, ImageCropperPropertyEditor>()
|
||||
.AddNotificationHandler<MediaDeletedNotification, ImageCropperPropertyEditor>()
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.Cache.PropertyEditors;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Models.Blocks;
|
||||
|
||||
namespace Umbraco.Cms.Infrastructure.Extensions;
|
||||
|
||||
/// <summary>
|
||||
/// Defines extensions on <see cref="RichTextEditorValue"/>.
|
||||
/// </summary>
|
||||
internal static class RichTextEditorValueExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Ensures that the property type property is populated on all blocks.
|
||||
/// </summary>
|
||||
/// <param name="richTextEditorValue">The <see cref="RichTextEditorValue"/> providing the blocks.</param>
|
||||
/// <param name="elementTypeCache">Cache for element types.</param>
|
||||
public static void EnsurePropertyTypePopulatedOnBlocks(this RichTextEditorValue richTextEditorValue, IBlockEditorElementTypeCache elementTypeCache)
|
||||
{
|
||||
Guid[] elementTypeKeys = (richTextEditorValue.Blocks?.ContentData ?? [])
|
||||
.Select(x => x.ContentTypeKey)
|
||||
.Union((richTextEditorValue.Blocks?.SettingsData ?? [])
|
||||
.Select(x => x.ContentTypeKey))
|
||||
.Distinct()
|
||||
.ToArray();
|
||||
|
||||
IEnumerable<IContentType> elementTypes = elementTypeCache.GetMany(elementTypeKeys);
|
||||
|
||||
foreach (BlockItemData dataItem in (richTextEditorValue.Blocks?.ContentData ?? [])
|
||||
.Union(richTextEditorValue.Blocks?.SettingsData ?? []))
|
||||
{
|
||||
foreach (BlockPropertyValue item in dataItem.Values)
|
||||
{
|
||||
item.PropertyType = elementTypes.FirstOrDefault(x => x.Key == dataItem.ContentTypeKey)?.PropertyTypes.FirstOrDefault(pt => pt.Alias == item.Alias);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,8 @@
|
||||
// Copyright (c) Umbraco.
|
||||
// See LICENSE for more details.
|
||||
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using Umbraco.Cms.Core.Cache;
|
||||
using Umbraco.Cms.Core.DependencyInjection;
|
||||
using Umbraco.Cms.Core.IO;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Models.Blocks;
|
||||
@@ -16,10 +14,16 @@ using Umbraco.Cms.Core.Strings;
|
||||
|
||||
namespace Umbraco.Cms.Core.PropertyEditors;
|
||||
|
||||
/// <summary>
|
||||
/// Provides an abstract base class for property value editors based on block editors.
|
||||
/// </summary>
|
||||
public abstract class BlockEditorPropertyValueEditor<TValue, TLayout> : BlockValuePropertyValueEditorBase<TValue, TLayout>
|
||||
where TValue : BlockValue<TLayout>, new()
|
||||
where TLayout : class, IBlockLayoutItem, new()
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="BlockEditorPropertyValueEditor{TValue, TLayout}"/> class.
|
||||
/// </summary>
|
||||
protected BlockEditorPropertyValueEditor(
|
||||
PropertyEditorCollection propertyEditors,
|
||||
DataValueReferenceFactoryCollection dataValueReferenceFactories,
|
||||
@@ -62,13 +66,7 @@ public abstract class BlockEditorPropertyValueEditor<TValue, TLayout> : BlockVal
|
||||
return BlockEditorValues.DeserializeAndClean(rawJson)?.BlockValue;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ensure that sub-editor values are translated through their ToEditor methods
|
||||
/// </summary>
|
||||
/// <param name="property"></param>
|
||||
/// <param name="culture"></param>
|
||||
/// <param name="segment"></param>
|
||||
/// <returns></returns>
|
||||
/// <inheritdoc />
|
||||
public override object ToEditor(IProperty property, string? culture = null, string? segment = null)
|
||||
{
|
||||
var val = property.GetValue(culture, segment);
|
||||
@@ -95,38 +93,48 @@ public abstract class BlockEditorPropertyValueEditor<TValue, TLayout> : BlockVal
|
||||
return blockEditorData.BlockValue;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ensure that sub-editor values are translated through their FromEditor methods
|
||||
/// </summary>
|
||||
/// <param name="editorValue"></param>
|
||||
/// <param name="currentValue"></param>
|
||||
/// <returns></returns>
|
||||
/// <inheritdoc />
|
||||
public override object? FromEditor(ContentPropertyData editorValue, object? currentValue)
|
||||
{
|
||||
if (editorValue.Value == null || string.IsNullOrWhiteSpace(editorValue.Value.ToString()))
|
||||
// Note: we can't early return here if editorValue is null or empty, because these is the following case:
|
||||
// - current value not null (which means doc has at least one element in block list)
|
||||
// - editor value (new value) is null (which means doc has no elements in block list)
|
||||
// If we check editor value for null value and return before MapBlockValueFromEditor, then we will not trigger updates for properties.
|
||||
// For most of the properties this is fine, but for properties which contain other state it might be critical (e.g. file upload field).
|
||||
// So, we must run MapBlockValueFromEditor even if editorValue is null or string.IsNullOrWhiteSpace(editorValue.Value.ToString()) is true.
|
||||
|
||||
BlockEditorData<TValue, TLayout>? currentBlockEditorData = GetBlockEditorData(currentValue);
|
||||
BlockEditorData<TValue, TLayout>? blockEditorData = GetBlockEditorData(editorValue.Value);
|
||||
|
||||
// We can skip MapBlockValueFromEditor if both editorValue and currentValue values are empty.
|
||||
if (IsBlockEditorDataEmpty(currentBlockEditorData) && IsBlockEditorDataEmpty(blockEditorData))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
BlockEditorData<TValue, TLayout>? blockEditorData;
|
||||
MapBlockValueFromEditor(blockEditorData?.BlockValue, currentBlockEditorData?.BlockValue, editorValue.ContentKey);
|
||||
|
||||
if (IsBlockEditorDataEmpty(blockEditorData))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return JsonSerializer.Serialize(blockEditorData.BlockValue);
|
||||
}
|
||||
|
||||
private BlockEditorData<TValue, TLayout>? GetBlockEditorData(object? value)
|
||||
{
|
||||
try
|
||||
{
|
||||
blockEditorData = BlockEditorValues.DeserializeAndClean(editorValue.Value);
|
||||
return BlockEditorValues.DeserializeAndClean(value);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// if this occurs it means the data is invalid, shouldn't happen but has happened if we change the data format.
|
||||
return string.Empty;
|
||||
// If this occurs it means the data is invalid. It shouldn't happen could if we change the data format.
|
||||
return null;
|
||||
}
|
||||
|
||||
if (blockEditorData == null || blockEditorData.BlockValue.ContentData.Count == 0)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
|
||||
MapBlockValueFromEditor(blockEditorData.BlockValue);
|
||||
|
||||
// return json
|
||||
return JsonSerializer.Serialize(blockEditorData.BlockValue);
|
||||
}
|
||||
|
||||
private static bool IsBlockEditorDataEmpty([NotNullWhen(false)] BlockEditorData<TValue, TLayout>? editorData)
|
||||
=> editorData is null || editorData.BlockValue.ContentData.Count == 0;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
using Umbraco.Cms.Core.Cache.PropertyEditors;
|
||||
using Umbraco.Cms.Core.Cache.PropertyEditors;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Models.Blocks;
|
||||
using Umbraco.Cms.Core.Models.Validation;
|
||||
@@ -88,7 +88,13 @@ public abstract class BlockEditorValidatorBase<TValue, TLayout> : ComplexEditorV
|
||||
|
||||
foreach (var group in itemDataGroups)
|
||||
{
|
||||
var allElementTypes = _elementTypeCache.GetMany(group.Items.Select(x => x.ContentTypeKey).ToArray()).ToDictionary(x => x.Key);
|
||||
Guid[] elementTypeKeys = group.Items.Select(x => x.ContentTypeKey).ToArray();
|
||||
if (elementTypeKeys.Length == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var allElementTypes = _elementTypeCache.GetMany(elementTypeKeys).ToDictionary(x => x.Key);
|
||||
|
||||
for (var i = 0; i < group.Items.Length; i++)
|
||||
{
|
||||
|
||||
@@ -129,10 +129,115 @@ public abstract class BlockValuePropertyValueEditorBase<TValue, TLayout> : DataV
|
||||
return result;
|
||||
}
|
||||
|
||||
protected void MapBlockValueFromEditor(TValue blockValue)
|
||||
[Obsolete("This method is no longer used within Umbraco. Please use the overload taking all parameters. Scheduled for removal in Umbraco 17.")]
|
||||
protected void MapBlockValueFromEditor(TValue blockValue) => MapBlockValueFromEditor(blockValue, null, Guid.Empty);
|
||||
|
||||
protected void MapBlockValueFromEditor(TValue? editedBlockValue, TValue? currentBlockValue, Guid contentKey)
|
||||
{
|
||||
MapBlockItemDataFromEditor(blockValue.ContentData);
|
||||
MapBlockItemDataFromEditor(blockValue.SettingsData);
|
||||
MapBlockItemDataFromEditor(
|
||||
editedBlockValue?.ContentData ?? [],
|
||||
currentBlockValue?.ContentData ?? [],
|
||||
contentKey);
|
||||
|
||||
MapBlockItemDataFromEditor(
|
||||
editedBlockValue?.SettingsData ?? [],
|
||||
currentBlockValue?.SettingsData ?? [],
|
||||
contentKey);
|
||||
}
|
||||
|
||||
private void MapBlockItemDataFromEditor(List<BlockItemData> editedItems, List<BlockItemData> currentItems, Guid contentKey)
|
||||
{
|
||||
// Create mapping between edited and current block items.
|
||||
IEnumerable<BlockStateMapping<BlockItemData>> itemsMapping = GetBlockStatesMapping(editedItems, currentItems, (mapping, current) => mapping.Edited?.Key == current.Key);
|
||||
|
||||
foreach (BlockStateMapping<BlockItemData> itemMapping in itemsMapping)
|
||||
{
|
||||
// Create mapping between edited and current block item values.
|
||||
IEnumerable<BlockStateMapping<BlockPropertyValue>> valuesMapping = GetBlockStatesMapping(itemMapping.Edited?.Values, itemMapping.Current?.Values, (mapping, current) => mapping.Edited?.Alias == current.Alias);
|
||||
|
||||
foreach (BlockStateMapping<BlockPropertyValue> valueMapping in valuesMapping)
|
||||
{
|
||||
BlockPropertyValue? editedValue = valueMapping.Edited;
|
||||
BlockPropertyValue? currentValue = valueMapping.Current;
|
||||
|
||||
IPropertyType propertyType = editedValue?.PropertyType
|
||||
?? currentValue?.PropertyType
|
||||
?? throw new ArgumentException("One or more block properties did not have a resolved property type. Block editor values must be resolved before attempting to map them from editor.", nameof(editedItems));
|
||||
|
||||
// Lookup the property editor.
|
||||
IDataEditor? propertyEditor = _propertyEditors[propertyType.PropertyEditorAlias];
|
||||
if (propertyEditor is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Fetch the property types prevalue.
|
||||
var configuration = _dataTypeConfigurationCache.GetConfiguration(propertyType.DataTypeKey);
|
||||
|
||||
// Create a real content property data object.
|
||||
var propertyData = new ContentPropertyData(editedValue?.Value, configuration)
|
||||
{
|
||||
ContentKey = contentKey,
|
||||
PropertyTypeKey = propertyType.Key,
|
||||
};
|
||||
|
||||
// Get the property editor to do it's conversion.
|
||||
IDataValueEditor valueEditor = propertyEditor.GetValueEditor();
|
||||
var newValue = valueEditor.FromEditor(propertyData, currentValue?.Value);
|
||||
|
||||
// Update the raw value since this is what will get serialized out.
|
||||
if (editedValue != null)
|
||||
{
|
||||
editedValue.Value = newValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class BlockStateMapping<T>
|
||||
{
|
||||
public T? Edited { get; set; }
|
||||
|
||||
public T? Current { get; set; }
|
||||
}
|
||||
|
||||
private static IEnumerable<BlockStateMapping<T>> GetBlockStatesMapping<T>(IList<T>? editedItems, IList<T>? currentItems, Func<BlockStateMapping<T>, T, bool> condition)
|
||||
{
|
||||
// filling with edited items first
|
||||
List<BlockStateMapping<T>> mapping = editedItems?
|
||||
.Select(editedItem => new BlockStateMapping<T>
|
||||
{
|
||||
Current = default,
|
||||
Edited = editedItem,
|
||||
})
|
||||
.ToList()
|
||||
?? [];
|
||||
|
||||
if (currentItems is null)
|
||||
{
|
||||
return mapping;
|
||||
}
|
||||
|
||||
// then adding current items
|
||||
foreach (T currentItem in currentItems)
|
||||
{
|
||||
BlockStateMapping<T>? mappingItem = mapping.FirstOrDefault(x => condition(x, currentItem));
|
||||
|
||||
if (mappingItem == null) // if there is no edited item, then adding just current
|
||||
{
|
||||
mapping.Add(new BlockStateMapping<T>
|
||||
{
|
||||
Current = currentItem,
|
||||
Edited = default,
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
mappingItem.Current = currentItem;
|
||||
}
|
||||
}
|
||||
|
||||
return mapping;
|
||||
}
|
||||
|
||||
protected void MapBlockValueToEditor(IProperty property, TValue blockValue, string? culture, string? segment)
|
||||
@@ -197,40 +302,6 @@ public abstract class BlockValuePropertyValueEditorBase<TValue, TLayout> : DataV
|
||||
}
|
||||
}
|
||||
|
||||
private void MapBlockItemDataFromEditor(List<BlockItemData> items)
|
||||
{
|
||||
foreach (BlockItemData item in items)
|
||||
{
|
||||
foreach (BlockPropertyValue blockPropertyValue in item.Values)
|
||||
{
|
||||
IPropertyType? propertyType = blockPropertyValue.PropertyType;
|
||||
if (propertyType is null)
|
||||
{
|
||||
throw new ArgumentException("One or more block properties did not have a resolved property type. Block editor values must be resolved before attempting to map them from editor.", nameof(items));
|
||||
}
|
||||
|
||||
// Lookup the property editor
|
||||
IDataEditor? propertyEditor = _propertyEditors[propertyType.PropertyEditorAlias];
|
||||
if (propertyEditor is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Fetch the property types prevalue
|
||||
var configuration = _dataTypeConfigurationCache.GetConfiguration(propertyType.DataTypeKey);
|
||||
|
||||
// Create a fake content property data object
|
||||
var propertyData = new ContentPropertyData(blockPropertyValue.Value, configuration);
|
||||
|
||||
// Get the property editor to do it's conversion
|
||||
var newValue = propertyEditor.GetValueEditor().FromEditor(propertyData, blockPropertyValue.Value);
|
||||
|
||||
// update the raw value since this is what will get serialized out
|
||||
blockPropertyValue.Value = newValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Updates the invariant data in the source with the invariant data in the value if allowed
|
||||
/// </summary>
|
||||
|
||||
@@ -10,10 +10,16 @@ using Umbraco.Cms.Core.Media;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Notifications;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Core.PropertyEditors;
|
||||
|
||||
// TODO (V17):
|
||||
// - Remove the implementation of INotificationHandler as these have all been refactored out into sepate notification handler classes.
|
||||
// - Remove the unused parameters from the constructor.
|
||||
|
||||
/// <summary>
|
||||
/// Defines the file upload property editor.
|
||||
/// </summary>
|
||||
[DataEditor(
|
||||
Constants.PropertyEditors.Aliases.UploadField,
|
||||
ValueEditorIsReusable = true)]
|
||||
@@ -22,12 +28,11 @@ public class FileUploadPropertyEditor : DataEditor, IMediaUrlGenerator,
|
||||
INotificationHandler<MediaDeletedNotification>, INotificationHandler<MediaSavingNotification>,
|
||||
INotificationHandler<MemberDeletedNotification>
|
||||
{
|
||||
private readonly IContentService _contentService;
|
||||
private readonly IOptionsMonitor<ContentSettings> _contentSettings;
|
||||
private readonly IIOHelper _ioHelper;
|
||||
private readonly MediaFileManager _mediaFileManager;
|
||||
private readonly UploadAutoFillProperties _uploadAutoFillProperties;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="FileUploadPropertyEditor"/> class.
|
||||
/// </summary>
|
||||
public FileUploadPropertyEditor(
|
||||
IDataValueEditorFactory dataValueEditorFactory,
|
||||
MediaFileManager mediaFileManager,
|
||||
@@ -37,14 +42,11 @@ public class FileUploadPropertyEditor : DataEditor, IMediaUrlGenerator,
|
||||
IIOHelper ioHelper)
|
||||
: base(dataValueEditorFactory)
|
||||
{
|
||||
_mediaFileManager = mediaFileManager ?? throw new ArgumentNullException(nameof(mediaFileManager));
|
||||
_contentSettings = contentSettings;
|
||||
_uploadAutoFillProperties = uploadAutoFillProperties;
|
||||
_contentService = contentService;
|
||||
_ioHelper = ioHelper;
|
||||
SupportsReadOnly = true;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public bool TryGetMediaPath(string? propertyEditorAlias, object? value, [MaybeNullWhen(false)] out string mediaPath)
|
||||
{
|
||||
if (propertyEditorAlias == Alias &&
|
||||
@@ -59,53 +61,6 @@ public class FileUploadPropertyEditor : DataEditor, IMediaUrlGenerator,
|
||||
return false;
|
||||
}
|
||||
|
||||
public void Handle(ContentCopiedNotification notification)
|
||||
{
|
||||
// get the upload field properties with a value
|
||||
IEnumerable<IProperty> properties = notification.Original.Properties.Where(IsUploadField);
|
||||
|
||||
// copy files
|
||||
var isUpdated = false;
|
||||
foreach (IProperty property in properties)
|
||||
{
|
||||
// copy each of the property values (variants, segments) to the destination
|
||||
foreach (IPropertyValue propertyValue in property.Values)
|
||||
{
|
||||
var propVal = property.GetValue(propertyValue.Culture, propertyValue.Segment);
|
||||
if (propVal == null || !(propVal is string str) || str.IsNullOrWhiteSpace())
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var sourcePath = _mediaFileManager.FileSystem.GetRelativePath(str);
|
||||
var copyPath = _mediaFileManager.CopyFile(notification.Copy, property.PropertyType, sourcePath);
|
||||
notification.Copy.SetValue(property.Alias, _mediaFileManager.FileSystem.GetUrl(copyPath),
|
||||
propertyValue.Culture, propertyValue.Segment);
|
||||
isUpdated = true;
|
||||
}
|
||||
}
|
||||
|
||||
// if updated, re-save the copy with the updated value
|
||||
if (isUpdated)
|
||||
{
|
||||
_contentService.Save(notification.Copy);
|
||||
}
|
||||
}
|
||||
|
||||
public void Handle(ContentDeletedNotification notification) => DeleteContainedFiles(notification.DeletedEntities);
|
||||
|
||||
public void Handle(MediaDeletedNotification notification) => DeleteContainedFiles(notification.DeletedEntities);
|
||||
|
||||
public void Handle(MediaSavingNotification notification)
|
||||
{
|
||||
foreach (IMedia entity in notification.SavedEntities)
|
||||
{
|
||||
AutoFillProperties(entity);
|
||||
}
|
||||
}
|
||||
|
||||
public void Handle(MemberDeletedNotification notification) => DeleteContainedFiles(notification.DeletedEntities);
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override IConfigurationEditor CreateConfigurationEditor() =>
|
||||
new FileUploadConfigurationEditor(_ioHelper);
|
||||
@@ -117,86 +72,43 @@ public class FileUploadPropertyEditor : DataEditor, IMediaUrlGenerator,
|
||||
protected override IDataValueEditor CreateValueEditor()
|
||||
=> DataValueEditorFactory.Create<FileUploadPropertyValueEditor>(Attribute!);
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether a property is an upload field.
|
||||
/// </summary>
|
||||
/// <param name="property">The property.</param>
|
||||
/// <returns>
|
||||
/// <c>true</c> if the specified property is an upload field; otherwise, <c>false</c>.
|
||||
/// </returns>
|
||||
private static bool IsUploadField(IProperty property) => property.PropertyType.PropertyEditorAlias ==
|
||||
Constants.PropertyEditors.Aliases.UploadField;
|
||||
#region Obsolete notification handler notifications
|
||||
|
||||
/// <summary>
|
||||
/// The paths to all file upload property files contained within a collection of content entities
|
||||
/// </summary>
|
||||
/// <param name="entities"></param>
|
||||
private IEnumerable<string> ContainedFilePaths(IEnumerable<IContentBase> entities) => entities
|
||||
.SelectMany(x => x.Properties)
|
||||
.Where(IsUploadField)
|
||||
.SelectMany(GetFilePathsFromPropertyValues)
|
||||
.Distinct();
|
||||
|
||||
/// <summary>
|
||||
/// Look through all property values stored against the property and resolve any file paths stored
|
||||
/// </summary>
|
||||
/// <param name="prop"></param>
|
||||
/// <returns></returns>
|
||||
private IEnumerable<string> GetFilePathsFromPropertyValues(IProperty prop)
|
||||
/// <inheritdoc/>
|
||||
[Obsolete("This handler is no longer registered. Logic has been migrated to FileUploadContentCopiedNotificationHandler. Scheduled for removal in Umbraco 17.")]
|
||||
public void Handle(ContentCopiedNotification notification)
|
||||
{
|
||||
IReadOnlyCollection<IPropertyValue> propVals = prop.Values;
|
||||
foreach (IPropertyValue propertyValue in propVals)
|
||||
{
|
||||
// check if the published value contains data and return it
|
||||
var propVal = propertyValue.PublishedValue;
|
||||
if (propVal != null && propVal is string str1 && !str1.IsNullOrWhiteSpace())
|
||||
{
|
||||
yield return _mediaFileManager.FileSystem.GetRelativePath(str1);
|
||||
}
|
||||
|
||||
// check if the edited value contains data and return it
|
||||
propVal = propertyValue.EditedValue;
|
||||
if (propVal != null && propVal is string str2 && !str2.IsNullOrWhiteSpace())
|
||||
{
|
||||
yield return _mediaFileManager.FileSystem.GetRelativePath(str2);
|
||||
}
|
||||
}
|
||||
// This handler is no longer registered. Logic has been migrated to FileUploadContentCopiedNotificationHandler.
|
||||
}
|
||||
|
||||
private void DeleteContainedFiles(IEnumerable<IContentBase> deletedEntities)
|
||||
/// <inheritdoc/>
|
||||
[Obsolete("This handler is no longer registered. Logic has been migrated to FileUploadMediaSavingNotificationHandler. Scheduled for removal in Umbraco 17.")]
|
||||
public void Handle(MediaSavingNotification notification)
|
||||
{
|
||||
IEnumerable<string> filePathsToDelete = ContainedFilePaths(deletedEntities);
|
||||
_mediaFileManager.DeleteMediaFiles(filePathsToDelete);
|
||||
// This handler is no longer registered. Logic has been migrated to FileUploadMediaSavingNotificationHandler.
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Auto-fill properties (or clear).
|
||||
/// </summary>
|
||||
private void AutoFillProperties(IContentBase model)
|
||||
/// <inheritdoc/>
|
||||
[Obsolete("This handler is no longer registered. Logic has been migrated to FileUploadContentDeletedNotificationHandler. Scheduled for removal in Umbraco 17.")]
|
||||
public void Handle(ContentDeletedNotification notification)
|
||||
{
|
||||
IEnumerable<IProperty> properties = model.Properties.Where(IsUploadField);
|
||||
|
||||
foreach (IProperty property in properties)
|
||||
{
|
||||
ImagingAutoFillUploadField? autoFillConfig = _contentSettings.CurrentValue.GetConfig(property.Alias);
|
||||
if (autoFillConfig == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach (IPropertyValue pvalue in property.Values)
|
||||
{
|
||||
var svalue = property.GetValue(pvalue.Culture, pvalue.Segment) as string;
|
||||
if (string.IsNullOrWhiteSpace(svalue))
|
||||
{
|
||||
_uploadAutoFillProperties.Reset(model, autoFillConfig, pvalue.Culture, pvalue.Segment);
|
||||
}
|
||||
else
|
||||
{
|
||||
_uploadAutoFillProperties.Populate(model, autoFillConfig,
|
||||
_mediaFileManager.FileSystem.GetRelativePath(svalue), pvalue.Culture, pvalue.Segment);
|
||||
}
|
||||
}
|
||||
}
|
||||
// This handler is no longer registered. Logic has been migrated to FileUploadContentDeletedNotificationHandler.
|
||||
}
|
||||
|
||||
|
||||
/// <inheritdoc/>
|
||||
[Obsolete("This handler is no longer registered. Logic has been migrated to FileUploadMediaDeletedNotificationHandler. Scheduled for removal in Umbraco 17.")]
|
||||
public void Handle(MediaDeletedNotification notification)
|
||||
{
|
||||
// This handler is no longer registered. Logic has been migrated to FileUploadMediaDeletedNotificationHandler.
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
[Obsolete("This handler is no longer registered. Logic has been migrated to FileUploadMemberDeletedNotificationHandler. Scheduled for removal in Umbraco 17.")]
|
||||
public void Handle(MemberDeletedNotification notification)
|
||||
{
|
||||
// This handler is no longer registered. Logic has been migrated to FileUploadMemberDeletedNotificationHandler.
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ using Umbraco.Cms.Core.Security;
|
||||
using Umbraco.Cms.Core.Serialization;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Core.Strings;
|
||||
using Umbraco.Cms.Infrastructure.PropertyEditors;
|
||||
using Umbraco.Cms.Infrastructure.Scoping;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
@@ -23,12 +24,15 @@ namespace Umbraco.Cms.Core.PropertyEditors;
|
||||
internal class FileUploadPropertyValueEditor : DataValueEditor
|
||||
{
|
||||
private readonly MediaFileManager _mediaFileManager;
|
||||
private readonly IJsonSerializer _jsonSerializer;
|
||||
private readonly ITemporaryFileService _temporaryFileService;
|
||||
private readonly IScopeProvider _scopeProvider;
|
||||
private readonly IFileStreamSecurityValidator _fileStreamSecurityValidator;
|
||||
private readonly FileUploadValueParser _valueParser;
|
||||
private ContentSettings _contentSettings;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="FileUploadPropertyValueEditor"/> class.
|
||||
/// </summary>
|
||||
public FileUploadPropertyValueEditor(
|
||||
DataEditorAttribute attribute,
|
||||
MediaFileManager mediaFileManager,
|
||||
@@ -42,10 +46,11 @@ internal class FileUploadPropertyValueEditor : DataValueEditor
|
||||
: base(shortStringHelper, jsonSerializer, ioHelper, attribute)
|
||||
{
|
||||
_mediaFileManager = mediaFileManager ?? throw new ArgumentNullException(nameof(mediaFileManager));
|
||||
_jsonSerializer = jsonSerializer;
|
||||
_temporaryFileService = temporaryFileService;
|
||||
_scopeProvider = scopeProvider;
|
||||
_fileStreamSecurityValidator = fileStreamSecurityValidator;
|
||||
_valueParser = new FileUploadValueParser(jsonSerializer);
|
||||
|
||||
_contentSettings = contentSettings.CurrentValue ?? throw new ArgumentNullException(nameof(contentSettings));
|
||||
contentSettings.OnChange(x => _contentSettings = x);
|
||||
|
||||
@@ -56,6 +61,7 @@ internal class FileUploadPropertyValueEditor : DataValueEditor
|
||||
IsAllowedInDataTypeConfiguration));
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override object? ToEditor(IProperty property, string? culture = null, string? segment = null)
|
||||
{
|
||||
// the stored property value (if any) is the path to the file; convert it to the client model (FileUploadValue)
|
||||
@@ -63,11 +69,12 @@ internal class FileUploadPropertyValueEditor : DataValueEditor
|
||||
return propertyValue is string stringValue
|
||||
? new FileUploadValue
|
||||
{
|
||||
Src = stringValue
|
||||
Src = stringValue,
|
||||
}
|
||||
: null;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
/// <summary>
|
||||
/// Converts the client model (FileUploadValue) into the value can be stored in the database (the file path).
|
||||
/// </summary>
|
||||
@@ -83,12 +90,15 @@ internal class FileUploadPropertyValueEditor : DataValueEditor
|
||||
/// </remarks>
|
||||
public override object? FromEditor(ContentPropertyData editorValue, object? currentValue)
|
||||
{
|
||||
FileUploadValue? editorModelValue = ParseFileUploadValue(editorValue.Value);
|
||||
FileUploadValue? editorModelValue = _valueParser.Parse(editorValue.Value);
|
||||
|
||||
// no change?
|
||||
if (editorModelValue?.TemporaryFileId.HasValue is not true && string.IsNullOrEmpty(editorModelValue?.Src) is false)
|
||||
{
|
||||
return currentValue;
|
||||
// since current value can be json string, we have to parse value
|
||||
FileUploadValue? currentModelValue = _valueParser.Parse(currentValue);
|
||||
|
||||
return currentModelValue?.Src;
|
||||
}
|
||||
|
||||
// the current editor value (if any) is the path to the file
|
||||
@@ -146,28 +156,8 @@ internal class FileUploadPropertyValueEditor : DataValueEditor
|
||||
return filepath is null ? null : _mediaFileManager.FileSystem.GetUrl(filepath);
|
||||
}
|
||||
|
||||
private FileUploadValue? ParseFileUploadValue(object? editorValue)
|
||||
{
|
||||
if (editorValue is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (editorValue is string sourceString && sourceString.DetectIsJson() is false)
|
||||
{
|
||||
return new FileUploadValue()
|
||||
{
|
||||
Src = sourceString
|
||||
};
|
||||
}
|
||||
|
||||
return _jsonSerializer.TryDeserialize(editorValue, out FileUploadValue? modelValue)
|
||||
? modelValue
|
||||
: throw new ArgumentException($"Could not parse editor value to a {nameof(FileUploadValue)} object.");
|
||||
}
|
||||
|
||||
private Guid? TryParseTemporaryFileKey(object? editorValue)
|
||||
=> ParseFileUploadValue(editorValue)?.TemporaryFileId;
|
||||
=> _valueParser.Parse(editorValue)?.TemporaryFileId;
|
||||
|
||||
private TemporaryFileModel? TryGetTemporaryFile(Guid temporaryFileKey)
|
||||
=> _temporaryFileService.GetAsync(temporaryFileKey).GetAwaiter().GetResult();
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
using Umbraco.Cms.Core.PropertyEditors.ValueConverters;
|
||||
using Umbraco.Cms.Core.Serialization;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Infrastructure.PropertyEditors;
|
||||
|
||||
/// <summary>
|
||||
/// Handles the parsing of raw values to <see cref="FileUploadValue"/> objects.
|
||||
/// </summary>
|
||||
internal sealed class FileUploadValueParser
|
||||
{
|
||||
private readonly IJsonSerializer _jsonSerializer;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="FileUploadValueParser"/> class.
|
||||
/// </summary>
|
||||
/// <param name="jsonSerializer"></param>
|
||||
public FileUploadValueParser(IJsonSerializer jsonSerializer) => _jsonSerializer = jsonSerializer;
|
||||
|
||||
/// <summary>
|
||||
/// Parses raw value to a <see cref="FileUploadValue"/>.
|
||||
/// </summary>
|
||||
/// <param name="editorValue">The editor value.</param>
|
||||
/// <returns><a cref="FileUploadValue"></a> value</returns>
|
||||
/// <exception cref="ArgumentException"></exception>
|
||||
public FileUploadValue? Parse(object? editorValue)
|
||||
{
|
||||
if (editorValue is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (editorValue is string sourceString && sourceString.DetectIsJson() is false)
|
||||
{
|
||||
return new FileUploadValue()
|
||||
{
|
||||
Src = sourceString,
|
||||
};
|
||||
}
|
||||
|
||||
return _jsonSerializer.TryDeserialize(editorValue, out FileUploadValue? modelValue)
|
||||
? modelValue
|
||||
: throw new ArgumentException($"Could not parse editor value to a {nameof(FileUploadValue)} object.");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,312 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.Cache.PropertyEditors;
|
||||
using Umbraco.Cms.Core.Events;
|
||||
using Umbraco.Cms.Core.IO;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Models.Blocks;
|
||||
using Umbraco.Cms.Core.Notifications;
|
||||
using Umbraco.Cms.Core.PropertyEditors;
|
||||
using Umbraco.Cms.Core.PropertyEditors.ValueConverters;
|
||||
using Umbraco.Cms.Core.Serialization;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
|
||||
namespace Umbraco.Cms.Infrastructure.PropertyEditors.NotificationHandlers;
|
||||
|
||||
/// <summary>
|
||||
/// Implements a notification handler that processes file uploads when content is copied, making sure the copied contetnt relates to a new instance
|
||||
/// of the file.
|
||||
/// </summary>
|
||||
internal sealed class FileUploadContentCopiedNotificationHandler : FileUploadNotificationHandlerBase, INotificationHandler<ContentCopiedNotification>
|
||||
{
|
||||
private readonly IContentService _contentService;
|
||||
|
||||
private readonly BlockEditorValues<BlockListValue, BlockListLayoutItem> _blockListEditorValues;
|
||||
private readonly BlockEditorValues<BlockGridValue, BlockGridLayoutItem> _blockGridEditorValues;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="FileUploadContentCopiedNotificationHandler"/> class.
|
||||
/// </summary>
|
||||
public FileUploadContentCopiedNotificationHandler(
|
||||
IJsonSerializer jsonSerializer,
|
||||
MediaFileManager mediaFileManager,
|
||||
IBlockEditorElementTypeCache elementTypeCache,
|
||||
ILogger<FileUploadContentCopiedNotificationHandler> logger,
|
||||
IContentService contentService)
|
||||
: base(jsonSerializer, mediaFileManager, elementTypeCache)
|
||||
{
|
||||
_blockListEditorValues = new(new BlockListEditorDataConverter(jsonSerializer), elementTypeCache, logger);
|
||||
_blockGridEditorValues = new(new BlockGridEditorDataConverter(jsonSerializer), elementTypeCache, logger);
|
||||
_contentService = contentService;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Handle(ContentCopiedNotification notification)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(notification);
|
||||
|
||||
var isUpdated = false;
|
||||
|
||||
foreach (IProperty property in notification.Original.Properties)
|
||||
{
|
||||
if (IsUploadFieldPropertyType(property.PropertyType))
|
||||
{
|
||||
isUpdated |= UpdateUploadFieldProperty(notification, property);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (IsBlockListPropertyType(property.PropertyType))
|
||||
{
|
||||
isUpdated |= UpdateBlockProperty(notification, property, _blockListEditorValues);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (IsBlockGridPropertyType(property.PropertyType))
|
||||
{
|
||||
isUpdated |= UpdateBlockProperty(notification, property, _blockGridEditorValues);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (IsRichTextPropertyType(property.PropertyType))
|
||||
{
|
||||
isUpdated |= UpdateRichTextProperty(notification, property);
|
||||
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// if updated, re-save the copy with the updated value
|
||||
if (isUpdated)
|
||||
{
|
||||
_contentService.Save(notification.Copy);
|
||||
}
|
||||
}
|
||||
|
||||
private bool UpdateUploadFieldProperty(ContentCopiedNotification notification, IProperty property)
|
||||
{
|
||||
var isUpdated = false;
|
||||
|
||||
// Copy each of the property values (variants, segments) to the destination.
|
||||
foreach (IPropertyValue propertyValue in property.Values)
|
||||
{
|
||||
var propVal = property.GetValue(propertyValue.Culture, propertyValue.Segment);
|
||||
if (propVal == null || propVal is not string sourceUrl || string.IsNullOrWhiteSpace(sourceUrl))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var copyUrl = CopyFile(sourceUrl, notification.Copy, property.PropertyType);
|
||||
|
||||
notification.Copy.SetValue(property.Alias, copyUrl, propertyValue.Culture, propertyValue.Segment);
|
||||
|
||||
isUpdated = true;
|
||||
}
|
||||
|
||||
return isUpdated;
|
||||
}
|
||||
|
||||
private bool UpdateBlockProperty<TValue, TLayout>(ContentCopiedNotification notification, IProperty property, BlockEditorValues<TValue, TLayout> blockEditorValues)
|
||||
where TValue : BlockValue<TLayout>, new()
|
||||
where TLayout : class, IBlockLayoutItem, new()
|
||||
{
|
||||
var isUpdated = false;
|
||||
|
||||
foreach (IPropertyValue blockPropertyValue in property.Values)
|
||||
{
|
||||
var rawBlockPropertyValue = property.GetValue(blockPropertyValue.Culture, blockPropertyValue.Segment);
|
||||
|
||||
BlockEditorData<TValue, TLayout>? blockEditorData = GetBlockEditorData(rawBlockPropertyValue, blockEditorValues);
|
||||
|
||||
(bool hasUpdates, string? updatedValue) = UpdateBlockEditorData(notification, blockEditorData);
|
||||
|
||||
if (hasUpdates)
|
||||
{
|
||||
notification.Copy.SetValue(property.Alias, updatedValue, blockPropertyValue.Culture, blockPropertyValue.Segment);
|
||||
}
|
||||
|
||||
isUpdated |= hasUpdates;
|
||||
}
|
||||
|
||||
return isUpdated;
|
||||
}
|
||||
|
||||
private (bool, string?) UpdateBlockEditorData<TValue, TLayout>(ContentCopiedNotification notification, BlockEditorData<TValue, TLayout>? blockEditorData)
|
||||
where TValue : BlockValue<TLayout>, new()
|
||||
where TLayout : class, IBlockLayoutItem, new()
|
||||
{
|
||||
var isUpdated = false;
|
||||
|
||||
if (blockEditorData is null)
|
||||
{
|
||||
return (isUpdated, null);
|
||||
}
|
||||
|
||||
IEnumerable<BlockPropertyValue> blockPropertyValues = blockEditorData.BlockValue.ContentData
|
||||
.Concat(blockEditorData.BlockValue.SettingsData)
|
||||
.SelectMany(x => x.Values);
|
||||
|
||||
isUpdated = UpdateBlockPropertyValues(notification, isUpdated, blockPropertyValues);
|
||||
|
||||
var updatedValue = JsonSerializer.Serialize(blockEditorData.BlockValue);
|
||||
|
||||
return (isUpdated, updatedValue);
|
||||
}
|
||||
|
||||
private bool UpdateRichTextProperty(ContentCopiedNotification notification, IProperty property)
|
||||
{
|
||||
var isUpdated = false;
|
||||
|
||||
foreach (IPropertyValue blockPropertyValue in property.Values)
|
||||
{
|
||||
var rawBlockPropertyValue = property.GetValue(blockPropertyValue.Culture, blockPropertyValue.Segment);
|
||||
|
||||
RichTextBlockValue? richTextBlockValue = GetRichTextBlockValue(rawBlockPropertyValue);
|
||||
|
||||
(bool hasUpdates, string? updatedValue) = UpdateBlockEditorData(notification, richTextBlockValue);
|
||||
|
||||
if (hasUpdates && string.IsNullOrEmpty(updatedValue) is false)
|
||||
{
|
||||
RichTextEditorValue? richTextEditorValue = GetRichTextEditorValue(rawBlockPropertyValue);
|
||||
if (richTextEditorValue is not null)
|
||||
{
|
||||
richTextEditorValue.Blocks = JsonSerializer.Deserialize<RichTextBlockValue>(updatedValue);
|
||||
notification.Copy.SetValue(property.Alias, JsonSerializer.Serialize(richTextEditorValue), blockPropertyValue.Culture, blockPropertyValue.Segment);
|
||||
}
|
||||
}
|
||||
|
||||
isUpdated |= hasUpdates;
|
||||
}
|
||||
|
||||
return isUpdated;
|
||||
}
|
||||
|
||||
private (bool, string?) UpdateBlockEditorData(ContentCopiedNotification notification, RichTextBlockValue? richTextBlockValue)
|
||||
{
|
||||
var isUpdated = false;
|
||||
|
||||
if (richTextBlockValue is null)
|
||||
{
|
||||
return (isUpdated, null);
|
||||
}
|
||||
|
||||
IEnumerable<BlockPropertyValue> blockPropertyValues = richTextBlockValue.ContentData
|
||||
.Concat(richTextBlockValue.SettingsData)
|
||||
.SelectMany(x => x.Values);
|
||||
|
||||
isUpdated = UpdateBlockPropertyValues(notification, isUpdated, blockPropertyValues);
|
||||
|
||||
var updatedValue = JsonSerializer.Serialize(richTextBlockValue);
|
||||
|
||||
return (isUpdated, updatedValue);
|
||||
}
|
||||
|
||||
private bool UpdateBlockPropertyValues(ContentCopiedNotification notification, bool isUpdated, IEnumerable<BlockPropertyValue> blockPropertyValues)
|
||||
{
|
||||
foreach (BlockPropertyValue blockPropertyValue in blockPropertyValues)
|
||||
{
|
||||
if (blockPropertyValue.Value is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
IPropertyType? propertyType = blockPropertyValue.PropertyType;
|
||||
|
||||
if (propertyType is null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (IsUploadFieldPropertyType(propertyType))
|
||||
{
|
||||
isUpdated |= UpdateUploadFieldBlockPropertyValue(blockPropertyValue, notification, propertyType);
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (IsBlockListPropertyType(propertyType))
|
||||
{
|
||||
(bool hasUpdates, string? newValue) = UpdateBlockPropertyValue(blockPropertyValue, notification, _blockListEditorValues);
|
||||
|
||||
isUpdated |= hasUpdates;
|
||||
|
||||
blockPropertyValue.Value = newValue;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (IsBlockGridPropertyType(propertyType))
|
||||
{
|
||||
(bool hasUpdates, string? newValue) = UpdateBlockPropertyValue(blockPropertyValue, notification, _blockGridEditorValues);
|
||||
|
||||
isUpdated |= hasUpdates;
|
||||
|
||||
blockPropertyValue.Value = newValue;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (IsRichTextPropertyType(propertyType))
|
||||
{
|
||||
(bool hasUpdates, string? newValue) = UpdateRichTextPropertyValue(blockPropertyValue, notification);
|
||||
|
||||
if (hasUpdates && string.IsNullOrEmpty(newValue) is false)
|
||||
{
|
||||
RichTextEditorValue? richTextEditorValue = GetRichTextEditorValue(blockPropertyValue.Value);
|
||||
if (richTextEditorValue is not null)
|
||||
{
|
||||
isUpdated |= hasUpdates;
|
||||
|
||||
richTextEditorValue.Blocks = JsonSerializer.Deserialize<RichTextBlockValue>(newValue);
|
||||
blockPropertyValue.Value = richTextEditorValue;
|
||||
}
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
return isUpdated;
|
||||
}
|
||||
|
||||
private bool UpdateUploadFieldBlockPropertyValue(BlockPropertyValue blockItemDataValue, ContentCopiedNotification notification, IPropertyType propertyType)
|
||||
{
|
||||
FileUploadValue? fileUploadValue = FileUploadValueParser.Parse(blockItemDataValue.Value);
|
||||
|
||||
// if original value is empty, we do not need to copy file
|
||||
if (string.IsNullOrWhiteSpace(fileUploadValue?.Src))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var copyFileUrl = CopyFile(fileUploadValue.Src, notification.Copy, propertyType);
|
||||
|
||||
blockItemDataValue.Value = copyFileUrl;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private (bool, string?) UpdateBlockPropertyValue<TValue, TLayout>(BlockPropertyValue blockItemDataValue, ContentCopiedNotification notification, BlockEditorValues<TValue, TLayout> blockEditorValues)
|
||||
where TValue : BlockValue<TLayout>, new()
|
||||
where TLayout : class, IBlockLayoutItem, new()
|
||||
{
|
||||
BlockEditorData<TValue, TLayout>? blockItemEditorDataValue = GetBlockEditorData(blockItemDataValue.Value, blockEditorValues);
|
||||
|
||||
return UpdateBlockEditorData(notification, blockItemEditorDataValue);
|
||||
}
|
||||
|
||||
private (bool, string?) UpdateRichTextPropertyValue(BlockPropertyValue blockItemDataValue, ContentCopiedNotification notification)
|
||||
{
|
||||
RichTextBlockValue? richTextBlockValue = GetRichTextBlockValue(blockItemDataValue.Value);
|
||||
return UpdateBlockEditorData(notification, richTextBlockValue);
|
||||
}
|
||||
|
||||
private string CopyFile(string sourceUrl, IContent destinationContent, IPropertyType propertyType)
|
||||
{
|
||||
var sourcePath = MediaFileManager.FileSystem.GetRelativePath(sourceUrl);
|
||||
var copyPath = MediaFileManager.CopyFile(destinationContent, propertyType, sourcePath);
|
||||
return MediaFileManager.FileSystem.GetUrl(copyPath);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Umbraco.Cms.Core.Cache.PropertyEditors;
|
||||
using Umbraco.Cms.Core.Events;
|
||||
using Umbraco.Cms.Core.IO;
|
||||
using Umbraco.Cms.Core.Notifications;
|
||||
using Umbraco.Cms.Core.Serialization;
|
||||
|
||||
namespace Umbraco.Cms.Infrastructure.PropertyEditors.NotificationHandlers;
|
||||
|
||||
/// <summary>
|
||||
/// Implements a notification handler that processes file uploads when content is deleted, removing associated files.
|
||||
/// </summary>
|
||||
internal sealed class FileUploadContentDeletedNotificationHandler : FileUploadEntityDeletedNotificationHandlerBase, INotificationHandler<ContentDeletedNotification>
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="FileUploadContentDeletedNotificationHandler"/> class.
|
||||
/// </summary>
|
||||
public FileUploadContentDeletedNotificationHandler(
|
||||
IJsonSerializer jsonSerializer,
|
||||
MediaFileManager mediaFileManager,
|
||||
IBlockEditorElementTypeCache elementTypeCache,
|
||||
ILogger<FileUploadContentDeletedNotificationHandler> logger)
|
||||
: base(jsonSerializer, mediaFileManager, elementTypeCache, logger)
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Handle(ContentDeletedNotification notification) => DeleteContainedFiles(notification.DeletedEntities);
|
||||
}
|
||||
@@ -0,0 +1,218 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.Cache.PropertyEditors;
|
||||
using Umbraco.Cms.Core.IO;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Models.Blocks;
|
||||
using Umbraco.Cms.Core.PropertyEditors;
|
||||
using Umbraco.Cms.Core.PropertyEditors.ValueConverters;
|
||||
using Umbraco.Cms.Core.Serialization;
|
||||
using Umbraco.Cms.Infrastructure.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Infrastructure.PropertyEditors.NotificationHandlers;
|
||||
|
||||
/// <summary>
|
||||
/// Provides base class for notification handler that processes file uploads when a content entity is deleted, removing associated files.
|
||||
/// </summary>
|
||||
internal abstract class FileUploadEntityDeletedNotificationHandlerBase : FileUploadNotificationHandlerBase
|
||||
{
|
||||
private readonly BlockEditorValues<BlockListValue, BlockListLayoutItem> _blockListEditorValues;
|
||||
private readonly BlockEditorValues<BlockGridValue, BlockGridLayoutItem> _blockGridEditorValues;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="FileUploadEntityDeletedNotificationHandlerBase"/> class.
|
||||
/// </summary>
|
||||
protected FileUploadEntityDeletedNotificationHandlerBase(
|
||||
IJsonSerializer jsonSerializer,
|
||||
MediaFileManager mediaFileManager,
|
||||
IBlockEditorElementTypeCache elementTypeCache,
|
||||
ILogger logger)
|
||||
: base(jsonSerializer, mediaFileManager, elementTypeCache)
|
||||
{
|
||||
_blockListEditorValues = new(new BlockListEditorDataConverter(jsonSerializer), elementTypeCache, logger);
|
||||
_blockGridEditorValues = new(new BlockGridEditorDataConverter(jsonSerializer), elementTypeCache, logger);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deletes all file upload property files contained within a collection of content entities.
|
||||
/// </summary>
|
||||
/// <param name="deletedEntities"></param>
|
||||
protected void DeleteContainedFiles(IEnumerable<IContentBase> deletedEntities)
|
||||
{
|
||||
IReadOnlyList<string> filePathsToDelete = ContainedFilePaths(deletedEntities);
|
||||
MediaFileManager.DeleteMediaFiles(filePathsToDelete);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the paths to all file upload property files contained within a collection of content entities.
|
||||
/// </summary>
|
||||
private IReadOnlyList<string> ContainedFilePaths(IEnumerable<IContentBase> entities)
|
||||
{
|
||||
var paths = new List<string>();
|
||||
|
||||
foreach (IProperty? property in entities.SelectMany(x => x.Properties))
|
||||
{
|
||||
if (IsUploadFieldPropertyType(property.PropertyType))
|
||||
{
|
||||
paths.AddRange(GetPathsFromUploadFieldProperty(property));
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (IsBlockListPropertyType(property.PropertyType))
|
||||
{
|
||||
paths.AddRange(GetPathsFromBlockProperty(property, _blockListEditorValues));
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (IsBlockGridPropertyType(property.PropertyType))
|
||||
{
|
||||
paths.AddRange(GetPathsFromBlockProperty(property, _blockGridEditorValues));
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (IsRichTextPropertyType(property.PropertyType))
|
||||
{
|
||||
paths.AddRange(GetPathsFromRichTextProperty(property));
|
||||
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
return paths.Distinct().ToList().AsReadOnly();
|
||||
}
|
||||
|
||||
private IEnumerable<string> GetPathsFromUploadFieldProperty(IProperty property)
|
||||
{
|
||||
foreach (IPropertyValue propertyValue in property.Values)
|
||||
{
|
||||
if (propertyValue.PublishedValue != null && propertyValue.PublishedValue is string publishedUrl && !string.IsNullOrWhiteSpace(publishedUrl))
|
||||
{
|
||||
yield return MediaFileManager.FileSystem.GetRelativePath(publishedUrl);
|
||||
}
|
||||
|
||||
if (propertyValue.EditedValue != null && propertyValue.EditedValue is string editedUrl && !string.IsNullOrWhiteSpace(editedUrl))
|
||||
{
|
||||
yield return MediaFileManager.FileSystem.GetRelativePath(editedUrl);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private IReadOnlyCollection<string> GetPathsFromBlockProperty<TValue, TLayout>(IProperty property, BlockEditorValues<TValue, TLayout> blockEditorValues)
|
||||
where TValue : BlockValue<TLayout>, new()
|
||||
where TLayout : class, IBlockLayoutItem, new()
|
||||
{
|
||||
var paths = new List<string>();
|
||||
|
||||
foreach (IPropertyValue blockPropertyValue in property.Values)
|
||||
{
|
||||
paths.AddRange(GetPathsFromBlockValue(GetBlockEditorData(blockPropertyValue.PublishedValue, blockEditorValues)?.BlockValue));
|
||||
paths.AddRange(GetPathsFromBlockValue(GetBlockEditorData(blockPropertyValue.EditedValue, blockEditorValues)?.BlockValue));
|
||||
}
|
||||
|
||||
return paths;
|
||||
}
|
||||
|
||||
private IReadOnlyCollection<string> GetPathsFromBlockValue(BlockValue? blockValue)
|
||||
{
|
||||
var paths = new List<string>();
|
||||
|
||||
if (blockValue is null)
|
||||
{
|
||||
return paths;
|
||||
}
|
||||
|
||||
IEnumerable<BlockPropertyValue> blockPropertyValues = blockValue.ContentData
|
||||
.Concat(blockValue.SettingsData)
|
||||
.SelectMany(x => x.Values);
|
||||
|
||||
foreach (BlockPropertyValue blockPropertyValue in blockPropertyValues)
|
||||
{
|
||||
if (blockPropertyValue.Value == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
IPropertyType? propertyType = blockPropertyValue.PropertyType;
|
||||
|
||||
if (propertyType == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (IsUploadFieldPropertyType(propertyType))
|
||||
{
|
||||
FileUploadValue? originalValue = FileUploadValueParser.Parse(blockPropertyValue.Value);
|
||||
|
||||
if (string.IsNullOrWhiteSpace(originalValue?.Src))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
paths.Add(MediaFileManager.FileSystem.GetRelativePath(originalValue.Src));
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (IsBlockListPropertyType(propertyType))
|
||||
{
|
||||
paths.AddRange(GetPathsFromBlockPropertyValue(blockPropertyValue, _blockListEditorValues));
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (IsBlockGridPropertyType(propertyType))
|
||||
{
|
||||
paths.AddRange(GetPathsFromBlockPropertyValue(blockPropertyValue, _blockGridEditorValues));
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (IsRichTextPropertyType(propertyType))
|
||||
{
|
||||
paths.AddRange(GetPathsFromRichTextPropertyValue(blockPropertyValue));
|
||||
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
return paths;
|
||||
}
|
||||
|
||||
private IReadOnlyCollection<string> GetPathsFromBlockPropertyValue<TValue, TLayout>(BlockPropertyValue blockItemDataValue, BlockEditorValues<TValue, TLayout> blockEditorValues)
|
||||
where TValue : BlockValue<TLayout>, new()
|
||||
where TLayout : class, IBlockLayoutItem, new()
|
||||
{
|
||||
BlockEditorData<TValue, TLayout>? blockItemEditorDataValue = GetBlockEditorData(blockItemDataValue.Value, blockEditorValues);
|
||||
|
||||
return GetPathsFromBlockValue(blockItemEditorDataValue?.BlockValue);
|
||||
}
|
||||
|
||||
private IReadOnlyCollection<string> GetPathsFromRichTextProperty(IProperty property)
|
||||
{
|
||||
var paths = new List<string>();
|
||||
|
||||
IPropertyValue? propertyValue = property.Values.FirstOrDefault();
|
||||
if (propertyValue is null)
|
||||
{
|
||||
return paths;
|
||||
}
|
||||
|
||||
paths.AddRange(GetPathsFromBlockValue(GetRichTextBlockValue(propertyValue.PublishedValue)));
|
||||
paths.AddRange(GetPathsFromBlockValue(GetRichTextBlockValue(propertyValue.EditedValue)));
|
||||
|
||||
return paths;
|
||||
}
|
||||
|
||||
private IReadOnlyCollection<string> GetPathsFromRichTextPropertyValue(BlockPropertyValue blockItemDataValue)
|
||||
{
|
||||
RichTextEditorValue? richTextEditorValue = GetRichTextEditorValue(blockItemDataValue.Value);
|
||||
|
||||
// Ensure the property type is populated on all blocks.
|
||||
richTextEditorValue?.EnsurePropertyTypePopulatedOnBlocks(ElementTypeCache);
|
||||
|
||||
return GetPathsFromBlockValue(richTextEditorValue?.Blocks);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Umbraco.Cms.Core.Cache.PropertyEditors;
|
||||
using Umbraco.Cms.Core.Events;
|
||||
using Umbraco.Cms.Core.IO;
|
||||
using Umbraco.Cms.Core.Notifications;
|
||||
using Umbraco.Cms.Core.Serialization;
|
||||
|
||||
namespace Umbraco.Cms.Infrastructure.PropertyEditors.NotificationHandlers;
|
||||
|
||||
/// <summary>
|
||||
/// Implements a notification handler that processes file uploads when media is deleted, removing associated files.
|
||||
/// </summary>
|
||||
internal sealed class FileUploadMediaDeletedNotificationHandler : FileUploadEntityDeletedNotificationHandlerBase, INotificationHandler<MediaDeletedNotification>
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="FileUploadMediaDeletedNotificationHandler"/> class.
|
||||
/// </summary>
|
||||
public FileUploadMediaDeletedNotificationHandler(
|
||||
IJsonSerializer jsonSerializer,
|
||||
MediaFileManager mediaFileManager,
|
||||
IBlockEditorElementTypeCache elementTypeCache,
|
||||
ILogger<FileUploadContentDeletedNotificationHandler> logger)
|
||||
: base(jsonSerializer, mediaFileManager, elementTypeCache, logger)
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Handle(MediaDeletedNotification notification) => DeleteContainedFiles(notification.DeletedEntities);
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
using Microsoft.Extensions.Options;
|
||||
using Umbraco.Cms.Core.Cache.PropertyEditors;
|
||||
using Umbraco.Cms.Core.Configuration.Models;
|
||||
using Umbraco.Cms.Core.Events;
|
||||
using Umbraco.Cms.Core.IO;
|
||||
using Umbraco.Cms.Core.Media;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Notifications;
|
||||
using Umbraco.Cms.Core.Serialization;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Infrastructure.PropertyEditors.NotificationHandlers;
|
||||
|
||||
/// <summary>
|
||||
/// Implements a notification handler that processes file uploads media is saved, completing properties on the media item.
|
||||
/// </summary>
|
||||
internal sealed class FileUploadMediaSavingNotificationHandler : FileUploadNotificationHandlerBase, INotificationHandler<MediaSavingNotification>
|
||||
{
|
||||
private readonly IOptionsMonitor<ContentSettings> _contentSettings;
|
||||
private readonly UploadAutoFillProperties _uploadAutoFillProperties;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="FileUploadMediaSavingNotificationHandler"/> class.
|
||||
/// </summary>
|
||||
public FileUploadMediaSavingNotificationHandler(
|
||||
IJsonSerializer jsonSerializer,
|
||||
MediaFileManager mediaFileManager,
|
||||
IBlockEditorElementTypeCache elementTypeCache,
|
||||
IOptionsMonitor<ContentSettings> contentSettings,
|
||||
UploadAutoFillProperties uploadAutoFillProperties)
|
||||
: base(jsonSerializer, mediaFileManager, elementTypeCache)
|
||||
{
|
||||
_contentSettings = contentSettings;
|
||||
_uploadAutoFillProperties = uploadAutoFillProperties;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Handle(MediaSavingNotification notification)
|
||||
{
|
||||
foreach (IMedia entity in notification.SavedEntities)
|
||||
{
|
||||
AutoFillProperties(entity);
|
||||
}
|
||||
}
|
||||
|
||||
private void AutoFillProperties(IContentBase model)
|
||||
{
|
||||
IEnumerable<IProperty> properties = model.Properties.Where(x => IsUploadFieldPropertyType(x.PropertyType));
|
||||
|
||||
foreach (IProperty property in properties)
|
||||
{
|
||||
ImagingAutoFillUploadField? autoFillConfig = _contentSettings.CurrentValue.GetConfig(property.Alias);
|
||||
if (autoFillConfig == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
foreach (IPropertyValue pvalue in property.Values)
|
||||
{
|
||||
var svalue = property.GetValue(pvalue.Culture, pvalue.Segment) as string;
|
||||
if (string.IsNullOrWhiteSpace(svalue))
|
||||
{
|
||||
_uploadAutoFillProperties.Reset(model, autoFillConfig, pvalue.Culture, pvalue.Segment);
|
||||
}
|
||||
else
|
||||
{
|
||||
_uploadAutoFillProperties.Populate(
|
||||
model,
|
||||
autoFillConfig,
|
||||
MediaFileManager.FileSystem.GetRelativePath(svalue),
|
||||
pvalue.Culture,
|
||||
pvalue.Segment);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Umbraco.Cms.Core.Cache.PropertyEditors;
|
||||
using Umbraco.Cms.Core.Events;
|
||||
using Umbraco.Cms.Core.IO;
|
||||
using Umbraco.Cms.Core.Notifications;
|
||||
using Umbraco.Cms.Core.Serialization;
|
||||
|
||||
namespace Umbraco.Cms.Infrastructure.PropertyEditors.NotificationHandlers;
|
||||
|
||||
/// <summary>
|
||||
/// Implements a notification handler that processes file uploads when a member is deleted, removing associated files.
|
||||
/// </summary>
|
||||
internal sealed class FileUploadMemberDeletedNotificationHandler : FileUploadEntityDeletedNotificationHandlerBase, INotificationHandler<MemberDeletedNotification>
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="FileUploadMemberDeletedNotificationHandler"/> class.
|
||||
/// </summary>
|
||||
public FileUploadMemberDeletedNotificationHandler(
|
||||
IJsonSerializer jsonSerializer,
|
||||
MediaFileManager mediaFileManager,
|
||||
IBlockEditorElementTypeCache elementTypeCache,
|
||||
ILogger<FileUploadContentDeletedNotificationHandler> logger)
|
||||
: base(jsonSerializer, mediaFileManager, elementTypeCache, logger)
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public void Handle(MemberDeletedNotification notification) => DeleteContainedFiles(notification.DeletedEntities);
|
||||
}
|
||||
@@ -0,0 +1,140 @@
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.Cache.PropertyEditors;
|
||||
using Umbraco.Cms.Core.IO;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Models.Blocks;
|
||||
using Umbraco.Cms.Core.PropertyEditors;
|
||||
using Umbraco.Cms.Core.Serialization;
|
||||
using Umbraco.Cms.Infrastructure.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Infrastructure.PropertyEditors.NotificationHandlers;
|
||||
|
||||
/// <summary>
|
||||
/// Provides a base class for all notification handlers relating to file uploads in property editors.
|
||||
/// </summary>
|
||||
internal abstract class FileUploadNotificationHandlerBase
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="FileUploadNotificationHandlerBase"/> class.
|
||||
/// </summary>
|
||||
protected FileUploadNotificationHandlerBase(
|
||||
IJsonSerializer jsonSerializer,
|
||||
MediaFileManager mediaFileManager,
|
||||
IBlockEditorElementTypeCache elementTypeCache)
|
||||
{
|
||||
JsonSerializer = jsonSerializer;
|
||||
MediaFileManager = mediaFileManager;
|
||||
ElementTypeCache = elementTypeCache;
|
||||
FileUploadValueParser = new FileUploadValueParser(jsonSerializer);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the <see cref="IJsonSerializer" /> used for serializing and deserializing values.
|
||||
/// </summary>
|
||||
protected IJsonSerializer JsonSerializer { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the <see cref="MediaFileManager" /> used for managing media files.
|
||||
/// </summary>
|
||||
protected MediaFileManager MediaFileManager { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the <IBlockEditorElementTypeCache> used for caching block editor element types.
|
||||
/// </summary>
|
||||
protected IBlockEditorElementTypeCache ElementTypeCache { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the <see cref="FileUploadValueParser" /> used for parsing file upload values.
|
||||
/// </summary>
|
||||
protected FileUploadValueParser FileUploadValueParser { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether a property is an upload field.
|
||||
/// </summary>
|
||||
/// <param name="propertyType">The property type.</param>
|
||||
/// <returns>
|
||||
/// <c>true</c> if the specified property is an upload field; otherwise, <c>false</c>.
|
||||
/// </returns>
|
||||
protected static bool IsUploadFieldPropertyType(IPropertyType propertyType)
|
||||
=> propertyType.PropertyEditorAlias == Constants.PropertyEditors.Aliases.UploadField;
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether a property is an block list field.
|
||||
/// </summary>
|
||||
/// <param name="propertyType">The property type.</param>
|
||||
/// <returns>
|
||||
/// <c>true</c> if the specified property is an block list field; otherwise, <c>false</c>.
|
||||
/// </returns>
|
||||
protected static bool IsBlockListPropertyType(IPropertyType propertyType)
|
||||
=> propertyType.PropertyEditorAlias == Constants.PropertyEditors.Aliases.BlockList;
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether a property is an block grid field.
|
||||
/// </summary>
|
||||
/// <param name="propertyType">The property type.</param>
|
||||
/// <returns>
|
||||
/// <c>true</c> if the specified property is an block grid field; otherwise, <c>false</c>.
|
||||
/// </returns>
|
||||
protected static bool IsBlockGridPropertyType(IPropertyType propertyType)
|
||||
=> propertyType.PropertyEditorAlias == Constants.PropertyEditors.Aliases.BlockGrid;
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether a property is an rich text field (supporting blocks).
|
||||
/// </summary>
|
||||
/// <param name="propertyType">The property type.</param>
|
||||
/// <returns>
|
||||
/// <c>true</c> if the specified property is an rich text field; otherwise, <c>false</c>.
|
||||
/// </returns>
|
||||
protected static bool IsRichTextPropertyType(IPropertyType propertyType)
|
||||
=> propertyType.PropertyEditorAlias == Constants.PropertyEditors.Aliases.RichText ||
|
||||
propertyType.PropertyEditorAlias == "Umbraco.TinyMCE";
|
||||
|
||||
/// <summary>
|
||||
/// Deserializes the block editor data value.
|
||||
/// </summary>
|
||||
protected static BlockEditorData<TValue, TLayout>? GetBlockEditorData<TValue, TLayout>(object? value, BlockEditorValues<TValue, TLayout> blockListEditorValues)
|
||||
where TValue : BlockValue<TLayout>, new()
|
||||
where TLayout : class, IBlockLayoutItem, new()
|
||||
{
|
||||
try
|
||||
{
|
||||
return blockListEditorValues.DeserializeAndClean(value);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// If this occurs it means the data is invalid. Shouldn't happen but could if we change the data format.
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deserializes the rich text editor value.
|
||||
/// </summary>
|
||||
protected RichTextEditorValue? GetRichTextEditorValue(object? value)
|
||||
{
|
||||
if (value is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
JsonSerializer.TryDeserialize(value, out RichTextEditorValue? richTextEditorValue);
|
||||
return richTextEditorValue;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Deserializes the rich text block value.
|
||||
/// </summary>
|
||||
protected RichTextBlockValue? GetRichTextBlockValue(object? value)
|
||||
{
|
||||
RichTextEditorValue? richTextEditorValue = GetRichTextEditorValue(value);
|
||||
if (richTextEditorValue?.Blocks is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// Ensure the property type is populated on all blocks.
|
||||
richTextEditorValue.EnsurePropertyTypePopulatedOnBlocks(ElementTypeCache);
|
||||
|
||||
return richTextEditorValue.Blocks;
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,7 @@ using Umbraco.Cms.Core.Serialization;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Core.Strings;
|
||||
using Umbraco.Cms.Core.Templates;
|
||||
using Umbraco.Cms.Infrastructure.Extensions;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Core.PropertyEditors;
|
||||
@@ -33,8 +34,11 @@ public class RichTextPropertyEditor : DataEditor
|
||||
private readonly IRichTextPropertyIndexValueFactory _richTextPropertyIndexValueFactory;
|
||||
|
||||
/// <summary>
|
||||
/// The constructor will setup the property editor based on the attribute if one is found.
|
||||
/// Initializes a new instance of the <see cref="RichTextPropertyEditor"/> class.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The constructor will setup the property editor based on the attribute if one is found.
|
||||
/// </remarks>
|
||||
public RichTextPropertyEditor(
|
||||
IDataValueEditorFactory dataValueEditorFactory,
|
||||
IIOHelper ioHelper,
|
||||
@@ -43,6 +47,7 @@ public class RichTextPropertyEditor : DataEditor
|
||||
{
|
||||
_ioHelper = ioHelper;
|
||||
_richTextPropertyIndexValueFactory = richTextPropertyIndexValueFactory;
|
||||
|
||||
SupportsReadOnly = true;
|
||||
}
|
||||
|
||||
@@ -95,6 +100,7 @@ public class RichTextPropertyEditor : DataEditor
|
||||
private readonly IRichTextRequiredValidator _richTextRequiredValidator;
|
||||
private readonly IRichTextRegexValidator _richTextRegexValidator;
|
||||
private readonly ILogger<RichTextPropertyValueEditor> _logger;
|
||||
private readonly IBlockEditorElementTypeCache _elementTypeCache;
|
||||
|
||||
public RichTextPropertyValueEditor(
|
||||
DataEditorAttribute attribute,
|
||||
@@ -123,6 +129,7 @@ public class RichTextPropertyEditor : DataEditor
|
||||
_localLinkParser = localLinkParser;
|
||||
_pastedImages = pastedImages;
|
||||
_htmlSanitizer = htmlSanitizer;
|
||||
_elementTypeCache = elementTypeCache;
|
||||
_richTextRequiredValidator = richTextRequiredValidator;
|
||||
_richTextRegexValidator = richTextRegexValidator;
|
||||
_jsonSerializer = jsonSerializer;
|
||||
@@ -242,7 +249,23 @@ public class RichTextPropertyEditor : DataEditor
|
||||
/// <returns></returns>
|
||||
public override object? FromEditor(ContentPropertyData editorValue, object? currentValue)
|
||||
{
|
||||
if (TryParseEditorValue(editorValue.Value, out RichTextEditorValue? richTextEditorValue) is false)
|
||||
// See note on BlockEditorPropertyValueEditor.FromEditor for why we can't return early with only a null or empty editorValue.
|
||||
TryParseEditorValue(editorValue.Value, out RichTextEditorValue? richTextEditorValue);
|
||||
TryParseEditorValue(currentValue, out RichTextEditorValue? currentRichTextEditorValue);
|
||||
|
||||
// We can early return if we have a null value and the current value doesn't have any blocks.
|
||||
if (richTextEditorValue is null && currentRichTextEditorValue?.Blocks is null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// Ensure the property type is populated on all blocks.
|
||||
richTextEditorValue?.EnsurePropertyTypePopulatedOnBlocks(_elementTypeCache);
|
||||
currentRichTextEditorValue?.EnsurePropertyTypePopulatedOnBlocks(_elementTypeCache);
|
||||
|
||||
RichTextEditorValue cleanedUpRichTextEditorValue = CleanAndMapBlocks(richTextEditorValue, blockValue => MapBlockValueFromEditor(blockValue, currentRichTextEditorValue?.Blocks, editorValue.ContentKey));
|
||||
|
||||
if (string.IsNullOrWhiteSpace(richTextEditorValue?.Markup))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
@@ -253,11 +276,6 @@ public class RichTextPropertyEditor : DataEditor
|
||||
var config = editorValue.DataTypeConfiguration as RichTextConfiguration;
|
||||
Guid mediaParentId = config?.MediaParentId ?? Guid.Empty;
|
||||
|
||||
if (string.IsNullOrWhiteSpace(richTextEditorValue.Markup))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var parseAndSavedTempImages = _pastedImages
|
||||
.FindAndPersistPastedTempImagesAsync(richTextEditorValue.Markup, mediaParentId, userKey)
|
||||
.GetAwaiter()
|
||||
@@ -267,8 +285,6 @@ public class RichTextPropertyEditor : DataEditor
|
||||
|
||||
richTextEditorValue.Markup = sanitized.NullOrWhiteSpaceAsNull() ?? string.Empty;
|
||||
|
||||
RichTextEditorValue cleanedUpRichTextEditorValue = CleanAndMapBlocks(richTextEditorValue, MapBlockValueFromEditor);
|
||||
|
||||
// return json
|
||||
return RichTextPropertyEditorHelper.SerializeRichTextEditorValue(cleanedUpRichTextEditorValue, _jsonSerializer);
|
||||
}
|
||||
@@ -377,19 +393,26 @@ public class RichTextPropertyEditor : DataEditor
|
||||
private bool TryParseEditorValue(object? value, [NotNullWhen(true)] out RichTextEditorValue? richTextEditorValue)
|
||||
=> RichTextPropertyEditorHelper.TryParseRichTextEditorValue(value, _jsonSerializer, _logger, out richTextEditorValue);
|
||||
|
||||
private RichTextEditorValue CleanAndMapBlocks(RichTextEditorValue richTextEditorValue, Action<RichTextBlockValue> handleMapping)
|
||||
private RichTextEditorValue CleanAndMapBlocks(RichTextEditorValue? richTextEditorValue, Action<RichTextBlockValue> handleMapping)
|
||||
{
|
||||
if (richTextEditorValue.Blocks is null)
|
||||
// We handle mapping of blocks even if the edited value is empty, so property editors can clean up any resources
|
||||
// relating to removed blocks, e.g. files uploaded to the media library from the file upload property editor.
|
||||
BlockEditorData<RichTextBlockValue, RichTextBlockLayoutItem>? blockEditorData = null;
|
||||
if (richTextEditorValue?.Blocks is not null)
|
||||
{
|
||||
blockEditorData = ConvertAndClean(richTextEditorValue.Blocks);
|
||||
}
|
||||
|
||||
handleMapping(blockEditorData?.BlockValue ?? new RichTextBlockValue());
|
||||
|
||||
if (richTextEditorValue?.Blocks is null)
|
||||
{
|
||||
// no blocks defined, store empty block value
|
||||
return MarkupWithEmptyBlocks();
|
||||
}
|
||||
|
||||
BlockEditorData<RichTextBlockValue, RichTextBlockLayoutItem>? blockEditorData = ConvertAndClean(richTextEditorValue.Blocks);
|
||||
|
||||
if (blockEditorData is not null)
|
||||
{
|
||||
handleMapping(blockEditorData.BlockValue);
|
||||
return new RichTextEditorValue
|
||||
{
|
||||
Markup = richTextEditorValue.Markup,
|
||||
@@ -402,7 +425,7 @@ public class RichTextPropertyEditor : DataEditor
|
||||
|
||||
RichTextEditorValue MarkupWithEmptyBlocks() => new()
|
||||
{
|
||||
Markup = richTextEditorValue.Markup,
|
||||
Markup = richTextEditorValue?.Markup ?? string.Empty,
|
||||
Blocks = new RichTextBlockValue(),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { UmbUnsupportedEditorSchemaAliases } from '../../types/index.js';
|
||||
|
||||
export const UMB_UNSUPPORTED_EDITOR_SCHEMA_ALIASES: UmbUnsupportedEditorSchemaAliases = {
|
||||
block: ['Umbraco.ImageCropper', 'Umbraco.UploadField'],
|
||||
block: ['Umbraco.ImageCropper'],
|
||||
};
|
||||
|
||||
@@ -11,6 +11,7 @@ using Microsoft.Extensions.Options;
|
||||
using Moq;
|
||||
using NUnit.Framework;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.Cache.PropertyEditors;
|
||||
using Umbraco.Cms.Core.Configuration.Models;
|
||||
using Umbraco.Cms.Core.IO;
|
||||
using Umbraco.Cms.Core.Media;
|
||||
@@ -18,6 +19,7 @@ using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Models.ContentEditing;
|
||||
using Umbraco.Cms.Core.Persistence.Repositories;
|
||||
using Umbraco.Cms.Core.PropertyEditors;
|
||||
using Umbraco.Cms.Core.Serialization;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Core.Strings;
|
||||
using Umbraco.Cms.Tests.Common.Builders;
|
||||
|
||||
@@ -7,7 +7,9 @@ using NUnit.Framework;
|
||||
using Umbraco.Cms.Core.Cache;
|
||||
using Umbraco.Cms.Core.Cache.PropertyEditors;
|
||||
using Umbraco.Cms.Core.IO;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Models.Blocks;
|
||||
using Umbraco.Cms.Core.Models.Editors;
|
||||
using Umbraco.Cms.Core.Models.Validation;
|
||||
using Umbraco.Cms.Core.PropertyEditors;
|
||||
using Umbraco.Cms.Core.PropertyEditors.ValueConverters;
|
||||
@@ -15,6 +17,9 @@ using Umbraco.Cms.Core.Serialization;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
using Umbraco.Cms.Core.Strings;
|
||||
using Umbraco.Cms.Infrastructure.Serialization;
|
||||
using Umbraco.Cms.Tests.Common.Builders;
|
||||
using Umbraco.Cms.Tests.Common.Builders.Extensions;
|
||||
using Umbraco.Extensions;
|
||||
using static Umbraco.Cms.Core.PropertyEditors.BlockListPropertyEditorBase;
|
||||
|
||||
namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.PropertyEditors;
|
||||
@@ -22,6 +27,9 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.PropertyEditors;
|
||||
[TestFixture]
|
||||
public class BlockListEditorPropertyValueEditorTests
|
||||
{
|
||||
private static readonly Guid _contentTypeKey = Guid.NewGuid();
|
||||
private static readonly Guid _contentKey = Guid.NewGuid();
|
||||
|
||||
[Test]
|
||||
public void Validates_Null_As_Below_Configured_Min()
|
||||
{
|
||||
@@ -84,14 +92,80 @@ public class BlockListEditorPropertyValueEditorTests
|
||||
Assert.IsNull(result);
|
||||
}
|
||||
|
||||
private static JsonObject CreateBlocksJson(int numberOfBlocks)
|
||||
[Test]
|
||||
public void FromEditor_With_Null_Current_Value_Returns_Expected_Json_Value()
|
||||
{
|
||||
var editedValue = CreateBlocksJson(1);
|
||||
var editor = CreateValueEditor();
|
||||
|
||||
var contentPropertyData = new ContentPropertyData(editedValue, null);
|
||||
|
||||
var result = editor.FromEditor(contentPropertyData, null);
|
||||
AssertResultValue(result, 0, "A");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void FromEditor_With_Current_Value_Returns_Expected_Json_Value()
|
||||
{
|
||||
var editedValue = CreateBlocksJson(1, "B");
|
||||
var currentValue = CreateBlocksJson(1);
|
||||
var editor = CreateValueEditor();
|
||||
|
||||
var contentPropertyData = new ContentPropertyData(editedValue, null);
|
||||
|
||||
var result = editor.FromEditor(contentPropertyData, currentValue);
|
||||
AssertResultValue(result, 0, "B");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void FromEditor_With_Block_Item_Editor_That_Uses_Current_Value_With_Edited_Property_Returns_Expected_Json_Value()
|
||||
{
|
||||
var editedValue = CreateBlocksJson(1, "B");
|
||||
var currentValue = CreateBlocksJson(1);
|
||||
var editor = CreateValueEditor(ValueEditorSetup.ConcatenatingTextValueEditor);
|
||||
|
||||
var contentPropertyData = new ContentPropertyData(editedValue, null);
|
||||
|
||||
var result = editor.FromEditor(contentPropertyData, currentValue);
|
||||
AssertResultValue(result, 0, "A, B");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void FromEditor_With_Block_Item_Editor_That_Uses_Current_Value_With_Edited_And_Added_Property_Returns_Expected_Json_Value()
|
||||
{
|
||||
var editedValue = CreateBlocksJson(1, "B", "C");
|
||||
var currentValue = CreateBlocksJson(1);
|
||||
var editor = CreateValueEditor(ValueEditorSetup.ConcatenatingTextValueEditor);
|
||||
|
||||
var contentPropertyData = new ContentPropertyData(editedValue, null);
|
||||
|
||||
var result = editor.FromEditor(contentPropertyData, currentValue);
|
||||
AssertResultValue(result, 0, "A, B");
|
||||
AssertResultValue(result, 1, "C");
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void FromEditor_With_Block_Item_Editor_That_Uses_Current_Value_With_Edited_And_Removed_Property_Returns_Expected_Json_Value()
|
||||
{
|
||||
var editedValue = CreateBlocksJson(1, "B", "C");
|
||||
var currentValue = CreateBlocksJson(1, null);
|
||||
var editor = CreateValueEditor(ValueEditorSetup.ConcatenatingTextValueEditor);
|
||||
|
||||
var contentPropertyData = new ContentPropertyData(editedValue, null);
|
||||
|
||||
var result = editor.FromEditor(contentPropertyData, currentValue);
|
||||
AssertResultValue(result, 0, "B");
|
||||
AssertResultValue(result, 1, "C");
|
||||
}
|
||||
|
||||
private static JsonObject CreateBlocksJson(int numberOfBlocks, string? blockMessagePropertyValue = "A", string? blockMessage2PropertyValue = null)
|
||||
{
|
||||
var layoutItems = new JsonArray();
|
||||
var contentData = new JsonArray();
|
||||
for (int i = 0; i < numberOfBlocks; i++)
|
||||
{
|
||||
layoutItems.Add(CreateLayoutBlockJson());
|
||||
contentData.Add(CreateContentDataBlockJson());
|
||||
contentData.Add(CreateContentDataBlockJson(blockMessagePropertyValue, blockMessage2PropertyValue));
|
||||
}
|
||||
|
||||
return new JsonObject
|
||||
@@ -110,48 +184,113 @@ public class BlockListEditorPropertyValueEditorTests
|
||||
new()
|
||||
{
|
||||
{ "$type", "BlockListLayoutItem" },
|
||||
{ "contentKey", Guid.NewGuid() },
|
||||
{ "contentKey", _contentKey },
|
||||
};
|
||||
|
||||
private static JsonObject CreateContentDataBlockJson() =>
|
||||
new()
|
||||
private static JsonObject CreateContentDataBlockJson(string? blockMessagePropertyValue, string? blockMessage2PropertyValue)
|
||||
{
|
||||
var values = new JsonArray();
|
||||
if (!string.IsNullOrEmpty(blockMessagePropertyValue))
|
||||
{
|
||||
{ "contentTypeKey", Guid.Parse("01935a73-c86b-4521-9dcb-ad7cea402215") },
|
||||
{ "key", Guid.NewGuid() },
|
||||
values.Add(new JsonObject
|
||||
{
|
||||
"values",
|
||||
new JsonArray
|
||||
{
|
||||
new JsonObject
|
||||
{
|
||||
{ "editorAlias", "Umbraco.TextBox" },
|
||||
{ "alias", "message" },
|
||||
{ "value", "Hello" },
|
||||
},
|
||||
}
|
||||
}
|
||||
};
|
||||
{ "editorAlias", "Umbraco.TextBox" },
|
||||
{ "alias", "message" },
|
||||
{ "value", blockMessagePropertyValue },
|
||||
});
|
||||
}
|
||||
|
||||
private static BlockListEditorPropertyValueEditor CreateValueEditor()
|
||||
if (!string.IsNullOrEmpty(blockMessage2PropertyValue))
|
||||
{
|
||||
values.Add(new JsonObject
|
||||
{
|
||||
{ "editorAlias", "Umbraco.TextBox" },
|
||||
{ "alias", "message2" },
|
||||
{ "value", blockMessage2PropertyValue },
|
||||
});
|
||||
}
|
||||
|
||||
return new()
|
||||
{
|
||||
{ "contentTypeKey", _contentTypeKey },
|
||||
{ "key", _contentKey },
|
||||
{ "values", values }
|
||||
};
|
||||
}
|
||||
|
||||
private enum ValueEditorSetup
|
||||
{
|
||||
TextOnlyValueEditor,
|
||||
ConcatenatingTextValueEditor,
|
||||
}
|
||||
|
||||
private static BlockListEditorPropertyValueEditor CreateValueEditor(ValueEditorSetup valueEditorSetup = ValueEditorSetup.TextOnlyValueEditor)
|
||||
{
|
||||
var localizedTextServiceMock = new Mock<ILocalizedTextService>();
|
||||
localizedTextServiceMock.Setup(x => x.Localize(
|
||||
localizedTextServiceMock
|
||||
.Setup(x => x.Localize(
|
||||
It.IsAny<string>(),
|
||||
It.IsAny<string>(),
|
||||
It.IsAny<CultureInfo>(),
|
||||
It.IsAny<IDictionary<string, string>>()))
|
||||
It.IsAny<IDictionary<string, string>>()))
|
||||
.Returns((string key, string alias, CultureInfo culture, IDictionary<string, string> args) => $"{key}_{alias}");
|
||||
|
||||
var jsonSerializer = new SystemTextJsonSerializer();
|
||||
var languageService = Mock.Of<ILanguageService>();
|
||||
|
||||
var dataValueEditorFactoryMock = new Mock<IDataValueEditorFactory>();
|
||||
|
||||
DataEditor textBoxEditor;
|
||||
switch (valueEditorSetup)
|
||||
{
|
||||
case ValueEditorSetup.ConcatenatingTextValueEditor:
|
||||
dataValueEditorFactoryMock
|
||||
.Setup(x => x.Create<ConcatenatingTextValueEditor>(It.IsAny<object[]>()))
|
||||
.Returns(new ConcatenatingTextValueEditor(
|
||||
Mock.Of<IShortStringHelper>(),
|
||||
new SystemTextJsonSerializer()));
|
||||
textBoxEditor = new ConcatenatingTextboxPropertyEditor(
|
||||
dataValueEditorFactoryMock.Object);
|
||||
|
||||
break;
|
||||
default:
|
||||
dataValueEditorFactoryMock
|
||||
.Setup(x => x.Create<TextOnlyValueEditor>(It.IsAny<object[]>()))
|
||||
.Returns(new TextOnlyValueEditor(
|
||||
new DataEditorAttribute("a"),
|
||||
Mock.Of<ILocalizedTextService>(),
|
||||
Mock.Of<IShortStringHelper>(),
|
||||
new SystemTextJsonSerializer(),
|
||||
Mock.Of<IIOHelper>()));
|
||||
textBoxEditor = new TextboxPropertyEditor(
|
||||
dataValueEditorFactoryMock.Object,
|
||||
Mock.Of<IIOHelper>());
|
||||
break;
|
||||
}
|
||||
|
||||
var propertyEditors = new PropertyEditorCollection(new DataEditorCollection(() => textBoxEditor.Yield()));
|
||||
|
||||
var elementType = new ContentTypeBuilder()
|
||||
.WithKey(_contentTypeKey)
|
||||
.AddPropertyType()
|
||||
.WithAlias("message")
|
||||
.Done()
|
||||
.AddPropertyType()
|
||||
.WithAlias("message2")
|
||||
.Done()
|
||||
.Build();
|
||||
var elementTypeCacheMock = new Mock<IBlockEditorElementTypeCache>();
|
||||
elementTypeCacheMock
|
||||
.Setup(x => x.GetMany(It.Is<IEnumerable<Guid>>(y => y.First() == _contentTypeKey)))
|
||||
.Returns([elementType]);
|
||||
|
||||
return new BlockListEditorPropertyValueEditor(
|
||||
new DataEditorAttribute("alias"),
|
||||
new BlockListEditorDataConverter(jsonSerializer),
|
||||
new(new DataEditorCollection(() => [])),
|
||||
propertyEditors,
|
||||
new DataValueReferenceFactoryCollection(Enumerable.Empty<IDataValueReferenceFactory>, Mock.Of<ILogger<DataValueReferenceFactoryCollection>>()),
|
||||
Mock.Of<IDataTypeConfigurationCache>(),
|
||||
Mock.Of<IBlockEditorElementTypeCache>(),
|
||||
elementTypeCacheMock.Object,
|
||||
localizedTextServiceMock.Object,
|
||||
new NullLogger<BlockListEditorPropertyValueEditor>(),
|
||||
Mock.Of<IShortStringHelper>(),
|
||||
@@ -171,4 +310,62 @@ public class BlockListEditorPropertyValueEditorTests
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// An illustrative property editor that uses the edited and current value when returning a result from the FromEditor calls.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This is used to simulate a real-world editor that needs to use this value from within the block editor and verify
|
||||
/// that it receives and processes the value.
|
||||
/// </remarks>
|
||||
[DataEditor(
|
||||
global::Umbraco.Cms.Core.Constants.PropertyEditors.Aliases.TextBox)]
|
||||
private class ConcatenatingTextboxPropertyEditor : DataEditor
|
||||
{
|
||||
public ConcatenatingTextboxPropertyEditor(IDataValueEditorFactory dataValueEditorFactory)
|
||||
: base(dataValueEditorFactory)
|
||||
{
|
||||
}
|
||||
|
||||
protected override IDataValueEditor CreateValueEditor() =>
|
||||
DataValueEditorFactory.Create<ConcatenatingTextValueEditor>(Attribute!);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// An illustrative value editor that uses the edited and current value when returning a result from the FromEditor calls.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// See notes on <see cref="ConcatenatingTextboxPropertyEditor"/>.
|
||||
/// </remarks>
|
||||
private class ConcatenatingTextValueEditor : DataValueEditor
|
||||
{
|
||||
public ConcatenatingTextValueEditor(IShortStringHelper shortStringHelper, IJsonSerializer? jsonSerializer)
|
||||
: base(shortStringHelper, jsonSerializer)
|
||||
{
|
||||
}
|
||||
|
||||
public override object FromEditor(ContentPropertyData propertyData, object? currentValue)
|
||||
{
|
||||
var values = new List<string>();
|
||||
if (currentValue is not null)
|
||||
{
|
||||
values.Add(currentValue.ToString());
|
||||
}
|
||||
|
||||
var editedValue = propertyData.Value;
|
||||
if (editedValue is not null)
|
||||
{
|
||||
values.Add(editedValue.ToString());
|
||||
}
|
||||
|
||||
return string.Join(", ", values);
|
||||
}
|
||||
}
|
||||
|
||||
private static void AssertResultValue(object? result, int valueIndex, string expectedValue)
|
||||
{
|
||||
Assert.IsNotNull(result);
|
||||
var resultAsJson = (JsonObject)JsonNode.Parse(result.ToString());
|
||||
Assert.AreEqual(expectedValue, resultAsJson["contentData"][0]["values"][valueIndex]["value"].ToString());
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user