Merge remote-tracking branch 'origin/temp-u4-8604-1' into dev-v7.7

This commit is contained in:
Shannon
2017-06-05 09:50:17 +02:00
28 changed files with 2008 additions and 80 deletions

View File

@@ -437,6 +437,11 @@ namespace Umbraco.Core
/// </summary>
public const string EmailAddressAlias = "Umbraco.EmailAddress";
/// <summary>
/// Alias for the nested content property editor.
/// </summary>
public const string NestedContentAlias = "Umbraco.NestedContent";
public static class PreValueKeys
{
/// <summary>

View File

@@ -28,7 +28,7 @@ namespace Umbraco.Core.Models.PublishedContent
if (_index.HasValue) return _index.Value;
// slow -- and don't cache, not in a set
if (_contentSet == null) return Content.GetIndex();
if (_contentSet == null) return WrappedContentInternal.GetIndex();
// slow -- but cache for next time
var index = _contentSet.FindIndex(x => x.Id == Id);
@@ -147,7 +147,7 @@ namespace Umbraco.Core.Models.PublishedContent
public override IEnumerable<IPublishedContent> ContentSet
{
get { return _contentSet ?? Content.ContentSet; }
get { return _contentSet ?? WrappedContentInternal.ContentSet; }
}
#endregion
@@ -161,8 +161,8 @@ namespace Umbraco.Core.Models.PublishedContent
get
{
return _properties == null
? Content.Properties
: Content.Properties.Union(_properties).ToList();
? WrappedContentInternal.Properties
: WrappedContentInternal.Properties.Union(_properties).ToList();
}
}
@@ -175,15 +175,15 @@ namespace Umbraco.Core.Models.PublishedContent
var property = _properties.FirstOrDefault(prop => prop.PropertyTypeAlias.InvariantEquals(alias));
if (property != null) return property.HasValue ? property.Value : null;
}
return Content[alias];
return WrappedContentInternal[alias];
}
}
public override IPublishedProperty GetProperty(string alias)
{
return _properties == null
? Content.GetProperty(alias)
: _properties.FirstOrDefault(prop => prop.PropertyTypeAlias.InvariantEquals(alias)) ?? Content.GetProperty(alias);
? WrappedContentInternal.GetProperty(alias)
: _properties.FirstOrDefault(prop => prop.PropertyTypeAlias.InvariantEquals(alias)) ?? WrappedContentInternal.GetProperty(alias);
}
#endregion

View File

@@ -9,6 +9,6 @@ namespace Umbraco.Core.Models.PublishedContent
: base(content)
{ }
public Guid Key { get { return ((IPublishedContentWithKey) Content).Key; } }
public Guid Key { get { return ((IPublishedContentWithKey) WrappedContentInternal).Key; } }
}
}

View File

@@ -8,6 +8,6 @@ namespace Umbraco.Core.Models.PublishedContent
: base (content)
{ }
public Guid Key { get { return ((IPublishedContentWithKey) Content).Key; } }
public Guid Key { get { return ((IPublishedContentWithKey) WrappedContentInternal).Key; } }
}
}

View File

@@ -12,6 +12,6 @@ namespace Umbraco.Core.Models.PublishedContent
: base(content)
{ }
public virtual Guid Key { get { return ((IPublishedContentWithKey) Content).Key; } }
public virtual Guid Key { get { return ((IPublishedContentWithKey) WrappedContentInternal).Key; } }
}
}

View File

