Retreat Amazing Nested Content Work
This commit is contained in:
166
src/Umbraco.Web/Models/DetachedPublishedContent.cs
Normal file
166
src/Umbraco.Web/Models/DetachedPublishedContent.cs
Normal file
@@ -0,0 +1,166 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Umbraco.Core;
|
||||
using Umbraco.Core.Models;
|
||||
using Umbraco.Core.Models.PublishedContent;
|
||||
|
||||
namespace Umbraco.Web.Models
|
||||
{
|
||||
public class DetachedPublishedContent : PublishedContentWithKeyBase
|
||||
{
|
||||
private readonly Guid _key;
|
||||
private readonly string _name;
|
||||
private readonly PublishedContentType _contentType;
|
||||
private readonly IEnumerable<IPublishedProperty> _properties;
|
||||
private readonly int _sortOrder;
|
||||
private readonly bool _isPreviewing;
|
||||
private readonly IPublishedContent _containerNode;
|
||||
|
||||
public DetachedPublishedContent(
|
||||
Guid key,
|
||||
string name,
|
||||
PublishedContentType contentType,
|
||||
IEnumerable<IPublishedProperty> properties,
|
||||
IPublishedContent containerNode = null,
|
||||
int sortOrder = 0,
|
||||
bool isPreviewing = false)
|
||||
{
|
||||
_key = key;
|
||||
_name = name;
|
||||
_contentType = contentType;
|
||||
_properties = properties;
|
||||
_sortOrder = sortOrder;
|
||||
_isPreviewing = isPreviewing;
|
||||
_containerNode = containerNode;
|
||||
}
|
||||
|
||||
public override Guid Key
|
||||
{
|
||||
get { return _key; }
|
||||
}
|
||||
|
||||
public override int Id
|
||||
{
|
||||
get { return 0; }
|
||||
}
|
||||
|
||||
public override string Name
|
||||
{
|
||||
get { return _name; }
|
||||
}
|
||||
|
||||
public override bool IsDraft
|
||||
{
|
||||
get { return _isPreviewing; }
|
||||
}
|
||||
|
||||
public override PublishedItemType ItemType
|
||||
{
|
||||
get { return PublishedItemType.Content; }
|
||||
}
|
||||
|
||||
public override PublishedContentType ContentType
|
||||
{
|
||||
get { return _contentType; }
|
||||
}
|
||||
|
||||
public override string DocumentTypeAlias
|
||||
{
|
||||
get { return _contentType.Alias; }
|
||||
}
|
||||
|
||||
public override int DocumentTypeId
|
||||
{
|
||||
get { return _contentType.Id; }
|
||||
}
|
||||
|
||||
public override ICollection<IPublishedProperty> Properties
|
||||
{
|
||||
get { return _properties.ToArray(); }
|
||||
}
|
||||
|
||||
public override IPublishedProperty GetProperty(string alias)
|
||||
{
|
||||
return _properties.FirstOrDefault(x => x.PropertyTypeAlias.InvariantEquals(alias));
|
||||
}
|
||||
|
||||
public override IPublishedProperty GetProperty(string alias, bool recurse)
|
||||
{
|
||||
if (recurse)
|
||||
throw new NotSupportedException();
|
||||
|
||||
return GetProperty(alias);
|
||||
}
|
||||
|
||||
public override IPublishedContent Parent
|
||||
{
|
||||
get { return null; }
|
||||
}
|
||||
|
||||
public override IEnumerable<IPublishedContent> Children
|
||||
{
|
||||
get { return Enumerable.Empty<IPublishedContent>(); }
|
||||
}
|
||||
|
||||
public override int TemplateId
|
||||
{
|
||||
get { return 0; }
|
||||
}
|
||||
|
||||
public override int SortOrder
|
||||
{
|
||||
get { return _sortOrder; }
|
||||
}
|
||||
|
||||
public override string UrlName
|
||||
{
|
||||
get { return null; }
|
||||
}
|
||||
|
||||
public override string WriterName
|
||||
{
|
||||
get { return _containerNode != null ? _containerNode.WriterName : null; }
|
||||
}
|
||||
|
||||
public override string CreatorName
|
||||
{
|
||||
get { return _containerNode != null ? _containerNode.CreatorName : null; }
|
||||
}
|
||||
|
||||
public override int WriterId
|
||||
{
|
||||
get { return _containerNode != null ? _containerNode.WriterId : 0; }
|
||||
}
|
||||
|
||||
public override int CreatorId
|
||||
{
|
||||
get { return _containerNode != null ? _containerNode.CreatorId : 0; }
|
||||
}
|
||||
|
||||
public override string Path
|
||||
{
|
||||
get { return null; }
|
||||
}
|
||||
|
||||
public override DateTime CreateDate
|
||||
{
|
||||
get { return _containerNode != null ? _containerNode.CreateDate : DateTime.MinValue; }
|
||||
}
|
||||
|
||||
public override DateTime UpdateDate
|
||||
{
|
||||
get { return _containerNode != null ? _containerNode.UpdateDate : DateTime.MinValue; }
|
||||
}
|
||||
|
||||
public override Guid Version
|
||||
{
|
||||
get { return _containerNode != null ? _containerNode.Version : Guid.Empty; }
|
||||
}
|
||||
|
||||
public override int Level
|
||||
{
|
||||
get { return 0; }
|
||||
}
|
||||
}
|
||||
}
|
||||
52
src/Umbraco.Web/Models/DetachedPublishedProperty.cs
Normal file
52
src/Umbraco.Web/Models/DetachedPublishedProperty.cs
Normal file
@@ -0,0 +1,52 @@
|
||||
using System;
|
||||
using Umbraco.Core.Models;
|
||||
using Umbraco.Core.Models.PublishedContent;
|
||||
|
||||
namespace Umbraco.Web.Models
|
||||
{
|
||||
internal class DetachedPublishedProperty : IPublishedProperty
|
||||
{
|
||||
private readonly PublishedPropertyType _propertyType;
|
||||
private readonly object _rawValue;
|
||||
private readonly Lazy<object> _sourceValue;
|
||||
private readonly Lazy<object> _objectValue;
|
||||
private readonly Lazy<object> _xpathValue;
|
||||
private readonly bool _isPreview;
|
||||
|
||||
public DetachedPublishedProperty(PublishedPropertyType propertyType, object value)
|
||||
: this(propertyType, value, false)
|
||||
{
|
||||
}
|
||||
|
||||
public DetachedPublishedProperty(PublishedPropertyType propertyType, object value, bool isPreview)
|
||||
{
|
||||
_propertyType = propertyType;
|
||||
_isPreview = isPreview;
|
||||
|
||||
_rawValue = value;
|
||||
|
||||
_sourceValue = new Lazy<object>(() => _propertyType.ConvertDataToSource(_rawValue, _isPreview));
|
||||
_objectValue = new Lazy<object>(() => _propertyType.ConvertSourceToObject(_sourceValue.Value, _isPreview));
|
||||
_xpathValue = new Lazy<object>(() => _propertyType.ConvertSourceToXPath(_sourceValue.Value, _isPreview));
|
||||
}
|
||||
|
||||
public string PropertyTypeAlias
|
||||
{
|
||||
get
|
||||
{
|
||||
return _propertyType.PropertyTypeAlias;
|
||||
}
|
||||
}
|
||||
|
||||
public bool HasValue
|
||||
{
|
||||
get { return DataValue != null && DataValue.ToString().Trim().Length > 0; }
|
||||
}
|
||||
|
||||
public object DataValue { get { return _rawValue; } }
|
||||
|
||||
public object Value { get { return _objectValue.Value; } }
|
||||
|
||||
public object XPathValue { get { return _xpathValue.Value; } }
|
||||
}
|
||||
}
|
||||
27
src/Umbraco.Web/PropertyEditors/NestedContentController.cs
Normal file
27
src/Umbraco.Web/PropertyEditors/NestedContentController.cs
Normal file
@@ -0,0 +1,27 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Umbraco.Web.Editors;
|
||||
using Umbraco.Web.Mvc;
|
||||
|
||||
namespace Umbraco.Web.PropertyEditors
|
||||
{
|
||||
[PluginController("UmbracoApi")]
|
||||
public class NestedContentController : UmbracoAuthorizedJsonController
|
||||
{
|
||||
[System.Web.Http.HttpGet]
|
||||
public IEnumerable<object> GetContentTypes()
|
||||
{
|
||||
return Services.ContentTypeService.GetAllContentTypes()
|
||||
.OrderBy(x => x.SortOrder)
|
||||
.Select(x => new
|
||||
{
|
||||
id = x.Id,
|
||||
guid = x.Key,
|
||||
name = x.Name,
|
||||
alias = x.Alias,
|
||||
icon = x.Icon,
|
||||
tabs = x.CompositionPropertyGroups.Select(y => y.Name).Distinct()
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
131
src/Umbraco.Web/PropertyEditors/NestedContentHelper.cs
Normal file
131
src/Umbraco.Web/PropertyEditors/NestedContentHelper.cs
Normal file
@@ -0,0 +1,131 @@
|
||||
using System;
|
||||
using System.Linq;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Umbraco.Core;
|
||||
using Umbraco.Core.Models;
|
||||
|
||||
namespace Umbraco.Web.PropertyEditors
|
||||
{
|
||||
internal static class NestedContentHelper
|
||||
{
|
||||
private const string CacheKeyPrefix = "Umbraco.Web.PropertyEditors.NestedContent.GetPreValuesCollectionByDataTypeId_";
|
||||
|
||||
public static PreValueCollection GetPreValuesCollectionByDataTypeId(int dtdId)
|
||||
{
|
||||
var preValueCollection = (PreValueCollection)ApplicationContext.Current.ApplicationCache.RuntimeCache.GetCacheItem(
|
||||
string.Concat(CacheKeyPrefix, dtdId),
|
||||
() => ApplicationContext.Current.Services.DataTypeService.GetPreValuesCollectionByDataTypeId(dtdId));
|
||||
|
||||
return preValueCollection;
|
||||
}
|
||||
|
||||
public static void ClearCache(int id)
|
||||
{
|
||||
ApplicationContext.Current.ApplicationCache.RuntimeCache.ClearCacheItem(
|
||||
string.Concat(CacheKeyPrefix, id));
|
||||
}
|
||||
|
||||
public static string GetContentTypeAliasFromItem(JObject item)
|
||||
{
|
||||
var contentTypeAliasProperty = item[NestedContentPropertyEditor.ContentTypeAliasPropertyKey];
|
||||
if (contentTypeAliasProperty == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return contentTypeAliasProperty.ToObject<string>();
|
||||
}
|
||||
|
||||
public static IContentType GetContentTypeFromItem(JObject item)
|
||||
{
|
||||
var contentTypeAlias = GetContentTypeAliasFromItem(item);
|
||||
if (string.IsNullOrEmpty(contentTypeAlias))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
return ApplicationContext.Current.Services.ContentTypeService.GetContentType(contentTypeAlias);
|
||||
}
|
||||
|
||||
#region Conversion from v0.1.1 data formats
|
||||
|
||||
public static void ConvertItemValueFromV011(JObject item, int dtdId, ref PreValueCollection preValues)
|
||||
{
|
||||
var contentTypeAlias = GetContentTypeAliasFromItem(item);
|
||||
if (contentTypeAlias != null)
|
||||
{
|
||||
// the item is already in >v0.1.1 format
|
||||
return;
|
||||
}
|
||||
|
||||
// old style (v0.1.1) data, let's attempt a conversion
|
||||
// - get the prevalues (if they're not loaded already)
|
||||
preValues = preValues ?? GetPreValuesCollectionByDataTypeId(dtdId);
|
||||
|
||||
// - convert the prevalues (if necessary)
|
||||
ConvertPreValueCollectionFromV011(preValues);
|
||||
|
||||
// - get the content types prevalue as JArray
|
||||
var preValuesAsDictionary = preValues.PreValuesAsDictionary.ToDictionary(x => x.Key, x => x.Value.Value);
|
||||
if (!preValuesAsDictionary.ContainsKey(ContentTypesPreValueKey) || string.IsNullOrEmpty(preValuesAsDictionary[ContentTypesPreValueKey]) != false)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var preValueContentTypes = JArray.Parse(preValuesAsDictionary[ContentTypesPreValueKey]);
|
||||
if (preValueContentTypes.Any())
|
||||
{
|
||||
// the only thing we can really do is assume that the item is the first available content type
|
||||
item[NestedContentPropertyEditor.ContentTypeAliasPropertyKey] = preValueContentTypes.First().Value<string>("ncAlias");
|
||||
}
|
||||
}
|
||||
|
||||
public static void ConvertPreValueCollectionFromV011(PreValueCollection preValueCollection)
|
||||
{
|
||||
if (preValueCollection == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var persistedPreValuesAsDictionary = preValueCollection.PreValuesAsDictionary.ToDictionary(x => x.Key, x => x.Value.Value);
|
||||
|
||||
// do we have a "docTypeGuid" prevalue and no "contentTypes" prevalue?
|
||||
if (persistedPreValuesAsDictionary.ContainsKey("docTypeGuid") == false || persistedPreValuesAsDictionary.ContainsKey(ContentTypesPreValueKey))
|
||||
{
|
||||
// the prevalues are already in >v0.1.1 format
|
||||
return;
|
||||
}
|
||||
|
||||
// attempt to parse the doc type guid
|
||||
Guid guid;
|
||||
if (Guid.TryParse(persistedPreValuesAsDictionary["docTypeGuid"], out guid) == false)
|
||||
{
|
||||
// this shouldn't happen... but just in case.
|
||||
return;
|
||||
}
|
||||
|
||||
// find the content type
|
||||
var contentType = ApplicationContext.Current.Services.ContentTypeService.GetAllContentTypes().FirstOrDefault(c => c.Key == guid);
|
||||
if (contentType == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// add a prevalue in the format expected by the new (>0.1.1) content type picker/configurator
|
||||
preValueCollection.PreValuesAsDictionary[ContentTypesPreValueKey] = new PreValue(
|
||||
string.Format(@"[{{""ncAlias"": ""{0}"", ""ncTabAlias"": ""{1}"", ""nameTemplate"": ""{2}"", }}]",
|
||||
contentType.Alias,
|
||||
persistedPreValuesAsDictionary["tabAlias"],
|
||||
persistedPreValuesAsDictionary["nameTemplate"]
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
private static string ContentTypesPreValueKey
|
||||
{
|
||||
get { return NestedContentPropertyEditor.NestedContentPreValueEditor.ContentTypesPreValueKey; }
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
413
src/Umbraco.Web/PropertyEditors/NestedContentPropertyEditor.cs
Normal file
413
src/Umbraco.Web/PropertyEditors/NestedContentPropertyEditor.cs
Normal file
@@ -0,0 +1,413 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Umbraco.Core;
|
||||
using Umbraco.Core.Models;
|
||||
using Umbraco.Core.Models.Editors;
|
||||
using Umbraco.Core.PropertyEditors;
|
||||
using Umbraco.Core.Services;
|
||||
|
||||
namespace Umbraco.Web.PropertyEditors
|
||||
{
|
||||
[PropertyEditor(Constants.PropertyEditors.NestedContentAlias, "Nested Content", "nestedcontent", ValueType = "JSON")]
|
||||
public class NestedContentPropertyEditor : PropertyEditor
|
||||
{
|
||||
internal const string ContentTypeAliasPropertyKey = "ncContentTypeAlias";
|
||||
|
||||
private IDictionary<string, object> _defaultPreValues;
|
||||
public override IDictionary<string, object> DefaultPreValues
|
||||
{
|
||||
get { return _defaultPreValues; }
|
||||
set { _defaultPreValues = value; }
|
||||
}
|
||||
|
||||
public NestedContentPropertyEditor()
|
||||
{
|
||||
// Setup default values
|
||||
_defaultPreValues = new Dictionary<string, object>
|
||||
{
|
||||
{NestedContentPreValueEditor.ContentTypesPreValueKey, ""},
|
||||
{"minItems", 0},
|
||||
{"maxItems", 0},
|
||||
{"confirmDeletes", "1"},
|
||||
{"showIcons", "1"}
|
||||
};
|
||||
}
|
||||
|
||||
#region Pre Value Editor
|
||||
|
||||
protected override PreValueEditor CreatePreValueEditor()
|
||||
{
|
||||
return new NestedContentPreValueEditor();
|
||||
}
|
||||
|
||||
internal class NestedContentPreValueEditor : PreValueEditor
|
||||
{
|
||||
internal const string ContentTypesPreValueKey = "contentTypes";
|
||||
|
||||
[PreValueField(ContentTypesPreValueKey, "Doc Types", "views/propertyeditors/nestedcontent/nestedcontent.doctypepicker.html", Description = "Select the doc types to use as the data blueprint.")]
|
||||
public string[] ContentTypes { get; set; }
|
||||
|
||||
[PreValueField("minItems", "Min Items", "number", Description = "Set the minimum number of items allowed.")]
|
||||
public string MinItems { get; set; }
|
||||
|
||||
[PreValueField("maxItems", "Max Items", "number", Description = "Set the maximum number of items allowed.")]
|
||||
public string MaxItems { get; set; }
|
||||
|
||||
[PreValueField("confirmDeletes", "Confirm Deletes", "boolean", Description = "Set whether item deletions should require confirming.")]
|
||||
public string ConfirmDeletes { get; set; }
|
||||
|
||||
[PreValueField("showIcons", "Show Icons", "boolean", Description = "Set whether to show the items doc type icon in the list.")]
|
||||
public string ShowIcons { get; set; }
|
||||
|
||||
[PreValueField("hideLabel", "Hide Label", "boolean", Description = "Set whether to hide the editor label and have the list take up the full width of the editor window.")]
|
||||
public string HideLabel { get; set; }
|
||||
|
||||
public override IDictionary<string, object> ConvertDbToEditor(IDictionary<string, object> defaultPreVals, PreValueCollection persistedPreVals)
|
||||
{
|
||||
// re-format old style (v0.1.1) pre values if necessary
|
||||
NestedContentHelper.ConvertPreValueCollectionFromV011(persistedPreVals);
|
||||
|
||||
return base.ConvertDbToEditor(defaultPreVals, persistedPreVals);
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Value Editor
|
||||
|
||||
protected override PropertyValueEditor CreateValueEditor()
|
||||
{
|
||||
return new NestedContentPropertyValueEditor(base.CreateValueEditor());
|
||||
}
|
||||
|
||||
internal class NestedContentPropertyValueEditor : PropertyValueEditorWrapper
|
||||
{
|
||||
public NestedContentPropertyValueEditor(PropertyValueEditor wrapped)
|
||||
: base(wrapped)
|
||||
{
|
||||
Validators.Add(new NestedContentValidator());
|
||||
}
|
||||
|
||||
internal ServiceContext Services
|
||||
{
|
||||
get { return ApplicationContext.Current.Services; }
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
public override string ConvertDbToString(Property property, PropertyType propertyType, IDataTypeService dataTypeService)
|
||||
{
|
||||
// Convert / validate value
|
||||
if (property.Value == null || string.IsNullOrWhiteSpace(property.Value.ToString()))
|
||||
return string.Empty;
|
||||
|
||||
var value = JsonConvert.DeserializeObject<List<object>>(property.Value.ToString());
|
||||
if (value == null)
|
||||
return string.Empty;
|
||||
|
||||
// Process value
|
||||
PreValueCollection preValues = null;
|
||||
for (var i = 0; i < value.Count; i++)
|
||||
{
|
||||
var o = value[i];
|
||||
var propValues = ((JObject)o);
|
||||
|
||||
// convert from old style (v0.1.1) data format if necessary
|
||||
NestedContentHelper.ConvertItemValueFromV011(propValues, propertyType.DataTypeDefinitionId, ref preValues);
|
||||
|
||||
var contentType = NestedContentHelper.GetContentTypeFromItem(propValues);
|
||||
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
|
||||
{
|
||||
try
|
||||
{
|
||||
// Create a fake property using the property abd stored value
|
||||
var prop = new Property(propType, propValues[propKey] == null ? null : propValues[propKey].ToString());
|
||||
|
||||
// Lookup the property editor
|
||||
var propEditor = PropertyEditorResolver.Current.GetByAlias(propType.PropertyEditorAlias);
|
||||
|
||||
// Get the editor to do it's conversion, and store it back
|
||||
propValues[propKey] = propEditor.ValueEditor.ConvertDbToString(prop, propType, dataTypeService);
|
||||
}
|
||||
catch (InvalidOperationException)
|
||||
{
|
||||
// https://github.com/umco/umbraco-nested-content/issues/111
|
||||
// Catch any invalid cast operations as likely means courier failed due to missing
|
||||
// or trashed item so couldn't convert a guid back to an int
|
||||
|
||||
propValues[propKey] = null;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// Update the value on the property
|
||||
property.Value = JsonConvert.SerializeObject(value);
|
||||
|
||||
// Pass the call down
|
||||
return base.ConvertDbToString(property, propertyType, dataTypeService);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region DB to Editor
|
||||
|
||||
public override object ConvertDbToEditor(Property property, PropertyType propertyType, IDataTypeService dataTypeService)
|
||||
{
|
||||
if (property.Value == null || string.IsNullOrWhiteSpace(property.Value.ToString()))
|
||||
return string.Empty;
|
||||
|
||||
var value = JsonConvert.DeserializeObject<List<object>>(property.Value.ToString());
|
||||
if (value == null)
|
||||
return string.Empty;
|
||||
|
||||
// Process value
|
||||
PreValueCollection preValues = null;
|
||||
for (var i = 0; i < value.Count; i++)
|
||||
{
|
||||
var o = value[i];
|
||||
var propValues = ((JObject)o);
|
||||
|
||||
// convert from old style (v0.1.1) data format if necessary
|
||||
NestedContentHelper.ConvertItemValueFromV011(propValues, propertyType.DataTypeDefinitionId, ref preValues);
|
||||
|
||||
var contentType = NestedContentHelper.GetContentTypeFromItem(propValues);
|
||||
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
|
||||
{
|
||||
try
|
||||
{
|
||||
// Create a fake property using the property and stored value
|
||||
var prop = new Property(propType, propValues[propKey] == null ? null : propValues[propKey].ToString());
|
||||
|
||||
// Lookup the property editor
|
||||
var propEditor = PropertyEditorResolver.Current.GetByAlias(propType.PropertyEditorAlias);
|
||||
|
||||
// Get the editor to do it's conversion
|
||||
var newValue = propEditor.ValueEditor.ConvertDbToEditor(prop, propType, dataTypeService);
|
||||
|
||||
// Store the value back
|
||||
propValues[propKey] = (newValue == null) ? null : JToken.FromObject(newValue);
|
||||
}
|
||||
catch (InvalidOperationException)
|
||||
{
|
||||
// https://github.com/umco/umbraco-nested-content/issues/111
|
||||
// Catch any invalid cast operations as likely means courier failed due to missing
|
||||
// or trashed item so couldn't convert a guid back to an int
|
||||
|
||||
propValues[propKey] = null;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// Update the value on the property
|
||||
property.Value = JsonConvert.SerializeObject(value);
|
||||
|
||||
// Pass the call down
|
||||
return base.ConvertDbToEditor(property, propertyType, dataTypeService);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Editor to DB
|
||||
|
||||
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);
|
||||
|
||||
var contentType = NestedContentHelper.GetContentTypeFromItem(propValues);
|
||||
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
|
||||
var propPreValues = Services.DataTypeService.GetPreValuesCollectionByDataTypeId(
|
||||
propType.DataTypeDefinitionId);
|
||||
|
||||
// Lookup the property editor
|
||||
var propEditor = PropertyEditorResolver.Current.GetByAlias(propType.PropertyEditorAlias);
|
||||
|
||||
// Create a fake content property data object
|
||||
var contentPropData = new ContentPropertyData(
|
||||
propValues[propKey], propPreValues,
|
||||
new Dictionary<string, object>());
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
internal class NestedContentValidator : IPropertyValidator
|
||||
{
|
||||
public IEnumerable<ValidationResult> Validate(object rawValue, PreValueCollection preValues, PropertyEditor editor)
|
||||
{
|
||||
var value = JsonConvert.DeserializeObject<List<object>>(rawValue.ToString());
|
||||
if (value == null)
|
||||
yield break;
|
||||
|
||||
IDataTypeService dataTypeService = ApplicationContext.Current.Services.DataTypeService;
|
||||
for (var i = 0; i < value.Count; i++)
|
||||
{
|
||||
var o = value[i];
|
||||
var propValues = ((JObject)o);
|
||||
|
||||
var contentType = NestedContentHelper.GetContentTypeFromItem(propValues);
|
||||
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)
|
||||
{
|
||||
PreValueCollection propPrevalues = dataTypeService.GetPreValuesCollectionByDataTypeId(propType.DataTypeDefinitionId);
|
||||
PropertyEditor propertyEditor = PropertyEditorResolver.Current.GetByAlias(propType.PropertyEditorAlias);
|
||||
|
||||
foreach (IPropertyValidator validator in propertyEditor.ValueEditor.Validators)
|
||||
{
|
||||
foreach (ValidationResult result in validator.Validate(propValues[propKey], propPrevalues, propertyEditor))
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using Umbraco.Core.Logging;
|
||||
using Umbraco.Core.Models;
|
||||
using Umbraco.Core.Models.PublishedContent;
|
||||
using Umbraco.Core.PropertyEditors;
|
||||
|
||||
namespace Umbraco.Web.PropertyEditors.ValueConverters
|
||||
{
|
||||
public class NestedContentManyValueConverter : PropertyValueConverterBase, IPropertyValueConverterMeta
|
||||
{
|
||||
public override bool IsConverter(PublishedPropertyType propertyType)
|
||||
{
|
||||
return propertyType.IsNestedContentProperty() && !propertyType.IsSingleNestedContentProperty();
|
||||
}
|
||||
|
||||
public override object ConvertDataToSource(PublishedPropertyType propertyType, object source, bool preview)
|
||||
{
|
||||
try
|
||||
{
|
||||
return propertyType.ConvertPropertyToNestedContent(source, preview);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
LogHelper.Error<NestedContentManyValueConverter>("Error converting value", e);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public virtual Type GetPropertyValueType(PublishedPropertyType propertyType)
|
||||
{
|
||||
return typeof(IEnumerable<IPublishedContent>);
|
||||
}
|
||||
|
||||
public virtual PropertyCacheLevel GetPropertyCacheLevel(PublishedPropertyType propertyType, PropertyCacheValue cacheValue)
|
||||
{
|
||||
return PropertyCacheLevel.Content;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using Umbraco.Core;
|
||||
using Umbraco.Core.Models;
|
||||
using Umbraco.Core.Models.PublishedContent;
|
||||
using Umbraco.Web.Models;
|
||||
|
||||
namespace Umbraco.Web.PropertyEditors.ValueConverters
|
||||
{
|
||||
internal static class NestedContentPublishedPropertyTypeExtensions
|
||||
{
|
||||
public static bool IsNestedContentProperty(this PublishedPropertyType publishedProperty)
|
||||
{
|
||||
return publishedProperty.PropertyEditorAlias.InvariantEquals(Constants.PropertyEditors.NestedContentAlias);
|
||||
}
|
||||
|
||||
public static bool IsSingleNestedContentProperty(this PublishedPropertyType publishedProperty)
|
||||
{
|
||||
if (!publishedProperty.IsNestedContentProperty())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var preValueCollection = NestedContentHelper.GetPreValuesCollectionByDataTypeId(publishedProperty.DataTypeId);
|
||||
var preValueDictionary = preValueCollection.PreValuesAsDictionary.ToDictionary(x => x.Key, x => x.Value.Value);
|
||||
|
||||
int minItems, maxItems;
|
||||
return preValueDictionary.ContainsKey("minItems") &&
|
||||
int.TryParse(preValueDictionary["minItems"], out minItems) && minItems == 1
|
||||
&& preValueDictionary.ContainsKey("maxItems") &&
|
||||
int.TryParse(preValueDictionary["maxItems"], out maxItems) && maxItems == 1;
|
||||
}
|
||||
|
||||
public static object ConvertPropertyToNestedContent(this PublishedPropertyType propertyType, object source, bool preview)
|
||||
{
|
||||
using (DisposableTimer.DebugDuration<PublishedPropertyType>(string.Format("ConvertPropertyToNestedContent ({0})", propertyType.DataTypeId)))
|
||||
{
|
||||
if (source != null && !source.ToString().IsNullOrWhiteSpace())
|
||||
{
|
||||
var rawValue = JsonConvert.DeserializeObject<List<object>>(source.ToString());
|
||||
var processedValue = new List<IPublishedContent>();
|
||||
|
||||
var preValueCollection = NestedContentHelper.GetPreValuesCollectionByDataTypeId(propertyType.DataTypeId);
|
||||
var preValueDictionary = preValueCollection.PreValuesAsDictionary.ToDictionary(x => x.Key, x => x.Value.Value);
|
||||
|
||||
for (var i = 0; i < rawValue.Count; i++)
|
||||
{
|
||||
var item = (JObject)rawValue[i];
|
||||
|
||||
// Convert from old style (v.0.1.1) data format if necessary
|
||||
// - Please note: This call has virtually no impact on rendering performance for new style (>v0.1.1).
|
||||
// Even so, this should be removed eventually, when it's safe to assume that there is
|
||||
// no longer any need for conversion.
|
||||
NestedContentHelper.ConvertItemValueFromV011(item, propertyType.DataTypeId, ref preValueCollection);
|
||||
|
||||
var contentTypeAlias = NestedContentHelper.GetContentTypeAliasFromItem(item);
|
||||
if (string.IsNullOrEmpty(contentTypeAlias))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var publishedContentType = PublishedContentType.Get(PublishedItemType.Content, contentTypeAlias);
|
||||
if (publishedContentType == null)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var propValues = item.ToObject<Dictionary<string, object>>();
|
||||
var properties = new List<IPublishedProperty>();
|
||||
|
||||
foreach (var jProp in propValues)
|
||||
{
|
||||
var propType = publishedContentType.GetPropertyType(jProp.Key);
|
||||
if (propType != null)
|
||||
{
|
||||
properties.Add(new DetachedPublishedProperty(propType, jProp.Value, preview));
|
||||
}
|
||||
}
|
||||
|
||||
// Parse out the name manually
|
||||
object nameObj = null;
|
||||
if (propValues.TryGetValue("name", out nameObj))
|
||||
{
|
||||
// Do nothing, we just want to parse out the name if we can
|
||||
}
|
||||
|
||||
object keyObj;
|
||||
var key = Guid.Empty;
|
||||
if (propValues.TryGetValue("key", out keyObj))
|
||||
{
|
||||
key = Guid.Parse(keyObj.ToString());
|
||||
}
|
||||
|
||||
// Get the current request node we are embedded in
|
||||
var pcr = UmbracoContext.Current == null ? null : UmbracoContext.Current.PublishedContentRequest;
|
||||
var containerNode = pcr != null && pcr.HasPublishedContent ? pcr.PublishedContent : null;
|
||||
|
||||
// Create the model based on our implementation of IPublishedContent
|
||||
IPublishedContent content = new DetachedPublishedContent(
|
||||
key,
|
||||
nameObj == null ? null : nameObj.ToString(),
|
||||
publishedContentType,
|
||||
properties.ToArray(),
|
||||
containerNode,
|
||||
i,
|
||||
preview);
|
||||
|
||||
if (PublishedContentModelFactoryResolver.HasCurrent)
|
||||
{
|
||||
// Let the current model factory create a typed model to wrap our model
|
||||
content = PublishedContentModelFactoryResolver.Current.Factory.CreateModel(content);
|
||||
}
|
||||
|
||||
// Add the (typed) model as a result
|
||||
processedValue.Add(content);
|
||||
}
|
||||
|
||||
if (propertyType.IsSingleNestedContentProperty())
|
||||
{
|
||||
return processedValue.FirstOrDefault();
|
||||
}
|
||||
|
||||
return processedValue;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
using System;
|
||||
using Umbraco.Core.Logging;
|
||||
using Umbraco.Core.Models;
|
||||
using Umbraco.Core.Models.PublishedContent;
|
||||
using Umbraco.Core.PropertyEditors;
|
||||
|
||||
namespace Umbraco.Web.PropertyEditors.ValueConverters
|
||||
{
|
||||
public class NestedContentSingleValueConverter : PropertyValueConverterBase, IPropertyValueConverterMeta
|
||||
{
|
||||
public override bool IsConverter(PublishedPropertyType propertyType)
|
||||
{
|
||||
return propertyType.IsSingleNestedContentProperty();
|
||||
}
|
||||
|
||||
public override object ConvertDataToSource(PublishedPropertyType propertyType, object source, bool preview)
|
||||
{
|
||||
try
|
||||
{
|
||||
return propertyType.ConvertPropertyToNestedContent(source, preview);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
LogHelper.Error<NestedContentSingleValueConverter>("Error converting value", e);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public virtual Type GetPropertyValueType(PublishedPropertyType propertyType)
|
||||
{
|
||||
return typeof(IPublishedContent);
|
||||
}
|
||||
|
||||
public virtual PropertyCacheLevel GetPropertyCacheLevel(PublishedPropertyType propertyType, PropertyCacheValue cacheValue)
|
||||
{
|
||||
return PropertyCacheLevel.Content;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -24,6 +24,12 @@ namespace Umbraco.Web
|
||||
|
||||
public static Guid GetKey(this IPublishedContent content)
|
||||
{
|
||||
var wrapped = content as PublishedContentWrapped;
|
||||
while (wrapped != null)
|
||||
{
|
||||
content = wrapped.Unwrap();
|
||||
wrapped = content as PublishedContentWrapped;
|
||||
}
|
||||
var contentWithKey = content as IPublishedContentWithKey;
|
||||
return contentWithKey == null ? Guid.Empty : contentWithKey.Key;
|
||||
}
|
||||
|
||||
@@ -304,7 +304,30 @@
|
||||
<Compile Include="ApplicationContextExtensions.cs" />
|
||||
<Compile Include="AreaRegistrationContextExtensions.cs" />
|
||||
<Compile Include="BatchedDatabaseServerMessengerStartup.cs" />
|
||||
<Compile Include="Cache\ApplicationCacheRefresher.cs" />
|
||||
<Compile Include="Cache\ApplicationTreeCacheRefresher.cs" />
|
||||
<Compile Include="Cache\CacheRefresherEventHandler.cs" />
|
||||
<Compile Include="Cache\ContentTypeCacheRefresher.cs" />
|
||||
<Compile Include="Cache\DataTypeCacheRefresher.cs" />
|
||||
<Compile Include="Cache\DictionaryCacheRefresher.cs" />
|
||||
<Compile Include="Cache\DistributedCache.cs" />
|
||||
<Compile Include="Cache\DistributedCacheExtensions.cs" />
|
||||
<Compile Include="Cache\DomainCacheRefresher.cs" />
|
||||
<Compile Include="Cache\LanguageCacheRefresher.cs" />
|
||||
<Compile Include="Cache\MacroCacheRefresher.cs" />
|
||||
<Compile Include="Cache\MediaCacheRefresher.cs" />
|
||||
<Compile Include="Cache\MemberCacheRefresher.cs" />
|
||||
<Compile Include="Cache\MemberGroupCacheRefresher.cs" />
|
||||
<Compile Include="Cache\PageCacheRefresher.cs" />
|
||||
<Compile Include="Cache\PublicAccessCacheRefresher.cs" />
|
||||
<Compile Include="Cache\RelationTypeCacheRefresher.cs" />
|
||||
<Compile Include="Cache\StylesheetCacheRefresher.cs" />
|
||||
<Compile Include="Cache\StylesheetPropertyCacheRefresher.cs" />
|
||||
<Compile Include="Cache\TemplateCacheRefresher.cs" />
|
||||
<Compile Include="Cache\UnpublishedPageCacheRefresher.cs" />
|
||||
<Compile Include="Cache\UserCacheRefresher.cs" />
|
||||
<Compile Include="Cache\UserPermissionsCacheRefresher.cs" />
|
||||
<Compile Include="Cache\UserTypeCacheRefresher.cs" />
|
||||
<Compile Include="Editors\BackOfficeNotificationsController.cs" />
|
||||
<Compile Include="Editors\EditorValidationResolver.cs" />
|
||||
<Compile Include="Editors\EditorValidator.cs" />
|
||||
@@ -366,6 +389,8 @@
|
||||
<Compile Include="Models\ContentEditing\SimpleNotificationModel.cs" />
|
||||
<Compile Include="Models\ContentEditing\SnippetDisplay.cs" />
|
||||
<Compile Include="Models\ContentEditing\TemplateDisplay.cs" />
|
||||
<Compile Include="Models\DetachedPublishedContent.cs" />
|
||||
<Compile Include="Models\DetachedPublishedProperty.cs" />
|
||||
<Compile Include="Models\LocalPackageInstallModel.cs" />
|
||||
<Compile Include="Models\Mapping\CodeFileDisplayMapper.cs" />
|
||||
<Compile Include="Models\Mapping\ContentTypeModelMapperExtensions.cs" />
|
||||
@@ -402,6 +427,9 @@
|
||||
<Compile Include="PropertyEditors\MediaPicker2PropertyEditor.cs" />
|
||||
<Compile Include="PropertyEditors\MemberPicker2PropertyEditor.cs" />
|
||||
<Compile Include="PropertyEditors\MultiNodeTreePicker2PropertyEditor.cs" />
|
||||
<Compile Include="PropertyEditors\NestedContentController.cs" />
|
||||
<Compile Include="PropertyEditors\NestedContentHelper.cs" />
|
||||
<Compile Include="PropertyEditors\NestedContentPropertyEditor.cs" />
|
||||
<Compile Include="PropertyEditors\RelatedLinks2PropertyEditor.cs" />
|
||||
<Compile Include="PropertyEditors\ValueConverters\ContentPickerPropertyConverter.cs" />
|
||||
<Compile Include="PropertyEditors\ParameterEditors\MultipleContentPickerParameterEditor.cs" />
|
||||
@@ -412,6 +440,9 @@
|
||||
<Compile Include="PropertyEditors\ValueConverters\MemberPickerPropertyConverter.cs" />
|
||||
<Compile Include="PropertyEditors\ValueConverters\MultiNodeTreePickerPropertyConverter.cs" />
|
||||
<Compile Include="PropertyEditors\ValueConverters\MultipleMediaPickerPropertyConverter.cs" />
|
||||
<Compile Include="PropertyEditors\ValueConverters\NestedContentSingleValueConverter.cs" />
|
||||
<Compile Include="PropertyEditors\ValueConverters\NestedContentManyValueConverter.cs" />
|
||||
<Compile Include="PropertyEditors\ValueConverters\NestedContentPublishedPropertyTypeExtensions.cs" />
|
||||
<Compile Include="PublishedContentQueryExtensions.cs" />
|
||||
<Compile Include="Routing\RedirectTrackingEventHandler.cs" />
|
||||
<Compile Include="Editors\RedirectUrlManagementController.cs" />
|
||||
@@ -470,29 +501,6 @@
|
||||
<Compile Include="BatchedDatabaseServerMessenger.cs" />
|
||||
<Compile Include="BatchedWebServiceServerMessenger.cs" />
|
||||
<Compile Include="CacheHelperExtensions.cs" />
|
||||
<Compile Include="Cache\ApplicationCacheRefresher.cs" />
|
||||
<Compile Include="Cache\ApplicationTreeCacheRefresher.cs" />
|
||||
<Compile Include="Cache\ContentTypeCacheRefresher.cs" />
|
||||
<Compile Include="Cache\DataTypeCacheRefresher.cs" />
|
||||
<Compile Include="Cache\DictionaryCacheRefresher.cs" />
|
||||
<Compile Include="Cache\DistributedCache.cs" />
|
||||
<Compile Include="Cache\DistributedCacheExtensions.cs" />
|
||||
<Compile Include="Cache\CacheRefresherEventHandler.cs" />
|
||||
<Compile Include="Cache\DomainCacheRefresher.cs" />
|
||||
<Compile Include="Cache\LanguageCacheRefresher.cs" />
|
||||
<Compile Include="Cache\MacroCacheRefresher.cs" />
|
||||
<Compile Include="Cache\MediaCacheRefresher.cs" />
|
||||
<Compile Include="Cache\MemberCacheRefresher.cs" />
|
||||
<Compile Include="Cache\MemberGroupCacheRefresher.cs" />
|
||||
<Compile Include="Cache\PageCacheRefresher.cs" />
|
||||
<Compile Include="Cache\PublicAccessCacheRefresher.cs" />
|
||||
<Compile Include="Cache\StylesheetCacheRefresher.cs" />
|
||||
<Compile Include="Cache\StylesheetPropertyCacheRefresher.cs" />
|
||||
<Compile Include="Cache\TemplateCacheRefresher.cs" />
|
||||
<Compile Include="Cache\UnpublishedPageCacheRefresher.cs" />
|
||||
<Compile Include="Cache\UserCacheRefresher.cs" />
|
||||
<Compile Include="Cache\UserPermissionsCacheRefresher.cs" />
|
||||
<Compile Include="Cache\UserTypeCacheRefresher.cs" />
|
||||
<Compile Include="Editors\AuthenticationController.cs" />
|
||||
<Compile Include="Controllers\UmbProfileController.cs" />
|
||||
<Compile Include="Editors\ContentController.cs" />
|
||||
|
||||
Reference in New Issue
Block a user