Merge branch 'v8/8.17' into v9/feature/merge_v8.17-rc

This commit is contained in:
Ronald Barendse
2021-09-07 12:10:58 +02:00
335 changed files with 9119 additions and 2275 deletions

View File

@@ -82,27 +82,35 @@ namespace Umbraco.Cms.Core.Models.ContentEditing
yield return validationResult;
}
var duplicateGroups = Groups.GroupBy(x => x.Name).Where(x => x.Count() > 1).ToArray();
if (duplicateGroups.Any())
foreach (var duplicateGroupAlias in Groups.GroupBy(x => x.Alias).Where(x => x.Count() > 1))
{
//we need to return the field name with an index so it's wired up correctly
var lastIndex = Groups.IndexOf(duplicateGroups.Last().Last());
yield return new ValidationResult("Duplicate group names not allowed", new[]
var lastGroupIndex = Groups.IndexOf(duplicateGroupAlias.Last());
yield return new ValidationResult("Duplicate aliases are not allowed: " + duplicateGroupAlias.Key, new[]
{
$"Groups[{lastIndex}].Name"
// TODO: We don't display the alias yet, so add the validation message to the name
$"Groups[{lastGroupIndex}].Name"
});
}
var duplicateProperties = Groups.SelectMany(x => x.Properties).Where(x => x.Inherited == false).GroupBy(x => x.Alias).Where(x => x.Count() > 1).ToArray();
if (duplicateProperties.Any())
foreach (var duplicateGroupName in Groups.GroupBy(x => (x.GetParentAlias(), x.Name)).Where(x => x.Count() > 1))
{
//we need to return the field name with an index so it's wired up correctly
var lastProperty = duplicateProperties.Last().Last();
var propertyGroup = Groups.Single(x => x.Properties.Contains(lastProperty));
yield return new ValidationResult("Duplicate property aliases not allowed: " + lastProperty.Alias, new[]
var lastGroupIndex = Groups.IndexOf(duplicateGroupName.Last());
yield return new ValidationResult("Duplicate names are not allowed", new[]
{
$"Groups[{propertyGroup.SortOrder}].Properties[{lastProperty.SortOrder}].Alias"
$"Groups[{lastGroupIndex}].Name"
});
}
foreach (var duplicatePropertyAlias in Groups.SelectMany(x => x.Properties).GroupBy(x => x.Alias).Where(x => x.Count() > 1))
{
var lastProperty = duplicatePropertyAlias.Last();
var propertyGroup = Groups.Single(x => x.Properties.Contains(lastProperty));
var lastPropertyIndex = propertyGroup.Properties.IndexOf(lastProperty);
var propertyGroupIndex = Groups.IndexOf(propertyGroup);
yield return new ValidationResult("Duplicate property aliases not allowed: " + duplicatePropertyAlias.Key, new[]
{
$"Groups[{propertyGroupIndex}].Properties[{lastPropertyIndex}].Alias"
});
}
}

View File

@@ -0,0 +1,26 @@
using System.ComponentModel.DataAnnotations;
using System.Runtime.Serialization;
namespace Umbraco.Cms.Core.Models.ContentEditing
{
/// <summary>
/// A model for retrieving multiple content types based on their aliases.
/// </summary>
[DataContract(Name = "contentTypes", Namespace = "")]
public class ContentTypesByAliases
{
/// <summary>
/// Id of the parent of the content type.
/// </summary>
[DataMember(Name = "parentId")]
[Required]
public int ParentId { get; set; }
/// <summary>
/// The alias of every content type to get.
/// </summary>
[DataMember(Name = "contentTypeAliases")]
[Required]
public string[] ContentTypeAliases { get; set; }
}
}

View File

@@ -1,4 +1,5 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Runtime.Serialization;
@@ -32,12 +33,22 @@ namespace Umbraco.Cms.Core.Models.ContentEditing
[DataMember(Name = "id")]
public int Id { get; set; }
[DataMember(Name = "sortOrder")]
public int SortOrder { get; set; }
[DataMember(Name = "key")]
public Guid Key { get; set; }
[DataMember(Name = "type")]
public PropertyGroupType Type { get; set; }
[Required]
[DataMember(Name = "name")]
public string Name { get; set; }
[Required]
[DataMember(Name = "alias")]
public string Alias { get; set; }
[DataMember(Name = "sortOrder")]
public int SortOrder { get; set; }
}
[DataContract(Name = "propertyGroup", Namespace = "")]
@@ -52,4 +63,10 @@ namespace Umbraco.Cms.Core.Models.ContentEditing
[DataMember(Name = "properties")]
public IEnumerable<TPropertyType> Properties { get; set; }
}
internal static class PropertyGroupBasicExtensions
{
public static string GetParentAlias(this PropertyGroupBasic propertyGroup)
=> PropertyGroupExtensions.GetParentAlias(propertyGroup.Alias);
}
}

View File

@@ -1,4 +1,5 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using System.Runtime.Serialization;
namespace Umbraco.Cms.Core.Models.ContentEditing
@@ -12,6 +13,12 @@ namespace Umbraco.Cms.Core.Models.ContentEditing
[DataMember(Name = "id")]
public int Id { get; set; }
[DataMember(Name = "key")]
public Guid Key { get; set; }
[DataMember(Name = "type")]
public int Type { get; set; }
[DataMember(Name = "active")]
public bool IsActive { get; set; }