@@ -27,7 +27,7 @@ namespace Umbraco.Core.Models.PublishedContent
/// </summary>
public abstract class PublishedContentWrapped : IPublishedContent
{
protected readonly IPublishedContent Content;
protected readonly IPublishedContent WrappedContentInternal;
/// <summary>
/// Initialize a new instance of the <see cref="PublishedContentWrapped"/> class
@@ -36,7 +36,7 @@ namespace Umbraco.Core.Models.PublishedContent
/// <param name="content">The content to wrap and extend.</param>
protected PublishedContentWrapped(IPublishedContent content)
{
Content = content;
WrappedContentInternal = content;
}
/// <summary>
@@ -45,21 +45,21 @@ namespace Umbraco.Core.Models.PublishedContent
/// <returns>The wrapped content, that was passed as an argument to the constructor.</returns>
public IPublishedContent Unwrap()
{
return Content;
return WrappedContentInternal;
}
#region ContentSet
public virtual IEnumerable<IPublishedContent> ContentSet
{
get { return Content.ContentSet; }
get { return WrappedContentInternal.ContentSet; }
}
#endregion
#region ContentType
public virtual PublishedContentType ContentType { get { return Content.ContentType; } }
public virtual PublishedContentType ContentType { get { return WrappedContentInternal.ContentType; } }
#endregion
@@ -67,102 +67,102 @@ namespace Umbraco.Core.Models.PublishedContent
public virtual int Id
{
get { return Content.Id; }
get { return WrappedContentInternal.Id; }
}
public virtual int TemplateId
{
get { return Content.TemplateId; }
get { return WrappedContentInternal.TemplateId; }
}
public virtual int SortOrder
{
get { return Content.SortOrder; }
get { return WrappedContentInternal.SortOrder; }
}
public virtual string Name
{
get { return Content.Name; }
get { return WrappedContentInternal.Name; }
}
public virtual string UrlName
{
get { return Content.UrlName; }
get { return WrappedContentInternal.UrlName; }
}
public virtual string DocumentTypeAlias
{
get { return Content.DocumentTypeAlias; }
get { return WrappedContentInternal.DocumentTypeAlias; }
}
public virtual int DocumentTypeId
{
get { return Content.DocumentTypeId; }
get { return WrappedContentInternal.DocumentTypeId; }
}
public virtual string WriterName
{
get { return Content.WriterName; }
get { return WrappedContentInternal.WriterName; }
}
public virtual string CreatorName
{
get { return Content.CreatorName; }
get { return WrappedContentInternal.CreatorName; }
}
public virtual int WriterId
{
get { return Content.WriterId; }
get { return WrappedContentInternal.WriterId; }
}
public virtual int CreatorId
{
get { return Content.CreatorId; }
get { return WrappedContentInternal.CreatorId; }
}
public virtual string Path
{
get { return Content.Path; }
get { return WrappedContentInternal.Path; }
}
public virtual DateTime CreateDate
{
get { return Content.CreateDate; }
get { return WrappedContentInternal.CreateDate; }
}
public virtual DateTime UpdateDate
{
get { return Content.UpdateDate; }
get { return WrappedContentInternal.UpdateDate; }
}
public virtual Guid Version
{
get { return Content.Version; }
get { return WrappedContentInternal.Version; }
}
public virtual int Level
{
get { return Content.Level; }
get { return WrappedContentInternal.Level; }
}
public virtual string Url
{
get { return Content.Url; }
get { return WrappedContentInternal.Url; }
}
public virtual PublishedItemType ItemType
{
get { return Content.ItemType; }
get { return WrappedContentInternal.ItemType; }
}
public virtual bool IsDraft
{
get { return Content.IsDraft; }
get { return WrappedContentInternal.IsDraft; }
}
public virtual int GetIndex()
{
return Content.GetIndex();
return WrappedContentInternal.GetIndex();
}
#endregion
@@ -171,12 +171,12 @@ namespace Umbraco.Core.Models.PublishedContent
public virtual IPublishedContent Parent
{
get { return Content.Parent; }
get { return WrappedContentInternal.Parent; }
}
public virtual IEnumerable<IPublishedContent> Children
{
get { return Content.Children; }
get { return WrappedContentInternal.Children; }
}
#endregion
@@ -185,22 +185,22 @@ namespace Umbraco.Core.Models.PublishedContent
public virtual ICollection<IPublishedProperty> Properties
{
get { return Content.Properties; }
get { return WrappedContentInternal.Properties; }
}
public virtual object this[string alias]
{
get { return Content[alias]; }
get { return WrappedContentInternal[alias]; }
}
public virtual IPublishedProperty GetProperty(string alias)
{
return Content.GetProperty(alias);
return WrappedContentInternal.GetProperty(alias);
}
public virtual IPublishedProperty GetProperty(string alias, bool recurse)
{
return Content.GetProperty(alias, recurse);
return WrappedContentInternal.GetProperty(alias, recurse);
}
#endregion

View File

@@ -42,7 +42,16 @@ namespace Umbraco.Core.PropertyEditors
internal PropertyEditorResolver(IServiceProvider serviceProvider, ILogger logger, Func<IEnumerable<Type>> typeListProducerList, ManifestBuilder builder)
: base(serviceProvider, logger, typeListProducerList, ObjectLifetimeScope.Application)
{
_unioned = new Lazy<List<PropertyEditor>>(() => Values.Union(builder.PropertyEditors).ToList());
_unioned = new Lazy<List<PropertyEditor>>(() => SanitizeNames(Values.Union(builder.PropertyEditors).ToList()));
}
private static List<PropertyEditor> SanitizeNames(List<PropertyEditor> editors)
{
var nestedContentEditorFromPackage = editors.FirstOrDefault(x => x.Alias == "Our.Umbraco.NestedContent");
if (nestedContentEditorFromPackage != null)
nestedContentEditorFromPackage.Name = "(Obsolete) " + nestedContentEditorFromPackage.Name;
return editors;
}
private readonly Lazy<List<PropertyEditor>> _unioned;

View File

@@ -48,7 +48,7 @@ namespace Umbraco.Tests.PublishedContent.StronglyTypedModels
protected T Resolve<T>(string propertyTypeAlias)
{
return Content.GetPropertyValue<T>(propertyTypeAlias);
return WrappedContentInternal.GetPropertyValue<T>(propertyTypeAlias);
}
protected T Resolve<T>(MethodBase methodBase, T ifCannotConvert)
@@ -59,7 +59,7 @@ namespace Umbraco.Tests.PublishedContent.StronglyTypedModels
protected T Resolve<T>(string propertyTypeAlias, T ifCannotConvert)
{
return Content.GetPropertyValue<T>(propertyTypeAlias, false, ifCannotConvert);
return WrappedContentInternal.GetPropertyValue<T>(propertyTypeAlias, false, ifCannotConvert);
}
protected T Resolve<T>(MethodBase methodBase, bool recursive, T ifCannotConvert)
@@ -70,7 +70,7 @@ namespace Umbraco.Tests.PublishedContent.StronglyTypedModels
protected T Resolve<T>(string propertyTypeAlias, bool recursive, T ifCannotConvert)
{
return Content.GetPropertyValue<T>(propertyTypeAlias, recursive, ifCannotConvert);
return WrappedContentInternal.GetPropertyValue<T>(propertyTypeAlias, recursive, ifCannotConvert);
}
#endregion
@@ -81,7 +81,7 @@ namespace Umbraco.Tests.PublishedContent.StronglyTypedModels
if (constructorInfo == null)
throw new Exception("No valid constructor found");
return (T) constructorInfo.Invoke(new object[] {Content.Parent});
return (T) constructorInfo.Invoke(new object[] {WrappedContentInternal.Parent});
}
protected IEnumerable<T> Children<T>(MethodBase methodBase) where T : TypedModelBase
@@ -98,7 +98,7 @@ namespace Umbraco.Tests.PublishedContent.StronglyTypedModels
string singularizedDocTypeAlias = docTypeAlias.ToSingular();
return Content.Children.Where(x => x.DocumentTypeAlias == singularizedDocTypeAlias)
return WrappedContentInternal.Children.Where(x => x.DocumentTypeAlias == singularizedDocTypeAlias)
.Select(x => (T)constructorInfo.Invoke(new object[] { x }));
}
@@ -116,7 +116,7 @@ namespace Umbraco.Tests.PublishedContent.StronglyTypedModels
string singularizedDocTypeAlias = docTypeAlias.ToSingular();
return Content.Ancestors().Where(x => x.DocumentTypeAlias == singularizedDocTypeAlias)
return WrappedContentInternal.Ancestors().Where(x => x.DocumentTypeAlias == singularizedDocTypeAlias)
.Select(x => (T)constructorInfo.Invoke(new object[] { x }));
}
@@ -134,7 +134,7 @@ namespace Umbraco.Tests.PublishedContent.StronglyTypedModels
string singularizedDocTypeAlias = docTypeAlias.ToSingular();
return Content.Descendants().Where(x => x.DocumentTypeAlias == singularizedDocTypeAlias)
return WrappedContentInternal.Descendants().Where(x => x.DocumentTypeAlias == singularizedDocTypeAlias)
.Select(x => (T)constructorInfo.Invoke(new object[] { x }));
}
#endregion

