Merge pull request #1009 from umbraco/temp-U4-7588
U4-7588 Conflicting compositions should not be allowed
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
|
||||
};
|
||||
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
|
||||
|
||||
@@ -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);
|
||||
},
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user