Server side validation for property editors (colour picker) (#18557)

* Added server-side validation for colour picker.

* Minor refactor.

* Add danish translation

---------

Co-authored-by: mole <nikolajlauridsen@protonmail.ch>
This commit is contained in:
Andy Butland
2025-03-04 13:13:37 +01:00
committed by GitHub
parent 7e4dda7bab
commit 6d5b6a4553
6 changed files with 149 additions and 3 deletions

View File

@@ -122,6 +122,7 @@ Mange hilsner fra Umbraco robotten
<key alias="unexpectedRange">Værdien %0% forventes ikke at indeholde et spænd.</key>
<key alias="invalidRange">Værdien %0% forventes at have en værdi der er større end fra værdien.</key>
<key alias="notOneOfOptions">"Værdien '%0%' er ikke en af de tilgængelige valgmuligheder.</key>
<key alias="invalidColor">"Den valgte farve '%0%' er ikke en af de tilgængelige valgmuligheder.</key>
</area>
<area alias="recycleBin">
<key alias="contentTrashed">Slettet indhold med Id: {0} Relateret til original "parent" med id: {1}</key>

View File

@@ -395,7 +395,8 @@
<key alias="invalidMediaType">The chosen media type is invalid.</key>
<key alias="multipleMediaNotAllowed">Multiple selected media is not allowed.</key>
<key alias="invalidStartNode">The selected media is from the wrong folder.</key>
<key alias="notOneOfOptions">"The value '%0%' is not one of the available options.</key>
<key alias="notOneOfOptions">The value '%0%' is not one of the available options.</key>
<key alias="invalidColor">"The selected colour '%0%' is not one of the available options.</key>
</area>
<area alias="healthcheck">
<!-- The following keys get these tokens passed in:

View File

@@ -396,7 +396,8 @@
<key alias="invalidMediaType">The chosen media type is invalid.</key>
<key alias="multipleMediaNotAllowed">Multiple selected media is not allowed.</key>
<key alias="invalidStartNode">The selected media is from the wrong folder.</key>
<key alias="notOneOfOptions">"The value '%0%' is not one of the available options.</key>
<key alias="notOneOfOptions">The value '%0%' is not one of the available options.</key>
<key alias="invalidColor">The selected color '%0%' is not one of the available options.</key>
</area>
<area alias="healthcheck">
<!-- The following keys get these tokens passed in:

View File

@@ -1,11 +1,21 @@
// Copyright (c) Umbraco.
// See LICENSE for more details.
using System.ComponentModel.DataAnnotations;
using System.Text.Json.Nodes;
using Umbraco.Cms.Core.IO;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.Validation;
using Umbraco.Cms.Core.Serialization;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Strings;
using Umbraco.Extensions;
namespace Umbraco.Cms.Core.PropertyEditors;
/// <summary>
/// Represents a color picker property editor.
/// </summary>
[DataEditor(
Constants.PropertyEditors.Aliases.ColorPicker,
ValueEditorIsReusable = true)]
@@ -14,6 +24,9 @@ public class ColorPickerPropertyEditor : DataEditor
private readonly IIOHelper _ioHelper;
private readonly IConfigurationEditorJsonSerializer _configurationEditorJsonSerializer;
/// <summary>
/// Initializes a new instance of the <see cref="ColorPickerPropertyEditor"/> class.
/// </summary>
public ColorPickerPropertyEditor(IDataValueEditorFactory dataValueEditorFactory, IIOHelper ioHelper, IConfigurationEditorJsonSerializer configurationEditorJsonSerializer)
: base(dataValueEditorFactory)
{
@@ -22,10 +35,78 @@ public class ColorPickerPropertyEditor : DataEditor
SupportsReadOnly = true;
}
/// <inheritdoc/>
public override IPropertyIndexValueFactory PropertyIndexValueFactory { get; } = new NoopPropertyIndexValueFactory();
/// <inheritdoc />
protected override IDataValueEditor CreateValueEditor()
=> DataValueEditorFactory.Create<ColorPickerPropertyValueEditor>(Attribute!);
/// <inheritdoc />
protected override IConfigurationEditor CreateConfigurationEditor() =>
new ColorPickerConfigurationEditor(_ioHelper, _configurationEditorJsonSerializer);
/// <summary>
/// Defines the value editor for the color picker property editor.
/// </summary>
internal class ColorPickerPropertyValueEditor : DataValueEditor
{
/// <summary>
/// Initializes a new instance of the <see cref="ColorPickerPropertyValueEditor"/> class.
/// </summary>
public ColorPickerPropertyValueEditor(
IShortStringHelper shortStringHelper,
IJsonSerializer jsonSerializer,
IIOHelper ioHelper,
DataEditorAttribute attribute,
ILocalizedTextService localizedTextService)
: base(shortStringHelper, jsonSerializer, ioHelper, attribute)
{
Validators.AddRange(new ConfiguredColorValidator(localizedTextService));
}
/// <summary>
/// Validates the color selection for the color picker property editor.
/// </summary>
internal class ConfiguredColorValidator : IValueValidator
{
private readonly ILocalizedTextService _localizedTextService;
/// <summary>
/// Initializes a new instance of the <see cref="ConfiguredColorValidator"/> class.
/// </summary>
public ConfiguredColorValidator(ILocalizedTextService localizedTextService) => _localizedTextService = localizedTextService;
/// <inheritdoc/>
public IEnumerable<ValidationResult> Validate(object? value, string? valueType, object? dataTypeConfiguration, PropertyValidationContext validationContext)
{
if (value is null || value is not JsonObject valueAsJsonObject)
{
yield break;
}
if (dataTypeConfiguration is not ColorPickerConfiguration colorPickerConfiguration)
{
yield break;
}
string? selectedColor = valueAsJsonObject["value"]?.GetValue<string>();
if (selectedColor.IsNullOrWhiteSpace())
{
yield break;
}
IEnumerable<string> validColors = colorPickerConfiguration.Items.Select(x => EnsureConsistentColorRepresentation(x.Value));
if (validColors.Contains(EnsureConsistentColorRepresentation(selectedColor)) is false)
{
yield return new ValidationResult(
_localizedTextService.Localize("validation", "invalidColor", [selectedColor]),
["value"]);
}
}
private static string EnsureConsistentColorRepresentation(string color)
=> (color.StartsWith('#') ? color : $"#{color}").ToLowerInvariant();
}
}
}

