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 _lazyConfiguration; diff --git a/src/Umbraco.Core/Models/PublishedContent/PublishedPropertyBase.cs b/src/Umbraco.Core/Models/PublishedContent/PublishedPropertyBase.cs index dd77d899ca..473addecc3 100644 --- a/src/Umbraco.Core/Models/PublishedContent/PublishedPropertyBase.cs +++ b/src/Umbraco.Core/Models/PublishedContent/PublishedPropertyBase.cs @@ -1,4 +1,5 @@ using System; +using System.Diagnostics; using Umbraco.Cms.Core.PropertyEditors; namespace Umbraco.Cms.Core.Models.PublishedContent @@ -7,6 +8,7 @@ namespace Umbraco.Cms.Core.Models.PublishedContent /// Provides a base class for IPublishedProperty implementations which converts and caches /// the value source to the actual value to use when rendering content. /// + [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: ' 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: "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
-    
+    
 
@example **/ @@ -33,8 +33,8 @@ Icon with additional attribute. It can be treated like any other dom element icon: "@", svgString: "=?" }, - link: function (scope, element) { + if (scope.svgString === undefined && scope.svgString !== null && scope.icon !== undefined && scope.icon !== null) { const observer = new IntersectionObserver(_lazyRequestIcon, {rootMargin: "100px"}); const iconEl = element[0]; @@ -49,6 +49,7 @@ Icon with additional attribute. It can be treated like any other dom element scope.$watch("icon", function (newValue, oldValue) { if (newValue && oldValue) { + var newicon = newValue.split(" ")[0]; var oldicon = oldValue.split(" ")[0]; @@ -64,6 +65,7 @@ Icon with additional attribute. It can be treated like any other dom element observer.disconnect(); var icon = scope.icon.split(" ")[0]; // Ensure that only the first part of the icon is used as sometimes the color is added too, e.g. see umbeditorheader.directive scope.openIconPicker + _requestIcon(icon); } }); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/umblistviewsettings.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/umblistviewsettings.directive.js index 318ffebd1b..d0fd8c7164 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/umblistviewsettings.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/umblistviewsettings.directive.js @@ -7,8 +7,17 @@ scope.dataType = {}; scope.customListViewCreated = false; + + const listViewPrefix = "List View - "; - const checkForCustomListView = () => scope.dataType.name === "List View - " + scope.modelAlias; + const checkForCustomListView = () => invariantEquals(scope.dataType.name, listViewPrefix + scope.modelAlias); + + // We also use "localeCompare" a few other places. Should probably be moved to a utility/helper function in future. + function invariantEquals(a, b) { + return typeof a === "string" && typeof b === "string" + ? a.localeCompare(b, undefined, { sensitivity: "base" }) === 0 + : a === b; + } /* ---------- INIT ---------- */ diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/upload/umbfiledropzone.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/upload/umbfiledropzone.directive.js index d80b884dab..e926ca23d7 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/upload/umbfiledropzone.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/upload/umbfiledropzone.directive.js @@ -187,6 +187,11 @@ angular.module("umbraco.directives") function _requestChooseMediaTypeDialog() { + if (scope.queue.length === 0) { + // if queue has no items so there is nothing to choose a type for + return false; + } + if (scope.acceptedMediatypes.length === 1) { // if only one accepted type, then we wont ask to choose. return false; diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/users/changepassword.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/users/changepassword.directive.js index 35ca2a8588..49cffa6ccb 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/users/changepassword.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/users/changepassword.directive.js @@ -58,6 +58,9 @@ } } + // set initial value for new password value + vm.passwordVal = vm.passwordValues.newPassword; + //the value to compare to match passwords if (!isNew) { vm.passwordValues.confirm = ""; diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/util/umbDroppable.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/util/umbDroppable.directive.js new file mode 100644 index 0000000000..5b4719edc0 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/common/directives/util/umbDroppable.directive.js @@ -0,0 +1,12 @@ +angular.module("umbraco.directives") + .directive('umbDroppable', function ($timeout) { + return { + restrict: 'A', + link: function (scope, element, attrs) { + $timeout(() => { + const options = scope.$eval(attrs.umbDroppable) + element.droppable(options); + }); + } + } + }); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/util/umboverflowchecker.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/util/umboverflowchecker.directive.js new file mode 100644 index 0000000000..076ae4b311 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/common/directives/util/umboverflowchecker.directive.js @@ -0,0 +1,49 @@ +angular.module("umbraco.directives") + .directive('umbOverflowChecker', function ($parse, $timeout, windowResizeListener) { + return { + restrict: 'A', + link: function (scope, element, attrs) { + const overflow = $parse(attrs.onOverflow); + + const scrollElement = element[0]; + const container = element[0].parentElement; + + function checkOverflow () { + $timeout(() => { + const scrollElementScrollWidth = scrollElement.scrollWidth; + const containerScrollWidth = container.scrollWidth; + + const overflowLeft = scrollElement.scrollLeft; + const overflowRight = containerScrollWidth - scrollElementScrollWidth + overflowLeft; + + scope.$evalAsync(() => overflow(scope, {overflowLeft, overflowRight})); + }, 50); + } + + function scrollTo (event, options) { + $timeout(() => { + if (options.position === 'end') { + scrollElement.scrollLeft = scrollElement.scrollWidth - scrollElement.clientWidth; + } + + if (options.position === 'start') { + scrollElement.scrollLeft = 0; + } + }, 50); + } + + scrollElement.addEventListener('scroll', checkOverflow); + windowResizeListener.register(checkOverflow); + + scope.$on('$destroy', () => { + scrollElement.removeEventListener('scroll', checkOverflow); + windowResizeListener.unregister(checkOverflow); + }); + + scope.$on('umbOverflowChecker.checkOverflow', checkOverflow); + scope.$on('umbOverflowChecker.scrollTo', scrollTo); + + checkOverflow(); + } + } + }); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/validation/nodirtycheck.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/validation/nodirtycheck.directive.js index 800ac87480..31ef125511 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/validation/nodirtycheck.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/validation/nodirtycheck.directive.js @@ -10,13 +10,18 @@ function noDirtyCheck() { require: 'ngModel', link: function (scope, elm, attrs, ctrl) { + // if "no-dirty-check" attribute is explicitly falsy, then skip and use default behaviour. In all other cases we consider it truthy + var skipNoDirtyCheck = attrs.noDirtyCheck === '0' || attrs.noDirtyCheck === 0 || attrs.noDirtyCheck.toString().toLowerCase() === 'false'; + if (skipNoDirtyCheck) + return; + var alwaysFalse = { - get: function () { return false; }, - set: function () { } - }; + get: function () { return false; }, + set: function () { } + }; + Object.defineProperty(ctrl, '$pristine', alwaysFalse); Object.defineProperty(ctrl, '$dirty', alwaysFalse); - } }; } diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/validation/valtab.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/validation/valtab.directive.js index b73aa0f29c..53edd0b108 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/validation/valtab.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/validation/valtab.directive.js @@ -6,30 +6,78 @@ * @description Used to show validation warnings for a tab to indicate that the tab content has validations errors in its data. * In order for this directive to work, the valFormManager directive must be placed on the containing form. **/ -function valTab() { +function valTab($timeout) { return { require: ['^^form', '^^valFormManager'], restrict: "A", link: function (scope, element, attr, ctrs) { - var valFormManager = ctrs[1]; - var tabAlias = scope.tab.alias; - scope.tabHasError = false; + var evts = []; + var form = ctrs[0]; + var tab = scope.$eval(attr.valTab) || scope.tab; - //listen for form validation changes - valFormManager.onValidationStatusChanged(function (evt, args) { - if (!args.form.$valid) { - var tabContent = element.closest(".umb-editor").find("[data-element='tab-content-" + tabAlias + "']"); - //check if the validation messages are contained inside of this tabs + if (!tab) { + return; + } + + let closestEditor = element.closest(".blockelement-inlineblock-editor"); + closestEditor = closestEditor.length === 0 ? element.closest(".umb-editor-sub-view") : closestEditor; + closestEditor = closestEditor.length === 0 ? element.closest(".umb-editor") : closestEditor; + + setSuccess(); + + function setValidity (form) { + var tabAlias = tab.alias || ''; + + if (!form.$valid) { + var tabContent = closestEditor.find("[data-element='tab-content-" + tabAlias + "']"); + + //check if the validation messages are contained inside of this tabs if (tabContent.find(".ng-invalid").length > 0) { - scope.tabHasError = true; + setError(); } else { - scope.tabHasError = false; + setSuccess(); } } else { - scope.tabHasError = false; + setSuccess(); } + } + + function setError () { + scope.valTab_tabHasError = true; + tab.hasError = true; + } + + function setSuccess () { + scope.valTab_tabHasError = false; + tab.hasError = false; + } + + function subscribe () { + for (let control of form.$$controls) { + var unbind = scope.$watch(() => control.$invalid, function () { + setValidity(form); + }); + + evts.push(unbind); + } + } + + function unsubscribe () { + evts.forEach(event => event()); + } + + // we need to watch validation state on individual controls so we can update specific tabs accordingly + $timeout(() => { + scope.$watchCollection(() => form.$$controls, function (newValue) { + unsubscribe(); + subscribe(); + }); + }); + + scope.$on('$destroy', function () { + unsubscribe(); }); } }; diff --git a/src/Umbraco.Web.UI.Client/src/common/mocks/services/localization.mocks.js b/src/Umbraco.Web.UI.Client/src/common/mocks/services/localization.mocks.js index 9a05e3cd7f..76fb8be8c4 100644 --- a/src/Umbraco.Web.UI.Client/src/common/mocks/services/localization.mocks.js +++ b/src/Umbraco.Web.UI.Client/src/common/mocks/services/localization.mocks.js @@ -139,6 +139,7 @@ angular.module('umbraco.mocks'). "content_publishStatus": "Publication Status", "content_releaseDate": "Publish at", "content_removeDate": "Clear Date", + "content_resetFocalPoint": "Reset focal point", "content_sortDone": "Sortorder is updated", "content_sortHelp": "To sort the nodes, simply drag the nodes or click one of the column headers. You can select multiple nodes by holding the 'shift' or 'control' key while selecting", "content_statistics": "Statistics", diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/content.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/content.resource.js index cdec76fcfd..a836f8db3d 100644 --- a/src/Umbraco.Web.UI.Client/src/common/resources/content.resource.js +++ b/src/Umbraco.Web.UI.Client/src/common/resources/content.resource.js @@ -642,6 +642,24 @@ function contentResource($q, $http, umbDataFormatter, umbRequestHelper) { return $q.when(umbDataFormatter.formatContentGetData(result)); }); }, + + getScaffolds: function(parentId, aliases){ + return umbRequestHelper.resourcePromise( + $http.post( + umbRequestHelper.getApiUrl( + "contentApiBaseUrl", + "GetEmptyByAliases"), + { parentId: parentId, contentTypeAliases: aliases } + ), + 'Failed to retrieve data for empty content item aliases ' + aliases.join(", ") + ).then(function(result) { + Object.keys(result).map(function(key){ + result[key] = umbDataFormatter.formatContentGetData(result[key]); + }); + + return $q.when(result); + }); + }, /** * @ngdoc method * @name umbraco.resources.contentResource#getScaffoldByKey diff --git a/src/Umbraco.Web.UI.Client/src/common/services/blockeditormodelobject.service.js b/src/Umbraco.Web.UI.Client/src/common/services/blockeditormodelobject.service.js index ee96cfbaaa..cb06218618 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/blockeditormodelobject.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/blockeditormodelobject.service.js @@ -87,12 +87,14 @@ for (var t = 0; t < fromVariant.tabs.length; t++) { var fromTab = fromVariant.tabs[t]; - var toTab = toVariant.tabs[t]; + var toTab = toVariant.tabs.find(tab => tab.alias === fromTab.alias); - for (var p = 0; p < fromTab.properties.length; p++) { - var fromProp = fromTab.properties[p]; - var toProp = toTab.properties[p]; - toProp.value = fromProp.value; + if (fromTab && fromTab.properties && fromTab.properties.length > 0 && toTab && toTab.properties && toTab.properties.length > 0) { + for (var p = 0; p < fromTab.properties.length; p++) { + var fromProp = fromTab.properties[p]; + var toProp = toTab.properties[p]; + toProp.value = fromProp.value; + } } } } @@ -606,6 +608,12 @@ return null; } + // the Settings model has been changed to a new Element Type. + // we need to update the settingsData with the new Content Type key + if (settingsData.contentTypeKey !== settingsScaffold.contentTypeKey) { + settingsData.contentTypeKey = settingsScaffold.contentTypeKey; + } + blockObject.settingsData = settingsData; // make basics from scaffold diff --git a/src/Umbraco.Web.UI.Client/src/common/services/contenteditinghelper.service.js b/src/Umbraco.Web.UI.Client/src/common/services/contenteditinghelper.service.js index 9733429b21..33214d6032 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/contenteditinghelper.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/contenteditinghelper.service.js @@ -75,8 +75,8 @@ function contentEditingHelper(fileManager, $q, $location, $routeParams, editorSt if (args.showNotifications === undefined) { args.showNotifications = true; } - // needed for infinite editing to create new items - if (args.create === undefined) { + // needed for infinite editing to create new items + if (args.create === undefined) { if ($routeParams.create) { args.create = true; } @@ -180,6 +180,38 @@ function contentEditingHelper(fileManager, $q, $location, $routeParams, editorSt }, + registerGenericTab: function (groups) { + if (!groups) { + return; + } + + const hasGenericTab = groups.find(group => group.isGenericTab); + if (hasGenericTab) { + return; + } + + const isRootGroup = (group) => group.type === 0 && group.parentAlias === null; + const hasRootGroups = groups.filter(group => isRootGroup(group)).length > 0; + if (!hasRootGroups) { + return; + } + + const genericTab = { + isGenericTab: true, + type: 1, + label: 'Generic', + key: String.CreateGuid(), + alias: null, + parentAlias: null, + properties: [] + }; + + localizationService.localize("general_generic").then(function (value) { + genericTab.label = value; + groups.unshift(genericTab); + }); + }, + /** Returns the action button definitions based on what permissions the user has. The content.allowedActions parameter contains a list of chars, each represents a button by permission so here we'll build the buttons according to the chars of the user. */ diff --git a/src/Umbraco.Web.UI.Client/src/common/services/contenttypehelper.service.js b/src/Umbraco.Web.UI.Client/src/common/services/contenttypehelper.service.js index eb401ebe5f..c940ed0d71 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/contenttypehelper.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/contenttypehelper.service.js @@ -7,6 +7,103 @@ function contentTypeHelper(contentTypeResource, dataTypeResource, $filter, $inje var contentTypeHelperService = { + TYPE_GROUP: 0, + TYPE_TAB: 1, + + isAliasUnique(groups, alias) { + return groups.find(group => group.alias === alias) ? false : true; + }, + + createUniqueAlias(groups, alias) { + let i = 1; + while(this.isAliasUnique(groups, alias + i.toString()) === false) { + i++; + } + return alias + i.toString(); + }, + + generateLocalAlias: function(name) { + return name ? name.toUmbracoAlias() : String.CreateGuid(); + }, + + getLocalAlias: function (alias) { + const lastIndex = alias.lastIndexOf('/'); + + return (lastIndex === -1) ? alias : alias.substring(lastIndex + 1); + }, + + updateLocalAlias: function (alias, localAlias) { + const parentAlias = this.getParentAlias(alias); + + return (parentAlias == null || parentAlias === '') ? localAlias : parentAlias + '/' + localAlias; + }, + + getParentAlias: function (alias) { + if(alias) { + const lastIndex = alias.lastIndexOf('/'); + + return (lastIndex === -1) ? null : alias.substring(0, lastIndex); + } + return null; + }, + + updateParentAlias: function (alias, parentAlias) { + const localAlias = this.getLocalAlias(alias); + + return (parentAlias == null || parentAlias === '') ? localAlias : parentAlias + '/' + localAlias; + }, + + updateDescendingAliases: function (groups, oldParentAlias, newParentAlias) { + groups.forEach(group => { + const parentAlias = this.getParentAlias(group.alias); + + if (parentAlias === oldParentAlias) { + const oldAlias = group.alias, + newAlias = this.updateParentAlias(oldAlias, newParentAlias); + + group.alias = newAlias; + group.parentAlias = newParentAlias; + this.updateDescendingAliases(groups, oldAlias, newAlias); + + } + }); + }, + + defineParentAliasOnGroups: function (groups) { + groups.forEach(group => { + group.parentAlias = this.getParentAlias(group.alias); + }); + }, + + relocateDisorientedGroups: function (groups) { + const existingAliases = groups.map(group => group.alias); + existingAliases.push(null); + const disorientedGroups = groups.filter(group => existingAliases.indexOf(group.parentAlias) === -1); + disorientedGroups.forEach(group => { + const oldAlias = group.alias, + newAlias = this.updateParentAlias(oldAlias, null); + + group.alias = newAlias; + group.parentAlias = null; + this.updateDescendingAliases(groups, oldAlias, newAlias); + }); + }, + + convertGroupToTab: function (groups, group) { + group.convertingToTab = true; + + group.type = this.TYPE_TAB; + + const newAlias = this.generateLocalAlias(group.name); + // when checking for alias uniqueness we need to exclude the current group or the alias would get a + 1 + const otherGroups = [...groups].filter(groupCopy => !groupCopy.convertingToTab); + + group.alias = this.isAliasUnique(otherGroups, newAlias) ? newAlias : this.createUniqueAlias(otherGroups, newAlias); + group.parentAlias = null; + + group.convertingToTab = false; + }, + createIdArray: function (array) { var newArray = []; @@ -143,7 +240,7 @@ function contentTypeHelper(contentTypeResource, dataTypeResource, $filter, $inje // if groups are named the same - merge the groups contentType.groups.forEach(function (contentTypeGroup) { - if (contentTypeGroup.name === compositionGroup.name) { + if (contentTypeGroup.name === compositionGroup.name && contentTypeGroup.type === compositionGroup.type) { // set flag to show if properties has been merged into a tab compositionGroup.groupIsMerged = true; @@ -256,8 +353,7 @@ function contentTypeHelper(contentTypeResource, dataTypeResource, $filter, $inje } // remove group if there are no properties left - if (contentTypeGroup.properties.length > 1) { - //contentType.groups.splice(groupIndex, 1); + if (contentTypeGroup.properties.length > 0) { groups.push(contentTypeGroup); } diff --git a/src/Umbraco.Web.UI.Client/src/common/services/tinymce.service.js b/src/Umbraco.Web.UI.Client/src/common/services/tinymce.service.js index 344a26ed0c..0297d7ffd7 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/tinymce.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/tinymce.service.js @@ -1553,6 +1553,7 @@ function tinyMceService($rootScope, $q, imageHelper, $locale, $http, $timeout, s dataTypeKey: args.model.dataTypeKey, ignoreUserStartNodes: args.model.config.ignoreUserStartNodes, anchors: anchorValues, + size: args.model.config.editor.overlayWidthSize, submit: function (model) { self.insertLinkInEditor(args.editor, model.target, anchorElement); editorService.close(); diff --git a/src/Umbraco.Web.UI.Client/src/common/services/umbdataformatter.service.js b/src/Umbraco.Web.UI.Client/src/common/services/umbdataformatter.service.js index 0788af66f7..e72900cacd 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/umbdataformatter.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/umbdataformatter.service.js @@ -75,7 +75,7 @@ }); saveModel.groups = _.map(realGroups, function (g) { - var saveGroup = _.pick(g, 'inherited', 'id', 'sortOrder', 'name'); + var saveGroup = _.pick(g, 'inherited', 'id', 'sortOrder', 'name', 'alias', 'type'); var realProperties = _.reject(g.properties, function (p) { //do not include these properties diff --git a/src/Umbraco.Web.UI.Client/src/less/components/check-circle.less b/src/Umbraco.Web.UI.Client/src/less/components/check-circle.less index fadf9c7940..a167644ea8 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/check-circle.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/check-circle.less @@ -1,19 +1,17 @@ .check_circle { display: flex; + justify-content: center; + align-items: center; width: 20px; height: 20px; - margin: 0 auto; .icon { background-color: rgba(0,0,0,.15); border-radius: 50%; + padding: 3px; color: @white; - font-size: 1em; display: flex; - width: 100%; - height: 100%; align-items: center; justify-content: center; - float: left; } } diff --git a/src/Umbraco.Web.UI.Client/src/less/components/editor.less b/src/Umbraco.Web.UI.Client/src/less/components/editor.less index d4069cdd2b..4f71fe46da 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/editor.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/editor.less @@ -104,7 +104,7 @@ } .-split-view-active .umb-editor-header__name-and-description { - margin-right: 0; + margin-right: 0; } .umb-editor-header__name-wrapper ng-form { @@ -164,6 +164,29 @@ input.umb-editor-header__name-input:disabled { } } +// Tab bar +.umb-editor-tab-bar { + position: sticky; + top: 0; + left: 0; + right: 0; + z-index: 90; + margin: -20px -20px 20px -20px; + padding: 0 20px; + background: @white; + box-shadow: 1px 1px 0 @gray-9; + + .umb-tabs-nav { + border-bottom: none; + } + + .umb-tab { + button { + padding: 15px 20px; + } + } +} + // container .umb-editor-container { position: absolute; diff --git a/src/Umbraco.Web.UI.Client/src/less/components/editor/subheader/umb-editor-sub-header.less b/src/Umbraco.Web.UI.Client/src/less/components/editor/subheader/umb-editor-sub-header.less index 3c4a037b0b..0dd7bfc7f4 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/editor/subheader/umb-editor-sub-header.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/editor/subheader/umb-editor-sub-header.less @@ -51,7 +51,6 @@ &.-top { height:1px; - transform:translateY(-10px); } } diff --git a/src/Umbraco.Web.UI.Client/src/less/components/editor/umb-variant-switcher.less b/src/Umbraco.Web.UI.Client/src/less/components/editor/umb-variant-switcher.less index 4429990b4f..e56669bfc2 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/editor/umb-variant-switcher.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/editor/umb-variant-switcher.less @@ -108,6 +108,16 @@ button.umb-variant-switcher__toggle { cursor: default; } +.umb-variant-switcher__name-wrapper { + display: flex; + align-items: center; + + .umb-variant-switcher__name-content { + display: flex; + flex-direction: column; + } +} + .umb-variant-switcher__item.--state-notCreated:not(.--active) { .umb-variant-switcher__name-wrapper::before { content: "+"; diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-editor-navigation-item.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-editor-navigation-item.less index 5fd743aaf0..dcb5130968 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-editor-navigation-item.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-editor-navigation-item.less @@ -72,14 +72,6 @@ animation-iteration-count: infinite; animation-name: umb-sub-views-nav-item--badge-bounce; animation-timing-function: ease; - @keyframes umb-sub-views-nav-item--badge-bounce { - 0% { transform: translateY(0); } - 20% { transform: translateY(-6px); } - 40% { transform: translateY(0); } - 55% { transform: translateY(-3px); } - 70% { transform: translateY(0); } - 100% { transform: translateY(0); } - } } .badge.--error-badge { display: block; @@ -226,3 +218,38 @@ } } } + +.umb-sub-views-nav-item-more { + .umb-sub-views-nav-item__action { + &.-has-error { + &.is-active { + color: @red !important; + + .badge { + display: block; + animation-duration: 1.4s; + animation-iteration-count: infinite; + animation-name: umb-sub-views-nav-item--badge-bounce; + animation-timing-function: ease; + } + + &.-active-has-error { + color: @ui-light-active-type !important; + + .badge { + display: none; + } + } + } + } + } +} + +@keyframes umb-sub-views-nav-item--badge-bounce { + 0% { transform: translateY(0); } + 20% { transform: translateY(-6px); } + 40% { transform: translateY(0); } + 55% { transform: translateY(-3px); } + 70% { transform: translateY(0); } + 100% { transform: translateY(0); } +} \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-file-icon.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-file-icon.less index ffe87277e6..c795f380c9 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-file-icon.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-file-icon.less @@ -4,7 +4,7 @@ flex-direction: column; align-items: center; - .file-icon { + &__inner { display: flex; flex-direction: column; align-items: flex-start; @@ -17,44 +17,36 @@ display: block; text-align: center; } - - > span { - position: absolute; - color: @ui-active-type; - background: @ui-active; - padding: 1px 3px; - font-size: 10px; - line-height: 130%; - display: block; - margin-bottom: 0.75rem; - min-width: 1.2rem; - bottom: 0; - } - - & + small { - display: block; - margin-top: 0.25rem; - } } -} -.umb-file-icon--s { - .file-icon { - > .icon { + &__extension { + position: absolute; + color: @ui-active-type; + background: @ui-active; + padding: 1px 3px; + font-size: 10px; + line-height: 130%; + display: block; + margin-bottom: 0.75rem; + min-width: 1.2rem; + bottom: 0; + } - } + &__text { + display: block; + margin-top: 0.25rem; } } .umb-file-icon--m { - .file-icon { + .umb-file-icon__inner { > .icon { font-size: 70px; } - > span { - font-size: 14px; - margin-bottom: 0.95rem; - min-width: 1.5rem; - } + } + .umb-file-icon__extension { + font-size: 14px; + margin-bottom: 0.95rem; + min-width: 1.5rem; } } diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-form-check.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-form-check.less index 9be50b877a..f13ae198d6 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-form-check.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-form-check.less @@ -156,6 +156,8 @@ font-size: 12px; opacity: 0; transition: .2s ease-out; + position: relative; + z-index: 1; &:before { display: flex; diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-group-builder.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-group-builder.less index a4a8388861..1122558b05 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-group-builder.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-group-builder.less @@ -1,3 +1,331 @@ +@umbGroupBuilderToolbarHeight: 60px; + +/* ---------- TOOLBAR --------- */ +.umb-group-builder__toolbar { + display: flex; + align-items: center; + padding: 0; + border-right-width: 21px; + margin-left: -20px; + width: calc(100% + 40px); + margin-top: -20px; + height: @umbGroupBuilderToolbarHeight; + + .left { + flex: 1 1 auto; + width: 50%; + display: block; + margin-right: 40px; + } + + .right { + flex: 0 0 auto; + } +} + +/* ---------- TABS ---------- */ + +.umb-group-builder__tabs { + height: @umbGroupBuilderToolbarHeight; + position: relative; +} + +.umb-group-builder__tabs-list { + height: 100%; + list-style: none; + margin: 0; + padding: 0; + display: flex; + align-items: center; + overflow-x: auto; + overflow-y: visible; + scroll-behavior: smooth; + -ms-overflow-style: none; + scrollbar-width: none; + margin-bottom: -100px; // allow validation messages to overflow container + padding-bottom: 100px; // allow validation messages to overflow container + pointer-events: none; // allow validation messages to overflow container + + li { + pointer-events: auto; + height: 100%; + + &:only-of-type { + .umb-group-builder__tab { + margin-left: 0; + } + } + } +} + +.umb-group-builder__tabs-list::-webkit-scrollbar { + display: none; +} + +.umb-group-builder__tabs-overflow { + height: 100%; + width: 30px; + position: absolute; + top: 0; + display: flex; + align-items: center; + justify-content: center; + z-index: 1; + background: white; +} + +.umb-group-builder__tabs-overflow--left { + left: 0; + box-shadow: 4px 0 5px rgba(0,0,0,0.08); + + .caret { + transform: rotate(90deg) translate(0, 2px); + } +} + +.umb-group-builder__tabs-overflow--right { + right: 0; + box-shadow: -4px 0 5px rgba(0,0,0,0.08); + + .caret { + transform: rotate(270deg) translate(0, -2px); + } +} + +.umb-group-builder__tabs-list__add-tab { + display: contents; + + > umb-button { + white-space: nowrap; + + .umb-button { + margin-left: 0; + } + + .umb-button__content { + flex-wrap: nowrap; + } + } + + > umb-button, + .umb-button, + .umb-button__button { + height: 100%; + } +} + +.umb-group-builder__tab { + background-color: @white; + position: relative; + padding: 0 15px; + display: flex; + align-items: center; + justify-content: center; + height: 100%; + border-right: 1px solid @gray-9; + &:first-of-type { + border-bottom-left-radius: 3px; + } + &:last-of-type { + border-bottom-right-radius: 3px; + } + + &:hover { + cursor: pointer; + + .umb-group-builder__tab-remove { + display: block; + } + } + + .ui-droppable-hover & { + + animation: umb-group-builder-tab--droppable-active 800ms ease-in-out alternate infinite; + + @keyframes umb-group-builder-tab--droppable-active { + 0% { background-color: white } + 50% { background-color: @gray-12 } + } + } + + .badge { + background-color: @red; + animation-duration: 1.4s; + animation-iteration-count: infinite; + animation-name: umb-group-builder-tab--badge-bounce; + animation-timing-function: ease; + display: none; + + @keyframes umb-group-builder-tab--badge-bounce { + 0% { transform: translateY(0); } + 20% { transform: translateY(-6px); } + 40% { transform: translateY(0); } + 55% { transform: translateY(-3px); } + 70% { transform: translateY(0); } + 100% { transform: translateY(0); } + } + } + + &::before { + content: ""; + position: absolute; + height: 0px; + left: 15px; + right: 15px; + background-color: @ui-light-active-border; + bottom: 0; + border-radius: @baseBorderRadius @baseBorderRadius 0 0; + opacity: 0; + transition: all .2s linear; + } + + &.is-active { + color: @ui-light-active-type !important; + + .umb-group-builder__tab-remove { + display: block; + } + + &::before { + opacity: 1; + height: 4px; + } + } + + &.is-deletable { + padding-right: 45px; + } + + &.is-inherited { + padding-right: 22px; + + .umb-group-builder__group-title-input { + padding: 0; + } + } + + .umb-group-builder__group-title-input { + &:disabled { + cursor: pointer; + } + } +} + +.show-validation { + .umb-group-builder__tab { + .badge { + display: block; + } + + &.has-error { + &::before { + background-color: @red; + } + } + } +} + +.umb-group-builder__tab-sortable { + list-style: none; +} + +.umb-group-builder__tab-sortable-placeholder { + background: transparent; + border: 1px dashed @gray-8; + border-top: none; + border-bottom: none; +} + +.umb-group-builder__tab-remove { + position: absolute; + right: 20px; + display: none; +} + +.umb-group-builder__tab-title-wrapper { + display: flex; + align-items: center; +} + +.umb-group-builder__tab-title-icon { + margin-right: 5px; +} + +.umb-group-builder__tab-name { + white-space: nowrap; +} + +.umb-group-builder__tab-val-message { + position: absolute; + top: calc(100% + 5px); + left: 20px; +} + +.umb-group-builder__tab--placeholder { + border: 1px dashed @ui-action-discreet-border; + color: @ui-action-discreet-type; + padding-right: 10px; + min-width: 100px; + background: transparent; + border-radius: @baseBorderRadius; + margin-left: 5px; + transition: color, border-color, 80ms; + &:hover { + color: @ui-action-discreet-type-hover; + border-color: @ui-action-discreet-border-hover; + } +} + +.umb-group-builder__tab-inherited-label { + position: absolute; + top: 100%; + left: 0; + z-index: 1; + display: block; + white-space: nowrap; + padding: 0 4px; + color: @black; + font-size: 12px; + background-color: @gray-8; + border-radius: @baseBorderRadius; + margin-top: 5px; + + &:after { + bottom: 100%; + left: 10px; + border: solid transparent; + content: " "; + height: 0; + width: 0; + position: absolute; + pointer-events: none; + border-color: rgba(255, 255, 255, 0); + border-bottom-color: @gray-8; + border-width: 4px; + margin-left: -4px; + } + + button { + font-size: 12px; + color: @black; + text-decoration: underline; + } +} + +.umb-group-builder__tab.-sortable { + cursor: move; + padding-right: 20px; +} + +.umb-group-builder__tab-sort-order { + margin-left: 10px; +} + +.umb-group-builder__ungrouped-properties { + margin-top: 20px; + position: relative; +} + /* ---------- GROUPS ---------- */ .umb-group-builder__groups { @@ -14,11 +342,12 @@ background-color: @white; position: relative; box-shadow: 0 1px 1px 0 rgba(0,0,0,0.16); + margin-top: 20px; + margin-bottom: 20px; } .umb-group-builder__group.-inherited { border-color: @gray-9; - animation: fadeIn 0.5s; box-shadow: none; } @@ -107,8 +436,6 @@ input.umb-group-builder__group-title-input { border-color: transparent; background: transparent; - font-weight: bold; - color: @black; margin-bottom: 0; } @@ -124,25 +451,32 @@ input.umb-group-builder__group-title-input:disabled:hover { border: 1px dashed @gray-6; } +.umb-group-builder__group-title-right { + display: flex; + align-items: center; + margin-left: auto; +} + .umb-group-builder__group-inherited-label { font-size: 13px; - display: inline-block; - position: relative; - top: 2px; + display: inline-flex; + align-items: center; + margin-right: 10px; +} + +.umb-group-builder__group-title-val-message { + display: flex; + align-items: center; } .umb-group-builder__group-sort-order { - margin-right: 10px; - margin-left: auto; + margin-right: 20px; } .umb-group-builder__group-add-property { - - width: calc(100% - 315px); - margin-left: 270px; + width: 100%; min-height: 46px; - border-radius: 3px; - + border-radius: @baseBorderRadius; display: flex; justify-content: center; align-items: center; @@ -159,12 +493,15 @@ input.umb-group-builder__group-title-input:disabled:hover { } } +.umb-group-builder__group-content { + padding: 10px 20px 20px 20px; +} + /* ---------- PROPERTIES ---------- */ .umb-group-builder__properties { list-style: none; margin: 0; - padding: 15px; padding-right: 5px; min-height: 35px; // the height of a sortable property } @@ -178,21 +515,12 @@ input.umb-group-builder__group-title-input:disabled:hover { padding: 10px 0; } -.umb-group-builder__property:first-of-type { - padding-top: 0; -} - -.umb-group-builder__property:last-of-type { - margin-bottom: 15px; -} - -.umb-group-builder__property.-inherited { - animation: fadeIn 0.5s; +.umb-group-builder__property-sortable { + list-style: none; } .umb-group-builder__property.-locked { border: transparent; - animation: fadeIn 0.5s; } .umb-group-builder__property.-locked:hover { @@ -349,6 +677,7 @@ input.umb-group-builder__group-title-input:disabled:hover { display: flex; align-items: flex-start; justify-content: flex-end; + margin-right: -20px; } .umb-group-builder__property-action { @@ -620,3 +949,25 @@ input.umb-group-builder__group-sort-value { width: 100%; } } + +// Convert to tab dropzone +.umb-group-builder__convert-dropzone { + display: inline-flex; + border: 1px dashed @gray-7; + align-items: center; + justify-content: center; + padding: 2px 15px; + border-radius: @baseBorderRadius; + // Hack for hiding as a droppable element: + visibility: hidden; + position: absolute; + + &.ui-droppable-hover { + border-color: @black; + } + + &.ui-droppable-active { + visibility: visible; + position: relative; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-icon.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-icon.less index 318ce0a563..e89ed96e79 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-icon.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-icon.less @@ -8,26 +8,43 @@ width: 100%; height: 100%; fill: currentColor; + animation: inherit; } - &.large{ - width: 32px; + &.large { + width: 32px; height: 32px; } - &.medium{ + + &.medium { width: 24px; height: 24px; } - &.small{ + + &.small { width: 14px; height: 14px; } - &:before, &:after { + &::before, + &::after { content: none !important; } - > i { - font-family: inherit; + &__inner { + // Clear pseudo classes + &::before, + &::after { + content: none !important; + } + + ng-transclude { + animation: inherit; + font-family: inherit; + + > span { + animation: inherit; + } + } } } diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-lightbox.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-lightbox.less index 6238900bf6..3977da188a 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-lightbox.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-lightbox.less @@ -11,6 +11,69 @@ align-items: center; justify-content: center; flex-direction: column; + + &__backdrop { + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + background: rgba(21, 21, 23, 0.7); + width: 100%; + height: 100%; + } + + &__close { + position: absolute; + top: 20px; + right: 20px; + height: 40px; + width: 40px; + + .umb-icon { + font-size: 20px; + height: inherit; + width: inherit; + position: absolute; + top: 0; + left: 0; + } + } + + &__images { + position: relative; + z-index: 1000; + max-width: calc(~'100%' - 200px); // subtract the width of the two arrow buttons + } + + &__image { + background: @white; + border-radius: 3px; + padding: 10px; + } + + &__control { + background-color: @white; + width: 50px; + height: 50px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + cursor: pointer; + position: absolute; + + &:hover { + .umb-lightbox__control-icon, &::before { + color: @ui-active-type-hover; + } + } + } + + &__control-icon { + color: @ui-active-type; + font-size: 20px; + } } .umb-drawer-is-visible .umb-lightbox { @@ -18,63 +81,6 @@ left: @drawerWidth; } -.umb-lightbox__backdrop { - position: absolute; - top: 0; - right: 0; - bottom: 0; - left: 0; - background: rgba(21, 21, 23, 0.7); - width: 100%; - height: 100%; -} - -.umb-lightbox__close { - position: absolute; - top: 20px; - right: 20px; - height: 40px; - width: 40px; -} - -.umb-lightbox__close i { - font-size: 20px; - height: inherit; - width: inherit; - position: absolute; - top: 0; - left: 0; -} - -.umb-lightbox__images { - position: relative; - z-index: 1000; - max-width: calc(~'100%' - 200px); // subtract the width of the two arrow buttons -} - -.umb-lightbox__image { - background: @white; - border-radius: 3px; - padding: 10px; -} - -.umb-lightbox__control { - background-color: @white; - width: 50px; - height: 50px; - border-radius: 50%; - display: flex; - align-items: center; - justify-content: center; - cursor: pointer; - position: absolute; - &:hover { - .umb-lightbox__control-icon, &::before { - color: @ui-active-type-hover; - } - } -} - .umb-lightbox__control.-next { right: 20px; top: 50%; @@ -94,9 +100,3 @@ margin-left: -4px; } } - -.umb-lightbox__control-icon { - color: @ui-active-type; - font-size: 20px; - -} diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-package-local-install.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-package-local-install.less index d229bc81b6..ead54ac49f 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-package-local-install.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-package-local-install.less @@ -22,20 +22,21 @@ align-items: center; margin-bottom: 30px; transition: 100ms box-shadow ease, 100ms border ease; - + &.drag-over { border-color: @ui-action-border-hover; border-style: solid; box-shadow: 0 3px 8px rgba(0,0,0, .1); transition: 100ms box-shadow ease, 100ms border ease; } -} -.umb-upload-local__dropzone i { - display: block; - color: @ui-action-type; - font-size: 110px; - line-height: 1; + .umb-icon { + display: block; + color: @ui-action-type; + font-size: 6.75rem; + line-height: 1; + margin: 0 auto; + } } .umb-upload-local__select-file { diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-packages.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-packages.less index 07a543cb81..34de1b76f9 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-packages.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-packages.less @@ -136,35 +136,40 @@ justify-content: center; opacity: .6; margin-top: 10px; + + small { + padding: 0 5px; + display: flex; + align-items: center; + justify-content: center; + } + + .umb-icon { + font-size: 0.9rem; + } } -.umb-package-numbers small { - padding: 0 5px; - display: flex; - align-items: center; - justify-content: center; -} +.umb-package-link { + &:hover { + .umb-package-numbers { + opacity: 1; -.umb-package-numbers i { - font-size: 14px; -} + .icon-hearts { + color: @red !important; + } + } + } -.umb-package-link:hover .umb-package-numbers { - opacity: 1; -} + .umb-package-cloud { + margin-top: 0.5rem; + font-size: 0.75rem; + min-height: 1rem; // ensures vertical space is taken up even if "works on cloud" isn't visible -.umb-package-link:hover .umb-package-numbers .icon-hearts { - color: @red !important; -} - -.umb-package-link .umb-package-cloud { - margin-top: 8px; - font-size: 11px; - height: 11px; // ensures vertical space is taken up even if "works on cloud" isn't visible -} - -.umb-package-link .umb-package-cloud .icon-cloud { - color: #2eadaf !important; + .umb-icon { + color: @turquoise !important; + font-size: 0.9rem; + } + } } // Version @@ -182,7 +187,7 @@ .umb-packages-categories { display: flex; - user-select: center; + user-select: none; flex-wrap: wrap; } diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-panel-group.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-panel-group.less index 46d7f04af6..27d09aaa1c 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-panel-group.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-panel-group.less @@ -75,7 +75,7 @@ flex: 0 0 50px; display: flex; justify-content: center; - padding: 0 20px; + padding: 0.25rem 20px; } .umb-panel-group__details-status-content { diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-search-filter.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-search-filter.less index b38f5937c7..a703636f30 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-search-filter.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-search-filter.less @@ -20,12 +20,12 @@ html .umb-search-filter { // "icon-search" class it kept for backward compatibility .umb-icon, - .icon-search { + i.icon-search { color: @gray-8; position: absolute; top: 0; bottom: 0; - left: 10px; + left: 8px; margin: auto 0; pointer-events: none; } diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-tabs.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-tabs.less index 1b249f1c3a..25073250f5 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-tabs.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-tabs.less @@ -7,6 +7,25 @@ margin-bottom: 20px; } +.umb-tabs-nav .badge { + background-color: @red; + animation-duration: 1.4s; + animation-iteration-count: infinite; + animation-name: umb-tab--badge-bounce; + animation-timing-function: ease; + display: none; + margin-left: 5px; + + @keyframes umb-tab--badge-bounce { + 0% { transform: translateY(0); } + 20% { transform: translateY(-6px); } + 40% { transform: translateY(0); } + 55% { transform: translateY(-3px); } + 70% { transform: translateY(0); } + 100% { transform: translateY(0); } + } +} + .umb-tab { display: inline-flex; position: relative; @@ -79,26 +98,37 @@ } } -.show-validation .umb-tab--error > .umb-tab-button, -.show-validation .umb-tab--error > .umb-tab-button:hover, -.show-validation .umb-tab--error > .umb-tab-button:focus { - color: @white !important; - background-color: @red !important; - border-color: @errorBorder; -} -.show-validation.show-validation-type-warning .umb-tab--error > .umb-tab-button, -.show-validation.show-validation-type-warning .umb-tab--error > .umb-tab-button:hover, -.show-validation.show-validation-type-warning .umb-tab--error > .umb-tab-button:focus { - color: @white !important; - background-color: @yellow-d2 !important; - border-color: @warningBorder; -} +.show-validation { -.show-validation .umb-tab--error .umb-tab-button:before { - content: "\e25d"; - font-family: "icomoon"; - margin-right: 5px; - vertical-align: top; + &.show-validation-type-error { + .umb-tab--error { + .badge { + display: block; + background-color: @red; + } + + .umb-tab-button { + &::after { + background-color: @red; + } + } + } + } + + &.show-validation-type-warning { + .umb-tab--error { + .badge { + display: block; + background-color: @yellow-d2; + } + + .umb-tab-button { + &::after { + background-color: @yellow-d2; + } + } + } + } } // tabs tray @@ -111,7 +141,3 @@ .umb-tabs-tray > .umb-tab-button { cursor: pointer; } - -.umb-tabs-tray-item--active { - border-left: 2px solid @ui-active; -} diff --git a/src/Umbraco.Web.UI.Client/src/less/dashboards/healthcheck.less b/src/Umbraco.Web.UI.Client/src/less/dashboards/healthcheck.less index bf01c21dca..57bc1f8518 100644 --- a/src/Umbraco.Web.UI.Client/src/less/dashboards/healthcheck.less +++ b/src/Umbraco.Web.UI.Client/src/less/dashboards/healthcheck.less @@ -75,27 +75,20 @@ font-size: 13px; } -.umb-healthcheck-message i { +.umb-healthcheck-message .umb-icon { font-size: 15px; margin-right: 3px; } .umb-healthcheck-details-link { - color: @turquoise-d1; + color: @turquoise-d1; + + &:hover { + color: @turquoise-d1; + text-decoration: none; + } } -.umb-healthcheck-details-link:hover { - text-decoration: none; - color: @turquoise-d1; -} - - -/* Helpers */ -.align-self-center { - align-self: center; -} - - /* Spacing for boxes */ .umb-air { flex: 0 0 auto; @@ -127,7 +120,6 @@ } .umb-healthcheck-status-icon { - font-size: 20px; margin-top: 2px; } diff --git a/src/Umbraco.Web.UI.Client/src/less/forms.less b/src/Umbraco.Web.UI.Client/src/less/forms.less index 60561f9acc..4000372cdd 100644 --- a/src/Umbraco.Web.UI.Client/src/less/forms.less +++ b/src/Umbraco.Web.UI.Client/src/less/forms.less @@ -65,7 +65,9 @@ label.control-label, .control-label { .form-search small { color: @gray-8; } -.form-search .icon, .form-search .icon-search { + +.form-search .icon, +.form-search i.icon-search { position: absolute; z-index: 1; top: 50%; diff --git a/src/Umbraco.Web.UI.Client/src/less/forms/umb-validation-label.less b/src/Umbraco.Web.UI.Client/src/less/forms/umb-validation-label.less index e5b84fc6ca..54bd03d51b 100644 --- a/src/Umbraco.Web.UI.Client/src/less/forms/umb-validation-label.less +++ b/src/Umbraco.Web.UI.Client/src/less/forms/umb-validation-label.less @@ -1,7 +1,6 @@ .umb-validation-label { position: absolute; z-index: 1; - top: 28px; min-width: 80px; max-width: 260px; padding: 2px 6px; @@ -58,14 +57,22 @@ top: 50%; left: auto; bottom: auto; - border: solid transparent; - content: " "; - height: 0; - width: 0; - position: absolute; - pointer-events: none; - border-color: rgba(255, 255, 255, 0); + border-color: transparent; border-right-color: @red; - border-width: 4px; margin-top: -4px; } + + +.umb-validation-label.-arrow-bottom { + margin-left: 0; + margin-bottom: 10px; +} + +.umb-validation-label.-arrow-bottom:after { + right: auto; + top: 100%; + left: 20px; + bottom: auto; + border-color: transparent; + border-top-color: @red; +} diff --git a/src/Umbraco.Web.UI.Client/src/less/navs.less b/src/Umbraco.Web.UI.Client/src/less/navs.less index 6dab771c94..3d5f5e40f5 100644 --- a/src/Umbraco.Web.UI.Client/src/less/navs.less +++ b/src/Umbraco.Web.UI.Client/src/less/navs.less @@ -2,14 +2,15 @@ // Navs // -------------------------------------------------- -.list-icons li{ - padding-left: 35px; - max-width: 300px -} +.list-icons li { + padding-left: 35px; + max-width: 300px; -.list-icons li > i.icon{ - margin-left: -25px; - padding-right: 7px; + > .umb-icon, + > i.icon { + margin-left: -25px; + padding-right: 7px; + } } .umb-icon.handle, @@ -235,6 +236,21 @@ padding-top: 0; padding-bottom: 0; } +.dropdown-menu > li { + position: relative; +} +.dropdown-menu > li .dropdown-menu--active { + &::after { + content: ""; + position: absolute; + width: 3px; + left:0; + top: 3px; + bottom: 3px; + background-color: @ui-light-active-border; + border-radius: 0 3px 3px 0; + } +} // fix dropdown with checkbox + long text in label .dropdown-menu > li > .flex > label { @@ -254,7 +270,8 @@ border: 0; padding: 8px 20px; color: @ui-option-type; - display: block; + display: flex; + justify-content: start; clear: both; font-weight: normal; line-height: 20px; diff --git a/src/Umbraco.Web.UI.Client/src/less/panel.less b/src/Umbraco.Web.UI.Client/src/less/panel.less index cc87a0edf5..8b1060a7e8 100644 --- a/src/Umbraco.Web.UI.Client/src/less/panel.less +++ b/src/Umbraco.Web.UI.Client/src/less/panel.less @@ -48,7 +48,7 @@ .form-search { flex: 1; - .icon-search { + i.icon-search { top: 16px; } diff --git a/src/Umbraco.Web.UI.Client/src/less/property-editors.less b/src/Umbraco.Web.UI.Client/src/less/property-editors.less index b735b6d7e4..bc14fc2840 100644 --- a/src/Umbraco.Web.UI.Client/src/less/property-editors.less +++ b/src/Umbraco.Web.UI.Client/src/less/property-editors.less @@ -587,6 +587,15 @@ vertical-align: top; } +.umb-cropper-imageholder-buttons { + display: flex; + justify-content: space-between; +} + +.media-crop-details-buttons { + text-align: right; +} + .umb-fileupload, .umb-cropper-gravity { diff --git a/src/Umbraco.Web.UI.Client/src/less/variables.less b/src/Umbraco.Web.UI.Client/src/less/variables.less index 9d114b093e..6877938574 100644 --- a/src/Umbraco.Web.UI.Client/src/less/variables.less +++ b/src/Umbraco.Web.UI.Client/src/less/variables.less @@ -163,7 +163,7 @@ @ui-selected-border-hover: darken(@blueDark, 10%); @ui-light-border: @pinkLight; -@ui-light-type: @gray-4; +@ui-light-type: @gray-3; @ui-light-type-hover: @blueMid; @ui-light-active-border: @pinkLight; diff --git a/src/Umbraco.Web.UI.Client/src/views/common/drawers/help/help.html b/src/Umbraco.Web.UI.Client/src/views/common/drawers/help/help.html index 4f6b283fd8..82a37e6efb 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/drawers/help/help.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/drawers/help/help.html @@ -46,7 +46,7 @@ ng-click="tourGroup.open = !tourGroup.open" aria-expanded="{{tourGroup.open === undefined ? false : tourGroup.open }}"> - + {{tourGroup.group}} Other @@ -101,7 +101,7 @@ {{topic.name}} - + {{topic.description}} @@ -118,9 +118,9 @@ @@ -129,7 +129,7 @@
- +
Visit umbraco.tv
@@ -139,8 +139,7 @@
- - +
Visit our.umbraco.com
diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/compositions/compositions.html b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/compositions/compositions.html index 98db2b0336..7ec69018b6 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/compositions/compositions.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/compositions/compositions.html @@ -49,7 +49,7 @@
  • - +  {{contentTypeEntity.contentType.name}}
  • @@ -59,7 +59,7 @@
    • - + {{group.containerPath}}
    • diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/datatypeconfigurationpicker/datatypeconfigurationpicker.html b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/datatypeconfigurationpicker/datatypeconfigurationpicker.html index cbc5b102cc..7146d6dc7c 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/datatypeconfigurationpicker/datatypeconfigurationpicker.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/datatypeconfigurationpicker/datatypeconfigurationpicker.html @@ -26,7 +26,7 @@
    @@ -36,7 +36,7 @@
  • diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/datatypepicker/datatypepicker.html b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/datatypepicker/datatypepicker.html index d19f537354..5a3f7e9fc6 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/datatypepicker/datatypepicker.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/datatypepicker/datatypepicker.html @@ -37,7 +37,7 @@ ng-click="vm.viewOptionsForEditor(dataType)" title="{{dataType.name}}"> - + {{dataType.name}} @@ -65,7 +65,7 @@ ng-click="vm.pickDataType(dataType)" title="{{dataType.name}}"> - + {{dataType.name}} @@ -86,7 +86,7 @@ ng-click="vm.pickEditor(dataType)" title="{{dataType.name}}"> - + {{dataType.name}} diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/datatypesettings/datatypesettings.html b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/datatypesettings/datatypesettings.html index 77cfc705a3..a4fef28740 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/datatypesettings/datatypesettings.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/datatypesettings/datatypesettings.html @@ -22,7 +22,7 @@
    - + using this editor will get updated with the new settings.
    diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/iconpicker/iconpicker.html b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/iconpicker/iconpicker.html index 19fe679189..41bf92b27e 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/iconpicker/iconpicker.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/iconpicker/iconpicker.html @@ -44,7 +44,7 @@
    • diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/itempicker/itempicker.html b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/itempicker/itempicker.html index c563394ab3..cd65922653 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/itempicker/itempicker.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/itempicker/itempicker.html @@ -13,23 +13,24 @@ -
  • diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/propertysettings/propertysettings.html b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/propertysettings/propertysettings.html index 482345c3b3..8379062807 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/propertysettings/propertysettings.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/propertysettings/propertysettings.html @@ -65,21 +65,21 @@
    @@ -153,7 +153,7 @@
    -
    +
    @@ -162,7 +162,7 @@
    -
    +
    @@ -159,7 +159,7 @@ diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/sectionpicker/sectionpicker.html b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/sectionpicker/sectionpicker.html index 452f4b2364..296cdb61d7 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/sectionpicker/sectionpicker.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/sectionpicker/sectionpicker.html @@ -23,10 +23,11 @@
    diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/templatesections/templatesections.html b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/templatesections/templatesections.html index b4e8d7fbe8..1fdfdfb145 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/templatesections/templatesections.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/templatesections/templatesections.html @@ -17,29 +17,33 @@
    -
    +
    + +
    -
    +
    - +
    -
    -
    +
    + +
    +
    - +
    - + - +
    @@ -51,7 +55,7 @@
    - +
    @@ -60,18 +64,20 @@
    -
    -
    +
    + +
    +
    - +
    - + - +
    diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/treepicker/treepicker.html b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/treepicker/treepicker.html index 522301d76f..cfe481c142 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/treepicker/treepicker.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/treepicker/treepicker.html @@ -21,7 +21,7 @@
    diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/usergrouppicker/usergrouppicker.html b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/usergrouppicker/usergrouppicker.html index dbddb141dd..1e95482d9a 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/usergrouppicker/usergrouppicker.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/usergrouppicker/usergrouppicker.html @@ -36,7 +36,7 @@
    - +
    diff --git a/src/Umbraco.Web.UI.Client/src/views/common/overlays/itempicker/itempicker.html b/src/Umbraco.Web.UI.Client/src/views/common/overlays/itempicker/itempicker.html index 2a31fbd6c4..2798d5615f 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/overlays/itempicker/itempicker.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/overlays/itempicker/itempicker.html @@ -13,7 +13,7 @@
    Paste from clipboard
    @@ -21,7 +21,7 @@
  • @@ -36,7 +36,7 @@
  • @@ -44,7 +44,7 @@
  • diff --git a/src/Umbraco.Web.UI.Client/src/views/common/overlays/user/user.html b/src/Umbraco.Web.UI.Client/src/views/common/overlays/user/user.html index 0b976688c0..8f2627795f 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/overlays/user/user.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/overlays/user/user.html @@ -1,11 +1,11 @@
    -
    +
    @@ -59,7 +59,7 @@ ng-class="login.properties.ButtonStyle" id="{{login.authType}}"> - + Link your {{login.caption}} account @@ -71,22 +71,22 @@ id="{{login.authType}}" name="provider" value="{{login.authType}}"> - + Un-link your {{login.caption}} account
    - +
  • -
    +
    • - + {{item.name}}
    • diff --git a/src/Umbraco.Web.UI.Client/src/views/components/application/umb-app-header.html b/src/Umbraco.Web.UI.Client/src/views/components/application/umb-app-header.html index 7271294a09..3ad4ebc188 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/application/umb-app-header.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/application/umb-app-header.html @@ -14,7 +14,7 @@ Open backoffice search... - +
    • @@ -22,7 +22,7 @@ Open/Close backoffice help... - +
    • diff --git a/src/Umbraco.Web.UI.Client/src/views/components/application/umb-contextmenu.html b/src/Umbraco.Web.UI.Client/src/views/components/application/umb-contextmenu.html index 0aa2e258a3..107103b1b4 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/application/umb-contextmenu.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/application/umb-contextmenu.html @@ -2,7 +2,7 @@ aria-labelledby="contextmenu-title" aria-describedby="contextmenu-description" on-outside-click="outSideClick()">
      -

      {{ menuDialogTitle }}

      +

      {{menuDialogTitle}}

      Select one of the options to edit the node.

      @@ -14,8 +14,8 @@ ng-class="{sep:action.separator, '-opens-dialog': action.opensDialog}" ng-repeat="action in menuActions">
    diff --git a/src/Umbraco.Web.UI.Client/src/views/components/application/umb-navigation.html b/src/Umbraco.Web.UI.Client/src/views/components/application/umb-navigation.html index bb4b025410..ff647ed411 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/application/umb-navigation.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/application/umb-navigation.html @@ -15,7 +15,7 @@ {{selectedLanguage.name}} - +
    diff --git a/src/Umbraco.Web.UI.Client/src/views/components/buttons/umb-button.html b/src/Umbraco.Web.UI.Client/src/views/components/buttons/umb-button.html index fceb27b6e7..552d0ffdb5 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/buttons/umb-button.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/buttons/umb-button.html @@ -1,8 +1,8 @@
    - - + +
    @@ -15,7 +15,7 @@ hotkey="{{vm.shortcut}}" hotkey-when-hidden="{{vm.shortcutWhenHidden}}"> - + {{vm.buttonLabel}} @@ -33,7 +33,7 @@ aria-haspopup="{{vm.hasPopup}}" aria-expanded="{{vm.isExpanded}}"> - + {{vm.buttonLabel}} @@ -48,7 +48,7 @@ ng-disabled="vm.disabled" umb-auto-focus="{{vm.autoFocus && !vm.disabled ? 'true' : 'false'}}"> - + {{vm.buttonLabel}} diff --git a/src/Umbraco.Web.UI.Client/src/views/components/buttons/umb-toggle.html b/src/Umbraco.Web.UI.Client/src/views/components/buttons/umb-toggle.html index 449c436149..5c417933e4 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/buttons/umb-toggle.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/buttons/umb-toggle.html @@ -5,8 +5,8 @@
    - - + +
    diff --git a/src/Umbraco.Web.UI.Client/src/views/components/content/umb-content-node-info.html b/src/Umbraco.Web.UI.Client/src/views/components/content/umb-content-node-info.html index e4fdb7fc33..028a002c7c 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/content/umb-content-node-info.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/content/umb-content-node-info.html @@ -9,7 +9,7 @@
  • {{url.culture}} - + {{url.text}}
    @@ -31,7 +31,7 @@ +
  • + + + + + + +
  • + + \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editor-tab-bar.html b/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editor-tab-bar.html new file mode 100644 index 0000000000..99b8790420 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editor-tab-bar.html @@ -0,0 +1,3 @@ +
    + +
    diff --git a/src/Umbraco.Web.UI.Client/src/views/components/elementeditor/umb-element-editor-content.component.html b/src/Umbraco.Web.UI.Client/src/views/components/elementeditor/umb-element-editor-content.component.html index ecbe880eee..5005edadc5 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/elementeditor/umb-element-editor-content.component.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/elementeditor/umb-element-editor-content.component.html @@ -1,14 +1,41 @@
    + + + + + + + + + +
    + + +
    + +
    +
    +
    +
    + ng-repeat="group in vm.model.variants[0].tabs track by group.key" + ng-show="(group.parentAlias === vm.activeTabAlias && group.type === 0) || vm.tabs.length === 0">
    {{ group.label }}
    -
    +
    { + + contentTypeHelper.defineParentAliasOnGroups(newValue); + contentTypeHelper.relocateDisorientedGroups(newValue); + + vm.tabs = $filter("filter")(newValue, (tab) => { + return tab.type === 1; + }); + + if (vm.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); + + setActiveTab(vm.tabs[0]); + } + }); + function getScope() { return $scope; } + function setActiveTab (tab) { + vm.activeTabAlias = tab.alias; + vm.tabs.forEach(tab => tab.active = false); + tab.active = true; + } } })(); diff --git a/src/Umbraco.Web.UI.Client/src/views/components/forms/umb-checkbox.html b/src/Umbraco.Web.UI.Client/src/views/components/forms/umb-checkbox.html index c25541da21..a093e66b61 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/forms/umb-checkbox.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/forms/umb-checkbox.html @@ -1,38 +1,26 @@ diff --git a/src/Umbraco.Web.UI.Client/src/views/components/forms/umb-radiobutton.html b/src/Umbraco.Web.UI.Client/src/views/components/forms/umb-radiobutton.html index 067e1ca8d9..fcdacb4423 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/forms/umb-radiobutton.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/forms/umb-radiobutton.html @@ -1,36 +1,24 @@ diff --git a/src/Umbraco.Web.UI.Client/src/views/components/forms/umb-search-filter.html b/src/Umbraco.Web.UI.Client/src/views/components/forms/umb-search-filter.html index 6a17f33c50..7792d381ba 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/forms/umb-search-filter.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/forms/umb-search-filter.html @@ -1,7 +1,14 @@
    - + +
    - + +
    - + +
    diff --git a/src/Umbraco.Web.UI.Client/src/views/components/media/umb-media-node-info.html b/src/Umbraco.Web.UI.Client/src/views/components/media/umb-media-node-info.html index b608b3eb6e..681d4f953a 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/media/umb-media-node-info.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/media/umb-media-node-info.html @@ -18,7 +18,7 @@
    -
    +
    {{::reference.name}}
    {{::reference.alias}}
    @@ -93,7 +93,7 @@
    -
    +
    {{::reference.name}}
    {{::reference.alias}}
    @@ -129,7 +129,7 @@
    -
    +
    {{::reference.name}}
    {{::reference.alias}}
    diff --git a/src/Umbraco.Web.UI.Client/src/views/components/mediacard/umb-media-card.html b/src/Umbraco.Web.UI.Client/src/views/components/mediacard/umb-media-card.html index 01ce31415e..6e7e63440d 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/mediacard/umb-media-card.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/mediacard/umb-media-card.html @@ -4,12 +4,12 @@

    - +

    - +

    diff --git a/src/Umbraco.Web.UI.Client/src/views/components/overlays/umb-overlay.html b/src/Umbraco.Web.UI.Client/src/views/components/overlays/umb-overlay.html index c41caca004..70914bcfb7 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/overlays/umb-overlay.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/overlays/umb-overlay.html @@ -21,10 +21,14 @@
    - -

    {{ model.itemDetails.title }}

    + + +

    {{model.itemDetails.title}}

    -

    {{ model.itemDetails.description }}

    +

    {{model.itemDetails.description}}

    @@ -32,9 +36,9 @@

    - {{ model.confirmSubmit.title }} + {{model.confirmSubmit.title}}

    -

    {{ model.confirmSubmit.description }}

    +

    {{model.confirmSubmit.description}}

    -
    +
    diff --git a/src/Umbraco.Web.UI.Client/src/views/documentTypes/delete.html b/src/Umbraco.Web.UI.Client/src/views/documentTypes/delete.html index 3aa3193069..13ea27b45c 100644 --- a/src/Umbraco.Web.UI.Client/src/views/documentTypes/delete.html +++ b/src/Umbraco.Web.UI.Client/src/views/documentTypes/delete.html @@ -17,7 +17,7 @@

    - + All Documents diff --git a/src/Umbraco.Web.UI.Client/src/views/documentTypes/edit.controller.js b/src/Umbraco.Web.UI.Client/src/views/documentTypes/edit.controller.js index 3946d09578..3672af900c 100644 --- a/src/Umbraco.Web.UI.Client/src/views/documentTypes/edit.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/documentTypes/edit.controller.js @@ -50,6 +50,7 @@ "treeHeaders_templates", "main_sections", "shortcuts_navigateSections", + "shortcuts_addTab", "shortcuts_addGroup", "shortcuts_addProperty", "defaultdialogs_selectEditor", @@ -86,15 +87,16 @@ // keyboard shortcuts vm.labels.sections = values[4]; vm.labels.navigateSections = values[5]; - vm.labels.addGroup = values[6]; - vm.labels.addProperty = values[7]; - vm.labels.addEditor = values[8]; - vm.labels.editDataType = values[9]; - vm.labels.toggleListView = values[10]; - vm.labels.allowAsRoot = values[11]; - vm.labels.addChildNode = values[12]; - vm.labels.addTemplate = values[13]; - vm.labels.allowCultureVariants = values[14]; + vm.labels.addTab = values[6] + vm.labels.addGroup = values[7]; + vm.labels.addProperty = values[8]; + vm.labels.addEditor = values[9]; + vm.labels.editDataType = values[10]; + vm.labels.toggleListView = values[11]; + vm.labels.allowAsRoot = values[12]; + vm.labels.addChildNode = values[13]; + vm.labels.addTemplate = values[14]; + vm.labels.allowCultureVariants = values[15]; vm.page.keyboardShortcutsOverview = [ { @@ -110,6 +112,10 @@ { "name": vm.labels.design, "shortcuts": [ + { + "description": vm.labels.addTab, + "keys": [{ "key": "alt" }, { "key": "shift" }, { "key": "a" }] + }, { "description": vm.labels.addGroup, "keys": [{ "key": "alt" }, { "key": "shift" }, { "key": "g" }] @@ -320,6 +326,7 @@ infiniteMode: infiniteMode, // we need to rebind... the IDs that have been created! rebindCallback: function (origContentType, savedContentType) { + vm.contentType.ModelState = savedContentType.ModelState; vm.contentType.id = savedContentType.id; vm.contentType.groups.forEach(function (group) { if (!group.name) return; @@ -348,6 +355,9 @@ }); } }).then(function (data) { + // allow UI to access server validation state + vm.contentType.ModelState = data.ModelState; + //success // we don't need to sync the tree in infinite mode if (!infiniteMode) { @@ -374,6 +384,8 @@ }, function (err) { //error if (err) { + // allow UI to access server validation state + vm.contentType.ModelState = err.data.ModelState; editorState.set($scope.content); } else { diff --git a/src/Umbraco.Web.UI.Client/src/views/documentTypes/property.html b/src/Umbraco.Web.UI.Client/src/views/documentTypes/property.html index ccd632246c..659e52b7b6 100644 --- a/src/Umbraco.Web.UI.Client/src/views/documentTypes/property.html +++ b/src/Umbraco.Web.UI.Client/src/views/documentTypes/property.html @@ -1,4 +1,4 @@

    - + {{property.description}} -
    \ No newline at end of file +
    diff --git a/src/Umbraco.Web.UI.Client/src/views/documentTypes/views/listview/listview.html b/src/Umbraco.Web.UI.Client/src/views/documentTypes/views/listview/listview.html index 5cd04bd834..7eea4d541b 100644 --- a/src/Umbraco.Web.UI.Client/src/views/documentTypes/views/listview/listview.html +++ b/src/Umbraco.Web.UI.Client/src/views/documentTypes/views/listview/listview.html @@ -2,8 +2,8 @@
    -
    - +
    +
    -
    - +
    +
    -
    - +
    +
    @@ -43,8 +43,8 @@
    -
    - +
    +
    @@ -61,8 +61,8 @@
    -
    - +
    +
    @@ -77,9 +77,9 @@
    -
    - -
    +
    + +
    diff --git a/src/Umbraco.Web.UI.Client/src/views/documentTypes/views/templates/templates.html b/src/Umbraco.Web.UI.Client/src/views/documentTypes/views/templates/templates.html index 385979a572..279ffb73c0 100644 --- a/src/Umbraco.Web.UI.Client/src/views/documentTypes/views/templates/templates.html +++ b/src/Umbraco.Web.UI.Client/src/views/documentTypes/views/templates/templates.html @@ -4,8 +4,8 @@
    -
    - +
    +
    diff --git a/src/Umbraco.Web.UI.Client/src/views/languages/overview.html b/src/Umbraco.Web.UI.Client/src/views/languages/overview.html index 9a11691e09..2cecb2b345 100644 --- a/src/Umbraco.Web.UI.Client/src/views/languages/overview.html +++ b/src/Umbraco.Web.UI.Client/src/views/languages/overview.html @@ -38,7 +38,7 @@ - + {{ language.name }} diff --git a/src/Umbraco.Web.UI.Client/src/views/logViewer/overview.html b/src/Umbraco.Web.UI.Client/src/views/logViewer/overview.html index 3d5dd7f610..d3f2f86428 100644 --- a/src/Umbraco.Web.UI.Client/src/views/logViewer/overview.html +++ b/src/Umbraco.Web.UI.Client/src/views/logViewer/overview.html @@ -37,13 +37,13 @@ - + - + diff --git a/src/Umbraco.Web.UI.Client/src/views/logViewer/search.controller.js b/src/Umbraco.Web.UI.Client/src/views/logViewer/search.controller.js index d8f799b18e..ea803919be 100644 --- a/src/Umbraco.Web.UI.Client/src/views/logViewer/search.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/logViewer/search.controller.js @@ -24,7 +24,7 @@ }, { name: 'Information', - logTypeColor: 'success' + logTypeColor: 'success' }, { name: 'Warning', @@ -44,7 +44,7 @@ enabled: false, interval: 0, promise: null, - + defaultButton: { labelKey: "logViewer_polling", handler: function() { @@ -156,6 +156,8 @@ vm.search = search; vm.getFilterName = getFilterName; vm.setLogLevelFilter = setLogLevelFilter; + vm.selectAllLogLevelFilters = selectAllLogLevelFilters; + vm.deselectAllLogLevelFilters = deselectAllLogLevelFilters; vm.toggleOrderBy = toggleOrderBy; vm.selectSearch = selectSearch; vm.resetSearch = resetSearch; @@ -259,7 +261,7 @@ } function setLogTypeColor(logItems) { - logItems.forEach(logItem => + logItems.forEach(logItem => logItem.logTypeColor = vm.logLevels.find(x => x.name === logItem.Level).logTypeColor); } @@ -295,6 +297,24 @@ getLogs(); } + function updateAllLogLevelFilterCheckboxes(bool) { + vm.logLevels.forEach(logLevel => logLevel.selected = bool); + } + + function selectAllLogLevelFilters() { + vm.logOptions.logLevels = vm.logLevels.map(logLevel => logLevel.name); + updateAllLogLevelFilterCheckboxes(true); + + getLogs(); + } + + function deselectAllLogLevelFilters() { + vm.logOptions.logLevels = []; + updateAllLogLevelFilterCheckboxes(false); + + getLogs(); + } + function toggleOrderBy(){ vm.logOptions.orderDirection = vm.logOptions.orderDirection === 'Descending' ? 'Ascending' : 'Descending'; diff --git a/src/Umbraco.Web.UI.Client/src/views/logViewer/search.html b/src/Umbraco.Web.UI.Client/src/views/logViewer/search.html index 49e0f9ef2e..5e2583ab7a 100644 --- a/src/Umbraco.Web.UI.Client/src/views/logViewer/search.html +++ b/src/Umbraco.Web.UI.Client/src/views/logViewer/search.html @@ -48,6 +48,29 @@
    + + + + + + + + + + + + +
    @@ -56,7 +79,7 @@ @@ -68,18 +91,18 @@ @@ -90,7 +113,7 @@ {{search.query}} @@ -133,7 +156,7 @@ Timestamp -   + Level Machine @@ -171,10 +194,10 @@ {{key}} - - - - + + + + {{val.Value}} @@ -187,7 +210,7 @@ aria-haspopup="true" aria-expanded="{{log.searchDropdownOpen === undefined ? false : log.searchDropdownOpen}}" ng-click="log.searchDropdownOpen = !log.searchDropdownOpen"> - + Search diff --git a/src/Umbraco.Web.UI.Client/src/views/macros/infiniteeditors/parameter.html b/src/Umbraco.Web.UI.Client/src/views/macros/infiniteeditors/parameter.html index ca3df1d4eb..f6ce3cc222 100644 --- a/src/Umbraco.Web.UI.Client/src/views/macros/infiniteeditors/parameter.html +++ b/src/Umbraco.Web.UI.Client/src/views/macros/infiniteeditors/parameter.html @@ -49,7 +49,7 @@ @@ -37,9 +37,11 @@ - - {{ child.name }} - {{ child.createDate }} + + + + {{child.name}} + {{child.createDate}} diff --git a/src/Umbraco.Web.UI.Client/src/views/mediaTypes/create.html b/src/Umbraco.Web.UI.Client/src/views/mediaTypes/create.html index 1ce8ab1465..6f74174776 100644 --- a/src/Umbraco.Web.UI.Client/src/views/mediaTypes/create.html +++ b/src/Umbraco.Web.UI.Client/src/views/mediaTypes/create.html @@ -8,7 +8,7 @@
    -
    +
    diff --git a/src/Umbraco.Web.UI.Client/src/views/mediaTypes/delete.html b/src/Umbraco.Web.UI.Client/src/views/mediaTypes/delete.html index 89bf449fb3..051126f2d5 100644 --- a/src/Umbraco.Web.UI.Client/src/views/mediaTypes/delete.html +++ b/src/Umbraco.Web.UI.Client/src/views/mediaTypes/delete.html @@ -15,7 +15,7 @@

    - + All media items diff --git a/src/Umbraco.Web.UI.Client/src/views/mediaTypes/views/listview/listview.html b/src/Umbraco.Web.UI.Client/src/views/mediaTypes/views/listview/listview.html index 21d29b2161..7a88502fde 100644 --- a/src/Umbraco.Web.UI.Client/src/views/mediaTypes/views/listview/listview.html +++ b/src/Umbraco.Web.UI.Client/src/views/mediaTypes/views/listview/listview.html @@ -2,8 +2,8 @@

    -
    - +
    +
    - \ No newline at end of file + diff --git a/src/Umbraco.Web.UI.Client/src/views/mediaTypes/views/permissions/permissions.html b/src/Umbraco.Web.UI.Client/src/views/mediaTypes/views/permissions/permissions.html index 09e7ab4f41..3fe28e49b3 100644 --- a/src/Umbraco.Web.UI.Client/src/views/mediaTypes/views/permissions/permissions.html +++ b/src/Umbraco.Web.UI.Client/src/views/mediaTypes/views/permissions/permissions.html @@ -3,8 +3,8 @@
    -
    - +
    +
    -
    - +
    +
    { + + contentTypeHelper.defineParentAliasOnGroups(newValue); + contentTypeHelper.relocateDisorientedGroups(newValue); + + vm.tabs = $filter("filter")(newValue, (tab) => { + return tab.type === 1; + }); + + if (vm.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); + + setActiveTab(vm.tabs[0]); + } + }); + + function setActiveTab (tab) { + vm.activeTabAlias = tab.alias; + vm.tabs.forEach(tab => tab.active = false); + tab.active = true; + } + + function hideSystemProperties (property) { // hide some specific, known properties by alias if (property.alias === "_umb_id" || property.alias === "_umb_doctype") { return false; diff --git a/src/Umbraco.Web.UI.Client/src/views/member/apps/content/content.html b/src/Umbraco.Web.UI.Client/src/views/member/apps/content/content.html index 29051cd855..305b5d4e77 100644 --- a/src/Umbraco.Web.UI.Client/src/views/member/apps/content/content.html +++ b/src/Umbraco.Web.UI.Client/src/views/member/apps/content/content.html @@ -1,13 +1,38 @@
    -
    + + + + + + + + + + + + + + +
    {{ group.label }}
    -
    - +
    + +
    diff --git a/src/Umbraco.Web.UI.Client/src/views/member/create.html b/src/Umbraco.Web.UI.Client/src/views/member/create.html index 1762308a2c..b25856d9ba 100644 --- a/src/Umbraco.Web.UI.Client/src/views/member/create.html +++ b/src/Umbraco.Web.UI.Client/src/views/member/create.html @@ -7,7 +7,7 @@
  • diff --git a/src/Umbraco.Web.UI.Client/src/views/memberTypes/delete.html b/src/Umbraco.Web.UI.Client/src/views/memberTypes/delete.html index 90ed968da7..5930339d6e 100644 --- a/src/Umbraco.Web.UI.Client/src/views/memberTypes/delete.html +++ b/src/Umbraco.Web.UI.Client/src/views/memberTypes/delete.html @@ -6,7 +6,7 @@

    - + All members using this member type will be deleted permanently, please confirm you want to delete these as well.

    diff --git a/src/Umbraco.Web.UI.Client/src/views/packages/views/created.html b/src/Umbraco.Web.UI.Client/src/views/packages/views/created.html index 64b2b20bd2..c9a5a70148 100644 --- a/src/Umbraco.Web.UI.Client/src/views/packages/views/created.html +++ b/src/Umbraco.Web.UI.Client/src/views/packages/views/created.html @@ -19,7 +19,7 @@
    - +
    diff --git a/src/Umbraco.Web.UI.Client/src/views/partialViewMacros/create.html b/src/Umbraco.Web.UI.Client/src/views/partialViewMacros/create.html index f7c1280094..9afd3447ea 100644 --- a/src/Umbraco.Web.UI.Client/src/views/partialViewMacros/create.html +++ b/src/Umbraco.Web.UI.Client/src/views/partialViewMacros/create.html @@ -12,25 +12,25 @@
    • @@ -45,7 +45,7 @@
      • diff --git a/src/Umbraco.Web.UI.Client/src/views/partialViews/create.html b/src/Umbraco.Web.UI.Client/src/views/partialViews/create.html index 6fbb69fa80..3203ed1b25 100644 --- a/src/Umbraco.Web.UI.Client/src/views/partialViews/create.html +++ b/src/Umbraco.Web.UI.Client/src/views/partialViews/create.html @@ -11,19 +11,19 @@
        • @@ -35,7 +35,7 @@
          • diff --git a/src/Umbraco.Web.UI.Client/src/views/prevalueeditors/imagepicker.html b/src/Umbraco.Web.UI.Client/src/views/prevalueeditors/imagepicker.html index 9f65035c23..fd9075f907 100644 --- a/src/Umbraco.Web.UI.Client/src/views/prevalueeditors/imagepicker.html +++ b/src/Umbraco.Web.UI.Client/src/views/prevalueeditors/imagepicker.html @@ -6,7 +6,7 @@
            @@ -17,7 +17,7 @@ ng-class="{'add-link-square': !model.value }" ng-click="add()" ng-hide="model.value"> - +
    diff --git a/src/Umbraco.Web.UI.Client/src/views/prevalueeditors/mediafolderpicker.html b/src/Umbraco.Web.UI.Client/src/views/prevalueeditors/mediafolderpicker.html index fc6af3a3e8..e3449f07b9 100644 --- a/src/Umbraco.Web.UI.Client/src/views/prevalueeditors/mediafolderpicker.html +++ b/src/Umbraco.Web.UI.Client/src/views/prevalueeditors/mediafolderpicker.html @@ -6,7 +6,7 @@

    - +

    @@ -19,14 +19,14 @@
  • diff --git a/src/Umbraco.Web.UI.Client/src/views/prevalueeditors/multivalues.html b/src/Umbraco.Web.UI.Client/src/views/prevalueeditors/multivalues.html index 43520b00da..f57254ad85 100644 --- a/src/Umbraco.Web.UI.Client/src/views/prevalueeditors/multivalues.html +++ b/src/Umbraco.Web.UI.Client/src/views/prevalueeditors/multivalues.html @@ -9,7 +9,7 @@
    - +
    diff --git a/src/Umbraco.Web.UI.Client/src/views/prevalueeditors/treesource.html b/src/Umbraco.Web.UI.Client/src/views/prevalueeditors/treesource.html index c9da687f0d..f56a6c8656 100644 --- a/src/Umbraco.Web.UI.Client/src/views/prevalueeditors/treesource.html +++ b/src/Umbraco.Web.UI.Client/src/views/prevalueeditors/treesource.html @@ -26,7 +26,7 @@ Choose a root node
    - +
    @@ -40,7 +40,7 @@
    • - +
      @@ -66,7 +66,7 @@
    • - +
    diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklistentryeditors/inlineblock/inlineblock.editor.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklistentryeditors/inlineblock/inlineblock.editor.html index c6b5e0a4fb..54210a522a 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklistentryeditors/inlineblock/inlineblock.editor.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklistentryeditors/inlineblock/inlineblock.editor.html @@ -5,7 +5,7 @@ ng-click="vm.openBlock(block)" ng-focus="block.focus"> - + {{block.label}}
    diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklistentryeditors/inlineblock/inlineblock.editor.less b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklistentryeditors/inlineblock/inlineblock.editor.less index 5b155ac5ad..b8ffcff3ec 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklistentryeditors/inlineblock/inlineblock.editor.less +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklistentryeditors/inlineblock/inlineblock.editor.less @@ -10,6 +10,12 @@ border-color: @gray-8; } + .umb-editor-tab-bar { + margin: 0; + position: static; + padding: 0; + } + > button { width: 100%; min-height: 48px; diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklistentryeditors/labelblock/labelblock.editor.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklistentryeditors/labelblock/labelblock.editor.html index a5c4993505..65530f0595 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklistentryeditors/labelblock/labelblock.editor.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklistentryeditors/labelblock/labelblock.editor.html @@ -3,6 +3,6 @@ ng-focus="block.focus" ng-class="{ '--active': block.active, '--error': parentForm.$invalid && valFormManager.isShowingValidation() }" val-server-property-class=""> - + {{block.label}} diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklistentryeditors/unsupportedblock/unsupportedblock.editor.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklistentryeditors/unsupportedblock/unsupportedblock.editor.html index 2252db3745..0c0d98105d 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklistentryeditors/unsupportedblock/unsupportedblock.editor.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklistentryeditors/unsupportedblock/unsupportedblock.editor.html @@ -1,6 +1,6 @@
    - + {{block.label}}
    diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/blocklist.blockconfiguration.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/blocklist.blockconfiguration.html index 2bd4be1505..de6a409415 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/blocklist.blockconfiguration.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/blocklist.blockconfiguration.html @@ -1,13 +1,6 @@ -
    +
    -
    +
    diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/blocklist.blockconfiguration.overlay.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/blocklist.blockconfiguration.overlay.html index a15a2fda25..cc324f70d5 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/blocklist.blockconfiguration.overlay.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/blocklist.blockconfiguration.overlay.html @@ -46,7 +46,7 @@
    @@ -67,7 +67,7 @@
    @@ -113,7 +113,7 @@
    @@ -130,10 +130,10 @@
    @@ -192,7 +192,7 @@
    diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/umb-block-list-property-editor.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/umb-block-list-property-editor.html index a6c1eb2199..3a1ea38da1 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/umb-block-list-property-editor.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/umb-block-list-property-editor.html @@ -14,7 +14,7 @@ ng-controller="Umbraco.PropertyEditors.BlockListPropertyEditor.CreateButtonController as inlineCreateButtonCtrl" ng-mousemove="inlineCreateButtonCtrl.onMouseMove($event)">
    - +
    @@ -36,15 +36,14 @@ Add content Add content -
    - +
    diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/datepicker/datepicker.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/datepicker/datepicker.html index f5ac69b9b8..548f3aed0b 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/datepicker/datepicker.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/datepicker/datepicker.html @@ -20,11 +20,11 @@ val-server="value" class="datepickerinput" /> - +
    diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/dialogs/layoutconfig.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/dialogs/layoutconfig.html index 5e05f56b48..457986bc19 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/dialogs/layoutconfig.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/dialogs/layoutconfig.html @@ -44,9 +44,9 @@ {{currentSection.grid}}
    - + diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/dialogs/rowconfig.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/dialogs/rowconfig.html index 312c158351..f5a6191ad1 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/dialogs/rowconfig.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/dialogs/rowconfig.html @@ -18,7 +18,7 @@
    -

    +

    Modifying a row configuration name will result in loss of @@ -55,7 +55,7 @@ {{currentCell.grid}}

    @@ -83,11 +83,11 @@ - - - + + + @@ -109,7 +109,7 @@ - + ({{editor.alias}}) diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/embed.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/embed.html index 9673109816..d3e14c5db1 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/embed.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/embed.html @@ -1,7 +1,7 @@
    diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/macro.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/macro.html index 300ec91bcc..9db4c324ec 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/macro.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/macro.html @@ -2,7 +2,7 @@
    @@ -207,7 +204,7 @@
    @@ -41,7 +41,7 @@
    @@ -82,7 +82,7 @@

    {{layout.label || layout.name}}

    @@ -91,7 +91,7 @@ @@ -116,9 +116,9 @@ ng-model="model.value.config">
  • - + {{configValue.label}}
  • @@ -127,7 +127,7 @@
    • @@ -143,9 +143,9 @@ ng-model="model.value.styles">
    • - + {{style.label}}
    • @@ -154,7 +154,7 @@
      • diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/imagecropper/imagecropper.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/imagecropper/imagecropper.html index 9dc1a3b91a..9953ea2cb8 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/imagecropper/imagecropper.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/imagecropper/imagecropper.html @@ -45,8 +45,13 @@ on-value-changed="focalPointChanged(left, top)" on-image-loaded="imageLoaded(isCroppable, hasDimensions)"> - - +
        + + + +
    diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/imagecropper/imagecropper.prevalues.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/imagecropper/imagecropper.prevalues.html index 9589909639..7d08ce43e4 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/imagecropper/imagecropper.prevalues.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/imagecropper/imagecropper.prevalues.html @@ -60,7 +60,7 @@
    - +

    {{item.alias}} ({{item.width}}px × {{item.height}}px)

    diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/includeproperties.prevalues.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/includeproperties.prevalues.html index 7d863f6730..5db348dbff 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/includeproperties.prevalues.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/includeproperties.prevalues.html @@ -27,7 +27,7 @@ - +
    @@ -54,7 +54,7 @@ diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/layouts.prevalues.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/layouts.prevalues.html index 295345a827..95bdb3644f 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/layouts.prevalues.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/layouts.prevalues.html @@ -6,7 +6,7 @@
    - +
    @@ -16,7 +16,7 @@ ng-click="vm.openIconPicker(layout)" umb-auto-focus="{{layout.isSystem !== 1}}"> - + @@ -37,7 +37,7 @@
    @@ -53,12 +53,12 @@ @@ -69,7 +69,7 @@
  • diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker3/umb-media-picker3-property-editor.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker3/umb-media-picker3-property-editor.html index d523bc6f2d..daf9566e2d 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker3/umb-media-picker3-property-editor.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker3/umb-media-picker3-property-editor.html @@ -15,7 +15,7 @@ ng-controller="Umbraco.PropertyEditors.MediaPicker3PropertyEditor.CreateButtonController as inlineCreateButtonCtrl" ng-mousemove="inlineCreateButtonCtrl.onMouseMove($event)">
    - +
    @@ -27,10 +27,10 @@ allowed-types="vm.allowedTypes">
    @@ -45,7 +45,7 @@ ng-disabled="!vm.allowAdd" ng-click="vm.addMediaAt(vm.model.value.length, $event)">
    - + Add
    diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/multipletextbox/multipletextbox.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/multipletextbox/multipletextbox.html index d1c202b867..9e90666eac 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/multipletextbox/multipletextbox.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/multipletextbox/multipletextbox.html @@ -9,11 +9,11 @@ focus-when="{{item.hasFocus}}" />
    - +
    +
    + +
    +
    \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.controller.js index 446fb8c076..64fc40d84d 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.controller.js @@ -522,10 +522,14 @@ ]; // Initialize - var scaffoldsLoaded = 0; vm.scaffolds = []; - _.each(model.config.contentTypes, function (contentType) { - contentResource.getScaffold(-20, contentType.ncAlias).then(function (scaffold) { + + contentResource.getScaffolds(-20, contentTypeAliases).then(function (scaffolds){ + // Loop through all the content types + _.each(model.config.contentTypes, function (contentType){ + // Get the scaffold from the result + var scaffold = scaffolds[contentType.ncAlias]; + // make sure it's an element type before allowing the user to create new ones if (scaffold.isElement) { // remove all tabs except the specified tab @@ -554,13 +558,10 @@ // Store the scaffold object vm.scaffolds.push(scaffold); } - - scaffoldsLoaded++; - initIfAllScaffoldsHaveLoaded(); - }, function (error) { - scaffoldsLoaded++; - initIfAllScaffoldsHaveLoaded(); }); + + // Initialize once all scaffolds have been loaded + initNestedContent(); }); /** @@ -586,57 +587,50 @@ }); } - var initIfAllScaffoldsHaveLoaded = function () { + var initNestedContent = function () { // Initialize when all scaffolds have loaded - if (model.config.contentTypes.length === scaffoldsLoaded) { - // Because we're loading the scaffolds async one at a time, we need to - // sort them explicitly according to the sort order defined by the data type. - contentTypeAliases = []; - _.each(model.config.contentTypes, function (contentType) { - contentTypeAliases.push(contentType.ncAlias); - }); - vm.scaffolds = $filter("orderBy")(vm.scaffolds, function (s) { - return contentTypeAliases.indexOf(s.contentTypeAlias); - }); + // Sort the scaffold explicitly according to the sort order defined by the data type. + vm.scaffolds = $filter("orderBy")(vm.scaffolds, function (s) { + return contentTypeAliases.indexOf(s.contentTypeAlias); + }); - // Convert stored nodes - if (model.value) { - for (var i = 0; i < model.value.length; i++) { - var item = model.value[i]; - var scaffold = getScaffold(item.ncContentTypeAlias); - if (scaffold == null) { - // No such scaffold - the content type might have been deleted. We need to skip it. - continue; - } - createNode(scaffold, item); + // Convert stored nodes + if (model.value) { + for (var i = 0; i < model.value.length; i++) { + var item = model.value[i]; + var scaffold = getScaffold(item.ncContentTypeAlias); + if (scaffold == null) { + // No such scaffold - the content type might have been deleted. We need to skip it. + continue; } + createNode(scaffold, item); } - - // Enforce min items if we only have one scaffold type - var modelWasChanged = false; - if (vm.nodes.length < vm.minItems && vm.scaffolds.length === 1) { - for (var i = vm.nodes.length; i < model.config.minItems; i++) { - addNode(vm.scaffolds[0].contentTypeAlias); - } - modelWasChanged = true; - } - - // If there is only one item, set it as current node - if (vm.singleMode || (vm.nodes.length === 1 && vm.maxItems === 1)) { - setCurrentNode(vm.nodes[0], false); - } - - validate(); - - vm.inited = true; - - if (modelWasChanged) { - updateModel(); - } - - updatePropertyActionStates(); - checkAbilityToPasteContent(); } + + // Enforce min items if we only have one scaffold type + var modelWasChanged = false; + if (vm.nodes.length < vm.minItems && vm.scaffolds.length === 1) { + for (var i = vm.nodes.length; i < model.config.minItems; i++) { + addNode(vm.scaffolds[0].contentTypeAlias); + } + modelWasChanged = true; + } + + // If there is only one item, set it as current node + if (vm.singleMode || (vm.nodes.length === 1 && vm.maxItems === 1)) { + setCurrentNode(vm.nodes[0], false); + } + + validate(); + + vm.inited = true; + + if (modelWasChanged) { + updateModel(); + } + + updatePropertyActionStates(); + checkAbilityToPasteContent(); } function extendPropertyWithNCData(prop) { diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.propertyeditor.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.propertyeditor.html index 22897e3ca2..33a384045f 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.propertyeditor.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.propertyeditor.html @@ -19,20 +19,17 @@ umb-auto-focus="{{vm.focusOnNode && vm.currentNode.key === node.key ? 'true' : 'false'}}">
    - - +
    - +
    diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/relatedlinks/relatedlinks.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/relatedlinks/relatedlinks.html index abd0c6bc81..387b7010c9 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/relatedlinks/relatedlinks.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/relatedlinks/relatedlinks.html @@ -12,7 +12,7 @@ - + {{link.caption}}
    @@ -21,7 +21,7 @@ @@ -34,7 +34,7 @@
    @@ -73,7 +73,7 @@
    diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.prevalues.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.prevalues.controller.js index 47d1f401c7..24eaba9886 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.prevalues.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.prevalues.controller.js @@ -23,6 +23,10 @@ angular.module("umbraco").controller("Umbraco.PrevalueEditors.RteController", $scope.model.value.mode = "classic"; } + if(!$scope.model.value.overlayWidthSize) { + $scope.model.value.overlayWidthSize = "small"; + } + tinyMceService.configuration().then(function(config){ $scope.tinyMceConfig = config; diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.prevalues.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.prevalues.html index 96de0cd040..68fb2dbf5f 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.prevalues.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.prevalues.html @@ -46,4 +46,14 @@
    + +
    + +
    +
    +
    diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/urllist/urllist.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/urllist/urllist.html index ef36aae593..268995d1e4 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/urllist/urllist.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/urllist/urllist.html @@ -2,7 +2,7 @@