View File

@@ -0,0 +1,97 @@
angular.module("umbraco.directives").directive('nestedContentEditor', [
function () {
var link = function ($scope) {
// Clone the model because some property editors
// do weird things like updating and config values
// so we want to ensure we start from a fresh every
// time, we'll just sync the value back when we need to
$scope.model = angular.copy($scope.ngModel);
$scope.nodeContext = $scope.model;
// Find the selected tab
var selectedTab = $scope.model.tabs[0];
if ($scope.tabAlias) {
angular.forEach($scope.model.tabs, function (tab) {
if (tab.alias.toLowerCase() === $scope.tabAlias.toLowerCase()) {
selectedTab = tab;
return;
}
});
}
$scope.tab = selectedTab;
// Listen for sync request
var unsubscribe = $scope.$on("ncSyncVal", function (ev, args) {
if (args.key === $scope.model.key) {
// Tell inner controls we are submitting
$scope.$broadcast("formSubmitting", { scope: $scope });
// Sync the values back
angular.forEach($scope.ngModel.tabs, function (tab) {
if (tab.alias.toLowerCase() === selectedTab.alias.toLowerCase()) {
var localPropsMap = selectedTab.properties.reduce(function (map, obj) {
map[obj.alias] = obj;
return map;
}, {});
angular.forEach(tab.properties, function (prop) {
if (localPropsMap.hasOwnProperty(prop.alias)) {
prop.value = localPropsMap[prop.alias].value;
}
});
}
});
}
});
$scope.$on('$destroy', function () {
unsubscribe();
});
};
return {
restrict: "E",
replace: true,
templateUrl: Umbraco.Sys.ServerVariables.umbracoSettings.umbracoPath + "/views/propertyeditors/nestedcontent/nestedcontent.editor.html",
scope: {
ngModel: '=',
tabAlias: '='
},
link: link
};
}
]);
//angular.module("umbraco.directives").directive('nestedContentSubmitWatcher', function () {
// var link = function (scope) {
// // call the load callback on scope to obtain the ID of this submit watcher
// var id = scope.loadCallback();
// scope.$on("formSubmitting", function (ev, args) {
// // on the "formSubmitting" event, call the submit callback on scope to notify the nestedContent controller to do it's magic
// if (id === scope.activeSubmitWatcher) {
// scope.submitCallback();
// }
// });
// }
// return {
// restrict: "E",
// replace: true,
// template: "",
// scope: {
// loadCallback: '=',
// submitCallback: '=',
// activeSubmitWatcher: '='
// },
// link: link
// }
//});

View File

