Files
Umbraco-CMS/src/Umbraco.Web/PropertyEditors/NestedContentPropertyEditor.cs

394 lines
17 KiB
C#
Raw Normal View History

2017-09-12 16:22:16 +02:00
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Text.RegularExpressions;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Umbraco.Core;
using Umbraco.Core.Composing;
using Umbraco.Core.Logging;
using Umbraco.Core.Models;
using Umbraco.Core.Models.Editors;
using Umbraco.Core.PropertyEditors;
using Umbraco.Core.Services;
namespace Umbraco.Web.PropertyEditors
{
2018-01-24 13:37:14 +01:00
[ValueEditor(Constants.PropertyEditors.Aliases.NestedContent, "Nested Content", "nestedcontent", ValueType = "JSON", Group = "lists", Icon = "icon-thumbnail-list")]
2017-09-12 16:22:16 +02:00
public class NestedContentPropertyEditor : PropertyEditor
{
2017-09-20 20:06:46 +02:00
private readonly Lazy<PropertyEditorCollection> _propertyEditors;
2017-09-14 11:41:46 +02:00
2017-09-12 16:22:16 +02:00
internal const string ContentTypeAliasPropertyKey = "ncContentTypeAlias";
private IDictionary<string, object> _defaultPreValues;
public override IDictionary<string, object> DefaultPreValues
{
get => _defaultPreValues;
set => _defaultPreValues = value;
}
2017-09-20 20:06:46 +02:00
public NestedContentPropertyEditor(ILogger logger, Lazy<PropertyEditorCollection> propertyEditors)
2017-09-12 16:22:16 +02:00
: base (logger)
{
2017-09-14 11:41:46 +02:00
_propertyEditors = propertyEditors;
2017-09-12 16:22:16 +02:00
// Setup default values
_defaultPreValues = new Dictionary<string, object>
{
2018-01-24 11:44:44 +01:00
{NestedContentConfigurationEditor.ContentTypesPreValueKey, ""},
2017-09-12 16:22:16 +02:00
{"minItems", 0},
{"maxItems", 0},
{"confirmDeletes", "1"},
{"showIcons", "1"}
};
}
2017-09-20 20:06:46 +02:00
// has to be lazy else circular dep in ctor
private PropertyEditorCollection PropertyEditors => _propertyEditors.Value;
2017-10-17 17:43:15 +02:00
private static IContentType GetElementType(JObject item)
{
var contentTypeAlias = item[ContentTypeAliasPropertyKey]?.ToObject<string>();
return string.IsNullOrEmpty(contentTypeAlias)
? null
: Current.Services.ContentTypeService.Get(contentTypeAlias);
}
2017-09-12 16:22:16 +02:00
#region Pre Value Editor
2018-01-24 11:44:44 +01:00
protected override ConfigurationEditor CreateConfigurationEditor()
2017-09-12 16:22:16 +02:00
{
2018-01-24 11:44:44 +01:00
return new NestedContentConfigurationEditor();
2017-09-12 16:22:16 +02:00
}
#endregion
2017-10-17 17:43:15 +02:00
#region DataType Configuration
2018-01-15 17:32:45 +01:00
public class Configuration
2017-10-17 17:43:15 +02:00
{
public NestedContentType[] ContentTypes { get; set; }
2017-10-17 17:43:15 +02:00
public int? MinItems { get; set; }
public int? MaxItems { get; set; }
public bool ConfirmDeletes { get; set; }
public bool ShowIcons { get; set; }
public bool HideLabel { get; set; }
public class NestedContentType
{
[JsonProperty("ncAlias")]
public string Alias { get; set; }
[JsonProperty("ncTabAlias")]
public string Tab { get; set; }
[JsonProperty("nameTemplate")]
public string Template { get; set; }
}
2017-10-17 17:43:15 +02:00
}
2018-01-15 17:32:45 +01:00
public override object DeserializeConfiguration(string json)
2017-10-17 17:43:15 +02:00
{
2018-01-15 17:32:45 +01:00
return JsonConvert.DeserializeObject<Configuration>(json);
// fixme - can we have issues converting true/1 and false/0?
//var d = preValues.PreValuesAsDictionary;
//return new Configuration
//{
// ContentTypes = d.TryGetValue("contentTypes", out var preValue)
// ? JsonConvert.DeserializeObject<Configuration.NestedContentType[]>(preValue.Value)
// : Array.Empty<Configuration.NestedContentType>(),
// MinItems = d.TryGetValue("minItems", out preValue) && int.TryParse(preValue.Value, out var minItems) ? (int?) minItems : null,
// MaxItems = d.TryGetValue("maxItems", out preValue) && int.TryParse(preValue.Value, out var maxItems) ? (int?) maxItems : null,
// ConfirmDeletes = d.TryGetValue("confirmDeletes", out preValue) && preValue.Value == "1",
// ShowIcons = d.TryGetValue("showIcons", out preValue) && preValue.Value == "1",
// HideLabel = d.TryGetValue("hideLabel", out preValue) && preValue.Value == "1"
//};
2017-10-17 17:43:15 +02:00
}
#endregion
2017-09-12 16:22:16 +02:00
#region Value Editor
2018-01-24 13:37:14 +01:00
protected override ValueEditor CreateValueEditor() => new NestedContentPropertyValueEditor(Attribute, PropertyEditors);
2017-09-12 16:22:16 +02:00
2018-01-24 13:37:14 +01:00
internal class NestedContentPropertyValueEditor : ValueEditor
2017-09-12 16:22:16 +02:00
{
2017-09-14 11:41:46 +02:00
private readonly PropertyEditorCollection _propertyEditors;
2018-01-24 13:37:14 +01:00
public NestedContentPropertyValueEditor(ValueEditorAttribute attribute, PropertyEditorCollection propertyEditors)
: base(attribute)
2017-09-12 16:22:16 +02:00
{
2017-09-14 11:41:46 +02:00
_propertyEditors = propertyEditors;
Validators.Add(new NestedContentValidator(propertyEditors));
2017-09-12 16:22:16 +02:00
}
2017-09-14 11:41:46 +02:00
internal ServiceContext Services => Current.Services;
2017-09-12 16:22:16 +02:00
public override void ConfigureForDisplay(PreValueCollection preValues)
{
base.ConfigureForDisplay(preValues);
var asDictionary = preValues.PreValuesAsDictionary.ToDictionary(x => x.Key, x => x.Value.Value);
if (asDictionary.ContainsKey("hideLabel"))
{
var boolAttempt = asDictionary["hideLabel"].TryConvertTo<bool>();
if (boolAttempt.Success)
{
HideLabel = boolAttempt.Result;
}
}
}
#region DB to String
2017-11-15 08:53:20 +01:00
public override string ConvertDbToString(PropertyType propertyType, object propertyValue, IDataTypeService dataTypeService)
2017-09-12 16:22:16 +02:00
{
2017-11-15 08:53:20 +01:00
if (propertyValue == null || string.IsNullOrWhiteSpace(propertyValue.ToString()))
2017-09-12 16:22:16 +02:00
return string.Empty;
2017-11-15 08:53:20 +01:00
var value = JsonConvert.DeserializeObject<List<object>>(propertyValue.ToString());
2017-09-12 16:22:16 +02:00
if (value == null)
return string.Empty;
2017-11-15 08:53:20 +01:00
foreach (var o in value)
2017-09-12 16:22:16 +02:00
{
2017-11-15 08:53:20 +01:00
var propValues = (JObject) o;
2017-09-12 16:22:16 +02:00
2017-10-17 17:43:15 +02:00
var contentType = GetElementType(propValues);
2017-09-12 16:22:16 +02:00
if (contentType == null)
continue;
2017-11-15 08:53:20 +01:00
var propAliases = propValues.Properties().Select(x => x.Name).ToArray();
foreach (var propAlias in propAliases)
2017-09-12 16:22:16 +02:00
{
2017-11-15 08:53:20 +01:00
var propType = contentType.CompositionPropertyTypes.FirstOrDefault(x => x.Alias == propAlias);
2017-09-12 16:22:16 +02:00
if (propType == null)
{
2017-11-15 08:53:20 +01:00
// type not found, and property is not system: just delete the value
if (IsSystemPropertyKey(propAlias) == false)
propValues[propAlias] = null;
2017-09-12 16:22:16 +02:00
}
else
{
try
{
2017-11-15 08:53:20 +01:00
// convert the value, and store the converted value
2017-09-14 11:41:46 +02:00
var propEditor = _propertyEditors[propType.PropertyEditorAlias];
2017-11-15 08:53:20 +01:00
var convValue = propEditor.ValueEditor.ConvertDbToString(propType, propValues[propAlias], dataTypeService);
propValues[propAlias] = convValue;
2017-09-12 16:22:16 +02:00
}
catch (InvalidOperationException)
{
2017-11-15 08:53:20 +01:00
// deal with weird situations by ignoring them (no comment)
propValues[propAlias] = null;
2017-09-12 16:22:16 +02:00
}
}
}
}
2017-11-15 08:53:20 +01:00
return JsonConvert.SerializeObject(value).ToXmlString<string>();
2017-09-12 16:22:16 +02:00
}
#endregion
2017-11-15 08:53:20 +01:00
#region Convert database // editor
// note: there is NO variant support here
2017-09-12 16:22:16 +02:00
public override object ConvertDbToEditor(Property property, PropertyType propertyType, IDataTypeService dataTypeService)
{
if (property.GetValue() == null || string.IsNullOrWhiteSpace(property.GetValue().ToString()))
2017-09-12 16:22:16 +02:00
return string.Empty;
var value = JsonConvert.DeserializeObject<List<object>>(property.GetValue().ToString());
2017-09-12 16:22:16 +02:00
if (value == null)
return string.Empty;
2017-11-15 08:53:20 +01:00
foreach (var o in value)
2017-09-12 16:22:16 +02:00
{
2017-11-15 08:53:20 +01:00
var propValues = (JObject) o;
2017-09-12 16:22:16 +02:00
2017-10-17 17:43:15 +02:00
var contentType = GetElementType(propValues);
2017-09-12 16:22:16 +02:00
if (contentType == null)
continue;
2017-11-15 08:53:20 +01:00
var propAliases = propValues.Properties().Select(x => x.Name).ToArray();
foreach (var propAlias in propAliases)
2017-09-12 16:22:16 +02:00
{
2017-11-15 08:53:20 +01:00
var propType = contentType.CompositionPropertyTypes.FirstOrDefault(x => x.Alias == propAlias);
2017-09-12 16:22:16 +02:00
if (propType == null)
{
2017-11-15 08:53:20 +01:00
// type not found, and property is not system: just delete the value
if (IsSystemPropertyKey(propAlias) == false)
propValues[propAlias] = null;
2017-09-12 16:22:16 +02:00
}
else
{
try
{
2017-11-15 08:53:20 +01:00
// create a temp property with the value
var tempProp = new Property(propType);
tempProp.SetValue(propValues[propAlias] == null ? null : propValues[propAlias].ToString());
2017-09-12 16:22:16 +02:00
2017-11-15 08:53:20 +01:00
// convert that temp property, and store the converted value
2017-09-14 11:41:46 +02:00
var propEditor = _propertyEditors[propType.PropertyEditorAlias];
2017-11-15 08:53:20 +01:00
var convValue = propEditor.ValueEditor.ConvertDbToEditor(tempProp, propType, dataTypeService);
propValues[propAlias] = convValue == null ? null : JToken.FromObject(convValue);
2017-09-12 16:22:16 +02:00
}
catch (InvalidOperationException)
{
2017-11-15 08:53:20 +01:00
// deal with weird situations by ignoring them (no comment)
propValues[propAlias] = null;
2017-09-12 16:22:16 +02:00
}
}
}
}
2017-11-15 08:53:20 +01:00
// return json
return value;
2017-09-12 16:22:16 +02:00
}
public override object ConvertEditorToDb(ContentPropertyData editorValue, object currentValue)
{
if (editorValue.Value == null || string.IsNullOrWhiteSpace(editorValue.Value.ToString()))
return null;
var value = JsonConvert.DeserializeObject<List<object>>(editorValue.Value.ToString());
if (value == null)
return null;
// Issue #38 - Keep recursive property lookups working
if (!value.Any())
return null;
// Process value
for (var i = 0; i < value.Count; i++)
{
var o = value[i];
var propValues = ((JObject)o);
2017-10-17 17:43:15 +02:00
var contentType = GetElementType(propValues);
2017-09-12 16:22:16 +02:00
if (contentType == null)
{
continue;
}
var propValueKeys = propValues.Properties().Select(x => x.Name).ToArray();
foreach (var propKey in propValueKeys)
{
var propType = contentType.CompositionPropertyTypes.FirstOrDefault(x => x.Alias == propKey);
if (propType == null)
{
if (IsSystemPropertyKey(propKey) == false)
{
// Property missing so just delete the value
propValues[propKey] = null;
}
}
else
{
// Fetch the property types prevalue
2018-01-24 11:44:44 +01:00
var propConfiguration = Services.DataTypeService.GetDataType(propType.DataTypeId).Configuration;
2017-09-12 16:22:16 +02:00
// Lookup the property editor
2017-09-14 11:41:46 +02:00
var propEditor = _propertyEditors[propType.PropertyEditorAlias];
2017-09-12 16:22:16 +02:00
// Create a fake content property data object
2018-01-20 12:09:15 +01:00
var contentPropData = new ContentPropertyData(propValues[propKey], propConfiguration);
2017-09-12 16:22:16 +02:00
// Get the property editor to do it's conversion
var newValue = propEditor.ValueEditor.ConvertEditorToDb(contentPropData, propValues[propKey]);
// Store the value back
propValues[propKey] = (newValue == null) ? null : JToken.FromObject(newValue);
}
}
}
return JsonConvert.SerializeObject(value);
}
#endregion
}
2018-01-24 11:44:44 +01:00
internal class NestedContentValidator : IValueValidator
2017-09-12 16:22:16 +02:00
{
2017-09-14 11:41:46 +02:00
private readonly PropertyEditorCollection _propertyEditors;
public NestedContentValidator(PropertyEditorCollection propertyEditors)
{
_propertyEditors = propertyEditors;
}
2018-01-24 11:44:44 +01:00
public IEnumerable<ValidationResult> Validate(object rawValue, string valueType, object dataTypeConfiguration)
2017-09-12 16:22:16 +02:00
{
var value = JsonConvert.DeserializeObject<List<object>>(rawValue.ToString());
if (value == null)
yield break;
2017-09-14 11:41:46 +02:00
var dataTypeService = Current.Services.DataTypeService;
2017-09-12 16:22:16 +02:00
for (var i = 0; i < value.Count; i++)
{
var o = value[i];
2017-09-14 11:41:46 +02:00
var propValues = (JObject) o;
2017-09-12 16:22:16 +02:00
2017-10-17 17:43:15 +02:00
var contentType = GetElementType(propValues);
2018-01-20 12:09:15 +01:00
if (contentType == null) continue;
2017-09-12 16:22:16 +02:00
var propValueKeys = propValues.Properties().Select(x => x.Name).ToArray();
foreach (var propKey in propValueKeys)
{
var propType = contentType.CompositionPropertyTypes.FirstOrDefault(x => x.Alias == propKey);
if (propType != null)
{
2018-01-24 11:44:44 +01:00
var config = dataTypeService.GetDataType(propType.DataTypeId).Configuration;
2018-01-20 12:09:15 +01:00
var propertyEditor = _propertyEditors[propType.PropertyEditorAlias];
2017-09-12 16:22:16 +02:00
2017-09-14 11:41:46 +02:00
foreach (var validator in propertyEditor.ValueEditor.Validators)
2017-09-12 16:22:16 +02:00
{
2018-01-24 11:44:44 +01:00
foreach (var result in validator.Validate(propValues[propKey], propertyEditor.ValueEditor.ValueType, config))
2017-09-12 16:22:16 +02:00
{
result.ErrorMessage = "Item " + (i + 1) + " '" + propType.Name + "' " + result.ErrorMessage;
yield return result;
}
}
// Check mandatory
if (propType.Mandatory)
{
if (propValues[propKey] == null)
yield return new ValidationResult("Item " + (i + 1) + " '" + propType.Name + "' cannot be null", new[] { propKey });
else if (propValues[propKey].ToString().IsNullOrWhiteSpace())
yield return new ValidationResult("Item " + (i + 1) + " '" + propType.Name + "' cannot be empty", new[] { propKey });
}
// Check regex
if (!propType.ValidationRegExp.IsNullOrWhiteSpace()
&& propValues[propKey] != null && !propValues[propKey].ToString().IsNullOrWhiteSpace())
{
var regex = new Regex(propType.ValidationRegExp);
if (!regex.IsMatch(propValues[propKey].ToString()))
{
yield return new ValidationResult("Item " + (i + 1) + " '" + propType.Name + "' is invalid, it does not match the correct pattern", new[] { propKey });
}
}
}
}
}
}
}
#endregion
private static bool IsSystemPropertyKey(string propKey)
{
return propKey == "name" || propKey == "key" || propKey == ContentTypeAliasPropertyKey;
}
}
}