Skip to content

Commit 14063a0

Browse files
PeterKvaytAndyButlandMigaroez
authored
Add support for file upload property editor within the block list and grid (#18976)
* Fix for #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 <[email protected]> * 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 <[email protected]> Co-authored-by: Sven Geusens <[email protected]>
1 parent 2cb114f commit 14063a0

19 files changed

+1394
-267
lines changed

src/Umbraco.Infrastructure/DependencyInjection/UmbracoBuilder.CoreServices.cs

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
using Umbraco.Cms.Infrastructure.Migrations.Install;
5353
using Umbraco.Cms.Infrastructure.Persistence;
5454
using Umbraco.Cms.Infrastructure.Persistence.Mappers;
55+
using Umbraco.Cms.Infrastructure.PropertyEditors.NotificationHandlers;
5556
using Umbraco.Cms.Infrastructure.Routing;
5657
using Umbraco.Cms.Infrastructure.Runtime;
5758
using Umbraco.Cms.Infrastructure.Runtime.RuntimeModeValidators;
@@ -355,11 +356,11 @@ public static IUmbracoBuilder AddCoreNotifications(this IUmbracoBuilder builder)
355356
.AddNotificationHandler<ContentSavingNotification, RichTextPropertyNotificationHandler>()
356357
.AddNotificationHandler<ContentCopyingNotification, RichTextPropertyNotificationHandler>()
357358
.AddNotificationHandler<ContentScaffoldedNotification, RichTextPropertyNotificationHandler>()
358-
.AddNotificationHandler<ContentCopiedNotification, FileUploadPropertyEditor>()
359-
.AddNotificationHandler<ContentDeletedNotification, FileUploadPropertyEditor>()
360-
.AddNotificationHandler<MediaDeletedNotification, FileUploadPropertyEditor>()
361-
.AddNotificationHandler<MediaSavingNotification, FileUploadPropertyEditor>()
362-
.AddNotificationHandler<MemberDeletedNotification, FileUploadPropertyEditor>()
359+
.AddNotificationHandler<ContentCopiedNotification, FileUploadContentCopiedNotificationHandler>()
360+
.AddNotificationHandler<ContentDeletedNotification, FileUploadContentDeletedNotificationHandler>()
361+
.AddNotificationHandler<MediaDeletedNotification, FileUploadMediaDeletedNotificationHandler>()
362+
.AddNotificationHandler<MediaSavingNotification, FileUploadMediaSavingNotificationHandler>()
363+
.AddNotificationHandler<MemberDeletedNotification, FileUploadMemberDeletedNotificationHandler>()
363364
.AddNotificationHandler<ContentCopiedNotification, ImageCropperPropertyEditor>()
364365
.AddNotificationHandler<ContentDeletedNotification, ImageCropperPropertyEditor>()
365366
.AddNotificationHandler<MediaDeletedNotification, ImageCropperPropertyEditor>()
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
using Umbraco.Cms.Core;
2+
using Umbraco.Cms.Core.Cache.PropertyEditors;
3+
using Umbraco.Cms.Core.Models;
4+
using Umbraco.Cms.Core.Models.Blocks;
5+
6+
namespace Umbraco.Cms.Infrastructure.Extensions;
7+
8+
/// <summary>
9+
/// Defines extensions on <see cref="RichTextEditorValue"/>.
10+
/// </summary>
11+
internal static class RichTextEditorValueExtensions
12+
{
13+
/// <summary>
14+
/// Ensures that the property type property is populated on all blocks.
15+
/// </summary>
16+
/// <param name="richTextEditorValue">The <see cref="RichTextEditorValue"/> providing the blocks.</param>
17+
/// <param name="elementTypeCache">Cache for element types.</param>
18+
public static void EnsurePropertyTypePopulatedOnBlocks(this RichTextEditorValue richTextEditorValue, IBlockEditorElementTypeCache elementTypeCache)
19+
{
20+
Guid[] elementTypeKeys = (richTextEditorValue.Blocks?.ContentData ?? [])
21+
.Select(x => x.ContentTypeKey)
22+
.Union((richTextEditorValue.Blocks?.SettingsData ?? [])
23+
.Select(x => x.ContentTypeKey))
24+
.Distinct()
25+
.ToArray();
26+
27+
IEnumerable<IContentType> elementTypes = elementTypeCache.GetMany(elementTypeKeys);
28+
29+
foreach (BlockItemData dataItem in (richTextEditorValue.Blocks?.ContentData ?? [])
30+
.Union(richTextEditorValue.Blocks?.SettingsData ?? []))
31+
{
32+
foreach (BlockPropertyValue item in dataItem.Values)
33+
{
34+
item.PropertyType = elementTypes.FirstOrDefault(x => x.Key == dataItem.ContentTypeKey)?.PropertyTypes.FirstOrDefault(pt => pt.Alias == item.Alias);
35+
}
36+
}
37+
}
38+
}

src/Umbraco.Infrastructure/PropertyEditors/BlockEditorPropertyValueEditor.cs

Lines changed: 39 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,8 @@
11
// Copyright (c) Umbraco.
22
// See LICENSE for more details.
33

4-
using Microsoft.Extensions.DependencyInjection;
5-
using Microsoft.Extensions.Logging;
4+
using System.Diagnostics.CodeAnalysis;
65
using Umbraco.Cms.Core.Cache;
7-
using Umbraco.Cms.Core.DependencyInjection;
86
using Umbraco.Cms.Core.IO;
97
using Umbraco.Cms.Core.Models;
108
using Umbraco.Cms.Core.Models.Blocks;
@@ -16,10 +14,16 @@
1614

1715
namespace Umbraco.Cms.Core.PropertyEditors;
1816

17+
/// <summary>
18+
/// Provides an abstract base class for property value editors based on block editors.
19+
/// </summary>
1920
public abstract class BlockEditorPropertyValueEditor<TValue, TLayout> : BlockValuePropertyValueEditorBase<TValue, TLayout>
2021
where TValue : BlockValue<TLayout>, new()
2122
where TLayout : class, IBlockLayoutItem, new()
2223
{
24+
/// <summary>
25+
/// Initializes a new instance of the <see cref="BlockEditorPropertyValueEditor{TValue, TLayout}"/> class.
26+
/// </summary>
2327
protected BlockEditorPropertyValueEditor(
2428
PropertyEditorCollection propertyEditors,
2529
DataValueReferenceFactoryCollection dataValueReferenceFactories,
@@ -62,13 +66,7 @@ public override IEnumerable<ITag> GetTags(object? value, object? dataTypeConfigu
6266
return BlockEditorValues.DeserializeAndClean(rawJson)?.BlockValue;
6367
}
6468

65-
/// <summary>
66-
/// Ensure that sub-editor values are translated through their ToEditor methods
67-
/// </summary>
68-
/// <param name="property"></param>
69-
/// <param name="culture"></param>
70-
/// <param name="segment"></param>
71-
/// <returns></returns>
69+
/// <inheritdoc />
7270
public override object ToEditor(IProperty property, string? culture = null, string? segment = null)
7371
{
7472
var val = property.GetValue(culture, segment);
@@ -95,38 +93,48 @@ public override object ToEditor(IProperty property, string? culture = null, stri
9593
return blockEditorData.BlockValue;
9694
}
9795

98-
/// <summary>
99-
/// Ensure that sub-editor values are translated through their FromEditor methods
100-
/// </summary>
101-
/// <param name="editorValue"></param>
102-
/// <param name="currentValue"></param>
103-
/// <returns></returns>
96+
/// <inheritdoc />
10497
public override object? FromEditor(ContentPropertyData editorValue, object? currentValue)
10598
{
106-
if (editorValue.Value == null || string.IsNullOrWhiteSpace(editorValue.Value.ToString()))
99+
// Note: we can't early return here if editorValue is null or empty, because these is the following case:
100+
// - current value not null (which means doc has at least one element in block list)
101+
// - editor value (new value) is null (which means doc has no elements in block list)
102+
// If we check editor value for null value and return before MapBlockValueFromEditor, then we will not trigger updates for properties.
103+
// For most of the properties this is fine, but for properties which contain other state it might be critical (e.g. file upload field).
104+
// So, we must run MapBlockValueFromEditor even if editorValue is null or string.IsNullOrWhiteSpace(editorValue.Value.ToString()) is true.
105+
106+
BlockEditorData<TValue, TLayout>? currentBlockEditorData = GetBlockEditorData(currentValue);
107+
BlockEditorData<TValue, TLayout>? blockEditorData = GetBlockEditorData(editorValue.Value);
108+
109+
// We can skip MapBlockValueFromEditor if both editorValue and currentValue values are empty.
110+
if (IsBlockEditorDataEmpty(currentBlockEditorData) && IsBlockEditorDataEmpty(blockEditorData))
107111
{
108112
return null;
109113
}
110114

111-
BlockEditorData<TValue, TLayout>? blockEditorData;
112-
try
115+
MapBlockValueFromEditor(blockEditorData?.BlockValue, currentBlockEditorData?.BlockValue, editorValue.ContentKey);
116+
117+
if (IsBlockEditorDataEmpty(blockEditorData))
113118
{
114-
blockEditorData = BlockEditorValues.DeserializeAndClean(editorValue.Value);
119+
return null;
115120
}
116-
catch
121+
122+
return JsonSerializer.Serialize(blockEditorData.BlockValue);
123+
}
124+
125+
private BlockEditorData<TValue, TLayout>? GetBlockEditorData(object? value)
126+
{
127+
try
117128
{
118-
// if this occurs it means the data is invalid, shouldn't happen but has happened if we change the data format.
119-
return string.Empty;
129+
return BlockEditorValues.DeserializeAndClean(value);
120130
}
121-
122-
if (blockEditorData == null || blockEditorData.BlockValue.ContentData.Count == 0)
131+
catch
123132
{
124-
return string.Empty;
133+
// If this occurs it means the data is invalid. It shouldn't happen could if we change the data format.
134+
return null;
125135
}
126-
127-
MapBlockValueFromEditor(blockEditorData.BlockValue);
128-
129-
// return json
130-
return JsonSerializer.Serialize(blockEditorData.BlockValue);
131136
}
137+
138+
private static bool IsBlockEditorDataEmpty([NotNullWhen(false)] BlockEditorData<TValue, TLayout>? editorData)
139+
=> editorData is null || editorData.BlockValue.ContentData.Count == 0;
132140
}

src/Umbraco.Infrastructure/PropertyEditors/BlockEditorValidatorBase.cs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
using Umbraco.Cms.Core.Cache.PropertyEditors;
1+
using Umbraco.Cms.Core.Cache.PropertyEditors;
22
using Umbraco.Cms.Core.Models;
33
using Umbraco.Cms.Core.Models.Blocks;
44
using Umbraco.Cms.Core.Models.Validation;
@@ -88,7 +88,13 @@ private IEnumerable<ElementTypeValidationModel> GetBlockEditorDataValidation(Blo
8888

8989
foreach (var group in itemDataGroups)
9090
{
91-
var allElementTypes = _elementTypeCache.GetMany(group.Items.Select(x => x.ContentTypeKey).ToArray()).ToDictionary(x => x.Key);
91+
Guid[] elementTypeKeys = group.Items.Select(x => x.ContentTypeKey).ToArray();
92+
if (elementTypeKeys.Length == 0)
93+
{
94+
continue;
95+
}
96+
97+
var allElementTypes = _elementTypeCache.GetMany(elementTypeKeys).ToDictionary(x => x.Key);
9298

9399
for (var i = 0; i < group.Items.Length; i++)
94100
{

src/Umbraco.Infrastructure/PropertyEditors/BlockValuePropertyValueEditorBase.cs

Lines changed: 108 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -129,10 +129,115 @@ protected IEnumerable<ITag> GetBlockValueTags(TValue blockValue, int? languageId
129129
return result;
130130
}
131131

132-
protected void MapBlockValueFromEditor(TValue blockValue)
132+
[Obsolete("This method is no longer used within Umbraco. Please use the overload taking all parameters. Scheduled for removal in Umbraco 17.")]
133+
protected void MapBlockValueFromEditor(TValue blockValue) => MapBlockValueFromEditor(blockValue, null, Guid.Empty);
134+
135+
protected void MapBlockValueFromEditor(TValue? editedBlockValue, TValue? currentBlockValue, Guid contentKey)
136+
{
137+
MapBlockItemDataFromEditor(
138+
editedBlockValue?.ContentData ?? [],
139+
currentBlockValue?.ContentData ?? [],
140+
contentKey);
141+
142+
MapBlockItemDataFromEditor(
143+
editedBlockValue?.SettingsData ?? [],
144+
currentBlockValue?.SettingsData ?? [],
145+
contentKey);
146+
}
147+
148+
private void MapBlockItemDataFromEditor(List<BlockItemData> editedItems, List<BlockItemData> currentItems, Guid contentKey)
149+
{
150+
// Create mapping between edited and current block items.
151+
IEnumerable<BlockStateMapping<BlockItemData>> itemsMapping = GetBlockStatesMapping(editedItems, currentItems, (mapping, current) => mapping.Edited?.Key == current.Key);
152+
153+
foreach (BlockStateMapping<BlockItemData> itemMapping in itemsMapping)
154+
{
155+
// Create mapping between edited and current block item values.
156+
IEnumerable<BlockStateMapping<BlockPropertyValue>> valuesMapping = GetBlockStatesMapping(itemMapping.Edited?.Values, itemMapping.Current?.Values, (mapping, current) => mapping.Edited?.Alias == current.Alias);
157+
158+
foreach (BlockStateMapping<BlockPropertyValue> valueMapping in valuesMapping)
159+
{
160+
BlockPropertyValue? editedValue = valueMapping.Edited;
161+
BlockPropertyValue? currentValue = valueMapping.Current;
162+
163+
IPropertyType propertyType = editedValue?.PropertyType
164+
?? currentValue?.PropertyType
165+
?? 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));
166+
167+
// Lookup the property editor.
168+
IDataEditor? propertyEditor = _propertyEditors[propertyType.PropertyEditorAlias];
169+
if (propertyEditor is null)
170+
{
171+
continue;
172+
}
173+
174+
// Fetch the property types prevalue.
175+
var configuration = _dataTypeConfigurationCache.GetConfiguration(propertyType.DataTypeKey);
176+
177+
// Create a real content property data object.
178+
var propertyData = new ContentPropertyData(editedValue?.Value, configuration)
179+
{
180+
ContentKey = contentKey,
181+
PropertyTypeKey = propertyType.Key,
182+
};
183+
184+
// Get the property editor to do it's conversion.
185+
IDataValueEditor valueEditor = propertyEditor.GetValueEditor();
186+
var newValue = valueEditor.FromEditor(propertyData, currentValue?.Value);
187+
188+
// Update the raw value since this is what will get serialized out.
189+
if (editedValue != null)
190+
{
191+
editedValue.Value = newValue;
192+
}
193+
}
194+
}
195+
}
196+
197+
private sealed class BlockStateMapping<T>
133198
{
134-
MapBlockItemDataFromEditor(blockValue.ContentData);
135-
MapBlockItemDataFromEditor(blockValue.SettingsData);
199+
public T? Edited { get; set; }
200+
201+
public T? Current { get; set; }
202+
}
203+
204+
private static IEnumerable<BlockStateMapping<T>> GetBlockStatesMapping<T>(IList<T>? editedItems, IList<T>? currentItems, Func<BlockStateMapping<T>, T, bool> condition)
205+
{
206+
// filling with edited items first
207+
List<BlockStateMapping<T>> mapping = editedItems?
208+
.Select(editedItem => new BlockStateMapping<T>
209+
{
210+
Current = default,
211+
Edited = editedItem,
212+
})
213+
.ToList()
214+
?? [];
215+
216+
if (currentItems is null)
217+
{
218+
return mapping;
219+
}
220+
221+
// then adding current items
222+
foreach (T currentItem in currentItems)
223+
{
224+
BlockStateMapping<T>? mappingItem = mapping.FirstOrDefault(x => condition(x, currentItem));
225+
226+
if (mappingItem == null) // if there is no edited item, then adding just current
227+
{
228+
mapping.Add(new BlockStateMapping<T>
229+
{
230+
Current = currentItem,
231+
Edited = default,
232+
});
233+
}
234+
else
235+
{
236+
mappingItem.Current = currentItem;
237+
}
238+
}
239+
240+
return mapping;
136241
}
137242

138243
protected void MapBlockValueToEditor(IProperty property, TValue blockValue, string? culture, string? segment)
@@ -197,40 +302,6 @@ private void MapBlockItemDataToEditor(IProperty property, List<BlockItemData> it
197302
}
198303
}
199304

200-
private void MapBlockItemDataFromEditor(List<BlockItemData> items)
201-
{
202-
foreach (BlockItemData item in items)
203-
{
204-
foreach (BlockPropertyValue blockPropertyValue in item.Values)
205-
{
206-
IPropertyType? propertyType = blockPropertyValue.PropertyType;
207-
if (propertyType is null)
208-
{
209-
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));
210-
}
211-
212-
// Lookup the property editor
213-
IDataEditor? propertyEditor = _propertyEditors[propertyType.PropertyEditorAlias];
214-
if (propertyEditor is null)
215-
{
216-
continue;
217-
}
218-
219-
// Fetch the property types prevalue
220-
var configuration = _dataTypeConfigurationCache.GetConfiguration(propertyType.DataTypeKey);
221-
222-
// Create a fake content property data object
223-
var propertyData = new ContentPropertyData(blockPropertyValue.Value, configuration);
224-
225-
// Get the property editor to do it's conversion
226-
var newValue = propertyEditor.GetValueEditor().FromEditor(propertyData, blockPropertyValue.Value);
227-
228-
// update the raw value since this is what will get serialized out
229-
blockPropertyValue.Value = newValue;
230-
}
231-
}
232-
}
233-
234305
/// <summary>
235306
/// Updates the invariant data in the source with the invariant data in the value if allowed
236307
/// </summary>

0 commit comments

Comments
 (0)