@@ -0,0 +1,47 @@
// Filter to take a node id and grab it's name instead
// Usage: {{ pickerAlias | ncNodeName }}
// Cache for node names so we don't make a ton of requests
var ncNodeNameCache = {
id: "",
keys: {}
};
angular.module("umbraco.filters").filter("ncNodeName", function (editorState, entityResource) {
return function (input) {
// Check we have a value at all
if (input === "" || input.toString() === "0") {
return "";
}
var currentNode = editorState.getCurrent();
// Ensure a unique cache per editor instance
var key = "ncNodeName_" + currentNode.key;
if (ncNodeNameCache.id !== key) {
ncNodeNameCache.id = key;
ncNodeNameCache.keys = {};
}
// See if there is a value in the cache and use that
if (ncNodeNameCache.keys[input]) {
return ncNodeNameCache.keys[input];
}
// No value, so go fetch one
// We'll put a temp value in the cache though so we don't
// make a load of requests while we wait for a response
ncNodeNameCache.keys[input] = "Loading...";
entityResource.getById(input, "Document")
.then(function (ent) {
ncNodeNameCache.keys[input] = ent.name;
});
// Return the current value for now
return ncNodeNameCache.keys[input];
};
});

View File

@@ -0,0 +1,12 @@
angular.module('umbraco.resources').factory('Umbraco.PropertyEditors.NestedContent.Resources',
function ($q, $http, umbRequestHelper) {
return {
getContentTypes: function () {
var url = Umbraco.Sys.ServerVariables.umbracoSettings.umbracoPath + "/backoffice/UmbracoApi/NestedContent/GetContentTypes";
return umbRequestHelper.resourcePromise(
$http.get(url),
'Failed to retrieve content types'
);
},
};
});

View File

@@ -120,6 +120,7 @@
@import "components/umb-querybuilder.less";
@import "components/umb-pagination.less";
@import "components/umb-mini-list-view.less";
@import "components/umb-nested-content.less";
@import "components/buttons/umb-button.less";
@import "components/buttons/umb-button-group.less";

View File

@@ -0,0 +1,192 @@
.nested-content
{
text-align: center;
}
.nested-content__item
{
position: relative;
text-align: left;
border-top: solid 1px transparent;
background: white;
}
.nested-content__item--active:not(.nested-content__item--single)
{
background: #f8f8f8;
}
.nested-content__item.ui-sortable-placeholder
{
background: #f8f8f8;
border: 1px dashed #d9d9d9;
visibility: visible !important;
height: 55px;
margin-top: -1px;
}
.nested-content__item--single > .nested-content__content
{
border: 0;
}
.nested-content__item--single > .nested-content__content > .umb-pane
{
margin: 0;
}
.nested-content__header-bar
{
padding: 15px 20px;
border-bottom: 1px dashed #e0e0e0;
text-align: right;
cursor: pointer;
background-color: white;
-moz-user-select: none;
-khtml-user-select: none;
-webkit-user-select: none;
-o-user-select: none;
}
.nested-content__heading
{
float: left;
line-height: 20px;
}
.nested-content__heading i
{
vertical-align: text-top;
color: #999; /* same icon color as the icons in the item type picker */
margin-right: 10px;
}
.nested-content__icons
{
margin: -6px 0;
opacity: 0;
transition: opacity .15s ease-in-out;
-moz-transition: opacity .15s ease-in-out;
-webkit-transition: opacity .15s ease-in-out;
}
.nested-content__header-bar:hover .nested-content__icons,
.nested-content__item--active > .nested-content__header-bar .nested-content__icons
{
opacity: 1;
}
.nested-content__icon,
.nested-content__icon.nested-content__icon--disabled:hover
{
display: inline-block;
padding: 4px 6px;
margin: 2px;
cursor: pointer;
background: #fff;
border: 1px solid #b6b6b6;
border-radius: 200px;
text-decoration: none !important;
}
.nested-content__icon:hover,
.nested-content__icon--active
{
color: white;
background: #2e8aea;
border-color: #2e8aea;
text-decoration: none;
}
.nested-content__icon .icon,
.nested-content__icon.nested-content__icon--disabled:hover .icon
{
display: block;
font-size: 16px !important;
color: #5f5f5f;
}
.nested-content__icon:hover .icon,
.nested-content__icon--active .icon
{
color: white;
}
.nested-content__icon--disabled
{
opacity: 0.3;
}
.nested-content__footer-bar
{
text-align: center;
padding-top: 20px;
}
.nested-content__content
{
border-bottom: 1px dashed #e0e0e0;
}
.nested-content__content .umb-control-group {
padding-bottom: 0;
}
.nested-content__item.ui-sortable-helper .nested-content__content
{
display: none !important;
}
.nested-content__help-text
{
display: inline-block;
padding: 10px 20px 10px 20px;
clear: both;
font-size: 14px;
color: #555;
background: #f8f8f8;
border-radius: 15px;
}
.nested-content__doctypepicker table input, .nested-content__doctypepicker table select {
width: 100%;
padding-right: 0;
}
.nested-content__doctypepicker table td.icon-navigation, .nested-content__doctypepicker i.nested-content__help-icon {
vertical-align: middle;
color: #CCC;
}
.nested-content__doctypepicker table td.icon-navigation:hover, .nested-content__doctypepicker i.nested-content__help-icon:hover {
color: #343434;
}
.nested-content__doctypepicker i.nested-content__help-icon {
margin-left: 10px;
}
.form-horizontal .nested-content--narrow .controls-row
{
margin-left: 40% !important;
}
.form-horizontal .nested-content--narrow .controls-row .umb-textstring,
.form-horizontal .nested-content--narrow .controls-row .umb-textarea
{
width: 95%;
}
.form-horizontal .nested-content--narrow .controls-row .umb-dropdown {
width: 99%;
}
.usky-grid.nested-content__node-type-picker .cell-tools-menu {
position: relative;
transform: translate(-50%, -25%);
}

