Server side validation for property editors (integer, decimal and slider) (#18428)
* Server side validation for integer property value editor. * Apply suggestions from code review Co-authored-by: Mole <nikolajlauridsen@protonmail.ch> * Applied further changes from code review. * Localized validation messages. * Added server-side validation for decimal property editor. * Added server-side validation for slider property editor. * Moved repeated step validation into a helper method. * Add danish translations --------- Co-authored-by: Mole <nikolajlauridsen@protonmail.ch>
This commit is contained in:
@@ -116,6 +116,11 @@ Mange hilsner fra Umbraco robotten
|
||||
<key alias="invalidMediaType">Den valgte medie type er ugyldig.</key>
|
||||
<key alias="multipleMediaNotAllowed">Det er kun tilladt at vælge ét medie.</key>
|
||||
<key alias="invalidStartNode">Valgt medie kommer fra en ugyldig mappe.</key>
|
||||
<key alias="outOfRangeMinimum">Værdien %0% er mindre end det tilladte minimum af %1%.</key>
|
||||
<key alias="outOfRangeMaximum">Værdien %0% er større end det tilladte maksimum af %1%.</key>
|
||||
<key alias="invalidStep">Værdien %0% passer ikke med den konfigureret trin værdi af %1% og mindste værdi af %2%.</key>
|
||||
<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>
|
||||
</area>
|
||||
<area alias="recycleBin">
|
||||
<key alias="contentTrashed">Slettet indhold med Id: {0} Relateret til original "parent" med id: {1}</key>
|
||||
|
||||
@@ -387,6 +387,11 @@
|
||||
<key alias="duplicateUserGroupName">User group name '%0%' is already taken</key>
|
||||
<key alias="duplicateMemberGroupName">Member group name '%0%' is already taken</key>
|
||||
<key alias="duplicateUsername">Username '%0%' is already taken</key>
|
||||
<key alias="outOfRangeMinimum">The value %0% is less than the allowed minimum value of %1%</key>
|
||||
<key alias="outOfRangeMaximum">The value %0% is greater than the allowed maximum value of %1%</key>
|
||||
<key alias="invalidStep">The value %0% does not correspond with the configured step value of %1% and minimum value of %2%</key>
|
||||
<key alias="unexpectedRange">The value %0% is not expected to contain a range</key>
|
||||
<key alias="invalidRange">The value %0% is not expected to have a to value less than the from value</key>
|
||||
<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>
|
||||
|
||||
@@ -385,6 +385,11 @@
|
||||
<key alias="duplicateUserGroupName">User group name '%0%' is already taken</key>
|
||||
<key alias="duplicateMemberGroupName">Member group name '%0%' is already taken</key>
|
||||
<key alias="duplicateUsername">Username '%0%' is already taken</key>
|
||||
<key alias="outOfRangeMinimum">The value %0% is less than the allowed minimum value of %1%</key>
|
||||
<key alias="outOfRangeMaximum">The value %0% is greater than the allowed maximum value of %1%</key>
|
||||
<key alias="invalidStep">The value %0% does not correspond with the configured step value of %1% and minimum value of %2%</key>
|
||||
<key alias="unexpectedRange">The value %0% is not expected to contain a range</key>
|
||||
<key alias="invalidRange">The value %0% is not expected to have a to value less than the from value</key>
|
||||
<key alias="entriesShort"><![CDATA[Minimum %0% entries, requires <strong>%1%</strong> more.]]></key>
|
||||
<key alias="entriesExceed"><![CDATA[Maximum %0% entries, <strong>%1%</strong> too many.]]></key>
|
||||
<key alias="entriesAreasMismatch">The content amount requirements are not met for one or more areas.</key>
|
||||
|
||||
@@ -1,15 +1,20 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Globalization;
|
||||
using Umbraco.Cms.Core.IO;
|
||||
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;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Core.PropertyEditors;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a decimal property and parameter editor.
|
||||
/// Represents a decimal property editor.
|
||||
/// </summary>
|
||||
[DataEditor(
|
||||
Constants.PropertyEditors.Aliases.Decimal,
|
||||
@@ -32,19 +37,29 @@ public class DecimalPropertyEditor : DataEditor
|
||||
/// <inheritdoc />
|
||||
protected override IConfigurationEditor CreateConfigurationEditor() => new DecimalConfigurationEditor();
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Defines the value editor for the decimal property editor.
|
||||
/// </summary>
|
||||
internal class DecimalPropertyValueEditor : DataValueEditor
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DecimalPropertyValueEditor"/> class.
|
||||
/// </summary>
|
||||
public DecimalPropertyValueEditor(
|
||||
IShortStringHelper shortStringHelper,
|
||||
IJsonSerializer jsonSerializer,
|
||||
IIOHelper ioHelper,
|
||||
DataEditorAttribute attribute)
|
||||
: base(shortStringHelper, jsonSerializer, ioHelper, attribute) =>
|
||||
Validators.Add(new DecimalValidator());
|
||||
DataEditorAttribute attribute,
|
||||
ILocalizedTextService localizedTextService)
|
||||
: base(shortStringHelper, jsonSerializer, ioHelper, attribute)
|
||||
=> Validators.AddRange([new DecimalValidator(), new MinMaxValidator(localizedTextService), new StepValidator(localizedTextService)]);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override object? ToEditor(IProperty property, string? culture = null, string? segment = null)
|
||||
=> TryParsePropertyValue(property.GetValue(culture, segment));
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override object? FromEditor(ContentPropertyData editorValue, object? currentValue)
|
||||
=> TryParsePropertyValue(editorValue.Value);
|
||||
|
||||
@@ -54,5 +69,109 @@ public class DecimalPropertyEditor : DataEditor
|
||||
: decimal.TryParse(value?.ToString(), CultureInfo.InvariantCulture, out var parsedDecimalValue)
|
||||
? parsedDecimalValue
|
||||
: null;
|
||||
|
||||
/// <summary>
|
||||
/// Base validator for the decimal property editor validation against data type configured values.
|
||||
/// </summary>
|
||||
internal abstract class DecimalPropertyConfigurationValidatorBase : SimplePropertyConfigurationValidatorBase<double>
|
||||
{
|
||||
/// <summary>
|
||||
/// The configuration key for the minimum value.
|
||||
/// </summary>
|
||||
protected const string ConfigurationKeyMinValue = "min";
|
||||
|
||||
/// <summary>
|
||||
/// The configuration key for the maximum value.
|
||||
/// </summary>
|
||||
protected const string ConfigurationKeyMaxValue = "max";
|
||||
|
||||
/// <summary>
|
||||
/// The configuration key for the step value.
|
||||
/// </summary>
|
||||
protected const string ConfigurationKeyStepValue = "step";
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DecimalPropertyConfigurationValidatorBase"/> class.
|
||||
/// </summary>
|
||||
protected DecimalPropertyConfigurationValidatorBase(ILocalizedTextService localizedTextService) => LocalizedTextService = localizedTextService;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the <see cref="ILocalizedTextService"/>.
|
||||
/// </summary>
|
||||
protected ILocalizedTextService LocalizedTextService { get; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override bool TryParsePropertyValue(object? value, out double parsedDecimalValue)
|
||||
=> double.TryParse(value?.ToString(), CultureInfo.InvariantCulture, out parsedDecimalValue);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates the min/max configuration for the decimal property editor.
|
||||
/// </summary>
|
||||
internal class MinMaxValidator : DecimalPropertyConfigurationValidatorBase, IValueValidator
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="MinMaxValidator"/> class.
|
||||
/// </summary>
|
||||
public MinMaxValidator(ILocalizedTextService localizedTextService)
|
||||
: base(localizedTextService)
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IEnumerable<ValidationResult> Validate(object? value, string? valueType, object? dataTypeConfiguration, PropertyValidationContext validationContext)
|
||||
{
|
||||
if (TryParsePropertyValue(value, out var parsedDecimalValue) is false)
|
||||
{
|
||||
yield break;
|
||||
}
|
||||
|
||||
if (TryGetConfiguredValue(dataTypeConfiguration, ConfigurationKeyMinValue, out double min) && parsedDecimalValue < min)
|
||||
{
|
||||
yield return new ValidationResult(
|
||||
LocalizedTextService.Localize("validation", "outOfRangeMinimum", [parsedDecimalValue.ToString(), min.ToString()]),
|
||||
["value"]);
|
||||
}
|
||||
|
||||
if (TryGetConfiguredValue(dataTypeConfiguration, ConfigurationKeyMaxValue, out double max) && parsedDecimalValue > max)
|
||||
{
|
||||
yield return new ValidationResult(
|
||||
LocalizedTextService.Localize("validation", "outOfRangeMaximum", [parsedDecimalValue.ToString(), max.ToString()]),
|
||||
["value"]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates the step configuration for the decimal property editor.
|
||||
/// </summary>
|
||||
internal class StepValidator : DecimalPropertyConfigurationValidatorBase, IValueValidator
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="StepValidator"/> class.
|
||||
/// </summary>
|
||||
public StepValidator(ILocalizedTextService localizedTextService)
|
||||
: base(localizedTextService)
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IEnumerable<ValidationResult> Validate(object? value, string? valueType, object? dataTypeConfiguration, PropertyValidationContext validationContext)
|
||||
{
|
||||
if (TryParsePropertyValue(value, out var parsedDecimalValue) is false)
|
||||
{
|
||||
yield break;
|
||||
}
|
||||
|
||||
if (TryGetConfiguredValue(dataTypeConfiguration, ConfigurationKeyMinValue, out double min) &&
|
||||
TryGetConfiguredValue(dataTypeConfiguration, ConfigurationKeyStepValue, out double step) &&
|
||||
ValidationHelper.IsValueValidForStep((decimal)parsedDecimalValue, (decimal)min, (decimal)step) is false)
|
||||
{
|
||||
yield return new ValidationResult(
|
||||
LocalizedTextService.Localize("validation", "invalidStep", [parsedDecimalValue.ToString(), step.ToString(), min.ToString()]),
|
||||
["value"]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +1,20 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Globalization;
|
||||
using Umbraco.Cms.Core.IO;
|
||||
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;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Core.PropertyEditors;
|
||||
|
||||
/// <summary>
|
||||
/// Represents an integer property and parameter editor.
|
||||
/// Represents a decimal property editor.
|
||||
/// </summary>
|
||||
[DataEditor(
|
||||
Constants.PropertyEditors.Aliases.Integer,
|
||||
@@ -17,6 +22,9 @@ namespace Umbraco.Cms.Core.PropertyEditors;
|
||||
ValueEditorIsReusable = true)]
|
||||
public class IntegerPropertyEditor : DataEditor
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="IntegerPropertyEditor"/> class.
|
||||
/// </summary>
|
||||
public IntegerPropertyEditor(IDataValueEditorFactory dataValueEditorFactory)
|
||||
: base(dataValueEditorFactory)
|
||||
=> SupportsReadOnly = true;
|
||||
@@ -28,19 +36,28 @@ public class IntegerPropertyEditor : DataEditor
|
||||
/// <inheritdoc />
|
||||
protected override IConfigurationEditor CreateConfigurationEditor() => new IntegerConfigurationEditor();
|
||||
|
||||
/// <summary>
|
||||
/// Defines the value editor for the integer property editor.
|
||||
/// </summary>
|
||||
internal class IntegerPropertyValueEditor : DataValueEditor
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="IntegerPropertyValueEditor"/> class.
|
||||
/// </summary>
|
||||
public IntegerPropertyValueEditor(
|
||||
IShortStringHelper shortStringHelper,
|
||||
IJsonSerializer jsonSerializer,
|
||||
IIOHelper ioHelper,
|
||||
DataEditorAttribute attribute)
|
||||
DataEditorAttribute attribute,
|
||||
ILocalizedTextService localizedTextService)
|
||||
: base(shortStringHelper, jsonSerializer, ioHelper, attribute)
|
||||
=> Validators.Add(new IntegerValidator());
|
||||
=> Validators.AddRange([new IntegerValidator(), new MinMaxValidator(localizedTextService), new StepValidator(localizedTextService)]);
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override object? ToEditor(IProperty property, string? culture = null, string? segment = null)
|
||||
=> TryParsePropertyValue(property.GetValue(culture, segment));
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override object? FromEditor(ContentPropertyData editorValue, object? currentValue)
|
||||
=> TryParsePropertyValue(editorValue.Value);
|
||||
|
||||
@@ -50,5 +67,109 @@ public class IntegerPropertyEditor : DataEditor
|
||||
: int.TryParse(value?.ToString(), CultureInfo.InvariantCulture, out var parsedIntegerValue)
|
||||
? parsedIntegerValue
|
||||
: null;
|
||||
|
||||
/// <summary>
|
||||
/// /// Base validator for the integer property editor validation against data type configured values.
|
||||
/// </summary>
|
||||
internal abstract class IntegerPropertyConfigurationValidatorBase : SimplePropertyConfigurationValidatorBase<int>
|
||||
{
|
||||
/// <summary>
|
||||
/// The configuration key for the minimum value.
|
||||
/// </summary>
|
||||
protected const string ConfigurationKeyMinValue = "min";
|
||||
|
||||
/// <summary>
|
||||
/// The configuration key for the maximum value.
|
||||
/// </summary>
|
||||
protected const string ConfigurationKeyMaxValue = "max";
|
||||
|
||||
/// <summary>
|
||||
/// The configuration key for the step value.
|
||||
/// </summary>
|
||||
protected const string ConfigurationKeyStepValue = "step";
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="IntegerPropertyConfigurationValidatorBase"/> class.
|
||||
/// </summary>
|
||||
protected IntegerPropertyConfigurationValidatorBase(ILocalizedTextService localizedTextService) => LocalizedTextService = localizedTextService;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the <see cref="ILocalizedTextService"/>.
|
||||
/// </summary>
|
||||
protected ILocalizedTextService LocalizedTextService { get; }
|
||||
|
||||
/// <inheritdoc/>
|
||||
protected override bool TryParsePropertyValue(object? value, out int parsedIntegerValue)
|
||||
=> int.TryParse(value?.ToString(), CultureInfo.InvariantCulture, out parsedIntegerValue);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates the min/max configuration for the integer property editor.
|
||||
/// </summary>
|
||||
internal class MinMaxValidator : IntegerPropertyConfigurationValidatorBase, IValueValidator
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="MinMaxValidator"/> class.
|
||||
/// </summary>
|
||||
public MinMaxValidator(ILocalizedTextService localizedTextService)
|
||||
: base(localizedTextService)
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IEnumerable<ValidationResult> Validate(object? value, string? valueType, object? dataTypeConfiguration, PropertyValidationContext validationContext)
|
||||
{
|
||||
if (TryParsePropertyValue(value, out var parsedIntegerValue) is false)
|
||||
{
|
||||
yield break;
|
||||
}
|
||||
|
||||
if (TryGetConfiguredValue(dataTypeConfiguration, ConfigurationKeyMinValue, out int min) && parsedIntegerValue < min)
|
||||
{
|
||||
yield return new ValidationResult(
|
||||
LocalizedTextService.Localize("validation", "outOfRangeMinimum", [parsedIntegerValue.ToString(), min.ToString()]),
|
||||
["value"]);
|
||||
}
|
||||
|
||||
if (TryGetConfiguredValue(dataTypeConfiguration, ConfigurationKeyMaxValue, out int max) && parsedIntegerValue > max)
|
||||
{
|
||||
yield return new ValidationResult(
|
||||
LocalizedTextService.Localize("validation", "outOfRangeMaximum", [parsedIntegerValue.ToString(), max.ToString()]),
|
||||
["value"]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates the step configuration for the integer property editor.
|
||||
/// </summary>
|
||||
internal class StepValidator : IntegerPropertyConfigurationValidatorBase, IValueValidator
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="StepValidator"/> class.
|
||||
/// </summary>
|
||||
public StepValidator(ILocalizedTextService localizedTextService)
|
||||
: base(localizedTextService)
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IEnumerable<ValidationResult> Validate(object? value, string? valueType, object? dataTypeConfiguration, PropertyValidationContext validationContext)
|
||||
{
|
||||
if (TryParsePropertyValue(value, out var parsedIntegerValue) is false)
|
||||
{
|
||||
yield break;
|
||||
}
|
||||
|
||||
if (TryGetConfiguredValue(dataTypeConfiguration, ConfigurationKeyMinValue, out int min) &&
|
||||
TryGetConfiguredValue(dataTypeConfiguration, ConfigurationKeyStepValue, out int step) &&
|
||||
ValidationHelper.IsValueValidForStep(parsedIntegerValue, min, step) is false)
|
||||
{
|
||||
yield return new ValidationResult(
|
||||
LocalizedTextService.Localize("validation", "invalidStep", [parsedIntegerValue.ToString(), step.ToString(), min.ToString()]),
|
||||
["value"]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,4 +13,7 @@ public class SliderConfiguration
|
||||
|
||||
[ConfigurationField("maxVal")]
|
||||
public decimal MaximumValue { get; set; }
|
||||
|
||||
[ConfigurationField("step")]
|
||||
public decimal Step { get; set; }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
namespace Umbraco.Cms.Core.PropertyEditors.Validation;
|
||||
|
||||
/// <summary>
|
||||
/// Provides helper methods for validation of property editor values based on data type configuration.
|
||||
/// </summary>
|
||||
public static class ValidationHelper
|
||||
{
|
||||
/// <summary>
|
||||
/// Checks if a provided value is valid based on the configured step and minimum values.
|
||||
/// </summary>
|
||||
/// <param name="value">The provided value.</param>
|
||||
/// <param name="min">The configured minimum value.</param>
|
||||
/// <param name="step">The configured step value.</param>
|
||||
/// <returns>True if the value is valid otherwise false.</returns>
|
||||
public static bool IsValueValidForStep(decimal value, decimal min, decimal step)
|
||||
{
|
||||
if (value < min)
|
||||
{
|
||||
return true; // Outside of the range, so we expect another validator will have picked this up.
|
||||
}
|
||||
|
||||
return (value - min) % step == 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
namespace Umbraco.Cms.Core.PropertyEditors.Validators;
|
||||
|
||||
/// <summary>
|
||||
/// Provides common functionality to validators that rely on data type configuration.
|
||||
/// </summary>
|
||||
public abstract class DictionaryConfigurationValidatorBase
|
||||
{
|
||||
/// <summary>
|
||||
/// Retrieves a typed value from data type dictionary configuration for the provided key.
|
||||
/// </summary>
|
||||
/// <param name="dataTypeConfiguration">The data type configuration.</param>
|
||||
/// <param name="key">The configuration key.</param>
|
||||
/// <param name="value">The configuration value (if found), otherwise zero.</param>
|
||||
/// <returns>True if the configured value was found.</returns>
|
||||
protected static bool TryGetConfiguredValue<TValue>(object? dataTypeConfiguration, string key, [NotNullWhen(true)] out TValue? value)
|
||||
{
|
||||
if (dataTypeConfiguration is not Dictionary<string, object> configuration)
|
||||
{
|
||||
value = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (configuration.TryGetValue(key, out object? obj) && obj is TValue castValue)
|
||||
{
|
||||
value = castValue;
|
||||
return true;
|
||||
}
|
||||
|
||||
value = default;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
using Umbraco.Cms.Core.Configuration.Models.Validation;
|
||||
|
||||
namespace Umbraco.Cms.Core.PropertyEditors.Validators;
|
||||
|
||||
/// <summary>
|
||||
/// Provides common functionality to validators that rely on data type configuration.
|
||||
/// </summary>
|
||||
/// <typeparam name="TValue">The type to parse to.</typeparam>
|
||||
public abstract class SimplePropertyConfigurationValidatorBase<TValue> : DictionaryConfigurationValidatorBase
|
||||
{
|
||||
/// <summary>
|
||||
/// Parses the raw property value into it's typed equivalent.
|
||||
/// </summary>
|
||||
/// <param name="value">The property value as a nullable object.</param>
|
||||
/// <param name="parsedValue">The parsed value.</param>
|
||||
/// <returns>True if the parse succeeded, otherwise false.</returns>
|
||||
protected abstract bool TryParsePropertyValue(object? value, out TValue parsedValue);
|
||||
}
|
||||
@@ -1,17 +1,25 @@
|
||||
// Copyright (c) Umbraco.
|
||||
// See LICENSE for more details.
|
||||
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Globalization;
|
||||
using System.Text.Json.Nodes;
|
||||
using Umbraco.Cms.Core.IO;
|
||||
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;
|
||||
using Umbraco.Extensions;
|
||||
|
||||
namespace Umbraco.Cms.Core.PropertyEditors;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a slider editor.
|
||||
/// Represents a slider editor.
|
||||
/// </summary>
|
||||
[DataEditor(
|
||||
Constants.PropertyEditors.Aliases.Slider,
|
||||
@@ -38,18 +46,29 @@ public class SliderPropertyEditor : DataEditor
|
||||
protected override IConfigurationEditor CreateConfigurationEditor() =>
|
||||
new SliderConfigurationEditor(_ioHelper);
|
||||
|
||||
/// <summary>
|
||||
/// Defines the value editor for the slider property editor.
|
||||
/// </summary>
|
||||
internal class SliderPropertyValueEditor : DataValueEditor
|
||||
{
|
||||
private readonly IJsonSerializer _jsonSerializer;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SliderPropertyValueEditor"/> class.
|
||||
/// </summary>
|
||||
public SliderPropertyValueEditor(
|
||||
IShortStringHelper shortStringHelper,
|
||||
IJsonSerializer jsonSerializer,
|
||||
IIOHelper ioHelper,
|
||||
DataEditorAttribute attribute)
|
||||
: base(shortStringHelper, jsonSerializer, ioHelper, attribute) =>
|
||||
DataEditorAttribute attribute,
|
||||
ILocalizedTextService localizedTextService)
|
||||
: base(shortStringHelper, jsonSerializer, ioHelper, attribute)
|
||||
{
|
||||
_jsonSerializer = jsonSerializer;
|
||||
Validators.AddRange(new RangeValidator(localizedTextService), new MinMaxValidator(localizedTextService), new StepValidator(localizedTextService));
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override object? ToEditor(IProperty property, string? culture = null, string? segment = null)
|
||||
{
|
||||
// value is stored as a string - either a single decimal value
|
||||
@@ -72,18 +91,219 @@ public class SliderPropertyEditor : DataEditor
|
||||
: null;
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public override object? FromEditor(ContentPropertyData editorValue, object? currentValue)
|
||||
=> editorValue.Value is not null && _jsonSerializer.TryDeserialize(editorValue.Value, out SliderRange? sliderRange)
|
||||
? sliderRange.ToString()
|
||||
: null;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a slider value.
|
||||
/// </summary>
|
||||
internal class SliderRange
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the slider range from value.
|
||||
/// </summary>
|
||||
public decimal From { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the slide range to value.
|
||||
/// </summary>
|
||||
public decimal To { get; set; }
|
||||
|
||||
public override string ToString() => From == To ? $"{From.ToString(CultureInfo.InvariantCulture)}" : $"{From.ToString(CultureInfo.InvariantCulture)},{To.ToString(CultureInfo.InvariantCulture)}";
|
||||
/// <inheritdoc/>
|
||||
public override string ToString()
|
||||
=> From == To
|
||||
? $"{From.ToString(CultureInfo.InvariantCulture)}"
|
||||
: $"{From.ToString(CultureInfo.InvariantCulture)},{To.ToString(CultureInfo.InvariantCulture)}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Base validator for the slider property editor validation against data type configured values.
|
||||
/// </summary>
|
||||
internal abstract class SliderPropertyConfigurationValidatorBase
|
||||
{
|
||||
/// <summary>
|
||||
/// The configuration key for the minimum value.
|
||||
/// </summary>
|
||||
protected const string ConfigurationKeyMinValue = "minVal";
|
||||
|
||||
/// <summary>
|
||||
/// The configuration key for the maximum value.
|
||||
/// </summary>
|
||||
protected const string ConfigurationKeyMaxValue = "maxVal";
|
||||
|
||||
/// <summary>
|
||||
/// The configuration key for the step value.
|
||||
/// </summary>
|
||||
protected const string ConfigurationKeyStepValue = "step";
|
||||
|
||||
/// <summary>
|
||||
/// The configuration key for the enable range value.
|
||||
/// </summary>
|
||||
protected const string ConfigurationKeyEnableRangeValue = "enableRange";
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="SliderPropertyConfigurationValidatorBase"/> class.
|
||||
/// </summary>
|
||||
protected SliderPropertyConfigurationValidatorBase(ILocalizedTextService localizedTextService) => LocalizedTextService = localizedTextService;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the <see cref="ILocalizedTextService"/>.
|
||||
/// </summary>
|
||||
protected ILocalizedTextService LocalizedTextService { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Parses a <see cref="SliderRange"/> from the provided value.
|
||||
/// </summary>
|
||||
protected bool TryParsePropertyValue(object? value, [NotNullWhen(true)] out SliderRange? parsedValue)
|
||||
{
|
||||
if (value is null || value is not JsonObject valueAsJsonObject)
|
||||
{
|
||||
parsedValue = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
var from = GetDecimalValue(valueAsJsonObject, nameof(SliderRange.From).ToLowerInvariant());
|
||||
var to = GetDecimalValue(valueAsJsonObject, nameof(SliderRange.To).ToLowerInvariant());
|
||||
if (from.HasValue is false || to.HasValue is false)
|
||||
{
|
||||
parsedValue = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
parsedValue = new SliderRange
|
||||
{
|
||||
From = from.Value,
|
||||
To = to.Value,
|
||||
};
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static decimal? GetDecimalValue(JsonObject valueAsJsonObject, string key)
|
||||
=> valueAsJsonObject[key]?.GetValue<decimal>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates the range configuration for the slider property editor.
|
||||
/// </summary>
|
||||
internal class RangeValidator : SliderPropertyConfigurationValidatorBase, IValueValidator
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="MinMaxValidator"/> class.
|
||||
/// </summary>
|
||||
public RangeValidator(ILocalizedTextService localizedTextService)
|
||||
: base(localizedTextService)
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IEnumerable<ValidationResult> Validate(object? value, string? valueType, object? dataTypeConfiguration, PropertyValidationContext validationContext)
|
||||
{
|
||||
if (TryParsePropertyValue(value, out SliderRange? sliderRange) is false)
|
||||
{
|
||||
yield break;
|
||||
}
|
||||
|
||||
if (dataTypeConfiguration is not SliderConfiguration sliderConfiguration)
|
||||
{
|
||||
yield break;
|
||||
}
|
||||
|
||||
if (sliderConfiguration.EnableRange is false && sliderRange.From != sliderRange.To)
|
||||
{
|
||||
yield return new ValidationResult(
|
||||
LocalizedTextService.Localize("validation", "unexpectedRange", [sliderRange.ToString()]),
|
||||
["value"]);
|
||||
}
|
||||
|
||||
if (sliderConfiguration.EnableRange && sliderRange.To < sliderRange.From)
|
||||
{
|
||||
yield return new ValidationResult(
|
||||
LocalizedTextService.Localize("validation", "invalidRange", [sliderRange.ToString()]),
|
||||
["value"]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates the min/max configuration for the slider property editor.
|
||||
/// </summary>
|
||||
internal class MinMaxValidator : SliderPropertyConfigurationValidatorBase, IValueValidator
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="MinMaxValidator"/> class.
|
||||
/// </summary>
|
||||
public MinMaxValidator(ILocalizedTextService localizedTextService)
|
||||
: base(localizedTextService)
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IEnumerable<ValidationResult> Validate(object? value, string? valueType, object? dataTypeConfiguration, PropertyValidationContext validationContext)
|
||||
{
|
||||
if (TryParsePropertyValue(value, out SliderRange? sliderRange) is false)
|
||||
{
|
||||
yield break;
|
||||
}
|
||||
|
||||
if (dataTypeConfiguration is not SliderConfiguration sliderConfiguration)
|
||||
{
|
||||
yield break;
|
||||
}
|
||||
|
||||
if (sliderRange.From < sliderConfiguration.MinimumValue)
|
||||
{
|
||||
yield return new ValidationResult(
|
||||
LocalizedTextService.Localize("validation", "outOfRangeMinimum", [sliderRange.From.ToString(), sliderConfiguration.MinimumValue.ToString()]),
|
||||
["value"]);
|
||||
}
|
||||
|
||||
if (sliderRange.To > sliderConfiguration.MaximumValue)
|
||||
{
|
||||
yield return new ValidationResult(
|
||||
LocalizedTextService.Localize("validation", "outOfRangeMaximum", [sliderRange.To.ToString(), sliderConfiguration.MaximumValue.ToString()]),
|
||||
["value"]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Validates the step configuration for the slider property editor.
|
||||
/// </summary>
|
||||
internal class StepValidator : SliderPropertyConfigurationValidatorBase, IValueValidator
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="StepValidator"/> class.
|
||||
/// </summary>
|
||||
public StepValidator(ILocalizedTextService localizedTextService)
|
||||
: base(localizedTextService)
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
public IEnumerable<ValidationResult> Validate(object? value, string? valueType, object? dataTypeConfiguration, PropertyValidationContext validationContext)
|
||||
{
|
||||
if (TryParsePropertyValue(value, out SliderRange? sliderRange) is false)
|
||||
{
|
||||
yield break;
|
||||
}
|
||||
|
||||
if (dataTypeConfiguration is not SliderConfiguration sliderConfiguration)
|
||||
{
|
||||
yield break;
|
||||
}
|
||||
|
||||
if (ValidationHelper.IsValueValidForStep(sliderRange.From, sliderConfiguration.MinimumValue, sliderConfiguration.Step) is false ||
|
||||
ValidationHelper.IsValueValidForStep(sliderRange.To, sliderConfiguration.MinimumValue, sliderConfiguration.Step) is false)
|
||||
{
|
||||
yield return new ValidationResult(
|
||||
LocalizedTextService.Localize("validation", "invalidStep", [sliderRange.ToString(), sliderConfiguration.Step.ToString(), sliderConfiguration.MinimumValue.ToString()]),
|
||||
["value"]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
using Moq;
|
||||
using System.Globalization;
|
||||
using Moq;
|
||||
using NUnit.Framework;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.IO;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Models.Editors;
|
||||
using Umbraco.Cms.Core.Models.Validation;
|
||||
using Umbraco.Cms.Core.PropertyEditors;
|
||||
using Umbraco.Cms.Core.Serialization;
|
||||
using Umbraco.Cms.Core.Services;
|
||||
@@ -71,6 +73,84 @@ public class DecimalValueEditorTests
|
||||
Assert.IsNull(result);
|
||||
}
|
||||
|
||||
[TestCase("x", false)]
|
||||
[TestCase(1.5, true)]
|
||||
public void Validates_Is_Decimal(object value, bool expectedSuccess)
|
||||
{
|
||||
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, $"The value {value} is not a valid decimal");
|
||||
}
|
||||
}
|
||||
|
||||
[TestCase(0.9, false)]
|
||||
[TestCase(1.1, true)]
|
||||
[TestCase(1.3, true)]
|
||||
public void Validates_Is_Greater_Than_Or_Equal_To_Configured_Min(object value, bool expectedSuccess)
|
||||
{
|
||||
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_outOfRangeMinimum");
|
||||
}
|
||||
}
|
||||
|
||||
[TestCase(1.7, true)]
|
||||
[TestCase(1.9, true)]
|
||||
[TestCase(2.1, false)]
|
||||
public void Validates_Is_Less_Than_Or_Equal_To_Configured_Max(object value, bool expectedSuccess)
|
||||
{
|
||||
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_outOfRangeMaximum");
|
||||
}
|
||||
}
|
||||
|
||||
[TestCase(1.4, false)]
|
||||
[TestCase(1.5, true)]
|
||||
public void Validates_Matches_Configured_Step(object value, bool expectedSuccess)
|
||||
{
|
||||
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_invalidStep");
|
||||
}
|
||||
}
|
||||
|
||||
private static object? FromEditor(object? value)
|
||||
=> CreateValueEditor().FromEditor(new ContentPropertyData(value, null), null);
|
||||
|
||||
@@ -86,11 +166,26 @@ public class DecimalValueEditorTests
|
||||
|
||||
private static DecimalPropertyEditor.DecimalPropertyValueEditor CreateValueEditor()
|
||||
{
|
||||
var valueEditor = new DecimalPropertyEditor.DecimalPropertyValueEditor(
|
||||
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 DecimalPropertyEditor.DecimalPropertyValueEditor(
|
||||
Mock.Of<IShortStringHelper>(),
|
||||
Mock.Of<IJsonSerializer>(),
|
||||
Mock.Of<IIOHelper>(),
|
||||
new DataEditorAttribute("alias"));
|
||||
return valueEditor;
|
||||
new DataEditorAttribute("alias"),
|
||||
localizedTextServiceMock.Object)
|
||||
{
|
||||
ConfigurationObject = new Dictionary<string, object>
|
||||
{
|
||||
{ "min", 1.1 },
|
||||
{ "max", 1.9 },
|
||||
{ "step", 0.2 }
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,191 @@
|
||||
using System.Globalization;
|
||||
using Moq;
|
||||
using NUnit.Framework;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.IO;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Models.Editors;
|
||||
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 IntegerValueEditorTests
|
||||
{
|
||||
// annoyingly we can't use decimals etc. in attributes, so we can't turn these into test cases :(
|
||||
private Dictionary<object?,object?> _valuesAndExpectedResults = new();
|
||||
|
||||
[SetUp]
|
||||
public void SetUp() => _valuesAndExpectedResults = new Dictionary<object?, object?>
|
||||
{
|
||||
{ 123m, 123 },
|
||||
{ 123, 123 },
|
||||
{ -123, -123 },
|
||||
{ 123.45d, null },
|
||||
{ "123.45", null },
|
||||
{ "1234.56", null },
|
||||
{ "123,45", null },
|
||||
{ "1.234,56", null },
|
||||
{ "123 45", null },
|
||||
{ "something", null },
|
||||
{ true, null },
|
||||
{ new object(), null },
|
||||
{ new List<string> { "some", "values" }, null },
|
||||
{ Guid.NewGuid(), null },
|
||||
{ new GuidUdi(Constants.UdiEntityType.Document, Guid.NewGuid()), null }
|
||||
};
|
||||
|
||||
[Test]
|
||||
public void Can_Parse_Values_From_Editor()
|
||||
{
|
||||
foreach (var (value, expected) in _valuesAndExpectedResults)
|
||||
{
|
||||
var fromEditor = FromEditor(value);
|
||||
Assert.AreEqual(expected, fromEditor, message: $"Failed for: {value}");
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Can_Parse_Values_To_Editor()
|
||||
{
|
||||
foreach (var (value, expected) in _valuesAndExpectedResults)
|
||||
{
|
||||
var toEditor = ToEditor(value);
|
||||
Assert.AreEqual(expected, toEditor, message: $"Failed for: {value}");
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Null_From_Editor_Yields_Null()
|
||||
{
|
||||
var result = FromEditor(null);
|
||||
Assert.IsNull(result);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Null_To_Editor_Yields_Null()
|
||||
{
|
||||
var result = ToEditor(null);
|
||||
Assert.IsNull(result);
|
||||
}
|
||||
|
||||
[TestCase("x", false)]
|
||||
[TestCase(10, true)]
|
||||
public void Validates_Is_Integer(object value, bool expectedSuccess)
|
||||
{
|
||||
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($"The value {value} is not a valid integer", validationResult.ErrorMessage);
|
||||
}
|
||||
}
|
||||
|
||||
[TestCase(8, false)]
|
||||
[TestCase(10, true)]
|
||||
[TestCase(12, true)]
|
||||
public void Validates_Is_Greater_Than_Or_Equal_To_Configured_Min(object value, bool expectedSuccess)
|
||||
{
|
||||
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("validation_outOfRangeMinimum", validationResult.ErrorMessage);
|
||||
}
|
||||
}
|
||||
|
||||
[TestCase(18, true)]
|
||||
[TestCase(20, true)]
|
||||
[TestCase(22, false)]
|
||||
public void Validates_Is_Less_Than_Or_Equal_To_Configured_Max(object value, bool expectedSuccess)
|
||||
{
|
||||
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("validation_outOfRangeMaximum", validationResult.ErrorMessage);
|
||||
}
|
||||
}
|
||||
|
||||
[TestCase(17, false)]
|
||||
[TestCase(18, true)]
|
||||
public void Validates_Matches_Configured_Step(object value, bool expectedSuccess)
|
||||
{
|
||||
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("validation_invalidStep", validationResult.ErrorMessage);
|
||||
}
|
||||
}
|
||||
|
||||
private static object? FromEditor(object? value)
|
||||
=> CreateValueEditor().FromEditor(new ContentPropertyData(value, null), null);
|
||||
|
||||
private static object? ToEditor(object? value)
|
||||
{
|
||||
var property = new Mock<IProperty>();
|
||||
property
|
||||
.Setup(p => p.GetValue(It.IsAny<string?>(), It.IsAny<string?>(), It.IsAny<bool>()))
|
||||
.Returns(value);
|
||||
|
||||
return CreateValueEditor().ToEditor(property.Object);
|
||||
}
|
||||
|
||||
private static IntegerPropertyEditor.IntegerPropertyValueEditor 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 IntegerPropertyEditor.IntegerPropertyValueEditor(
|
||||
Mock.Of<IShortStringHelper>(),
|
||||
Mock.Of<IJsonSerializer>(),
|
||||
Mock.Of<IIOHelper>(),
|
||||
new DataEditorAttribute("alias"),
|
||||
localizedTextServiceMock.Object)
|
||||
{
|
||||
ConfigurationObject = new Dictionary<string, object>
|
||||
{
|
||||
{ "min", 10 },
|
||||
{ "max", 20 },
|
||||
{ "step", 2 }
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,96 +0,0 @@
|
||||
using Moq;
|
||||
using NUnit.Framework;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.IO;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Models.Editors;
|
||||
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 NumericValueEditorTests
|
||||
{
|
||||
// annoyingly we can't use decimals etc. in attributes, so we can't turn these into test cases :(
|
||||
private Dictionary<object?,object?> _valuesAndExpectedResults = new();
|
||||
|
||||
[SetUp]
|
||||
public void SetUp() => _valuesAndExpectedResults = new Dictionary<object?, object?>
|
||||
{
|
||||
{ 123m, 123 },
|
||||
{ 123, 123 },
|
||||
{ -123, -123 },
|
||||
{ 123.45d, null },
|
||||
{ "123.45", null },
|
||||
{ "1234.56", null },
|
||||
{ "123,45", null },
|
||||
{ "1.234,56", null },
|
||||
{ "123 45", null },
|
||||
{ "something", null },
|
||||
{ true, null },
|
||||
{ new object(), null },
|
||||
{ new List<string> { "some", "values" }, null },
|
||||
{ Guid.NewGuid(), null },
|
||||
{ new GuidUdi(Constants.UdiEntityType.Document, Guid.NewGuid()), null }
|
||||
};
|
||||
|
||||
[Test]
|
||||
public void Can_Parse_Values_From_Editor()
|
||||
{
|
||||
foreach (var (value, expected) in _valuesAndExpectedResults)
|
||||
{
|
||||
var fromEditor = FromEditor(value);
|
||||
Assert.AreEqual(expected, fromEditor, message: $"Failed for: {value}");
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Can_Parse_Values_To_Editor()
|
||||
{
|
||||
foreach (var (value, expected) in _valuesAndExpectedResults)
|
||||
{
|
||||
var toEditor = ToEditor(value);
|
||||
Assert.AreEqual(expected, toEditor, message: $"Failed for: {value}");
|
||||
}
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Null_From_Editor_Yields_Null()
|
||||
{
|
||||
var result = FromEditor(null);
|
||||
Assert.IsNull(result);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public void Null_To_Editor_Yields_Null()
|
||||
{
|
||||
var result = ToEditor(null);
|
||||
Assert.IsNull(result);
|
||||
}
|
||||
|
||||
private static object? FromEditor(object? value)
|
||||
=> CreateValueEditor().FromEditor(new ContentPropertyData(value, null), null);
|
||||
|
||||
private static object? ToEditor(object? value)
|
||||
{
|
||||
var property = new Mock<IProperty>();
|
||||
property
|
||||
.Setup(p => p.GetValue(It.IsAny<string?>(), It.IsAny<string?>(), It.IsAny<bool>()))
|
||||
.Returns(value);
|
||||
|
||||
return CreateValueEditor().ToEditor(property.Object);
|
||||
}
|
||||
|
||||
private static IntegerPropertyEditor.IntegerPropertyValueEditor CreateValueEditor()
|
||||
{
|
||||
var valueEditor = new IntegerPropertyEditor.IntegerPropertyValueEditor(
|
||||
Mock.Of<IShortStringHelper>(),
|
||||
Mock.Of<IJsonSerializer>(),
|
||||
Mock.Of<IIOHelper>(),
|
||||
new DataEditorAttribute("alias"));
|
||||
return valueEditor;
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,15 @@
|
||||
using System.Text.Json.Nodes;
|
||||
using System.Globalization;
|
||||
using System.Text.Json.Nodes;
|
||||
using Moq;
|
||||
using NUnit.Framework;
|
||||
using Umbraco.Cms.Core;
|
||||
using Umbraco.Cms.Core.IO;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
using Umbraco.Cms.Core.Models.Editors;
|
||||
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;
|
||||
using Umbraco.Cms.Infrastructure.Serialization;
|
||||
|
||||
@@ -14,7 +18,9 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.PropertyEditors;
|
||||
[TestFixture]
|
||||
public class SliderValueEditorTests
|
||||
{
|
||||
#pragma warning disable IDE1006 // Naming Styles
|
||||
public static object[] InvalidCaseData = new object[]
|
||||
#pragma warning restore IDE1006 // Naming Styles
|
||||
{
|
||||
123m,
|
||||
123,
|
||||
@@ -99,6 +105,132 @@ public class SliderValueEditorTests
|
||||
Assert.IsNull(result);
|
||||
}
|
||||
|
||||
[TestCase(true, 1.1, 1.1, true)]
|
||||
[TestCase(true, 1.1, 1.3, true)]
|
||||
[TestCase(false, 1.1, 1.1, true)]
|
||||
[TestCase(false, 1.1, 1.3, false)]
|
||||
public void Validates_Contains_Range_Only_When_Enabled(bool enableRange, decimal from, decimal to, bool expectedSuccess)
|
||||
{
|
||||
var value = new JsonObject
|
||||
{
|
||||
{ "from", from },
|
||||
{ "to", to },
|
||||
};
|
||||
var editor = CreateValueEditor(enableRange: enableRange);
|
||||
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_unexpectedRange");
|
||||
}
|
||||
}
|
||||
|
||||
[TestCase(1.1, 1.1, true)]
|
||||
[TestCase(1.1, 1.3, true)]
|
||||
[TestCase(1.3, 1.1, false)]
|
||||
public void Validates_Contains_Valid_Range_Only_When_Enabled(decimal from, decimal to, bool expectedSuccess)
|
||||
{
|
||||
var value = new JsonObject
|
||||
{
|
||||
{ "from", from },
|
||||
{ "to", to },
|
||||
};
|
||||
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_invalidRange");
|
||||
}
|
||||
}
|
||||
|
||||
[TestCase(0.9, 1.1, false)]
|
||||
[TestCase(1.1, 1.1, true)]
|
||||
[TestCase(1.3, 1.7, true)]
|
||||
public void Validates_Is_Greater_Than_Or_Equal_To_Configured_Min(decimal from, decimal to, bool expectedSuccess)
|
||||
{
|
||||
var value = new JsonObject
|
||||
{
|
||||
{ "from", from },
|
||||
{ "to", to },
|
||||
};
|
||||
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_outOfRangeMinimum");
|
||||
}
|
||||
}
|
||||
|
||||
[TestCase(1.3, 1.7, true)]
|
||||
[TestCase(1.9, 1.9, true)]
|
||||
[TestCase(1.9, 2.1, false)]
|
||||
public void Validates_Is_Less_Than_Or_Equal_To_Configured_Max(decimal from, decimal to, bool expectedSuccess)
|
||||
{
|
||||
var value = new JsonObject
|
||||
{
|
||||
{ "from", from },
|
||||
{ "to", to },
|
||||
};
|
||||
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_outOfRangeMaximum");
|
||||
}
|
||||
}
|
||||
|
||||
[TestCase(1.3, 1.7, true)]
|
||||
[TestCase(1.4, 1.7, false)]
|
||||
[TestCase(1.3, 1.6, false)]
|
||||
public void Validates_Matches_Configured_Step(decimal from, decimal to, bool expectedSuccess)
|
||||
{
|
||||
var value = new JsonObject
|
||||
{
|
||||
{ "from", from },
|
||||
{ "to", to },
|
||||
};
|
||||
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_invalidStep");
|
||||
}
|
||||
}
|
||||
|
||||
private static object? FromEditor(object? value)
|
||||
=> CreateValueEditor().FromEditor(new ContentPropertyData(value, null), null);
|
||||
|
||||
@@ -112,13 +244,29 @@ public class SliderValueEditorTests
|
||||
return CreateValueEditor().ToEditor(property.Object);
|
||||
}
|
||||
|
||||
private static SliderPropertyEditor.SliderPropertyValueEditor CreateValueEditor()
|
||||
private static SliderPropertyEditor.SliderPropertyValueEditor CreateValueEditor(bool enableRange = true)
|
||||
{
|
||||
var valueEditor = new SliderPropertyEditor.SliderPropertyValueEditor(
|
||||
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 SliderPropertyEditor.SliderPropertyValueEditor(
|
||||
Mock.Of<IShortStringHelper>(),
|
||||
new SystemTextJsonSerializer(),
|
||||
Mock.Of<IIOHelper>(),
|
||||
new DataEditorAttribute("alias"));
|
||||
return valueEditor;
|
||||
new DataEditorAttribute("alias"),
|
||||
localizedTextServiceMock.Object)
|
||||
{
|
||||
ConfigurationObject = new SliderConfiguration
|
||||
{
|
||||
EnableRange = enableRange,
|
||||
MinimumValue = 1.1m,
|
||||
MaximumValue = 1.9m,
|
||||
Step = 0.2m
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user