diff --git a/src/Umbraco.Core/Models/PropertyTagsExtensions.cs b/src/Umbraco.Core/Models/PropertyTagsExtensions.cs index 390f644831..7168f99078 100644 --- a/src/Umbraco.Core/Models/PropertyTagsExtensions.cs +++ b/src/Umbraco.Core/Models/PropertyTagsExtensions.cs @@ -68,12 +68,15 @@ namespace Umbraco.Extensions switch (storageType) { case TagsStorageType.Csv: - property.SetValue(string.Join(delimiter.ToString(), currentTags.Union(trimmedTags)), culture); // csv string + property.SetValue(string.Join(delimiter.ToString(), currentTags.Union(trimmedTags)).NullOrWhiteSpaceAsNull(), culture); // csv string break; case TagsStorageType.Json: + var updatedTags = currentTags.Union(trimmedTags).ToArray(); + var updatedValue = updatedTags.Length == 0 ? null : serializer.Serialize(updatedTags); + property.SetValue(updatedValue, culture); // json array + break; property.SetValue(serializer.Serialize(currentTags.Union(trimmedTags).ToArray()), culture); // json array - break; } } else @@ -81,7 +84,7 @@ namespace Umbraco.Extensions switch (storageType) { case TagsStorageType.Csv: - property.SetValue(string.Join(delimiter.ToString(), trimmedTags), culture); // csv string + property.SetValue(string.Join(delimiter.ToString(), trimmedTags).NullOrWhiteSpaceAsNull(), culture); // csv string break; case TagsStorageType.Json: @@ -124,11 +127,13 @@ namespace Umbraco.Extensions switch (storageType) { case TagsStorageType.Csv: - property.SetValue(string.Join(delimiter.ToString(), currentTags.Except(trimmedTags)), culture); // csv string + property.SetValue(string.Join(delimiter.ToString(), currentTags.Except(trimmedTags)).NullOrWhiteSpaceAsNull(), culture); // csv string break; case TagsStorageType.Json: - property.SetValue(serializer.Serialize(currentTags.Except(trimmedTags).ToArray()), culture); // json array + var updatedTags = currentTags.Except(trimmedTags).ToArray(); + var updatedValue = updatedTags.Length == 0 ? null : serializer.Serialize(updatedTags); + property.SetValue(updatedValue, culture); // json array break; } } @@ -160,7 +165,7 @@ namespace Umbraco.Extensions case TagsStorageType.Json: try { - return serializer.Deserialize(value).Select(x => x.ToString().Trim()); + return serializer.Deserialize(value).Select(x => x.Trim()); } catch (Exception) { diff --git a/src/Umbraco.Core/PropertyEditors/DataValueEditor.cs b/src/Umbraco.Core/PropertyEditors/DataValueEditor.cs index 07607be2b0..6d3e40067e 100644 --- a/src/Umbraco.Core/PropertyEditors/DataValueEditor.cs +++ b/src/Umbraco.Core/PropertyEditors/DataValueEditor.cs @@ -197,6 +197,7 @@ namespace Umbraco.Cms.Core.PropertyEditors default: throw new ArgumentOutOfRangeException(); } + return value.TryConvertTo(valueType); } @@ -232,6 +233,7 @@ namespace Umbraco.Cms.Core.PropertyEditors StaticApplicationLogging.Logger.LogWarning("The value {EditorValue} cannot be converted to the type {StorageTypeValue}", editorValue.Value, ValueTypes.ToStorageType(ValueType)); return null; } + return result.Result; } diff --git a/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorPropertyEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorPropertyEditor.cs index b4b46f2f7c..7a2d626fcd 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorPropertyEditor.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorPropertyEditor.cs @@ -238,7 +238,7 @@ namespace Umbraco.Cms.Core.PropertyEditors MapBlockItemData(blockEditorData.BlockValue.SettingsData); // return json - return JsonConvert.SerializeObject(blockEditorData.BlockValue); + return JsonConvert.SerializeObject(blockEditorData.BlockValue, Formatting.None); } #endregion diff --git a/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorPropertyHandler.cs b/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorPropertyHandler.cs index cfa1c4b3cb..7248a7f5b0 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorPropertyHandler.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/BlockEditorPropertyHandler.cs @@ -52,7 +52,7 @@ namespace Umbraco.Cms.Core.PropertyEditors UpdateBlockListRecursively(blockListValue, createGuid); - return JsonConvert.SerializeObject(blockListValue.BlockValue); + return JsonConvert.SerializeObject(blockListValue.BlockValue, Formatting.None); } private void UpdateBlockListRecursively(BlockEditorData blockListData, Func createGuid) diff --git a/src/Umbraco.Infrastructure/PropertyEditors/ColorPickerConfigurationEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/ColorPickerConfigurationEditor.cs index 36f11f5ce8..0c9cd40995 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/ColorPickerConfigurationEditor.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/ColorPickerConfigurationEditor.cs @@ -6,6 +6,7 @@ using System.ComponentModel.DataAnnotations; using System.Linq; using System.Runtime.Serialization; using System.Text.RegularExpressions; +using Newtonsoft.Json; using Newtonsoft.Json.Linq; using Umbraco.Cms.Core.IO; using Umbraco.Cms.Core.Serialization; diff --git a/src/Umbraco.Infrastructure/PropertyEditors/ComplexPropertyEditorContentNotificationHandler.cs b/src/Umbraco.Infrastructure/PropertyEditors/ComplexPropertyEditorContentNotificationHandler.cs index e4039b6cee..ac7d5a4ef4 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/ComplexPropertyEditorContentNotificationHandler.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/ComplexPropertyEditorContentNotificationHandler.cs @@ -2,6 +2,8 @@ // See LICENSE for more details. using System.Collections.Generic; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; using Umbraco.Cms.Core.Events; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Notifications; @@ -41,11 +43,13 @@ namespace Umbraco.Cms.Core.PropertyEditors foreach (var cultureVal in propVals) { // Remove keys from published value & any nested properties - var updatedPublishedVal = FormatPropertyValue(cultureVal.PublishedValue?.ToString(), onlyMissingKeys); + var publishedValue = cultureVal.PublishedValue is JToken jsonPublishedValue ? jsonPublishedValue.ToString(Formatting.None) : cultureVal.PublishedValue?.ToString(); + var updatedPublishedVal = FormatPropertyValue(publishedValue, onlyMissingKeys).NullOrWhiteSpaceAsNull(); cultureVal.PublishedValue = updatedPublishedVal; // Remove keys from edited/draft value & any nested properties - var updatedEditedVal = FormatPropertyValue(cultureVal.EditedValue?.ToString(), onlyMissingKeys); + var editedValue = cultureVal.EditedValue is JToken jsonEditedValue ? jsonEditedValue.ToString(Formatting.None) : cultureVal.EditedValue?.ToString(); + var updatedEditedVal = FormatPropertyValue(editedValue, onlyMissingKeys).NullOrWhiteSpaceAsNull(); cultureVal.EditedValue = updatedEditedVal; } } diff --git a/src/Umbraco.Infrastructure/PropertyEditors/GridPropertyEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/GridPropertyEditor.cs index e3a6987110..52a1e50fc4 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/GridPropertyEditor.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/GridPropertyEditor.cs @@ -139,7 +139,7 @@ namespace Umbraco.Cms.Core.PropertyEditors } // Convert back to raw JSON for persisting - return JsonConvert.SerializeObject(grid); + return JsonConvert.SerializeObject(grid, Formatting.None); } /// diff --git a/src/Umbraco.Infrastructure/PropertyEditors/ImageCropperPropertyEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/ImageCropperPropertyEditor.cs index 95023cecf6..8a38a85551 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/ImageCropperPropertyEditor.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/ImageCropperPropertyEditor.cs @@ -211,7 +211,7 @@ namespace Umbraco.Cms.Core.PropertyEditors var sourcePath = _mediaFileManager.FileSystem.GetRelativePath(src); var copyPath = _mediaFileManager.CopyFile(notification.Copy, property.PropertyType, sourcePath); jo["src"] = _mediaFileManager.FileSystem.GetUrl(copyPath); - notification.Copy.SetValue(property.Alias, jo.ToString(), propertyValue.Culture, propertyValue.Segment); + notification.Copy.SetValue(property.Alias, jo.ToString(Formatting.None), propertyValue.Culture, propertyValue.Segment); isUpdated = true; } } @@ -272,17 +272,11 @@ namespace Umbraco.Cms.Core.PropertyEditors // it can happen when an image is uploaded via the folder browser, in which case // the property value will be the file source eg '/media/23454/hello.jpg' and we // are fixing that anomaly here - does not make any sense at all but... bah... - - var dt = _dataTypeService.GetDataType(property.PropertyType.DataTypeId); - var config = dt?.ConfigurationAs(); src = svalue; - var json = new + property.SetValue(JsonConvert.SerializeObject(new { - src = svalue, - crops = config == null ? Array.Empty() : config.Crops - }; - - property.SetValue(JsonConvert.SerializeObject(json), pvalue.Culture, pvalue.Segment); + src = svalue + }, Formatting.None), pvalue.Culture, pvalue.Segment); } else { diff --git a/src/Umbraco.Infrastructure/PropertyEditors/ImageCropperPropertyValueEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/ImageCropperPropertyValueEditor.cs index 10a99ccd99..5ae10bc178 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/ImageCropperPropertyValueEditor.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/ImageCropperPropertyValueEditor.cs @@ -205,6 +205,10 @@ namespace Umbraco.Cms.Core.PropertyEditors { src = val, crops = crops + },new JsonSerializerSettings() + { + Formatting = Formatting.None, + NullValueHandling = NullValueHandling.Ignore }); } } diff --git a/src/Umbraco.Infrastructure/PropertyEditors/MediaPicker3PropertyEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/MediaPicker3PropertyEditor.cs index 45aa507a54..2cfe5dd56e 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/MediaPicker3PropertyEditor.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/MediaPicker3PropertyEditor.cs @@ -12,6 +12,7 @@ using Newtonsoft.Json; using System; using System.Linq; using System.Runtime.Serialization; +using Newtonsoft.Json.Linq; using Umbraco.Extensions; namespace Umbraco.Cms.Core.PropertyEditors @@ -53,23 +54,55 @@ namespace Umbraco.Cms.Core.PropertyEditors internal class MediaPicker3PropertyValueEditor : DataValueEditor, IDataValueReference { private readonly IJsonSerializer _jsonSerializer; + private readonly IDataTypeService _dataTypeService; public MediaPicker3PropertyValueEditor( ILocalizedTextService localizedTextService, IShortStringHelper shortStringHelper, IJsonSerializer jsonSerializer, IIOHelper ioHelper, - DataEditorAttribute attribute) + DataEditorAttribute attribute, + IDataTypeService dataTypeService) : base(localizedTextService, shortStringHelper, jsonSerializer, ioHelper, attribute) { _jsonSerializer = jsonSerializer; + _dataTypeService = dataTypeService; } public override object ToEditor(IProperty property, string culture = null, string segment = null) { var value = property.GetValue(culture, segment); - return Deserialize(_jsonSerializer, value); + var dtos = Deserialize(_jsonSerializer, value).ToList(); + + var dataType = _dataTypeService.GetDataType(property.PropertyType.DataTypeId); + if (dataType?.Configuration != null) + { + var configuration = dataType.ConfigurationAs(); + + foreach (var dto in dtos) + { + dto.ApplyConfiguration(configuration); + } + } + + return dtos; + } + + public override object FromEditor(ContentPropertyData editorValue, object currentValue) + { + if (editorValue.Value is JArray dtos) + { + // Clean up redundant/default data + foreach (var dto in dtos.Values()) + { + MediaWithCropsDto.Prune(dto); + } + + return dtos.ToString(Formatting.None); + } + + return base.FromEditor(editorValue, currentValue); } /// @@ -142,6 +175,52 @@ namespace Umbraco.Cms.Core.PropertyEditors [DataMember(Name = "focalPoint")] public ImageCropperValue.ImageCropperFocalPoint FocalPoint { get; set; } + + + /// + /// Applies the configuration to ensure only valid crops are kept and have the correct width/height. + /// + /// The configuration. + public void ApplyConfiguration(MediaPicker3Configuration configuration) + { + var crops = new List(); + + var configuredCrops = configuration?.Crops; + if (configuredCrops != null) + { + foreach (var configuredCrop in configuredCrops) + { + var crop = Crops?.FirstOrDefault(x => x.Alias == configuredCrop.Alias); + + crops.Add(new ImageCropperValue.ImageCropperCrop + { + Alias = configuredCrop.Alias, + Width = configuredCrop.Width, + Height = configuredCrop.Height, + Coordinates = crop?.Coordinates + }); + } + } + + Crops = crops; + + if (configuration?.EnableLocalFocalPoint == false) + { + FocalPoint = null; + } + } + + /// + /// Removes redundant crop data/default focal point. + /// + /// The media with crops DTO. + /// + /// The cleaned up value. + /// + /// + /// Because the DTO uses the same JSON keys as the image cropper value for crops and focal point, we can re-use the prune method. + /// + public static void Prune(JObject value) => ImageCropperValue.Prune(value); } } } diff --git a/src/Umbraco.Infrastructure/PropertyEditors/MultiUrlPickerValueEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/MultiUrlPickerValueEditor.cs index c1d8aa33f8..f6d8a598b0 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/MultiUrlPickerValueEditor.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/MultiUrlPickerValueEditor.cs @@ -141,6 +141,7 @@ namespace Umbraco.Cms.Core.PropertyEditors private static readonly JsonSerializerSettings LinkDisplayJsonSerializerSettings = new JsonSerializerSettings { + Formatting = Formatting.None, NullValueHandling = NullValueHandling.Ignore }; @@ -150,13 +151,17 @@ namespace Umbraco.Cms.Core.PropertyEditors if (string.IsNullOrEmpty(value)) { - return string.Empty; + return null; } try { + var links = JsonConvert.DeserializeObject>(value); + if (links.Count == 0) + return null; + return JsonConvert.SerializeObject( - from link in JsonConvert.DeserializeObject>(value) + from link in links select new MultiUrlPickerValueEditor.LinkDto { Name = link.Name, @@ -164,8 +169,8 @@ namespace Umbraco.Cms.Core.PropertyEditors Target = link.Target, Udi = link.Udi, Url = link.Udi == null ? link.Url : null, // only save the URL for external links - }, LinkDisplayJsonSerializerSettings - ); + }, + LinkDisplayJsonSerializerSettings); } catch (Exception ex) { diff --git a/src/Umbraco.Infrastructure/PropertyEditors/MultipleTextStringPropertyEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/MultipleTextStringPropertyEditor.cs index c3a5bae383..97cb677d4c 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/MultipleTextStringPropertyEditor.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/MultipleTextStringPropertyEditor.cs @@ -80,7 +80,7 @@ namespace Umbraco.Cms.Core.PropertyEditors public override object FromEditor(ContentPropertyData editorValue, object currentValue) { var asArray = editorValue.Value as JArray; - if (asArray == null) + if (asArray == null || asArray.HasValues == false) { return null; } diff --git a/src/Umbraco.Infrastructure/PropertyEditors/MultipleValueEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/MultipleValueEditor.cs index c0e544b50c..47f8c9a169 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/MultipleValueEditor.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/MultipleValueEditor.cs @@ -59,14 +59,19 @@ namespace Umbraco.Cms.Core.PropertyEditors public override object FromEditor(ContentPropertyData editorValue, object currentValue) { var json = editorValue.Value as JArray; - if (json == null) + if (json == null || json.HasValues == false) { return null; } var values = json.Select(item => item.Value()).ToArray(); - return JsonConvert.SerializeObject(values); + if (values.Length == 0) + { + return null; + } + + return JsonConvert.SerializeObject(values, Formatting.None); } } } diff --git a/src/Umbraco.Infrastructure/PropertyEditors/NestedContentPropertyEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/NestedContentPropertyEditor.cs index 80ed34e6e1..835431820c 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/NestedContentPropertyEditor.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/NestedContentPropertyEditor.cs @@ -105,7 +105,7 @@ namespace Umbraco.Cms.Core.PropertyEditors var rows = _nestedContentValues.GetPropertyValues(propertyValue); if (rows.Count == 0) - return string.Empty; + return null; foreach (var row in rows.ToList()) { @@ -136,7 +136,7 @@ namespace Umbraco.Cms.Core.PropertyEditors } } - return JsonConvert.SerializeObject(rows).ToXmlString(); + return JsonConvert.SerializeObject(rows, Formatting.None).ToXmlString(); } #endregion @@ -231,7 +231,7 @@ namespace Umbraco.Cms.Core.PropertyEditors var rows = _nestedContentValues.GetPropertyValues(editorValue.Value); if (rows.Count == 0) - return string.Empty; + return null; foreach (var row in rows.ToList()) { @@ -256,7 +256,7 @@ namespace Umbraco.Cms.Core.PropertyEditors } // return json - return JsonConvert.SerializeObject(rows); + return JsonConvert.SerializeObject(rows, Formatting.None); } #endregion diff --git a/src/Umbraco.Infrastructure/PropertyEditors/NestedContentPropertyHandler.cs b/src/Umbraco.Infrastructure/PropertyEditors/NestedContentPropertyHandler.cs index 1bc5dd2f4b..aa825bb0f8 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/NestedContentPropertyHandler.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/NestedContentPropertyHandler.cs @@ -3,6 +3,7 @@ using System; using System.Linq; +using Newtonsoft.Json; using Newtonsoft.Json.Linq; using Umbraco.Extensions; @@ -32,7 +33,7 @@ namespace Umbraco.Cms.Core.PropertyEditors UpdateNestedContentKeysRecursively(complexEditorValue, onlyMissingKeys, createGuid); - return complexEditorValue.ToString(); + return complexEditorValue.ToString(Formatting.None); } private void UpdateNestedContentKeysRecursively(JToken json, bool onlyMissingKeys, Func createGuid) @@ -65,7 +66,7 @@ namespace Umbraco.Cms.Core.PropertyEditors var parsed = JToken.Parse(propVal); UpdateNestedContentKeysRecursively(parsed, onlyMissingKeys, createGuid); // set the value to the updated one - prop.Value = parsed.ToString(); + prop.Value = parsed.ToString(Formatting.None); } } } diff --git a/src/Umbraco.Infrastructure/PropertyEditors/RichTextPropertyEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/RichTextPropertyEditor.cs index 50b6d7a881..1f05da3bde 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/RichTextPropertyEditor.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/RichTextPropertyEditor.cs @@ -157,7 +157,7 @@ namespace Umbraco.Cms.Core.PropertyEditors var editorValueWithMediaUrlsRemoved = _imageSourceParser.RemoveImageSources(parseAndSavedTempImages); var parsed = MacroTagParser.FormatRichTextContentForPersistence(editorValueWithMediaUrlsRemoved); - return parsed; + return parsed.NullOrWhiteSpaceAsNull(); } /// diff --git a/src/Umbraco.Infrastructure/PropertyEditors/TagsPropertyEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/TagsPropertyEditor.cs index 4683851936..42f6424bfa 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/TagsPropertyEditor.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/TagsPropertyEditor.cs @@ -73,7 +73,7 @@ namespace Umbraco.Cms.Core.PropertyEditors if (editorValue.Value is JArray json) { - return json.Select(x => x.Value()); + return json.HasValues ? json.Select(x => x.Value()) : null; } if (string.IsNullOrWhiteSpace(value) == false) diff --git a/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/ImageCropperValue.cs b/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/ImageCropperValue.cs index c0efaac4ae..97f1b8398c 100644 --- a/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/ImageCropperValue.cs +++ b/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/ImageCropperValue.cs @@ -7,6 +7,7 @@ using System.ComponentModel; using System.Linq; using System.Runtime.Serialization; using Newtonsoft.Json; +using Newtonsoft.Json.Linq; using Umbraco.Cms.Core.Media; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Strings; @@ -122,13 +123,19 @@ namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters /// /// public bool HasFocalPoint() - => FocalPoint != null && (FocalPoint.Left != 0.5m || FocalPoint.Top != 0.5m); + => FocalPoint is ImageCropperFocalPoint focalPoint && (focalPoint.Left != 0.5m || focalPoint.Top != 0.5m); + + /// + /// Determines whether the value has crops. + /// + public bool HasCrops() + => Crops is IEnumerable crops && crops.Any(); /// /// Determines whether the value has a specified crop. /// public bool HasCrop(string alias) - => Crops != null && Crops.Any(x => x.Alias == alias); + => Crops is IEnumerable crops && crops.Any(x => x.Alias == alias); /// /// Determines whether the value has a source image. @@ -167,6 +174,51 @@ namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters }; } + /// + /// Removes redundant crop data/default focal point. + /// + /// The image cropper value. + /// + /// The cleaned up value. + /// + public static void Prune(JObject value) + { + if (value is null) throw new ArgumentNullException(nameof(value)); + + if (value.TryGetValue("crops", out var crops)) + { + if (crops.HasValues) + { + foreach (var crop in crops.Values().ToList()) + { + if (crop.TryGetValue("coordinates", out var coordinates) == false || coordinates.HasValues == false) + { + // Remove crop without coordinates + crop.Remove(); + continue; + } + + // Width/height are already stored in the crop configuration + crop.Remove("width"); + crop.Remove("height"); + } + } + + if (crops.HasValues == false) + { + // Remove empty crops + value.Remove("crops"); + } + } + + if (value.TryGetValue("focalPoint", out var focalPoint) && + (focalPoint.HasValues == false || (focalPoint.Value("top") == 0.5m && focalPoint.Value("left") == 0.5m))) + { + // Remove empty/default focal point + value.Remove("focalPoint"); + } + } + #region IEquatable /// diff --git a/src/Umbraco.Infrastructure/Serialization/ConfigurationEditorJsonSerializer.cs b/src/Umbraco.Infrastructure/Serialization/ConfigurationEditorJsonSerializer.cs index e8e32fe7da..ab978c903e 100644 --- a/src/Umbraco.Infrastructure/Serialization/ConfigurationEditorJsonSerializer.cs +++ b/src/Umbraco.Infrastructure/Serialization/ConfigurationEditorJsonSerializer.cs @@ -12,6 +12,8 @@ namespace Umbraco.Cms.Infrastructure.Serialization { JsonSerializerSettings.Converters.Add(new FuzzyBooleanConverter()); JsonSerializerSettings.ContractResolver = new ConfigurationCustomContractResolver(); + JsonSerializerSettings.Formatting = Formatting.None; + JsonSerializerSettings.NullValueHandling = NullValueHandling.Ignore; } private class ConfigurationCustomContractResolver : DefaultContractResolver diff --git a/src/Umbraco.Infrastructure/Serialization/JsonNetSerializer.cs b/src/Umbraco.Infrastructure/Serialization/JsonNetSerializer.cs index a728630680..5c5377c0a1 100644 --- a/src/Umbraco.Infrastructure/Serialization/JsonNetSerializer.cs +++ b/src/Umbraco.Infrastructure/Serialization/JsonNetSerializer.cs @@ -14,7 +14,8 @@ namespace Umbraco.Cms.Infrastructure.Serialization Converters = new List() { new StringEnumConverter() - } + }, + Formatting = Formatting.None }; public string Serialize(object input) { diff --git a/src/Umbraco.Infrastructure/Serialization/JsonToStringConverter.cs b/src/Umbraco.Infrastructure/Serialization/JsonToStringConverter.cs index e4c17ab918..2e7416b2d2 100644 --- a/src/Umbraco.Infrastructure/Serialization/JsonToStringConverter.cs +++ b/src/Umbraco.Infrastructure/Serialization/JsonToStringConverter.cs @@ -22,7 +22,7 @@ namespace Umbraco.Cms.Infrastructure.Serialization } // Load JObject from stream JObject jObject = JObject.Load(reader); - return jObject.ToString(); + return jObject.ToString(Formatting.None); } public override bool CanConvert(Type objectType) diff --git a/src/Umbraco.Web.BackOffice/Controllers/AuthenticationController.cs b/src/Umbraco.Web.BackOffice/Controllers/AuthenticationController.cs index 30ad3f75ae..cdc9d2e913 100644 --- a/src/Umbraco.Web.BackOffice/Controllers/AuthenticationController.cs +++ b/src/Umbraco.Web.BackOffice/Controllers/AuthenticationController.cs @@ -678,7 +678,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers r = code }); - // Construct full URL using configured application URL (which will fall back to request) + // Construct full URL using configured application URL (which will fall back to current request) Uri applicationUri = _httpContextAccessor.GetRequiredHttpContext().Request.GetApplicationUri(_webRoutingSettings); var callbackUri = new Uri(applicationUri, action); return callbackUri.ToString(); diff --git a/src/Umbraco.Web.BackOffice/Trees/ContentTreeController.cs b/src/Umbraco.Web.BackOffice/Trees/ContentTreeController.cs index 3dd303d8f7..a41f396faf 100644 --- a/src/Umbraco.Web.BackOffice/Trees/ContentTreeController.cs +++ b/src/Umbraco.Web.BackOffice/Trees/ContentTreeController.cs @@ -282,7 +282,12 @@ namespace Umbraco.Cms.Web.BackOffice.Trees if (_emailSender.CanSendRequiredEmail()) { - AddActionNode(item, menu, true, opensDialog: true); + menu.Items.Add(new MenuItem("notify", LocalizedTextService) + { + Icon = "megaphone", + SeparatorBefore = true, + OpensDialog = true + }); } if((item is DocumentEntitySlim documentEntity && documentEntity.IsContainer) == false) diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/entity.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/entity.resource.js index d94bb4e6be..ff05656a4e 100644 --- a/src/Umbraco.Web.UI.Client/src/common/resources/entity.resource.js +++ b/src/Umbraco.Web.UI.Client/src/common/resources/entity.resource.js @@ -152,12 +152,12 @@ function entityResource($q, $http, umbRequestHelper) { $http.post( umbRequestHelper.getApiUrl( "entityApiBaseUrl", - "GetUrlsByUdis", + "GetUrlsByIds", query), { - udis: udis + ids: ids }), - 'Failed to retrieve url map for udis ' + udis); + 'Failed to retrieve url map for ids ' + ids); }, getUrlByUdi: function (udi, culture) { diff --git a/src/Umbraco.Web/HealthCheck/Checks/Security/UmbracoApplicationUrlCheck.cs b/src/Umbraco.Web/HealthCheck/Checks/Security/UmbracoApplicationUrlCheck.cs new file mode 100644 index 0000000000..bc14f43235 --- /dev/null +++ b/src/Umbraco.Web/HealthCheck/Checks/Security/UmbracoApplicationUrlCheck.cs @@ -0,0 +1,109 @@ +using System; +using System.Collections.Generic; +using Umbraco.Core; +using Umbraco.Core.Configuration.UmbracoSettings; +using Umbraco.Core.IO; +using Umbraco.Core.Services; +using Umbraco.Web.HealthCheck.Checks.Config; + +namespace Umbraco.Web.HealthCheck.Checks.Security +{ + [HealthCheck( + "6708CA45-E96E-40B8-A40A-0607C1CA7F28", + "Application URL Configuration", + Description = "Checks if the Umbraco application URL is configured for your site.", + Group = "Security")] + public class UmbracoApplicationUrlCheck : HealthCheck + { + private readonly ILocalizedTextService _textService; + private readonly IRuntimeState _runtime; + private readonly IUmbracoSettingsSection _settings; + + private const string SetApplicationUrlAction = "setApplicationUrl"; + + public UmbracoApplicationUrlCheck(ILocalizedTextService textService, IRuntimeState runtime, IUmbracoSettingsSection settings) + { + _textService = textService; + _runtime = runtime; + _settings = settings; + } + + /// + /// Executes the action and returns its status + /// + /// + /// + public override HealthCheckStatus ExecuteAction(HealthCheckAction action) + { + switch (action.Alias) + { + case SetApplicationUrlAction: + return SetUmbracoApplicationUrl(); + default: + throw new InvalidOperationException("UmbracoApplicationUrlCheck action requested is either not executable or does not exist"); + } + } + + public override IEnumerable GetStatus() + { + //return the statuses + return new[] { CheckUmbracoApplicationUrl() }; + } + + private HealthCheckStatus CheckUmbracoApplicationUrl() + { + var url = _settings.WebRouting.UmbracoApplicationUrl; + + string resultMessage; + StatusResultType resultType; + var actions = new List(); + + if (url.IsNullOrWhiteSpace()) + { + resultMessage = _textService.Localize("healthcheck", "umbracoApplicationUrlCheckResultFalse"); + resultType = StatusResultType.Warning; + + actions.Add(new HealthCheckAction(SetApplicationUrlAction, Id) + { + Name = _textService.Localize("healthcheck", "umbracoApplicationUrlConfigureButton"), + Description = _textService.Localize("healthcheck", "umbracoApplicationUrlConfigureDescription") + }); + } + else + { + resultMessage = _textService.Localize("healthcheck", "umbracoApplicationUrlCheckResultTrue", new[] { url }); + resultType = StatusResultType.Success; + } + + return new HealthCheckStatus(resultMessage) + { + ResultType = resultType, + Actions = actions + }; + } + + private HealthCheckStatus SetUmbracoApplicationUrl() + { + var configFilePath = IOHelper.MapPath("~/config/umbracoSettings.config"); + const string xPath = "/settings/web.routing/@umbracoApplicationUrl"; + var configurationService = new ConfigurationService(configFilePath, xPath, _textService); + var urlValue = _runtime.ApplicationUrl.ToString(); + var updateConfigFile = configurationService.UpdateConfigFile(urlValue); + + if (updateConfigFile.Success) + { + return + new HealthCheckStatus(_textService.Localize("healthcheck", "umbracoApplicationUrlConfigureSuccess", new[] { urlValue })) + { + ResultType = StatusResultType.Success + }; + } + + return + new HealthCheckStatus(_textService.Localize("healthcheck", "umbracoApplicationUrlConfigureError", new[] { updateConfigFile.Result })) + { + ResultType = StatusResultType.Error + }; + } + } +} diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/NestedContentPropertyComponentTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/NestedContentPropertyComponentTests.cs index 0ada6a20dd..f32f252633 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/NestedContentPropertyComponentTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/PropertyEditors/NestedContentPropertyComponentTests.cs @@ -3,6 +3,7 @@ using System; using Newtonsoft.Json; +using Newtonsoft.Json.Linq; using NUnit.Framework; using Umbraco.Cms.Core.PropertyEditors; @@ -11,6 +12,11 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.PropertyEditors [TestFixture] public class NestedContentPropertyComponentTests { + private static void AreEqualJson(string expected, string actual) + { + Assert.AreEqual(JToken.Parse(expected), JToken.Parse(actual)); + } + [Test] public void Invalid_Json() { @@ -27,17 +33,17 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.PropertyEditors Guid GuidFactory() => guids[guidCounter++]; var json = @"[ - {""key"":""04a6dba8-813c-4144-8aca-86a3f24ebf08"",""name"":""Item 1"",""ncContentTypeAlias"":""nested"",""text"":""woot""}, - {""key"":""d8e214d8-c5a5-4b45-9b51-4050dd47f5fa"",""name"":""Item 2"",""ncContentTypeAlias"":""nested"",""text"":""zoot""} -]"; + {""key"":""04a6dba8-813c-4144-8aca-86a3f24ebf08"",""name"":""Item 1"",""ncContentTypeAlias"":""nested"",""text"":""woot""}, + {""key"":""d8e214d8-c5a5-4b45-9b51-4050dd47f5fa"",""name"":""Item 2"",""ncContentTypeAlias"":""nested"",""text"":""zoot""} + ]"; var expected = json .Replace("04a6dba8-813c-4144-8aca-86a3f24ebf08", guids[0].ToString()) .Replace("d8e214d8-c5a5-4b45-9b51-4050dd47f5fa", guids[1].ToString()); var component = new NestedContentPropertyHandler(); - var result = component.CreateNestedContentKeys(json, false, GuidFactory); + var actual = component.CreateNestedContentKeys(json, false, GuidFactory); - Assert.AreEqual(JsonConvert.DeserializeObject(expected).ToString(), JsonConvert.DeserializeObject(result).ToString()); + AreEqualJson(expected, actual); } [Test] @@ -48,29 +54,27 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.PropertyEditors Guid GuidFactory() => guids[guidCounter++]; var json = @"[{ - ""key"": ""04a6dba8-813c-4144-8aca-86a3f24ebf08"", - ""name"": ""Item 1"", - ""ncContentTypeAlias"": ""text"", - ""text"": ""woot"" - }, { - ""key"": ""d8e214d8-c5a5-4b45-9b51-4050dd47f5fa"", - ""name"": ""Item 2"", - ""ncContentTypeAlias"": ""list"", - ""text"": ""zoot"", - ""subItems"": [{ - ""key"": ""dccf550c-3a05-469e-95e1-a8f560f788c2"", - ""name"": ""Item 1"", - ""ncContentTypeAlias"": ""text"", - ""text"": ""woot"" - }, { - ""key"": ""fbde4288-8382-4e13-8933-ed9c160de050"", - ""name"": ""Item 2"", - ""ncContentTypeAlias"": ""text"", - ""text"": ""zoot"" - } - ] - } -]"; + ""key"": ""04a6dba8-813c-4144-8aca-86a3f24ebf08"", + ""name"": ""Item 1"", + ""ncContentTypeAlias"": ""text"", + ""text"": ""woot"" + }, { + ""key"": ""d8e214d8-c5a5-4b45-9b51-4050dd47f5fa"", + ""name"": ""Item 2"", + ""ncContentTypeAlias"": ""list"", + ""text"": ""zoot"", + ""subItems"": [{ + ""key"": ""dccf550c-3a05-469e-95e1-a8f560f788c2"", + ""name"": ""Item 1"", + ""ncContentTypeAlias"": ""text"", + ""text"": ""woot"" + }, { + ""key"": ""fbde4288-8382-4e13-8933-ed9c160de050"", + ""name"": ""Item 2"", + ""ncContentTypeAlias"": ""text"", + ""text"": ""zoot"" + }] + }]"; var expected = json .Replace("04a6dba8-813c-4144-8aca-86a3f24ebf08", guids[0].ToString()) @@ -79,9 +83,9 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.PropertyEditors .Replace("fbde4288-8382-4e13-8933-ed9c160de050", guids[3].ToString()); var component = new NestedContentPropertyHandler(); - var result = component.CreateNestedContentKeys(json, false, GuidFactory); + var actual = component.CreateNestedContentKeys(json, false, GuidFactory); - Assert.AreEqual(JsonConvert.DeserializeObject(expected).ToString(), JsonConvert.DeserializeObject(result).ToString()); + AreEqualJson(expected, actual); } [Test] @@ -93,7 +97,8 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.PropertyEditors // we need to ensure the escaped json is consistent with how it will be re-escaped after parsing // and this is how to do that, the result will also include quotes around it. - var subJsonEscaped = JsonConvert.ToString(JsonConvert.DeserializeObject(@"[{ + var subJsonEscaped = JsonConvert.ToString(JToken.Parse(@" + [{ ""key"": ""dccf550c-3a05-469e-95e1-a8f560f788c2"", ""name"": ""Item 1"", ""ncContentTypeAlias"": ""text"", @@ -104,21 +109,21 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.PropertyEditors ""ncContentTypeAlias"": ""text"", ""text"": ""zoot"" } - ]").ToString()); + ]").ToString(Formatting.None)); var json = @"[{ - ""key"": ""04a6dba8-813c-4144-8aca-86a3f24ebf08"", - ""name"": ""Item 1"", - ""ncContentTypeAlias"": ""text"", - ""text"": ""woot"" - }, { - ""key"": ""d8e214d8-c5a5-4b45-9b51-4050dd47f5fa"", - ""name"": ""Item 2"", - ""ncContentTypeAlias"": ""list"", - ""text"": ""zoot"", - ""subItems"":" + subJsonEscaped + @" - } -]"; + ""key"": ""04a6dba8-813c-4144-8aca-86a3f24ebf08"", + ""name"": ""Item 1"", + ""ncContentTypeAlias"": ""text"", + ""text"": ""woot"" + }, { + ""key"": ""d8e214d8-c5a5-4b45-9b51-4050dd47f5fa"", + ""name"": ""Item 2"", + ""ncContentTypeAlias"": ""list"", + ""text"": ""zoot"", + ""subItems"":" + subJsonEscaped + @" + } + ]"; var expected = json .Replace("04a6dba8-813c-4144-8aca-86a3f24ebf08", guids[0].ToString()) @@ -127,9 +132,9 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.PropertyEditors .Replace("fbde4288-8382-4e13-8933-ed9c160de050", guids[3].ToString()); var component = new NestedContentPropertyHandler(); - var result = component.CreateNestedContentKeys(json, false, GuidFactory); + var actual = component.CreateNestedContentKeys(json, false, GuidFactory); - Assert.AreEqual(JsonConvert.DeserializeObject(expected).ToString(), JsonConvert.DeserializeObject(result).ToString()); + AreEqualJson(expected, actual); } [Test] @@ -141,7 +146,7 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.PropertyEditors // we need to ensure the escaped json is consistent with how it will be re-escaped after parsing // and this is how to do that, the result will also include quotes around it. - var subJsonEscaped = JsonConvert.ToString(JsonConvert.DeserializeObject(@"[{ + var subJsonEscaped = JsonConvert.ToString(JToken.Parse(@"[{ ""key"": ""dccf550c-3a05-469e-95e1-a8f560f788c2"", ""name"": ""Item 1"", ""ncContentTypeAlias"": ""text"", @@ -152,7 +157,7 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.PropertyEditors ""ncContentTypeAlias"": ""text"", ""text"": ""zoot"" } - ]").ToString()); + ]").ToString(Formatting.None)); // Complex editor such as the grid var complexEditorJsonEscaped = @"{ @@ -231,9 +236,9 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.PropertyEditors .Replace("fbde4288-8382-4e13-8933-ed9c160de050", guids[3].ToString()); var component = new NestedContentPropertyHandler(); - var result = component.CreateNestedContentKeys(json, false, GuidFactory); + var actual = component.CreateNestedContentKeys(json, false, GuidFactory); - Assert.AreEqual(JsonConvert.DeserializeObject(expected).ToString(), JsonConvert.DeserializeObject(result).ToString()); + AreEqualJson(expected, actual); } [Test] @@ -252,10 +257,10 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.PropertyEditors var result = component.CreateNestedContentKeys(json, true, GuidFactory); // Ensure the new GUID is put in a key into the JSON - Assert.IsTrue(JsonConvert.DeserializeObject(result).ToString().Contains(guids[0].ToString())); + Assert.IsTrue(result.Contains(guids[0].ToString())); // Ensure that the original key is NOT changed/modified & still exists - Assert.IsTrue(JsonConvert.DeserializeObject(result).ToString().Contains("04a6dba8-813c-4144-8aca-86a3f24ebf08")); + Assert.IsTrue(result.Contains("04a6dba8-813c-4144-8aca-86a3f24ebf08")); } [Test] @@ -267,7 +272,7 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.PropertyEditors // we need to ensure the escaped json is consistent with how it will be re-escaped after parsing // and this is how to do that, the result will also include quotes around it. - var subJsonEscaped = JsonConvert.ToString(JsonConvert.DeserializeObject(@"[{ + var subJsonEscaped = JsonConvert.ToString(JToken.Parse(@"[{ ""name"": ""Item 1"", ""ncContentTypeAlias"": ""text"", ""text"": ""woot"" @@ -276,7 +281,7 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.PropertyEditors ""ncContentTypeAlias"": ""text"", ""text"": ""zoot"" } - ]").ToString()); + ]").ToString(Formatting.None)); var json = @"[{ ""name"": ""Item 1 was copied and has no key"", @@ -295,9 +300,9 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.PropertyEditors var result = component.CreateNestedContentKeys(json, true, GuidFactory); // Ensure the new GUID is put in a key into the JSON for each item - Assert.IsTrue(JsonConvert.DeserializeObject(result).ToString().Contains(guids[0].ToString())); - Assert.IsTrue(JsonConvert.DeserializeObject(result).ToString().Contains(guids[1].ToString())); - Assert.IsTrue(JsonConvert.DeserializeObject(result).ToString().Contains(guids[2].ToString())); + Assert.IsTrue(result.Contains(guids[0].ToString())); + Assert.IsTrue(result.Contains(guids[1].ToString())); + Assert.IsTrue(result.Contains(guids[2].ToString())); } [Test] @@ -309,7 +314,7 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.PropertyEditors // we need to ensure the escaped json is consistent with how it will be re-escaped after parsing // and this is how to do that, the result will also include quotes around it. - var subJsonEscaped = JsonConvert.ToString(JsonConvert.DeserializeObject(@"[{ + var subJsonEscaped = JsonConvert.ToString(JToken.Parse(@"[{ ""key"": ""dccf550c-3a05-469e-95e1-a8f560f788c2"", ""name"": ""Item 1"", ""ncContentTypeAlias"": ""text"", @@ -319,7 +324,7 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.PropertyEditors ""ncContentTypeAlias"": ""text"", ""text"": ""zoot"" } - ]").ToString()); + ]").ToString(Formatting.None)); // Complex editor such as the grid var complexEditorJsonEscaped = @"{ @@ -394,8 +399,8 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.PropertyEditors var result = component.CreateNestedContentKeys(json, true, GuidFactory); // Ensure the new GUID is put in a key into the JSON for each item - Assert.IsTrue(JsonConvert.DeserializeObject(result).ToString().Contains(guids[0].ToString())); - Assert.IsTrue(JsonConvert.DeserializeObject(result).ToString().Contains(guids[1].ToString())); + Assert.IsTrue(result.Contains(guids[0].ToString())); + Assert.IsTrue(result.Contains(guids[1].ToString())); } } } diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Web.Common/ImageCropperTest.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Web.Common/ImageCropperTest.cs index f00225e7b4..f5d5d7c766 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Web.Common/ImageCropperTest.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Web.Common/ImageCropperTest.cs @@ -22,13 +22,13 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Web.Common { private const string CropperJson1 = "{\"focalPoint\": {\"left\": 0.96,\"top\": 0.80827067669172936},\"src\": \"/media/1005/img_0671.jpg\",\"crops\": [{\"alias\":\"thumb\",\"width\": 100,\"height\": 100,\"coordinates\": {\"x1\": 0.58729977382575338,\"y1\": 0.055768992440203169,\"x2\": 0,\"y2\": 0.32457553600198386}}]}"; private const string CropperJson2 = "{\"focalPoint\": {\"left\": 0.98,\"top\": 0.80827067669172936},\"src\": \"/media/1005/img_0672.jpg\",\"crops\": [{\"alias\":\"thumb\",\"width\": 100,\"height\": 100,\"coordinates\": {\"x1\": 0.58729977382575338,\"y1\": 0.055768992440203169,\"x2\": 0,\"y2\": 0.32457553600198386}}]}"; - private const string CropperJson3 = "{\"focalPoint\": {\"left\": 0.98,\"top\": 0.80827067669172936},\"src\": \"/media/1005/img_0672.jpg\",\"crops\": []}"; + private const string CropperJson3 = "{\"focalPoint\": {\"left\": 0.5,\"top\": 0.5},\"src\": \"/media/1005/img_0672.jpg\",\"crops\": []}"; private const string MediaPath = "/media/1005/img_0671.jpg"; [Test] public void CanConvertImageCropperDataSetSrcToString() { - // cropperJson3 - has not crops + // cropperJson3 - has no crops ImageCropperValue cropperValue = CropperJson3.DeserializeImageCropperValue(); Attempt serialized = cropperValue.TryConvertTo(); Assert.IsTrue(serialized.Success); @@ -38,7 +38,7 @@ namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Web.Common [Test] public void CanConvertImageCropperDataSetJObject() { - // cropperJson3 - has not crops + // cropperJson3 - has no crops ImageCropperValue cropperValue = CropperJson3.DeserializeImageCropperValue(); Attempt serialized = cropperValue.TryConvertTo(); Assert.IsTrue(serialized.Success);