View File

@@ -0,0 +1,417 @@
angular.module("umbraco").controller("Umbraco.PropertyEditors.NestedContent.DocTypePickerController", [
"$scope",
"Umbraco.PropertyEditors.NestedContent.Resources",
function ($scope, ncResources) {
$scope.add = function () {
$scope.model.value.push({
// As per PR #4, all stored content type aliases must be prefixed "nc" for easier recognition.
// For good measure we'll also prefix the tab alias "nc"
ncAlias: "",
ncTabAlias: "",
nameTemplate: ""
}
);
}
$scope.remove = function (index) {
$scope.model.value.splice(index, 1);
}
$scope.sortableOptions = {
axis: 'y',
cursor: "move",
handle: ".icon-navigation"
};
$scope.selectedDocTypeTabs = {};
ncResources.getContentTypes().then(function (docTypes) {
$scope.model.docTypes = docTypes;
// Populate document type tab dictionary
docTypes.forEach(function (value) {
$scope.selectedDocTypeTabs[value.alias] = value.tabs;
});
});
if (!$scope.model.value) {
$scope.model.value = [];
$scope.add();
}
}
]);
angular.module("umbraco").controller("Umbraco.PropertyEditors.NestedContent.PropertyEditorController", [
"$scope",
"$interpolate",
"$filter",
"$timeout",
"contentResource",
"localizationService",
"iconHelper",
"Umbraco.PropertyEditors.NestedContent.Resources",
function ($scope, $interpolate, $filter, $timeout, contentResource, localizationService, iconHelper, ncResources) {
//$scope.model.config.contentTypes;
//$scope.model.config.minItems;
//$scope.model.config.maxItems;
//console.log($scope);
var inited = false;
_.each($scope.model.config.contentTypes, function (contentType) {
contentType.nameExp = !!contentType.nameTemplate
? $interpolate(contentType.nameTemplate)
: undefined;
});
$scope.editIconTitle = '';
$scope.moveIconTitle = '';
$scope.deleteIconTitle = '';
// localize the edit icon title
localizationService.localize('general_edit').then(function (value) {
$scope.editIconTitle = value;
});
// localize the delete icon title
localizationService.localize('general_delete').then(function (value) {
$scope.deleteIconTitle = value;
});
// localize the move icon title
localizationService.localize('actions_move').then(function (value) {
$scope.moveIconTitle = value;
});
$scope.nodes = [];
$scope.currentNode = undefined;
$scope.realCurrentNode = undefined;
$scope.scaffolds = undefined;
$scope.sorting = false;
$scope.minItems = $scope.model.config.minItems || 0;
$scope.maxItems = $scope.model.config.maxItems || 0;
if ($scope.maxItems == 0)
$scope.maxItems = 1000;
$scope.singleMode = $scope.minItems == 1 && $scope.maxItems == 1;
$scope.showIcons = $scope.model.config.showIcons || true;
$scope.wideMode = $scope.model.config.hideLabel == "1";
$scope.overlayMenu = {
show: false,
style: {}
};
// helper to force the current form into the dirty state
$scope.setDirty = function () {
if ($scope.propertyForm) {
$scope.propertyForm.$setDirty();
}
};
$scope.addNode = function (alias) {
var scaffold = $scope.getScaffold(alias);
var newNode = initNode(scaffold, null);
$scope.currentNode = newNode;
$scope.setDirty();
$scope.closeNodeTypePicker();
};
$scope.openNodeTypePicker = function (event) {
if ($scope.nodes.length >= $scope.maxItems) {
return;
}
// this could be used for future limiting on node types
$scope.overlayMenu.scaffolds = [];
_.each($scope.scaffolds, function (scaffold) {
$scope.overlayMenu.scaffolds.push({
alias: scaffold.contentTypeAlias,
name: scaffold.contentTypeName,
icon: iconHelper.convertFromLegacyIcon(scaffold.icon)
});
});
if ($scope.overlayMenu.scaffolds.length == 0) {
return;
}
if ($scope.overlayMenu.scaffolds.length == 1) {
// only one scaffold type - no need to display the picker
$scope.addNode($scope.scaffolds[0].contentTypeAlias);
return;
}
$scope.overlayMenu.show = true;
};
$scope.closeNodeTypePicker = function () {
$scope.overlayMenu.show = false;
};
$scope.editNode = function (idx) {
if ($scope.currentNode && $scope.currentNode.key == $scope.nodes[idx].key) {
$scope.currentNode = undefined;
} else {
$scope.currentNode = $scope.nodes[idx];
}
};
$scope.deleteNode = function (idx) {
if ($scope.nodes.length > $scope.model.config.minItems) {
if ($scope.model.config.confirmDeletes && $scope.model.config.confirmDeletes == 1) {
if (confirm("Are you sure you want to delete this item?")) {
$scope.nodes.splice(idx, 1);
$scope.setDirty();
updateModel();
}
} else {
$scope.nodes.splice(idx, 1);
$scope.setDirty();
updateModel();
}
}
};
$scope.getName = function (idx) {
var name = "Item " + (idx + 1);
if ($scope.model.value[idx]) {
var contentType = $scope.getContentTypeConfig($scope.model.value[idx].ncContentTypeAlias);
if (contentType != null && contentType.nameExp) {
// Run the expression against the stored dictionary value, NOT the node object
var item = $scope.model.value[idx];
// Add a temporary index property
item['$index'] = (idx + 1);
var newName = contentType.nameExp(item);
if (newName && (newName = $.trim(newName))) {
name = newName;
}
// Delete the index property as we don't want to persist it
delete item['$index'];
}
}
// Update the nodes actual name value
if ($scope.nodes[idx].name !== name) {
$scope.nodes[idx].name = name;
}
return name;
};
$scope.getIcon = function (idx) {
var scaffold = $scope.getScaffold($scope.model.value[idx].ncContentTypeAlias);
return scaffold && scaffold.icon ? iconHelper.convertFromLegacyIcon(scaffold.icon) : "icon-folder";
}
$scope.sortableOptions = {
axis: 'y',
cursor: "move",
handle: ".nested-content__icon--move",
start: function (ev, ui) {
// Yea, yea, we shouldn't modify the dom, sue me
$("#nested-content--" + $scope.model.id + " .umb-rte textarea").each(function () {
tinymce.execCommand('mceRemoveEditor', false, $(this).attr('id'));
$(this).css("visibility", "hidden");
});
$scope.$apply(function () {
$scope.sorting = true;
});
},
update: function (ev, ui) {
$scope.setDirty();
},
stop: function (ev, ui) {
$("#nested-content--" + $scope.model.id + " .umb-rte textarea").each(function () {
tinymce.execCommand('mceAddEditor', true, $(this).attr('id'));
$(this).css("visibility", "visible");
});
$scope.$apply(function () {
$scope.sorting = false;
updateModel();
});
}
};
$scope.getScaffold = function (alias) {
return _.find($scope.scaffolds, function (scaffold) {
return scaffold.contentTypeAlias == alias;
});
}
$scope.getContentTypeConfig = function (alias) {
return _.find($scope.model.config.contentTypes, function (contentType) {
return contentType.ncAlias == alias;
});
}
// Initialize
var scaffoldsLoaded = 0;
$scope.scaffolds = [];
_.each($scope.model.config.contentTypes, function (contentType) {
contentResource.getScaffold(-20, contentType.ncAlias).then(function (scaffold) {
// remove all tabs except the specified tab
var tab = _.find(scaffold.tabs, function (tab) {
return tab.id != 0 && (tab.alias.toLowerCase() == contentType.ncTabAlias.toLowerCase() || contentType.ncTabAlias == "");
});
scaffold.tabs = [];
if (tab) {
scaffold.tabs.push(tab);
}
// Store the scaffold object
$scope.scaffolds.push(scaffold);
scaffoldsLoaded++;
initIfAllScaffoldsHaveLoaded();
}, function (error) {
scaffoldsLoaded++;
initIfAllScaffoldsHaveLoaded();
});
});
var initIfAllScaffoldsHaveLoaded = function () {
// Initialize when all scaffolds have loaded
if ($scope.model.config.contentTypes.length == scaffoldsLoaded) {
// Because we're loading the scaffolds async one at a time, we need to
// sort them explicitly according to the sort order defined by the data type.
var contentTypeAliases = [];
_.each($scope.model.config.contentTypes, function (contentType) {
contentTypeAliases.push(contentType.ncAlias);
});
$scope.scaffolds = $filter('orderBy')($scope.scaffolds, function (s) {
return contentTypeAliases.indexOf(s.contentTypeAlias);
});
// Convert stored nodes
if ($scope.model.value) {
for (var i = 0; i < $scope.model.value.length; i++) {
var item = $scope.model.value[i];
var scaffold = $scope.getScaffold(item.ncContentTypeAlias);
if (scaffold == null) {
// No such scaffold - the content type might have been deleted. We need to skip it.
continue;
}
initNode(scaffold, item);
}
}
// Enforce min items
if ($scope.nodes.length < $scope.model.config.minItems) {
for (var i = $scope.nodes.length; i < $scope.model.config.minItems; i++) {
$scope.addNode($scope.scaffolds[0].contentTypeAlias);
}
}
// If there is only one item, set it as current node
if ($scope.singleMode || ($scope.nodes.length == 1 && $scope.maxItems == 1)) {
$scope.currentNode = $scope.nodes[0];
}
inited = true;
}
}
var initNode = function (scaffold, item) {
var node = angular.copy(scaffold);
node.key = guid();
node.ncContentTypeAlias = scaffold.contentTypeAlias;
for (var t = 0; t < node.tabs.length; t++) {
var tab = node.tabs[t];
for (var p = 0; p < tab.properties.length; p++) {
var prop = tab.properties[p];
prop.propertyAlias = prop.alias;
prop.alias = $scope.model.alias + "___" + prop.alias;
// Force validation to occur server side as this is the
// only way we can have consistancy between mandatory and
// regex validation messages. Not ideal, but it works.
prop.validation = {
mandatory: false,
pattern: ""
};
if (item) {
if (item[prop.propertyAlias]) {
prop.value = item[prop.propertyAlias];
}
}
}
}
$scope.nodes.push(node);
return node;
}
var updateModel = function () {
if ($scope.realCurrentNode) {
$scope.$broadcast("ncSyncVal", { key: $scope.realCurrentNode.key });
}
if (inited) {
var newValues = [];
for (var i = 0; i < $scope.nodes.length; i++) {
var node = $scope.nodes[i];
var newValue = {
key: node.key,
name: node.name,
ncContentTypeAlias: node.ncContentTypeAlias
};
for (var t = 0; t < node.tabs.length; t++) {
var tab = node.tabs[t];
for (var p = 0; p < tab.properties.length; p++) {
var prop = tab.properties[p];
if (typeof prop.value !== "function") {
newValue[prop.propertyAlias] = prop.value;
}
}
}
newValues.push(newValue);
}
$scope.model.value = newValues;
}
}
$scope.$watch("currentNode", function (newVal) {
updateModel();
$scope.realCurrentNode = newVal;
});
var unsubscribe = $scope.$on("formSubmitting", function (ev, args) {
updateModel();
});
$scope.$on('$destroy', function () {
unsubscribe();
});
var guid = function () {
function _p8(s) {
var p = (Math.random().toString(16) + "000000000").substr(2, 8);
return s ? "-" + p.substr(0, 4) + "-" + p.substr(4, 4) : p;
}
return _p8() + _p8(true) + _p8(true) + _p8();
};
}
]);