View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Diagnostics;
@@ -302,21 +302,19 @@ namespace Umbraco.Cms.Core.Models
/// <returns>Returns <c>True</c> if a PropertyType with the passed in alias exists, otherwise <c>False</c></returns>
public abstract bool PropertyTypeExists(string propertyTypeAlias);
/// <summary>
/// Adds a PropertyGroup.
/// This method will also check if a group already exists with the same name and link it to the parent.
/// </summary>
/// <param name="groupName">Name of the PropertyGroup to add</param>
/// <returns>Returns <c>True</c> if a PropertyGroup with the passed in name was added, otherwise <c>False</c></returns>
public abstract bool AddPropertyGroup(string groupName);
/// <inheritdoc />
[Obsolete("Use AddPropertyGroup(name, alias) instead to explicitly set the alias.")]
public virtual bool AddPropertyGroup(string groupName) => AddPropertyGroup(groupName, groupName.ToSafeAlias(_shortStringHelper, true));
/// <summary>
/// Adds a PropertyType to a specific PropertyGroup
/// </summary>
/// <param name="propertyType"><see cref="IPropertyType"/> to add</param>
/// <param name="propertyGroupName">Name of the PropertyGroup to add the PropertyType to</param>
/// <returns>Returns <c>True</c> if PropertyType was added, otherwise <c>False</c></returns>
public abstract bool AddPropertyType(IPropertyType propertyType, string propertyGroupName);
/// <inheritdoc />
public abstract bool AddPropertyGroup(string name, string alias);
/// <inheritdoc />
[Obsolete("Use AddPropertyType(propertyType, groupAlias, groupName) instead to explicitly set the alias of the group (note the slighty different parameter order).")]
public virtual bool AddPropertyType(IPropertyType propertyType, string propertyGroupName) => AddPropertyType(propertyType, propertyGroupName.ToSafeAlias(_shortStringHelper, true), propertyGroupName);
/// <inheritdoc />
public abstract bool AddPropertyType(IPropertyType propertyType, string groupAlias, string groupName);
/// <summary>
/// Adds a PropertyType, which does not belong to a PropertyGroup.
@@ -344,18 +342,20 @@ namespace Umbraco.Cms.Core.Models
/// "generic properties" ie does not have a tab anymore.</remarks>
public bool MovePropertyType(string propertyTypeAlias, string propertyGroupName)
{
// note: not dealing with alias casing at all here?
// get property, ensure it exists
var propertyType = PropertyTypes.FirstOrDefault(x => x.Alias == propertyTypeAlias);
if (propertyType == null) return false;
// get new group, if required, and ensure it exists
var newPropertyGroup = propertyGroupName == null
? null
: PropertyGroups.FirstOrDefault(x => x.Name == propertyGroupName);
if (propertyGroupName != null && newPropertyGroup == null) return false;
PropertyGroup newPropertyGroup = null;
if (propertyGroupName != null)
{
var index = PropertyGroups.IndexOfKey(propertyGroupName);
if (index == -1) return false;
newPropertyGroup = PropertyGroups[index];
}
// get old group
var oldPropertyGroup = PropertyGroups.FirstOrDefault(x =>
x.PropertyTypes.Any(y => y.Alias == propertyTypeAlias));
@@ -408,11 +408,13 @@ namespace Umbraco.Cms.Core.Models
public void RemovePropertyGroup(string propertyGroupName)
{
// if no group exists with that name, do nothing
var group = PropertyGroups[propertyGroupName];
if (group == null) return;
var index = PropertyGroups.IndexOfKey(propertyGroupName);
if (index == -1) return;
var group = PropertyGroups[index];
// first remove the group
PropertyGroups.RemoveItem(propertyGroupName);
PropertyGroups.Remove(group);
// Then re-assign the group's properties to no group
foreach (var property in group.PropertyTypes)

View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.Serialization;
@@ -65,15 +65,14 @@ namespace Umbraco.Cms.Core.Models
propertyType.ResetDirtyProperties(false);
}
return ContentTypeComposition.SelectMany(x => x.CompositionPropertyGroups)
return PropertyGroups.Union(ContentTypeComposition.SelectMany(x => x.CompositionPropertyGroups)
.Select(group =>
{
group = (PropertyGroup) group.DeepClone();
foreach (var property in group.PropertyTypes)
AcquireProperty(property);
return group;
})
.Union(PropertyGroups);
}));
}
}
@@ -202,29 +201,29 @@ namespace Umbraco.Cms.Core.Models
return CompositionPropertyTypes.Any(x => x.Alias == propertyTypeAlias);
}
/// <summary>
/// Adds a PropertyGroup.
/// </summary>
/// <param name="groupName">Name of the PropertyGroup to add</param>
/// <returns>Returns <c>True</c> if a PropertyGroup with the passed in name was added, otherwise <c>False</c></returns>
public override bool AddPropertyGroup(string groupName)
/// <inheritdoc />
public override bool AddPropertyGroup(string name, string alias)
{
return AddAndReturnPropertyGroup(groupName) != null;
return AddAndReturnPropertyGroup(name, alias) != null;
}
private PropertyGroup AddAndReturnPropertyGroup(string name)
private PropertyGroup AddAndReturnPropertyGroup(string name, string alias)
{
// ensure we don't have it already
if (PropertyGroups.Any(x => x.Name == name))
// Ensure we don't have it already
if (PropertyGroups.Contains(alias))
return null;
// create the new group
var group = new PropertyGroup(SupportsPublishing) { Name = name, SortOrder = 0 };
// Add new group
var group = new PropertyGroup(SupportsPublishing)
{
Name = name,
Alias = alias
};
// check if it is inherited - there might be more than 1 but we want the 1st, to
// reuse its sort order - if there are more than 1 and they have different sort
// orders... there isn't much we can do anyways
var inheritGroup = CompositionPropertyGroups.FirstOrDefault(x => x.Name == name);
var inheritGroup = CompositionPropertyGroups.FirstOrDefault(x => x.Alias == alias);
if (inheritGroup == null)
{
// no, just local, set sort order
@@ -244,24 +243,30 @@ namespace Umbraco.Cms.Core.Models
return group;
}
/// <summary>
/// Adds a PropertyType to a specific PropertyGroup
/// </summary>
/// <param name="propertyType"><see cref="IPropertyType"/> to add</param>
/// <param name="propertyGroupName">Name of the PropertyGroup to add the PropertyType to</param>
/// <returns>Returns <c>True</c> if PropertyType was added, otherwise <c>False</c></returns>
public override bool AddPropertyType(IPropertyType propertyType, string propertyGroupName)
/// <inheritdoc />
public override bool AddPropertyType(IPropertyType propertyType, string groupAlias, string groupName)
{
// ensure no duplicate alias - over all composition properties
if (PropertyTypeExists(propertyType.Alias))
return false;
// get and ensure a group local to this content type
var group = PropertyGroups.Contains(propertyGroupName)
? PropertyGroups[propertyGroupName]
: AddAndReturnPropertyGroup(propertyGroupName);
if (group == null)
PropertyGroup group;
var index = PropertyGroups.IndexOfKey(groupAlias);
if (index != -1)
{
group = PropertyGroups[index];
}
else if (!string.IsNullOrEmpty(groupName))
{
group = AddAndReturnPropertyGroup(groupName, groupAlias);
if (group == null) return false;
}
else
{
// No group name specified, so we can't create a new one and add the property type
return false;
}
// add property to group
propertyType.PropertyGroupId = new Lazy<int>(() => group.Id);

View File

@@ -1,4 +1,5 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using Umbraco.Cms.Core.Models.Entities;
namespace Umbraco.Cms.Core.Models
@@ -116,10 +117,10 @@ namespace Umbraco.Cms.Core.Models
void RemovePropertyType(string propertyTypeAlias);
/// <summary>
/// Removes a PropertyGroup from the current ContentType
/// Removes a property group from the current content type.
/// </summary>
/// <param name="propertyGroupName">Name of the <see cref="PropertyGroup"/> to remove</param>
void RemovePropertyGroup(string propertyGroupName);
/// <param name="propertyGroupName">Name of the <see cref="PropertyGroup" /> to remove</param>
void RemovePropertyGroup(string propertyGroupName); // TODO Rename to propertyGroupAlias
/// <summary>
/// Checks whether a PropertyType with a given alias already exists
@@ -129,13 +130,27 @@ namespace Umbraco.Cms.Core.Models
bool PropertyTypeExists(string propertyTypeAlias);
/// <summary>
/// Adds a PropertyType to a specific PropertyGroup
/// Adds the property type to the specified property group (creates a new group if not found).
/// </summary>
/// <param name="propertyType"><see cref="IPropertyType"/> to add</param>
/// <param name="propertyGroupName">Name of the PropertyGroup to add the PropertyType to</param>
/// <returns>Returns <c>True</c> if PropertyType was added, otherwise <c>False</c></returns>
/// <param name="propertyType">The property type to add.</param>
/// <param name="propertyGroupName">The name of the property group to add the property type to.</param>
/// <returns>
/// Returns <c>true</c> if the property type was added; otherwise, <c>false</c>.
/// </returns>
[Obsolete("Use AddPropertyType(propertyType, groupAlias, groupName) instead to explicitly set the alias of the group (note the slighty different parameter order).")]
bool AddPropertyType(IPropertyType propertyType, string propertyGroupName);
/// <summary>
/// Adds the property type to the specified property group (creates a new group if not found and a name is specified).
/// </summary>
/// <param name="propertyType">The property type to add.</param>
/// <param name="groupAlias">The alias of the property group to add the property type to.</param>
/// <param name="groupName">The name of the property group to create when not found.</param>
/// <returns>
/// Returns <c>true</c> if the property type was added; otherwise, <c>false</c>.
/// </returns>
bool AddPropertyType(IPropertyType propertyType, string groupAlias, string groupName); // TODO Make groupName optional (add null as default value) after removing obsolete overload
/// <summary>
/// Adds a PropertyType, which does not belong to a PropertyGroup.
/// </summary>
@@ -144,20 +159,38 @@ namespace Umbraco.Cms.Core.Models
bool AddPropertyType(IPropertyType propertyType);
/// <summary>
/// Adds a PropertyGroup.
/// This method will also check if a group already exists with the same name and link it to the parent.
/// Adds a property group with the alias based on the specified <paramref name="groupName" />.
/// </summary>
/// <param name="groupName">Name of the PropertyGroup to add</param>
/// <returns>Returns <c>True</c> if a PropertyGroup with the passed in name was added, otherwise <c>False</c></returns>
/// <param name="groupName">Name of the group.</param>
/// <returns>
/// Returns <c>true</c> if a property group with specified <paramref name="groupName" /> was added; otherwise, <c>false</c>.
/// </returns>
/// <remarks>
/// This method will also check if a group already exists with the same alias.
/// </remarks>
[Obsolete("Use AddPropertyGroup(name, alias) instead to explicitly set the alias.")]
bool AddPropertyGroup(string groupName);
/// <summary>
/// Adds a property group with the specified <paramref name="name" /> and <paramref name="alias" />.
/// </summary>
/// <param name="name">Name of the group.</param>
/// <param name="alias">The alias.</param>
/// <returns>
/// Returns <c>true</c> if a property group with specified <paramref name="alias" /> was added; otherwise, <c>false</c>.
/// </returns>
/// <remarks>
/// This method will also check if a group already exists with the same alias.
/// </remarks>
bool AddPropertyGroup(string name, string alias);
/// <summary>
/// Moves a PropertyType to a specified PropertyGroup
/// </summary>
/// <param name="propertyTypeAlias">Alias of the PropertyType to move</param>
/// <param name="propertyGroupName">Name of the PropertyGroup to move the PropertyType to</param>
/// <returns></returns>
bool MovePropertyType(string propertyTypeAlias, string propertyGroupName);
bool MovePropertyType(string propertyTypeAlias, string propertyGroupName); // TODO Rename to propertyGroupAlias
/// <summary>
/// Gets an <see cref="ISimpleContentType"/> corresponding to this content type.

View File

@@ -1,4 +1,4 @@
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging;
using Umbraco.Cms.Core.Dictionary;
using Umbraco.Cms.Core.Mapping;
using Umbraco.Cms.Core.Models.ContentEditing;
@@ -36,8 +36,11 @@ namespace Umbraco.Cms.Core.Models.Mapping
private void Map(PropertyGroup source, Tab<ContentPropertyDisplay> target, MapperContext mapper)
{
target.Id = source.Id;
target.IsActive = true;
target.Key = source.Key;
target.Type = (int)source.Type;
target.Label = source.Name;
target.Alias = source.Alias;
target.IsActive = true;
}
private void Map(IProperty source, ContentPropertyBasic target, MapperContext context)

View File

@@ -1,4 +1,4 @@
using System;
using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Extensions.Logging;
@@ -319,7 +319,10 @@ namespace Umbraco.Cms.Core.Models.Mapping
{
if (source.Id > 0)
target.Id = source.Id;
target.Key = source.Key;
target.Type = source.Type;
target.Name = source.Name;
target.Alias = source.Alias;
target.SortOrder = source.SortOrder;
}
@@ -328,33 +331,38 @@ namespace Umbraco.Cms.Core.Models.Mapping
{
if (source.Id > 0)
target.Id = source.Id;
target.Key = source.Key;
target.Type = source.Type;
target.Name = source.Name;
target.Alias = source.Alias;
target.SortOrder = source.SortOrder;
}
// Umbraco.Code.MapAll -ContentTypeId -ParentTabContentTypes -ParentTabContentTypeNames
private static void Map(PropertyGroupBasic<PropertyTypeBasic> source, PropertyGroupDisplay<PropertyTypeDisplay> target, MapperContext context)
{
target.Inherited = source.Inherited;
if (source.Id > 0)
target.Id = source.Id;
target.Inherited = source.Inherited;
target.Key = source.Key;
target.Type = source.Type;
target.Name = source.Name;
target.Alias = source.Alias;
target.SortOrder = source.SortOrder;
target.Properties = context.MapEnumerable<PropertyTypeBasic, PropertyTypeDisplay>(source.Properties);
}
// Umbraco.Code.MapAll -ContentTypeId -ParentTabContentTypes -ParentTabContentTypeNames
private static void Map(PropertyGroupBasic<MemberPropertyTypeBasic> source, PropertyGroupDisplay<MemberPropertyTypeDisplay> target, MapperContext context)
{
target.Inherited = source.Inherited;
if (source.Id > 0)
target.Id = source.Id;
target.Inherited = source.Inherited;
target.Key = source.Key;
target.Type = source.Type;
target.Name = source.Name;
target.Alias = source.Alias;
target.SortOrder = source.SortOrder;
target.Properties = context.MapEnumerable<MemberPropertyTypeBasic, MemberPropertyTypeDisplay>(source.Properties);
}
@@ -452,6 +460,7 @@ namespace Umbraco.Cms.Core.Models.Mapping
var destOrigProperties = target.PropertyTypes.ToArray(); // all properties, in groups or not
var destGroups = new List<PropertyGroup>();
var sourceGroups = source.Groups.Where(x => x.IsGenericProperties == false).ToArray();
var sourceGroupParentAliases = sourceGroups.Select(x => x.GetParentAlias()).Distinct().ToArray();
foreach (var sourceGroup in sourceGroups)
{
// get the dest group
@@ -463,9 +472,9 @@ namespace Umbraco.Cms.Core.Models.Mapping
.Select(x => MapSaveProperty(x, destOrigProperties, context))
.ToArray();
// if the group has no local properties, skip it, ie sort-of garbage-collect
// if the group has no local properties and is not used as parent, skip it, ie sort-of garbage-collect
// local groups which would not have local properties anymore
if (destProperties.Length == 0)
if (destProperties.Length == 0 && !sourceGroupParentAliases.Contains(sourceGroup.Alias))
continue;
// ensure no duplicate alias, then assign the group properties collection
@@ -475,7 +484,7 @@ namespace Umbraco.Cms.Core.Models.Mapping
}
// ensure no duplicate name, then assign the groups collection
EnsureUniqueNames(destGroups);
EnsureUniqueAliases(destGroups);
target.PropertyGroups = new PropertyGroupCollection(destGroups);
// because the property groups collection was rebuilt, there is no need to remove
@@ -682,22 +691,22 @@ namespace Umbraco.Cms.Core.Models.Mapping
{
var propertiesA = properties.ToArray();
var distinctProperties = propertiesA
.Select(x => x.Alias.ToUpperInvariant())
.Select(x => x.Alias?.ToUpperInvariant())
.Distinct()
.Count();
if (distinctProperties != propertiesA.Length)
throw new InvalidOperationException("Cannot map properties due to alias conflict.");
}
private static void EnsureUniqueNames(IEnumerable<PropertyGroup> groups)
private static void EnsureUniqueAliases(IEnumerable<PropertyGroup> groups)
{
var groupsA = groups.ToArray();
var distinctProperties = groupsA
.Select(x => x.Name.ToUpperInvariant())
.Select(x => x.Alias)
.Distinct()
.Count();
if (distinctProperties != groupsA.Length)
throw new InvalidOperationException("Cannot map groups due to name conflict.");
throw new InvalidOperationException("Cannot map groups due to alias conflict.");
}
private static void MapComposition(ContentTypeSave source, IContentTypeComposition target, Func<string, IContentTypeComposition> getContentType)

View File

@@ -72,45 +72,50 @@ namespace Umbraco.Cms.Core.Models.Mapping
var groups = new List<PropertyGroupDisplay<TPropertyType>>();
// add groups local to this content type
foreach (var tab in source.PropertyGroups)
foreach (var propertyGroup in source.PropertyGroups)
{
var group = new PropertyGroupDisplay<TPropertyType>
{
Id = tab.Id,
Inherited = false,
Name = tab.Name,
SortOrder = tab.SortOrder,
Id = propertyGroup.Id,
Key = propertyGroup.Key,
Type = propertyGroup.Type,
Name = propertyGroup.Name,
Alias = propertyGroup.Alias,
SortOrder = propertyGroup.SortOrder,
Properties = MapProperties(propertyGroup.PropertyTypes, source, propertyGroup.Id, false),
ContentTypeId = source.Id
};
group.Properties = MapProperties(tab.PropertyTypes, source, tab.Id, false);
groups.Add(group);
}
// add groups inherited through composition
var localGroupIds = groups.Select(x => x.Id).ToArray();
foreach (var tab in source.CompositionPropertyGroups)
foreach (var propertyGroup in source.CompositionPropertyGroups)
{
// skip those that are local to this content type
if (localGroupIds.Contains(tab.Id)) continue;
if (localGroupIds.Contains(propertyGroup.Id)) continue;
// get the content type that defines this group
var definingContentType = GetContentTypeForPropertyGroup(source, tab.Id);
var definingContentType = GetContentTypeForPropertyGroup(source, propertyGroup.Id);
if (definingContentType == null)
throw new Exception("PropertyGroup with id=" + tab.Id + " was not found on any of the content type's compositions.");
throw new Exception("PropertyGroup with id=" + propertyGroup.Id + " was not found on any of the content type's compositions.");
var group = new PropertyGroupDisplay<TPropertyType>
{
Id = tab.Id,
Inherited = true,
Name = tab.Name,
SortOrder = tab.SortOrder,
Id = propertyGroup.Id,
Key = propertyGroup.Key,
Type = propertyGroup.Type,
Name = propertyGroup.Name,
Alias = propertyGroup.Alias,
SortOrder = propertyGroup.SortOrder,
Properties = MapProperties(propertyGroup.PropertyTypes, definingContentType, propertyGroup.Id, true),
ContentTypeId = definingContentType.Id,
ParentTabContentTypes = new[] { definingContentType.Id },
ParentTabContentTypeNames = new[] { definingContentType.Name }
};
group.Properties = MapProperties(tab.PropertyTypes, definingContentType, tab.Id, true);
groups.Add(group);
}
@@ -137,16 +142,16 @@ namespace Umbraco.Cms.Core.Models.Mapping
// if there are any generic properties, add the corresponding tab
if (genericProperties.Any())
{
var genericTab = new PropertyGroupDisplay<TPropertyType>
var genericGroup = new PropertyGroupDisplay<TPropertyType>
{
Id = PropertyGroupBasic.GenericPropertiesGroupId,
Name = "Generic properties",
ContentTypeId = source.Id,
SortOrder = 999,
Inherited = false,
Properties = genericProperties
Properties = genericProperties,
ContentTypeId = source.Id
};
groups.Add(genericTab);
groups.Add(genericGroup);
}
// handle locked properties
@@ -162,33 +167,33 @@ namespace Umbraco.Cms.Core.Models.Mapping
property.Locked = lockedPropertyAliases.Contains(property.Alias);
}
// now merge tabs based on names
// now merge tabs based on alias
// as for one name, we might have one local tab, plus some inherited tabs
var groupsGroupsByName = groups.GroupBy(x => x.Name).ToArray();
var groupsGroupsByAlias = groups.GroupBy(x => x.Alias).ToArray();
groups = new List<PropertyGroupDisplay<TPropertyType>>(); // start with a fresh list
foreach (var groupsByName in groupsGroupsByName)
foreach (var groupsByAlias in groupsGroupsByAlias)
{
// single group, just use it
if (groupsByName.Count() == 1)
if (groupsByAlias.Count() == 1)
{
groups.Add(groupsByName.First());
groups.Add(groupsByAlias.First());
continue;
}
// multiple groups, merge
var group = groupsByName.FirstOrDefault(x => x.Inherited == false) // try local
?? groupsByName.First(); // else pick one randomly
var group = groupsByAlias.FirstOrDefault(x => x.Inherited == false) // try local
?? groupsByAlias.First(); // else pick one randomly
groups.Add(group);
// in case we use the local one, flag as inherited
group.Inherited = true;
group.Inherited = true; // TODO Remove to allow changing sort order of the local one (and use the inherited group order below)
// merge (and sort) properties
var properties = groupsByName.SelectMany(x => x.Properties).OrderBy(x => x.SortOrder).ToArray();
var properties = groupsByAlias.SelectMany(x => x.Properties).OrderBy(x => x.SortOrder).ToArray();
group.Properties = properties;
// collect parent group info
var parentGroups = groupsByName.Where(x => x.ContentTypeId != source.Id).ToArray();
var parentGroups = groupsByAlias.Where(x => x.ContentTypeId != source.Id).ToArray();
group.ParentTabContentTypes = parentGroups.SelectMany(x => x.ParentTabContentTypes).ToArray();
group.ParentTabContentTypeNames = parentGroups.SelectMany(x => x.ParentTabContentTypeNames).ToArray();
}

View File

@@ -13,18 +13,17 @@ namespace Umbraco.Cms.Core.Models.Mapping
{
protected ICultureDictionary CultureDictionary { get; }
protected ILocalizedTextService LocalizedTextService { get; }
protected IEnumerable<string> IgnoreProperties { get; set; }
protected TabsAndPropertiesMapper(ICultureDictionary cultureDictionary, ILocalizedTextService localizedTextService)
: this(cultureDictionary, localizedTextService, new List<string>())
{ }
protected TabsAndPropertiesMapper(ICultureDictionary cultureDictionary, ILocalizedTextService localizedTextService, IEnumerable<string> ignoreProperties)
{
CultureDictionary = cultureDictionary ?? throw new ArgumentNullException(nameof(cultureDictionary));
LocalizedTextService = localizedTextService ?? throw new ArgumentNullException(nameof(localizedTextService));
IgnoreProperties = new List<string>();
}
protected TabsAndPropertiesMapper(ICultureDictionary cultureDictionary, ILocalizedTextService localizedTextService, IEnumerable<string> ignoreProperties)
: this(cultureDictionary, localizedTextService)
{
IgnoreProperties = ignoreProperties ?? throw new ArgumentNullException(nameof(ignoreProperties));
}
@@ -128,51 +127,48 @@ namespace Umbraco.Cms.Core.Models.Mapping
{
var tabs = new List<Tab<ContentPropertyDisplay>>();
// Property groups only exist on the content type (as it's only used for display purposes)
var contentType = _contentTypeBaseServiceProvider.GetContentTypeOf(source);
// add the tabs, for properties that belong to a tab
// need to aggregate the tabs, as content.PropertyGroups contains all the composition tabs,
// and there might be duplicates (content does not work like contentType and there is no
// content.CompositionPropertyGroups).
var groupsGroupsByName = contentType.CompositionPropertyGroups.OrderBy(x => x.SortOrder).GroupBy(x => x.Name);
foreach (var groupsByName in groupsGroupsByName)
// Merge the groups, as compositions can introduce duplicate aliases
var groups = contentType.CompositionPropertyGroups.OrderBy(x => x.SortOrder).ToArray();
var parentAliases = groups.Select(x => x.GetParentAlias()).Distinct().ToArray();
foreach (var groupsByAlias in groups.GroupBy(x => x.Alias))
{
var properties = new List<IProperty>();
// merge properties for groups with the same name
foreach (var group in groupsByName)
// Merge properties for groups with the same alias
foreach (var group in groupsByAlias)
{
var groupProperties = source.GetPropertiesForGroup(group)
.Where(x => IgnoreProperties.Contains(x.Alias) == false); // skip ignored
.Where(x => IgnoreProperties.Contains(x.Alias) == false); // Skip ignored properties
properties.AddRange(groupProperties);
}
if (properties.Count == 0)
if (properties.Count == 0 && !parentAliases.Contains(groupsByAlias.Key))
continue;
//map the properties
// Map the properties
var mappedProperties = MapProperties(source, properties, context);
// add the tab
// we need to pick an identifier... there is no "right" way...
var g = groupsByName.FirstOrDefault(x => x.Id == source.ContentTypeId) // try local
?? groupsByName.First(); // else pick one randomly
var groupId = g.Id;
var groupName = groupsByName.Key;
// Add the tab (the first is closest to the content type, e.g. local, then direct composition)
var g = groupsByAlias.First();
tabs.Add(new Tab<ContentPropertyDisplay>
{
Id = groupId,
Alias = groupName,
Label = LocalizedTextService.UmbracoDictionaryTranslate(CultureDictionary, groupName),
Properties = mappedProperties,
IsActive = false
Id = g.Id,
Key = g.Key,
Type = (int)g.Type,
Alias = g.Alias,
Label = LocalizedTextService.UmbracoDictionaryTranslate(CultureDictionary, g.Name),
Properties = mappedProperties
});
}
MapGenericProperties(source, tabs, context);
// activate the first tab, if any
// Activate the first tab, if any
if (tabs.Count > 0)
tabs[0].IsActive = true;

View File

@@ -1,21 +1,25 @@
using System;
using System;
using System.Collections.Specialized;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.Serialization;
using Umbraco.Cms.Core.Models.Entities;
using Umbraco.Extensions;
namespace Umbraco.Cms.Core.Models
{
/// <summary>
/// A group of property types, which corresponds to the properties grouped under a Tab.
/// Represents a group of property types.
/// </summary>
[Serializable]
[DataContract(IsReference = true)]
[DebuggerDisplay("Id: {Id}, Name: {Name}")]
[DebuggerDisplay("Id: {Id}, Name: {Name}, Alias: {Alias}")]
public class PropertyGroup : EntityBase, IEquatable<PropertyGroup>
{
[SuppressMessage("Style", "IDE1006:Naming Styles", Justification = "This field is for internal use only (to allow changing item keys).")]
internal PropertyGroupCollection Collection;
private PropertyGroupType _type;
private string _name;
private string _alias;
private int _sortOrder;
private PropertyTypeCollection _propertyTypes;
@@ -34,8 +38,24 @@ namespace Umbraco.Cms.Core.Models
}
/// <summary>
/// Gets or sets the Name of the Group, which corresponds to the Tab-name in the UI
/// Gets or sets the type of the group.
/// </summary>
/// <value>
/// The type.
/// </value>
[DataMember]
public PropertyGroupType Type
{
get => _type;
set => SetPropertyValueAndDetectChanges(value, ref _type, nameof(Type));
}
/// <summary>
/// Gets or sets the name of the group.
/// </summary>
/// <value>
/// The name.
/// </value>
[DataMember]
public string Name
{
@@ -44,8 +64,30 @@ namespace Umbraco.Cms.Core.Models
}
/// <summary>
/// Gets or sets the Sort Order of the Group
/// Gets or sets the alias of the group.
/// </summary>
/// <value>
/// The alias.
/// </value>
[DataMember]
public string Alias
{
get => _alias;
set
{
// If added to a collection, ensure the key is changed before setting it (this ensures the internal lookup dictionary is updated)
Collection?.ChangeKey(this, value);
SetPropertyValueAndDetectChanges(value, ref _alias, nameof(Alias));
}
}
/// <summary>
/// Gets or sets the sort order of the group.
/// </summary>
/// <value>
/// The sort order.
/// </value>
[DataMember]
public int SortOrder
{
@@ -54,10 +96,13 @@ namespace Umbraco.Cms.Core.Models
}
/// <summary>
/// Gets or sets a collection of PropertyTypes for this PropertyGroup
/// Gets or sets a collection of property types for the group.
/// </summary>
/// <value>
/// The property types.
/// </value>
/// <remarks>
/// Marked DoNotClone because we will manually deal with cloning and the event handlers
/// Marked with DoNotClone, because we will manually deal with cloning and the event handlers.
/// </remarks>
[DataMember]
[DoNotClone]
@@ -83,30 +128,103 @@ namespace Umbraco.Cms.Core.Models
}
}
public bool Equals(PropertyGroup other)
{
if (base.Equals(other)) return true;
return other != null && Name.InvariantEquals(other.Name);
}
public bool Equals(PropertyGroup other) => base.Equals(other) || (other != null && Type == other.Type && Alias == other.Alias);
public override int GetHashCode()
{
var baseHash = base.GetHashCode();
var nameHash = Name.ToLowerInvariant().GetHashCode();
return baseHash ^ nameHash;
}
public override int GetHashCode() => (base.GetHashCode(), Type, Alias).GetHashCode();
protected override void PerformDeepClone(object clone)
{
base.PerformDeepClone(clone);
var clonedEntity = (PropertyGroup)clone;
clonedEntity.Collection = null;
if (clonedEntity._propertyTypes != null)
{
clonedEntity._propertyTypes.ClearCollectionChangedEvents(); //clear this event handler if any
clonedEntity._propertyTypes.ClearCollectionChangedEvents(); //clear this event handler if any
clonedEntity._propertyTypes = (PropertyTypeCollection) _propertyTypes.DeepClone(); //manually deep clone
clonedEntity._propertyTypes.CollectionChanged += clonedEntity.PropertyTypesChanged; //re-assign correct event handler
clonedEntity._propertyTypes.CollectionChanged += clonedEntity.PropertyTypesChanged; //re-assign correct event handler
}
}
}
public static class PropertyGroupExtensions
{
private const char aliasSeparator = '/';
internal static string GetLocalAlias(string alias)
{
var lastIndex = alias?.LastIndexOf(aliasSeparator) ?? -1;
if (lastIndex != -1)
{
return alias.Substring(lastIndex + 1);
}
return alias;
}
internal static string GetParentAlias(string alias)
{
var lastIndex = alias?.LastIndexOf(aliasSeparator) ?? -1;
if (lastIndex == -1)
{
return null;
}
return alias.Substring(0, lastIndex);
}
/// <summary>
/// Gets the local alias.
/// </summary>
/// <param name="propertyGroup">The property group.</param>
/// <returns>
/// The local alias.
/// </returns>
public static string GetLocalAlias(this PropertyGroup propertyGroup) => GetLocalAlias(propertyGroup.Alias);
/// <summary>
/// Updates the local alias.
/// </summary>
/// <param name="propertyGroup">The property group.</param>
/// <param name="localAlias">The local alias.</param>
public static void UpdateLocalAlias(this PropertyGroup propertyGroup, string localAlias)
{
var parentAlias = propertyGroup.GetParentAlias();
if (string.IsNullOrEmpty(parentAlias))
{
propertyGroup.Alias = localAlias;
}
else
{
propertyGroup.Alias = parentAlias + aliasSeparator + localAlias;
}
}
/// <summary>
/// Gets the parent alias.
/// </summary>
/// <param name="propertyGroup">The property group.</param>
/// <returns>
/// The parent alias.
/// </returns>
public static string GetParentAlias(this PropertyGroup propertyGroup) => GetParentAlias(propertyGroup.Alias);
/// <summary>
/// Updates the parent alias.
/// </summary>
/// <param name="propertyGroup">The property group.</param>
/// <param name="parentAlias">The parent alias.</param>
public static void UpdateParentAlias(this PropertyGroup propertyGroup, string parentAlias)
{
var localAlias = propertyGroup.GetLocalAlias();
if (string.IsNullOrEmpty(parentAlias))
{
propertyGroup.Alias = localAlias;
}
else
{
propertyGroup.Alias = parentAlias + aliasSeparator + localAlias;
}
}
}

View File

@@ -1,13 +1,13 @@
using System;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.Linq;
using System.Runtime.Serialization;
using Umbraco.Extensions;
namespace Umbraco.Cms.Core.Models
{
/// <summary>
/// Represents a collection of <see cref="PropertyGroup"/> objects
/// </summary>
@@ -16,9 +16,16 @@ namespace Umbraco.Cms.Core.Models
// TODO: Change this to ObservableDictionary so we can reduce the INotifyCollectionChanged implementation details
public class PropertyGroupCollection : KeyedCollection<string, PropertyGroup>, INotifyCollectionChanged, IDeepCloneable
{
/// <summary>
/// Initializes a new instance of the <see cref="PropertyGroupCollection" /> class.
/// </summary>
public PropertyGroupCollection()
{ }
/// <summary>
/// Initializes a new instance of the <see cref="PropertyGroupCollection" /> class.
/// </summary>
/// <param name="groups">The groups.</param>
public PropertyGroupCollection(IEnumerable<PropertyGroup> groups)
{
Reset(groups);
@@ -31,10 +38,10 @@ namespace Umbraco.Cms.Core.Models
/// <remarks></remarks>
internal void Reset(IEnumerable<PropertyGroup> groups)
{
//collection events will be raised in each of these calls
// Collection events will be raised in each of these calls
Clear();
//collection events will be raised in each of these calls
// Collection events will be raised in each of these calls
foreach (var group in groups)
Add(group);
}
@@ -42,73 +49,87 @@ namespace Umbraco.Cms.Core.Models
protected override void SetItem(int index, PropertyGroup item)
{
var oldItem = index >= 0 ? this[index] : item;
base.SetItem(index, item);
oldItem.Collection = null;
item.Collection = this;
OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Replace, item, oldItem));
}
protected override void RemoveItem(int index)
{
var removed = this[index];
base.RemoveItem(index);
removed.Collection = null;
OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Remove, removed));
}
protected override void InsertItem(int index, PropertyGroup item)
{
base.InsertItem(index, item);
item.Collection = this;
OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Add, item));
}
protected override void ClearItems()
{
foreach (var item in this)
{
item.Collection = null;
}
base.ClearItems();
OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
}
public new void Add(PropertyGroup item)
{
//Note this is done to ensure existing groups can be renamed
// Ensure alias is set
if (string.IsNullOrEmpty(item.Alias))
{
throw new InvalidOperationException("Set an alias before adding the property group.");
}
// Note this is done to ensure existing groups can be renamed
if (item.HasIdentity && item.Id > 0)
{
var exists = Contains(item.Id);
if (exists)
var index = IndexOfKey(item.Id);
if (index != -1)
{
var keyExists = Contains(item.Name);
var keyExists = Contains(item.Alias);
if (keyExists)
throw new Exception($"Naming conflict: Changing the name of PropertyGroup '{item.Name}' would result in duplicates");
throw new ArgumentException($"Naming conflict: changing the alias of property group '{item.Name}' would result in duplicates.");
//collection events will be raised in SetItem
SetItem(IndexOfKey(item.Id), item);
// Collection events will be raised in SetItem
SetItem(index, item);
return;
}
}
else
{
var key = GetKeyForItem(item);
if (key != null)
var index = IndexOfKey(item.Alias);
if (index != -1)
{
var exists = Contains(key);
if (exists)
{
//collection events will be raised in SetItem
SetItem(IndexOfKey(key), item);
return;
}
// Collection events will be raised in SetItem
SetItem(index, item);
return;
}
}
//collection events will be raised in InsertItem
// Collection events will be raised in InsertItem
base.Add(item);
}
/// <summary>
/// Determines whether this collection contains a <see cref="PropertyGroup"/> whose name matches the specified parameter.
/// </summary>
/// <param name="groupName">Name of the PropertyGroup.</param>
/// <returns><c>true</c> if the collection contains the specified name; otherwise, <c>false</c>.</returns>
/// <remarks></remarks>
public new bool Contains(string groupName)
internal void ChangeKey(PropertyGroup item, string newKey)
{
return this.Any(x => x.Name == groupName);
ChangeItemKey(item, newKey);
}
public bool Contains(int id)
@@ -116,34 +137,12 @@ namespace Umbraco.Cms.Core.Models
return this.Any(x => x.Id == id);
}
public void RemoveItem(string propertyGroupName)
{
var key = IndexOfKey(propertyGroupName);
//Only removes an item if the key was found
if (key != -1)
RemoveItem(key);
}
public int IndexOfKey(string key)
{
for (var i = 0; i < Count; i++)
if (this[i].Name == key)
return i;
return -1;
}
public int IndexOfKey(string key) => this.FindIndex(x => x.Alias == key);
public int IndexOfKey(int id)
{
for (var i = 0; i < Count; i++)
if (this[i].Id == id)
return i;
return -1;
}
public int IndexOfKey(int id) => this.FindIndex(x => x.Id == id);
protected override string GetKeyForItem(PropertyGroup item)
{
return item.Name;
}
protected override string GetKeyForItem(PropertyGroup item) => item.Alias;
public event NotifyCollectionChangedEventHandler CollectionChanged;
@@ -164,6 +163,7 @@ namespace Umbraco.Cms.Core.Models
{
clone.Add((PropertyGroup)group.DeepClone());
}
return clone;
}
}

