Moved PropertyEditors from Umbraco.Core to Umbraco.Infrastructure
This commit is contained in:
57
src/Umbraco.Infrastructure/ObjectJsonExtensions.cs
Normal file
57
src/Umbraco.Infrastructure/ObjectJsonExtensions.cs
Normal file
@@ -0,0 +1,57 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Linq;
|
||||
using System.Linq.Expressions;
|
||||
using System.Reflection;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Xml;
|
||||
using Newtonsoft.Json;
|
||||
using Umbraco.Core.Collections;
|
||||
|
||||
namespace Umbraco.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// Provides object extension methods.
|
||||
/// </summary>
|
||||
public static class ObjectJsonExtensions
|
||||
{
|
||||
private static readonly ConcurrentDictionary<Type, Dictionary<string, object>> ToObjectTypes = new ConcurrentDictionary<Type, Dictionary<string, object>>();
|
||||
|
||||
/// <summary>
|
||||
/// Converts an object's properties into a dictionary.
|
||||
/// </summary>
|
||||
/// <param name="obj">The object to convert.</param>
|
||||
/// <param name="namer">A property namer function.</param>
|
||||
/// <returns>A dictionary containing each properties.</returns>
|
||||
public static Dictionary<string, object> ToObjectDictionary<T>(T obj, Func<PropertyInfo, string> namer = null)
|
||||
{
|
||||
if (obj == null) return new Dictionary<string, object>();
|
||||
|
||||
string DefaultNamer(PropertyInfo property)
|
||||
{
|
||||
var jsonProperty = property.GetCustomAttribute<JsonPropertyAttribute>();
|
||||
return jsonProperty?.PropertyName ?? property.Name;
|
||||
}
|
||||
|
||||
var t = obj.GetType();
|
||||
|
||||
if (namer == null) namer = DefaultNamer;
|
||||
|
||||
if (!ToObjectTypes.TryGetValue(t, out var properties))
|
||||
{
|
||||
properties = new Dictionary<string, object>();
|
||||
|
||||
foreach (var p in t.GetProperties(BindingFlags.Public | BindingFlags.Instance | BindingFlags.FlattenHierarchy))
|
||||
properties[namer(p)] = ReflectionUtilities.EmitPropertyGetter<T, object>(p);
|
||||
|
||||
ToObjectTypes[t] = properties;
|
||||
}
|
||||
|
||||
return properties.ToDictionary(x => x.Key, x => ((Func<T, object>) x.Value)(obj));
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Serialization;
|
||||
using Umbraco.Core.Serialization;
|
||||
|
||||
namespace Umbraco.Core.PropertyEditors
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a data type configuration editor.
|
||||
/// </summary>
|
||||
public class ConfigurationEditor : IConfigurationEditor
|
||||
{
|
||||
private IDictionary<string, object> _defaultConfiguration;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ConfigurationEditor"/> class.
|
||||
/// </summary>
|
||||
public ConfigurationEditor()
|
||||
{
|
||||
Fields = new List<ConfigurationField>();
|
||||
_defaultConfiguration = new Dictionary<string, object>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ConfigurationEditor"/> class.
|
||||
/// </summary>
|
||||
protected ConfigurationEditor(List<ConfigurationField> fields)
|
||||
{
|
||||
Fields = fields;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the fields.
|
||||
/// </summary>
|
||||
[JsonProperty("fields")]
|
||||
public List<ConfigurationField> Fields { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a field by its property name.
|
||||
/// </summary>
|
||||
/// <remarks>Can be used in constructors to add infos to a field that has been defined
|
||||
/// by a property marked with the <see cref="ConfigurationFieldAttribute"/>.</remarks>
|
||||
protected ConfigurationField Field(string name)
|
||||
=> Fields.First(x => x.PropertyName == name);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the configuration as a typed object.
|
||||
/// </summary>
|
||||
public static TConfiguration ConfigurationAs<TConfiguration>(object obj)
|
||||
{
|
||||
if (obj == null) return default;
|
||||
if (obj is TConfiguration configuration) return configuration;
|
||||
throw new InvalidCastException($"Cannot cast configuration of type {obj.GetType().Name} to {typeof(TConfiguration).Name}.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a configuration object into a serialized database value.
|
||||
/// </summary>
|
||||
public static string ToDatabase(object configuration)
|
||||
=> configuration == null ? null : JsonConvert.SerializeObject(configuration, ConfigurationJsonSettings);
|
||||
|
||||
/// <inheritdoc />
|
||||
[JsonProperty("defaultConfig")]
|
||||
public virtual IDictionary<string, object> DefaultConfiguration {
|
||||
get => _defaultConfiguration;
|
||||
set => _defaultConfiguration = value;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual object DefaultConfigurationObject => DefaultConfiguration;
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual bool IsConfiguration(object obj) => obj is IDictionary<string, object>;
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual object FromDatabase(string configurationJson)
|
||||
=> string.IsNullOrWhiteSpace(configurationJson)
|
||||
? new Dictionary<string, object>()
|
||||
: JsonConvert.DeserializeObject<Dictionary<string, object>>(configurationJson);
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual object FromConfigurationEditor(IDictionary<string, object> editorValues, object configuration)
|
||||
{
|
||||
// by default, return the posted dictionary
|
||||
// but only keep entries that have a non-null/empty value
|
||||
// rest will fall back to default during ToConfigurationEditor()
|
||||
|
||||
var keys = editorValues.Where(x =>
|
||||
x.Value == null || x.Value is string stringValue && string.IsNullOrWhiteSpace(stringValue))
|
||||
.Select(x => x.Key).ToList();
|
||||
|
||||
foreach (var key in keys) editorValues.Remove(key);
|
||||
|
||||
return editorValues;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual IDictionary<string, object> ToConfigurationEditor(object configuration)
|
||||
{
|
||||
// editors that do not override ToEditor/FromEditor have their configuration
|
||||
// as a dictionary of <string, object> and, by default, we merge their default
|
||||
// configuration with their current configuration
|
||||
|
||||
if (configuration == null)
|
||||
configuration = new Dictionary<string, object>();
|
||||
|
||||
if (!(configuration is IDictionary<string, object> c))
|
||||
throw new ArgumentException($"Expecting a {typeof(Dictionary<string,object>).Name} instance but got {configuration.GetType().Name}.", nameof(configuration));
|
||||
|
||||
// clone the default configuration, and apply the current configuration values
|
||||
var d = new Dictionary<string, object>(DefaultConfiguration);
|
||||
foreach (var (key, value) in c)
|
||||
d[key] = value;
|
||||
return d;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual IDictionary<string, object> ToValueEditor(object configuration)
|
||||
=> ToConfigurationEditor(configuration);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the custom json serializer settings for configurations.
|
||||
/// </summary>
|
||||
public static JsonSerializerSettings ConfigurationJsonSettings { get; } = new JsonSerializerSettings
|
||||
{
|
||||
ContractResolver = new ConfigurationCustomContractResolver(),
|
||||
Converters = new List<JsonConverter>(new[]{new FuzzyBooleanConverter()})
|
||||
};
|
||||
|
||||
private class ConfigurationCustomContractResolver : DefaultContractResolver
|
||||
{
|
||||
protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization)
|
||||
{
|
||||
// base.CreateProperty deals with [JsonProperty("name")]
|
||||
var property = base.CreateProperty(member, memberSerialization);
|
||||
|
||||
// override with our custom attribute, if any
|
||||
var attribute = member.GetCustomAttribute<ConfigurationFieldAttribute>();
|
||||
if (attribute != null) property.PropertyName = attribute.Key;
|
||||
|
||||
// for value types,
|
||||
// don't try to deserialize nulls (in legacy json)
|
||||
// no impact on serialization (value cannot be null)
|
||||
if (member is PropertyInfo propertyInfo && propertyInfo.PropertyType.IsValueType)
|
||||
property.NullValueHandling = NullValueHandling.Ignore;
|
||||
|
||||
return property;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,197 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Reflection;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Umbraco.Core.Composing;
|
||||
using Umbraco.Core.IO;
|
||||
|
||||
namespace Umbraco.Core.PropertyEditors
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a data type configuration editor with a typed configuration.
|
||||
/// </summary>
|
||||
public abstract class ConfigurationEditor<TConfiguration> : ConfigurationEditor
|
||||
where TConfiguration : new()
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ConfigurationEditor{TConfiguration}"/> class.
|
||||
/// </summary>
|
||||
protected ConfigurationEditor(IIOHelper ioHelper)
|
||||
: base(DiscoverFields(ioHelper))
|
||||
{ }
|
||||
|
||||
/// <summary>
|
||||
/// Discovers fields from configuration properties marked with the field attribute.
|
||||
/// </summary>
|
||||
private static List<ConfigurationField> DiscoverFields(IIOHelper ioHelper)
|
||||
{
|
||||
var fields = new List<ConfigurationField>();
|
||||
var properties = TypeHelper.CachedDiscoverableProperties(typeof(TConfiguration));
|
||||
|
||||
foreach (var property in properties)
|
||||
{
|
||||
var attribute = property.GetCustomAttribute<ConfigurationFieldAttribute>(false);
|
||||
if (attribute == null) continue;
|
||||
|
||||
ConfigurationField field;
|
||||
|
||||
var attributeView = ioHelper.ResolveVirtualUrl(attribute.View);
|
||||
// if the field does not have its own type, use the base type
|
||||
if (attribute.Type == null)
|
||||
{
|
||||
field = new ConfigurationField
|
||||
{
|
||||
// if the key is empty then use the property name
|
||||
Key = string.IsNullOrWhiteSpace(attribute.Key) ? property.Name : attribute.Key,
|
||||
Name = attribute.Name,
|
||||
PropertyName = property.Name,
|
||||
PropertyType = property.PropertyType,
|
||||
Description = attribute.Description,
|
||||
HideLabel = attribute.HideLabel,
|
||||
View = attributeView
|
||||
};
|
||||
|
||||
fields.Add(field);
|
||||
continue;
|
||||
}
|
||||
|
||||
// if the field has its own type, instantiate it
|
||||
try
|
||||
{
|
||||
field = (ConfigurationField) Activator.CreateInstance(attribute.Type);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new Exception($"Failed to create an instance of type \"{attribute.Type}\" for property \"{property.Name}\" of configuration \"{typeof(TConfiguration).Name}\" (see inner exception).", ex);
|
||||
}
|
||||
|
||||
// then add it, and overwrite values if they are assigned in the attribute
|
||||
fields.Add(field);
|
||||
|
||||
field.PropertyName = property.Name;
|
||||
field.PropertyType = property.PropertyType;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(attribute.Key))
|
||||
field.Key = attribute.Key;
|
||||
|
||||
// if the key is still empty then use the property name
|
||||
if (string.IsNullOrWhiteSpace(field.Key))
|
||||
field.Key = property.Name;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(attribute.Name))
|
||||
field.Name = attribute.Name;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(attribute.View))
|
||||
field.View = attributeView;
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(attribute.Description))
|
||||
field.Description = attribute.Description;
|
||||
|
||||
if (attribute.HideLabelSettable.HasValue)
|
||||
field.HideLabel = attribute.HideLabel;
|
||||
}
|
||||
|
||||
return fields;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override IDictionary<string, object> DefaultConfiguration => ToConfigurationEditor(DefaultConfigurationObject);
|
||||
|
||||
/// <inheritdoc />
|
||||
public override object DefaultConfigurationObject => new TConfiguration();
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool IsConfiguration(object obj)
|
||||
=> obj is TConfiguration;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override object FromDatabase(string configuration)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(configuration)) return new TConfiguration();
|
||||
return JsonConvert.DeserializeObject<TConfiguration>(configuration, ConfigurationJsonSettings);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
throw new InvalidOperationException($"Failed to parse configuration \"{configuration}\" as \"{typeof(TConfiguration).Name}\" (see inner exception).", e);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public sealed override object FromConfigurationEditor(IDictionary<string, object> editorValues, object configuration)
|
||||
{
|
||||
return FromConfigurationEditor(editorValues, (TConfiguration) configuration);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts the configuration posted by the editor.
|
||||
/// </summary>
|
||||
/// <param name="editorValues">The configuration object posted by the editor.</param>
|
||||
/// <param name="configuration">The current configuration object.</param>
|
||||
public virtual TConfiguration FromConfigurationEditor(IDictionary<string, object> editorValues, TConfiguration configuration)
|
||||
{
|
||||
// note - editorValue contains a mix of CLR types (string, int...) and JToken
|
||||
// turning everything back into a JToken... might not be fastest but is simplest
|
||||
// for now
|
||||
|
||||
var o = new JObject();
|
||||
|
||||
foreach (var field in Fields)
|
||||
{
|
||||
// field only, JsonPropertyAttribute is ignored here
|
||||
// only keep fields that have a non-null/empty value
|
||||
// rest will fall back to default during ToObject()
|
||||
if (editorValues.TryGetValue(field.Key, out var value) && value != null && (!(value is string stringValue) || !string.IsNullOrWhiteSpace(stringValue)))
|
||||
{
|
||||
if (value is JToken jtoken)
|
||||
{
|
||||
//if it's a jtoken then set it
|
||||
o[field.PropertyName] = jtoken;
|
||||
}
|
||||
else if (field.PropertyType == typeof(bool) && value is string sBool)
|
||||
{
|
||||
//if it's a boolean property type but a string is found, try to do a conversion
|
||||
var converted = sBool.TryConvertTo<bool>();
|
||||
if (converted)
|
||||
o[field.PropertyName] = converted.Result;
|
||||
}
|
||||
else
|
||||
{
|
||||
//default behavior
|
||||
o[field.PropertyName] = JToken.FromObject(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return o.ToObject<TConfiguration>();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public sealed override IDictionary<string, object> ToConfigurationEditor(object configuration)
|
||||
{
|
||||
return ToConfigurationEditor((TConfiguration) configuration);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts configuration values to values for the editor.
|
||||
/// </summary>
|
||||
/// <param name="configuration">The configuration.</param>
|
||||
public virtual Dictionary<string, object> ToConfigurationEditor(TConfiguration configuration)
|
||||
{
|
||||
string FieldNamer(PropertyInfo property)
|
||||
{
|
||||
// try the field
|
||||
var field = property.GetCustomAttribute<ConfigurationFieldAttribute>();
|
||||
if (field != null) return field.Key;
|
||||
|
||||
// but the property may not be a field just an extra thing
|
||||
var json = property.GetCustomAttribute<JsonPropertyAttribute>();
|
||||
return json?.PropertyName ?? property.Name;
|
||||
}
|
||||
|
||||
return ObjectJsonExtensions.ToObjectDictionary(configuration, FieldNamer);
|
||||
}
|
||||
}
|
||||
}
|
||||
203
src/Umbraco.Infrastructure/PropertyEditors/DataEditor.cs
Normal file
203
src/Umbraco.Infrastructure/PropertyEditors/DataEditor.cs
Normal file
@@ -0,0 +1,203 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Runtime.Serialization;
|
||||
using Umbraco.Composing;
|
||||
using Umbraco.Core.Composing;
|
||||
using Umbraco.Core.Logging;
|
||||
using Umbraco.Core.Services;
|
||||
using Umbraco.Core.Strings;
|
||||
|
||||
namespace Umbraco.Core.PropertyEditors
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a data editor.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>Editors can be deserialized from e.g. manifests, which is. why the class is not abstract,
|
||||
/// the json serialization attributes are required, and the properties have an internal setter.</para>
|
||||
/// </remarks>
|
||||
[DebuggerDisplay("{" + nameof(DebuggerDisplay) + "(),nq}")]
|
||||
[HideFromTypeFinder]
|
||||
[DataContract]
|
||||
public class DataEditor : IDataEditor
|
||||
{
|
||||
private readonly IDataTypeService _dataTypeService;
|
||||
private readonly ILocalizationService _localizationService;
|
||||
private readonly IShortStringHelper _shortStringHelper;
|
||||
private IDictionary<string, object> _defaultConfiguration;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DataEditor"/> class.
|
||||
/// </summary>
|
||||
public DataEditor(ILogger logger, IDataTypeService dataTypeService, ILocalizationService localizationService, IShortStringHelper shortStringHelper, EditorType type = EditorType.PropertyValue)
|
||||
{
|
||||
_dataTypeService = dataTypeService;
|
||||
_localizationService = localizationService;
|
||||
_shortStringHelper = shortStringHelper;
|
||||
Logger = logger ?? throw new ArgumentNullException(nameof(logger));
|
||||
|
||||
// defaults
|
||||
Type = type;
|
||||
Icon = Constants.Icons.PropertyEditor;
|
||||
Group = Constants.PropertyEditors.Groups.Common;
|
||||
|
||||
// assign properties based on the attribute, if it is found
|
||||
Attribute = GetType().GetCustomAttribute<DataEditorAttribute>(false);
|
||||
if (Attribute == null) return;
|
||||
|
||||
Alias = Attribute.Alias;
|
||||
Type = Attribute.Type;
|
||||
Name = Attribute.Name;
|
||||
Icon = Attribute.Icon;
|
||||
Group = Attribute.Group;
|
||||
IsDeprecated = Attribute.IsDeprecated;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the editor attribute.
|
||||
/// </summary>
|
||||
protected DataEditorAttribute Attribute { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets a logger.
|
||||
/// </summary>
|
||||
protected ILogger Logger { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
[DataMember(Name = "alias", IsRequired = true)]
|
||||
public string Alias { get; internal set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
[IgnoreDataMember]
|
||||
public EditorType Type { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
[DataMember(Name = "name", IsRequired = true)]
|
||||
public string Name { get; internal set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
[DataMember(Name = "icon")]
|
||||
public string Icon { get; internal set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
[DataMember(Name = "group")]
|
||||
public string Group { get; internal set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
[IgnoreDataMember]
|
||||
public bool IsDeprecated { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
/// <remarks>
|
||||
/// <para>If an explicit value editor has been assigned, then this explicit
|
||||
/// instance is returned. Otherwise, a new instance is created by CreateValueEditor.</para>
|
||||
/// <para>The instance created by CreateValueEditor is not cached, i.e.
|
||||
/// a new instance is created each time the property value is retrieved. The
|
||||
/// property editor is a singleton, and the value editor cannot be a singleton
|
||||
/// since it depends on the datatype configuration.</para>
|
||||
/// <para>Technically, it could be cached by datatype but let's keep things
|
||||
/// simple enough for now.</para>
|
||||
/// </remarks>
|
||||
// TODO: point of that one? shouldn't we always configure?
|
||||
public IDataValueEditor GetValueEditor() => ExplicitValueEditor ?? CreateValueEditor();
|
||||
|
||||
/// <inheritdoc />
|
||||
/// <remarks>
|
||||
/// <para>If an explicit value editor has been assigned, then this explicit
|
||||
/// instance is returned. Otherwise, a new instance is created by CreateValueEditor,
|
||||
/// and configured with the configuration.</para>
|
||||
/// <para>The instance created by CreateValueEditor is not cached, i.e.
|
||||
/// a new instance is created each time the property value is retrieved. The
|
||||
/// property editor is a singleton, and the value editor cannot be a singleton
|
||||
/// since it depends on the datatype configuration.</para>
|
||||
/// <para>Technically, it could be cached by datatype but let's keep things
|
||||
/// simple enough for now.</para>
|
||||
/// </remarks>
|
||||
public IDataValueEditor GetValueEditor(object configuration)
|
||||
{
|
||||
// if an explicit value editor has been set (by the manifest parser)
|
||||
// then return it, and ignore the configuration, which is going to be
|
||||
// empty anyways
|
||||
if (ExplicitValueEditor != null)
|
||||
return ExplicitValueEditor;
|
||||
|
||||
var editor = CreateValueEditor();
|
||||
((DataValueEditor) editor).Configuration = configuration; // TODO: casting is bad
|
||||
return editor;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets an explicit value editor.
|
||||
/// </summary>
|
||||
/// <remarks>Used for manifest data editors.</remarks>
|
||||
[DataMember(Name = "editor")]
|
||||
public IDataValueEditor ExplicitValueEditor { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
/// <remarks>
|
||||
/// <para>If an explicit configuration editor has been assigned, then this explicit
|
||||
/// instance is returned. Otherwise, a new instance is created by CreateConfigurationEditor.</para>
|
||||
/// <para>The instance created by CreateConfigurationEditor is not cached, i.e.
|
||||
/// a new instance is created each time. The property editor is a singleton, and although the
|
||||
/// configuration editor could technically be a singleton too, we'd rather not keep configuration editor
|
||||
/// cached.</para>
|
||||
/// </remarks>
|
||||
public IConfigurationEditor GetConfigurationEditor() => ExplicitConfigurationEditor ?? CreateConfigurationEditor();
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets an explicit configuration editor.
|
||||
/// </summary>
|
||||
/// <remarks>Used for manifest data editors.</remarks>
|
||||
[DataMember(Name = "config")]
|
||||
public IConfigurationEditor ExplicitConfigurationEditor { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
[DataMember(Name = "defaultConfig")]
|
||||
public IDictionary<string, object> DefaultConfiguration
|
||||
{
|
||||
// for property value editors, get the ConfigurationEditor.DefaultConfiguration
|
||||
// else fallback to a default, empty dictionary
|
||||
|
||||
get => _defaultConfiguration ?? ((Type & EditorType.PropertyValue) > 0 ? GetConfigurationEditor().DefaultConfiguration : (_defaultConfiguration = new Dictionary<string, object>()));
|
||||
set => _defaultConfiguration = value;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public virtual IPropertyIndexValueFactory PropertyIndexValueFactory => new DefaultPropertyIndexValueFactory();
|
||||
|
||||
/// <summary>
|
||||
/// Creates a value editor instance.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
protected virtual IDataValueEditor CreateValueEditor()
|
||||
{
|
||||
if (Attribute == null)
|
||||
throw new InvalidOperationException("The editor does not specify a view.");
|
||||
|
||||
return new DataValueEditor(_dataTypeService, _localizationService, _shortStringHelper, Attribute);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates a configuration editor instance.
|
||||
/// </summary>
|
||||
protected virtual IConfigurationEditor CreateConfigurationEditor()
|
||||
{
|
||||
var editor = new ConfigurationEditor();
|
||||
// pass the default configuration if this is not a property value editor
|
||||
if((Type & EditorType.PropertyValue) == 0)
|
||||
{
|
||||
editor.DefaultConfiguration = _defaultConfiguration;
|
||||
}
|
||||
return editor;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Provides a summary of the PropertyEditor for use with the <see cref="DebuggerDisplayAttribute"/>.
|
||||
/// </summary>
|
||||
protected virtual string DebuggerDisplay()
|
||||
{
|
||||
return $"Name: {Name}, Alias: {Alias}";
|
||||
}
|
||||
}
|
||||
}
|
||||
372
src/Umbraco.Infrastructure/PropertyEditors/DataValueEditor.cs
Normal file
372
src/Umbraco.Infrastructure/PropertyEditors/DataValueEditor.cs
Normal file
@@ -0,0 +1,372 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Xml.Linq;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Umbraco.Composing;
|
||||
using Umbraco.Core.Composing;
|
||||
using Umbraco.Core.Logging;
|
||||
using Umbraco.Core.Models;
|
||||
using Umbraco.Core.Models.Editors;
|
||||
using Umbraco.Core.PropertyEditors.Validators;
|
||||
using Umbraco.Core.Services;
|
||||
using Umbraco.Core.Strings;
|
||||
|
||||
namespace Umbraco.Core.PropertyEditors
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a value editor.
|
||||
/// </summary>
|
||||
public class DataValueEditor : IDataValueEditor
|
||||
{
|
||||
private readonly ILocalizedTextService _localizedTextService;
|
||||
private readonly IShortStringHelper _shortStringHelper;
|
||||
protected IDataTypeService DataTypeService { get; }
|
||||
protected ILocalizationService LocalizationService { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DataValueEditor"/> class.
|
||||
/// </summary>
|
||||
public DataValueEditor(IDataTypeService dataTypeService, ILocalizationService localizationService, ILocalizedTextService localizedTextService, IShortStringHelper shortStringHelper) // for tests, and manifest
|
||||
{
|
||||
_localizedTextService = localizedTextService;
|
||||
_shortStringHelper = shortStringHelper;
|
||||
ValueType = ValueTypes.String;
|
||||
Validators = new List<IValueValidator>();
|
||||
DataTypeService = dataTypeService;
|
||||
LocalizationService = localizationService;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="DataValueEditor"/> class.
|
||||
/// </summary>
|
||||
public DataValueEditor(IDataTypeService dataTypeService, ILocalizationService localizationService, IShortStringHelper shortStringHelper, DataEditorAttribute attribute)
|
||||
{
|
||||
if (attribute == null) throw new ArgumentNullException(nameof(attribute));
|
||||
_shortStringHelper = shortStringHelper;
|
||||
|
||||
var view = attribute.View;
|
||||
if (string.IsNullOrWhiteSpace(view))
|
||||
throw new ArgumentException("The attribute does not specify a view.", nameof(attribute));
|
||||
|
||||
View = view;
|
||||
ValueType = attribute.ValueType;
|
||||
HideLabel = attribute.HideLabel;
|
||||
|
||||
DataTypeService = dataTypeService;
|
||||
LocalizationService = localizationService;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the value editor configuration.
|
||||
/// </summary>
|
||||
public virtual object Configuration { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the editor view.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>The view can be three things: (1) the full virtual path, or (2) the relative path to the current Umbraco
|
||||
/// folder, or (3) a view name which maps to views/propertyeditors/{view}/{view}.html.</para>
|
||||
/// </remarks>
|
||||
[JsonProperty("view", Required = Required.Always)]
|
||||
public string View { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// The value type which reflects how it is validated and stored in the database
|
||||
/// </summary>
|
||||
[JsonProperty("valueType")]
|
||||
public string ValueType { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public IEnumerable<ValidationResult> Validate(object value, bool required, string format)
|
||||
{
|
||||
List<ValidationResult> results = null;
|
||||
var r = Validators.SelectMany(v => v.Validate(value, ValueType, Configuration)).ToList();
|
||||
if (r.Any()) { results = r; }
|
||||
|
||||
// mandatory and regex validators cannot be part of valueEditor.Validators because they
|
||||
// depend on values that are not part of the configuration, .Mandatory and .ValidationRegEx,
|
||||
// so they have to be explicitly invoked here.
|
||||
|
||||
if (required)
|
||||
{
|
||||
r = RequiredValidator.ValidateRequired(value, ValueType).ToList();
|
||||
if (r.Any()) { if (results == null) results = r; else results.AddRange(r); }
|
||||
}
|
||||
|
||||
var stringValue = value?.ToString();
|
||||
if (!string.IsNullOrWhiteSpace(format) && !string.IsNullOrWhiteSpace(stringValue))
|
||||
{
|
||||
r = FormatValidator.ValidateFormat(value, ValueType, format).ToList();
|
||||
if (r.Any()) { if (results == null) results = r; else results.AddRange(r); }
|
||||
}
|
||||
|
||||
return results ?? Enumerable.Empty<ValidationResult>();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A collection of validators for the pre value editor
|
||||
/// </summary>
|
||||
[JsonProperty("validation")]
|
||||
public List<IValueValidator> Validators { get; private set; } = new List<IValueValidator>();
|
||||
|
||||
/// <summary>
|
||||
/// Gets the validator used to validate the special property type -level "required".
|
||||
/// </summary>
|
||||
public virtual IValueRequiredValidator RequiredValidator => new RequiredValidator(_localizedTextService);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the validator used to validate the special property type -level "format".
|
||||
/// </summary>
|
||||
public virtual IValueFormatValidator FormatValidator => new RegexValidator(_localizedTextService);
|
||||
|
||||
/// <summary>
|
||||
/// If this is true than the editor will be displayed full width without a label
|
||||
/// </summary>
|
||||
[JsonProperty("hideLabel")]
|
||||
public bool HideLabel { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Set this to true if the property editor is for display purposes only
|
||||
/// </summary>
|
||||
public virtual bool IsReadOnly => false;
|
||||
|
||||
/// <summary>
|
||||
/// Used to try to convert the string value to the correct CLR type based on the DatabaseDataType specified for this value editor
|
||||
/// </summary>
|
||||
/// <param name="value"></param>
|
||||
/// <returns></returns>
|
||||
internal Attempt<object> TryConvertValueToCrlType(object value)
|
||||
{
|
||||
if (value is JValue)
|
||||
value = value.ToString();
|
||||
|
||||
//this is a custom check to avoid any errors, if it's a string and it's empty just make it null
|
||||
if (value is string s && string.IsNullOrWhiteSpace(s))
|
||||
value = null;
|
||||
|
||||
Type valueType;
|
||||
//convert the string to a known type
|
||||
switch (ValueTypes.ToStorageType(ValueType))
|
||||
{
|
||||
case ValueStorageType.Ntext:
|
||||
case ValueStorageType.Nvarchar:
|
||||
valueType = typeof(string);
|
||||
break;
|
||||
case ValueStorageType.Integer:
|
||||
//ensure these are nullable so we can return a null if required
|
||||
//NOTE: This is allowing type of 'long' because I think json.net will deserialize a numerical value as long
|
||||
// instead of int. Even though our db will not support this (will get truncated), we'll at least parse to this.
|
||||
|
||||
valueType = typeof(long?);
|
||||
|
||||
//if parsing is successful, we need to return as an Int, we're only dealing with long's here because of json.net, we actually
|
||||
//don't support long values and if we return a long value it will get set as a 'long' on the Property.Value (object) and then
|
||||
//when we compare the values for dirty tracking we'll be comparing an int -> long and they will not match.
|
||||
var result = value.TryConvertTo(valueType);
|
||||
return result.Success && result.Result != null
|
||||
? Attempt<object>.Succeed((int)(long)result.Result)
|
||||
: result;
|
||||
|
||||
case ValueStorageType.Decimal:
|
||||
//ensure these are nullable so we can return a null if required
|
||||
valueType = typeof(decimal?);
|
||||
break;
|
||||
|
||||
case ValueStorageType.Date:
|
||||
//ensure these are nullable so we can return a null if required
|
||||
valueType = typeof(DateTime?);
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException();
|
||||
}
|
||||
return value.TryConvertTo(valueType);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A method to deserialize the string value that has been saved in the content editor
|
||||
/// to an object to be stored in the database.
|
||||
/// </summary>
|
||||
/// <param name="editorValue"></param>
|
||||
/// <param name="currentValue">
|
||||
/// The current value that has been persisted to the database for this editor. This value may be useful for
|
||||
/// how the value then get's deserialized again to be re-persisted. In most cases it will probably not be used.
|
||||
/// </param>
|
||||
/// <param name="languageId"></param>
|
||||
/// <param name="segment"></param>
|
||||
/// <returns></returns>
|
||||
/// <remarks>
|
||||
/// By default this will attempt to automatically convert the string value to the value type supplied by ValueType.
|
||||
///
|
||||
/// If overridden then the object returned must match the type supplied in the ValueType, otherwise persisting the
|
||||
/// value to the DB will fail when it tries to validate the value type.
|
||||
/// </remarks>
|
||||
public virtual object FromEditor(ContentPropertyData editorValue, object currentValue)
|
||||
{
|
||||
//if it's json but it's empty json, then return null
|
||||
if (ValueType.InvariantEquals(ValueTypes.Json) && editorValue.Value != null && editorValue.Value.ToString().DetectIsEmptyJson())
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var result = TryConvertValueToCrlType(editorValue.Value);
|
||||
if (result.Success == false)
|
||||
{
|
||||
Current.Logger.Warn<DataValueEditor>("The value {EditorValue} cannot be converted to the type {StorageTypeValue}", editorValue.Value, ValueTypes.ToStorageType(ValueType));
|
||||
return null;
|
||||
}
|
||||
return result.Result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A method used to format the database value to a value that can be used by the editor
|
||||
/// </summary>
|
||||
/// <param name="property"></param>
|
||||
/// <param name="dataTypeService"></param>
|
||||
/// <param name="culture"></param>
|
||||
/// <param name="segment"></param>
|
||||
/// <returns></returns>
|
||||
/// <remarks>
|
||||
/// The object returned will automatically be serialized into json notation. For most property editors
|
||||
/// the value returned is probably just a string but in some cases a json structure will be returned.
|
||||
/// </remarks>
|
||||
public virtual object ToEditor(IProperty property, string culture = null, string segment = null)
|
||||
{
|
||||
var val = property.GetValue(culture, segment);
|
||||
if (val == null) return string.Empty;
|
||||
|
||||
switch (ValueTypes.ToStorageType(ValueType))
|
||||
{
|
||||
case ValueStorageType.Ntext:
|
||||
case ValueStorageType.Nvarchar:
|
||||
//if it is a string type, we will attempt to see if it is json stored data, if it is we'll try to convert
|
||||
//to a real json object so we can pass the true json object directly to angular!
|
||||
var asString = val.ToString();
|
||||
if (asString.DetectIsJson())
|
||||
{
|
||||
try
|
||||
{
|
||||
var json = JsonConvert.DeserializeObject(asString);
|
||||
return json;
|
||||
}
|
||||
catch
|
||||
{
|
||||
//swallow this exception, we thought it was json but it really isn't so continue returning a string
|
||||
}
|
||||
}
|
||||
return asString;
|
||||
case ValueStorageType.Integer:
|
||||
case ValueStorageType.Decimal:
|
||||
//Decimals need to be formatted with invariant culture (dots, not commas)
|
||||
//Anything else falls back to ToString()
|
||||
var decim = val.TryConvertTo<decimal>();
|
||||
return decim.Success
|
||||
? decim.Result.ToString(NumberFormatInfo.InvariantInfo)
|
||||
: val.ToString();
|
||||
case ValueStorageType.Date:
|
||||
var date = val.TryConvertTo<DateTime?>();
|
||||
if (date.Success == false || date.Result == null)
|
||||
{
|
||||
return string.Empty;
|
||||
}
|
||||
//Dates will be formatted as yyyy-MM-dd HH:mm:ss
|
||||
return date.Result.Value.ToIsoString();
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException();
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: the methods below should be replaced by proper property value convert ToXPath usage!
|
||||
|
||||
/// <summary>
|
||||
/// Converts a property to Xml fragments.
|
||||
/// </summary>
|
||||
public IEnumerable<XElement> ConvertDbToXml(IProperty property, bool published)
|
||||
{
|
||||
published &= property.PropertyType.SupportsPublishing;
|
||||
|
||||
var nodeName = property.PropertyType.Alias.ToSafeAlias(_shortStringHelper);
|
||||
|
||||
foreach (var pvalue in property.Values)
|
||||
{
|
||||
var value = published ? pvalue.PublishedValue : pvalue.EditedValue;
|
||||
if (value == null || value is string stringValue && string.IsNullOrWhiteSpace(stringValue))
|
||||
continue;
|
||||
|
||||
var xElement = new XElement(nodeName);
|
||||
if (pvalue.Culture != null)
|
||||
xElement.Add(new XAttribute("lang", pvalue.Culture));
|
||||
if (pvalue.Segment != null)
|
||||
xElement.Add(new XAttribute("segment", pvalue.Segment));
|
||||
|
||||
var xValue = ConvertDbToXml(property.PropertyType, value);
|
||||
xElement.Add(xValue);
|
||||
|
||||
yield return xElement;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a property value to an Xml fragment.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>By default, this returns the value of ConvertDbToString but ensures that if the db value type is
|
||||
/// NVarchar or NText, the value is returned as a CDATA fragment - else it's a Text fragment.</para>
|
||||
/// <para>Returns an XText or XCData instance which must be wrapped in a element.</para>
|
||||
/// <para>If the value is empty we will not return as CDATA since that will just take up more space in the file.</para>
|
||||
/// </remarks>
|
||||
public XNode ConvertDbToXml(IPropertyType propertyType, object value)
|
||||
{
|
||||
//check for null or empty value, we don't want to return CDATA if that is the case
|
||||
if (value == null || value.ToString().IsNullOrWhiteSpace())
|
||||
{
|
||||
return new XText(ConvertDbToString(propertyType, value));
|
||||
}
|
||||
|
||||
switch (ValueTypes.ToStorageType(ValueType))
|
||||
{
|
||||
case ValueStorageType.Date:
|
||||
case ValueStorageType.Integer:
|
||||
case ValueStorageType.Decimal:
|
||||
return new XText(ConvertDbToString(propertyType, value));
|
||||
case ValueStorageType.Nvarchar:
|
||||
case ValueStorageType.Ntext:
|
||||
//put text in cdata
|
||||
return new XCData(ConvertDbToString(propertyType, value));
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converts a property value to a string.
|
||||
/// </summary>
|
||||
public virtual string ConvertDbToString(IPropertyType propertyType, object value)
|
||||
{
|
||||
if (value == null)
|
||||
return string.Empty;
|
||||
|
||||
switch (ValueTypes.ToStorageType(ValueType))
|
||||
{
|
||||
case ValueStorageType.Nvarchar:
|
||||
case ValueStorageType.Ntext:
|
||||
return value.ToXmlString<string>();
|
||||
case ValueStorageType.Integer:
|
||||
case ValueStorageType.Decimal:
|
||||
return value.ToXmlString(value.GetType());
|
||||
case ValueStorageType.Date:
|
||||
//treat dates differently, output the format as xml format
|
||||
var date = value.TryConvertTo<DateTime?>();
|
||||
if (date.Success == false || date.Result == null)
|
||||
return string.Empty;
|
||||
return date.Result.ToXmlString<DateTime>();
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
using System.Collections.Generic;
|
||||
using Umbraco.Core.IO;
|
||||
|
||||
namespace Umbraco.Core.PropertyEditors
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents the configuration for the label value editor.
|
||||
/// </summary>
|
||||
public class LabelConfigurationEditor : ConfigurationEditor<LabelConfiguration>
|
||||
{
|
||||
public LabelConfigurationEditor(IIOHelper ioHelper) : base(ioHelper)
|
||||
{
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override LabelConfiguration FromConfigurationEditor(IDictionary<string, object> editorValues, LabelConfiguration configuration)
|
||||
{
|
||||
var newConfiguration = new LabelConfiguration();
|
||||
|
||||
// get the value type
|
||||
// not simply deserializing Json because we want to validate the valueType
|
||||
|
||||
if (editorValues.TryGetValue(Constants.PropertyEditors.ConfigurationKeys.DataValueType, out var valueTypeObj)
|
||||
&& valueTypeObj is string stringValue)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(stringValue) && ValueTypes.IsValue(stringValue)) // validate
|
||||
newConfiguration.ValueType = stringValue;
|
||||
}
|
||||
|
||||
return newConfiguration;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
using Umbraco.Composing;
|
||||
using Umbraco.Core.Composing;
|
||||
using Umbraco.Core.IO;
|
||||
using Umbraco.Core.Logging;
|
||||
using Umbraco.Core.Services;
|
||||
using Umbraco.Core.Strings;
|
||||
|
||||
namespace Umbraco.Core.PropertyEditors
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a property editor for label properties.
|
||||
/// </summary>
|
||||
[DataEditor(
|
||||
Constants.PropertyEditors.Aliases.Label,
|
||||
"Label",
|
||||
"readonlyvalue",
|
||||
Icon = "icon-readonly")]
|
||||
public class LabelPropertyEditor : DataEditor
|
||||
{
|
||||
private readonly IIOHelper _ioHelper;
|
||||
private readonly IDataTypeService _dataTypeService;
|
||||
private readonly ILocalizationService _localizationService;
|
||||
private readonly IShortStringHelper _shortStringHelper;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="LabelPropertyEditor"/> class.
|
||||
/// </summary>
|
||||
public LabelPropertyEditor(ILogger logger, IIOHelper ioHelper, IDataTypeService dataTypeService, ILocalizationService localizationService, IShortStringHelper shortStringHelper)
|
||||
: base(logger, dataTypeService, localizationService, shortStringHelper)
|
||||
{
|
||||
_ioHelper = ioHelper;
|
||||
_dataTypeService = dataTypeService;
|
||||
_localizationService = localizationService;
|
||||
_shortStringHelper = shortStringHelper;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override IDataValueEditor CreateValueEditor() => new LabelPropertyValueEditor(_dataTypeService, _localizationService, _shortStringHelper, Attribute);
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override IConfigurationEditor CreateConfigurationEditor() => new LabelConfigurationEditor(_ioHelper);
|
||||
|
||||
// provides the property value editor
|
||||
internal class LabelPropertyValueEditor : DataValueEditor
|
||||
{
|
||||
public LabelPropertyValueEditor(IDataTypeService dataTypeService, ILocalizationService localizationService, IShortStringHelper shortStringHelper, DataEditorAttribute attribute)
|
||||
: base(dataTypeService, localizationService, shortStringHelper, attribute)
|
||||
{ }
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool IsReadOnly => true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
using System;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Umbraco.Core.Models.PublishedContent;
|
||||
|
||||
namespace Umbraco.Core.PropertyEditors.ValueConverters
|
||||
{
|
||||
[DefaultPropertyValueConverter]
|
||||
public class ColorPickerValueConverter : PropertyValueConverterBase
|
||||
{
|
||||
public override bool IsConverter(IPublishedPropertyType propertyType)
|
||||
=> propertyType.EditorAlias.InvariantEquals(Constants.PropertyEditors.Aliases.ColorPicker);
|
||||
|
||||
public override Type GetPropertyValueType(IPublishedPropertyType propertyType)
|
||||
=> UseLabel(propertyType) ? typeof(PickedColor) : typeof(string);
|
||||
|
||||
public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType)
|
||||
=> PropertyCacheLevel.Element;
|
||||
|
||||
public override object ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object source, bool preview)
|
||||
{
|
||||
var useLabel = UseLabel(propertyType);
|
||||
|
||||
if (source == null) return useLabel ? null : string.Empty;
|
||||
|
||||
var ssource = source.ToString();
|
||||
if (ssource.DetectIsJson())
|
||||
{
|
||||
try
|
||||
{
|
||||
var jo = JsonConvert.DeserializeObject<JObject>(ssource);
|
||||
if (useLabel) return new PickedColor(jo["value"].ToString(), jo["label"].ToString());
|
||||
return jo["value"].ToString();
|
||||
}
|
||||
catch { /* not json finally */ }
|
||||
}
|
||||
|
||||
if (useLabel) return new PickedColor(ssource, ssource);
|
||||
return ssource;
|
||||
}
|
||||
|
||||
private bool UseLabel(IPublishedPropertyType propertyType)
|
||||
{
|
||||
return ConfigurationEditor.ConfigurationAs<ColorPickerConfiguration>(propertyType.DataType.Configuration).UseLabel;
|
||||
}
|
||||
|
||||
public class PickedColor
|
||||
{
|
||||
public PickedColor(string color, string label)
|
||||
{
|
||||
Color = color;
|
||||
Label = label;
|
||||
}
|
||||
|
||||
public string Color { get; }
|
||||
public string Label { get; }
|
||||
|
||||
public override string ToString() => Color;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Umbraco.Core.Composing;
|
||||
using Umbraco.Core.Configuration.Grid;
|
||||
using Umbraco.Core.Logging;
|
||||
using Umbraco.Core.Models.PublishedContent;
|
||||
using Umbraco.Composing;
|
||||
|
||||
namespace Umbraco.Core.PropertyEditors.ValueConverters
|
||||
{
|
||||
/// <summary>
|
||||
/// This ensures that the grid config is merged in with the front-end value
|
||||
/// </summary>
|
||||
[DefaultPropertyValueConverter(typeof(JsonValueConverter))] //this shadows the JsonValueConverter
|
||||
public class GridValueConverter : JsonValueConverter
|
||||
{
|
||||
private readonly IGridConfig _config;
|
||||
|
||||
public GridValueConverter(PropertyEditorCollection propertyEditors, IGridConfig config)
|
||||
: base(propertyEditors)
|
||||
{
|
||||
_config = config;
|
||||
}
|
||||
|
||||
public override bool IsConverter(IPublishedPropertyType propertyType)
|
||||
=> propertyType.EditorAlias.InvariantEquals(Constants.PropertyEditors.Aliases.Grid);
|
||||
|
||||
public override Type GetPropertyValueType(IPublishedPropertyType propertyType)
|
||||
=> typeof (JToken);
|
||||
|
||||
public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType)
|
||||
=> PropertyCacheLevel.Element;
|
||||
|
||||
public override object ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object source, bool preview)
|
||||
{
|
||||
if (source == null) return null;
|
||||
var sourceString = source.ToString();
|
||||
|
||||
if (sourceString.DetectIsJson())
|
||||
{
|
||||
try
|
||||
{
|
||||
var obj = JsonConvert.DeserializeObject<JObject>(sourceString);
|
||||
|
||||
//so we have the grid json... we need to merge in the grid's configuration values with the values
|
||||
// we've saved in the database so that when the front end gets this value, it is up-to-date.
|
||||
|
||||
var sections = GetArray(obj, "sections");
|
||||
foreach (var section in sections.Cast<JObject>())
|
||||
{
|
||||
var rows = GetArray(section, "rows");
|
||||
foreach (var row in rows.Cast<JObject>())
|
||||
{
|
||||
var areas = GetArray(row, "areas");
|
||||
foreach (var area in areas.Cast<JObject>())
|
||||
{
|
||||
var controls = GetArray(area, "controls");
|
||||
foreach (var control in controls.Cast<JObject>())
|
||||
{
|
||||
var editor = control.Value<JObject>("editor");
|
||||
if (editor != null)
|
||||
{
|
||||
var alias = editor.Value<string>("alias");
|
||||
if (alias.IsNullOrWhiteSpace() == false)
|
||||
{
|
||||
//find the alias in config
|
||||
var found = _config.EditorsConfig.Editors.FirstOrDefault(x => x.Alias == alias);
|
||||
if (found != null)
|
||||
{
|
||||
//add/replace the editor value with the one from config
|
||||
|
||||
var serialized = new JObject();
|
||||
serialized["name"] = found.Name;
|
||||
serialized["alias"] = found.Alias;
|
||||
serialized["view"] = found.View;
|
||||
serialized["render"] = found.Render;
|
||||
serialized["icon"] = found.Icon;
|
||||
serialized["config"] = JObject.FromObject(found.Config);
|
||||
|
||||
control["editor"] = serialized;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return obj;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Current.Logger.Error<GridValueConverter>(ex, "Could not parse the string '{JsonString}' to a json object", sourceString);
|
||||
}
|
||||
}
|
||||
|
||||
//it's not json, just return the string
|
||||
return sourceString;
|
||||
}
|
||||
|
||||
private JArray GetArray(JObject obj, string propertyName)
|
||||
{
|
||||
JToken token;
|
||||
if (obj.TryGetValue(propertyName, out token))
|
||||
{
|
||||
var asArray = token as JArray;
|
||||
return asArray ?? new JArray();
|
||||
}
|
||||
return new JArray();
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,398 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using System.Runtime.Serialization;
|
||||
using System.Text;
|
||||
using Newtonsoft.Json;
|
||||
using Umbraco.Core.Serialization;
|
||||
using Umbraco.Core.Strings;
|
||||
|
||||
namespace Umbraco.Core.PropertyEditors.ValueConverters
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a value of the image cropper value editor.
|
||||
/// </summary>
|
||||
[JsonConverter(typeof(NoTypeConverterJsonConverter<ImageCropperValue>))]
|
||||
[TypeConverter(typeof(ImageCropperValueTypeConverter))]
|
||||
[DataContract(Name="imageCropDataSet")]
|
||||
public class ImageCropperValue : IHtmlEncodedString, IEquatable<ImageCropperValue>
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the value source image.
|
||||
/// </summary>
|
||||
[DataMember(Name="src")]
|
||||
public string Src { get; set;}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the value focal point.
|
||||
/// </summary>
|
||||
[DataMember(Name = "focalPoint")]
|
||||
public ImageCropperFocalPoint FocalPoint { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the value crops.
|
||||
/// </summary>
|
||||
[DataMember(Name = "crops")]
|
||||
public IEnumerable<ImageCropperCrop> Crops { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public override string ToString()
|
||||
{
|
||||
return Crops != null ? (Crops.Any() ? JsonConvert.SerializeObject(this) : Src) : string.Empty;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public string ToHtmlString() => Src;
|
||||
|
||||
/// <summary>
|
||||
/// Gets a crop.
|
||||
/// </summary>
|
||||
public ImageCropperCrop GetCrop(string alias)
|
||||
{
|
||||
if (Crops == null)
|
||||
return null;
|
||||
|
||||
return string.IsNullOrWhiteSpace(alias)
|
||||
? Crops.FirstOrDefault()
|
||||
: Crops.FirstOrDefault(x => x.Alias.InvariantEquals(alias));
|
||||
}
|
||||
|
||||
public void AppendCropBaseUrl(StringBuilder url, ImageCropperCrop crop, bool defaultCrop, bool preferFocalPoint)
|
||||
{
|
||||
if (preferFocalPoint && HasFocalPoint()
|
||||
|| crop != null && crop.Coordinates == null && HasFocalPoint()
|
||||
|| defaultCrop && HasFocalPoint())
|
||||
{
|
||||
url.Append("?center=");
|
||||
url.Append(FocalPoint.Top.ToString(CultureInfo.InvariantCulture));
|
||||
url.Append(",");
|
||||
url.Append(FocalPoint.Left.ToString(CultureInfo.InvariantCulture));
|
||||
url.Append("&mode=crop");
|
||||
}
|
||||
else if (crop != null && crop.Coordinates != null && preferFocalPoint == false)
|
||||
{
|
||||
url.Append("?crop=");
|
||||
url.Append(crop.Coordinates.X1.ToString(CultureInfo.InvariantCulture)).Append(",");
|
||||
url.Append(crop.Coordinates.Y1.ToString(CultureInfo.InvariantCulture)).Append(",");
|
||||
url.Append(crop.Coordinates.X2.ToString(CultureInfo.InvariantCulture)).Append(",");
|
||||
url.Append(crop.Coordinates.Y2.ToString(CultureInfo.InvariantCulture));
|
||||
url.Append("&cropmode=percentage");
|
||||
}
|
||||
else
|
||||
{
|
||||
url.Append("?anchor=center");
|
||||
url.Append("&mode=crop");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the value image url for a specified crop.
|
||||
/// </summary>
|
||||
public string GetCropUrl(string alias, bool useCropDimensions = true, bool useFocalPoint = false, string cacheBusterValue = null)
|
||||
{
|
||||
var crop = GetCrop(alias);
|
||||
|
||||
// could not find a crop with the specified, non-empty, alias
|
||||
if (crop == null && !string.IsNullOrWhiteSpace(alias))
|
||||
return null;
|
||||
|
||||
var url = new StringBuilder();
|
||||
|
||||
AppendCropBaseUrl(url, crop, string.IsNullOrWhiteSpace(alias), useFocalPoint);
|
||||
|
||||
if (crop != null && useCropDimensions)
|
||||
{
|
||||
url.Append("&width=").Append(crop.Width);
|
||||
url.Append("&height=").Append(crop.Height);
|
||||
}
|
||||
|
||||
if (cacheBusterValue != null)
|
||||
url.Append("&rnd=").Append(cacheBusterValue);
|
||||
|
||||
return url.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the value image url for a specific width and height.
|
||||
/// </summary>
|
||||
public string GetCropUrl(int width, int height, bool useFocalPoint = false, string cacheBusterValue = null)
|
||||
{
|
||||
var url = new StringBuilder();
|
||||
|
||||
AppendCropBaseUrl(url, null, true, useFocalPoint);
|
||||
|
||||
url.Append("&width=").Append(width);
|
||||
url.Append("&height=").Append(height);
|
||||
|
||||
if (cacheBusterValue != null)
|
||||
url.Append("&rnd=").Append(cacheBusterValue);
|
||||
|
||||
return url.ToString();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the value has a focal point.
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
public bool HasFocalPoint()
|
||||
=> FocalPoint != null && (FocalPoint.Left != 0.5m || FocalPoint.Top != 0.5m);
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the value has a specified crop.
|
||||
/// </summary>
|
||||
public bool HasCrop(string alias)
|
||||
=> Crops.Any(x => x.Alias == alias);
|
||||
|
||||
/// <summary>
|
||||
/// Determines whether the value has a source image.
|
||||
/// </summary>
|
||||
public bool HasImage()
|
||||
=> !string.IsNullOrWhiteSpace(Src);
|
||||
|
||||
/// <summary>
|
||||
/// Applies a configuration.
|
||||
/// </summary>
|
||||
/// <remarks>Ensures that all crops defined in the configuration exists in the value.</remarks>
|
||||
public void ApplyConfiguration(ImageCropperConfiguration configuration)
|
||||
{
|
||||
// merge the crop values - the alias + width + height comes from
|
||||
// configuration, but each crop can store its own coordinates
|
||||
|
||||
if (Crops == null) return;
|
||||
|
||||
var configuredCrops = configuration?.Crops;
|
||||
if (configuredCrops == null) return;
|
||||
|
||||
var crops = Crops.ToList();
|
||||
|
||||
foreach (var configuredCrop in configuredCrops)
|
||||
{
|
||||
var crop = crops.FirstOrDefault(x => x.Alias == configuredCrop.Alias);
|
||||
if (crop != null)
|
||||
{
|
||||
// found, apply the height & width
|
||||
crop.Width = configuredCrop.Width;
|
||||
crop.Height = configuredCrop.Height;
|
||||
}
|
||||
else
|
||||
{
|
||||
// not found, add
|
||||
crops.Add(new ImageCropperCrop
|
||||
{
|
||||
Alias = configuredCrop.Alias,
|
||||
Width = configuredCrop.Width,
|
||||
Height = configuredCrop.Height
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// assume we don't have to remove the crops in value, that
|
||||
// are not part of configuration anymore?
|
||||
|
||||
Crops = crops;
|
||||
}
|
||||
|
||||
#region IEquatable
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool Equals(ImageCropperValue other)
|
||||
=> ReferenceEquals(this, other) || Equals(this, other);
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool Equals(object obj)
|
||||
=> ReferenceEquals(this, obj) || obj is ImageCropperValue other && Equals(this, other);
|
||||
|
||||
private static bool Equals(ImageCropperValue left, ImageCropperValue right)
|
||||
=> ReferenceEquals(left, right) // deals with both being null, too
|
||||
|| !ReferenceEquals(left, null) && !ReferenceEquals(right, null)
|
||||
&& string.Equals(left.Src, right.Src)
|
||||
&& Equals(left.FocalPoint, right.FocalPoint)
|
||||
&& left.ComparableCrops.SequenceEqual(right.ComparableCrops);
|
||||
|
||||
private IEnumerable<ImageCropperCrop> ComparableCrops
|
||||
=> Crops?.OrderBy(x => x.Alias) ?? Enumerable.Empty<ImageCropperCrop>();
|
||||
|
||||
public static bool operator ==(ImageCropperValue left, ImageCropperValue right)
|
||||
=> Equals(left, right);
|
||||
|
||||
public static bool operator !=(ImageCropperValue left, ImageCropperValue right)
|
||||
=> !Equals(left, right);
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
unchecked
|
||||
{
|
||||
// properties are, practically, readonly
|
||||
// ReSharper disable NonReadonlyMemberInGetHashCode
|
||||
var hashCode = Src?.GetHashCode() ?? 0;
|
||||
hashCode = (hashCode*397) ^ (FocalPoint?.GetHashCode() ?? 0);
|
||||
hashCode = (hashCode*397) ^ (Crops?.GetHashCode() ?? 0);
|
||||
return hashCode;
|
||||
// ReSharper restore NonReadonlyMemberInGetHashCode
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
[DataContract(Name = "imageCropFocalPoint")]
|
||||
public class ImageCropperFocalPoint : IEquatable<ImageCropperFocalPoint>
|
||||
{
|
||||
[DataMember(Name = "left")]
|
||||
public decimal Left { get; set; }
|
||||
|
||||
[DataMember(Name = "top")]
|
||||
public decimal Top { get; set; }
|
||||
|
||||
#region IEquatable
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool Equals(ImageCropperFocalPoint other)
|
||||
=> ReferenceEquals(this, other) || Equals(this, other);
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool Equals(object obj)
|
||||
=> ReferenceEquals(this, obj) || obj is ImageCropperFocalPoint other && Equals(this, other);
|
||||
|
||||
private static bool Equals(ImageCropperFocalPoint left, ImageCropperFocalPoint right)
|
||||
=> ReferenceEquals(left, right) // deals with both being null, too
|
||||
|| !ReferenceEquals(left, null) && !ReferenceEquals(right, null)
|
||||
&& left.Left == right.Left
|
||||
&& left.Top == right.Top;
|
||||
|
||||
public static bool operator ==(ImageCropperFocalPoint left, ImageCropperFocalPoint right)
|
||||
=> Equals(left, right);
|
||||
|
||||
public static bool operator !=(ImageCropperFocalPoint left, ImageCropperFocalPoint right)
|
||||
=> !Equals(left, right);
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
unchecked
|
||||
{
|
||||
// properties are, practically, readonly
|
||||
// ReSharper disable NonReadonlyMemberInGetHashCode
|
||||
return (Left.GetHashCode()*397) ^ Top.GetHashCode();
|
||||
// ReSharper restore NonReadonlyMemberInGetHashCode
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
[DataContract(Name = "imageCropData")]
|
||||
public class ImageCropperCrop : IEquatable<ImageCropperCrop>
|
||||
{
|
||||
[DataMember(Name = "alias")]
|
||||
public string Alias { get; set; }
|
||||
|
||||
[DataMember(Name = "width")]
|
||||
public int Width { get; set; }
|
||||
|
||||
[DataMember(Name = "height")]
|
||||
public int Height { get; set; }
|
||||
|
||||
[DataMember(Name = "coordinates")]
|
||||
public ImageCropperCropCoordinates Coordinates { get; set; }
|
||||
|
||||
#region IEquatable
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool Equals(ImageCropperCrop other)
|
||||
=> ReferenceEquals(this, other) || Equals(this, other);
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool Equals(object obj)
|
||||
=> ReferenceEquals(this, obj) || obj is ImageCropperCrop other && Equals(this, other);
|
||||
|
||||
private static bool Equals(ImageCropperCrop left, ImageCropperCrop right)
|
||||
=> ReferenceEquals(left, right) // deals with both being null, too
|
||||
|| !ReferenceEquals(left, null) && !ReferenceEquals(right, null)
|
||||
&& string.Equals(left.Alias, right.Alias)
|
||||
&& left.Width == right.Width
|
||||
&& left.Height == right.Height
|
||||
&& Equals(left.Coordinates, right.Coordinates);
|
||||
|
||||
public static bool operator ==(ImageCropperCrop left, ImageCropperCrop right)
|
||||
=> Equals(left, right);
|
||||
|
||||
public static bool operator !=(ImageCropperCrop left, ImageCropperCrop right)
|
||||
=> !Equals(left, right);
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
unchecked
|
||||
{
|
||||
// properties are, practically, readonly
|
||||
// ReSharper disable NonReadonlyMemberInGetHashCode
|
||||
var hashCode = Alias?.GetHashCode() ?? 0;
|
||||
hashCode = (hashCode*397) ^ Width;
|
||||
hashCode = (hashCode*397) ^ Height;
|
||||
hashCode = (hashCode*397) ^ (Coordinates?.GetHashCode() ?? 0);
|
||||
return hashCode;
|
||||
// ReSharper restore NonReadonlyMemberInGetHashCode
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
|
||||
[DataContract(Name = "imageCropCoordinates")]
|
||||
public class ImageCropperCropCoordinates : IEquatable<ImageCropperCropCoordinates>
|
||||
{
|
||||
[DataMember(Name = "x1")]
|
||||
public decimal X1 { get; set; }
|
||||
|
||||
[DataMember(Name = "y1")]
|
||||
public decimal Y1 { get; set; }
|
||||
|
||||
[DataMember(Name = "x2")]
|
||||
public decimal X2 { get; set; }
|
||||
|
||||
[DataMember(Name = "y2")]
|
||||
public decimal Y2 { get; set; }
|
||||
|
||||
#region IEquatable
|
||||
|
||||
/// <inheritdoc />
|
||||
public bool Equals(ImageCropperCropCoordinates other)
|
||||
=> ReferenceEquals(this, other) || Equals(this, other);
|
||||
|
||||
/// <inheritdoc />
|
||||
public override bool Equals(object obj)
|
||||
=> ReferenceEquals(this, obj) || obj is ImageCropperCropCoordinates other && Equals(this, other);
|
||||
|
||||
private static bool Equals(ImageCropperCropCoordinates left, ImageCropperCropCoordinates right)
|
||||
=> ReferenceEquals(left, right) // deals with both being null, too
|
||||
|| !ReferenceEquals(left, null) && !ReferenceEquals(right, null)
|
||||
&& left.X1 == right.X1
|
||||
&& left.X2 == right.X2
|
||||
&& left.Y1 == right.Y1
|
||||
&& left.Y2 == right.Y2;
|
||||
|
||||
public static bool operator ==(ImageCropperCropCoordinates left, ImageCropperCropCoordinates right)
|
||||
=> Equals(left, right);
|
||||
|
||||
public static bool operator !=(ImageCropperCropCoordinates left, ImageCropperCropCoordinates right)
|
||||
=> !Equals(left, right);
|
||||
|
||||
public override int GetHashCode()
|
||||
{
|
||||
unchecked
|
||||
{
|
||||
// properties are, practically, readonly
|
||||
// ReSharper disable NonReadonlyMemberInGetHashCode
|
||||
var hashCode = X1.GetHashCode();
|
||||
hashCode = (hashCode*397) ^ Y1.GetHashCode();
|
||||
hashCode = (hashCode*397) ^ X2.GetHashCode();
|
||||
hashCode = (hashCode*397) ^ Y2.GetHashCode();
|
||||
return hashCode;
|
||||
// ReSharper restore NonReadonlyMemberInGetHashCode
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using Newtonsoft.Json;
|
||||
using Umbraco.Core.Composing;
|
||||
using Umbraco.Core.Logging;
|
||||
using Umbraco.Core.Models.PublishedContent;
|
||||
using Umbraco.Composing;
|
||||
|
||||
namespace Umbraco.Core.PropertyEditors.ValueConverters
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a value converter for the image cropper value editor.
|
||||
/// </summary>
|
||||
[DefaultPropertyValueConverter(typeof(JsonValueConverter))]
|
||||
public class ImageCropperValueConverter : PropertyValueConverterBase
|
||||
{
|
||||
/// <inheritdoc />
|
||||
public override bool IsConverter(IPublishedPropertyType propertyType)
|
||||
=> propertyType.EditorAlias.InvariantEquals(Constants.PropertyEditors.Aliases.ImageCropper);
|
||||
|
||||
/// <inheritdoc />
|
||||
public override Type GetPropertyValueType(IPublishedPropertyType propertyType)
|
||||
=> typeof (ImageCropperValue);
|
||||
|
||||
/// <inheritdoc />
|
||||
public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType)
|
||||
=> PropertyCacheLevel.Element;
|
||||
|
||||
/// <inheritdoc />
|
||||
public override object ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object source, bool preview)
|
||||
{
|
||||
if (source == null) return null;
|
||||
var sourceString = source.ToString();
|
||||
|
||||
ImageCropperValue value;
|
||||
try
|
||||
{
|
||||
value = JsonConvert.DeserializeObject<ImageCropperValue>(sourceString, new JsonSerializerSettings
|
||||
{
|
||||
Culture = CultureInfo.InvariantCulture,
|
||||
FloatParseHandling = FloatParseHandling.Decimal
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// cannot deserialize, assume it may be a raw image url
|
||||
Current.Logger.Error<ImageCropperValueConverter>(ex, "Could not deserialize string '{JsonString}' into an image cropper value.", sourceString);
|
||||
value = new ImageCropperValue { Src = sourceString };
|
||||
}
|
||||
|
||||
value.ApplyConfiguration(propertyType.DataType.ConfigurationAs<ImageCropperConfiguration>());
|
||||
|
||||
return value;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
using System;
|
||||
using System.ComponentModel;
|
||||
using System.Globalization;
|
||||
using System.Linq;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Umbraco.Core.Composing;
|
||||
|
||||
namespace Umbraco.Core.PropertyEditors.ValueConverters
|
||||
{
|
||||
/// <summary>
|
||||
/// Converts <see cref="ImageCropperValue"/> to string or JObject (why?).
|
||||
/// </summary>
|
||||
public class ImageCropperValueTypeConverter : TypeConverter
|
||||
{
|
||||
private static readonly Type[] ConvertableTypes = { typeof(JObject) };
|
||||
|
||||
public override bool CanConvertTo(ITypeDescriptorContext context, Type destinationType)
|
||||
{
|
||||
return ConvertableTypes.Any(x => TypeHelper.IsTypeAssignableFrom(x, destinationType))
|
||||
|| CanConvertFrom(context, destinationType);
|
||||
}
|
||||
|
||||
public override object ConvertTo(ITypeDescriptorContext context, CultureInfo culture, object value, Type destinationType)
|
||||
{
|
||||
var cropperValue = value as ImageCropperValue;
|
||||
if (cropperValue == null)
|
||||
return null;
|
||||
|
||||
return TypeHelper.IsTypeAssignableFrom<JObject>(destinationType)
|
||||
? JObject.FromObject(cropperValue)
|
||||
: base.ConvertTo(context, culture, value, destinationType);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
using System;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Umbraco.Core.Composing;
|
||||
using Umbraco.Core.Logging;
|
||||
using Umbraco.Core.Models.PublishedContent;
|
||||
using Umbraco.Composing;
|
||||
|
||||
namespace Umbraco.Core.PropertyEditors.ValueConverters
|
||||
{
|
||||
/// <summary>
|
||||
/// The default converter for all property editors that expose a JSON value type
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Since this is a default (umbraco) converter it will be ignored if another converter found conflicts with this one.
|
||||
/// </remarks>
|
||||
[DefaultPropertyValueConverter]
|
||||
public class JsonValueConverter : PropertyValueConverterBase
|
||||
{
|
||||
private readonly PropertyEditorCollection _propertyEditors;
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="JsonValueConverter"/> class.
|
||||
/// </summary>
|
||||
public JsonValueConverter(PropertyEditorCollection propertyEditors)
|
||||
{
|
||||
_propertyEditors = propertyEditors;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// It is a converter for any value type that is "JSON"
|
||||
/// </summary>
|
||||
/// <param name="propertyType"></param>
|
||||
/// <returns></returns>
|
||||
public override bool IsConverter(IPublishedPropertyType propertyType)
|
||||
{
|
||||
return _propertyEditors.TryGet(propertyType.EditorAlias, out var editor)
|
||||
&& editor.GetValueEditor().ValueType.InvariantEquals(ValueTypes.Json);
|
||||
}
|
||||
|
||||
public override Type GetPropertyValueType(IPublishedPropertyType propertyType)
|
||||
=> typeof (JToken);
|
||||
|
||||
public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType)
|
||||
=> PropertyCacheLevel.Element;
|
||||
|
||||
public override object ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object source, bool preview)
|
||||
{
|
||||
if (source == null) return null;
|
||||
var sourceString = source.ToString();
|
||||
|
||||
if (sourceString.DetectIsJson())
|
||||
{
|
||||
try
|
||||
{
|
||||
var obj = JsonConvert.DeserializeObject(sourceString);
|
||||
return obj;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Current.Logger.Error<JsonValueConverter>(ex, "Could not parse the string '{JsonString}' to a json object", sourceString);
|
||||
}
|
||||
}
|
||||
|
||||
//it's not json, just return the string
|
||||
return sourceString;
|
||||
}
|
||||
|
||||
// TODO: Now to convert that to XPath!
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
using System;
|
||||
using System.Globalization;
|
||||
using Umbraco.Core.Models.PublishedContent;
|
||||
|
||||
namespace Umbraco.Core.PropertyEditors.ValueConverters
|
||||
{
|
||||
/// <summary>
|
||||
/// We need this property converter so that we always force the value of a label to be a string
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Without a property converter defined for the label type, the value will be converted with
|
||||
/// the `ConvertUsingDarkMagic` method which will try to parse the value into it's correct type, but this
|
||||
/// can cause issues if the string is detected as a number and then strips leading zeros.
|
||||
/// Example: http://issues.umbraco.org/issue/U4-7929
|
||||
/// </remarks>
|
||||
[DefaultPropertyValueConverter]
|
||||
public class LabelValueConverter : PropertyValueConverterBase
|
||||
{
|
||||
public override bool IsConverter(IPublishedPropertyType propertyType)
|
||||
=> Constants.PropertyEditors.Aliases.Label.Equals(propertyType.EditorAlias);
|
||||
|
||||
public override Type GetPropertyValueType(IPublishedPropertyType propertyType)
|
||||
{
|
||||
var valueType = ConfigurationEditor.ConfigurationAs<LabelConfiguration>(propertyType.DataType.Configuration);
|
||||
switch (valueType.ValueType)
|
||||
{
|
||||
case ValueTypes.DateTime:
|
||||
case ValueTypes.Date:
|
||||
return typeof(DateTime);
|
||||
case ValueTypes.Time:
|
||||
return typeof(TimeSpan);
|
||||
case ValueTypes.Decimal:
|
||||
return typeof(decimal);
|
||||
case ValueTypes.Integer:
|
||||
return typeof(int);
|
||||
case ValueTypes.Bigint:
|
||||
return typeof(long);
|
||||
default: // everything else is a string
|
||||
return typeof(string);
|
||||
}
|
||||
}
|
||||
|
||||
public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType)
|
||||
=> PropertyCacheLevel.Element;
|
||||
|
||||
public override object ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object source, bool preview)
|
||||
{
|
||||
var valueType = ConfigurationEditor.ConfigurationAs<LabelConfiguration>(propertyType.DataType.Configuration);
|
||||
switch (valueType.ValueType)
|
||||
{
|
||||
case ValueTypes.DateTime:
|
||||
case ValueTypes.Date:
|
||||
if (source is DateTime sourceDateTime)
|
||||
return sourceDateTime;
|
||||
if (source is string sourceDateTimeString)
|
||||
return DateTime.TryParse(sourceDateTimeString, CultureInfo.InvariantCulture, DateTimeStyles.None, out var dt) ? dt : DateTime.MinValue;
|
||||
return DateTime.MinValue;
|
||||
case ValueTypes.Time:
|
||||
if (source is DateTime sourceTime)
|
||||
return sourceTime.TimeOfDay;
|
||||
if (source is string sourceTimeString)
|
||||
return TimeSpan.TryParse(sourceTimeString, CultureInfo.InvariantCulture, out var ts) ? ts : TimeSpan.Zero;
|
||||
return TimeSpan.Zero;
|
||||
case ValueTypes.Decimal:
|
||||
if (source is decimal sourceDecimal) return sourceDecimal;
|
||||
if (source is string sourceDecimalString)
|
||||
return decimal.TryParse(sourceDecimalString, NumberStyles.Any, CultureInfo.InvariantCulture, out var d) ? d : 0;
|
||||
if (source is double sourceDouble)
|
||||
return Convert.ToDecimal(sourceDouble);
|
||||
return (decimal) 0;
|
||||
case ValueTypes.Integer:
|
||||
if (source is int sourceInt) return sourceInt;
|
||||
if (source is string sourceIntString)
|
||||
return int.TryParse(sourceIntString, out var i) ? i : 0;
|
||||
return 0;
|
||||
case ValueTypes.Bigint:
|
||||
if (source is string sourceLongString)
|
||||
return long.TryParse(sourceLongString, out var i) ? i : 0;
|
||||
return (long) 0;
|
||||
default: // everything else is a string
|
||||
return source?.ToString() ?? string.Empty;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
using System;
|
||||
using Umbraco.Core.Models.PublishedContent;
|
||||
using Umbraco.Core.Strings;
|
||||
|
||||
namespace Umbraco.Core.PropertyEditors.ValueConverters
|
||||
{
|
||||
[DefaultPropertyValueConverter]
|
||||
public class MarkdownEditorValueConverter : PropertyValueConverterBase
|
||||
{
|
||||
public override bool IsConverter(IPublishedPropertyType propertyType)
|
||||
=> Constants.PropertyEditors.Aliases.MarkdownEditor.Equals(propertyType.EditorAlias);
|
||||
|
||||
public override Type GetPropertyValueType(IPublishedPropertyType propertyType)
|
||||
=> typeof (IHtmlEncodedString);
|
||||
|
||||
// PropertyCacheLevel.Content is ok here because that converter does not parse {locallink} nor executes macros
|
||||
public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType)
|
||||
=> PropertyCacheLevel.Element;
|
||||
|
||||
public override object ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object source, bool preview)
|
||||
{
|
||||
// in xml a string is: string
|
||||
// in the database a string is: string
|
||||
// default value is: null
|
||||
return source;
|
||||
}
|
||||
|
||||
public override object ConvertIntermediateToObject(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object inter, bool preview)
|
||||
{
|
||||
// source should come from ConvertSource and be a string (or null) already
|
||||
return new HtmlEncodedString(inter == null ? string.Empty : (string) inter);
|
||||
}
|
||||
|
||||
public override object ConvertIntermediateToXPath(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object inter, bool preview)
|
||||
{
|
||||
// source should come from ConvertSource and be a string (or null) already
|
||||
return inter?.ToString() ?? string.Empty;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Umbraco.Core.Models;
|
||||
using Umbraco.Core.Models.PublishedContent;
|
||||
using Umbraco.Core.Services;
|
||||
|
||||
namespace Umbraco.Core.PropertyEditors.ValueConverters
|
||||
{
|
||||
[DefaultPropertyValueConverter]
|
||||
public class SliderValueConverter : PropertyValueConverterBase
|
||||
{
|
||||
private readonly IDataTypeService _dataTypeService;
|
||||
|
||||
public SliderValueConverter(IDataTypeService dataTypeService)
|
||||
{
|
||||
_dataTypeService = dataTypeService ?? throw new ArgumentNullException(nameof(dataTypeService));
|
||||
}
|
||||
|
||||
public override bool IsConverter(IPublishedPropertyType propertyType)
|
||||
=> propertyType.EditorAlias.InvariantEquals(Constants.PropertyEditors.Aliases.Slider);
|
||||
|
||||
public override Type GetPropertyValueType(IPublishedPropertyType propertyType)
|
||||
=> IsRangeDataType(propertyType.DataType.Id) ? typeof (Range<decimal>) : typeof (decimal);
|
||||
|
||||
public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType)
|
||||
=> PropertyCacheLevel.Element;
|
||||
|
||||
public override object ConvertIntermediateToObject(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel cacheLevel, object source, bool preview)
|
||||
{
|
||||
if (source == null)
|
||||
return null;
|
||||
|
||||
if (IsRangeDataType(propertyType.DataType.Id))
|
||||
{
|
||||
var rangeRawValues = source.ToString().Split(',');
|
||||
var minimumAttempt = rangeRawValues[0].TryConvertTo<decimal>();
|
||||
var maximumAttempt = rangeRawValues[1].TryConvertTo<decimal>();
|
||||
|
||||
if (minimumAttempt.Success && maximumAttempt.Success)
|
||||
{
|
||||
return new Range<decimal> { Maximum = maximumAttempt.Result, Minimum = minimumAttempt.Result };
|
||||
}
|
||||
}
|
||||
|
||||
var valueAttempt = source.ToString().TryConvertTo<decimal>();
|
||||
if (valueAttempt.Success)
|
||||
return valueAttempt.Result;
|
||||
|
||||
// Something failed in the conversion of the strings to decimals
|
||||
return null;
|
||||
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Discovers if the slider is set to range mode.
|
||||
/// </summary>
|
||||
/// <param name="dataTypeId">
|
||||
/// The data type id.
|
||||
/// </param>
|
||||
/// <returns>
|
||||
/// The <see cref="bool"/>.
|
||||
/// </returns>
|
||||
private bool IsRangeDataType(int dataTypeId)
|
||||
{
|
||||
// GetPreValuesCollectionByDataTypeId is cached at repository level;
|
||||
// still, the collection is deep-cloned so this is kinda expensive,
|
||||
// better to cache here + trigger refresh in DataTypeCacheRefresher
|
||||
// TODO: this is cheap now, remove the caching
|
||||
|
||||
return Storages.GetOrAdd(dataTypeId, id =>
|
||||
{
|
||||
var dataType = _dataTypeService.GetDataType(id);
|
||||
var configuration = dataType.ConfigurationAs<SliderConfiguration>();
|
||||
return configuration.EnableRange;
|
||||
});
|
||||
}
|
||||
|
||||
private static readonly ConcurrentDictionary<int, bool> Storages = new ConcurrentDictionary<int, bool>();
|
||||
|
||||
public static void ClearCaches()
|
||||
{
|
||||
Storages.Clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Umbraco.Core.Models;
|
||||
using Umbraco.Core.Models.PublishedContent;
|
||||
using Umbraco.Core.Services;
|
||||
|
||||
namespace Umbraco.Core.PropertyEditors.ValueConverters
|
||||
{
|
||||
[DefaultPropertyValueConverter]
|
||||
public class TagsValueConverter : PropertyValueConverterBase
|
||||
{
|
||||
private readonly IDataTypeService _dataTypeService;
|
||||
|
||||
public TagsValueConverter(IDataTypeService dataTypeService)
|
||||
{
|
||||
_dataTypeService = dataTypeService ?? throw new ArgumentNullException(nameof(dataTypeService));
|
||||
}
|
||||
|
||||
public override bool IsConverter(IPublishedPropertyType propertyType)
|
||||
=> propertyType.EditorAlias.InvariantEquals(Constants.PropertyEditors.Aliases.Tags);
|
||||
|
||||
public override Type GetPropertyValueType(IPublishedPropertyType propertyType)
|
||||
=> typeof (IEnumerable<string>);
|
||||
|
||||
public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType)
|
||||
=> PropertyCacheLevel.Element;
|
||||
|
||||
public override object ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object source, bool preview)
|
||||
{
|
||||
if (source == null) return Array.Empty<string>();
|
||||
|
||||
// if Json storage type deserialize and return as string array
|
||||
if (JsonStorageType(propertyType.DataType.Id))
|
||||
{
|
||||
var jArray = JsonConvert.DeserializeObject<JArray>(source.ToString());
|
||||
return jArray.ToObject<string[]>() ?? Array.Empty<string>();
|
||||
}
|
||||
|
||||
// Otherwise assume CSV storage type and return as string array
|
||||
return source.ToString().Split(new[] { "," }, StringSplitOptions.RemoveEmptyEntries);
|
||||
}
|
||||
|
||||
public override object ConvertIntermediateToObject(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel cacheLevel, object source, bool preview)
|
||||
{
|
||||
return (string[]) source;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Discovers if the tags data type is storing its data in a Json format
|
||||
/// </summary>
|
||||
/// <param name="dataTypeId">
|
||||
/// The data type id.
|
||||
/// </param>
|
||||
/// <returns>
|
||||
/// The <see cref="bool"/>.
|
||||
/// </returns>
|
||||
private bool JsonStorageType(int dataTypeId)
|
||||
{
|
||||
// GetDataType(id) is cached at repository level; still, there is some
|
||||
// deep-cloning involved (expensive) - better cache here + trigger
|
||||
// refresh in DataTypeCacheRefresher
|
||||
|
||||
return Storages.GetOrAdd(dataTypeId, id =>
|
||||
{
|
||||
var configuration = _dataTypeService.GetDataType(id).ConfigurationAs<TagConfiguration>();
|
||||
return configuration.StorageType == TagsStorageType.Json;
|
||||
});
|
||||
}
|
||||
|
||||
private static readonly ConcurrentDictionary<int, bool> Storages = new ConcurrentDictionary<int, bool>();
|
||||
|
||||
public static void ClearCaches()
|
||||
{
|
||||
Storages.Clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
using System;
|
||||
using Umbraco.Core.Models.PublishedContent;
|
||||
using Umbraco.Core.Strings;
|
||||
|
||||
namespace Umbraco.Core.PropertyEditors.ValueConverters
|
||||
{
|
||||
/// <summary>
|
||||
/// Value converter for the RTE so that it always returns IHtmlString so that Html.Raw doesn't have to be used.
|
||||
/// </summary>
|
||||
[DefaultPropertyValueConverter]
|
||||
public class TinyMceValueConverter : PropertyValueConverterBase
|
||||
{
|
||||
public override bool IsConverter(IPublishedPropertyType propertyType)
|
||||
=> propertyType.EditorAlias == Constants.PropertyEditors.Aliases.TinyMce;
|
||||
|
||||
public override Type GetPropertyValueType(IPublishedPropertyType propertyType)
|
||||
=> typeof (IHtmlEncodedString);
|
||||
|
||||
// PropertyCacheLevel.Content is ok here because that converter does not parse {locallink} nor executes macros
|
||||
public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType)
|
||||
=> PropertyCacheLevel.Element;
|
||||
|
||||
public override object ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object source, bool preview)
|
||||
{
|
||||
// in xml a string is: string
|
||||
// in the database a string is: string
|
||||
// default value is: null
|
||||
return source;
|
||||
}
|
||||
|
||||
public override object ConvertIntermediateToObject(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object inter, bool preview)
|
||||
{
|
||||
// source should come from ConvertSource and be a string (or null) already
|
||||
return new HtmlEncodedString(inter == null ? string.Empty : (string)inter);
|
||||
}
|
||||
|
||||
public override object ConvertIntermediateToXPath(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object inter, bool preview)
|
||||
{
|
||||
// source should come from ConvertSource and be a string (or null) already
|
||||
return inter;
|
||||
}
|
||||
}
|
||||
}
|
||||
41
src/Umbraco.Infrastructure/PropertyEditors/VoidEditor.cs
Normal file
41
src/Umbraco.Infrastructure/PropertyEditors/VoidEditor.cs
Normal file
@@ -0,0 +1,41 @@
|
||||
using Umbraco.Core.Composing;
|
||||
using Umbraco.Core.Logging;
|
||||
using Umbraco.Core.Services;
|
||||
using Umbraco.Core.Strings;
|
||||
|
||||
namespace Umbraco.Core.PropertyEditors
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a void editor.
|
||||
/// </summary>
|
||||
/// <remarks>Can be used in some places where an editor is needed but no actual
|
||||
/// editor is available. Not to be used otherwise. Not discovered, and therefore
|
||||
/// not part of the editors collection.</remarks>
|
||||
[HideFromTypeFinder]
|
||||
public class VoidEditor : DataEditor
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="VoidEditor"/> class.
|
||||
/// </summary>
|
||||
/// <param name="aliasSuffix">An optional alias suffix.</param>
|
||||
/// <param name="logger">A logger.</param>
|
||||
/// <remarks>The default alias of the editor is "Umbraco.Void". When a suffix is provided,
|
||||
/// it is appended to the alias. Eg if the suffix is "Foo" the alias is "Umbraco.Void.Foo".</remarks>
|
||||
public VoidEditor(string aliasSuffix, ILogger logger, IDataTypeService dataTypeService, ILocalizationService localizationService, IShortStringHelper shortStringHelper)
|
||||
: base(logger, dataTypeService, localizationService, shortStringHelper)
|
||||
{
|
||||
Alias = "Umbraco.Void";
|
||||
if (string.IsNullOrWhiteSpace(aliasSuffix)) return;
|
||||
Alias += "." + aliasSuffix;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="VoidEditor"/> class.
|
||||
/// </summary>
|
||||
/// <param name="logger">A logger.</param>
|
||||
/// <remarks>The alias of the editor is "Umbraco.Void".</remarks>
|
||||
public VoidEditor(ILogger logger, IDataTypeService dataTypeService, ILocalizationService localizationService, IShortStringHelper shortStringHelper)
|
||||
: this(null, logger, dataTypeService, localizationService, shortStringHelper)
|
||||
{ }
|
||||
}
|
||||
}
|
||||
@@ -41,4 +41,16 @@
|
||||
<ProjectReference Include="..\Umbraco.Abstractions\Umbraco.Abstractions.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleTo">
|
||||
<_Parameter1>Umbraco.Core</_Parameter1>
|
||||
</AssemblyAttribute>
|
||||
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleTo">
|
||||
<_Parameter1>Umbraco.Tests</_Parameter1>
|
||||
</AssemblyAttribute>
|
||||
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleTo">
|
||||
<_Parameter1>Umbraco.Tests.Benchmarks</_Parameter1>
|
||||
</AssemblyAttribute>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
Reference in New Issue
Block a user