Merge pull request #1009 from umbraco/temp-U4-7588

U4-7588 Conflicting compositions should not be allowed
This commit is contained in:
Claus
2016-01-13 11:58:38 +01:00
12 changed files with 519 additions and 87 deletions

View File

@@ -10,27 +10,51 @@ namespace Umbraco.Core.Services
/// <summary>
/// Returns the available composite content types for a given content type
/// </summary>
/// <param name="allContentTypes"></param>
/// <param name="filterContentTypes">
/// This is normally an empty list but if additional content type aliases are passed in, any content types containing those aliases will be filtered out
/// along with any content types that have matching property types that are included in the filtered content types
/// </param>
/// <param name="ctService"></param>
/// <param name="source"></param>
/// <param name="filterPropertyTypes">
/// This is normally an empty list but if additional property type aliases are passed in, any content types that have these aliases will be filtered out.
/// This is required because in the case of creating/modifying a content type because new property types being added to it are not yet persisted so cannot
/// be looked up via the db, they need to be passed in.
/// </param>
/// <returns></returns>
public static IEnumerable<IContentTypeComposition> GetAvailableCompositeContentTypes(this IContentTypeService ctService,
public static IEnumerable<Tuple<IContentTypeComposition, bool>> GetAvailableCompositeContentTypes(this IContentTypeService ctService,
IContentTypeComposition source,
IContentTypeComposition[] allContentTypes)
{
//below is all ported from the old doc type editor and comes with the same weaknesses /insanity / magic
IContentTypeComposition[] allContentTypes,
string[] filterContentTypes = null,
string[] filterPropertyTypes = null)
{
filterContentTypes = filterContentTypes == null
? new string[] { }
: filterContentTypes.Where(x => x.IsNullOrWhiteSpace() == false).ToArray();
// note: there are many sanity checks missing here and there ;-((
// make sure once and for all
//if (allContentTypes.Any(x => x.ParentId > 0 && x.ContentTypeComposition.Any(y => y.Id == x.ParentId) == false))
// throw new Exception("A parent does not belong to a composition.");
filterPropertyTypes = filterPropertyTypes == null
? new string[] {}
: filterPropertyTypes.Where(x => x.IsNullOrWhiteSpace() == false).ToArray();
if (source != null)
//create the full list of property types to use as the filter
//this is the combination of all property type aliases found in the content types passed in for the filter
//as well as the specific property types passed in for the filter
filterPropertyTypes = allContentTypes
.Where(c => filterContentTypes.InvariantContains(c.Alias))
.SelectMany(c => c.PropertyTypes)
.Select(c => c.Alias)
.Union(filterPropertyTypes)
.ToArray();
var sourceId = source != null ? source.Id : 0;
// find out if any content type uses this content type
var isUsing = allContentTypes.Where(x => x.ContentTypeComposition.Any(y => y.Id == sourceId)).ToArray();
if (isUsing.Length > 0)
{
// find out if any content type uses this content type
var isUsing = allContentTypes.Where(x => x.ContentTypeComposition.Any(y => y.Id == source.Id)).ToArray();
if (isUsing.Length > 0)
{
//if already in use a composition, do not allow any composited types
return new List<IContentTypeComposition>();
}
//if already in use a composition, do not allow any composited types
return new List<Tuple<IContentTypeComposition, bool>>();
}
// if it is not used then composition is possible
@@ -50,21 +74,36 @@ namespace Umbraco.Core.Services
foreach (var x in indirectContentTypes)
list.Add(x);
//// directContentTypes are those we use directly
//// they are already in indirectContentTypes, no need to add to the list
//var directContentTypes = source.ContentTypeComposition.ToArray();
//var enabled = usableContentTypes.Select(x => x.Id) // those we can use
// .Except(indirectContentTypes.Select(x => x.Id)) // except those that are indirectly used
// .Union(directContentTypes.Select(x => x.Id)) // but those that are directly used
// .Where(x => x != source.ParentId) // but not the parent
// .Distinct()
// .ToArray();
return list
.Where(x => x.Id != (source != null ? source.Id : 0))
//At this point we have a list of content types that 'could' be compositions
//now we'll filter this list based on the filters requested
var filtered = list
.Where(x =>
{
//need to filter any content types that are included in this list
return filterContentTypes.Any(c => c.InvariantEquals(x.Alias)) == false;
})
.Where(x =>
{
//need to filter any content types that have matching property aliases that are included in this list
//ensure that we don't return if there's any overlapping property aliases from the filtered ones specified
return filterPropertyTypes.Intersect(
x.PropertyTypes.Select(p => p.Alias),
StringComparer.InvariantCultureIgnoreCase).Any() == false;
})
.OrderBy(x => x.Name)
.ToList();
//now we can create our result based on what is still available
var result = list
//not itself
.Where(x => x.Id != sourceId)
.OrderBy(x => x.Name)
.Select(composition => filtered.Contains(composition)
? new Tuple<IContentTypeComposition, bool>(composition, true)
: new Tuple<IContentTypeComposition, bool>(composition, false)).ToList();
return result;
}
/// <summary>
@@ -74,6 +113,8 @@ namespace Umbraco.Core.Services
/// <returns></returns>
private static IEnumerable<IContentTypeComposition> GetDirectOrIndirect(IContentTypeComposition ctype)
{
if (ctype == null) return Enumerable.Empty<IContentTypeComposition>();
// hashset guarantees unicity on Id
var all = new HashSet<IContentTypeComposition>(new DelegateEqualityComparer<IContentTypeComposition>(
(x, y) => x.Id == y.Id,

View File

@@ -1,6 +1,9 @@
using System;
using System.Linq;
using Moq;
using NUnit.Framework;
using Umbraco.Core;
using Umbraco.Core.Models;
using Umbraco.Core.Services;
using Umbraco.Tests.TestHelpers;
using Umbraco.Tests.TestHelpers.Entities;
@@ -10,6 +13,119 @@ namespace Umbraco.Tests.Services
[TestFixture]
public class ContentTypeServiceExtensionsTests : BaseUmbracoApplicationTest
{
[Test]
public void GetAvailableCompositeContentTypes_No_Overlap_By_Content_Type_And_Property_Type_Alias()
{
Action<string, IContentType> addPropType = (alias, ct) =>
{
var contentCollection = new PropertyTypeCollection
{
new PropertyType(Constants.PropertyEditors.TextboxAlias, DataTypeDatabaseType.Ntext) {Alias = alias, Name = "Title", Description = "", Mandatory = false, SortOrder = 1, DataTypeDefinitionId = -88}
};
var pg = new PropertyGroup(contentCollection) { Name = "test", SortOrder = 1 };
ct.PropertyGroups.Add(pg);
};
var ct1 = MockedContentTypes.CreateBasicContentType("ct1", "CT1", null);
var ct2 = MockedContentTypes.CreateBasicContentType("ct2", "CT2", null);
addPropType("title", ct2);
var ct3 = MockedContentTypes.CreateBasicContentType("ct3", "CT3", null);
addPropType("title", ct3);
var ct4 = MockedContentTypes.CreateBasicContentType("ct4", "CT4", null);
var ct5 = MockedContentTypes.CreateBasicContentType("ct5", "CT5", null);
addPropType("blah", ct5);
ct1.Id = 1;
ct2.Id = 2;
ct3.Id = 3;
ct4.Id = 4;
ct5.Id = 4;
var service = new Mock<IContentTypeService>();
var availableTypes = service.Object.GetAvailableCompositeContentTypes(
ct1,
new[] { ct1, ct2, ct3, ct4, ct5 },
new[] { ct2.Alias },
new[] { "blah" })
.Where(x => x.Item2).Select(x => x.Item1).ToArray();
Assert.AreEqual(1, availableTypes.Count());
Assert.AreEqual(ct4.Id, availableTypes.ElementAt(0).Id);
}
[Test]
public void GetAvailableCompositeContentTypes_No_Overlap_By_Property_Type_Alias()
{
Action<IContentType> addPropType = ct =>
{
var contentCollection = new PropertyTypeCollection
{
new PropertyType(Constants.PropertyEditors.TextboxAlias, DataTypeDatabaseType.Ntext) {Alias = "title", Name = "Title", Description = "", Mandatory = false, SortOrder = 1, DataTypeDefinitionId = -88}
};
var pg = new PropertyGroup(contentCollection) { Name = "test", SortOrder = 1 };
ct.PropertyGroups.Add(pg);
};
var ct1 = MockedContentTypes.CreateBasicContentType("ct1", "CT1", null);
var ct2 = MockedContentTypes.CreateBasicContentType("ct2", "CT2", null);
addPropType(ct2);
var ct3 = MockedContentTypes.CreateBasicContentType("ct3", "CT3", null);
addPropType(ct3);
var ct4 = MockedContentTypes.CreateBasicContentType("ct4", "CT4", null);
ct1.Id = 1;
ct2.Id = 2;
ct3.Id = 3;
ct4.Id = 4;
var service = new Mock<IContentTypeService>();
var availableTypes = service.Object.GetAvailableCompositeContentTypes(
ct1,
new[] { ct1, ct2, ct3, ct4 },
new string[] { },
new[] { "title" })
.Where(x => x.Item2).Select(x => x.Item1).ToArray();
Assert.AreEqual(1, availableTypes.Count());
Assert.AreEqual(ct4.Id, availableTypes.ElementAt(0).Id);
}
[Test]
public void GetAvailableCompositeContentTypes_No_Overlap_By_Content_Type()
{
Action<IContentType> addPropType = ct =>
{
var contentCollection = new PropertyTypeCollection
{
new PropertyType(Constants.PropertyEditors.TextboxAlias, DataTypeDatabaseType.Ntext) {Alias = "title", Name = "Title", Description = "", Mandatory = false, SortOrder = 1, DataTypeDefinitionId = -88}
};
var pg = new PropertyGroup(contentCollection) { Name = "test", SortOrder = 1 };
ct.PropertyGroups.Add(pg);
};
var ct1 = MockedContentTypes.CreateBasicContentType("ct1", "CT1", null);
var ct2 = MockedContentTypes.CreateBasicContentType("ct2", "CT2", null);
addPropType(ct2);
var ct3 = MockedContentTypes.CreateBasicContentType("ct3", "CT3", null);
addPropType(ct3);
var ct4 = MockedContentTypes.CreateBasicContentType("ct4", "CT4", null);
ct1.Id = 1;
ct2.Id = 2;
ct3.Id = 3;
ct4.Id = 4;
var service = new Mock<IContentTypeService>();
var availableTypes = service.Object.GetAvailableCompositeContentTypes(
ct1,
new[] { ct1, ct2, ct3, ct4 },
new [] {ct2.Alias})
.Where(x => x.Item2).Select(x => x.Item1).ToArray();
Assert.AreEqual(1, availableTypes.Count());
Assert.AreEqual(ct4.Id, availableTypes.ElementAt(0).Id);
}
[Test]
public void GetAvailableCompositeContentTypes_Not_Itself()
{
@@ -24,7 +140,8 @@ namespace Umbraco.Tests.Services
var availableTypes = service.Object.GetAvailableCompositeContentTypes(
ct1,
new[] {ct1, ct2, ct3});
new[] {ct1, ct2, ct3})
.Where(x => x.Item2).Select(x => x.Item1).ToArray();
Assert.AreEqual(2, availableTypes.Count());
Assert.AreEqual(ct2.Id, availableTypes.ElementAt(0).Id);
@@ -89,7 +206,8 @@ namespace Umbraco.Tests.Services
var availableTypes = service.Object.GetAvailableCompositeContentTypes(
ct1,
new[] { ct1, ct2, ct3 });
new[] { ct1, ct2, ct3 })
.Where(x => x.Item2).Select(x => x.Item1).ToArray();
Assert.AreEqual(1, availableTypes.Count());
Assert.AreEqual(ct3.Id, availableTypes.Single().Id);
@@ -111,7 +229,8 @@ namespace Umbraco.Tests.Services
var availableTypes = service.Object.GetAvailableCompositeContentTypes(
ct1,
new[] { ct1, ct2, ct3 });
new[] { ct1, ct2, ct3 })
.Where(x => x.Item2).Select(x => x.Item1).ToArray();
Assert.AreEqual(2, availableTypes.Count());
Assert.AreEqual(ct2.Id, availableTypes.ElementAt(0).Id);
@@ -137,7 +256,8 @@ namespace Umbraco.Tests.Services
var availableTypes = service.Object.GetAvailableCompositeContentTypes(
ct1,
new[] { ct1, ct2, ct3 });
new[] { ct1, ct2, ct3 })
.Where(x => x.Item2).Select(x => x.Item1).ToArray();
Assert.AreEqual(3, availableTypes.Count());
Assert.AreEqual(ct2.Id, availableTypes.ElementAt(0).Id);

View File

@@ -1,7 +1,7 @@
(function() {
'use strict';
function GroupsBuilderDirective(contentTypeHelper, contentTypeResource, mediaTypeResource, dataTypeHelper, dataTypeResource, $filter, iconHelper, $q) {
function GroupsBuilderDirective(contentTypeHelper, contentTypeResource, mediaTypeResource, dataTypeHelper, dataTypeResource, $filter, iconHelper, $q, $timeout) {
function link(scope, el, attr, ctrl) {
@@ -116,6 +116,47 @@
}
function filterAvailableCompositions(selectedContentType, selecting) {
//selecting = true if the user has check the item, false if the user has unchecked the item
var selectedContentTypeAliases = selecting ?
//the user has selected the item so add to the current list
_.union(scope.compositionsDialogModel.compositeContentTypes, [selectedContentType.alias]) :
//the user has unselected the item so remove from the current list
_.reject(scope.compositionsDialogModel.compositeContentTypes, function(i) {
return i === selectedContentType.alias;
});
//get the currently assigned property type aliases - ensure we pass these to the server side filer
var propAliasesExisting = _.filter(_.flatten(_.map(scope.model.groups, function(g) {
return _.map(g.properties, function(p) {
return p.alias;
});
})), function (f) {
return f !== null && f !== undefined;
});
//use a different resource lookup depending on the content type type
var resourceLookup = scope.contentType === "documentType" ? contentTypeResource.getAvailableCompositeContentTypes : mediaTypeResource.getAvailableCompositeContentTypes;
return resourceLookup(scope.model.id, selectedContentTypeAliases, propAliasesExisting).then(function (filteredAvailableCompositeTypes) {
_.each(scope.compositionsDialogModel.availableCompositeContentTypes, function (current) {
//reset first
current.allowed = true;
//see if this list item is found in the response (allowed) list
var found = _.find(filteredAvailableCompositeTypes, function (f) {
return current.contentType.alias === f.contentType.alias;
});
//allow if the item was found in the response (allowed) list -
// and ensure its set to allowed if it is currently checked
current.allowed = (selectedContentTypeAliases.indexOf(current.contentType.alias) !== -1) ||
((found !== null && found !== undefined) ? found.allowed : false);
});
});
}
function updatePropertiesSortOrder() {
angular.forEach(scope.model.groups, function(group){
@@ -186,7 +227,7 @@
// submit overlay if no compositions has been removed
// or the action has been confirmed
} else {
// make sure that all tabs has an init property
if (scope.model.groups.length !== 0) {
angular.forEach(scope.model.groups, function(group) {
@@ -211,30 +252,47 @@
scope.compositionsDialogModel = null;
},
selectCompositeContentType: function(compositeContentType) {
selectCompositeContentType: function (selectedContentType) {
if (scope.model.compositeContentTypes.indexOf(compositeContentType.alias) === -1) {
//merge composition with content type
//first check if this is a new selection - we need to store this value here before any further digests/async
// because after that the scope.model.compositeContentTypes will be populated with the selected value.
var newSelection = scope.model.compositeContentTypes.indexOf(selectedContentType.alias) === -1;
if(scope.contentType === "documentType") {
if (newSelection) {
//merge composition with content type
contentTypeResource.getById(compositeContentType.id).then(function(composition){
contentTypeHelper.mergeCompositeContentType(scope.model, composition);
});
//use a different resource lookup depending on the content type type
var resourceLookup = scope.contentType === "documentType" ? contentTypeResource.getById : mediaTypeResource.getById;
resourceLookup(selectedContentType.id).then(function (composition) {
//based on the above filtering we shouldn't be able to select an invalid one, but let's be safe and
// double check here.
var overlappingAliases = contentTypeHelper.validateAddingComposition(scope.model, composition);
if (overlappingAliases.length > 0) {
//this will create an invalid composition, need to uncheck it
scope.compositionsDialogModel.compositeContentTypes.splice(
scope.compositionsDialogModel.compositeContentTypes.indexOf(composition.alias), 1);
//dissallow this until something else is unchecked
selectedContentType.allowed = false;
}
else {
contentTypeHelper.mergeCompositeContentType(scope.model, composition);
}
} else if(scope.contentType === "mediaType") {
//based on the selection, we need to filter the available composite types list
filterAvailableCompositions(selectedContentType, newSelection).then(function () {
//TODO: Here we could probably re-enable selection if we previously showed a throbber or something
});
});
}
else {
// split composition from content type
contentTypeHelper.splitCompositeContentType(scope.model, selectedContentType);
mediaTypeResource.getById(compositeContentType.id).then(function(composition){
contentTypeHelper.mergeCompositeContentType(scope.model, composition);
});
}
} else {
// split composition from content type
contentTypeHelper.splitCompositeContentType(scope.model, compositeContentType);
//based on the selection, we need to filter the available composite types list
filterAvailableCompositions(selectedContentType, newSelection).then(function () {
//TODO: Here we could probably re-enable selection if we previously showed a throbber or something
});
}
}
@@ -242,21 +300,33 @@
var availableContentTypeResource = scope.contentType === "documentType" ? contentTypeResource.getAvailableCompositeContentTypes : mediaTypeResource.getAvailableCompositeContentTypes;
var countContentTypeResource = scope.contentType === "documentType" ? contentTypeResource.getCount : mediaTypeResource.getCount;
$q.all([
//get the currently assigned property type aliases - ensure we pass these to the server side filer
var propAliasesExisting = _.filter(_.flatten(_.map(scope.model.groups, function(g) {
return _.map(g.properties, function(p) {
return p.alias;
});
})), function(f) {
return f !== null && f !== undefined;
});
$q.all([
//get available composite types
availableContentTypeResource(scope.model.id).then(function (result) {
availableContentTypeResource(scope.model.id, [], propAliasesExisting).then(function (result) {
scope.compositionsDialogModel.availableCompositeContentTypes = result;
var contentTypes = _.map(scope.compositionsDialogModel.availableCompositeContentTypes, function(c) {
return c.contentType;
});
// convert icons for composite content types
iconHelper.formatContentTypeIcons(scope.compositionsDialogModel.availableCompositeContentTypes);
iconHelper.formatContentTypeIcons(contentTypes);
}),
//get content type count
countContentTypeResource().then(function (result) {
countContentTypeResource().then(function(result) {
scope.compositionsDialogModel.totalContentTypes = parseInt(result, 10);
})
]).then(function () {
//resolves when both other promises are done, now show it
scope.compositionsDialogModel.show = true;
});
]).then(function() {
//resolves when both other promises are done, now show it
scope.compositionsDialogModel.show = true;
});
};

View File

@@ -16,13 +16,37 @@ function contentTypeResource($q, $http, umbRequestHelper, umbDataFormatter) {
'Failed to retrieve count');
},
getAvailableCompositeContentTypes: function (contentTypeId) {
getAvailableCompositeContentTypes: function (contentTypeId, filterContentTypes, filterPropertyTypes) {
if (!filterContentTypes) {
filterContentTypes = [];
}
if (!filterPropertyTypes) {
filterPropertyTypes = [];
}
var query = "";
_.each(filterContentTypes, function (item) {
query += "filterContentTypes=" + item + "&";
});
// if filterContentTypes array is empty we need a empty variable in the querystring otherwise the service returns a error
if (filterContentTypes.length === 0) {
query += "filterContentTypes=&";
}
_.each(filterPropertyTypes, function (item) {
query += "filterPropertyTypes=" + item + "&";
});
// if filterPropertyTypes array is empty we need a empty variable in the querystring otherwise the service returns a error
if (filterPropertyTypes.length === 0) {
query += "filterPropertyTypes=&";
}
query += "contentTypeId=" + contentTypeId;
return umbRequestHelper.resourcePromise(
$http.get(
umbRequestHelper.getApiUrl(
"contentTypeApiBaseUrl",
"GetAvailableCompositeContentTypes",
[{ contentTypeId: contentTypeId }])),
query)),
'Failed to retrieve data for content type id ' + contentTypeId);
},

View File

@@ -16,13 +16,37 @@ function mediaTypeResource($q, $http, umbRequestHelper, umbDataFormatter) {
'Failed to retrieve count');
},
getAvailableCompositeContentTypes: function (contentTypeId) {
getAvailableCompositeContentTypes: function (contentTypeId, filterContentTypes, filterPropertyTypes) {
if (!filterContentTypes) {
filterContentTypes = [];
}
if (!filterPropertyTypes) {
filterPropertyTypes = [];
}
var query = "";
_.each(filterContentTypes, function (item) {
query += "filterContentTypes=" + item + "&";
});
// if filterContentTypes array is empty we need a empty variable in the querystring otherwise the service returns a error
if (filterContentTypes.length === 0) {
query += "filterContentTypes=&";
}
_.each(filterPropertyTypes, function (item) {
query += "filterPropertyTypes=" + item + "&";
});
// if filterPropertyTypes array is empty we need a empty variable in the querystring otherwise the service returns a error
if (filterPropertyTypes.length === 0) {
query += "filterPropertyTypes=&";
}
query += "contentTypeId=" + contentTypeId;
return umbRequestHelper.resourcePromise(
$http.get(
umbRequestHelper.getApiUrl(
"mediaTypeApiBaseUrl",
"GetAvailableCompositeMediaTypes",
[{ contentTypeId: contentTypeId }])),
query)),
'Failed to retrieve data for content type id ' + contentTypeId);
},

View File

@@ -7,13 +7,37 @@ function memberTypeResource($q, $http, umbRequestHelper, umbDataFormatter) {
return {
getAvailableCompositeContentTypes: function (contentTypeId) {
getAvailableCompositeContentTypes: function (contentTypeId, filterContentTypes, filterPropertyTypes) {
if (!filterContentTypes) {
filterContentTypes = [];
}
if (!filterPropertyTypes) {
filterPropertyTypes = [];
}
var query = "";
_.each(filterContentTypes, function (item) {
query += "filterContentTypes=" + item + "&";
});
// if filterContentTypes array is empty we need a empty variable in the querystring otherwise the service returns a error
if (filterContentTypes.length === 0) {
query += "filterContentTypes=&";
}
_.each(filterPropertyTypes, function (item) {
query += "filterPropertyTypes=" + item + "&";
});
// if filterPropertyTypes array is empty we need a empty variable in the querystring otherwise the service returns a error
if (filterPropertyTypes.length === 0) {
query += "filterPropertyTypes=&";
}
query += "contentTypeId=" + contentTypeId;
return umbRequestHelper.resourcePromise(
$http.get(
umbRequestHelper.getApiUrl(
"memberTypeApiBaseUrl",
"GetAvailableCompositeMemberTypes",
[{ contentTypeId: contentTypeId }])),
query)),
'Failed to retrieve data for content type id ' + contentTypeId);
},

View File

@@ -43,8 +43,41 @@ function contentTypeHelper(contentTypeResource, dataTypeResource, $filter) {
return newArray;
},
validateAddingComposition: function(contentType, compositeContentType) {
//Validate that by adding this group that we are not adding duplicate property type aliases
var propertiesAdding = _.flatten(_.map(compositeContentType.groups, function(g) {
return _.map(g.properties, function(p) {
return p.alias;
});
}));
var propAliasesExisting = _.filter(_.flatten(_.map(contentType.groups, function(g) {
return _.map(g.properties, function(p) {
return p.alias;
});
})), function(f) {
return f !== null && f !== undefined;
});
var intersec = _.intersection(propertiesAdding, propAliasesExisting);
if (intersec.length > 0) {
//return the overlapping property aliases
return intersec;
}
//no overlapping property aliases
return [];
},
mergeCompositeContentType: function(contentType, compositeContentType) {
//Validate that there are no overlapping aliases
var overlappingAliases = this.validateAddingComposition(contentType, compositeContentType);
if (overlappingAliases.length > 0) {
throw new Error("Cannot add this composition, these properties already exist on the content type: " + overlappingAliases.join());
}
angular.forEach(compositeContentType.groups, function(compositionGroup) {
// order composition groups based on sort order
@@ -134,7 +167,7 @@ function contentTypeHelper(contentTypeResource, dataTypeResource, $filter) {
// push id to array of merged composite content types
compositionGroup.parentTabContentTypes.push(compositeContentType.id);
// push group before placeholder tab
contentType.groups.unshift(compositionGroup);

View File

@@ -26,19 +26,24 @@
</umb-empty-state>
<ul class="umb-checkbox-list">
<li class="umb-checkbox-list__item" ng-repeat="compositeContentType in model.availableCompositeContentTypes | filter:searchTerm" ng-class="{ '-selected': model.contentType.compositeContentTypes.indexOf(compositeContentType.alias)+1 }">
<div class="umb-checkbox-list__item-checkbox" ng-class="{ '-selected': model.contentType.compositeContentTypes.indexOf(compositeContentType.alias)+1 }">
<input type="checkbox"
<li class="umb-checkbox-list__item"
ng-repeat="compositeContentType in model.availableCompositeContentTypes | filter:searchTerm"
ng-class="{ '-selected': model.compositeContentTypes.indexOf(compositeContentType.alias)+1 }">
<div class="umb-checkbox-list__item-checkbox"
ng-class="{ '-selected': model.compositeContentTypes.indexOf(compositeContentType.alias)+1 }">
<input type="checkbox"
checklist-model="model.compositeContentTypes"
checklist-value="compositeContentType.alias"
ng-change="model.selectCompositeContentType(compositeContentType)" />
checklist-value="compositeContentType.contentType.alias"
ng-change="model.selectCompositeContentType(compositeContentType.contentType)"
ng-disabled="compositeContentType.allowed===false" />
</div>
<div class="umb-checkbox-list__item-text">
<i class="{{ compositeContentType.icon }} umb-checkbox-list__item-icon"></i>
{{ compositeContentType.name }}
<i class="{{ compositeContentType.contentType.icon }} umb-checkbox-list__item-icon"></i>
{{ compositeContentType.contentType.name }}
</div>
</li>
</ul>
</ul>

View File

@@ -95,9 +95,31 @@ namespace Umbraco.Web.Editors
return ApplicationContext.Services.ContentTypeService.GetAllPropertyTypeAliases();
}
public IEnumerable<EntityBasic> GetAvailableCompositeContentTypes(int contentTypeId)
/// <summary>
/// Returns the avilable compositions for this content type
/// </summary>
/// <param name="contentTypeId"></param>
/// <param name="filterContentTypes">
/// This is normally an empty list but if additional content type aliases are passed in, any content types containing those aliases will be filtered out
/// along with any content types that have matching property types that are included in the filtered content types
/// </param>
/// <param name="filterPropertyTypes">
/// This is normally an empty list but if additional property type aliases are passed in, any content types that have these aliases will be filtered out.
/// This is required because in the case of creating/modifying a content type because new property types being added to it are not yet persisted so cannot
/// be looked up via the db, they need to be passed in.
/// </param>
/// <returns></returns>
public HttpResponseMessage GetAvailableCompositeContentTypes(int contentTypeId,
[FromUri]string[] filterContentTypes,
[FromUri]string[] filterPropertyTypes)
{
return PerformGetAvailableCompositeContentTypes(contentTypeId, UmbracoObjectTypes.DocumentType);
var result = PerformGetAvailableCompositeContentTypes(contentTypeId, UmbracoObjectTypes.DocumentType, filterContentTypes, filterPropertyTypes)
.Select(x => new
{
contentType = x.Item1,
allowed = x.Item2
});
return Request.CreateResponse(result);
}
[UmbracoTreeAuthorize(

View File

@@ -49,8 +49,22 @@ namespace Umbraco.Web.Editors
/// <summary>
/// Returns the available composite content types for a given content type
/// </summary>
/// <param name="type"></param>
/// <param name="filterContentTypes">
/// This is normally an empty list but if additional content type aliases are passed in, any content types containing those aliases will be filtered out
/// along with any content types that have matching property types that are included in the filtered content types
/// </param>
/// <param name="filterPropertyTypes">
/// This is normally an empty list but if additional property type aliases are passed in, any content types that have these aliases will be filtered out.
/// This is required because in the case of creating/modifying a content type because new property types being added to it are not yet persisted so cannot
/// be looked up via the db, they need to be passed in.
/// </param>
/// <param name="contentTypeId"></param>
/// <returns></returns>
protected IEnumerable<EntityBasic> PerformGetAvailableCompositeContentTypes(int contentTypeId, UmbracoObjectTypes type)
protected IEnumerable<Tuple<EntityBasic, bool>> PerformGetAvailableCompositeContentTypes(int contentTypeId,
UmbracoObjectTypes type,
string[] filterContentTypes,
string[] filterPropertyTypes)
{
IContentTypeComposition source = null;
@@ -90,13 +104,24 @@ namespace Umbraco.Web.Editors
throw new ArgumentOutOfRangeException("The entity type was not a content type");
}
var filtered = Services.ContentTypeService.GetAvailableCompositeContentTypes(source, allContentTypes);
var filtered = Services.ContentTypeService.GetAvailableCompositeContentTypes(source, allContentTypes, filterContentTypes, filterPropertyTypes);
var currCompositions = source == null ? new string[] { } : source.ContentTypeComposition.Select(x => x.Alias).ToArray();
return filtered
.Select(Mapper.Map<IContentTypeComposition, EntityBasic>)
.Select(x => new Tuple<EntityBasic, bool>(Mapper.Map<IContentTypeComposition, EntityBasic>(x.Item1), x.Item2))
.Select(x =>
{
x.Name = TranslateItem(x.Name);
//translate the name
x.Item1.Name = TranslateItem(x.Item1.Name);
//we need to ensure that the item is enabled if it is already selected
if (currCompositions.Contains(x.Item1.Alias))
{
//re-set x to be allowed (NOTE: I didn't know you could set an enumerable item in a lambda!)
x = new Tuple<EntityBasic, bool>(x.Item1, true);
}
return x;
})
.ToList();

View File

@@ -87,9 +87,31 @@ namespace Umbraco.Web.Editors
return Request.CreateResponse(HttpStatusCode.OK);
}
public IEnumerable<EntityBasic> GetAvailableCompositeMediaTypes(int contentTypeId)
/// <summary>
/// Returns the avilable compositions for this content type
/// </summary>
/// <param name="contentTypeId"></param>
/// <param name="filterContentTypes">
/// This is normally an empty list but if additional content type aliases are passed in, any content types containing those aliases will be filtered out
/// along with any content types that have matching property types that are included in the filtered content types
/// </param>
/// <param name="filterPropertyTypes">
/// This is normally an empty list but if additional property type aliases are passed in, any content types that have these aliases will be filtered out.
/// This is required because in the case of creating/modifying a content type because new property types being added to it are not yet persisted so cannot
/// be looked up via the db, they need to be passed in.
/// </param>
/// <returns></returns>
public HttpResponseMessage GetAvailableCompositeMediaTypes(int contentTypeId,
[FromUri]string[] filterContentTypes,
[FromUri]string[] filterPropertyTypes)
{
return PerformGetAvailableCompositeContentTypes(contentTypeId, UmbracoObjectTypes.MediaType);
var result = PerformGetAvailableCompositeContentTypes(contentTypeId, UmbracoObjectTypes.MediaType, filterContentTypes, filterPropertyTypes)
.Select(x => new
{
contentType = x.Item1,
allowed = x.Item2
});
return Request.CreateResponse(result);
}
public ContentTypeCompositionDisplay GetEmpty(int parentId)

View File

@@ -79,9 +79,31 @@ namespace Umbraco.Web.Editors
return Request.CreateResponse(HttpStatusCode.OK);
}
public IEnumerable<EntityBasic> GetAvailableCompositeMemberTypes(int contentTypeId)
/// <summary>
/// Returns the avilable compositions for this content type
/// </summary>
/// <param name="contentTypeId"></param>
/// <param name="filterContentTypes">
/// This is normally an empty list but if additional content type aliases are passed in, any content types containing those aliases will be filtered out
/// along with any content types that have matching property types that are included in the filtered content types
/// </param>
/// <param name="filterPropertyTypes">
/// This is normally an empty list but if additional property type aliases are passed in, any content types that have these aliases will be filtered out.
/// This is required because in the case of creating/modifying a content type because new property types being added to it are not yet persisted so cannot
/// be looked up via the db, they need to be passed in.
/// </param>
/// <returns></returns>
public HttpResponseMessage GetAvailableCompositeMemberTypes(int contentTypeId,
[FromUri]string[] filterContentTypes,
[FromUri]string[] filterPropertyTypes)
{
return PerformGetAvailableCompositeContentTypes(contentTypeId, UmbracoObjectTypes.MemberType);
var result = PerformGetAvailableCompositeContentTypes(contentTypeId, UmbracoObjectTypes.MemberType, filterContentTypes, filterPropertyTypes)
.Select(x => new
{
contentType = x.Item1,
allowed = x.Item2
});
return Request.CreateResponse(result);
}
public ContentTypeCompositionDisplay GetEmpty()