diff --git a/src/Umbraco.Core/EmbeddedResources/Lang/da.xml b/src/Umbraco.Core/EmbeddedResources/Lang/da.xml
index 7bec4bb600..1fb0445bb1 100644
--- a/src/Umbraco.Core/EmbeddedResources/Lang/da.xml
+++ b/src/Umbraco.Core/EmbeddedResources/Lang/da.xml
@@ -116,6 +116,11 @@ Mange hilsner fra Umbraco robotten
Den valgte medie type er ugyldig.Det er kun tilladt at vælge ét medie.Valgt medie kommer fra en ugyldig mappe.
+ Værdien %0% er mindre end det tilladte minimum af %1%.
+ Værdien %0% er større end det tilladte maksimum af %1%.
+ Værdien %0% passer ikke med den konfigureret trin værdi af %1% og mindste værdi af %2%.
+ Værdien %0% forventes ikke at indeholde et spænd.
+ Værdien %0% forventes at have en værdi der er større end fra værdien.Slettet indhold med Id: {0} Relateret til original "parent" med id: {1}
diff --git a/src/Umbraco.Core/EmbeddedResources/Lang/en.xml b/src/Umbraco.Core/EmbeddedResources/Lang/en.xml
index c3b71b5ab0..e1fdba85d4 100644
--- a/src/Umbraco.Core/EmbeddedResources/Lang/en.xml
+++ b/src/Umbraco.Core/EmbeddedResources/Lang/en.xml
@@ -387,6 +387,11 @@
User group name '%0%' is already takenMember group name '%0%' is already takenUsername '%0%' is already taken
+ The value %0% is less than the allowed minimum value of %1%
+ The value %0% is greater than the allowed maximum value of %1%
+ The value %0% does not correspond with the configured step value of %1% and minimum value of %2%
+ The value %0% is not expected to contain a range
+ The value %0% is not expected to have a to value less than the from valueThe chosen media type is invalid.Multiple selected media is not allowed.The selected media is from the wrong folder.
diff --git a/src/Umbraco.Core/EmbeddedResources/Lang/en_us.xml b/src/Umbraco.Core/EmbeddedResources/Lang/en_us.xml
index 827f73a198..6434289b1b 100644
--- a/src/Umbraco.Core/EmbeddedResources/Lang/en_us.xml
+++ b/src/Umbraco.Core/EmbeddedResources/Lang/en_us.xml
@@ -385,6 +385,11 @@
User group name '%0%' is already takenMember group name '%0%' is already takenUsername '%0%' is already taken
+ The value %0% is less than the allowed minimum value of %1%
+ The value %0% is greater than the allowed maximum value of %1%
+ The value %0% does not correspond with the configured step value of %1% and minimum value of %2%
+ The value %0% is not expected to contain a range
+ The value %0% is not expected to have a to value less than the from value%1% more.]]>%1% too many.]]>The content amount requirements are not met for one or more areas.
diff --git a/src/Umbraco.Core/PropertyEditors/DecimalPropertyEditor.cs b/src/Umbraco.Core/PropertyEditors/DecimalPropertyEditor.cs
index 1ecae6b0bd..fbce2b7a5d 100644
--- a/src/Umbraco.Core/PropertyEditors/DecimalPropertyEditor.cs
+++ b/src/Umbraco.Core/PropertyEditors/DecimalPropertyEditor.cs
@@ -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;
///
-/// Represents a decimal property and parameter editor.
+/// Represents a decimal property editor.
///
[DataEditor(
Constants.PropertyEditors.Aliases.Decimal,
@@ -32,19 +37,29 @@ public class DecimalPropertyEditor : DataEditor
///
protected override IConfigurationEditor CreateConfigurationEditor() => new DecimalConfigurationEditor();
+
+ ///
+ /// Defines the value editor for the decimal property editor.
+ ///
internal class DecimalPropertyValueEditor : DataValueEditor
{
+ ///
+ /// Initializes a new instance of the class.
+ ///
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)]);
+ ///
public override object? ToEditor(IProperty property, string? culture = null, string? segment = null)
=> TryParsePropertyValue(property.GetValue(culture, segment));
+ ///
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;
+
+ ///
+ /// Base validator for the decimal property editor validation against data type configured values.
+ ///
+ internal abstract class DecimalPropertyConfigurationValidatorBase : SimplePropertyConfigurationValidatorBase
+ {
+ ///
+ /// The configuration key for the minimum value.
+ ///
+ protected const string ConfigurationKeyMinValue = "min";
+
+ ///
+ /// The configuration key for the maximum value.
+ ///
+ protected const string ConfigurationKeyMaxValue = "max";
+
+ ///
+ /// The configuration key for the step value.
+ ///
+ protected const string ConfigurationKeyStepValue = "step";
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ protected DecimalPropertyConfigurationValidatorBase(ILocalizedTextService localizedTextService) => LocalizedTextService = localizedTextService;
+
+ ///
+ /// Gets the .
+ ///
+ protected ILocalizedTextService LocalizedTextService { get; }
+
+ ///
+ protected override bool TryParsePropertyValue(object? value, out double parsedDecimalValue)
+ => double.TryParse(value?.ToString(), CultureInfo.InvariantCulture, out parsedDecimalValue);
+ }
+
+ ///
+ /// Validates the min/max configuration for the decimal property editor.
+ ///
+ internal class MinMaxValidator : DecimalPropertyConfigurationValidatorBase, IValueValidator
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public MinMaxValidator(ILocalizedTextService localizedTextService)
+ : base(localizedTextService)
+ {
+ }
+
+ ///
+ public IEnumerable 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"]);
+ }
+ }
+ }
+
+ ///
+ /// Validates the step configuration for the decimal property editor.
+ ///
+ internal class StepValidator : DecimalPropertyConfigurationValidatorBase, IValueValidator
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public StepValidator(ILocalizedTextService localizedTextService)
+ : base(localizedTextService)
+ {
+ }
+
+ ///
+ public IEnumerable 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"]);
+ }
+ }
+ }
}
}
diff --git a/src/Umbraco.Core/PropertyEditors/IntegerPropertyEditor.cs b/src/Umbraco.Core/PropertyEditors/IntegerPropertyEditor.cs
index 216a82f348..5f282061b0 100644
--- a/src/Umbraco.Core/PropertyEditors/IntegerPropertyEditor.cs
+++ b/src/Umbraco.Core/PropertyEditors/IntegerPropertyEditor.cs
@@ -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;
///
-/// Represents an integer property and parameter editor.
+/// Represents a decimal property editor.
///
[DataEditor(
Constants.PropertyEditors.Aliases.Integer,
@@ -17,6 +22,9 @@ namespace Umbraco.Cms.Core.PropertyEditors;
ValueEditorIsReusable = true)]
public class IntegerPropertyEditor : DataEditor
{
+ ///
+ /// Initializes a new instance of the class.
+ ///
public IntegerPropertyEditor(IDataValueEditorFactory dataValueEditorFactory)
: base(dataValueEditorFactory)
=> SupportsReadOnly = true;
@@ -28,19 +36,28 @@ public class IntegerPropertyEditor : DataEditor
///
protected override IConfigurationEditor CreateConfigurationEditor() => new IntegerConfigurationEditor();
+ ///
+ /// Defines the value editor for the integer property editor.
+ ///
internal class IntegerPropertyValueEditor : DataValueEditor
{
+ ///
+ /// Initializes a new instance of the class.
+ ///
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)]);
+ ///
public override object? ToEditor(IProperty property, string? culture = null, string? segment = null)
=> TryParsePropertyValue(property.GetValue(culture, segment));
+ ///
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;
+
+ ///
+ /// /// Base validator for the integer property editor validation against data type configured values.
+ ///
+ internal abstract class IntegerPropertyConfigurationValidatorBase : SimplePropertyConfigurationValidatorBase
+ {
+ ///
+ /// The configuration key for the minimum value.
+ ///
+ protected const string ConfigurationKeyMinValue = "min";
+
+ ///
+ /// The configuration key for the maximum value.
+ ///
+ protected const string ConfigurationKeyMaxValue = "max";
+
+ ///
+ /// The configuration key for the step value.
+ ///
+ protected const string ConfigurationKeyStepValue = "step";
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ protected IntegerPropertyConfigurationValidatorBase(ILocalizedTextService localizedTextService) => LocalizedTextService = localizedTextService;
+
+ ///
+ /// Gets the .
+ ///
+ protected ILocalizedTextService LocalizedTextService { get; }
+
+ ///
+ protected override bool TryParsePropertyValue(object? value, out int parsedIntegerValue)
+ => int.TryParse(value?.ToString(), CultureInfo.InvariantCulture, out parsedIntegerValue);
+ }
+
+ ///
+ /// Validates the min/max configuration for the integer property editor.
+ ///
+ internal class MinMaxValidator : IntegerPropertyConfigurationValidatorBase, IValueValidator
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public MinMaxValidator(ILocalizedTextService localizedTextService)
+ : base(localizedTextService)
+ {
+ }
+
+ ///
+ public IEnumerable 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"]);
+ }
+ }
+ }
+
+ ///
+ /// Validates the step configuration for the integer property editor.
+ ///
+ internal class StepValidator : IntegerPropertyConfigurationValidatorBase, IValueValidator
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public StepValidator(ILocalizedTextService localizedTextService)
+ : base(localizedTextService)
+ {
+ }
+
+ ///
+ public IEnumerable 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"]);
+ }
+ }
+ }
}
}
diff --git a/src/Umbraco.Core/PropertyEditors/SliderConfiguration.cs b/src/Umbraco.Core/PropertyEditors/SliderConfiguration.cs
index 68b90c7403..12ca9f461c 100644
--- a/src/Umbraco.Core/PropertyEditors/SliderConfiguration.cs
+++ b/src/Umbraco.Core/PropertyEditors/SliderConfiguration.cs
@@ -13,4 +13,7 @@ public class SliderConfiguration
[ConfigurationField("maxVal")]
public decimal MaximumValue { get; set; }
+
+ [ConfigurationField("step")]
+ public decimal Step { get; set; }
}
diff --git a/src/Umbraco.Core/PropertyEditors/Validation/ValidationHelper.cs b/src/Umbraco.Core/PropertyEditors/Validation/ValidationHelper.cs
new file mode 100644
index 0000000000..afaba8fd57
--- /dev/null
+++ b/src/Umbraco.Core/PropertyEditors/Validation/ValidationHelper.cs
@@ -0,0 +1,24 @@
+namespace Umbraco.Cms.Core.PropertyEditors.Validation;
+
+///
+/// Provides helper methods for validation of property editor values based on data type configuration.
+///
+public static class ValidationHelper
+{
+ ///
+ /// Checks if a provided value is valid based on the configured step and minimum values.
+ ///
+ /// The provided value.
+ /// The configured minimum value.
+ /// The configured step value.
+ /// True if the value is valid otherwise false.
+ 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;
+ }
+}
diff --git a/src/Umbraco.Core/PropertyEditors/Validators/DictionaryConfigurationValidatorBase.cs b/src/Umbraco.Core/PropertyEditors/Validators/DictionaryConfigurationValidatorBase.cs
new file mode 100644
index 0000000000..0ed0af57e2
--- /dev/null
+++ b/src/Umbraco.Core/PropertyEditors/Validators/DictionaryConfigurationValidatorBase.cs
@@ -0,0 +1,33 @@
+using System.Diagnostics.CodeAnalysis;
+namespace Umbraco.Cms.Core.PropertyEditors.Validators;
+
+///
+/// Provides common functionality to validators that rely on data type configuration.
+///
+public abstract class DictionaryConfigurationValidatorBase
+{
+ ///
+ /// Retrieves a typed value from data type dictionary configuration for the provided key.
+ ///
+ /// The data type configuration.
+ /// The configuration key.
+ /// The configuration value (if found), otherwise zero.
+ /// True if the configured value was found.
+ protected static bool TryGetConfiguredValue(object? dataTypeConfiguration, string key, [NotNullWhen(true)] out TValue? value)
+ {
+ if (dataTypeConfiguration is not Dictionary configuration)
+ {
+ value = default;
+ return false;
+ }
+
+ if (configuration.TryGetValue(key, out object? obj) && obj is TValue castValue)
+ {
+ value = castValue;
+ return true;
+ }
+
+ value = default;
+ return false;
+ }
+}
diff --git a/src/Umbraco.Core/PropertyEditors/Validators/SimplePropertyConfigurationValidatorBase.cs b/src/Umbraco.Core/PropertyEditors/Validators/SimplePropertyConfigurationValidatorBase.cs
new file mode 100644
index 0000000000..debf8664dd
--- /dev/null
+++ b/src/Umbraco.Core/PropertyEditors/Validators/SimplePropertyConfigurationValidatorBase.cs
@@ -0,0 +1,18 @@
+using Umbraco.Cms.Core.Configuration.Models.Validation;
+
+namespace Umbraco.Cms.Core.PropertyEditors.Validators;
+
+///
+/// Provides common functionality to validators that rely on data type configuration.
+///
+/// The type to parse to.
+public abstract class SimplePropertyConfigurationValidatorBase : DictionaryConfigurationValidatorBase
+{
+ ///
+ /// Parses the raw property value into it's typed equivalent.
+ ///
+ /// The property value as a nullable object.
+ /// The parsed value.
+ /// True if the parse succeeded, otherwise false.
+ protected abstract bool TryParsePropertyValue(object? value, out TValue parsedValue);
+}
diff --git a/src/Umbraco.Infrastructure/PropertyEditors/SliderPropertyEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/SliderPropertyEditor.cs
index b67e4af0a2..5277a2f552 100644
--- a/src/Umbraco.Infrastructure/PropertyEditors/SliderPropertyEditor.cs
+++ b/src/Umbraco.Infrastructure/PropertyEditors/SliderPropertyEditor.cs
@@ -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;
///
-/// Represents a slider editor.
+/// Represents a slider editor.
///
[DataEditor(
Constants.PropertyEditors.Aliases.Slider,
@@ -38,18 +46,29 @@ public class SliderPropertyEditor : DataEditor
protected override IConfigurationEditor CreateConfigurationEditor() =>
new SliderConfigurationEditor(_ioHelper);
+ ///
+ /// Defines the value editor for the slider property editor.
+ ///
internal class SliderPropertyValueEditor : DataValueEditor
{
private readonly IJsonSerializer _jsonSerializer;
+ ///
+ /// Initializes a new instance of the class.
+ ///
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));
+ }
+ ///
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;
}
+ ///
public override object? FromEditor(ContentPropertyData editorValue, object? currentValue)
=> editorValue.Value is not null && _jsonSerializer.TryDeserialize(editorValue.Value, out SliderRange? sliderRange)
? sliderRange.ToString()
: null;
+ ///
+ /// Represents a slider value.
+ ///
internal class SliderRange
{
+ ///
+ /// Gets or sets the slider range from value.
+ ///
public decimal From { get; set; }
+ ///
+ /// Gets or sets the slide range to value.
+ ///
public decimal To { get; set; }
- public override string ToString() => From == To ? $"{From.ToString(CultureInfo.InvariantCulture)}" : $"{From.ToString(CultureInfo.InvariantCulture)},{To.ToString(CultureInfo.InvariantCulture)}";
+ ///
+ public override string ToString()
+ => From == To
+ ? $"{From.ToString(CultureInfo.InvariantCulture)}"
+ : $"{From.ToString(CultureInfo.InvariantCulture)},{To.ToString(CultureInfo.InvariantCulture)}";
+ }
+
+ ///
+ /// Base validator for the slider property editor validation against data type configured values.
+ ///
+ internal abstract class SliderPropertyConfigurationValidatorBase
+ {
+ ///
+ /// The configuration key for the minimum value.
+ ///
+ protected const string ConfigurationKeyMinValue = "minVal";
+
+ ///
+ /// The configuration key for the maximum value.
+ ///
+ protected const string ConfigurationKeyMaxValue = "maxVal";
+
+ ///
+ /// The configuration key for the step value.
+ ///
+ protected const string ConfigurationKeyStepValue = "step";
+
+ ///
+ /// The configuration key for the enable range value.
+ ///
+ protected const string ConfigurationKeyEnableRangeValue = "enableRange";
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ protected SliderPropertyConfigurationValidatorBase(ILocalizedTextService localizedTextService) => LocalizedTextService = localizedTextService;
+
+ ///
+ /// Gets the .
+ ///
+ protected ILocalizedTextService LocalizedTextService { get; }
+
+ ///
+ /// Parses a from the provided value.
+ ///
+ 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();
+ }
+
+ ///
+ /// Validates the range configuration for the slider property editor.
+ ///
+ internal class RangeValidator : SliderPropertyConfigurationValidatorBase, IValueValidator
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public RangeValidator(ILocalizedTextService localizedTextService)
+ : base(localizedTextService)
+ {
+ }
+
+ ///
+ public IEnumerable 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"]);
+ }
+ }
+ }
+
+ ///
+ /// Validates the min/max configuration for the slider property editor.
+ ///
+ internal class MinMaxValidator : SliderPropertyConfigurationValidatorBase, IValueValidator
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public MinMaxValidator(ILocalizedTextService localizedTextService)
+ : base(localizedTextService)
+ {
+ }
+
+ ///
+ public IEnumerable 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"]);
+ }
+ }
+ }
+
+ ///
+ /// Validates the step configuration for the slider property editor.
+ ///
+ internal class StepValidator : SliderPropertyConfigurationValidatorBase, IValueValidator
+ {
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ public StepValidator(ILocalizedTextService localizedTextService)
+ : base(localizedTextService)
+ {
+ }
+
+ ///
+ public IEnumerable 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"]);
+ }
+ }
}
}
}
diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/DecimalValueEditorTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/DecimalValueEditorTests.cs
index 18254d923e..59569c7583 100644
--- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/DecimalValueEditorTests.cs
+++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/DecimalValueEditorTests.cs
@@ -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();
+ localizedTextServiceMock.Setup(x => x.Localize(
+ It.IsAny(),
+ It.IsAny(),
+ It.IsAny(),
+ It.IsAny>()))
+ .Returns((string key, string alias, CultureInfo culture, IDictionary args) => $"{key}_{alias}");
+ return new DecimalPropertyEditor.DecimalPropertyValueEditor(
Mock.Of(),
Mock.Of(),
Mock.Of(),
- new DataEditorAttribute("alias"));
- return valueEditor;
+ new DataEditorAttribute("alias"),
+ localizedTextServiceMock.Object)
+ {
+ ConfigurationObject = new Dictionary
+ {
+ { "min", 1.1 },
+ { "max", 1.9 },
+ { "step", 0.2 }
+ }
+ };
}
}
diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/IntegerValueEditorTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/IntegerValueEditorTests.cs
new file mode 100644
index 0000000000..d9545b8126
--- /dev/null
+++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/IntegerValueEditorTests.cs
@@ -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