Skip to content
5 changes: 5 additions & 0 deletions src/Umbraco.Core/EmbeddedResources/Lang/da.xml
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down
5 changes: 5 additions & 0 deletions src/Umbraco.Core/EmbeddedResources/Lang/en.xml
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down
5 changes: 5 additions & 0 deletions src/Umbraco.Core/EmbeddedResources/Lang/en_us.xml
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down
127 changes: 123 additions & 4 deletions src/Umbraco.Core/PropertyEditors/DecimalPropertyEditor.cs
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -32,19 +37,29 @@ protected override IDataValueEditor CreateValueEditor()
/// <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);

Expand All @@ -54,5 +69,109 @@ public DecimalPropertyValueEditor(
: 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"]);
}
}
}
}
}
127 changes: 124 additions & 3 deletions src/Umbraco.Core/PropertyEditors/IntegerPropertyEditor.cs
Original file line number Diff line number Diff line change
@@ -1,22 +1,30 @@
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,
ValueType = ValueTypes.Integer,
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;
Expand All @@ -28,19 +36,28 @@ protected override IDataValueEditor CreateValueEditor()
/// <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);

Expand All @@ -50,5 +67,109 @@ public IntegerPropertyValueEditor(
: 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"]);
}
}
}
}
}
3 changes: 3 additions & 0 deletions src/Umbraco.Core/PropertyEditors/SliderConfiguration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,7 @@ public class SliderConfiguration

[ConfigurationField("maxVal")]
public decimal MaximumValue { get; set; }

[ConfigurationField("step")]
public decimal Step { get; set; }
}
Loading