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:
Peter
2025-06-30 14:21:10 +03:00
committed by GitHub
parent 2cb114ffaf
commit 14063a0b89
19 changed files with 1397 additions and 270 deletions

View File

@@ -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;

View File

@@ -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());
}
}