View File

@@ -0,0 +1,58 @@
<div id="{{model.alias}}" class="nested-content__doctypepicker" ng-controller="Umbraco.PropertyEditors.NestedContent.DocTypePickerController">
<div>
<table class="table table-striped">
<thead>
<tr>
<th/>
<th>
Document Type
</th>
<th>
Tab
</th>
<th>
Name Template
</th>
<th />
</tr>
</thead>
<tbody ui-sortable="sortableOptions" ng-model="model.value">
<tr ng-repeat="config in model.value">
<td class="icon icon-navigation">
</td>
<td>
<select id="{{model.alias}}_doctype_select"
ng-options="dt.alias as dt.name for dt in model.docTypes | orderBy: 'name'"
ng-model="config.ncAlias" required></select>
</td>
<td>
<select id="{{model.alias}}_tab_select"
ng-options="t for t in selectedDocTypeTabs[config.ncAlias]"
ng-model="config.ncTabAlias" required></select>
</td>
<td>
<input type="text" ng-model="config.nameTemplate" />
</td>
<td>
<a class="btn btn-danger" ng-click="remove($index)" ng-show="model.value.length > 1">Remove</a>
</td>
</tr>
</tbody>
</table>
<div>
<a class="btn" ng-click="add()">Add</a>
<i class="icon icon-help-alt medium nested-content__help-icon" ng-click="showHelpText = !showHelpText"></i>
</div>
</div>
<br/>
<div class="nested-content__help-text" ng-show="showHelpText">
<p>
<b>Tab:</b><br/>
Select the tab who's properties should be displayed. If left blank, the first tab on the doc type will be used.
</p>
<p>
<b>Name template:</b><br/>
Enter an angular expression to evaluate against each item for its name. Use <code ng-non-bindable>{{$index}}</code> to display the item index
</p>
</div>
</div>

