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