View File

@@ -0,0 +1,17 @@
namespace Umbraco.Cms.Core.Models
{
/// <summary>
/// Represents the type of a property group.
/// </summary>
public enum PropertyGroupType : short
{
/// <summary>
/// Display property types in a group.
/// </summary>
Group = 0,
/// <summary>
/// Display property types in a tab.
/// </summary>
Tab = 1
}
}

View File

@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using Umbraco.Extensions;
@@ -10,6 +11,7 @@ namespace Umbraco.Cms.Core.Models.PublishedContent
/// </summary>
/// <remarks>Instances of the <see cref="PublishedContentType"/> class are immutable, ie
/// if the content type changes, then a new class needs to be created.</remarks>
[DebuggerDisplay("{Alias}")]
public class PublishedContentType : IPublishedContentType
{
private readonly IPublishedPropertyType[] _propertyTypes;

View File

@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
namespace Umbraco.Cms.Core.Models.PublishedContent
{
@@ -18,6 +19,7 @@ namespace Umbraco.Cms.Core.Models.PublishedContent
/// Provides an abstract base class for <c>IPublishedContent</c> implementations that
/// wrap and extend another <c>IPublishedContent</c>.
/// </summary>
[DebuggerDisplay("{Id}: {Name} ({ContentType?.Alias})")]
public abstract class PublishedContentWrapped : IPublishedContent
{
private readonly IPublishedContent _content;

View File

@@ -1,10 +1,12 @@
using System;
using System.Diagnostics;
namespace Umbraco.Cms.Core.Models.PublishedContent
{
/// <summary>
/// Contains culture specific values for <see cref="IPublishedContent"/>.
/// </summary>
[DebuggerDisplay("{Culture}")]
public class PublishedCultureInfo
{
/// <summary>

View File

@@ -1,4 +1,5 @@
using System;
using System.Diagnostics;
namespace Umbraco.Cms.Core.Models.PublishedContent
{
@@ -10,6 +11,7 @@ namespace Umbraco.Cms.Core.Models.PublishedContent
/// if the data type changes, then a new class needs to be created.</para>
/// <para>These instances should be created by an <see cref="IPublishedContentTypeFactory"/>.</para>
/// </remarks>
[DebuggerDisplay("{EditorAlias}")]
public class PublishedDataType
{
private readonly Lazy<object> _lazyConfiguration;

View File

@@ -1,4 +1,5 @@
using System;
using System.Diagnostics;
using Umbraco.Cms.Core.PropertyEditors;
namespace Umbraco.Cms.Core.Models.PublishedContent
@@ -7,6 +8,7 @@ namespace Umbraco.Cms.Core.Models.PublishedContent
/// Provides a base class for <c>IPublishedProperty</c> implementations which converts and caches
/// the value source to the actual value to use when rendering content.
/// </summary>
[DebuggerDisplay("{Alias} ({PropertyType?.EditorAlias})")]
public abstract class PublishedPropertyBase : IPublishedProperty
{
/// <summary>

View File

@@ -1,4 +1,5 @@
using System;
using System.Diagnostics;
using System.Xml.Linq;
using System.Xml.XPath;
using Umbraco.Cms.Core.PropertyEditors;
@@ -10,6 +11,7 @@ namespace Umbraco.Cms.Core.Models.PublishedContent
/// </summary>
/// <remarks>Instances of the <see cref="PublishedPropertyType"/> class are immutable, ie
/// if the property type changes, then a new class needs to be created.</remarks>
[DebuggerDisplay("{Alias} ({EditorAlias})")]
public class PublishedPropertyType : IPublishedPropertyType
{
private readonly IPublishedModelFactory _publishedModelFactory;

View File

@@ -1,5 +1,8 @@
namespace Umbraco.Cms.Core.Models.PublishedContent
using System.Diagnostics;
namespace Umbraco.Cms.Core.Models.PublishedContent
{
[DebuggerDisplay("{Content?.Name} ({Score})")]
public class PublishedSearchResult
{
public PublishedSearchResult(IPublishedContent content, float score)