View File

@@ -10,7 +10,6 @@ using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.Editors;
using Umbraco.Cms.Core.Models.Validation;
using Umbraco.Cms.Core.PropertyEditors.Validation;
using Umbraco.Cms.Core.PropertyEditors.Validators;
using Umbraco.Cms.Core.Serialization;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Strings;

View File

@@ -0,0 +1,63 @@
using System.Globalization;
using Humanizer;
using System.Text.Json.Nodes;
using Moq;
using NUnit.Framework;
using Umbraco.Cms.Core.IO;
using Umbraco.Cms.Core.Models.Validation;
using Umbraco.Cms.Core.PropertyEditors;
using Umbraco.Cms.Core.Serialization;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Strings;
namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.PropertyEditors;
[TestFixture]
public class ColorPickerValueEditorTests
{
[TestCase("#ffffff", true)]
[TestCase("#f0f0f0", false)]
public void Validates_Is_Configured_Color(string color, bool expectedSuccess)
{
var value = JsonNode.Parse($"{{\"label\": \"\", \"value\": \"{color}\"}}");
var editor = CreateValueEditor();
var result = editor.Validate(value, false, null, PropertyValidationContext.Empty());
if (expectedSuccess)
{
Assert.IsEmpty(result);
}
else
{
Assert.AreEqual(1, result.Count());
var validationResult = result.First();
Assert.AreEqual(validationResult.ErrorMessage, "validation_invalidColor");
}
}
private static ColorPickerPropertyEditor.ColorPickerPropertyValueEditor CreateValueEditor()
{
var localizedTextServiceMock = new Mock<ILocalizedTextService>();
localizedTextServiceMock.Setup(x => x.Localize(
It.IsAny<string>(),
It.IsAny<string>(),
It.IsAny<CultureInfo>(),
It.IsAny<IDictionary<string, string>>()))
.Returns((string key, string alias, CultureInfo culture, IDictionary<string, string> args) => $"{key}_{alias}");
return new ColorPickerPropertyEditor.ColorPickerPropertyValueEditor(
Mock.Of<IShortStringHelper>(),
Mock.Of<IJsonSerializer>(),
Mock.Of<IIOHelper>(),
new DataEditorAttribute("alias"),
localizedTextServiceMock.Object)
{
ConfigurationObject = new ColorPickerConfiguration
{
Items = [
new ColorPickerConfiguration.ColorPickerItem { Value = "ffffff", Label = "White" },
new ColorPickerConfiguration.ColorPickerItem { Value = "000000", Label = "Black" }
]
}
};
}
}