diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml
index 4dfa5a83b9..3d08bf3a4b 100644
--- a/.github/workflows/codeql-analysis.yml
+++ b/.github/workflows/codeql-analysis.yml
@@ -56,6 +56,13 @@ jobs:
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v1
with:
- config-file: ./.github/codeql-config.yml
-
+ config-file: ./.github/workflows/codeql-config.yml
+
+ # This job is to prevent the workflow status from showing as failed when all other jobs are skipped - See https://github.community/t/workflow-is-failing-if-no-job-can-be-ran-due-to-condition/16873
+ always_job:
+ name: Always run job
+ runs-on: ubuntu-latest
+ steps:
+ - name: Always run
+ run: echo "This job is to prevent the workflow status from showing as failed when all other jobs are skipped"
diff --git a/src/Umbraco.Core/Constants-Conventions.cs b/src/Umbraco.Core/Constants-Conventions.cs
index 32bea251d5..decb7559f7 100644
--- a/src/Umbraco.Core/Constants-Conventions.cs
+++ b/src/Umbraco.Core/Constants-Conventions.cs
@@ -227,7 +227,12 @@ namespace Umbraco.Cms.Core
public const string FailedPasswordAttemptsLabel = "Failed Password Attempts";
///
- /// Group name to put the membership properties on
+ /// Group alias to put the membership properties on.
+ ///
+ public const string StandardPropertiesGroupAlias = "membership";
+
+ ///
+ /// Group name to put the membership properties on.
///
public const string StandardPropertiesGroupName = "Membership";
}
diff --git a/src/Umbraco.Core/Exceptions/InvalidCompositionException.cs b/src/Umbraco.Core/Exceptions/InvalidCompositionException.cs
index 90e1d03490..4fb335be84 100644
--- a/src/Umbraco.Core/Exceptions/InvalidCompositionException.cs
+++ b/src/Umbraco.Core/Exceptions/InvalidCompositionException.cs
@@ -35,6 +35,14 @@ namespace Umbraco.Cms.Core.Exceptions
///
public string[] PropertyTypeAliases { get; }
+ ///
+ /// Gets the property group aliases.
+ ///
+ ///
+ /// The property group aliases.
+ ///
+ public string[] PropertyGroupAliases { get; }
+
///
/// Initializes a new instance of the class.
///
@@ -57,7 +65,29 @@ namespace Umbraco.Cms.Core.Exceptions
/// The added composition alias.
/// The property type aliases.
public InvalidCompositionException(string contentTypeAlias, string addedCompositionAlias, string[] propertyTypeAliases)
- : this(addedCompositionAlias.IsNullOrWhiteSpace()
+ : this(contentTypeAlias, addedCompositionAlias, propertyTypeAliases, new string[0])
+ { }
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The content type alias.
+ /// The added composition alias.
+ /// The property type aliases.
+ /// The property group aliases.
+ public InvalidCompositionException(string contentTypeAlias, string addedCompositionAlias, string[] propertyTypeAliases, string[] propertyGroupAliases)
+ : this(FormatMessage(contentTypeAlias, addedCompositionAlias, propertyTypeAliases, propertyGroupAliases))
+ {
+ ContentTypeAlias = contentTypeAlias;
+ AddedCompositionAlias = addedCompositionAlias;
+ PropertyTypeAliases = propertyTypeAliases;
+ PropertyGroupAliases = propertyGroupAliases;
+ }
+
+ private static string FormatMessage(string contentTypeAlias, string addedCompositionAlias, string[] propertyTypeAliases, string[] propertyGroupAliases)
+ {
+ // TODO Add property group aliases to message
+ return addedCompositionAlias.IsNullOrWhiteSpace()
? string.Format(
"ContentType with alias '{0}' has an invalid composition " +
"and there was a conflict on the following PropertyTypes: '{1}'. " +
@@ -67,11 +97,7 @@ namespace Umbraco.Cms.Core.Exceptions
"ContentType with alias '{0}' was added as a Composition to ContentType with alias '{1}', " +
"but there was a conflict on the following PropertyTypes: '{2}'. " +
"PropertyTypes must have a unique alias across all Compositions in order to compose a valid ContentType Composition.",
- addedCompositionAlias, contentTypeAlias, string.Join(", ", propertyTypeAliases)))
- {
- ContentTypeAlias = contentTypeAlias;
- AddedCompositionAlias = addedCompositionAlias;
- PropertyTypeAliases = propertyTypeAliases;
+ addedCompositionAlias, contentTypeAlias, string.Join(", ", propertyTypeAliases));
}
///
@@ -102,6 +128,7 @@ namespace Umbraco.Cms.Core.Exceptions
ContentTypeAlias = info.GetString(nameof(ContentTypeAlias));
AddedCompositionAlias = info.GetString(nameof(AddedCompositionAlias));
PropertyTypeAliases = (string[])info.GetValue(nameof(PropertyTypeAliases), typeof(string[]));
+ PropertyGroupAliases = (string[])info.GetValue(nameof(PropertyGroupAliases), typeof(string[]));
}
///
@@ -120,6 +147,7 @@ namespace Umbraco.Cms.Core.Exceptions
info.AddValue(nameof(ContentTypeAlias), ContentTypeAlias);
info.AddValue(nameof(AddedCompositionAlias), AddedCompositionAlias);
info.AddValue(nameof(PropertyTypeAliases), PropertyTypeAliases);
+ info.AddValue(nameof(PropertyGroupAliases), PropertyGroupAliases);
base.GetObjectData(info, context);
}
diff --git a/src/Umbraco.Core/Models/ContentEditing/ContentTypeSave.cs b/src/Umbraco.Core/Models/ContentEditing/ContentTypeSave.cs
index a45c4ac4f6..79f3fba1fd 100644
--- a/src/Umbraco.Core/Models/ContentEditing/ContentTypeSave.cs
+++ b/src/Umbraco.Core/Models/ContentEditing/ContentTypeSave.cs
@@ -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"
});
}
}
diff --git a/src/Umbraco.Core/Models/ContentEditing/ContentTypesByAliases.cs b/src/Umbraco.Core/Models/ContentEditing/ContentTypesByAliases.cs
new file mode 100644
index 0000000000..dc7b1cdc8c
--- /dev/null
+++ b/src/Umbraco.Core/Models/ContentEditing/ContentTypesByAliases.cs
@@ -0,0 +1,26 @@
+using System.ComponentModel.DataAnnotations;
+using System.Runtime.Serialization;
+
+namespace Umbraco.Cms.Core.Models.ContentEditing
+{
+ ///
+ /// A model for retrieving multiple content types based on their aliases.
+ ///
+ [DataContract(Name = "contentTypes", Namespace = "")]
+ public class ContentTypesByAliases
+ {
+ ///
+ /// Id of the parent of the content type.
+ ///
+ [DataMember(Name = "parentId")]
+ [Required]
+ public int ParentId { get; set; }
+
+ ///
+ /// The alias of every content type to get.
+ ///
+ [DataMember(Name = "contentTypeAliases")]
+ [Required]
+ public string[] ContentTypeAliases { get; set; }
+ }
+}
diff --git a/src/Umbraco.Core/Models/ContentEditing/PropertyGroupBasic.cs b/src/Umbraco.Core/Models/ContentEditing/PropertyGroupBasic.cs
index 0bea10a476..d2089f38d4 100644
--- a/src/Umbraco.Core/Models/ContentEditing/PropertyGroupBasic.cs
+++ b/src/Umbraco.Core/Models/ContentEditing/PropertyGroupBasic.cs
@@ -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 Properties { get; set; }
}
+
+ internal static class PropertyGroupBasicExtensions
+ {
+ public static string GetParentAlias(this PropertyGroupBasic propertyGroup)
+ => PropertyGroupExtensions.GetParentAlias(propertyGroup.Alias);
+ }
}
diff --git a/src/Umbraco.Core/Models/ContentEditing/Tab.cs b/src/Umbraco.Core/Models/ContentEditing/Tab.cs
index 5971162575..f8e80a1bdb 100644
--- a/src/Umbraco.Core/Models/ContentEditing/Tab.cs
+++ b/src/Umbraco.Core/Models/ContentEditing/Tab.cs
@@ -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; }
diff --git a/src/Umbraco.Core/Models/ContentTypeBase.cs b/src/Umbraco.Core/Models/ContentTypeBase.cs
index 2bda8d5751..4d0bcb0e29 100644
--- a/src/Umbraco.Core/Models/ContentTypeBase.cs
+++ b/src/Umbraco.Core/Models/ContentTypeBase.cs
@@ -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 True if a PropertyType with the passed in alias exists, otherwise False
public abstract bool PropertyTypeExists(string propertyTypeAlias);
- ///
- /// Adds a PropertyGroup.
- /// This method will also check if a group already exists with the same name and link it to the parent.
- ///
- /// Name of the PropertyGroup to add
- /// Returns True if a PropertyGroup with the passed in name was added, otherwise False
- public abstract bool AddPropertyGroup(string groupName);
+ ///
+ [Obsolete("Use AddPropertyGroup(name, alias) instead to explicitly set the alias.")]
+ public virtual bool AddPropertyGroup(string groupName) => AddPropertyGroup(groupName, groupName.ToSafeAlias(_shortStringHelper, true));
- ///
- /// Adds a PropertyType to a specific PropertyGroup
- ///
- /// to add
- /// Name of the PropertyGroup to add the PropertyType to
- /// Returns True if PropertyType was added, otherwise False
- public abstract bool AddPropertyType(IPropertyType propertyType, string propertyGroupName);
+ ///
+ public abstract bool AddPropertyGroup(string name, string alias);
+
+ ///
+ [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);
+
+ ///
+ public abstract bool AddPropertyType(IPropertyType propertyType, string groupAlias, string groupName);
///
/// 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.
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)
diff --git a/src/Umbraco.Core/Models/ContentTypeCompositionBase.cs b/src/Umbraco.Core/Models/ContentTypeCompositionBase.cs
index bfe6bae659..31093077bc 100644
--- a/src/Umbraco.Core/Models/ContentTypeCompositionBase.cs
+++ b/src/Umbraco.Core/Models/ContentTypeCompositionBase.cs
@@ -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);
}
- ///
- /// Adds a PropertyGroup.
- ///
- /// Name of the PropertyGroup to add
- /// Returns True if a PropertyGroup with the passed in name was added, otherwise False
- public override bool AddPropertyGroup(string groupName)
+ ///
+ 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;
}
- ///
- /// Adds a PropertyType to a specific PropertyGroup
- ///
- /// to add
- /// Name of the PropertyGroup to add the PropertyType to
- /// Returns True if PropertyType was added, otherwise False
- public override bool AddPropertyType(IPropertyType propertyType, string propertyGroupName)
+ ///
+ 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(() => group.Id);
diff --git a/src/Umbraco.Core/Models/IContentTypeBase.cs b/src/Umbraco.Core/Models/IContentTypeBase.cs
index d0dc798eca..6b55464a2c 100644
--- a/src/Umbraco.Core/Models/IContentTypeBase.cs
+++ b/src/Umbraco.Core/Models/IContentTypeBase.cs
@@ -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);
///
- /// Removes a PropertyGroup from the current ContentType
+ /// Removes a property group from the current content type.
///
- /// Name of the to remove
- void RemovePropertyGroup(string propertyGroupName);
+ /// Name of the to remove
+ void RemovePropertyGroup(string propertyGroupName); // TODO Rename to propertyGroupAlias
///
/// Checks whether a PropertyType with a given alias already exists
@@ -129,13 +130,27 @@ namespace Umbraco.Cms.Core.Models
bool PropertyTypeExists(string propertyTypeAlias);
///
- /// Adds a PropertyType to a specific PropertyGroup
+ /// Adds the property type to the specified property group (creates a new group if not found).
///
- /// to add
- /// Name of the PropertyGroup to add the PropertyType to
- /// Returns True if PropertyType was added, otherwise False
+ /// The property type to add.
+ /// The name of the property group to add the property type to.
+ ///
+ /// Returns true if the property type was added; otherwise, false.
+ ///
+ [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);
+ ///
+ /// Adds the property type to the specified property group (creates a new group if not found and a name is specified).
+ ///
+ /// The property type to add.
+ /// The alias of the property group to add the property type to.
+ /// The name of the property group to create when not found.
+ ///
+ /// Returns true if the property type was added; otherwise, false.
+ ///
+ bool AddPropertyType(IPropertyType propertyType, string groupAlias, string groupName); // TODO Make groupName optional (add null as default value) after removing obsolete overload
+
///
/// Adds a PropertyType, which does not belong to a PropertyGroup.
///
@@ -144,20 +159,38 @@ namespace Umbraco.Cms.Core.Models
bool AddPropertyType(IPropertyType propertyType);
///
- /// 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 .
///
- /// Name of the PropertyGroup to add
- /// Returns True if a PropertyGroup with the passed in name was added, otherwise False
+ /// Name of the group.
+ ///
+ /// Returns true if a property group with specified was added; otherwise, false.
+ ///
+ ///
+ /// This method will also check if a group already exists with the same alias.
+ ///
+ [Obsolete("Use AddPropertyGroup(name, alias) instead to explicitly set the alias.")]
bool AddPropertyGroup(string groupName);
+ ///
+ /// Adds a property group with the specified and .
+ ///
+ /// Name of the group.
+ /// The alias.
+ ///
+ /// Returns true if a property group with specified was added; otherwise, false.
+ ///
+ ///
+ /// This method will also check if a group already exists with the same alias.
+ ///
+ bool AddPropertyGroup(string name, string alias);
+
///
/// Moves a PropertyType to a specified PropertyGroup
///
/// Alias of the PropertyType to move
/// Name of the PropertyGroup to move the PropertyType to
///
- bool MovePropertyType(string propertyTypeAlias, string propertyGroupName);
+ bool MovePropertyType(string propertyTypeAlias, string propertyGroupName); // TODO Rename to propertyGroupAlias
///
/// Gets an corresponding to this content type.
diff --git a/src/Umbraco.Core/Models/Mapping/ContentPropertyMapDefinition.cs b/src/Umbraco.Core/Models/Mapping/ContentPropertyMapDefinition.cs
index 797232fc60..11aad3ea20 100644
--- a/src/Umbraco.Core/Models/Mapping/ContentPropertyMapDefinition.cs
+++ b/src/Umbraco.Core/Models/Mapping/ContentPropertyMapDefinition.cs
@@ -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 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)
diff --git a/src/Umbraco.Core/Models/Mapping/ContentTypeMapDefinition.cs b/src/Umbraco.Core/Models/Mapping/ContentTypeMapDefinition.cs
index 850bc59e76..7449a6f778 100644
--- a/src/Umbraco.Core/Models/Mapping/ContentTypeMapDefinition.cs
+++ b/src/Umbraco.Core/Models/Mapping/ContentTypeMapDefinition.cs
@@ -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 source, PropertyGroupDisplay 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(source.Properties);
}
// Umbraco.Code.MapAll -ContentTypeId -ParentTabContentTypes -ParentTabContentTypeNames
private static void Map(PropertyGroupBasic source, PropertyGroupDisplay 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(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();
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 groups)
+ private static void EnsureUniqueAliases(IEnumerable 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 getContentType)
diff --git a/src/Umbraco.Core/Models/Mapping/PropertyTypeGroupMapper.cs b/src/Umbraco.Core/Models/Mapping/PropertyTypeGroupMapper.cs
index 4545414e51..f3a7e3513a 100644
--- a/src/Umbraco.Core/Models/Mapping/PropertyTypeGroupMapper.cs
+++ b/src/Umbraco.Core/Models/Mapping/PropertyTypeGroupMapper.cs
@@ -72,45 +72,50 @@ namespace Umbraco.Cms.Core.Models.Mapping
var groups = new List>();
// add groups local to this content type
- foreach (var tab in source.PropertyGroups)
+ foreach (var propertyGroup in source.PropertyGroups)
{
var group = new PropertyGroupDisplay
{
- 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
{
- 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
+ var genericGroup = new PropertyGroupDisplay
{
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>(); // 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();
}
diff --git a/src/Umbraco.Core/Models/Mapping/TabsAndPropertiesMapper.cs b/src/Umbraco.Core/Models/Mapping/TabsAndPropertiesMapper.cs
index 583f921e3f..537e99e9e8 100644
--- a/src/Umbraco.Core/Models/Mapping/TabsAndPropertiesMapper.cs
+++ b/src/Umbraco.Core/Models/Mapping/TabsAndPropertiesMapper.cs
@@ -13,18 +13,17 @@ namespace Umbraco.Cms.Core.Models.Mapping
{
protected ICultureDictionary CultureDictionary { get; }
protected ILocalizedTextService LocalizedTextService { get; }
+
protected IEnumerable IgnoreProperties { get; set; }
protected TabsAndPropertiesMapper(ICultureDictionary cultureDictionary, ILocalizedTextService localizedTextService)
+ : this(cultureDictionary, localizedTextService, new List())
+ { }
+
+ protected TabsAndPropertiesMapper(ICultureDictionary cultureDictionary, ILocalizedTextService localizedTextService, IEnumerable ignoreProperties)
{
CultureDictionary = cultureDictionary ?? throw new ArgumentNullException(nameof(cultureDictionary));
LocalizedTextService = localizedTextService ?? throw new ArgumentNullException(nameof(localizedTextService));
- IgnoreProperties = new List();
- }
-
- protected TabsAndPropertiesMapper(ICultureDictionary cultureDictionary, ILocalizedTextService localizedTextService, IEnumerable 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>();
+ // 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();
- // 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
{
- 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;
diff --git a/src/Umbraco.Core/Models/PropertyGroup.cs b/src/Umbraco.Core/Models/PropertyGroup.cs
index e086839304..b63ea59de2 100644
--- a/src/Umbraco.Core/Models/PropertyGroup.cs
+++ b/src/Umbraco.Core/Models/PropertyGroup.cs
@@ -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
{
///
- /// A group of property types, which corresponds to the properties grouped under a Tab.
+ /// Represents a group of property types.
///
[Serializable]
[DataContract(IsReference = true)]
- [DebuggerDisplay("Id: {Id}, Name: {Name}")]
+ [DebuggerDisplay("Id: {Id}, Name: {Name}, Alias: {Alias}")]
public class PropertyGroup : EntityBase, IEquatable
{
+ [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
}
///
- /// 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.
///
+ ///
+ /// The type.
+ ///
+ [DataMember]
+ public PropertyGroupType Type
+ {
+ get => _type;
+ set => SetPropertyValueAndDetectChanges(value, ref _type, nameof(Type));
+ }
+
+ ///
+ /// Gets or sets the name of the group.
+ ///
+ ///
+ /// The name.
+ ///
[DataMember]
public string Name
{
@@ -44,8 +64,30 @@ namespace Umbraco.Cms.Core.Models
}
///
- /// Gets or sets the Sort Order of the Group
+ /// Gets or sets the alias of the group.
///
+ ///
+ /// The alias.
+ ///
+ [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));
+ }
+ }
+
+ ///
+ /// Gets or sets the sort order of the group.
+ ///
+ ///
+ /// The sort order.
+ ///
[DataMember]
public int SortOrder
{
@@ -54,10 +96,13 @@ namespace Umbraco.Cms.Core.Models
}
///
- /// Gets or sets a collection of PropertyTypes for this PropertyGroup
+ /// Gets or sets a collection of property types for the group.
///
+ ///
+ /// The property types.
+ ///
///
- /// 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.
///
[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);
+ }
+
+ ///
+ /// Gets the local alias.
+ ///
+ /// The property group.
+ ///
+ /// The local alias.
+ ///
+ public static string GetLocalAlias(this PropertyGroup propertyGroup) => GetLocalAlias(propertyGroup.Alias);
+
+ ///
+ /// Updates the local alias.
+ ///
+ /// The property group.
+ /// The local alias.
+ 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;
+ }
+ }
+
+ ///
+ /// Gets the parent alias.
+ ///
+ /// The property group.
+ ///
+ /// The parent alias.
+ ///
+ public static string GetParentAlias(this PropertyGroup propertyGroup) => GetParentAlias(propertyGroup.Alias);
+
+ ///
+ /// Updates the parent alias.
+ ///
+ /// The property group.
+ /// The parent alias.
+ 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;
}
}
}
diff --git a/src/Umbraco.Core/Models/PropertyGroupCollection.cs b/src/Umbraco.Core/Models/PropertyGroupCollection.cs
index 1d7fb202ae..44fc3777ea 100644
--- a/src/Umbraco.Core/Models/PropertyGroupCollection.cs
+++ b/src/Umbraco.Core/Models/PropertyGroupCollection.cs
@@ -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
{
-
///
/// Represents a collection of objects
///
@@ -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, INotifyCollectionChanged, IDeepCloneable
{
+ ///
+ /// Initializes a new instance of the class.
+ ///
public PropertyGroupCollection()
{ }
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// The groups.
public PropertyGroupCollection(IEnumerable groups)
{
Reset(groups);
@@ -31,10 +38,10 @@ namespace Umbraco.Cms.Core.Models
///
internal void Reset(IEnumerable 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);
}
- ///
- /// Determines whether this collection contains a whose name matches the specified parameter.
- ///
- /// Name of the PropertyGroup.
- /// true if the collection contains the specified name; otherwise, false.
- ///
- 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;
}
}
diff --git a/src/Umbraco.Core/Models/PropertyGroupType.cs b/src/Umbraco.Core/Models/PropertyGroupType.cs
new file mode 100644
index 0000000000..03bcbc08f0
--- /dev/null
+++ b/src/Umbraco.Core/Models/PropertyGroupType.cs
@@ -0,0 +1,17 @@
+namespace Umbraco.Cms.Core.Models
+{
+ ///
+ /// Represents the type of a property group.
+ ///
+ public enum PropertyGroupType : short
+ {
+ ///
+ /// Display property types in a group.
+ ///
+ Group = 0,
+ ///
+ /// Display property types in a tab.
+ ///
+ Tab = 1
+ }
+}
diff --git a/src/Umbraco.Core/Models/PublishedContent/PublishedContentType.cs b/src/Umbraco.Core/Models/PublishedContent/PublishedContentType.cs
index b48c13f21f..c4d7b96feb 100644
--- a/src/Umbraco.Core/Models/PublishedContent/PublishedContentType.cs
+++ b/src/Umbraco.Core/Models/PublishedContent/PublishedContentType.cs
@@ -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
///
/// Instances of the class are immutable, ie
/// if the content type changes, then a new class needs to be created.
+ [DebuggerDisplay("{Alias}")]
public class PublishedContentType : IPublishedContentType
{
private readonly IPublishedPropertyType[] _propertyTypes;
diff --git a/src/Umbraco.Core/Models/PublishedContent/PublishedContentWrapped.cs b/src/Umbraco.Core/Models/PublishedContent/PublishedContentWrapped.cs
index 22a537da0f..92fb4a4c28 100644
--- a/src/Umbraco.Core/Models/PublishedContent/PublishedContentWrapped.cs
+++ b/src/Umbraco.Core/Models/PublishedContent/PublishedContentWrapped.cs
@@ -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 IPublishedContent implementations that
/// wrap and extend another IPublishedContent.
///
+ [DebuggerDisplay("{Id}: {Name} ({ContentType?.Alias})")]
public abstract class PublishedContentWrapped : IPublishedContent
{
private readonly IPublishedContent _content;
diff --git a/src/Umbraco.Core/Models/PublishedContent/PublishedCultureInfos.cs b/src/Umbraco.Core/Models/PublishedContent/PublishedCultureInfos.cs
index 5f8d209162..180503341b 100644
--- a/src/Umbraco.Core/Models/PublishedContent/PublishedCultureInfos.cs
+++ b/src/Umbraco.Core/Models/PublishedContent/PublishedCultureInfos.cs
@@ -1,10 +1,12 @@
using System;
+using System.Diagnostics;
namespace Umbraco.Cms.Core.Models.PublishedContent
{
///
/// Contains culture specific values for .
///
+ [DebuggerDisplay("{Culture}")]
public class PublishedCultureInfo
{
///
diff --git a/src/Umbraco.Core/Models/PublishedContent/PublishedDataType.cs b/src/Umbraco.Core/Models/PublishedContent/PublishedDataType.cs
index ecf3981c36..725616f708 100644
--- a/src/Umbraco.Core/Models/PublishedContent/PublishedDataType.cs
+++ b/src/Umbraco.Core/Models/PublishedContent/PublishedDataType.cs
@@ -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.
/// These instances should be created by an .
///
+ [DebuggerDisplay("{EditorAlias}")]
public class PublishedDataType
{
private readonly Lazy
+ [DebuggerDisplay("{Alias} ({PropertyType?.EditorAlias})")]
public abstract class PublishedPropertyBase : IPublishedProperty
{
///
diff --git a/src/Umbraco.Core/Models/PublishedContent/PublishedPropertyType.cs b/src/Umbraco.Core/Models/PublishedContent/PublishedPropertyType.cs
index 236edc0ff0..ee55917c4c 100644
--- a/src/Umbraco.Core/Models/PublishedContent/PublishedPropertyType.cs
+++ b/src/Umbraco.Core/Models/PublishedContent/PublishedPropertyType.cs
@@ -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
///
/// Instances of the class are immutable, ie
/// if the property type changes, then a new class needs to be created.
+ [DebuggerDisplay("{Alias} ({EditorAlias})")]
public class PublishedPropertyType : IPublishedPropertyType
{
private readonly IPublishedModelFactory _publishedModelFactory;
diff --git a/src/Umbraco.Core/Models/PublishedContent/PublishedSearchResult.cs b/src/Umbraco.Core/Models/PublishedContent/PublishedSearchResult.cs
index c713d02bbe..edc6cd9150 100644
--- a/src/Umbraco.Core/Models/PublishedContent/PublishedSearchResult.cs
+++ b/src/Umbraco.Core/Models/PublishedContent/PublishedSearchResult.cs
@@ -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)
diff --git a/src/Umbraco.Core/PropertyEditors/MarkdownConfiguration.cs b/src/Umbraco.Core/PropertyEditors/MarkdownConfiguration.cs
index 193c916f2c..064ddc2f64 100644
--- a/src/Umbraco.Core/PropertyEditors/MarkdownConfiguration.cs
+++ b/src/Umbraco.Core/PropertyEditors/MarkdownConfiguration.cs
@@ -10,5 +10,9 @@
[ConfigurationField("defaultValue", "Default value", "textarea", Description = "If value is blank, the editor will show this")]
public string DefaultValue { get; set; }
+
+
+ [ConfigurationField("overlayWidthSize", "Overlay Width Size", "views/propertyeditors/multiurlpicker/multiurlpicker.prevalues.html")]
+ public string OverlayWidthSize { get; set; }
}
}
diff --git a/src/Umbraco.Core/PropertyEditors/MultiUrlPickerConfiguration.cs b/src/Umbraco.Core/PropertyEditors/MultiUrlPickerConfiguration.cs
index c3913fd6b8..4131c6f00e 100644
--- a/src/Umbraco.Core/PropertyEditors/MultiUrlPickerConfiguration.cs
+++ b/src/Umbraco.Core/PropertyEditors/MultiUrlPickerConfiguration.cs
@@ -9,8 +9,11 @@ namespace Umbraco.Cms.Core.PropertyEditors
[ConfigurationField("maxNumber", "Maximum number of items", "number")]
public int MaxNumber { get; set; }
+ [ConfigurationField("overlayWidthSize", "Overlay width size", "views/propertyeditors/multiurlpicker/multiurlpicker.prevalues.html")]
+ public string OverlayWidthSize { get; set; }
+
[ConfigurationField(Constants.DataTypes.ReservedPreValueKeys.IgnoreUserStartNodes,
- "Ignore User Start Nodes", "boolean",
+ "Ignore user start nodes", "boolean",
Description = "Selecting this option allows a user to choose nodes that they normally don't have access to.")]
public bool IgnoreUserStartNodes { get; set; }
diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/TagsValueConverter.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/TagsValueConverter.cs
index d9c7cbeb75..7b14f5caf7 100644
--- a/src/Umbraco.Core/PropertyEditors/ValueConverters/TagsValueConverter.cs
+++ b/src/Umbraco.Core/PropertyEditors/ValueConverters/TagsValueConverter.cs
@@ -42,7 +42,7 @@ namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters
}
// Otherwise assume CSV storage type and return as string array
- return source.ToString().Split(new[] { "," }, StringSplitOptions.RemoveEmptyEntries);
+ return source.ToString().Split(Constants.CharArrays.Comma, StringSplitOptions.RemoveEmptyEntries);
}
public override object ConvertIntermediateToObject(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel cacheLevel, object source, bool preview)
diff --git a/src/Umbraco.Core/Runtime/MainDomSemaphoreLock.cs b/src/Umbraco.Core/Runtime/MainDomSemaphoreLock.cs
index 212e3a88c6..905bfe7f25 100644
--- a/src/Umbraco.Core/Runtime/MainDomSemaphoreLock.cs
+++ b/src/Umbraco.Core/Runtime/MainDomSemaphoreLock.cs
@@ -22,10 +22,11 @@ namespace Umbraco.Cms.Core.Runtime
public MainDomSemaphoreLock(ILogger logger, IHostingEnvironment hostingEnvironment)
{
- var lockName = "UMBRACO-" + MainDom.GetMainDomId(hostingEnvironment) + "-MAINDOM-LCK";
+ var mainDomId = MainDom.GetMainDomId(hostingEnvironment);
+ var lockName = "UMBRACO-" + mainDomId + "-MAINDOM-LCK";
_systemLock = new SystemLock(lockName);
- var eventName = "UMBRACO-" + MainDom.GetMainDomId(hostingEnvironment) + "-MAINDOM-EVT";
+ var eventName = "UMBRACO-" + mainDomId + "-MAINDOM-EVT";
_signal = new EventWaitHandle(false, EventResetMode.AutoReset, eventName);
_logger = logger;
}
diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj
index 7e71756379..3f41bc16c1 100644
--- a/src/Umbraco.Core/Umbraco.Core.csproj
+++ b/src/Umbraco.Core/Umbraco.Core.csproj
@@ -59,7 +59,4 @@
-
-
-
diff --git a/src/Umbraco.Infrastructure/Logging/Serilog/Enrichers/Log4NetLevelMapperEnricher.cs b/src/Umbraco.Infrastructure/Logging/Serilog/Enrichers/Log4NetLevelMapperEnricher.cs
index 2cf782c5bf..a9ae85e2f0 100644
--- a/src/Umbraco.Infrastructure/Logging/Serilog/Enrichers/Log4NetLevelMapperEnricher.cs
+++ b/src/Umbraco.Infrastructure/Logging/Serilog/Enrichers/Log4NetLevelMapperEnricher.cs
@@ -11,7 +11,7 @@ namespace Umbraco.Cms.Core.Logging.Serilog.Enrichers
{
public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory)
{
- var log4NetLevel = string.Empty;
+ string log4NetLevel;
switch (logEvent.Level)
{
@@ -28,21 +28,21 @@ namespace Umbraco.Cms.Core.Logging.Serilog.Enrichers
break;
case LogEventLevel.Information:
- log4NetLevel = "INFO";
+ log4NetLevel = "INFO "; //Padded string so that all log levels are 5 chars long (needed to keep the txt log file lined up nicely)
break;
case LogEventLevel.Verbose:
- log4NetLevel = "ALL";
+ log4NetLevel = "ALL "; //Padded string so that all log levels are 5 chars long (needed to keep the txt log file lined up nicely)
break;
case LogEventLevel.Warning:
- log4NetLevel = "WARN";
+ log4NetLevel = "WARN "; //Padded string so that all log levels are 5 chars long (needed to keep the txt log file lined up nicely)
+ break;
+ default:
+ log4NetLevel = string.Empty;
break;
}
- //Pad string so that all log levels are 5 chars long (needed to keep the txt log file lined up nicely)
- log4NetLevel = log4NetLevel.PadRight(5);
-
logEvent.AddPropertyIfAbsent(propertyFactory.CreateProperty("Log4NetLevel", log4NetLevel));
}
}
diff --git a/src/Umbraco.Infrastructure/Migrations/Install/DatabaseDataCreator.cs b/src/Umbraco.Infrastructure/Migrations/Install/DatabaseDataCreator.cs
index a7cf92e2a9..8c99fd6630 100644
--- a/src/Umbraco.Infrastructure/Migrations/Install/DatabaseDataCreator.cs
+++ b/src/Umbraco.Infrastructure/Migrations/Install/DatabaseDataCreator.cs
@@ -234,14 +234,14 @@ namespace Umbraco.Cms.Infrastructure.Migrations.Install
private void CreatePropertyTypeGroupData()
{
- _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.PropertyTypeGroup, "id", false, new PropertyTypeGroupDto { Id = 3, ContentTypeNodeId = 1032, Text = "Image", SortOrder = 1, UniqueId = new Guid(Cms.Core.Constants.PropertyTypeGroups.Image) });
- _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.PropertyTypeGroup, "id", false, new PropertyTypeGroupDto { Id = 4, ContentTypeNodeId = 1033, Text = "File", SortOrder = 1, UniqueId = new Guid(Cms.Core.Constants.PropertyTypeGroups.File) });
- _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.PropertyTypeGroup, "id", false, new PropertyTypeGroupDto { Id = 52, ContentTypeNodeId = 1034, Text = "Video", SortOrder = 1, UniqueId = new Guid(Cms.Core.Constants.PropertyTypeGroups.Video) });
- _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.PropertyTypeGroup, "id", false, new PropertyTypeGroupDto { Id = 53, ContentTypeNodeId = 1035, Text = "Audio", SortOrder = 1, UniqueId = new Guid(Cms.Core.Constants.PropertyTypeGroups.Audio) });
- _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.PropertyTypeGroup, "id", false, new PropertyTypeGroupDto { Id = 54, ContentTypeNodeId = 1036, Text = "Article", SortOrder = 1, UniqueId = new Guid(Cms.Core.Constants.PropertyTypeGroups.Article) });
- _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.PropertyTypeGroup, "id", false, new PropertyTypeGroupDto { Id = 55, ContentTypeNodeId = 1037, Text = "Vector Graphics", SortOrder = 1, UniqueId = new Guid(Cms.Core.Constants.PropertyTypeGroups.VectorGraphics) });
+ _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.PropertyTypeGroup, "id", false, new PropertyTypeGroupDto { Id = 3, UniqueId = new Guid(Cms.Core.Constants.PropertyTypeGroups.Image), ContentTypeNodeId = 1032, Text = "Image", Alias = "image", SortOrder = 1 });
+ _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.PropertyTypeGroup, "id", false, new PropertyTypeGroupDto { Id = 4, UniqueId = new Guid(Cms.Core.Constants.PropertyTypeGroups.File), ContentTypeNodeId = 1033, Text = "File", Alias = "file", SortOrder = 1, });
+ _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.PropertyTypeGroup, "id", false, new PropertyTypeGroupDto { Id = 52, UniqueId = new Guid(Cms.Core.Constants.PropertyTypeGroups.Video), ContentTypeNodeId = 1034, Text = "Video", Alias = "video", SortOrder = 1 });
+ _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.PropertyTypeGroup, "id", false, new PropertyTypeGroupDto { Id = 53, UniqueId = new Guid(Cms.Core.Constants.PropertyTypeGroups.Audio), ContentTypeNodeId = 1035, Text = "Audio", Alias = "audio", SortOrder = 1 });
+ _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.PropertyTypeGroup, "id", false, new PropertyTypeGroupDto { Id = 54, UniqueId = new Guid(Cms.Core.Constants.PropertyTypeGroups.Article), ContentTypeNodeId = 1036, Text = "Article", Alias = "article", SortOrder = 1 });
+ _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.PropertyTypeGroup, "id", false, new PropertyTypeGroupDto { Id = 55, UniqueId = new Guid(Cms.Core.Constants.PropertyTypeGroups.VectorGraphics), ContentTypeNodeId = 1037, Text = "Vector Graphics", Alias = "vectorGraphics", SortOrder = 1 });
//membership property group
- _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.PropertyTypeGroup, "id", false, new PropertyTypeGroupDto { Id = 11, ContentTypeNodeId = 1044, Text = "Membership", SortOrder = 1, UniqueId = new Guid(Cms.Core.Constants.PropertyTypeGroups.Membership) });
+ _database.Insert(Cms.Core.Constants.DatabaseSchema.Tables.PropertyTypeGroup, "id", false, new PropertyTypeGroupDto { Id = 11, UniqueId = new Guid(Cms.Core.Constants.PropertyTypeGroups.Membership), ContentTypeNodeId = 1044, Text = "Membership", Alias = "membership", SortOrder = 1 });
}
private void CreatePropertyTypeData()
diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_17_0/AddPropertyTypeGroupColumns.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_17_0/AddPropertyTypeGroupColumns.cs
new file mode 100644
index 0000000000..9230987389
--- /dev/null
+++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_8_17_0/AddPropertyTypeGroupColumns.cs
@@ -0,0 +1,65 @@
+using System.Collections.Generic;
+using System.Linq;
+using Microsoft.Extensions.Logging;
+using Umbraco.Cms.Core.Strings;
+using Umbraco.Cms.Infrastructure.Persistence.Dtos;
+using Umbraco.Extensions;
+
+namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_17_0
+{
+ public class AddPropertyTypeGroupColumns : MigrationBase
+ {
+ private readonly IShortStringHelper _shortStringHelper;
+
+ public AddPropertyTypeGroupColumns(IMigrationContext context, IShortStringHelper shortStringHelper)
+ : base(context) => _shortStringHelper = shortStringHelper;
+
+ protected override void Migrate()
+ {
+ AddColumn("type");
+
+ // Add column without constraints
+ AddColumn("alias", out var sqls);
+
+ // Populate non-null alias column
+ var dtos = Database.Fetch();
+ foreach (var dto in PopulateAliases(dtos))
+ Database.Update(dto, x => new { x.Alias });
+
+ // Finally add the constraints
+ foreach (var sql in sqls)
+ Database.Execute(sql);
+ }
+
+ internal IEnumerable PopulateAliases(IEnumerable dtos)
+ {
+ foreach (var dtosPerAlias in dtos.GroupBy(x => x.Text.ToSafeAlias(_shortStringHelper, true)))
+ {
+ var dtosPerAliasAndText = dtosPerAlias.GroupBy(x => x.Text);
+ var numberSuffix = 1;
+ foreach (var dtosPerText in dtosPerAliasAndText)
+ {
+ foreach (var dto in dtosPerText)
+ {
+ dto.Alias = dtosPerAlias.Key;
+
+ if (numberSuffix > 1)
+ {
+ // More than 1 name found for the alias, so add a suffix
+ dto.Alias += numberSuffix;
+ }
+
+ yield return dto;
+ }
+
+ numberSuffix++;
+ }
+
+ if (numberSuffix > 2)
+ {
+ Logger.LogError("Detected the same alias {Alias} for different property group names {Names}, the migration added suffixes, but this might break backwards compatibility.", dtosPerAlias.Key, dtosPerAliasAndText.Select(x => x.Key));
+ }
+ }
+ }
+ }
+}
diff --git a/src/Umbraco.Infrastructure/Models/Mapping/EntityMapDefinition.cs b/src/Umbraco.Infrastructure/Models/Mapping/EntityMapDefinition.cs
index e2ba56e739..e96e96f521 100644
--- a/src/Umbraco.Infrastructure/Models/Mapping/EntityMapDefinition.cs
+++ b/src/Umbraco.Infrastructure/Models/Mapping/EntityMapDefinition.cs
@@ -81,7 +81,7 @@ namespace Umbraco.Cms.Core.Models.Mapping
// Umbraco.Code.MapAll -Udi -Trashed
private static void Map(PropertyGroup source, EntityBasic target, MapperContext context)
{
- target.Alias = source.Name.ToLowerInvariant();
+ target.Alias = source.Alias;
target.Icon = "icon-tab";
target.Id = source.Id;
target.Key = source.Key;
diff --git a/src/Umbraco.Infrastructure/Packaging/PackageDataInstallation.cs b/src/Umbraco.Infrastructure/Packaging/PackageDataInstallation.cs
index 2511aab600..bed078820a 100644
--- a/src/Umbraco.Infrastructure/Packaging/PackageDataInstallation.cs
+++ b/src/Umbraco.Infrastructure/Packaging/PackageDataInstallation.cs
@@ -768,7 +768,7 @@ namespace Umbraco.Cms.Infrastructure.Packaging
UpdateContentTypesAllowedTemplates(contentTypex, infoElement.Element("AllowedTemplates"), defaultTemplateElement);
}
- UpdateContentTypesTabs(contentType, documentType.Element("Tabs"));
+ UpdateContentTypesPropertyGroups(contentType, documentType.Element("Tabs"));
UpdateContentTypesProperties(contentType, documentType.Element("GenericProperties"));
return contentType;
@@ -813,27 +813,40 @@ namespace Umbraco.Cms.Infrastructure.Packaging
}
}
- private void UpdateContentTypesTabs(T contentType, XElement tabElement)
+ private void UpdateContentTypesPropertyGroups(T contentType, XElement propertyGroupsContainer)
where T : IContentTypeComposition
{
- if (tabElement == null)
+ if (propertyGroupsContainer == null)
return;
- var tabs = tabElement.Elements("Tab");
- foreach (var tab in tabs)
+ var propertyGroupElements = propertyGroupsContainer.Elements("Tab");
+ foreach (var propertyGroupElement in propertyGroupElements)
{
- var caption = tab.Element("Caption").Value;
+ var name = propertyGroupElement.Element("Caption").Value; // TODO Rename to Name (same in EntityXmlSerializer)
- if (contentType.PropertyGroups.Contains(caption) == false)
+ var alias = propertyGroupElement.Element("Alias")?.Value;
+ if (string.IsNullOrEmpty(alias))
{
- contentType.AddPropertyGroup(caption);
-
+ alias = name.ToSafeAlias(_shortStringHelper, true);
}
- if (tab.Element("SortOrder") != null && int.TryParse(tab.Element("SortOrder").Value, out int sortOrder))
+ contentType.AddPropertyGroup(name, alias);
+ var propertyGroup = contentType.PropertyGroups[alias];
+
+ if (Guid.TryParse(propertyGroupElement.Element("Key")?.Value, out var key))
+ {
+ propertyGroup.Key = key;
+ }
+
+ if (Enum.TryParse(propertyGroupElement.Element("Type")?.Value, out var type))
+ {
+ propertyGroup.Type = type;
+ }
+
+ if (int.TryParse(propertyGroupElement.Element("SortOrder")?.Value, out var sortOrder))
{
// Override the sort order with the imported value
- contentType.PropertyGroups[caption].SortOrder = sortOrder;
+ propertyGroup.SortOrder = sortOrder;
}
}
}
@@ -926,14 +939,21 @@ namespace Umbraco.Cms.Infrastructure.Packaging
propertyType.Key = new Guid(property.Element("Key").Value);
}
- var tab = (string)property.Element("Tab");
- if (string.IsNullOrEmpty(tab))
+ var tabElement = property.Element("Tab");
+ if (tabElement == null || string.IsNullOrEmpty(tabElement.Value))
{
contentType.AddPropertyType(propertyType);
}
else
{
- contentType.AddPropertyType(propertyType, tab);
+ var propertyGroupName = tabElement.Value;
+ var propertyGroupAlias = tabElement.Attribute("Alias")?.Value;
+ if (string.IsNullOrEmpty(propertyGroupAlias))
+ {
+ propertyGroupAlias = propertyGroupName.ToSafeAlias(_shortStringHelper, true);
+ }
+
+ contentType.AddPropertyType(propertyType, propertyGroupAlias, propertyGroupName);
}
}
}
diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/AxisDefintionDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/AxisDefintionDto.cs
new file mode 100644
index 0000000000..226011cf90
--- /dev/null
+++ b/src/Umbraco.Infrastructure/Persistence/Dtos/AxisDefintionDto.cs
@@ -0,0 +1,16 @@
+using NPoco;
+
+namespace Umbraco.Cms.Infrastructure.Persistence.Dtos
+{
+ internal class AxisDefintionDto
+ {
+ [Column("nodeId")]
+ public int NodeId { get; set; }
+
+ [Column("alias")]
+ public string Alias { get; set; }
+
+ [Column("ParentID")]
+ public int ParentId { get; set; }
+ }
+}
diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/ColumnInSchemaDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/ColumnInSchemaDto.cs
new file mode 100644
index 0000000000..c5c1c158e2
--- /dev/null
+++ b/src/Umbraco.Infrastructure/Persistence/Dtos/ColumnInSchemaDto.cs
@@ -0,0 +1,25 @@
+using NPoco;
+
+namespace Umbraco.Cms.Infrastructure.Persistence.Dtos
+{
+ internal class ColumnInSchemaDto
+ {
+ [Column("TABLE_NAME")]
+ public string TableName { get; set; }
+
+ [Column("COLUMN_NAME")]
+ public string ColumnName { get; set; }
+
+ [Column("ORDINAL_POSITION")]
+ public int OrdinalPosition { get; set; }
+
+ [Column("COLUMN_DEFAULT")]
+ public string ColumnDefault { get; set; }
+
+ [Column("IS_NULLABLE")]
+ public string IsNullable { get; set; }
+
+ [Column("DATA_TYPE")]
+ public string DataType { get; set; }
+ }
+}
diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/ConstraintPerColumnDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/ConstraintPerColumnDto.cs
new file mode 100644
index 0000000000..c8a05b41d7
--- /dev/null
+++ b/src/Umbraco.Infrastructure/Persistence/Dtos/ConstraintPerColumnDto.cs
@@ -0,0 +1,16 @@
+using NPoco;
+
+namespace Umbraco.Cms.Infrastructure.Persistence.Dtos
+{
+ internal class ConstraintPerColumnDto
+ {
+ [Column("TABLE_NAME")]
+ public string TableName { get; set; }
+
+ [Column("COLUMN_NAME")]
+ public string ColumnName { get; set; }
+
+ [Column("CONSTRAINT_NAME")]
+ public string ConstraintName { get; set; }
+ }
+}
diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/ConstraintPerTableDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/ConstraintPerTableDto.cs
new file mode 100644
index 0000000000..c8bbf17114
--- /dev/null
+++ b/src/Umbraco.Infrastructure/Persistence/Dtos/ConstraintPerTableDto.cs
@@ -0,0 +1,13 @@
+using NPoco;
+
+namespace Umbraco.Cms.Infrastructure.Persistence.Dtos
+{
+ internal class ConstraintPerTableDto
+ {
+ [Column("TABLE_NAME")]
+ public string TableName { get; set; }
+
+ [Column("CONSTRAINT_NAME")]
+ public string ConstraintName { get; set; }
+ }
+}
diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/DefaultConstraintPerColumnDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/DefaultConstraintPerColumnDto.cs
new file mode 100644
index 0000000000..445f38f53c
--- /dev/null
+++ b/src/Umbraco.Infrastructure/Persistence/Dtos/DefaultConstraintPerColumnDto.cs
@@ -0,0 +1,19 @@
+using NPoco;
+
+namespace Umbraco.Cms.Infrastructure.Persistence.Dtos
+{
+ internal class DefaultConstraintPerColumnDto
+ {
+ [Column("TABLE_NAME")]
+ public string TableName { get; set; }
+
+ [Column("COLUMN_NAME")]
+ public string ColumnName { get; set; }
+
+ [Column("NAME")]
+ public string Name { get; set; }
+
+ [Column("DEFINITION")]
+ public string Definition { get; set; }
+ }
+}
diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/DefinedIndexDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/DefinedIndexDto.cs
new file mode 100644
index 0000000000..79a7de2273
--- /dev/null
+++ b/src/Umbraco.Infrastructure/Persistence/Dtos/DefinedIndexDto.cs
@@ -0,0 +1,20 @@
+using NPoco;
+
+namespace Umbraco.Cms.Infrastructure.Persistence.Dtos
+{
+ internal class DefinedIndexDto
+ {
+
+ [Column("TABLE_NAME")]
+ public string TableName { get; set; }
+
+ [Column("INDEX_NAME")]
+ public string IndexName { get; set; }
+
+ [Column("COLUMN_NAME")]
+ public string ColumnName { get; set; }
+
+ [Column("UNIQUE")]
+ public short Unique { get; set; }
+ }
+}
diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/PropertyTypeGroupDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/PropertyTypeGroupDto.cs
index 1fbc1b734c..42137ed6f1 100644
--- a/src/Umbraco.Infrastructure/Persistence/Dtos/PropertyTypeGroupDto.cs
+++ b/src/Umbraco.Infrastructure/Persistence/Dtos/PropertyTypeGroupDto.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Collections.Generic;
using NPoco;
using Umbraco.Cms.Infrastructure.Persistence.DatabaseAnnotations;
@@ -6,33 +6,42 @@ using Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions;
namespace Umbraco.Cms.Infrastructure.Persistence.Dtos
{
- [TableName(Cms.Core.Constants.DatabaseSchema.Tables.PropertyTypeGroup)]
+ [TableName(TableName)]
[PrimaryKey("id", AutoIncrement = true)]
[ExplicitColumns]
internal class PropertyTypeGroupDto
{
+ public const string TableName = Core.Constants.DatabaseSchema.Tables.PropertyTypeGroup;
+
[Column("id")]
[PrimaryKeyColumn(IdentitySeed = 56)]
public int Id { get; set; }
+ [Column("uniqueID")]
+ [NullSetting(NullSetting = NullSettings.NotNull)]
+ [Constraint(Default = SystemMethods.NewGuid)]
+ [Index(IndexTypes.UniqueNonClustered, Name = "IX_cmsPropertyTypeGroupUniqueID")]
+ public Guid UniqueId { get; set; }
+
[Column("contenttypeNodeId")]
[ForeignKey(typeof(ContentTypeDto), Column = "nodeId")]
public int ContentTypeNodeId { get; set; }
+ [Column("type")]
+ [Constraint(Default = 0)]
+ public short Type { get; set; }
+
[Column("text")]
public string Text { get; set; }
+ [Column("alias")]
+ public string Alias { get; set; }
+
[Column("sortorder")]
public int SortOrder { get; set; }
[ResultColumn]
[Reference(ReferenceType.Many, ReferenceMemberName = "PropertyTypeGroupId")]
public List PropertyTypeDtos { get; set; }
-
- [Column("uniqueID")]
- [NullSetting(NullSetting = NullSettings.NotNull)]
- [Constraint(Default = SystemMethods.NewGuid)]
- [Index(IndexTypes.UniqueNonClustered, Name = "IX_cmsPropertyTypeGroupUniqueID")]
- public Guid UniqueId { get; set; }
}
}
diff --git a/src/Umbraco.Infrastructure/Persistence/Dtos/UserNotificationDto.cs b/src/Umbraco.Infrastructure/Persistence/Dtos/UserNotificationDto.cs
new file mode 100644
index 0000000000..7c947dd9f2
--- /dev/null
+++ b/src/Umbraco.Infrastructure/Persistence/Dtos/UserNotificationDto.cs
@@ -0,0 +1,20 @@
+using System;
+using NPoco;
+
+namespace Umbraco.Cms.Infrastructure.Persistence.Dtos
+{
+ internal class UserNotificationDto
+ {
+ [Column("nodeId")]
+ public int NodeId { get; set; }
+
+ [Column("userId")]
+ public int UserId { get; set; }
+
+ [Column("nodeObjectType")]
+ public Guid NodeObjectType { get; set; }
+
+ [Column("action")]
+ public string Action { get; set; }
+ }
+}
diff --git a/src/Umbraco.Infrastructure/Persistence/Factories/PropertyGroupFactory.cs b/src/Umbraco.Infrastructure/Persistence/Factories/PropertyGroupFactory.cs
index 46a84000ab..10b6aadea3 100644
--- a/src/Umbraco.Infrastructure/Persistence/Factories/PropertyGroupFactory.cs
+++ b/src/Umbraco.Infrastructure/Persistence/Factories/PropertyGroupFactory.cs
@@ -35,10 +35,13 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Factories
if (groupDto.ContentTypeNodeId == contentTypeId)
group.Id = groupDto.Id;
- group.Name = groupDto.Text;
- group.SortOrder = groupDto.SortOrder;
- group.PropertyTypes = new PropertyTypeCollection(isPublishing);
group.Key = groupDto.UniqueId;
+ group.Type = (PropertyGroupType)groupDto.Type;
+ group.Name = groupDto.Text;
+ group.Alias = groupDto.Alias;
+ group.SortOrder = groupDto.SortOrder;
+
+ group.PropertyTypes = new PropertyTypeCollection(isPublishing);
//Because we are likely to have a group with no PropertyTypes we need to ensure that these are excluded
var typeDtos = groupDto.PropertyTypeDtos.Where(x => x.Id > 0);
@@ -104,10 +107,12 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Factories
{
var dto = new PropertyTypeGroupDto
{
+ UniqueId = propertyGroup.Key,
+ Type = (short)propertyGroup.Type,
ContentTypeNodeId = contentTypeId,
- SortOrder = propertyGroup.SortOrder,
Text = propertyGroup.Name,
- UniqueId = propertyGroup.Key
+ Alias = propertyGroup.Alias,
+ SortOrder = propertyGroup.SortOrder
};
if (propertyGroup.HasIdentity)
@@ -118,7 +123,7 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Factories
return dto;
}
- internal static PropertyTypeDto BuildPropertyTypeDto(int tabId, IPropertyType propertyType, int contentTypeId)
+ internal static PropertyTypeDto BuildPropertyTypeDto(int groupId, IPropertyType propertyType, int contentTypeId)
{
var propertyTypeDto = new PropertyTypeDto
{
@@ -137,9 +142,9 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Factories
LabelOnTop = propertyType.LabelOnTop
};
- if (tabId != default)
+ if (groupId != default)
{
- propertyTypeDto.PropertyTypeGroupId = tabId;
+ propertyTypeDto.PropertyTypeGroupId = groupId;
}
else
{
diff --git a/src/Umbraco.Infrastructure/Persistence/IUmbracoDatabaseFactory.cs b/src/Umbraco.Infrastructure/Persistence/IUmbracoDatabaseFactory.cs
index 92afc631f5..5af76d7220 100644
--- a/src/Umbraco.Infrastructure/Persistence/IUmbracoDatabaseFactory.cs
+++ b/src/Umbraco.Infrastructure/Persistence/IUmbracoDatabaseFactory.cs
@@ -35,6 +35,12 @@ namespace Umbraco.Cms.Infrastructure.Persistence
/// May return null if the database factory is not configured.
string ConnectionString { get; }
+ ///
+ /// Gets the provider name.
+ ///
+ /// May return null if the database factory is not configured.
+ string ProviderName { get; }
+
///
/// Gets a value indicating whether the database factory is configured (see ),
/// and it is possible to connect to the database. The factory may however not be initialized (see
diff --git a/src/Umbraco.Infrastructure/Persistence/Mappers/PropertyGroupMapper.cs b/src/Umbraco.Infrastructure/Persistence/Mappers/PropertyGroupMapper.cs
index 96a63450c9..47c7df3a8e 100644
--- a/src/Umbraco.Infrastructure/Persistence/Mappers/PropertyGroupMapper.cs
+++ b/src/Umbraco.Infrastructure/Persistence/Mappers/PropertyGroupMapper.cs
@@ -19,8 +19,10 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Mappers
{
DefineMap(nameof(PropertyGroup.Id), nameof(PropertyTypeGroupDto.Id));
DefineMap(nameof(PropertyGroup.Key), nameof(PropertyTypeGroupDto.UniqueId));
- DefineMap(nameof(PropertyGroup.SortOrder), nameof(PropertyTypeGroupDto.SortOrder));
+ DefineMap(nameof(PropertyGroup.Type), nameof(PropertyTypeGroupDto.Type));
DefineMap(nameof(PropertyGroup.Name), nameof(PropertyTypeGroupDto.Text));
+ DefineMap(nameof(PropertyGroup.Alias), nameof(PropertyTypeGroupDto.Alias));
+ DefineMap(nameof(PropertyGroup.SortOrder), nameof(PropertyTypeGroupDto.SortOrder));
}
}
}
diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentTypeCommonRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentTypeCommonRepository.cs
index 80f1cbc8f6..50411ab5d8 100644
--- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentTypeCommonRepository.cs
+++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentTypeCommonRepository.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Collections.Generic;
using System.Linq;
using NPoco;
@@ -255,12 +255,12 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement
if (contentType is IMemberType memberType)
{
// ensure that the group exists (ok if it already exists)
- memberType.AddPropertyGroup(Cms.Core.Constants.Conventions.Member.StandardPropertiesGroupName);
+ memberType.AddPropertyGroup(Cms.Core.Constants.Conventions.Member.StandardPropertiesGroupName, Cms.Core.Constants.Conventions.Member.StandardPropertiesGroupAlias);
// ensure that property types exist (ok if they already exist)
foreach (var (alias, propertyType) in builtinProperties)
{
- var added = memberType.AddPropertyType(propertyType, Cms.Core.Constants.Conventions.Member.StandardPropertiesGroupName);
+ var added = memberType.AddPropertyType(propertyType, Cms.Core.Constants.Conventions.Member.StandardPropertiesGroupAlias, Cms.Core.Constants.Conventions.Member.StandardPropertiesGroupName);
if (added)
{
@@ -278,9 +278,11 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement
return new PropertyGroup(new PropertyTypeCollection(isPublishing))
{
Id = dto.Id,
+ Key = dto.UniqueId,
+ Type = (PropertyGroupType)dto.Type,
Name = dto.Text,
- SortOrder = dto.SortOrder,
- Key = dto.UniqueId
+ Alias = dto.Alias,
+ SortOrder = dto.SortOrder
};
}
diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentTypeRepositoryBase.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentTypeRepositoryBase.cs
index e5f618878d..3b7ed6ab85 100644
--- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentTypeRepositoryBase.cs
+++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/ContentTypeRepositoryBase.cs
@@ -203,7 +203,6 @@ AND umbracoNode.nodeObjectType = @objectType",
});
}
-
//Insert Tabs
foreach (var propertyGroup in entity.PropertyGroups)
{
@@ -393,7 +392,7 @@ AND umbracoNode.id <> @id",
// see http://issues.umbraco.org/issue/U4-8663
orphanPropertyTypeIds = Database.Fetch("WHERE propertyTypeGroupId IN (@ids)", new { ids = groupsToDelete })
.Select(x => x.Id).ToList();
- Database.Update("SET propertyTypeGroupId=NULL WHERE propertyTypeGroupId IN (@ids)", new { ids = groupsToDelete });
+ Database.Update("SET propertyTypeGroupId = NULL WHERE propertyTypeGroupId IN (@ids)", new { ids = groupsToDelete });
// now we can delete the tabs
Database.Delete("WHERE id IN (@ids)", new { ids = groupsToDelete });
@@ -1384,9 +1383,9 @@ WHERE {Cms.Core.Constants.DatabaseSchema.Tables.Content}.nodeId IN (@ids) AND cm
"DELETE FROM cmsContentTypeAllowedContentType WHERE AllowedId = @id",
"DELETE FROM cmsContentType2ContentType WHERE parentContentTypeId = @id",
"DELETE FROM cmsContentType2ContentType WHERE childContentTypeId = @id",
- "DELETE FROM " + Cms.Core.Constants.DatabaseSchema.Tables.PropertyData + " WHERE propertyTypeId IN (SELECT id FROM cmsPropertyType WHERE contentTypeId = @id)",
- "DELETE FROM cmsPropertyType WHERE contentTypeId = @id",
- "DELETE FROM cmsPropertyTypeGroup WHERE contenttypeNodeId = @id",
+ "DELETE FROM " + Constants.DatabaseSchema.Tables.PropertyData + " WHERE propertyTypeId IN (SELECT id FROM cmsPropertyType WHERE contentTypeId = @id)",
+ "DELETE FROM " + Constants.DatabaseSchema.Tables.PropertyType + " WHERE contentTypeId = @id",
+ "DELETE FROM " + Constants.DatabaseSchema.Tables.PropertyTypeGroup + " WHERE contenttypeNodeId = @id"
};
return list;
}
diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MemberTypeRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MemberTypeRepository.cs
index 32a4d71cb8..b8cfe29dc8 100644
--- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MemberTypeRepository.cs
+++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/MemberTypeRepository.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Collections.Generic;
using System.Linq;
using Microsoft.Extensions.Logging;
@@ -146,11 +146,11 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement
}
//By Convention we add 9 standard PropertyTypes to an Umbraco MemberType
- entity.AddPropertyGroup(Cms.Core.Constants.Conventions.Member.StandardPropertiesGroupName);
+ entity.AddPropertyGroup(Cms.Core.Constants.Conventions.Member.StandardPropertiesGroupName, Cms.Core.Constants.Conventions.Member.StandardPropertiesGroupAlias);
var standardPropertyTypes = ConventionsHelper.GetStandardPropertyTypeStubs(_shortStringHelper);
foreach (var standardPropertyType in standardPropertyTypes)
{
- entity.AddPropertyType(standardPropertyType.Value, Cms.Core.Constants.Conventions.Member.StandardPropertiesGroupName);
+ entity.AddPropertyType(standardPropertyType.Value, Cms.Core.Constants.Conventions.Member.StandardPropertiesGroupAlias, Cms.Core.Constants.Conventions.Member.StandardPropertiesGroupName);
}
EnsureExplicitDataTypeForBuiltInProperties(entity);
diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/NotificationsRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/NotificationsRepository.cs
index e5f3d84e2e..2d68c95fe2 100644
--- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/NotificationsRepository.cs
+++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/NotificationsRepository.cs
@@ -39,22 +39,22 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement
sql
.OrderBy(x => x.Id)
.OrderBy(dto => dto.NodeId);
- return AmbientScope.Database.Fetch(sql).Select(x => new Notification(x.nodeId, x.userId, x.action, objectType));
+ return AmbientScope.Database.Fetch(sql).Select(x => new Notification(x.NodeId, x.UserId, x.Action, objectType));
}
public IEnumerable GetUserNotifications(IUser user)
{
var sql = AmbientScope.SqlContext.Sql()
- .Select("DISTINCT umbracoNode.id, umbracoUser2NodeNotify.userId, umbracoNode.nodeObjectType, umbracoUser2NodeNotify.action")
+ .Select("DISTINCT umbracoNode.id AS nodeId, umbracoUser2NodeNotify.userId, umbracoNode.nodeObjectType, umbracoUser2NodeNotify.action")
.From()
.InnerJoin()
.On(dto => dto.NodeId, dto => dto.NodeId)
.Where(dto => dto.UserId == (int)user.Id)
.OrderBy(dto => dto.NodeId);
- var dtos = AmbientScope.Database.Fetch(sql);
+ var dtos = AmbientScope.Database.Fetch(sql);
//need to map the results
- return dtos.Select(d => new Notification(d.id, d.userId, d.action, d.nodeObjectType)).ToList();
+ return dtos.Select(d => new Notification(d.NodeId, d.UserId, d.Action, d.NodeObjectType)).ToList();
}
public IEnumerable SetNotifications(IUser user, IEntity entity, string[] actions)
@@ -66,16 +66,16 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement
public IEnumerable GetEntityNotifications(IEntity entity)
{
var sql = AmbientScope.SqlContext.Sql()
- .Select("DISTINCT umbracoNode.id, umbracoUser2NodeNotify.userId, umbracoNode.nodeObjectType, umbracoUser2NodeNotify.action")
+ .Select("DISTINCT umbracoNode.id as nodeId, umbracoUser2NodeNotify.userId, umbracoNode.nodeObjectType, umbracoUser2NodeNotify.action")
.From()
.InnerJoin()
.On(dto => dto.NodeId, dto => dto.NodeId)
.Where(dto => dto.NodeId == entity.Id)
.OrderBy(dto => dto.NodeId);
- var dtos = AmbientScope.Database.Fetch(sql);
+ var dtos = AmbientScope.Database.Fetch(sql);
//need to map the results
- return dtos.Select(d => new Notification(d.id, d.userId, d.action, d.nodeObjectType)).ToList();
+ return dtos.Select(d => new Notification(d.NodeId, d.UserId, d.Action, d.NodeObjectType)).ToList();
}
public int DeleteNotifications(IEntity entity)
diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/TemplateRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/TemplateRepository.cs
index 2039c7f2eb..b0cabe5312 100644
--- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/TemplateRepository.cs
+++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/TemplateRepository.cs
@@ -311,13 +311,14 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement
.Where("umbracoNode." + SqlContext.SqlSyntax.GetQuotedColumnName("id") + " IN (@parentIds) OR umbracoNode.parentID IN (@childIds)",
new {parentIds = templates.Select(x => x.NodeDto.ParentId), childIds = templates.Select(x => x.NodeId)});
- IEnumerable childIds = Database.Fetch(childIdsSql)
+ var childIds = Database.Fetch(childIdsSql)
.Select(x => new EntitySlim
{
- Id = x.nodeId,
- ParentId = x.parentID,
- Name = x.alias
+ Id = x.NodeId,
+ ParentId = x.ParentId,
+ Name = x.Alias
});
+
return childIds;
}
diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/UserRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/UserRepository.cs
index cc61a44a44..855beac27e 100644
--- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/UserRepository.cs
+++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/UserRepository.cs
@@ -15,6 +15,7 @@ using Umbraco.Cms.Core.Persistence.Querying;
using Umbraco.Cms.Core.Persistence.Repositories;
using Umbraco.Cms.Core.Scoping;
using Umbraco.Cms.Core.Serialization;
+using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Infrastructure.Persistence.Dtos;
using Umbraco.Cms.Infrastructure.Persistence.Factories;
using Umbraco.Cms.Infrastructure.Persistence.Mappers;
@@ -32,19 +33,26 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement
private readonly GlobalSettings _globalSettings;
private readonly UserPasswordConfigurationSettings _passwordConfiguration;
private readonly IJsonSerializer _jsonSerializer;
+ private readonly IRuntimeState _runtimeState;
private string _passwordConfigJson;
private bool _passwordConfigInitialized;
///
- /// Constructor
+ /// Initializes a new instance of the class.
///
- ///
- ///
- ///
- ///
- /// A dictionary specifying the configuration for user passwords. If this is null then no password configuration will be persisted or read.
- ///
- ///
+ /// The scope accessor.
+ /// The application caches.
+ /// The logger.
+ /// A dictionary specifying the configuration for user passwords. If this is null then no password configuration will be persisted or read.
+ /// The global settings.
+ /// The password configuration.
+ /// The JSON serializer.
+ /// State of the runtime.
+ /// mapperCollection
+ /// or
+ /// globalSettings
+ /// or
+ /// passwordConfiguration
public UserRepository(
IScopeAccessor scopeAccessor,
AppCaches appCaches,
@@ -52,13 +60,15 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement
IMapperCollection mapperCollection,
IOptions globalSettings,
IOptions passwordConfiguration,
- IJsonSerializer jsonSerializer)
+ IJsonSerializer jsonSerializer,
+ IRuntimeState runtimeState)
: base(scopeAccessor, appCaches, logger)
{
_mapperCollection = mapperCollection ?? throw new ArgumentNullException(nameof(mapperCollection));
_globalSettings = globalSettings.Value ?? throw new ArgumentNullException(nameof(globalSettings));
_passwordConfiguration = passwordConfiguration.Value ?? throw new ArgumentNullException(nameof(passwordConfiguration));
_jsonSerializer = jsonSerializer;
+ _runtimeState = runtimeState;
}
///
@@ -91,9 +101,21 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement
// This will never resolve to a user, yet this is asked
// for all of the time (especially in cases of members).
// Don't issue a SQL call for this, we know it will not exist.
- if (id == default || id < -1)
+ if (_runtimeState.Level == RuntimeLevel.Upgrade)
{
- return null;
+ // when upgrading people might come from version 7 where user 0 was the default,
+ // only in upgrade mode do we want to fetch the user of Id 0
+ if (id < -1)
+ {
+ return null;
+ }
+ }
+ else
+ {
+ if (id == default || id < -1)
+ {
+ return null;
+ }
}
var sql = SqlContext.Sql()
@@ -154,30 +176,22 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement
public IDictionary GetUserStates()
{
- var sql = @"SELECT '1CountOfAll' AS colName, COUNT(id) AS num FROM umbracoUser
+ // These keys in this query map to the `Umbraco.Core.Models.Membership.UserState` enum
+ var sql = @"SELECT -1 AS [Key], COUNT(id) AS [Value] FROM umbracoUser
UNION
-SELECT '2CountOfActive' AS colName, COUNT(id) AS num FROM umbracoUser WHERE userDisabled = 0 AND userNoConsole = 0 AND lastLoginDate IS NOT NULL
+SELECT 0 AS [Key], COUNT(id) AS [Value] FROM umbracoUser WHERE userDisabled = 0 AND userNoConsole = 0 AND lastLoginDate IS NOT NULL
UNION
-SELECT '3CountOfDisabled' AS colName, COUNT(id) AS num FROM umbracoUser WHERE userDisabled = 1
+SELECT 1 AS [Key], COUNT(id) AS [Value] FROM umbracoUser WHERE userDisabled = 1
UNION
-SELECT '4CountOfLockedOut' AS colName, COUNT(id) AS num FROM umbracoUser WHERE userNoConsole = 1
+SELECT 2 AS [Key], COUNT(id) AS [Value] FROM umbracoUser WHERE userNoConsole = 1
UNION
-SELECT '5CountOfInvited' AS colName, COUNT(id) AS num FROM umbracoUser WHERE lastLoginDate IS NULL AND userDisabled = 1 AND invitedDate IS NOT NULL
+SELECT 3 AS [Key], COUNT(id) AS [Value] FROM umbracoUser WHERE lastLoginDate IS NULL AND userDisabled = 1 AND invitedDate IS NOT NULL
UNION
-SELECT '6CountOfDisabled' AS colName, COUNT(id) AS num FROM umbracoUser WHERE userDisabled = 0 AND userNoConsole = 0 AND lastLoginDate IS NULL
-ORDER BY colName";
+SELECT 4 AS [Key], COUNT(id) AS [Value] FROM umbracoUser WHERE userDisabled = 0 AND userNoConsole = 0 AND lastLoginDate IS NULL";
- var result = Database.Fetch(sql);
+ var result = Database.Dictionary(sql);
- return new Dictionary
- {
- {UserState.All, (int) result[0].num},
- {UserState.Active, (int) result[1].num},
- {UserState.Disabled, (int) result[2].num},
- {UserState.LockedOut, (int) result[3].num},
- {UserState.Invited, (int) result[4].num},
- {UserState.Inactive, (int) result[5].num}
- };
+ return result.ToDictionary(x => (UserState)x.Key, x => x.Value);
}
public Guid CreateLoginSession(int userId, string requestingIpAddress, bool cleanStaleSessions = true)
diff --git a/src/Umbraco.Infrastructure/Persistence/SqlSyntax/SqlServerSyntaxProvider.cs b/src/Umbraco.Infrastructure/Persistence/SqlSyntax/SqlServerSyntaxProvider.cs
index 0beaae113e..210b3f2d6b 100644
--- a/src/Umbraco.Infrastructure/Persistence/SqlSyntax/SqlServerSyntaxProvider.cs
+++ b/src/Umbraco.Infrastructure/Persistence/SqlSyntax/SqlServerSyntaxProvider.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Collections.Generic;
using System.Data;
using System.Data.SqlClient;
@@ -9,6 +9,7 @@ using NPoco;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Configuration.Models;
using Umbraco.Cms.Infrastructure.Persistence.DatabaseModelDefinitions;
+using Umbraco.Cms.Infrastructure.Persistence.Dtos;
using Umbraco.Extensions;
namespace Umbraco.Cms.Infrastructure.Persistence.SqlSyntax
@@ -185,7 +186,7 @@ namespace Umbraco.Cms.Infrastructure.Persistence.SqlSyntax
///
public IEnumerable> GetDefaultConstraintsPerColumn(IDatabase db)
{
- var items = db.Fetch("SELECT TableName = t.Name, ColumnName = c.Name, dc.Name, dc.[Definition] FROM sys.tables t INNER JOIN sys.default_constraints dc ON t.object_id = dc.parent_object_id INNER JOIN sys.columns c ON dc.parent_object_id = c.object_id AND c.column_id = dc.parent_column_id INNER JOIN sys.schemas as s on t.[schema_id] = s.[schema_id] WHERE s.name = (SELECT SCHEMA_NAME())");
+ var items = db.Fetch("SELECT TableName = t.Name, ColumnName = c.Name, dc.Name, dc.[Definition] FROM sys.tables t INNER JOIN sys.default_constraints dc ON t.object_id = dc.parent_object_id INNER JOIN sys.columns c ON dc.parent_object_id = c.object_id AND c.column_id = dc.parent_column_id INNER JOIN sys.schemas as s on t.[schema_id] = s.[schema_id] WHERE s.name = (SELECT SCHEMA_NAME())");
return items.Select(x => new Tuple(x.TableName, x.ColumnName, x.Name, x.Definition));
}
@@ -193,45 +194,44 @@ namespace Umbraco.Cms.Infrastructure.Persistence.SqlSyntax
public override IEnumerable GetTablesInSchema(IDatabase db)
{
- var items = db.Fetch("SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = (SELECT SCHEMA_NAME())");
- return items.Select(x => x.TABLE_NAME).Cast().ToList();
+ return db.Fetch("SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = (SELECT SCHEMA_NAME())");
}
public override IsolationLevel DefaultIsolationLevel => IsolationLevel.ReadCommitted;
public override IEnumerable GetColumnsInSchema(IDatabase db)
{
- var items = db.Fetch("SELECT TABLE_NAME, COLUMN_NAME, ORDINAL_POSITION, COLUMN_DEFAULT, IS_NULLABLE, DATA_TYPE FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = (SELECT SCHEMA_NAME())");
+ var items = db.Fetch("SELECT TABLE_NAME, COLUMN_NAME, ORDINAL_POSITION, COLUMN_DEFAULT, IS_NULLABLE, DATA_TYPE FROM INFORMATION_SCHEMA.COLUMNS WHERE TABLE_SCHEMA = (SELECT SCHEMA_NAME())");
return
items.Select(
item =>
- new ColumnInfo(item.TABLE_NAME, item.COLUMN_NAME, item.ORDINAL_POSITION, item.COLUMN_DEFAULT,
- item.IS_NULLABLE, item.DATA_TYPE)).ToList();
+ new ColumnInfo(item.TableName, item.ColumnName, item.OrdinalPosition, item.ColumnDefault,
+ item.IsNullable, item.DataType)).ToList();
}
///
public override IEnumerable> GetConstraintsPerTable(IDatabase db)
{
var items =
- db.Fetch(
+ db.Fetch(
"SELECT TABLE_NAME, CONSTRAINT_NAME FROM INFORMATION_SCHEMA.CONSTRAINT_TABLE_USAGE WHERE TABLE_SCHEMA = (SELECT SCHEMA_NAME())");
- return items.Select(item => new Tuple(item.TABLE_NAME, item.CONSTRAINT_NAME)).ToList();
+ return items.Select(item => new Tuple(item.TableName, item.ConstraintName)).ToList();
}
///
public override IEnumerable> GetConstraintsPerColumn(IDatabase db)
{
var items =
- db.Fetch(
+ db.Fetch(
"SELECT TABLE_NAME, COLUMN_NAME, CONSTRAINT_NAME FROM INFORMATION_SCHEMA.CONSTRAINT_COLUMN_USAGE WHERE TABLE_SCHEMA = (SELECT SCHEMA_NAME())");
- return items.Select(item => new Tuple(item.TABLE_NAME, item.COLUMN_NAME, item.CONSTRAINT_NAME)).ToList();
+ return items.Select(item => new Tuple(item.TableName, item.ColumnName, item.ConstraintName)).ToList();
}
///
public override IEnumerable> GetDefinedIndexes(IDatabase db)
{
var items =
- db.Fetch(
+ db.Fetch(
@"select T.name as TABLE_NAME, I.name as INDEX_NAME, AC.Name as COLUMN_NAME,
CASE WHEN I.is_unique_constraint = 1 OR I.is_unique = 1 THEN 1 ELSE 0 END AS [UNIQUE]
from sys.tables as T inner join sys.indexes as I on T.[object_id] = I.[object_id]
@@ -240,8 +240,8 @@ from sys.tables as T inner join sys.indexes as I on T.[object_id] = I.[object_id
inner join sys.schemas as S on T.[schema_id] = S.[schema_id]
WHERE S.name = (SELECT SCHEMA_NAME()) AND I.is_primary_key = 0
order by T.name, I.name");
- return items.Select(item => new Tuple(item.TABLE_NAME, item.INDEX_NAME, item.COLUMN_NAME,
- item.UNIQUE == 1)).ToList();
+ return items.Select(item => new Tuple(item.TableName, item.IndexName, item.ColumnName,
+ item.Unique == 1)).ToList();
}
diff --git a/src/Umbraco.Infrastructure/Persistence/SqlSyntax/SqlSyntaxProviderBase.cs b/src/Umbraco.Infrastructure/Persistence/SqlSyntax/SqlSyntaxProviderBase.cs
index 753a372e82..db6adeca77 100644
--- a/src/Umbraco.Infrastructure/Persistence/SqlSyntax/SqlSyntaxProviderBase.cs
+++ b/src/Umbraco.Infrastructure/Persistence/SqlSyntax/SqlSyntaxProviderBase.cs
@@ -529,7 +529,7 @@ namespace Umbraco.Cms.Infrastructure.Persistence.SqlSyntax
return string.Empty;
// HACK: probably not needed with latest changes
- if (column.DefaultValue.ToString().ToLower().Equals("getdate()".ToLower()))
+ if (string.Equals(column.DefaultValue.ToString(), "GETDATE()", StringComparison.OrdinalIgnoreCase))
column.DefaultValue = SystemMethods.CurrentDateTime;
// see if this is for a system method
diff --git a/src/Umbraco.Infrastructure/Persistence/UmbracoDatabaseFactory.cs b/src/Umbraco.Infrastructure/Persistence/UmbracoDatabaseFactory.cs
index 03977a0abe..5c04fbf010 100644
--- a/src/Umbraco.Infrastructure/Persistence/UmbracoDatabaseFactory.cs
+++ b/src/Umbraco.Infrastructure/Persistence/UmbracoDatabaseFactory.cs
@@ -136,6 +136,9 @@ namespace Umbraco.Cms.Infrastructure.Persistence
///
public string ConnectionString { get; private set; }
+ ///
+ public string ProviderName => _providerName;
+
///
public bool CanConnect =>
// actually tries to connect to the database (regardless of configured/initialized)
diff --git a/src/Umbraco.Infrastructure/PropertyEditors/FileUploadPropertyValueEditor.cs b/src/Umbraco.Infrastructure/PropertyEditors/FileUploadPropertyValueEditor.cs
index 805d92a267..ca3132e330 100644
--- a/src/Umbraco.Infrastructure/PropertyEditors/FileUploadPropertyValueEditor.cs
+++ b/src/Umbraco.Infrastructure/PropertyEditors/FileUploadPropertyValueEditor.cs
@@ -92,7 +92,7 @@ namespace Umbraco.Cms.Core.PropertyEditors
}
// process the file
- var filepath = editorFile == null ? null : ProcessFile(file, cuid, puid);
+ var filepath = editorFile == null ? null : ProcessFile(file, editorValue.DataTypeConfiguration, cuid, puid);
// remove all temp files
foreach (ContentPropertyFile f in uploads)
@@ -111,11 +111,12 @@ namespace Umbraco.Cms.Core.PropertyEditors
}
- private string ProcessFile(ContentPropertyFile file, Guid cuid, Guid puid)
+ private string ProcessFile(ContentPropertyFile file, object dataTypeConfiguration, Guid cuid, Guid puid)
{
// process the file
// no file, invalid file, reject change
- if (UploadFileTypeValidator.IsValidFileExtension(file.FileName, _contentSettings) == false)
+ if (UploadFileTypeValidator.IsValidFileExtension(file.FileName, _contentSettings) is false ||
+ UploadFileTypeValidator.IsAllowedInDataTypeConfiguration(file.FileName, dataTypeConfiguration) is false)
{
return null;
}
diff --git a/src/Umbraco.Infrastructure/PropertyEditors/UploadFileTypeValidator.cs b/src/Umbraco.Infrastructure/PropertyEditors/UploadFileTypeValidator.cs
index 61597bc47b..f866ed405f 100644
--- a/src/Umbraco.Infrastructure/PropertyEditors/UploadFileTypeValidator.cs
+++ b/src/Umbraco.Infrastructure/PropertyEditors/UploadFileTypeValidator.cs
@@ -1,4 +1,4 @@
-// Copyright (c) Umbraco.
+// Copyright (c) Umbraco.
// See LICENSE for more details.
using System;
@@ -46,7 +46,7 @@ namespace Umbraco.Cms.Core.PropertyEditors
foreach (string filename in fileNames)
{
- if (IsValidFileExtension(filename, _contentSettings) == false)
+ if (IsValidFileExtension(filename, _contentSettings) is false || IsAllowedInDataTypeConfiguration(filename, dataTypeConfiguration) is false)
{
//we only store a single value for this editor so the 'member' or 'field'
// we'll associate this error with will simply be called 'value'
@@ -57,9 +57,36 @@ namespace Umbraco.Cms.Core.PropertyEditors
internal static bool IsValidFileExtension(string fileName, ContentSettings contentSettings)
{
- if (fileName.IndexOf('.') <= 0) return false;
- var extension = fileName.GetFileExtension().TrimStart(".");
+ if (TryGetFileExtension(fileName, out var extension) is false)
+ return false;
+
return contentSettings.IsFileAllowedForUpload(extension);
}
+
+ internal static bool IsAllowedInDataTypeConfiguration(string filename, object dataTypeConfiguration)
+ {
+ if (TryGetFileExtension(filename, out var extension) is false)
+ return false;
+
+ if (dataTypeConfiguration is FileUploadConfiguration fileUploadConfiguration)
+ {
+ // If FileExtensions is empty and no allowed extensions have been specified, we allow everything.
+ // If there are any extensions specified, we need to check that the uploaded extension is one of them.
+ return fileUploadConfiguration.FileExtensions.IsCollectionEmpty() ||
+ fileUploadConfiguration.FileExtensions.Any(x => x.Value.InvariantEquals(extension));
+ }
+
+ return false;
+ }
+
+ internal static bool TryGetFileExtension(string fileName, out string extension)
+ {
+ extension = null;
+ if (fileName.IndexOf('.') <= 0)
+ return false;
+
+ extension = fileName.GetFileExtension().TrimStart(".");
+ return true;
+ }
}
}
diff --git a/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/BlockEditorConverter.cs b/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/BlockEditorConverter.cs
index a1bc885d50..9b4c6a6db4 100644
--- a/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/BlockEditorConverter.cs
+++ b/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/BlockEditorConverter.cs
@@ -10,7 +10,7 @@ using Umbraco.Extensions;
namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters
{
///
- /// Converts json block objects into
+ /// Converts JSON block objects into .
///
public sealed class BlockEditorConverter
{
@@ -23,29 +23,44 @@ namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters
_publishedModelFactory = publishedModelFactory;
}
- public IPublishedElement ConvertToElement(
- BlockItemData data,
- PropertyCacheLevel referenceCacheLevel, bool preview)
+ public IPublishedElement ConvertToElement(BlockItemData data, PropertyCacheLevel referenceCacheLevel, bool preview)
{
- var publishedSnapshot = _publishedSnapshotAccessor.GetRequiredPublishedSnapshot();
- // hack! we need to cast, we have no choice beacuse we cannot make breaking changes.
- var publishedContentCache = publishedSnapshot.Content;
+ var publishedContentCache = _publishedSnapshotAccessor.GetRequiredPublishedSnapshot().Content;
- // only convert element types - content types will cause an exception when PublishedModelFactory creates the model
+ // Only convert element types - content types will cause an exception when PublishedModelFactory creates the model
var publishedContentType = publishedContentCache.GetContentType(data.ContentTypeKey);
if (publishedContentType == null || publishedContentType.IsElement == false)
+ {
return null;
+ }
var propertyValues = data.RawPropertyValues;
- // Get the udi from the deserialized object. If this is empty we can fallback to checking the 'key' if there is one
+ // Get the UDI from the deserialized object. If this is empty, we can fallback to checking the 'key' if there is one
var key = (data.Udi is GuidUdi gudi) ? gudi.Guid : Guid.Empty;
- if (propertyValues.TryGetValue("key", out var keyo))
+ if (key == Guid.Empty && propertyValues.TryGetValue("key", out var keyo))
+ {
Guid.TryParse(keyo.ToString(), out key);
+ }
IPublishedElement element = new PublishedElement(publishedContentType, key, propertyValues, preview, referenceCacheLevel, _publishedSnapshotAccessor);
element = _publishedModelFactory.CreateModel(element);
+
return element;
}
+
+ public Type GetModelType(Guid contentTypeKey)
+ {
+ var publishedContentCache = _publishedSnapshotAccessor.GetRequiredPublishedSnapshot().Content;
+ var publishedContentType = publishedContentCache.GetContentType(contentTypeKey);
+ if (publishedContentType != null)
+ {
+ var modelType = ModelType.For(publishedContentType.Alias);
+
+ return _publishedModelFactory.MapModelType(modelType);
+ }
+
+ return typeof(IPublishedElement);
+ }
}
}
diff --git a/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/BlockListPropertyValueConverter.cs b/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/BlockListPropertyValueConverter.cs
index 98879fb0c3..6916f2ea3f 100644
--- a/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/BlockListPropertyValueConverter.cs
+++ b/src/Umbraco.Infrastructure/PropertyEditors/ValueConverters/BlockListPropertyValueConverter.cs
@@ -49,16 +49,9 @@ namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters
using (_proflog.DebugDuration($"ConvertPropertyToBlockList ({propertyType.DataType.Id})"))
{
- var configuration = propertyType.DataType.ConfigurationAs();
- var blockConfigMap = configuration.Blocks.ToDictionary(x => x.ContentElementTypeKey);
- var validSettingElementTypes = blockConfigMap.Values.Select(x => x.SettingsElementTypeKey).Where(x => x.HasValue).Distinct().ToList();
-
- var contentPublishedElements = new Dictionary();
- var settingsPublishedElements = new Dictionary();
-
- var layout = new List();
-
var value = (string)inter;
+
+ // Short-circuit on empty values
if (string.IsNullOrWhiteSpace(value)) return BlockListModel.Empty;
var converted = _blockListEditorDataConverter.Deserialize(value);
@@ -66,58 +59,69 @@ namespace Umbraco.Cms.Core.PropertyEditors.ValueConverters
var blockListLayout = converted.Layout.ToObject>();
- // convert the content data
+ // Get configuration
+ var configuration = propertyType.DataType.ConfigurationAs();
+ var blockConfigMap = configuration.Blocks.ToDictionary(x => x.ContentElementTypeKey);
+ var validSettingsElementTypes = blockConfigMap.Values.Select(x => x.SettingsElementTypeKey).Where(x => x.HasValue).Distinct().ToList();
+
+ // Convert the content data
+ var contentPublishedElements = new Dictionary();
foreach (var data in converted.BlockValue.ContentData)
{
if (!blockConfigMap.ContainsKey(data.ContentTypeKey)) continue;
var element = _blockConverter.ConvertToElement(data, referenceCacheLevel, preview);
if (element == null) continue;
+
contentPublishedElements[element.Key] = element;
}
- // convert the settings data
+
+ // If there are no content elements, it doesn't matter what is stored in layout
+ if (contentPublishedElements.Count == 0) return BlockListModel.Empty;
+
+ // Convert the settings data
+ var settingsPublishedElements = new Dictionary();
foreach (var data in converted.BlockValue.SettingsData)
{
- if (!validSettingElementTypes.Contains(data.ContentTypeKey)) continue;
+ if (!validSettingsElementTypes.Contains(data.ContentTypeKey)) continue;
var element = _blockConverter.ConvertToElement(data, referenceCacheLevel, preview);
if (element == null) continue;
+
settingsPublishedElements[element.Key] = element;
}
- // if there's no elements just return since if there's no data it doesn't matter what is stored in layout
- if (contentPublishedElements.Count == 0) return BlockListModel.Empty;
-
+ var layout = new List();
foreach (var layoutItem in blockListLayout)
{
- // get the content reference
+ // Get the content reference
var contentGuidUdi = (GuidUdi)layoutItem.ContentUdi;
if (!contentPublishedElements.TryGetValue(contentGuidUdi.Guid, out var contentData))
continue;
- // get the setting reference
+ if (!blockConfigMap.TryGetValue(contentData.ContentType.Key, out var blockConfig))
+ continue;
+
+ // Get the setting reference
IPublishedElement settingsData = null;
var settingGuidUdi = layoutItem.SettingsUdi != null ? (GuidUdi)layoutItem.SettingsUdi : null;
if (settingGuidUdi != null)
settingsPublishedElements.TryGetValue(settingGuidUdi.Guid, out settingsData);
- var contentTypeKey = contentData.ContentType.Key;
-
- if (!blockConfigMap.TryGetValue(contentTypeKey, out var blockConfig))
- continue;
-
- // this can happen if they have a settings type, save content, remove the settings type, and display the front-end page before saving the content again
- // we also ensure that the content type's match since maybe the settings type has been changed after this has been persisted.
- if (settingsData != null)
+ // This can happen if they have a settings type, save content, remove the settings type, and display the front-end page before saving the content again
+ // We also ensure that the content types match, since maybe the settings type has been changed after this has been persisted
+ if (settingsData != null && (!blockConfig.SettingsElementTypeKey.HasValue || settingsData.ContentType.Key != blockConfig.SettingsElementTypeKey))
{
- var settingsElementTypeKey = settingsData.ContentType.Key;
-
- if (!blockConfig.SettingsElementTypeKey.HasValue || settingsElementTypeKey != blockConfig.SettingsElementTypeKey)
- settingsData = null;
+ settingsData = null;
}
+ // Get settings type from configuration
+ var settingsType = blockConfig.SettingsElementTypeKey.HasValue
+ ? _blockConverter.GetModelType(blockConfig.SettingsElementTypeKey.Value)
+ : typeof(IPublishedElement);
+
// TODO: This should be optimized/cached, as calling Activator.CreateInstance is slow
- var layoutType = typeof(BlockListItem<,>).MakeGenericType(contentData.GetType(), settingsData?.GetType() ?? typeof(IPublishedElement));
+ var layoutType = typeof(BlockListItem<,>).MakeGenericType(contentData.GetType(), settingsType);
var layoutRef = (BlockListItem)Activator.CreateInstance(layoutType, contentGuidUdi, contentData, settingGuidUdi, settingsData);
layout.Add(layoutRef);
diff --git a/src/Umbraco.Infrastructure/Serialization/AutoInterningStringKeyCaseInsensitiveDictionaryConverter.cs b/src/Umbraco.Infrastructure/Serialization/AutoInterningStringKeyCaseInsensitiveDictionaryConverter.cs
index fa87a9a203..cff3aa69a2 100644
--- a/src/Umbraco.Infrastructure/Serialization/AutoInterningStringKeyCaseInsensitiveDictionaryConverter.cs
+++ b/src/Umbraco.Infrastructure/Serialization/AutoInterningStringKeyCaseInsensitiveDictionaryConverter.cs
@@ -5,7 +5,7 @@ using Newtonsoft.Json;
namespace Umbraco.Cms.Infrastructure.Serialization
{
///
- /// When applied to a dictionary with a string key, will ensure the deserialized string keys are interned
+ /// When applied to a dictionary with a string key, will ensure the deserialized string keys are interned
///
///
///
@@ -24,7 +24,7 @@ namespace Umbraco.Cms.Infrastructure.Serialization
{
if (reader.TokenType == JsonToken.StartObject)
{
- var dictionary = new Dictionary();
+ var dictionary = Create(objectType);
while (reader.Read())
{
switch (reader.TokenType)
diff --git a/src/Umbraco.Infrastructure/Services/Implement/ContentService.cs b/src/Umbraco.Infrastructure/Services/Implement/ContentService.cs
index 4ea399f500..dceae58446 100644
--- a/src/Umbraco.Infrastructure/Services/Implement/ContentService.cs
+++ b/src/Umbraco.Infrastructure/Services/Implement/ContentService.cs
@@ -304,7 +304,7 @@ namespace Umbraco.Cms.Core.Services.Implement
if (parent == null) throw new ArgumentNullException(nameof(parent));
- using (var scope = ScopeProvider.CreateScope(autoComplete:true))
+ using (var scope = ScopeProvider.CreateScope(autoComplete: true))
{
// locking the content tree secures content types too
scope.WriteLock(Cms.Core.Constants.Locks.ContentTree);
@@ -918,7 +918,7 @@ namespace Umbraco.Cms.Core.Services.Implement
}
///
- public PublishResult SaveAndPublish(IContent content, string[] cultures, int userId = 0)
+ public PublishResult SaveAndPublish(IContent content, string[] cultures, int userId = Cms.Core.Constants.Security.SuperUserId)
{
if (content == null) throw new ArgumentNullException(nameof(content));
if (cultures == null) throw new ArgumentNullException(nameof(cultures));
diff --git a/src/Umbraco.Infrastructure/Services/Implement/ContentTypeServiceBaseOfTRepositoryTItemTService.cs b/src/Umbraco.Infrastructure/Services/Implement/ContentTypeServiceBaseOfTRepositoryTItemTService.cs
index 942c9c1e5e..fd23db2615 100644
--- a/src/Umbraco.Infrastructure/Services/Implement/ContentTypeServiceBaseOfTRepositoryTItemTService.cs
+++ b/src/Umbraco.Infrastructure/Services/Implement/ContentTypeServiceBaseOfTRepositoryTItemTService.cs
@@ -98,13 +98,16 @@ namespace Umbraco.Cms.Core.Services.Implement
var compositionAliases = compositionContentType.CompositionAliases();
var compositions = allContentTypes.Where(x => compositionAliases.Any(y => x.Alias.Equals(y)));
- var propertyTypeAliases = compositionContentType.PropertyTypes.Select(x => x.Alias.ToLowerInvariant()).ToArray();
+ var propertyTypeAliases = compositionContentType.PropertyTypes.Select(x => x.Alias).ToArray();
+ var propertyGroupAliases = compositionContentType.PropertyGroups.ToDictionary(x => x.Alias, x => x.Type, StringComparer.InvariantCultureIgnoreCase);
var indirectReferences = allContentTypes.Where(x => x.ContentTypeComposition.Any(y => y.Id == compositionContentType.Id));
var comparer = new DelegateEqualityComparer((x, y) => x.Id == y.Id, x => x.Id);
var dependencies = new HashSet(compositions, comparer);
+
var stack = new Stack();
foreach (var indirectReference in indirectReferences)
stack.Push(indirectReference); // push indirect references to a stack, so we can add recursively
+
while (stack.Count > 0)
{
var indirectReference = stack.Pop();
@@ -114,8 +117,11 @@ namespace Umbraco.Cms.Core.Services.Implement
var directReferences = indirectReference.ContentTypeComposition;
foreach (var directReference in directReferences)
{
- if (directReference.Id == compositionContentType.Id || directReference.Alias.Equals(compositionContentType.Alias)) continue;
+ if (directReference.Id == compositionContentType.Id || directReference.Alias.Equals(compositionContentType.Alias))
+ continue;
+
dependencies.Add(directReference);
+
// a direct reference has compositions of its own - these also need to be taken into account
var directReferenceGraph = directReference.CompositionAliases();
foreach (var c in allContentTypes.Where(x => directReferenceGraph.Any(y => x.Alias.Equals(y, StringComparison.InvariantCultureIgnoreCase))))
@@ -129,13 +135,20 @@ namespace Umbraco.Cms.Core.Services.Implement
foreach (var dependency in dependencies)
{
- if (dependency.Id == compositionContentType.Id) continue;
- var contentTypeDependency = allContentTypes.FirstOrDefault(x => x.Alias.Equals(dependency.Alias, StringComparison.InvariantCultureIgnoreCase));
- if (contentTypeDependency == null) continue;
- var intersect = contentTypeDependency.PropertyTypes.Select(x => x.Alias.ToLowerInvariant()).Intersect(propertyTypeAliases).ToArray();
- if (intersect.Length == 0) continue;
+ if (dependency.Id == compositionContentType.Id)
+ continue;
- throw new InvalidCompositionException(compositionContentType.Alias, intersect.ToArray());
+ var contentTypeDependency = allContentTypes.FirstOrDefault(x => x.Alias.Equals(dependency.Alias, StringComparison.InvariantCultureIgnoreCase));
+ if (contentTypeDependency == null)
+ continue;
+
+ var duplicatePropertyTypeAliases = contentTypeDependency.PropertyTypes.Select(x => x.Alias).Intersect(propertyTypeAliases, StringComparer.InvariantCultureIgnoreCase).ToArray();
+ var invalidPropertyGroupAliases = contentTypeDependency.PropertyGroups.Where(x => propertyGroupAliases.TryGetValue(x.Alias, out var type) && type != x.Type).Select(x => x.Alias).ToArray();
+
+ if (duplicatePropertyTypeAliases.Length == 0 && invalidPropertyGroupAliases.Length == 0)
+ continue;
+
+ throw new InvalidCompositionException(compositionContentType.Alias, null, duplicatePropertyTypeAliases, invalidPropertyGroupAliases);
}
}
diff --git a/src/Umbraco.Infrastructure/Services/Implement/EntityXmlSerializer.cs b/src/Umbraco.Infrastructure/Services/Implement/EntityXmlSerializer.cs
index 11fbd87232..568e18b13e 100644
--- a/src/Umbraco.Infrastructure/Services/Implement/EntityXmlSerializer.cs
+++ b/src/Umbraco.Infrastructure/Services/Implement/EntityXmlSerializer.cs
@@ -374,29 +374,9 @@ namespace Umbraco.Cms.Core.Services.Implement
structure.Add(new XElement("MediaType", allowedType.Alias));
}
- var genericProperties = new XElement("GenericProperties"); // actually, all of them
- foreach (var propertyType in mediaType.PropertyTypes)
- {
- var definition = _dataTypeService.GetDataType(propertyType.DataTypeId);
+ var genericProperties = new XElement("GenericProperties", SerializePropertyTypes(mediaType.PropertyTypes, mediaType.PropertyGroups)); // actually, all of them
- var propertyGroup = propertyType.PropertyGroupId == null // true generic property
- ? null
- : mediaType.PropertyGroups.FirstOrDefault(x => x.Id == propertyType.PropertyGroupId.Value);
-
- XElement genericProperty = SerializePropertyType(propertyType, definition, propertyGroup);
- genericProperties.Add(genericProperty);
- }
-
- var tabs = new XElement("Tabs");
- foreach (var propertyGroup in mediaType.PropertyGroups)
- {
- var tab = new XElement("Tab",
- new XElement("Id", propertyGroup.Id.ToString(CultureInfo.InvariantCulture)),
- new XElement("Caption", propertyGroup.Name),
- new XElement("SortOrder", propertyGroup.SortOrder));
-
- tabs.Add(tab);
- }
+ var tabs = new XElement("Tabs", SerializePropertyGroups(mediaType.PropertyGroups)); // TODO Rename to PropertyGroups
var xml = new XElement("MediaType",
info,
@@ -500,30 +480,9 @@ namespace Umbraco.Cms.Core.Services.Implement
structure.Add(new XElement("DocumentType", allowedType.Alias));
}
- var genericProperties = new XElement("GenericProperties"); // actually, all of them
- foreach (var propertyType in contentType.PropertyTypes)
- {
- var definition = _dataTypeService.GetDataType(propertyType.DataTypeId);
+ var genericProperties = new XElement("GenericProperties", SerializePropertyTypes(contentType.PropertyTypes, contentType.PropertyGroups)); // actually, all of them
- var propertyGroup = propertyType.PropertyGroupId == null // true generic property
- ? null
- : contentType.PropertyGroups.FirstOrDefault(x => x.Id == propertyType.PropertyGroupId.Value);
-
- XElement genericProperty = SerializePropertyType(propertyType, definition, propertyGroup);
- genericProperty.Add(new XElement("Variations", propertyType.Variations.ToString()));
-
- genericProperties.Add(genericProperty);
- }
-
- var tabs = new XElement("Tabs");
- foreach (var propertyGroup in contentType.PropertyGroups)
- {
- var tab = new XElement("Tab",
- new XElement("Id", propertyGroup.Id.ToString(CultureInfo.InvariantCulture)),
- new XElement("Caption", propertyGroup.Name),
- new XElement("SortOrder", propertyGroup.SortOrder));
- tabs.Add(tab);
- }
+ var tabs = new XElement("Tabs", SerializePropertyGroups(contentType.PropertyGroups)); // TODO Rename to PropertyGroups
var xml = new XElement("DocumentType",
info,
@@ -536,7 +495,7 @@ namespace Umbraco.Cms.Core.Services.Implement
if (contentType.Level != 1 && masterContentType == null)
{
//get URL encoded folder names
- IEnumerable folders = _contentTypeService.GetContainers(contentType)
+ var folders = _contentTypeService.GetContainers(contentType)
.OrderBy(x => x.Level)
.Select(x => WebUtility.UrlEncode(x.Name));
@@ -544,13 +503,42 @@ namespace Umbraco.Cms.Core.Services.Implement
}
if (string.IsNullOrWhiteSpace(folderNames) == false)
- {
xml.Add(new XAttribute("Folders", folderNames));
- }
return xml;
}
+ private IEnumerable SerializePropertyTypes(IEnumerable propertyTypes, IEnumerable propertyGroups)
+ {
+ foreach (var propertyType in propertyTypes)
+ {
+ var definition = _dataTypeService.GetDataType(propertyType.DataTypeId);
+
+ var propertyGroup = propertyType.PropertyGroupId == null // true generic property
+ ? null
+ : propertyGroups.FirstOrDefault(x => x.Id == propertyType.PropertyGroupId.Value);
+
+ XElement genericProperty = SerializePropertyType(propertyType, definition, propertyGroup);
+ genericProperty.Add(new XElement("Variations", propertyType.Variations.ToString()));
+
+ yield return genericProperty;
+ }
+ }
+
+ private IEnumerable SerializePropertyGroups(IEnumerable propertyGroups)
+ {
+ foreach (var propertyGroup in propertyGroups)
+ {
+ yield return new XElement("Tab", // TODO Rename to PropertyGroup
+ new XElement("Id", propertyGroup.Id),
+ new XElement("Key", propertyGroup.Key),
+ new XElement("Type", propertyGroup.Type.ToString()),
+ new XElement("Caption", propertyGroup.Name), // TODO Rename to Name (same in PackageDataInstallation)
+ new XElement("Alias", propertyGroup.Alias),
+ new XElement("SortOrder", propertyGroup.SortOrder));
+ }
+ }
+
private XElement SerializePropertyType(IPropertyType propertyType, IDataType definition, PropertyGroup propertyGroup)
=> new XElement("GenericProperty",
new XElement("Name", propertyType.Name),
@@ -558,7 +546,7 @@ namespace Umbraco.Cms.Core.Services.Implement
new XElement("Key", propertyType.Key),
new XElement("Type", propertyType.PropertyEditorAlias),
new XElement("Definition", definition.Key),
- new XElement("Tab", propertyGroup == null ? "" : propertyGroup.Name),
+ propertyGroup != null ? new XElement("Tab", propertyGroup.Name, new XAttribute("Alias", propertyGroup.Alias)) : null, // TODO Replace with PropertyGroupAlias
new XElement("SortOrder", propertyType.SortOrder),
new XElement("Mandatory", propertyType.Mandatory.ToString()),
new XElement("LabelOnTop", propertyType.LabelOnTop.ToString()),
diff --git a/src/Umbraco.PublishedCache.NuCache/DataSource/ContentCacheDataModel.cs b/src/Umbraco.PublishedCache.NuCache/DataSource/ContentCacheDataModel.cs
index af3136da54..34df43d87c 100644
--- a/src/Umbraco.PublishedCache.NuCache/DataSource/ContentCacheDataModel.cs
+++ b/src/Umbraco.PublishedCache.NuCache/DataSource/ContentCacheDataModel.cs
@@ -1,7 +1,8 @@
using System.Collections.Generic;
+using System.Runtime.Serialization;
+using MessagePack;
using Newtonsoft.Json;
using Umbraco.Cms.Infrastructure.Serialization;
-using System.Runtime.Serialization;
namespace Umbraco.Cms.Infrastructure.PublishedCache.DataSource
{
@@ -16,11 +17,13 @@ namespace Umbraco.Cms.Infrastructure.PublishedCache.DataSource
[DataMember(Order = 0)]
[JsonProperty("pd")]
[JsonConverter(typeof(AutoInterningStringKeyCaseInsensitiveDictionaryConverter))]
+ [MessagePackFormatter(typeof(MessagePackAutoInterningStringKeyCaseInsensitiveDictionaryFormatter))]
public Dictionary PropertyData { get; set; }
[DataMember(Order = 1)]
[JsonProperty("cd")]
[JsonConverter(typeof(AutoInterningStringKeyCaseInsensitiveDictionaryConverter))]
+ [MessagePackFormatter(typeof(MessagePackAutoInterningStringKeyCaseInsensitiveDictionaryFormatter))]
public Dictionary CultureData { get; set; }
[DataMember(Order = 2)]
diff --git a/src/Umbraco.PublishedCache.NuCache/DataSource/MessagePackAutoInterningStringKeyCaseInsensitiveDictionaryFormatter.cs b/src/Umbraco.PublishedCache.NuCache/DataSource/MessagePackAutoInterningStringKeyCaseInsensitiveDictionaryFormatter.cs
new file mode 100644
index 0000000000..416955559e
--- /dev/null
+++ b/src/Umbraco.PublishedCache.NuCache/DataSource/MessagePackAutoInterningStringKeyCaseInsensitiveDictionaryFormatter.cs
@@ -0,0 +1,27 @@
+using System;
+using System.Collections.Generic;
+using MessagePack;
+using MessagePack.Formatters;
+
+namespace Umbraco.Cms.Infrastructure.PublishedCache.DataSource
+{
+ ///
+ /// A MessagePack formatter (deserializer) for a string key dictionary that uses for the key string comparison.
+ ///
+ /// The type of the value.
+ ///
+ public sealed class MessagePackAutoInterningStringKeyCaseInsensitiveDictionaryFormatter : DictionaryFormatterBase, Dictionary.Enumerator, Dictionary>
+ {
+ protected override void Add(Dictionary collection, int index, string key, TValue value, MessagePackSerializerOptions options)
+ {
+ string.Intern(key);
+ collection.Add(key, value);
+ }
+
+ protected override Dictionary Complete(Dictionary intermediateCollection) => intermediateCollection;
+
+ protected override Dictionary.Enumerator GetSourceEnumerator(Dictionary source) => source.GetEnumerator();
+
+ protected override Dictionary Create(int count, MessagePackSerializerOptions options) => new Dictionary(count, StringComparer.OrdinalIgnoreCase);
+ }
+}
diff --git a/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/ContentTypeRepositoryTest.cs b/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/ContentTypeRepositoryTest.cs
index a72e8c6c55..0eaeb8b088 100644
--- a/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/ContentTypeRepositoryTest.cs
+++ b/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/ContentTypeRepositoryTest.cs
@@ -397,9 +397,12 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence.Repos
{
Inherited = x.Inherited,
Id = x.Id,
- Properties = x.Properties,
+ Key = x.Key,
+ Type = x.Type,
+ Name = x.Name,
+ Alias = x.Alias,
SortOrder = x.SortOrder,
- Name = x.Name
+ Properties = x.Properties
}).ToArray()
};
diff --git a/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/UserRepositoryTest.cs b/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/UserRepositoryTest.cs
index 028c10a5eb..a613a42c36 100644
--- a/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/UserRepositoryTest.cs
+++ b/src/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/UserRepositoryTest.cs
@@ -16,6 +16,7 @@ using Umbraco.Cms.Core.Models.Membership;
using Umbraco.Cms.Core.Persistence.Querying;
using Umbraco.Cms.Core.Persistence.Repositories;
using Umbraco.Cms.Core.Scoping;
+using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Infrastructure.Persistence;
using Umbraco.Cms.Infrastructure.Persistence.Dtos;
using Umbraco.Cms.Infrastructure.Persistence.Mappers;
@@ -45,10 +46,19 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence.Repos
private UserRepository CreateRepository(IScopeProvider provider)
{
var accessor = (IScopeAccessor)provider;
- var repository = new UserRepository(accessor, AppCaches.Disabled, LoggerFactory.CreateLogger(), Mappers, Options.Create(GlobalSettings), Options.Create(new UserPasswordConfigurationSettings()), new JsonNetSerializer());
+ Mock mockRuntimeState = CreateMockRuntimeState(RuntimeLevel.Run);
+
+ var repository = new UserRepository(accessor, AppCaches.Disabled, LoggerFactory.CreateLogger(), Mappers, Options.Create(GlobalSettings), Options.Create(new UserPasswordConfigurationSettings()), new JsonNetSerializer(), mockRuntimeState.Object);
return repository;
}
+ private static Mock CreateMockRuntimeState(RuntimeLevel runtimeLevel)
+ {
+ var mockRuntimeState = new Mock();
+ mockRuntimeState.SetupGet(x => x.Level).Returns(runtimeLevel);
+ return mockRuntimeState;
+ }
+
private UserGroupRepository CreateUserGroupRepository(IScopeProvider provider)
{
var accessor = (IScopeAccessor)provider;
@@ -134,7 +144,9 @@ namespace Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Persistence.Repos
int id = user.Id;
- var repository2 = new UserRepository((IScopeAccessor)provider, AppCaches.Disabled, LoggerFactory.CreateLogger(), Mock.Of(), Options.Create(GlobalSettings), Options.Create(new UserPasswordConfigurationSettings()), new JsonNetSerializer());
+ Mock mockRuntimeState = CreateMockRuntimeState(RuntimeLevel.Run);
+
+ var repository2 = new UserRepository((IScopeAccessor)provider, AppCaches.Disabled, LoggerFactory.CreateLogger(), Mock.Of(), Options.Create(GlobalSettings), Options.Create(new UserPasswordConfigurationSettings()), new JsonNetSerializer(), mockRuntimeState.Object);
repository2.Delete(user);
diff --git a/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Migrations/Upgrade/V_8_17_0/AddPropertyTypeGroupColumnsTests.cs b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Migrations/Upgrade/V_8_17_0/AddPropertyTypeGroupColumnsTests.cs
new file mode 100644
index 0000000000..d832f09325
--- /dev/null
+++ b/src/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/Migrations/Upgrade/V_8_17_0/AddPropertyTypeGroupColumnsTests.cs
@@ -0,0 +1,54 @@
+using System.Linq;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.Options;
+using Moq;
+using NUnit.Framework;
+using Umbraco.Cms.Core.Configuration.Models;
+using Umbraco.Cms.Core.Strings;
+using Umbraco.Cms.Infrastructure.Migrations;
+using Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_8_17_0;
+using Umbraco.Cms.Infrastructure.Persistence.Dtos;
+using Umbraco.Cms.Tests.Common.TestHelpers;
+
+namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Infrastructure.Migrations.Upgrade.V_8_17_0
+{
+ [TestFixture]
+ public class AddPropertyTypeGroupColumnsTests
+ {
+ private readonly IShortStringHelper _shortStringHelper = new DefaultShortStringHelper(Options.Create(new RequestHandlerSettings()));
+ private readonly ILogger _contextLogger = Mock.Of>();
+
+ [Test]
+ public void CreateColumn()
+ {
+ var database = new TestDatabase();
+ var context = new MigrationContext(new MigrationPlan("test"), database, _contextLogger);
+ var migration = new AddPropertyTypeGroupColumns(context, _shortStringHelper);
+
+ var dtos = new[]
+ {
+ new PropertyTypeGroupDto() { Id = 0, Text = "Content" },
+ new PropertyTypeGroupDto() { Id = 1, Text = "Content" },
+ new PropertyTypeGroupDto() { Id = 2, Text = "Settings" },
+ new PropertyTypeGroupDto() { Id = 3, Text = "Content " }, // The trailing space is intentional
+ new PropertyTypeGroupDto() { Id = 4, Text = "SEO/OpenGraph" },
+ new PropertyTypeGroupDto() { Id = 5, Text = "Site defaults" }
+ };
+
+ var populatedDtos = migration.PopulateAliases(dtos)
+ .OrderBy(x => x.Id) // The populated DTOs can be returned in a different order
+ .ToArray();
+
+ // All DTOs should be returned and Id and Text should be unaltered
+ Assert.That(dtos.Select(x => (x.Id, x.Text)), Is.EquivalentTo(populatedDtos.Select(x => (x.Id, x.Text))));
+
+ // Check populated aliases
+ Assert.That(populatedDtos[0].Alias, Is.EqualTo("content"));
+ Assert.That(populatedDtos[1].Alias, Is.EqualTo("content"));
+ Assert.That(populatedDtos[2].Alias, Is.EqualTo("settings"));
+ Assert.That(populatedDtos[3].Alias, Is.EqualTo("content2"));
+ Assert.That(populatedDtos[4].Alias, Is.EqualTo("sEOOpenGraph"));
+ Assert.That(populatedDtos[5].Alias, Is.EqualTo("siteDefaults"));
+ }
+ }
+}
diff --git a/src/Umbraco.Tests/Umbraco.Tests.csproj b/src/Umbraco.Tests/Umbraco.Tests.csproj
index 5b1797ebe3..6ef757b795 100644
--- a/src/Umbraco.Tests/Umbraco.Tests.csproj
+++ b/src/Umbraco.Tests/Umbraco.Tests.csproj
@@ -160,6 +160,25 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -304,6 +323,38 @@
+
+ ResXFileCodeGenerator
+ SqlResources.Designer.cs
+ Designer
+
+
+ ResXFileCodeGenerator
+ ImportResources.Designer.cs
+ Designer
+
+
+ ResXFileCodeGenerator
+ TestFiles.Designer.cs
+ Designer
+
+
+
+
+ Designer
+ Always
+
+
+ Always
+
+
+
+
+
+ Designer
+
+
+
@@ -324,4 +375,4 @@
-
+
\ No newline at end of file
diff --git a/src/Umbraco.Web.BackOffice/Controllers/AuthenticationController.cs b/src/Umbraco.Web.BackOffice/Controllers/AuthenticationController.cs
index 54bfbeeb5f..e79b49ace3 100644
--- a/src/Umbraco.Web.BackOffice/Controllers/AuthenticationController.cs
+++ b/src/Umbraco.Web.BackOffice/Controllers/AuthenticationController.cs
@@ -22,7 +22,6 @@ using Umbraco.Cms.Core.Models.Membership;
using Umbraco.Cms.Core.Net;
using Umbraco.Cms.Core.Security;
using Umbraco.Cms.Core.Services;
-using Umbraco.Cms.Web.BackOffice.Extensions;
using Umbraco.Cms.Web.BackOffice.Filters;
using Umbraco.Cms.Web.BackOffice.Security;
using Umbraco.Cms.Web.Common.ActionsResults;
@@ -48,6 +47,7 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers
//[ValidationFilter] // TODO: I don't actually think this is required with our custom Application Model conventions applied
[AngularJsonOnlyConfiguration] // TODO: This could be applied with our Application Model conventions
[IsBackOffice]
+ [DisableBrowserCache]
public class AuthenticationController : UmbracoApiControllerBase
{
// NOTE: Each action must either be explicitly authorized or explicitly [AllowAnonymous], the latter is optional because
diff --git a/src/Umbraco.Web.BackOffice/Controllers/ContentController.cs b/src/Umbraco.Web.BackOffice/Controllers/ContentController.cs
index 183284d5b8..beebb246d9 100644
--- a/src/Umbraco.Web.BackOffice/Controllers/ContentController.cs
+++ b/src/Umbraco.Web.BackOffice/Controllers/ContentController.cs
@@ -386,6 +386,23 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers
return GetEmptyInner(contentType, parentId);
}
+ ///
+ /// Gets a dictionary containing empty content items for every alias specified in the contentTypeAliases array in the body of the request.
+ ///
+ ///
+ /// This is a post request in order to support a large amount of aliases without hitting the URL length limit.
+ ///
+ ///
+ ///
+ [OutgoingEditorModelEvent]
+ [HttpPost]
+ public ActionResult> GetEmptyByAliases(ContentTypesByAliases contentTypesByAliases)
+ {
+ // It's important to do this operation within a scope to reduce the amount of readlock queries.
+ using var scope = _scopeProvider.CreateScope(autoComplete: true);
+ var contentTypes = contentTypesByAliases.ContentTypeAliases.Select(alias => _contentTypeService.Get(alias));
+ return GetEmpties(contentTypes, contentTypesByAliases.ParentId).ToDictionary(x => x.ContentTypeAlias);
+ }
///
/// Gets an empty content item for the document type.
diff --git a/src/Umbraco.Web.BackOffice/Controllers/ContentTypeControllerBase.cs b/src/Umbraco.Web.BackOffice/Controllers/ContentTypeControllerBase.cs
index 0b4c311a8c..acfac62741 100644
--- a/src/Umbraco.Web.BackOffice/Controllers/ContentTypeControllerBase.cs
+++ b/src/Umbraco.Web.BackOffice/Controllers/ContentTypeControllerBase.cs
@@ -513,11 +513,13 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers
{
foreach (var propertyAlias in invalidPropertyAliases)
{
- //find the property relating to these
- var prop = contentTypeSave.Groups.SelectMany(x => x.Properties).Single(x => x.Alias == propertyAlias);
- var group = contentTypeSave.Groups.Single(x => x.Properties.Contains(prop));
+ // Find the property relating to these
+ var property = contentTypeSave.Groups.SelectMany(x => x.Properties).Single(x => x.Alias == propertyAlias);
+ var group = contentTypeSave.Groups.Single(x => x.Properties.Contains(property));
+ var propertyIndex = group.Properties.IndexOf(property);
+ var groupIndex = contentTypeSave.Groups.IndexOf(group);
- var key = string.Format("Groups[{0}].Properties[{1}].Alias", group.SortOrder, prop.SortOrder);
+ var key = $"Groups[{groupIndex}].Properties[{propertyIndex}].Alias";
ModelState.AddModelError(key, "Duplicate property aliases not allowed between compositions");
}
}
diff --git a/src/Umbraco.Web.BackOffice/Controllers/DashboardController.cs b/src/Umbraco.Web.BackOffice/Controllers/DashboardController.cs
index 64c1279d0f..7b64d05633 100644
--- a/src/Umbraco.Web.BackOffice/Controllers/DashboardController.cs
+++ b/src/Umbraco.Web.BackOffice/Controllers/DashboardController.cs
@@ -1,4 +1,4 @@
-using System;
+using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
@@ -227,8 +227,10 @@ namespace Umbraco.Cms.Web.BackOffice.Controllers
return _dashboardService.GetDashboards(section, currentUser).Select(x => new Tab
{
Id = x.Id,
- Alias = x.Alias,
+ Key = x.Key,
Label = x.Label,
+ Alias = x.Alias,
+ Type = x.Type,
Expanded = x.Expanded,
IsActive = x.IsActive,
Properties = x.Properties.Select(y => new DashboardSlim
diff --git a/src/Umbraco.Web.BackOffice/EmbeddedResources/Tours/getting-started.json b/src/Umbraco.Web.BackOffice/EmbeddedResources/Tours/getting-started.json
index f82ad96c55..10817d7ef0 100644
--- a/src/Umbraco.Web.BackOffice/EmbeddedResources/Tours/getting-started.json
+++ b/src/Umbraco.Web.BackOffice/EmbeddedResources/Tours/getting-started.json
@@ -152,10 +152,16 @@
"title": "Enter a description",
"content": "
A description helps to pick the right document type when creating content.
Write a description for our Home page. It could be:
The home page of the website
"
},
+ {
+ "element": "[data-element='groups-builder']",
+ "elementPreventClick": true,
+ "title": "Properties, groups, and tabs",
+ "content": "A Document Type consist of Properties (data fields/attributes) where an editor can input data. For complex Document Types you can organize Properties in groups and tabs."
+ },
{
"element": "[data-element='group-add']",
"title": "Add group",
- "content": "Group are used to organize properties on content in the Content section. Click Add Group to add a group.",
+ "content": "In this tour we only need a group. Click Add Group to add a group.",
"event": "click"
},
{
@@ -274,9 +280,9 @@
"view": "nodename"
},
{
- "element": "[data-element='editor-content'] [data-element='property-welcomeText']",
+ "element": "[data-element='editor-content'] [data-element='property-welcomeText'] > div",
"title": "Add a welcome text",
- "content": "
Add content to the Welcome Text field.
If you don't have any ideas here is a start:
I am learning Umbraco. High Five I Rock #H5IR
."
+ "content": "
Add content to the Welcome Text field.
If you don't have any ideas here is a start:
I am learning Umbraco. High Five I Rock #H5IR
"
},
{
"element": "[data-element='editor-content'] [data-element='button-saveAndPublish']",
diff --git a/src/Umbraco.Web.BackOffice/Filters/OutgoingEditorModelEventAttribute.cs b/src/Umbraco.Web.BackOffice/Filters/OutgoingEditorModelEventAttribute.cs
index 5bda5eb936..8899499887 100644
--- a/src/Umbraco.Web.BackOffice/Filters/OutgoingEditorModelEventAttribute.cs
+++ b/src/Umbraco.Web.BackOffice/Filters/OutgoingEditorModelEventAttribute.cs
@@ -1,9 +1,9 @@
using System;
+using System.Collections;
using System.Collections.Generic;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Filters;
using Umbraco.Cms.Core.Dashboards;
-using Umbraco.Cms.Core.Editors;
using Umbraco.Cms.Core.Events;
using Umbraco.Cms.Core.Models.ContentEditing;
using Umbraco.Cms.Core.Notifications;
@@ -54,25 +54,37 @@ namespace Umbraco.Cms.Web.BackOffice.Filters
if (context.Result is ObjectResult objectContent)
{
- var model = objectContent.Value;
-
- switch (model)
+ // Support both batch (dictionary) and single results
+ IEnumerable models;
+ if (objectContent.Value is IDictionary modelDictionary)
{
- case ContentItemDisplay content:
- _eventAggregator.Publish(new SendingContentNotification(content, umbracoContext));
- break;
- case MediaItemDisplay media:
- _eventAggregator.Publish(new SendingMediaNotification(media, umbracoContext));
- break;
- case MemberDisplay member:
- _eventAggregator.Publish(new SendingMemberNotification(member, umbracoContext));
- break;
- case UserDisplay user:
- _eventAggregator.Publish(new SendingUserNotification(user, umbracoContext));
- break;
- case IEnumerable> dashboards:
- _eventAggregator.Publish(new SendingDashboardsNotification(dashboards, umbracoContext));
- break;
+ models = modelDictionary.Values;
+ }
+ else
+ {
+ models = new[] { objectContent.Value };
+ }
+
+ foreach (var model in models)
+ {
+ switch (model)
+ {
+ case ContentItemDisplay content:
+ _eventAggregator.Publish(new SendingContentNotification(content, umbracoContext));
+ break;
+ case MediaItemDisplay media:
+ _eventAggregator.Publish(new SendingMediaNotification(media, umbracoContext));
+ break;
+ case MemberDisplay member:
+ _eventAggregator.Publish(new SendingMemberNotification(member, umbracoContext));
+ break;
+ case UserDisplay user:
+ _eventAggregator.Publish(new SendingUserNotification(user, umbracoContext));
+ break;
+ case IEnumerable> dashboards:
+ _eventAggregator.Publish(new SendingDashboardsNotification(dashboards, umbracoContext));
+ break;
+ }
}
}
}
diff --git a/src/Umbraco.Web.BackOffice/Trees/ContentTreeController.cs b/src/Umbraco.Web.BackOffice/Trees/ContentTreeController.cs
index 6f50182092..011cbe74e6 100644
--- a/src/Umbraco.Web.BackOffice/Trees/ContentTreeController.cs
+++ b/src/Umbraco.Web.BackOffice/Trees/ContentTreeController.cs
@@ -251,7 +251,7 @@ namespace Umbraco.Cms.Web.BackOffice.Trees
var culture = queryStrings["culture"].TryConvertTo();
//if this is null we'll set it to the default.
- var cultureVal = (culture.Success ? culture.Result : null) ?? _localizationService.GetDefaultLanguageIsoCode();
+ var cultureVal = (culture.Success ? culture.Result : null).IfNullOrWhiteSpace(_localizationService.GetDefaultLanguageIsoCode());
// set names according to variations
foreach (var entity in result.Value)
diff --git a/src/Umbraco.Web.UI.Client/.eslintrc b/src/Umbraco.Web.UI.Client/.eslintrc
index a4010917fb..b3e410109e 100644
--- a/src/Umbraco.Web.UI.Client/.eslintrc
+++ b/src/Umbraco.Web.UI.Client/.eslintrc
@@ -8,7 +8,7 @@
},
"parserOptions": {
- "ecmaVersion": 6
+ "ecmaVersion": 2018
},
"globals": {
diff --git a/src/Umbraco.Web.UI.Client/package.json b/src/Umbraco.Web.UI.Client/package.json
index 0b869b4b06..8adbdd9d42 100644
--- a/src/Umbraco.Web.UI.Client/package.json
+++ b/src/Umbraco.Web.UI.Client/package.json
@@ -42,7 +42,7 @@
"lazyload-js": "1.0.0",
"moment": "2.22.2",
"ng-file-upload": "12.2.13",
- "nouislider": "15.2.0",
+ "nouislider": "15.4.0",
"npm": "^6.14.7",
"spectrum-colorpicker2": "2.0.8",
"tinymce": "4.9.11",
diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbpasswordtip.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbpasswordtip.directive.js
index 86e1d3d32f..e088d84847 100644
--- a/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbpasswordtip.directive.js
+++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbpasswordtip.directive.js
@@ -7,7 +7,7 @@
controller: UmbPasswordTipController,
controllerAs: 'vm',
template:
- '{{vm.passwordTip}}',
+ '',
bindings: {
passwordVal: "<",
minPwdLength: "<",
@@ -21,6 +21,11 @@
let defaultMinPwdNonAlphaNum = Umbraco.Sys.ServerVariables.umbracoSettings.minimumPasswordNonAlphaNum;
var vm = this;
+
+ vm.passwordNonAlphaTip = '';
+ vm.passwordTip = '';
+ vm.passwordLength = 0;
+
vm.$onInit = onInit;
vm.$onChanges = onChanges;
@@ -36,26 +41,27 @@
if (vm.minPwdNonAlphaNum > 0) {
localizationService.localize('user_newPasswordFormatNonAlphaTip', [vm.minPwdNonAlphaNum]).then(data => {
vm.passwordNonAlphaTip = data;
- updatePasswordTip(0);
+ updatePasswordTip(vm.passwordLength);
});
} else {
vm.passwordNonAlphaTip = '';
- updatePasswordTip(0);
+ updatePasswordTip(vm.passwordLength);
}
}
function onChanges(simpleChanges) {
+
if (simpleChanges.passwordVal) {
- if (simpleChanges.passwordVal.currentValue) {
- updatePasswordTip(simpleChanges.passwordVal.currentValue.length);
- } else {
- updatePasswordTip(0);
- }
+ vm.passwordLength = simpleChanges.passwordVal.currentValue ? simpleChanges.passwordVal.currentValue.length : 0;
+
+ updatePasswordTip(vm.passwordLength);
}
}
const updatePasswordTip = passwordLength => {
+
const remainingLength = vm.minPwdLength - passwordLength;
+
if (remainingLength > 0) {
localizationService.localize('user_newPasswordFormatLengthTip', [remainingLength]).then(data => {
vm.passwordTip = data;
diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/content/edit.controller.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/content/edit.controller.js
index bce797d5c8..4a1988cc27 100644
--- a/src/Umbraco.Web.UI.Client/src/common/directives/components/content/edit.controller.js
+++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/content/edit.controller.js
@@ -223,6 +223,7 @@
//we are editing so get the content item from the server
return $scope.getMethod()($scope.contentId)
.then(function (data) {
+
$scope.content = data;
appendRuntimeData();
@@ -235,10 +236,7 @@
eventsService.emit("content.loaded", { content: $scope.content });
return $q.resolve($scope.content);
-
-
});
-
}
/**
@@ -281,6 +279,7 @@
$scope.page.saveButtonStyle = content.trashed || content.isElement || isBlueprint ? "primary" : "info";
// only create the save/publish/preview buttons if the
// content app is "Conent"
+
if ($scope.activeApp && !contentAppHelper.isContentBasedApp($scope.activeApp)) {
$scope.defaultButton = null;
$scope.subButtons = null;
diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/content/umbtabbedcontent.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/content/umbtabbedcontent.directive.js
index 3aa0470262..ed6d34873c 100644
--- a/src/Umbraco.Web.UI.Client/src/common/directives/components/content/umbtabbedcontent.directive.js
+++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/content/umbtabbedcontent.directive.js
@@ -2,34 +2,66 @@
'use strict';
/** This directive is used to render out the current variant tabs and properties and exposes an API for other directives to consume */
- function tabbedContentDirective($timeout) {
+ function tabbedContentDirective($timeout, $filter, contentEditingHelper, contentTypeHelper) {
function link($scope, $element) {
-
+
var appRootNode = $element[0];
-
+
// Directive for cached property groups.
var propertyGroupNodesDictionary = {};
-
+
var scrollableNode = appRootNode.closest(".umb-scrollable");
- scrollableNode.addEventListener("scroll", onScroll);
- scrollableNode.addEventListener("mousewheel", cancelScrollTween);
-
+
+ $scope.activeTabAlias = null;
+ $scope.tabs = [];
+
+ $scope.$watchCollection('content.tabs', (newValue) => {
+
+ contentTypeHelper.defineParentAliasOnGroups(newValue);
+ contentTypeHelper.relocateDisorientedGroups(newValue);
+
+ // make a collection with only tabs and not all groups
+ $scope.tabs = $filter("filter")(newValue, (tab) => {
+ return tab.type === 1;
+ });
+
+ if ($scope.tabs.length > 0) {
+ // if we have tabs and some groups that doesn't belong to a tab we need to render those on an "Other" tab.
+ contentEditingHelper.registerGenericTab(newValue);
+
+ $scope.setActiveTab($scope.tabs[0]);
+
+ scrollableNode.removeEventListener("scroll", onScroll);
+ scrollableNode.removeEventListener("mousewheel", cancelScrollTween);
+
+ // only trigger anchor scroll when there are no tabs
+ } else {
+ scrollableNode.addEventListener("scroll", onScroll);
+ scrollableNode.addEventListener("mousewheel", cancelScrollTween);
+ }
+ });
+
function onScroll(event) {
-
+
var viewFocusY = scrollableNode.scrollTop + scrollableNode.clientHeight * .5;
-
+
for(var i in $scope.content.tabs) {
var group = $scope.content.tabs[i];
var node = propertyGroupNodesDictionary[group.id];
+
+ if (!node) {
+ return;
+ }
+
if (viewFocusY >= node.offsetTop && viewFocusY <= node.offsetTop + node.clientHeight) {
setActiveAnchor(group);
return;
}
}
-
+
}
-
+
function setActiveAnchor(tab) {
if (tab.active !== true) {
var i = $scope.content.tabs.length;
@@ -39,6 +71,7 @@
tab.active = true;
}
}
+
function getActiveAnchor() {
var i = $scope.content.tabs.length;
while(i--) {
@@ -47,20 +80,22 @@
}
return false;
}
+
function getScrollPositionFor(id) {
if (propertyGroupNodesDictionary[id]) {
- return propertyGroupNodesDictionary[id].offsetTop - 20;// currently only relative to closest relatively positioned parent
+ return propertyGroupNodesDictionary[id].offsetTop - 20;// currently only relative to closest relatively positioned parent
}
return null;
}
+
function scrollTo(id) {
var y = getScrollPositionFor(id);
if (getScrollPositionFor !== null) {
-
+
var viewportHeight = scrollableNode.clientHeight;
var from = scrollableNode.scrollTop;
var to = Math.min(y, scrollableNode.scrollHeight - viewportHeight);
-
+
var animeObject = {_y: from};
$scope.scrollTween = anime({
targets: animeObject,
@@ -74,6 +109,7 @@
}
}
+
function jumpTo(id) {
var y = getScrollPositionFor(id);
if (getScrollPositionFor !== null) {
@@ -81,52 +117,59 @@
scrollableNode.scrollTo(0, y);
}
}
+
function cancelScrollTween() {
if($scope.scrollTween) {
$scope.scrollTween.pause();
}
}
-
+
$scope.registerPropertyGroup = function(element, appAnchor) {
propertyGroupNodesDictionary[appAnchor] = element;
};
-
+
+ $scope.setActiveTab = function(tab) {
+ $scope.activeTabAlias = tab.alias;
+ $scope.tabs.forEach(tab => tab.active = false);
+ tab.active = true;
+ };
+
$scope.$on("editors.apps.appChanged", function($event, $args) {
// if app changed to this app, then we want to scroll to the current anchor
- if($args.app.alias === "umbContent") {
+ if($args.app.alias === "umbContent" && $scope.tabs.length === 0) {
var activeAnchor = getActiveAnchor();
$timeout(jumpTo.bind(null, [activeAnchor.id]));
}
});
-
+
$scope.$on("editors.apps.appAnchorChanged", function($event, $args) {
if($args.app.alias === "umbContent") {
setActiveAnchor($args.anchor);
scrollTo($args.anchor.id);
}
});
-
+
//ensure to unregister from all dom-events
$scope.$on('$destroy', function () {
cancelScrollTween();
scrollableNode.removeEventListener("scroll", onScroll);
scrollableNode.removeEventListener("mousewheel", cancelScrollTween);
});
-
+
}
function controller($scope) {
-
+
//expose the property/methods for other directives to use
this.content = $scope.content;
-
+
if($scope.contentNodeModel) {
$scope.defaultVariant = _.find($scope.contentNodeModel.variants, variant => {
// defaultVariant will never have segment. Wether it has a language or not depends on the setup.
return !variant.segment && ((variant.language && variant.language.isDefault) || (!variant.language));
});
}
-
+
$scope.unlockInvariantValue = function(property) {
property.unlockInvariantValue = !property.unlockInvariantValue;
};
@@ -143,14 +186,14 @@
if (property.unlockInvariantValue) {
return false;
}
-
+
var contentLanguage = $scope.content.language;
var canEditCulture = !contentLanguage ||
// If the property culture equals the content culture it can be edited
property.culture === contentLanguage.culture ||
// A culture-invariant property can only be edited by the default language variant
- (property.culture == null && contentLanguage.isDefault);
+ (property.culture == null && contentLanguage.isDefault);
var canEditSegment = property.segment === $scope.content.segment;
@@ -166,14 +209,15 @@
link: link,
scope: {
content: "=", // in this context the content is the variant model.
- contentNodeModel: "=?" //contentNodeModel is the content model for the node,
+ contentNodeModel: "=?", //contentNodeModel is the content model for the node,
+ contentApp: "=?" // contentApp is the origin app model for this view
}
};
return directive;
}
-
+
angular.module('umbraco.directives').directive('umbTabbedContent', tabbedContentDirective);
})();
diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/content/umbvariantcontent.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/content/umbvariantcontent.directive.js
index 55e66c5706..7663616549 100644
--- a/src/Umbraco.Web.UI.Client/src/common/directives/components/content/umbvariantcontent.directive.js
+++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/content/umbvariantcontent.directive.js
@@ -110,7 +110,7 @@
function onAppChanged(activeApp) {
// disable the name field if the active content app is not "Content" or "Info"
- vm.nameDisabled = (activeApp && !contentAppHelper.isContentBasedApp(activeApp));
+ vm.nameDisabled = (activeApp && !contentAppHelper.isContentBasedApp(activeApp));
}
/**
diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/contenttype/umbcontenttypegroup.component.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/contenttype/umbcontenttypegroup.component.js
new file mode 100644
index 0000000000..82112012c0
--- /dev/null
+++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/contenttype/umbcontenttypegroup.component.js
@@ -0,0 +1,77 @@
+(function () {
+ 'use strict';
+
+ /**
+ * A component to render the content type group
+ */
+
+ function umbContentTypeGroupController() {
+
+ const vm = this;
+
+ vm.updateName = updateName;
+ vm.removeGroup = removeGroup;
+ vm.whenNameFocus = whenNameFocus;
+ vm.whenFocus = whenFocus;
+ vm.changeSortOrderValue = changeSortOrderValue;
+ vm.clickComposition = clickComposition;
+
+ function updateName (group) {
+ if (vm.onUpdateName) {
+ vm.onUpdateName({ group });
+ }
+ }
+
+ function removeGroup () {
+ if (vm.onRemove) {
+ vm.onRemove({ group: vm.group });
+ }
+ }
+
+ function whenNameFocus () {
+ if (vm.onNameFocus) {
+ vm.onNameFocus();
+ }
+ }
+
+ function whenFocus () {
+ if (vm.onFocus) {
+ vm.onFocus();
+ }
+ }
+
+ function changeSortOrderValue () {
+ if (vm.onChangeSortOrderValue) {
+ vm.onChangeSortOrderValue( {group: vm.group});
+ }
+ }
+ function clickComposition (documentTypeId) {
+ if (vm.onClickComposition) {
+ vm.onClickComposition({documentTypeId: documentTypeId});
+ }
+ }
+ }
+
+ const umbContentTypeGroupComponent = {
+ templateUrl: 'views/components/contenttype/umb-content-type-group.html',
+ controllerAs: 'vm',
+ transclude: true,
+ bindings: {
+ group: '<',
+ allowName: '<',
+ onUpdateName: '&',
+ allowRemove: '<',
+ onRemove: '&',
+ sorting: '<',
+ onNameFocus: '&',
+ onFocus: '&',
+ onChangeSortOrderValue: '&',
+ valServerFieldName: '@',
+ valTabAlias: "@",
+ onClickComposition: '&?'
+ },
+ controller: umbContentTypeGroupController
+ };
+
+ angular.module('umbraco.directives').component('umbContentTypeGroup', umbContentTypeGroupComponent);
+})();
diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/contenttype/umbcontenttypegroups.component.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/contenttype/umbcontenttypegroups.component.js
new file mode 100644
index 0000000000..90c83b1a39
--- /dev/null
+++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/contenttype/umbcontenttypegroups.component.js
@@ -0,0 +1,23 @@
+(function () {
+ 'use strict';
+
+ /**
+ * A component to render the content type groups
+ */
+
+ function umbContentTypeGroupsController() {
+
+ const vm = this;
+
+ }
+
+ const umbContentTypeGroupsComponent = {
+ templateUrl: 'views/components/contenttype/umb-content-type-groups.html',
+ controllerAs: 'vm',
+ transclude: true,
+ controller: umbContentTypeGroupsController
+ };
+
+ angular.module('umbraco.directives').component('umbContentTypeGroups', umbContentTypeGroupsComponent);
+
+})();
diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/contenttype/umbcontenttypeproperty.component.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/contenttype/umbcontenttypeproperty.component.js
new file mode 100644
index 0000000000..8f27332ec2
--- /dev/null
+++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/contenttype/umbcontenttypeproperty.component.js
@@ -0,0 +1,54 @@
+(function () {
+ 'use strict';
+
+ /**
+ * A component to render the content type property
+ */
+
+ function umbContentTypePropertyController() {
+
+ const vm = this;
+
+ vm.edit = edit;
+ vm.remove = remove;
+ vm.changeSortOrderValue = changeSortOrderValue;
+
+ function edit () {
+ if (vm.onEdit) {
+ vm.onEdit();
+ }
+ }
+
+ function remove () {
+ if (vm.onRemove) {
+ vm.onRemove({ property: vm.property });
+ }
+ }
+
+ function changeSortOrderValue () {
+ if (vm.onChangeSortOrderValue) {
+ vm.onChangeSortOrderValue( {property: vm.property});
+ }
+ }
+
+ }
+
+ const umbContentTypePropertyComponent = {
+ templateUrl: 'views/components/contenttype/umb-content-type-property.html',
+ bindings: {
+ property: '<',
+ sortable: '<',
+ onEdit: '&',
+ onRemove: '&',
+ onChangeSortOrderValue: '&',
+ valServerFieldAlias: '@',
+ valServerFieldLabel: '@',
+ valTabAlias: '@'
+ },
+ controllerAs: 'vm',
+ controller: umbContentTypePropertyController
+ };
+
+ angular.module('umbraco.directives').component('umbContentTypeProperty', umbContentTypePropertyComponent);
+
+})();
diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/contenttype/umbcontenttypetab.component.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/contenttype/umbcontenttypetab.component.js
new file mode 100644
index 0000000000..6c0e5c8baf
--- /dev/null
+++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/contenttype/umbcontenttypetab.component.js
@@ -0,0 +1,108 @@
+(function () {
+ 'use strict';
+
+ /**
+ * A component to render the content type tab
+ */
+
+ function umbContentTypeTabController($timeout) {
+
+ const vm = this;
+
+ vm.compositionLabelIsVisible = false;
+
+ vm.click = click;
+ vm.removeTab = removeTab;
+ vm.whenFocusName = whenFocusName;
+ vm.whenFocus = whenFocus;
+ vm.changeSortOrderValue = changeSortOrderValue;
+ vm.changeName = changeName;
+ vm.clickComposition = clickComposition;
+ vm.mouseenter = mouseenter;
+ vm.mouseleave = mouseleave;
+
+ let timeout = null;
+
+ function click () {
+ if (vm.onClick) {
+ vm.onClick({ tab: vm.tab });
+ }
+ }
+
+ function removeTab () {
+ if (vm.onRemove) {
+ vm.onRemove({ tab: vm.tab });
+ }
+ }
+
+ function whenFocusName () {
+ if (vm.onFocusName) {
+ vm.onFocusName();
+ }
+ }
+
+ function whenFocus () {
+ if (vm.onFocus) {
+ vm.onFocus();
+ }
+ }
+
+ function changeSortOrderValue () {
+ if (vm.onChangeSortOrderValue) {
+ vm.onChangeSortOrderValue( {tab: vm.tab});
+ }
+ }
+
+ function changeName () {
+ if (vm.onChangeName) {
+ vm.onChangeName({ key: vm.tab.key, name: vm.tab.name });
+ }
+ }
+
+ function clickComposition (documentTypeId) {
+ if (vm.onClickComposition) {
+ vm.onClickComposition({documentTypeId: documentTypeId});
+ }
+ }
+
+ function mouseenter () {
+ if (vm.tab.inherited) {
+ vm.compositionLabelIsVisible = true;
+ $timeout.cancel(timeout);
+ }
+ }
+
+ function mouseleave () {
+ if (vm.tab.inherited) {
+ timeout = $timeout(() => {
+ vm.compositionLabelIsVisible = false;
+ }, 300);
+ }
+ }
+
+ }
+
+ const umbContentTypeTabComponent = {
+ templateUrl: 'views/components/contenttype/umb-content-type-tab.html',
+ controllerAs: 'vm',
+ transclude: true,
+ bindings: {
+ tab: '<',
+ onClick: '&?',
+ onClickComposition: '&?',
+ isOpen: '',
+ allowRemove: '',
+ onRemove: '&?',
+ sorting: '',
+ onFocusName: '&?',
+ onFocus: '&?',
+ onChangeSortOrderValue: '&?',
+ allowChangeName: '',
+ onChangeName: '&?',
+ valServerFieldName: '@'
+ },
+ controller: umbContentTypeTabController
+ };
+
+ angular.module('umbraco.directives').component('umbContentTypeTab', umbContentTypeTabComponent);
+})();
diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbeditorcontentheader.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbeditorcontentheader.directive.js
index 31e51fe115..e43b84c5ab 100644
--- a/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbeditorcontentheader.directive.js
+++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbeditorcontentheader.directive.js
@@ -105,8 +105,8 @@
}
});
- scope.vm.hasVariants = (scope.vm.hasCulture || scope.vm.hasSegments);
- scope.vm.hasSubVariants = (scope.vm.hasCulture && scope.vm.hasSegments);
+ scope.vm.hasVariants = scope.content.variants.length > 1 && (scope.vm.hasCulture || scope.vm.hasSegments);
+ scope.vm.hasSubVariants = scope.content.variants.length > 1 &&(scope.vm.hasCulture && scope.vm.hasSegments);
updateVaraintErrors();
@@ -135,8 +135,10 @@
}
scope.editor.variantApps.forEach((app) => {
+ // only render quick links on the content app if there are no tabs
if (app.alias === "umbContent") {
- app.anchors = scope.editor.content.tabs;
+ const hasTabs = scope.editor.content.tabs && scope.editor.content.tabs.filter(group => group.type === 1).length > 0;
+ app.anchors = hasTabs ? [] : scope.editor.content.tabs;
}
});
@@ -214,6 +216,10 @@
}
};
+ unsubscribe.push(scope.$watch('splitViewOpen', (newVal) => {
+ scope.vm.navigationItemLimit = newVal === true ? 0 : undefined;
+ }));
+
onInit();
scope.$on('$destroy', function () {
diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbeditornavigation.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbeditornavigation.directive.js
index a912eab609..f80770c902 100644
--- a/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbeditornavigation.directive.js
+++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbeditornavigation.directive.js
@@ -5,11 +5,13 @@
function link(scope) {
+ const unsubscribe = [];
+
scope.showNavigation = true;
scope.showMoreButton = false;
scope.showDropdown = false;
scope.overflowingItems = 0;
- scope.itemsLimit = 6;
+ scope.itemsLimit = Number.isInteger(scope.limit) ? scope.limit : 6;
scope.moreButton = {
alias: "more",
@@ -47,31 +49,39 @@
function onInit() {
var firstRun = true;
- scope.$watch("navigation.length",
- (newVal, oldVal) => {
- if (firstRun || newVal !== undefined && newVal !== oldVal) {
- firstRun = false;
- scope.showNavigation = newVal > 1;
- calculateVisibleItems($window.innerWidth);
- }
- });
+ calculateVisibleItems($window.innerWidth);
+;
+ unsubscribe.push(scope.$watch("navigation", (newVal, oldVal) => {
+ const newLength = newVal.length;
+ const oldLength = oldVal.length;
+
+ if (firstRun || newLength !== undefined && newLength !== oldLength) {
+ firstRun = false;
+ scope.showNavigation = newLength > 1;
+ calculateVisibleItems($window.innerWidth);
+ }
+
+ setMoreButtonErrorState();
+ }, true));
}
function calculateVisibleItems(windowWidth) {
-
// if we don't get a windowWidth stick with the default item limit
if (!windowWidth) {
return;
}
- scope.itemsLimit = 0;
+ // if we haven't set a specific limit prop we base the amount of visible items on the window width
+ if (scope.limit === undefined) {
+ scope.itemsLimit = 0;
- // set visible items based on browser width
- if (windowWidth > 1500) {
- scope.itemsLimit = 6;
- }
- else if (windowWidth > 700) {
- scope.itemsLimit = 4;
+ // set visible items based on browser width
+ if (windowWidth > 1500) {
+ scope.itemsLimit = 6;
+ }
+ else if (windowWidth > 700) {
+ scope.itemsLimit = 4;
+ }
}
// toggle more button
@@ -82,6 +92,10 @@
scope.showMoreButton = false;
scope.overflowingItems = 0;
}
+
+ scope.moreButton.name = scope.itemsLimit === 0 ? "Menu" : "More";
+ setMoreButtonActiveState();
+ setMoreButtonErrorState();
}
function runItemAction(selectedItem) {
@@ -100,18 +114,30 @@
// set clicked item to active
selectedItem.active = true;
-
- // set more button to active if item in dropdown is clicked
- var selectedItemIndex = scope.navigation.indexOf(selectedItem);
- if (selectedItemIndex + 1 > scope.itemsLimit) {
- scope.moreButton.active = true;
- } else {
- scope.moreButton.active = false;
- }
-
+ setMoreButtonActiveState();
+ setMoreButtonErrorState();
}
}
+ function setMoreButtonActiveState() {
+ // set active state on more button if any of the overflown items is active
+ scope.moreButton.active = scope.navigation.findIndex(item => item.active) + 1 > scope.itemsLimit;
+ };
+
+ function setMoreButtonErrorState() {
+ if (scope.overflowingItems === 0) {
+ return;
+ }
+
+ const overflow = scope.navigation.slice(scope.itemsLimit, scope.navigation.length);
+ const active = scope.navigation.find(item => item.active)
+ // set error state on more button if any of the overflown items has an error. We use it show the error badge and color the item
+ scope.moreButton.hasError = overflow.filter(item => item.hasError).length > 0;
+ // set special active/error state on button if the current selected item is has an error
+ // we don't want to show the error badge in this case so we need a special state for that
+ scope.moreButton.activeHasError = active.hasError;
+ };
+
var resizeCallback = size => {
if (size && size.width) {
calculateVisibleItems(size.width);
@@ -120,13 +146,20 @@
windowResizeListener.register(resizeCallback);
- //ensure to unregister from all events and kill jquery plugins
+ unsubscribe.push(scope.$watch('limit', (newVal) => {
+ scope.itemsLimit = newVal;
+ calculateVisibleItems($window.innerWidth);
+ }));
+
scope.$on('$destroy', function () {
windowResizeListener.unregister(resizeCallback);
+
+ for (var u in unsubscribe) {
+ unsubscribe[u]();
+ }
});
onInit();
-
}
var directive = {
@@ -136,7 +169,8 @@
scope: {
navigation: "=",
onSelect: "&",
- onAnchorSelect: "&"
+ onAnchorSelect: "&",
+ limit: "<"
},
link: link
};
diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbeditortabbar.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbeditortabbar.directive.js
new file mode 100644
index 0000000000..17e0bbf43e
--- /dev/null
+++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbeditortabbar.directive.js
@@ -0,0 +1,22 @@
+(function () {
+ 'use strict';
+
+ /**
+ * A component to render the editor tab bar
+ */
+
+ function umbEditorTabBarController() {
+
+ const vm = this;
+
+ }
+
+ const umbEditorTabBarComponent = {
+ templateUrl: 'views/components/editor/umb-editor-tab-bar.html',
+ controllerAs: 'vm',
+ transclude: true,
+ controller: umbEditorTabBarController
+ };
+
+ angular.module('umbraco.directives').component('umbEditorTabBar', umbEditorTabBarComponent);
+})();
diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/umbautoresize.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/umbautoresize.directive.js
index 2ec3960a59..58c9be1121 100644
--- a/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/umbautoresize.directive.js
+++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/umbautoresize.directive.js
@@ -17,6 +17,12 @@ angular.module("umbraco.directives")
}
}
+ // if the element is hidden the width will be 0 even though it has a value.
+ // This could happen if the element is hidden in a tab.
+ if (ngModelController.$modelValue && domEl.clientWidth === 0) {
+ element.width('auto');
+ }
+
if (!ngModelController.$modelValue && attr.placeholder) {
attr.$set('size', attr.placeholder.length);
element.width('auto');
@@ -25,39 +31,25 @@ angular.module("umbraco.directives")
}
function resizeTextarea() {
-
if (domEl.scrollHeight !== domEl.clientHeight) {
-
element.height(domEl.scrollHeight);
-
}
-
}
var update = function (force) {
-
-
if (force === true) {
-
if (domElType === "textarea") {
element.height(0);
} else if (domElType === "text") {
element.width(0);
}
-
}
-
if (domElType === "textarea") {
-
resizeTextarea();
-
} else if (domElType === "text") {
-
resizeInput();
-
}
-
};
//listen for tab changes
diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/umbcheckbox.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/umbcheckbox.directive.js
index 717cefbb0a..a4508c8879 100644
--- a/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/umbcheckbox.directive.js
+++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/umbcheckbox.directive.js
@@ -41,7 +41,7 @@
(function () {
'use strict';
- function UmbCheckboxController($timeout, localizationService) {
+ function UmbCheckboxController($timeout, $attrs, localizationService) {
var vm = this;
@@ -50,7 +50,12 @@
function onInit() {
vm.inputId = vm.inputId || "umb-check_" + String.CreateGuid();
-
+ vm.disableDirtyCheck =
+ $attrs.hasOwnProperty("disableDirtyCheck") &&
+ vm.disableDirtyCheck !== '0' &&
+ vm.disableDirtyCheck !== 0 &&
+ vm.disableDirtyCheck !== 'false' &&
+ vm.disableDirtyCheck !== false;
vm.icon = vm.icon || vm.iconClass || null;
// If a labelKey is passed let's update the returned text if it's does not contain an opening square bracket [
diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/umbradiobutton.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/umbradiobutton.directive.js
index 8c7157c414..dd0d2fc31b 100644
--- a/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/umbradiobutton.directive.js
+++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/umbradiobutton.directive.js
@@ -40,7 +40,7 @@
(function () {
'use strict';
- function UmbRadiobuttonController($timeout, localizationService) {
+ function UmbRadiobuttonController($timeout, $attrs, localizationService) {
var vm = this;
@@ -49,7 +49,12 @@
function onInit() {
vm.inputId = vm.inputId || "umb-radio_" + String.CreateGuid();
-
+ vm.disableDirtyCheck =
+ $attrs.hasOwnProperty("disableDirtyCheck") &&
+ vm.disableDirtyCheck !== '0' &&
+ vm.disableDirtyCheck !== 0 &&
+ vm.disableDirtyCheck !== 'false' &&
+ vm.disableDirtyCheck !== false;
vm.icon = vm.icon || vm.iconClass || null;
// If a labelKey is passed let's update the returned text if it's does not contain an opening square bracket [
diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/umbsearchfilter.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/umbsearchfilter.directive.js
index 5eb22dcecd..2d0d9eda95 100644
--- a/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/umbsearchfilter.directive.js
+++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/umbsearchfilter.directive.js
@@ -25,9 +25,11 @@
@param {string} inputId Set the id of the checkbox.
@param {string} text Set the text for the checkbox label.
@param {string} labelKey Set a dictionary/localization string for the checkbox label
-@param {callback} onChange Callback when the value of the checkbox change by interaction.
+@param {callback} onChange Callback when the value of the searchbox change.
+@param {callback} onBack Callback when clicking back button.
@param {boolean} autoFocus Add autofocus to the input field
@param {boolean} preventSubmitOnEnter Set the enter prevent directive or not
+@param {boolean} showBackButton Show back button on search
**/
@@ -42,15 +44,17 @@
vm.change = change;
vm.keyDown = keyDown;
vm.blur = blur;
+ vm.goBack = goBack;
function onInit() {
vm.inputId = vm.inputId || "umb-search-filter_" + String.CreateGuid();
vm.autoFocus = Object.toBoolean(vm.autoFocus) === true;
vm.preventSubmitOnEnter = Object.toBoolean(vm.preventSubmitOnEnter) === true;
+ vm.showBackButton = Object.toBoolean(vm.showBackButton) === true;
// If a labelKey is passed let's update the returned text if it's does not contain an opening square bracket [
if (vm.labelKey) {
- localizationService.localize(vm.labelKey).then(function (data) {
+ localizationService.localize(vm.labelKey).then(data => {
if (data.indexOf('[') === -1){
vm.text = data;
}
@@ -58,6 +62,12 @@
}
}
+ function goBack() {
+ if (vm.onBack) {
+ vm.onBack();
+ }
+ }
+
function change() {
if (vm.onChange) {
$timeout(function () {
@@ -97,8 +107,10 @@
onChange: "&?",
onSearch: "&?",
onBlur: "&?",
+ onBack: "&?",
autoFocus: "",
preventSubmitOnEnter: "",
+ showBackButton: "",
cssClass: "@?"
}
};
diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/imaging/umbimagegravity.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/imaging/umbimagegravity.directive.js
index 277848811b..79a6e39fb4 100644
--- a/src/Umbraco.Web.UI.Client/src/common/directives/components/imaging/umbimagegravity.directive.js
+++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/imaging/umbimagegravity.directive.js
@@ -25,6 +25,7 @@
vm.style = {};
vm.overlayStyle = {};
vm.setFocalPoint = setFocalPoint;
+ vm.resetFocalPoint = resetFocalPoint;
/** Sets the css style for the Dot */
function updateStyle() {
@@ -39,6 +40,13 @@
};
+ function resetFocalPoint() {
+ vm.onValueChanged({
+ left: 0.5,
+ top: 0.5
+ });
+ };
+
function setFocalPoint(event) {
$scope.$emit("imageFocalPointStart");
diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/tabs/umbtabsnav.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/tabs/umbtabsnav.directive.js
index 7a10ff51b5..30a0f5140e 100644
--- a/src/Umbraco.Web.UI.Client/src/common/directives/components/tabs/umbtabsnav.directive.js
+++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/tabs/umbtabsnav.directive.js
@@ -90,7 +90,7 @@ Use this directive to render a tabs navigation.
(function() {
'use strict';
- function TabsNavDirective($timeout, $window) {
+ function TabsNavDirective($timeout, $window, eventsService) {
function link(scope, element, attrs, ctrl) {
@@ -98,11 +98,16 @@ Use this directive to render a tabs navigation.
// the parent is the component itself so we need to go one level higher
var container = element.parent().parent();
+ const ro = new ResizeObserver(function () {
+ calculateWidth();
+ });
+
+ ro.observe(container[0]);
+
$timeout(function(){
element.find("li:not(umb-tab--expand)").each(function() {
tabNavItemsWidths.push($(this).outerWidth());
});
- calculateWidth();
});
function calculateWidth(){
@@ -131,14 +136,9 @@ Use this directive to render a tabs navigation.
});
}
- $(window).on('resize.tabsNav', function () {
- calculateWidth();
- });
-
scope.$on('$destroy', function() {
- $(window).off('resize.tabsNav');
+ ro.unobserve(container[0]);
});
-
}
function UmbTabsNavController(eventsService) {
diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/tree/umbtreesearchbox.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/tree/umbtreesearchbox.directive.js
index 2ae17fdc6b..1067a3e440 100644
--- a/src/Umbraco.Web.UI.Client/src/common/directives/components/tree/umbtreesearchbox.directive.js
+++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/tree/umbtreesearchbox.directive.js
@@ -5,7 +5,7 @@
* @element ANY
* @restrict E
**/
-function treeSearchBox(localizationService, searchService, $q) {
+function treeSearchBox($q, searchService) {
return {
scope: {
searchFromId: "@",
@@ -24,20 +24,16 @@ function treeSearchBox(localizationService, searchService, $q) {
link: function (scope, element, attrs, ctrl) {
scope.term = "";
+
scope.hideSearch = function() {
scope.term = "";
scope.hideSearchCallback();
};
- localizationService.localize("general_typeToSearch").then(function (value) {
- scope.searchPlaceholderText = value;
- });
-
if (!scope.showSearch) {
scope.showSearch = "false";
}
-
//used to cancel any request in progress if another one needs to take it's place
var canceler = null;
diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbcolorpicker.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbcolorpicker.directive.js
index a6a26dbe57..cf424ec99a 100644
--- a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbcolorpicker.directive.js
+++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbcolorpicker.directive.js
@@ -63,11 +63,13 @@
@param {string} ngModel (binding): Value for the color picker.
@param {object} options (binding): Config object for the color picker.
-@param {function} onBeforeShow (expression): Callback function before color picker is shown.
-@param {function} onChange (expression): Callback function when the color is changed.
-@param {function} onShow (expression): Callback function when color picker is shown.
-@param {function} onHide (expression): Callback function when color picker is hidden.
-@param {function} onMove (expression): Callback function when the color is moved in color picker.
+@param {function} onBeforeShow (expression): You can prevent the color picker from showing up if you return false in the beforeShow event. This event is ignored on a flat color picker.
+@param {function} onChange (expression): Called as the original input changes. Only happens when the input is closed or the 'Choose' button is clicked.
+@param {function} onShow (expression): Called after the color picker is opened. This is ignored on a flat color picker. Note, when any color picker on the page is shown it will hide any that are already open.
+@param {function} onHide (expression): Called after the color picker is hidden. This happens when clicking outside of the picker while it is open. Note, when any color picker on the page is shown it will hide any that are already open. This event is ignored on a flat color picker.
+@param {function} onMove (expression): Called as the user moves around within the color picker.
+@param {function} onDragStart (expression): Called at the beginning of a drag event on either hue slider, alpha slider, or main color picker areas.
+@param {function} onDragStop (expression): Called at the end of a drag event on either hue slider, alpha slider, or main color picker areas.
**/
@@ -220,6 +222,24 @@
});
}
+ // bind hook for drag start
+ if (ctrl.onDragStart) {
+ colorPickerInstance.on('dragstart.spectrum', (e, tinycolor) => {
+ $timeout(function () {
+ ctrl.onDragStart({ color: tinycolor });
+ });
+ });
+ }
+
+ // bind hook for drag stop
+ if (ctrl.onDragStop) {
+ colorPickerInstance.on('dragstop.spectrum', (e, tinycolor) => {
+ $timeout(function () {
+ ctrl.onDragStop({ color: tinycolor });
+ });
+ });
+ }
+
}
}
}
@@ -232,11 +252,13 @@
bindings: {
ngModel: '<',
options: '<',
- onBeforeShow: '&',
- onShow: '&',
- onHide: '&',
- onChange: '&',
- onMove: '&'
+ onBeforeShow: '&?',
+ onShow: '&?',
+ onHide: '&?',
+ onChange: '&?',
+ onMove: '&?',
+ onDragStart: '&?',
+ onDragStop: '&?'
}
});
diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbconfirmaction.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbconfirmaction.directive.js
index 1dcccda481..665b837946 100644
--- a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbconfirmaction.directive.js
+++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbconfirmaction.directive.js
@@ -14,7 +14,7 @@ The prompt can be opened in four direction up, down, left or right.
-
+ {
- addInitProperty(group);
- });
+ eventBindings.push(scope.$watchCollection('model.groups', (newValue, oldValue) => {
+ // we only want to run this logic when new groups are added or removed
+ if (newValue.length === oldValue.length && tabsInitialized) {
+ tabsInitialized = true;
+ return;
}
- // add init tab
- addInitGroup(scope.model.groups);
+ contentTypeHelper.defineParentAliasOnGroups(newValue);
+ contentTypeHelper.relocateDisorientedGroups(newValue);
- activateFirstGroup(scope.model.groups);
-
- var labelKeys = [
- "validation_validation",
- "contentTypeEditor_tabHasNoSortOrder"
- ];
+ scope.tabs = $filter("filter")(newValue, group => {
+ return group.type === TYPE_TAB && group.parentAlias == null;
+ });
+
+ // order tabs
+ scope.orderTabs();
+
+ // set server validation index
+ // the server filters out inherited groups if they don't have any local properties when returning the group index
+ const noInherited = newValue.filter(group => !group.inherited || (group.inherited && group.properties.filter(property => !property.inherited).length > 0));
+
+ noInherited.forEach((group, index) => {
+ group.serverValidationIndex = !group.inherited ? index : undefined;
+ });
+
+ checkGenericTabVisibility();
+
+ if (!scope.openTabAlias && scope.hasGenericTab) {
+ scope.openTabAlias = null;
+ } else if (!scope.openTabAlias && scope.tabs.length > 0) {
+ scope.openTabAlias = scope.tabs[0].alias;
+ }
+
+ tabsInitialized = true;
+ }));
+
+ function activate() {
+ setSortingOptions();
// localize texts
- localizationService.localizeMany(labelKeys).then(data => {
+ localizationService.localizeMany([
+ "validation_validation",
+ "contentTypeEditor_tabHasNoSortOrder",
+ "general_generic",
+ "contentTypeEditor_tabDirectPropertiesDropZone"
+ ]).then(data => {
validationTranslated = data[0];
tabNoSortOrderTranslated = data[1];
+ scope.genericTab.name = data[2];
+ scope.tabDirectPropertiesDropZoneLabel = data[3];
});
}
function setSortingOptions() {
- scope.sortableOptionsGroup = {
- axis: 'y',
- distance: 10,
+ const defaultOptions = {
+ axis: '',
tolerance: "pointer",
opacity: 0.7,
scroll: true,
cursor: "move",
- placeholder: "umb-group-builder__group-sortable-placeholder",
zIndex: 6000,
+ forcePlaceholderSize: true,
+ dropOnEmpty: true,
+ helper: "clone",
+ appendTo: "body"
+ };
+
+ scope.sortableOptionsTab = {
+ ...defaultOptions,
+ connectWith: ".umb-group-builder__tabs",
+ placeholder: "umb-group-builder__tab-sortable-placeholder",
+ handle: ".umb-group-builder__tab-handle",
+ items: ".umb-group-builder__tab-sortable",
+ stop: (event, ui) => {
+ const tabKey = ui.item[0].dataset.tabKey ? ui.item[0].dataset.tabKey : false;
+ const dropIndex = scope.tabs.findIndex(tab => tab.key === tabKey);
+ updateSortOrder(scope.tabs, dropIndex);
+ }
+ };
+
+ scope.sortableOptionsGroup = {
+ ...defaultOptions,
+ connectWith: ".umb-group-builder__groups",
+ placeholder: "umb-group-builder__group-sortable-placeholder",
handle: ".umb-group-builder__group-handle",
items: ".umb-group-builder__group-sortable",
- start: function (e, ui) {
- ui.placeholder.height(ui.item.height());
- },
- stop: function (e, ui) {
- updateTabsSortOrder();
+ stop: (e, ui) => {
+ const groupKey = ui.item[0].dataset.groupKey ? ui.item[0].dataset.groupKey : false;
+ const group = groupKey ? scope.model.groups.find(group => group.key === groupKey) : {};
+
+ // the code also runs when you convert a group to a tab.
+ // We want to make sure it only run when groups are reordered
+ if (group && group.type === TYPE_GROUP) {
+
+ // Update aliases
+ const parentAlias = scope.openTabAlias;
+ const oldAlias = group.alias || null; // null when group comes from root aka. 'generic'
+ const newAlias = contentTypeHelper.updateParentAlias(oldAlias, parentAlias);
+
+ group.alias = newAlias;
+ group.parentAlias = parentAlias;
+ contentTypeHelper.updateDescendingAliases(scope.model.groups, oldAlias, newAlias);
+
+ const groupsInTab = scope.model.groups.filter(group => group.parentAlias === parentAlias);
+ const dropIndex = groupsInTab.findIndex(group => group.key === groupKey);
+
+ updateSortOrder(groupsInTab, dropIndex);
+ }
}
};
scope.sortableOptionsProperty = {
- axis: 'y',
- distance: 10,
- tolerance: "pointer",
+ ...defaultOptions,
connectWith: ".umb-group-builder__properties",
- opacity: 0.7,
- scroll: true,
- cursor: "move",
placeholder: "umb-group-builder__property_sortable-placeholder",
- zIndex: 6000,
handle: ".umb-group-builder__property-handle",
items: ".umb-group-builder__property-sortable",
- start: function (e, ui) {
- ui.placeholder.height(ui.item.height());
- },
- stop: function (e, ui) {
+ stop: (e, ui) => {
updatePropertiesSortOrder();
}
};
- }
+ scope.droppableOptionsConvert = {
+ accept: '.umb-group-builder__group-sortable',
+ tolerance : 'pointer',
+ drop: (evt, ui) => {
+ const groupKey = ui.draggable[0].dataset.groupKey ? ui.draggable[0].dataset.groupKey : false;
+ const group = groupKey ? scope.model.groups.find(group => group.key === groupKey) : {};
- function updateTabsSortOrder() {
+ if (group) {
+ contentTypeHelper.convertGroupToTab(scope.model.groups, group);
- var first = true;
- var prevSortOrder = 0;
+ scope.tabs.push(group);
+ scope.$broadcast('umbOverflowChecker.checkOverflow');
+ scope.$broadcast('umbOverflowChecker.scrollTo', { position: 'end' });
+ }
+ }
+ };
- scope.model.groups.map(function (group) {
+ scope.sortableRequestedTabAlias = undefined;//set to undefined as null is the generic group.
+ scope.sortableRequestedTabTimeout = null;
+ scope.droppableOptionsTab = {
+ accept: '.umb-group-builder__property-sortable, .umb-group-builder__group-sortable',
+ tolerance : 'pointer',
+ over: (evt, ui) => {
+ const hoveredTabAlias = evt.target.dataset.tabAlias === "" ? null : evt.target.dataset.tabAlias;
- var index = scope.model.groups.indexOf(group);
+ // if dragging a group
+ if (ui.draggable[0].dataset.groupKey) {
- if (group.tabState !== "init") {
+ const groupKey = ui.draggable[0].dataset.groupKey ? ui.draggable[0].dataset.groupKey : false;
+ const group = groupKey ? scope.model.groups.find(group => group.key === groupKey) : {};
- // set the first not inherited tab to sort order 0
- if (!group.inherited && first) {
-
- // set the first tab sort order to 0 if prev is 0
- if (prevSortOrder === 0) {
- group.sortOrder = 0;
- // when the first tab is inherited and sort order is not 0
- } else {
- group.sortOrder = prevSortOrder + 1;
+ const newAlias = contentTypeHelper.updateParentAlias(group.alias || null, hoveredTabAlias);
+ // Check alias is unique
+ if (group.alias !== newAlias && contentTypeHelper.isAliasUnique(scope.model.groups, newAlias) === false) {
+ // TODO: Missing UI indication of why you cant move here.
+ return;
}
-
- first = false;
-
- } else if (!group.inherited && !first) {
-
- // find next group
- var nextGroup = scope.model.groups[index + 1];
-
- // if a groups is dropped in the middle of to groups with
- // same sort order. Give it the dropped group same sort order
- if (prevSortOrder === nextGroup.sortOrder) {
- group.sortOrder = prevSortOrder;
- } else {
- group.sortOrder = prevSortOrder + 1;
- }
-
}
- // store this tabs sort order as reference for the next
- prevSortOrder = group.sortOrder;
+ if(scope.sortableRequestedTabAlias !== hoveredTabAlias) {
+ if(scope.sortableRequestedTabTimeout !== null) {
+ $timeout.cancel(scope.sortableRequestedTabTimeout);
+ scope.sortableRequestedTabTimeout = null;
+ scope.sortableRequestedTabAlias = undefined;
+ }
+ scope.sortableRequestedTabAlias = hoveredTabAlias;
+ scope.sortableRequestedTabTimeout = $timeout(() => {
+ scope.openTabAlias = scope.sortableRequestedTabAlias;
+ scope.sortableRequestedTabTimeout = null;
+ /* hack to update sortable positions when switching from one tab to another.
+ without this sorting direct properties doesn't work correctly */
+ scope.$apply();
+ $('.umb-group-builder__ungrouped-properties .umb-group-builder__properties').sortable('refresh');
+ $('.umb-group-builder__groups').sortable('refresh');
+ }, 400);
+ }
+ },
+ out: (evt, ui) => {
+ const hoveredTabAlias = evt.target.dataset.tabAlias === "" ? null : evt.target.dataset.tabAlias;
+ if(scope.sortableRequestedTabTimeout !== null && (hoveredTabAlias === undefined || scope.sortableRequestedTabAlias === hoveredTabAlias)) {
+ $timeout.cancel(scope.sortableRequestedTabTimeout);
+ scope.sortableRequestedTabTimeout = null;
+ scope.sortableRequestedTabAlias = null;
+ }
}
+ };
+ }
- });
+ function updateSortOrder(items, movedIndex) {
+ if (items && items.length <= 1) {
+ return;
+ }
+
+ // update the moved item sort order to fit into where it is dragged
+ const movedItem = items[movedIndex];
+ if (movedIndex === 0) {
+ const nextItem = items[movedIndex + 1];
+ movedItem.sortOrder = nextItem.sortOrder - 1;
+ } else {
+ const prevItem = items[movedIndex - 1];
+ movedItem.sortOrder = prevItem.sortOrder + 1;
+ }
+
+ /* After the above two items next to each other might have the same sort order
+ to prevent this we run through the rest of the
+ items and update the sort order if they are next to each other.
+ This will make it possible to make gaps without the number being updated */
+ for (let i = movedIndex; i < items.length; i++) {
+ const item = items[i];
+
+ if (!item.inherited && i !== 0) {
+ const prev = items[i - 1];
+
+ if (item.sortOrder === prev.sortOrder) {
+ item.sortOrder = item.sortOrder + 1;
+ }
+ }
+ }
}
function filterAvailableCompositions(selectedContentType, selecting) {
@@ -145,23 +267,23 @@
//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) {
+ _.reject(scope.compositionsDialogModel.compositeContentTypes, 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) {
+ var propAliasesExisting = _.filter(_.flatten(_.map(scope.model.groups, g => {
+ return _.map(g.properties, p => {
return p.alias;
});
- })), function (f) {
+ })), 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) {
+ return resourceLookup(scope.model.id, selectedContentTypeAliases, propAliasesExisting).then(filteredAvailableCompositeTypes => {
scope.compositionsDialogModel.availableCompositeContentTypes.forEach(current => {
//reset first
current.allowed = true;
@@ -179,12 +301,7 @@
}
function updatePropertiesSortOrder() {
-
- Utilities.forEach(scope.model.groups, group => {
- if (group.tabState !== "init") {
- group.properties = contentTypeHelper.updatePropertiesSortOrder(group.properties);
- }
- });
+ scope.model.groups.forEach(group => group.properties = contentTypeHelper.updatePropertiesSortOrder(group.properties));
}
function setupAvailableContentTypesModel(result) {
@@ -208,17 +325,17 @@
/* ---------- DELETE PROMT ---------- */
- scope.togglePrompt = function (object) {
+ scope.togglePrompt = object => {
object.deletePrompt = !object.deletePrompt;
};
- scope.hidePrompt = function (object) {
+ scope.hidePrompt = object => {
object.deletePrompt = false;
};
/* ---------- TOOLBAR ---------- */
- scope.toggleSortingMode = function (tool) {
+ scope.toggleSortingMode = () => {
if (scope.sortingMode === true) {
@@ -238,15 +355,21 @@
scope.sortingButtonKey = "general_reorder";
}
- } else {
+ // When exiting the reorder mode while the generic tab is empty, set the active tab to the first available one
+ if (scope.tabs.length > 0 && !scope.openTabAlias) {
+ scope.openTabAlias = scope.tabs[0].alias;
+ }
+ } else {
scope.sortingMode = true;
scope.sortingButtonKey = "general_reorderDone";
}
+ checkGenericTabVisibility();
+ scope.$broadcast('umbOverflowChecker.checkOverflow');
};
- scope.openCompositionsDialog = function () {
+ scope.openCompositionsDialog = () => {
scope.compositionsDialogModel = {
contentType: scope.model,
@@ -254,26 +377,14 @@
view: "views/common/infiniteeditors/compositions/compositions.html",
size: "small",
submit: () => {
-
- // make sure that all tabs has an init property
- if (scope.model.groups.length !== 0) {
- Utilities.forEach(scope.model.groups, group => {
- addInitProperty(group);
- });
- }
-
- // remove overlay
editorService.close();
},
close: oldModel => {
-
// reset composition changes
scope.model.groups = oldModel.contentType.groups;
scope.model.compositeContentTypes = oldModel.contentType.compositeContentTypes;
- // remove overlay
editorService.close();
-
},
selectCompositeContentType: selectedContentType => {
@@ -305,11 +416,11 @@
}
//based on the selection, we need to filter the available composite types list
- filterAvailableCompositions(selectedContentType, newSelection).then(function () {
- deferred.resolve({ selectedContentType, newSelection });
- // TODO: Here we could probably re-enable selection if we previously showed a throbber or something
- }, function () {
- deferred.reject();
+ filterAvailableCompositions(selectedContentType, newSelection).then(() => {
+ deferred.resolve({ selectedContentType, newSelection });
+ // TODO: Here we could probably re-enable selection if we previously showed a throbber or something
+ }, () => {
+ deferred.reject();
});
});
}
@@ -318,10 +429,10 @@
contentTypeHelper.splitCompositeContentType(scope.model, selectedContentType);
//based on the selection, we need to filter the available composite types list
- filterAvailableCompositions(selectedContentType, newSelection).then(function () {
- deferred.resolve({ selectedContentType, newSelection });
+ filterAvailableCompositions(selectedContentType, newSelection).then(() => {
+ deferred.resolve({ selectedContentType, newSelection });
// TODO: Here we could probably re-enable selection if we previously showed a throbber or something
- }, function () {
+ }, () => {
deferred.reject();
});
}
@@ -336,31 +447,31 @@
var countContentTypeResource = scope.contentType === "documentType" ? contentTypeResource.getCount : mediaTypeResource.getCount;
//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) {
+ var propAliasesExisting = _.filter(_.flatten(_.map(scope.model.groups, g => {
+ return _.map(g.properties, p => {
return p.alias;
});
- })), function (f) {
+ })), f => {
return f !== null && f !== undefined;
});
scope.compositionsButtonState = "busy";
$q.all([
//get available composite types
- availableContentTypeResource(scope.model.id, [], propAliasesExisting, scope.model.isElement).then(function (result) {
+ availableContentTypeResource(scope.model.id, [], propAliasesExisting, scope.model.isElement).then(result => {
setupAvailableContentTypesModel(result);
}),
//get where used document types
- whereUsedContentTypeResource(scope.model.id).then(function (whereUsed) {
+ whereUsedContentTypeResource(scope.model.id).then(whereUsed => {
//pass to the dialog model the content type eg documentType or mediaType
scope.compositionsDialogModel.section = scope.contentType;
//pass the list of 'where used' document types
scope.compositionsDialogModel.whereCompositionUsed = whereUsed;
}),
//get content type count
- countContentTypeResource().then(function (result) {
+ countContentTypeResource().then(result => {
scope.compositionsDialogModel.totalContentTypes = parseInt(result, 10);
})
- ]).then(function () {
+ ]).then(() => {
//resolves when both other promises are done, now show it
editorService.open(scope.compositionsDialogModel);
scope.compositionsButtonState = "init";
@@ -368,16 +479,15 @@
};
-
- scope.openDocumentType = function (documentTypeId) {
+ scope.openDocumentType = (documentTypeId) => {
const editor = {
id: documentTypeId,
- submit: function (model) {
+ submit: () => {
const args = { node: scope.model };
eventsService.emit("editors.documentType.reload", args);
editorService.close();
},
- close: function () {
+ close: () => {
editorService.close();
}
};
@@ -385,34 +495,227 @@
};
- /* ---------- GROUPS ---------- */
-
- scope.addGroup = function (group) {
-
- // set group sort order
- var index = scope.model.groups.indexOf(group);
- var prevGroup = scope.model.groups[index - 1];
-
- if (index > 0) {
- // set index to 1 higher than the previous groups sort order
- group.sortOrder = prevGroup.sortOrder + 1;
-
- } else {
- // first group - sort order will be 0
- group.sortOrder = 0;
- }
-
- // activate group
- scope.activateGroup(group);
-
- // push new init tab to the scope
- addInitGroup(scope.model.groups);
+ /* ---------- TABS ---------- */
+ scope.changeTab = ({ alias }) => {
+ scope.openTabAlias = alias;
};
- scope.activateGroup = function (selectedGroup) {
+ scope.addTab = () => {
+ const newTabIndex = scope.tabs.length;
+ const lastTab = scope.tabs[newTabIndex - 1];
+ const sortOrder = lastTab && lastTab.sortOrder !== undefined ? lastTab.sortOrder + 1 : 0;
+
+ const key = String.CreateGuid();
+ const tab = {
+ key: key,
+ type: TYPE_TAB,
+ name: '',
+ alias: key, // Temporarily set alias to key, because the name is empty
+ parentAlias: null,
+ sortOrder,
+ properties: []
+ };
+
+ if (newTabIndex === 0 && scope.hasGenericTab === false) {
+ scope.model.groups.forEach(group => {
+ if (!group.inherited && group.parentAlias == null) {
+ group.parentAlias = tab.alias;
+ group.alias = contentTypeHelper.updateParentAlias(group.alias, group.parentAlias);
+ }
+ });
+ }
+
+ scope.model.groups = [...scope.model.groups, tab];
+
+ scope.openTabAlias = tab.alias;
+
+ scope.$broadcast('umbOverflowChecker.checkOverflow');
+ scope.$broadcast('umbOverflowChecker.scrollTo', { position: 'end' });
+ };
+
+ scope.removeTab = (tab, indexInTabs) => {
+ const tabName = tab.name || "";
+
+ const localizeMany = localizationService.localizeMany(['general_delete', 'contentTypeEditor_confirmDeleteTabNotice']);
+ const localize = localizationService.localize('contentTypeEditor_confirmDeleteTabMessage', [tabName]);
+
+ $q.all([localizeMany, localize]).then(values => {
+ const translations = values[0];
+ const message = values[1];
+
+ overlayService.confirmDelete({
+ title: `${translations[0]} ${tabName}`,
+ content: message,
+ confirmMessage: translations[1],
+ submitButtonLabelKey: 'actions_delete',
+ submit: () => {
+ const indexInGroups = scope.model.groups.findIndex(group => group.alias === tab.alias);
+ scope.model.groups.splice(indexInGroups, 1);
+
+ // remove all child groups
+ scope.model.groups = scope.model.groups.filter(group => group.parentAlias !== tab.alias);
+
+ // we need a timeout because the filter hasn't updated the tabs collection
+ $timeout(() => {
+ if (scope.tabs.length > 0) {
+ scope.openTabAlias = indexInTabs > 0 ? scope.tabs[indexInTabs - 1].alias : scope.tabs[0].alias;
+ } else {
+ scope.openTabAlias = null;
+ }
+ });
+
+ scope.$broadcast('umbOverflowChecker.checkOverflow');
+
+ overlayService.close();
+ }
+ });
+ });
+ };
+
+ scope.canRemoveTab = (tab) => {
+ return tab.inherited !== true;
+ };
+
+ scope.setTabOverflowState = (overflowLeft, overflowRight) => {
+ scope.overflow = { left: overflowLeft, right: overflowRight };
+ };
+
+ scope.moveTabsOverflowLeft = () => {
+ //TODO: optimize this...
+ const el = element[0].querySelector(".umb-group-builder__tabs-list");
+ el.scrollLeft -= el.clientWidth * 0.5;
+ }
+ scope.moveTabsOverflowRight = () => {
+ //TODO: optimize this...
+ const el = element[0].querySelector(".umb-group-builder__tabs-list");
+ el.scrollLeft += el.clientWidth * 0.5;
+ }
+
+ scope.orderTabs = () => {
+ scope.tabs = $filter('orderBy')(scope.tabs, 'sortOrder');
+ };
+
+ scope.onChangeTabName = tab => {
+ if (updateGroupAlias(tab)) {
+ scope.openTabAlias = tab.alias;
+ scope.$broadcast('umbOverflowChecker.checkOverflow');
+ }
+ };
+
+ /** Universal method for updating group alias (for tabs, field-sets etc.) */
+ function updateGroupAlias(group) {
+ const localAlias = contentTypeHelper.generateLocalAlias(group.name),
+ oldAlias = group.alias;
+ let newAlias = contentTypeHelper.updateLocalAlias(oldAlias, localAlias);
+
+ // Ensure unique alias, otherwise we would be transforming groups of other parents, we do not want this.
+ if(contentTypeHelper.isAliasUnique(scope.model.groups, newAlias) === false) {
+ newAlias = contentTypeHelper.createUniqueAlias(scope.model.groups, newAlias);
+ }
+
+ group.alias = newAlias;
+ group.parentAlias = contentTypeHelper.getParentAlias(newAlias);
+ contentTypeHelper.updateDescendingAliases(scope.model.groups, oldAlias, newAlias);
+ return true;
+ }
+
+ scope.isUngroupedPropertiesVisible = ({alias, properties}) => {
+ const isOpenTab = alias === scope.openTabAlias;
+
+ if (isOpenTab && properties.length > 0) {
+ return true;
+ }
+
+ if (isOpenTab && scope.sortingMode) {
+ return true;
+ }
+
+ const tabHasGroups = scope.model.groups.filter(group => group.parentAlias === alias).length > 0;
+
+ if (isOpenTab && !tabHasGroups) {
+ return true;
+ }
+ };
+
+ function checkGenericTabVisibility () {
+ const hasRootGroups = scope.model.groups.filter(group => group.type === TYPE_GROUP && group.parentAlias === null).length > 0;
+ scope.hasGenericTab = (hasRootGroups && scope.tabs.length > 0) || scope.sortingMode;
+ }
+
+ /* Properties */
+
+ scope.addNewProperty = group => {
+ let newProperty = {
+ label: null,
+ alias: null,
+ propertyState: "init",
+ validation: {
+ mandatory: false,
+ mandatoryMessage: null,
+ pattern: null,
+ patternMessage: null
+ },
+ labelOnTop: false
+ };
+
+ const propertySettings = {
+ title: "Property settings",
+ property: newProperty,
+ contentType: scope.contentType,
+ contentTypeName: scope.model.name,
+ contentTypeAllowCultureVariant: scope.model.allowCultureVariant,
+ contentTypeAllowSegmentVariant: scope.model.allowSegmentVariant,
+ view: "views/common/infiniteeditors/propertysettings/propertysettings.html",
+ size: "small",
+ submit: model => {
+ newProperty = {...model.property};
+ newProperty.propertyState = "active";
+
+ group.properties.push(newProperty);
+
+ editorService.close();
+ },
+ close: () => {
+ editorService.close();
+ }
+ };
+
+ editorService.open(propertySettings);
+ };
+
+ /* ---------- GROUPS ---------- */
+
+ scope.addGroup = tabAlias => {
+ scope.model.groups = scope.model.groups || [];
+
+ const groupsInTab = scope.model.groups.filter(group => group.parentAlias === tabAlias);
+ const lastGroupSortOrder = groupsInTab.length > 0 ? groupsInTab[groupsInTab.length - 1].sortOrder + 1 : 0;
+
+ const key = String.CreateGuid();
+ const group = {
+ key: key,
+ type: TYPE_GROUP,
+ name: '',
+ alias: contentTypeHelper.updateParentAlias(key, tabAlias), // Temporarily set alias to key, because the name is empty
+ parentAlias: tabAlias || null,
+ sortOrder: lastGroupSortOrder,
+ properties: [],
+ parentTabContentTypes: [],
+ parentTabContentTypeNames: []
+ };
+
+ scope.model.groups = [...scope.model.groups, group];
+
+ scope.activateGroup(group);
+ };
+
+ scope.activateGroup = selectedGroup => {
+ if (!selectedGroup) {
+ return;
+ }
// set all other groups that are inactive to active
- Utilities.forEach(scope.model.groups, group => {
+ scope.model.groups.forEach(group => {
// skip init tab
if (group.tabState !== "init") {
group.tabState = "inActive";
@@ -420,84 +723,73 @@
});
selectedGroup.tabState = "active";
-
};
- scope.canRemoveGroup = function (group) {
- return group.inherited !== true && _.find(group.properties, function (property) { return property.locked === true; }) == null;
+ scope.onChangeGroupName = group => {
+ updateGroupAlias(group);
}
- scope.removeGroup = function (groupIndex) {
- scope.model.groups.splice(groupIndex, 1);
+ scope.canRemoveGroup = group => {
+ return group.inherited !== true && _.find(group.properties, property => property.locked === true) == null;
};
- scope.updateGroupTitle = function (group) {
- if (group.properties.length === 0) {
- addInitProperty(group);
- }
+ scope.removeGroup = (selectedGroup) => {
+ const groupName = selectedGroup.name || "";
+
+ const localizeMany = localizationService.localizeMany(['general_delete', 'contentTypeEditor_confirmDeleteGroupNotice']);
+ const localize = localizationService.localize('contentTypeEditor_confirmDeleteGroupMessage', [groupName]);
+
+ $q.all([localizeMany, localize]).then(values => {
+ const translations = values[0];
+ const message = values[1];
+
+ overlayService.confirmDelete({
+ title: `${translations[0]} ${groupName}`,
+ content: message,
+ confirmMessage: translations[1],
+ submitButtonLabelKey: 'actions_delete',
+ submit: () => {
+ const index = scope.model.groups.findIndex(group => group.alias === selectedGroup.alias);
+ scope.model.groups.splice(index, 1);
+
+ overlayService.close();
+ }
+ });
+ });
};
- scope.changeSortOrderValue = function (group) {
+ scope.addGroupToActiveTab = () => {
+ scope.addGroup(scope.openTabAlias);
+ };
+
+ scope.changeSortOrderValue = group => {
if (group.sortOrder !== undefined) {
group.showSortOrderMissing = false;
}
+
scope.model.groups = $filter('orderBy')(scope.model.groups, 'sortOrder');
};
- function addInitGroup(groups) {
-
- // check i init tab already exists
- var addGroup = true;
-
- Utilities.forEach(groups, group => {
- if (group.tabState === "init") {
- addGroup = false;
- }
- });
-
- if (addGroup) {
- groups.push({
- properties: [],
- parentTabContentTypes: [],
- parentTabContentTypeNames: [],
- name: "",
- tabState: "init"
- });
- }
-
- return groups;
- }
-
- function activateFirstGroup(groups) {
- if (groups && groups.length > 0) {
- var firstGroup = groups[0];
- if (!firstGroup.tabState || firstGroup.tabState === "inActive") {
- firstGroup.tabState = "active";
- }
- }
- }
+ scope.onChangeGroupSortOrderValue = sortedGroup => {
+ const groupsInTab = scope.model.groups.filter(group => group.parentAlias === sortedGroup.parentAlias);
+ const otherGroups = scope.model.groups.filter(group => group.parentAlias !== sortedGroup.parentAlias);
+ const sortedGroups = $filter('orderBy')(groupsInTab, 'sortOrder');
+ scope.model.groups = [...otherGroups, ...sortedGroups];
+ };
/* ---------- PROPERTIES ---------- */
+ scope.addPropertyToActiveGroup = () => {
+ let activeGroup = scope.model.groups.find(group => group.tabState === "active");
- scope.addPropertyToActiveGroup = function () {
- var group = _.find(scope.model.groups, group => group.tabState === "active");
- if (!group && scope.model.groups.length) {
- group = scope.model.groups[0];
+ if (!activeGroup && scope.model.groups.length) {
+ activeGroup = scope.model.groups[0];
}
- if (!group || !group.name) {
- return;
- }
+ scope.addNewProperty(activeGroup);
+ };
- var property = _.find(group.properties, property => property.propertyState === "init");
- if (!property) {
- return;
- }
- scope.addProperty(property, group);
- }
-
- scope.addProperty = function (property, group) {
+ scope.addProperty = (property, group) => {
// set property sort order
var index = group.properties.indexOf(property);
@@ -517,7 +809,7 @@
};
- scope.editPropertyTypeSettings = function (property, group) {
+ scope.editPropertyTypeSettings = (property, group) => {
if (!property.inherited) {
@@ -538,7 +830,7 @@
contentTypeAllowSegmentVariant: scope.model.allowSegmentVariant,
view: "views/common/infiniteeditors/propertysettings/propertysettings.html",
size: "small",
- submit: function (model) {
+ submit: model => {
property.inherited = false;
property.dialogIsOpen = false;
@@ -574,28 +866,35 @@
// close the editor
editorService.close();
- // push new init property to group
- addInitProperty(group);
+ if (group) {
+ // push new init property to group
+ addInitProperty(group);
- // set focus on init property
- var numberOfProperties = group.properties.length;
- group.properties[numberOfProperties - 1].focus = true;
+ // set focus on init property
+ var numberOfProperties = group.properties.length;
+ group.properties[numberOfProperties - 1].focus = true;
+ }
notifyChanged();
},
- close: function () {
+ close: () => {
if (_.isEqual(oldPropertyModel, propertyModel) === false) {
- localizationService.localizeMany(["general_confirm", "contentTypeEditor_propertyHasChanges", "general_cancel", "general_ok"]).then(function (data) {
+ localizationService.localizeMany([
+ "general_confirm",
+ "contentTypeEditor_propertyHasChanges",
+ "general_cancel",
+ "general_ok"
+ ]).then(data => {
const overlay = {
title: data[0],
content: data[1],
closeButtonLabel: data[2],
submitButtonLabel: data[3],
submitButtonStyle: "danger",
- close: function () {
+ close: () => {
overlayService.close();
},
- submit: function () {
+ submit: () => {
// close the confirmation
overlayService.close();
// close the editor
@@ -622,12 +921,33 @@
}
};
- scope.deleteProperty = function (tab, propertyIndex) {
+ scope.deleteProperty = (properties, { id, label }) => {
+ const propertyName = label || "";
- // remove property
- tab.properties.splice(propertyIndex, 1);
+ const localizeMany = localizationService.localizeMany(['general_delete']);
+ const localize = localizationService.localize('contentTypeEditor_confirmDeletePropertyMessage', [propertyName]);
+
+ $q.all([localizeMany, localize]).then(values => {
+ const translations = values[0];
+ const message = values[1];
- notifyChanged();
+ overlayService.confirmDelete({
+ title: `${translations[0]} ${propertyName}`,
+ content: message,
+ submitButtonLabelKey: 'actions_delete',
+ submit: () => {
+ const index = properties.findIndex(property => property.id === id);
+ properties.splice(index, 1);
+ notifyChanged();
+
+ overlayService.close();
+ }
+ });
+ });
+ };
+
+ scope.onChangePropertySortOrderValue = group => {
+ group.properties = $filter('orderBy')(group.properties, 'sortOrder');
};
function notifyChanged() {
@@ -679,7 +999,9 @@
property.dataTypeId = newProperty.dataTypeId;
property.dataTypeIcon = newProperty.dataTypeIcon;
property.dataTypeName = newProperty.dataTypeName;
+
}
+
});
});
}
@@ -697,21 +1019,21 @@
}
- eventBindings.push(scope.$watch('model', function (newValue, oldValue) {
+ eventBindings.push(scope.$watch('model', (newValue, oldValue) => {
if (newValue !== undefined && newValue.groups !== undefined) {
activate();
}
}));
// clean up
- eventBindings.push(eventsService.on("editors.dataTypeSettings.saved", function (name, args) {
+ eventBindings.push(eventsService.on("editors.dataTypeSettings.saved", (name, args) => {
if (hasPropertyOfDataTypeId(args.dataType.id)) {
scope.dataTypeHasChanged = true;
}
}));
// clean up
- eventBindings.push(scope.$on('$destroy', function () {
+ eventBindings.push(scope.$on('$destroy', () => {
for (var e in eventBindings) {
eventBindings[e]();
}
diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbicon.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbicon.directive.js
index 73a9617aee..781b3e7df9 100644
--- a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbicon.directive.js
+++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbicon.directive.js
@@ -15,7 +15,7 @@ Simple icon
Icon with additional attribute. It can be treated like any other dom element