View File

@@ -0,0 +1,9 @@
<div class="umb-pane">
<umb-property
property="property"
ng-repeat="property in tab.properties">
<umb-editor model="property"></umb-editor>
</umb-property>
</div>

View File

@@ -0,0 +1,62 @@
<div id="nested-content--{{model.id}}" class="nested-content"
ng-controller="Umbraco.PropertyEditors.NestedContent.PropertyEditorController"
ng-class="{'nested-content--narrow':!wideMode, 'nested-content--wide':wideMode}">
<ng-form>
<div class="nested-content__items" ng-hide="nodes.length == 0" ui-sortable="sortableOptions" ng-model="nodes">
<div class="nested-content__item" ng-repeat="node in nodes" ng-class="{ 'nested-content__item--active' : $parent.realCurrentNode.key == node.key, 'nested-content__item--single' : $parent.singleMode }">
<div class="nested-content__header-bar" ng-click="$parent.editNode($index)" ng-hide="$parent.singleMode">
<div class="nested-content__heading"><i ng-if="showIcons" class="icon" ng-class="$parent.getIcon($index)"></i><span ng-bind="$parent.getName($index)"></span></div>
<div class="nested-content__icons">
<a class="nested-content__icon nested-content__icon--edit" title="{{editIconTitle}}" ng-class="{ 'nested-content__icon--active' : $parent.realCurrentNode.id == node.id }" ng-click="$parent.editNode($index); $event.stopPropagation();" ng-show="$parent.maxItems > 1" prevent-default>
<i class="icon icon-edit"></i>
</a>
<a class="nested-content__icon nested-content__icon--move" title="{{moveIconTitle}}" ng-click="$event.stopPropagation();" ng-show="$parent.nodes.length > 1" prevent-default>
<i class="icon icon-navigation"></i>
</a>
<a class="nested-content__icon nested-content__icon--delete" title="{{deleteIconTitle}}" ng-class="{ 'nested-content__icon--disabled': $parent.nodes.length <= $parent.minItems }" ng-click="$parent.deleteNode($index); $event.stopPropagation();" prevent-default>
<i class="icon icon-trash"></i>
</a>
</div>
</div>
<div class="nested-content__content" ng-if="$parent.realCurrentNode.key == node.key && !$parent.sorting">
<nested-content-editor ng-model="node" tab-alias="ncTabAlias" />
</div>
</div>
</div>
<div class="nested-content__help-text" ng-show="nodes.length == 0">
<localize key="grid_addElement"></localize>
</div>
<div class="nested-content__footer-bar" ng-hide="nodes.length >= maxItems">
<a class="nested-content__icon" ng-click="openNodeTypePicker($event)" prevent-default>
<i class="icon icon-add"></i>
</a>
</div>
<div class="usky-grid nested-content__node-type-picker" ng-if="overlayMenu.show">
<div class="cell-tools-menu" ng-style="overlayMenu.style" style="margin: 0;" delayed-mouseleave="closeNodeTypePicker()" on-delayed-mouseleave="closeNodeTypePicker()">
<h5>
<localize key="grid_insertControl" />
</h5>
<ul class="elements">
<li ng-repeat="scaffold in overlayMenu.scaffolds">
<a ng-click="addNode(scaffold.alias)" href>
<i class="icon {{scaffold.icon}}"></i>
{{scaffold.name}}
</a>
</li>
</ul>
</div>
</div>
</ng-form>
</div>

View File

@@ -6,6 +6,7 @@ using System.Linq;
using Umbraco.Core.Models;
using Umbraco.Core.Models.PublishedContent;
using Umbraco.Core.PropertyEditors.ValueConverters;
using Umbraco.Web.PropertyEditors;
using Umbraco.Web.PropertyEditors.ValueConverters;
@@ -13,12 +14,12 @@ namespace Umbraco.Web.Cache
{
/// <summary>
/// A cache refresher to ensure member cache is updated when members change
/// </summary>
/// </summary>
public sealed class DataTypeCacheRefresher : JsonCacheRefresherBase<DataTypeCacheRefresher>
{
#region Static helpers
/// <summary>
/// Converts the json to a JsonPayload object
/// </summary>
@@ -29,7 +30,7 @@ namespace Umbraco.Web.Cache
var serializer = new JavaScriptSerializer();
var jsonObject = serializer.Deserialize<JsonPayload[]>(json);
return jsonObject;
}
}
/// <summary>
/// Creates the custom Json payload used to refresh cache amongst the servers
@@ -43,7 +44,7 @@ namespace Umbraco.Web.Cache
var json = serializer.Serialize(items);
return json;
}
/// <summary>
/// Converts a macro to a jsonPayload object
/// </summary>
@@ -58,7 +59,7 @@ namespace Umbraco.Web.Cache
};
return payload;
}
#endregion
#region Sub classes
@@ -93,7 +94,7 @@ namespace Umbraco.Web.Cache
//we need to clear the ContentType runtime cache since that is what caches the
// db data type to store the value against and anytime a datatype changes, this also might change
// we basically need to clear all sorts of runtime caches here because so many things depend upon a data type
ClearAllIsolatedCacheByEntityType<IContent>();
ClearAllIsolatedCacheByEntityType<IContentType>();
ClearAllIsolatedCacheByEntityType<IMedia>();
@@ -104,14 +105,15 @@ namespace Umbraco.Web.Cache
ApplicationContext.Current.ApplicationCache.RuntimeCache.ClearCacheByKeySearch(CacheKeys.KeyToIdCacheKey);
var dataTypeCache = ApplicationContext.Current.ApplicationCache.IsolatedRuntimeCache.GetCache<IDataTypeDefinition>();
payloads.ForEach(payload =>
foreach (var payload in payloads)
{
//clears the prevalue cache
if (dataTypeCache)
dataTypeCache.Result.ClearCacheByKeySearch(string.Format("{0}_{1}", CacheKeys.DataTypePreValuesCacheKey, payload.Id));
PublishedContentType.ClearDataType(payload.Id);
});
NestedContentHelper.ClearCache(payload.Id);
}
TagsValueConverter.ClearCaches();
MultipleMediaPickerPropertyConverter.ClearCaches();

View 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; }
}
}
}

View 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; } }
}
}

View 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()
});
}
}
}

View 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
}
}

View 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", Group = "lists", Icon = "icon-thumbnail-list")]
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;
}
}
}

View File

@@ -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;
}
}
}

View File

@@ -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;
}
}
}

View File

@@ -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;
}
}
}

View File

@@ -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;
}

View File

@@ -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" />