diff --git a/.github/V8_GETTING_STARTED.md b/.github/V8_GETTING_STARTED.md index 87ddbb166a..1cc33bb126 100644 --- a/.github/V8_GETTING_STARTED.md +++ b/.github/V8_GETTING_STARTED.md @@ -33,5 +33,5 @@ We recommend running the site with the Visual Studio since you'll be able to rem We are keeping track of [known issues and limitations here](http://issues.umbraco.org/issue/U4-11279). These line items will eventually be turned into actual tasks to be worked on. Feel free to help us keep this list updated if you find issues and even help fix some of these items. If there is a particular item you'd like to help fix please mention this on the task and we'll create a sub task for the item to continue discussion there. -There's [a list of tasks for v8 that haven't been completed](hhttps://github.com/umbraco/Umbraco-CMS/labels/release%2F8.0.0). If you are interested in helping out with any of these please mention this on the task. This list will be constantly updated as we begin to document and design some of the other tasks that still need to get done. +There's [a list of tasks for v8 that haven't been completed](https://github.com/umbraco/Umbraco-CMS/labels/release%2F8.0.0). If you are interested in helping out with any of these please mention this on the task. This list will be constantly updated as we begin to document and design some of the other tasks that still need to get done. diff --git a/build/NuSpecs/UmbracoCms.Web.nuspec b/build/NuSpecs/UmbracoCms.Web.nuspec index e9bd8ca6ea..adf090c69b 100644 --- a/build/NuSpecs/UmbracoCms.Web.nuspec +++ b/build/NuSpecs/UmbracoCms.Web.nuspec @@ -25,7 +25,7 @@ - + diff --git a/src/Umbraco.Core/ByteArrayExtensions.cs b/src/Umbraco.Core/ByteArrayExtensions.cs deleted file mode 100644 index dacdd509ca..0000000000 --- a/src/Umbraco.Core/ByteArrayExtensions.cs +++ /dev/null @@ -1,39 +0,0 @@ -namespace Umbraco.Core -{ - public static class ByteArrayExtensions - { - private static readonly char[] BytesToHexStringLookup = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F' }; - - public static string ToHexString(this byte[] bytes) - { - int i = 0, p = 0, bytesLength = bytes.Length; - var chars = new char[bytesLength * 2]; - while (i < bytesLength) - { - var b = bytes[i++]; - chars[p++] = BytesToHexStringLookup[b / 0x10]; - chars[p++] = BytesToHexStringLookup[b % 0x10]; - } - return new string(chars, 0, chars.Length); - } - - public static string ToHexString(this byte[] bytes, char separator, int blockSize, int blockCount) - { - int p = 0, bytesLength = bytes.Length, count = 0, size = 0; - var chars = new char[bytesLength * 2 + blockCount]; - for (var i = 0; i < bytesLength; i++) - { - var b = bytes[i++]; - chars[p++] = BytesToHexStringLookup[b / 0x10]; - chars[p++] = BytesToHexStringLookup[b % 0x10]; - if (count == blockCount) continue; - if (++size < blockSize) continue; - - chars[p++] = '/'; - size = 0; - count++; - } - return new string(chars, 0, chars.Length); - } - } -} diff --git a/src/Umbraco.Core/GuidUtils.cs b/src/Umbraco.Core/GuidUtils.cs new file mode 100644 index 0000000000..3768e1385d --- /dev/null +++ b/src/Umbraco.Core/GuidUtils.cs @@ -0,0 +1,44 @@ +using System; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +namespace Umbraco.Core +{ + /// + /// Utility methods for the struct. + /// + internal static class GuidUtils + { + /// + /// Combines two guid instances utilizing an exclusive disjunction. + /// The resultant guid is not guaranteed to be unique since the number of unique bits is halved. + /// + /// The first guid. + /// The seconds guid. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Guid Combine(Guid a, Guid b) + { + var ad = new DecomposedGuid(a); + var bd = new DecomposedGuid(b); + + ad.Hi ^= bd.Hi; + ad.Lo ^= bd.Lo; + + return ad.Value; + } + + /// + /// A decomposed guid. Allows access to the high and low bits without unsafe code. + /// + [StructLayout(LayoutKind.Explicit)] + private struct DecomposedGuid + { + [FieldOffset(00)] public Guid Value; + [FieldOffset(00)] public long Hi; + [FieldOffset(08)] public long Lo; + + public DecomposedGuid(Guid value) : this() => this.Value = value; + } + } +} diff --git a/src/Umbraco.Core/HexEncoder.cs b/src/Umbraco.Core/HexEncoder.cs new file mode 100644 index 0000000000..073dc8b543 --- /dev/null +++ b/src/Umbraco.Core/HexEncoder.cs @@ -0,0 +1,84 @@ +using System.Linq; +using System.Runtime.CompilerServices; + +namespace Umbraco.Core +{ + /// + /// Provides methods for encoding byte arrays into hexidecimal strings. + /// + internal static class HexEncoder + { + // LUT's that provide the hexidecimal representation of each possible byte value. + private static readonly char[] HexLutBase = new char[] { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F' }; + + // The base LUT arranged in 16x each item order. 0 * 16, 1 * 16, .... F * 16 + private static readonly char[] HexLutHi = Enumerable.Range(0, 256).Select(x => HexLutBase[x / 0x10]).ToArray(); + + // The base LUT repeated 16x. + private static readonly char[] HexLutLo = Enumerable.Range(0, 256).Select(x => HexLutBase[x % 0x10]).ToArray(); + + /// + /// Converts a to a hexidecimal formatted padded to 2 digits. + /// + /// The bytes. + /// The . + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static string Encode(byte[] bytes) + { + var length = bytes.Length; + var chars = new char[length * 2]; + + var index = 0; + for (var i = 0; i < length; i++) + { + var byteIndex = bytes[i]; + chars[index++] = HexLutHi[byteIndex]; + chars[index++] = HexLutLo[byteIndex]; + } + + return new string(chars, 0, chars.Length); + } + + /// + /// Converts a to a hexidecimal formatted padded to 2 digits + /// and split into blocks with the given char separator. + /// + /// The bytes. + /// The separator. + /// The block size. + /// The block count. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static string Encode(byte[] bytes, char separator, int blockSize, int blockCount) + { + var length = bytes.Length; + var chars = new char[(length * 2) + blockCount]; + var count = 0; + var size = 0; + var index = 0; + + for (var i = 0; i < length; i++) + { + var byteIndex = bytes[i]; + chars[index++] = HexLutHi[byteIndex]; + chars[index++] = HexLutLo[byteIndex]; + + if (count == blockCount) + { + continue; + } + + if (++size < blockSize) + { + continue; + } + + chars[index++] = separator; + size = 0; + count++; + } + + return new string(chars, 0, chars.Length); + } + } +} diff --git a/src/Umbraco.Core/IO/MediaPathSchemes/CombinedGuidsMediaPathScheme.cs b/src/Umbraco.Core/IO/MediaPathSchemes/CombinedGuidsMediaPathScheme.cs index ef71aff3bc..37c6a7b209 100644 --- a/src/Umbraco.Core/IO/MediaPathSchemes/CombinedGuidsMediaPathScheme.cs +++ b/src/Umbraco.Core/IO/MediaPathSchemes/CombinedGuidsMediaPathScheme.cs @@ -20,24 +20,11 @@ namespace Umbraco.Core.IO.MediaPathSchemes { // assumes that cuid and puid keys can be trusted - and that a single property type // for a single content cannot store two different files with the same name - var directory = Combine(itemGuid, propertyGuid).ToHexString(/*'/', 2, 4*/); // could use ext to fragment path eg 12/e4/f2/... + var directory = HexEncoder.Encode(GuidUtils.Combine(itemGuid, propertyGuid).ToByteArray()/*'/', 2, 4*/); // could use ext to fragment path eg 12/e4/f2/... return Path.Combine(directory, filename).Replace('\\', '/'); } /// - public string GetDeleteDirectory(string filepath) - { - return Path.GetDirectoryName(filepath); - } - - private static byte[] Combine(Guid guid1, Guid guid2) - { - var bytes1 = guid1.ToByteArray(); - var bytes2 = guid2.ToByteArray(); - var bytes = new byte[bytes1.Length]; - for (var i = 0; i < bytes1.Length; i++) - bytes[i] = (byte) (bytes1[i] ^ bytes2[i]); - return bytes; - } + public string GetDeleteDirectory(string filepath) => Path.GetDirectoryName(filepath); } } diff --git a/src/Umbraco.Core/Models/ContentTypeBase.cs b/src/Umbraco.Core/Models/ContentTypeBase.cs index caa63d7526..88b1179f6d 100644 --- a/src/Umbraco.Core/Models/ContentTypeBase.cs +++ b/src/Umbraco.Core/Models/ContentTypeBase.cs @@ -92,8 +92,8 @@ namespace Umbraco.Core.Models public readonly PropertyInfo AllowedAsRootSelector = ExpressionHelper.GetPropertyInfo(x => x.AllowedAsRoot); public readonly PropertyInfo IsContainerSelector = ExpressionHelper.GetPropertyInfo(x => x.IsContainer); public readonly PropertyInfo AllowedContentTypesSelector = ExpressionHelper.GetPropertyInfo>(x => x.AllowedContentTypes); - public readonly PropertyInfo PropertyGroupCollectionSelector = ExpressionHelper.GetPropertyInfo(x => x.PropertyGroups); - public readonly PropertyInfo PropertyTypeCollectionSelector = ExpressionHelper.GetPropertyInfo>(x => x.PropertyTypes); + public readonly PropertyInfo PropertyGroupsSelector = ExpressionHelper.GetPropertyInfo(x => x.PropertyGroups); + public readonly PropertyInfo PropertyTypesSelector = ExpressionHelper.GetPropertyInfo>(x => x.PropertyTypes); public readonly PropertyInfo HasPropertyTypeBeenRemovedSelector = ExpressionHelper.GetPropertyInfo(x => x.HasPropertyTypeBeenRemoved); public readonly PropertyInfo VaryBy = ExpressionHelper.GetPropertyInfo(x => x.Variations); @@ -106,12 +106,12 @@ namespace Umbraco.Core.Models protected void PropertyGroupsChanged(object sender, NotifyCollectionChangedEventArgs e) { - OnPropertyChanged(Ps.Value.PropertyGroupCollectionSelector); + OnPropertyChanged(Ps.Value.PropertyGroupsSelector); } protected void PropertyTypesChanged(object sender, NotifyCollectionChangedEventArgs e) { - OnPropertyChanged(Ps.Value.PropertyTypeCollectionSelector); + OnPropertyChanged(Ps.Value.PropertyTypesSelector); } /// @@ -263,6 +263,8 @@ namespace Umbraco.Core.Models get => _noGroupPropertyTypes; set { + if (_noGroupPropertyTypes != null) + _noGroupPropertyTypes.CollectionChanged -= PropertyTypesChanged; _noGroupPropertyTypes = new PropertyTypeCollection(IsPublishing, value); _noGroupPropertyTypes.CollectionChanged += PropertyTypesChanged; PropertyTypesChanged(_noGroupPropertyTypes, new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); @@ -376,7 +378,7 @@ namespace Umbraco.Core.Models if (!HasPropertyTypeBeenRemoved) { HasPropertyTypeBeenRemoved = true; - OnPropertyChanged(Ps.Value.PropertyTypeCollectionSelector); + OnPropertyChanged(Ps.Value.PropertyTypesSelector); } break; } @@ -388,7 +390,7 @@ namespace Umbraco.Core.Models if (!HasPropertyTypeBeenRemoved) { HasPropertyTypeBeenRemoved = true; - OnPropertyChanged(Ps.Value.PropertyTypeCollectionSelector); + OnPropertyChanged(Ps.Value.PropertyTypesSelector); } } } @@ -412,7 +414,7 @@ namespace Umbraco.Core.Models // actually remove the group PropertyGroups.RemoveItem(propertyGroupName); - OnPropertyChanged(Ps.Value.PropertyGroupCollectionSelector); + OnPropertyChanged(Ps.Value.PropertyGroupsSelector); } /// diff --git a/src/Umbraco.Core/Models/PropertyGroup.cs b/src/Umbraco.Core/Models/PropertyGroup.cs index 1d0b949932..595e8d1d6a 100644 --- a/src/Umbraco.Core/Models/PropertyGroup.cs +++ b/src/Umbraco.Core/Models/PropertyGroup.cs @@ -35,12 +35,12 @@ namespace Umbraco.Core.Models { public readonly PropertyInfo NameSelector = ExpressionHelper.GetPropertyInfo(x => x.Name); public readonly PropertyInfo SortOrderSelector = ExpressionHelper.GetPropertyInfo(x => x.SortOrder); - public readonly PropertyInfo PropertyTypeCollectionSelector = ExpressionHelper.GetPropertyInfo(x => x.PropertyTypes); + public readonly PropertyInfo PropertyTypes = ExpressionHelper.GetPropertyInfo(x => x.PropertyTypes); } private void PropertyTypesChanged(object sender, NotifyCollectionChangedEventArgs e) { - OnPropertyChanged(Ps.Value.PropertyTypeCollectionSelector); + OnPropertyChanged(Ps.Value.PropertyTypes); } /// @@ -76,6 +76,8 @@ namespace Umbraco.Core.Models get => _propertyTypes; set { + if (_propertyTypes != null) + _propertyTypes.CollectionChanged -= PropertyTypesChanged; _propertyTypes = value; // since we're adding this collection to this group, @@ -83,6 +85,7 @@ namespace Umbraco.Core.Models foreach (var propertyType in _propertyTypes) propertyType.PropertyGroupId = new Lazy(() => Id); + OnPropertyChanged(Ps.Value.PropertyTypes); _propertyTypes.CollectionChanged += PropertyTypesChanged; } } diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeRepositoryBase.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeRepositoryBase.cs index 3184c69dfe..44215b7f7e 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeRepositoryBase.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/ContentTypeRepositoryBase.cs @@ -190,8 +190,8 @@ AND umbracoNode.nodeObjectType = @objectType", SortOrder = allowedContentType.SortOrder }); } - - + + //Insert Tabs foreach (var propertyGroup in entity.PropertyGroups) { @@ -328,14 +328,14 @@ AND umbracoNode.id <> @id", // We check if the entity's own PropertyTypes has been modified and then also check // any of the property groups PropertyTypes has been modified. // This specifically tells us if any property type collections have changed. - if (entity.IsPropertyDirty("PropertyTypes") || entity.PropertyGroups.Any(x => x.IsPropertyDirty("PropertyTypes"))) + if (entity.IsPropertyDirty("NoGroupPropertyTypes") || entity.PropertyGroups.Any(x => x.IsPropertyDirty("PropertyTypes"))) { var dbPropertyTypes = Database.Fetch("WHERE contentTypeId = @Id", new { entity.Id }); - var dbPropertyTypeAlias = dbPropertyTypes.Select(x => x.Id); + var dbPropertyTypeIds = dbPropertyTypes.Select(x => x.Id); var entityPropertyTypes = entity.PropertyTypes.Where(x => x.HasIdentity).Select(x => x.Id); - var items = dbPropertyTypeAlias.Except(entityPropertyTypes); - foreach (var item in items) - DeletePropertyType(entity.Id, item); + var propertyTypeToDeleteIds = dbPropertyTypeIds.Except(entityPropertyTypes); + foreach (var propertyTypeId in propertyTypeToDeleteIds) + DeletePropertyType(entity.Id, propertyTypeId); } // Delete tabs ... by excepting entries from db with entries from collections. @@ -620,7 +620,7 @@ AND umbracoNode.id <> @id", var sqlDelete = Sql() .Delete() .WhereIn((System.Linq.Expressions.Expression>)(x => x.ContentKey), sqlSelect); - + Database.Execute(sqlDelete); } @@ -690,7 +690,7 @@ AND umbracoNode.id <> @id", //first clear out any existing names that might already exists under the default lang //there's 2x tables to update - //clear out the versionCultureVariation table + //clear out the versionCultureVariation table var sqlSelect = Sql().Select(x => x.Id) .From() .InnerJoin().On(x => x.Id, x => x.VersionId) diff --git a/src/Umbraco.Core/Services/Implement/MemberService.cs b/src/Umbraco.Core/Services/Implement/MemberService.cs index 5a644cfec1..e191493736 100644 --- a/src/Umbraco.Core/Services/Implement/MemberService.cs +++ b/src/Umbraco.Core/Services/Implement/MemberService.cs @@ -1033,7 +1033,7 @@ namespace Umbraco.Core.Services.Implement scope.WriteLock(Constants.Locks.MemberTree); var ids = _memberGroupRepository.GetMemberIds(usernames); _memberGroupRepository.AssignRoles(ids, roleNames); - scope.Events.Dispatch(AssignedRoles, this, new RolesEventArgs(ids, roleNames)); + scope.Events.Dispatch(AssignedRoles, this, new RolesEventArgs(ids, roleNames), nameof(AssignedRoles)); scope.Complete(); } } @@ -1050,7 +1050,7 @@ namespace Umbraco.Core.Services.Implement scope.WriteLock(Constants.Locks.MemberTree); var ids = _memberGroupRepository.GetMemberIds(usernames); _memberGroupRepository.DissociateRoles(ids, roleNames); - scope.Events.Dispatch(RemovedRoles, this, new RolesEventArgs(ids, roleNames)); + scope.Events.Dispatch(RemovedRoles, this, new RolesEventArgs(ids, roleNames), nameof(RemovedRoles)); scope.Complete(); } } @@ -1066,7 +1066,7 @@ namespace Umbraco.Core.Services.Implement { scope.WriteLock(Constants.Locks.MemberTree); _memberGroupRepository.AssignRoles(memberIds, roleNames); - scope.Events.Dispatch(AssignedRoles, this, new RolesEventArgs(memberIds, roleNames)); + scope.Events.Dispatch(AssignedRoles, this, new RolesEventArgs(memberIds, roleNames), nameof(AssignedRoles)); scope.Complete(); } } @@ -1082,7 +1082,7 @@ namespace Umbraco.Core.Services.Implement { scope.WriteLock(Constants.Locks.MemberTree); _memberGroupRepository.DissociateRoles(memberIds, roleNames); - scope.Events.Dispatch(RemovedRoles, this, new RolesEventArgs(memberIds, roleNames)); + scope.Events.Dispatch(RemovedRoles, this, new RolesEventArgs(memberIds, roleNames), nameof(RemovedRoles)); scope.Complete(); } } diff --git a/src/Umbraco.Core/Services/PublicAccessServiceExtensions.cs b/src/Umbraco.Core/Services/PublicAccessServiceExtensions.cs index 12db4daf40..b0dc979ebf 100644 --- a/src/Umbraco.Core/Services/PublicAccessServiceExtensions.cs +++ b/src/Umbraco.Core/Services/PublicAccessServiceExtensions.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using System.Web.Security; +using Umbraco.Core.Models; namespace Umbraco.Core.Services { @@ -41,7 +42,7 @@ namespace Umbraco.Core.Services return hasChange; } - public static bool HasAccess(this IPublicAccessService publicAccessService, int documentId, IContentService contentService, IEnumerable currentMemberRoles) + public static bool HasAccess(this IPublicAccessService publicAccessService, int documentId, IContentService contentService, string username, IEnumerable currentMemberRoles) { var content = contentService.GetById(documentId); if (content == null) return true; @@ -49,8 +50,7 @@ namespace Umbraco.Core.Services var entry = publicAccessService.GetEntryForContent(content); if (entry == null) return true; - return entry.Rules.Any(x => x.RuleType == Constants.Conventions.PublicAccess.MemberRoleRuleType - && currentMemberRoles.Contains(x.RuleValue)); + return HasAccess(entry, username, currentMemberRoles); } public static bool HasAccess(this IPublicAccessService publicAccessService, string path, MembershipUser member, RoleProvider roleProvider) @@ -77,8 +77,15 @@ namespace Umbraco.Core.Services var roles = rolesCallback(username); - return entry.Rules.Any(x => x.RuleType == Constants.Conventions.PublicAccess.MemberRoleRuleType - && roles.Contains(x.RuleValue)); + return HasAccess(entry, username, roles); + } + + private static bool HasAccess(PublicAccessEntry entry, string username, IEnumerable roles) + { + return entry.Rules.Any(x => + (x.RuleType == Constants.Conventions.PublicAccess.MemberUsernameRuleType && username.Equals(x.RuleValue, StringComparison.OrdinalIgnoreCase)) + || (x.RuleType == Constants.Conventions.PublicAccess.MemberRoleRuleType && roles.Contains(x.RuleValue)) + ); } } } diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index a8361a237a..f57c14a03e 100755 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -111,7 +111,6 @@ - @@ -321,6 +320,8 @@ + + diff --git a/src/Umbraco.Examine/ContentValueSetBuilder.cs b/src/Umbraco.Examine/ContentValueSetBuilder.cs index 39ffab98d9..07e5e4565b 100644 --- a/src/Umbraco.Examine/ContentValueSetBuilder.cs +++ b/src/Umbraco.Examine/ContentValueSetBuilder.cs @@ -42,7 +42,7 @@ namespace Umbraco.Examine var values = new Dictionary> { {"icon", c.ContentType.Icon.Yield()}, - {UmbracoExamineIndexer.PublishedFieldName, new object[] {c.Published ? 1 : 0}}, //Always add invariant published value + {UmbracoExamineIndex.PublishedFieldName, new object[] {c.Published ? 1 : 0}}, //Always add invariant published value {"id", new object[] {c.Id}}, {"key", new object[] {c.Key}}, {"parentID", new object[] {c.Level > 1 ? c.ParentId : -1}}, @@ -61,12 +61,12 @@ namespace Umbraco.Examine {"writerName",(c.GetWriterProfile(_userService)?.Name ?? "??").Yield() }, {"writerID", new object[] {c.WriterId}}, {"template", new object[] {c.Template?.Id ?? 0}}, - {UmbracoContentIndexer.VariesByCultureFieldName, new object[] {0}}, + {UmbracoContentIndex.VariesByCultureFieldName, new object[] {0}}, }; if (isVariant) { - values[UmbracoContentIndexer.VariesByCultureFieldName] = new object[] { 1 }; + values[UmbracoContentIndex.VariesByCultureFieldName] = new object[] { 1 }; foreach (var culture in c.AvailableCultures) { @@ -76,7 +76,7 @@ namespace Umbraco.Examine values[$"nodeName_{lowerCulture}"] = PublishedValuesOnly ? c.GetPublishName(culture).Yield() : c.GetCultureName(culture).Yield(); - values[$"{UmbracoExamineIndexer.PublishedFieldName}_{lowerCulture}"] = (c.IsCulturePublished(culture) ? 1 : 0).Yield(); + values[$"{UmbracoExamineIndex.PublishedFieldName}_{lowerCulture}"] = (c.IsCulturePublished(culture) ? 1 : 0).Yield(); values[$"updateDate_{lowerCulture}"] = PublishedValuesOnly ? c.GetPublishDate(culture).Yield() : c.GetUpdateDate(culture).Yield(); diff --git a/src/Umbraco.Examine/ContentValueSetValidator.cs b/src/Umbraco.Examine/ContentValueSetValidator.cs index eabf15f672..d4f6ceb15f 100644 --- a/src/Umbraco.Examine/ContentValueSetValidator.cs +++ b/src/Umbraco.Examine/ContentValueSetValidator.cs @@ -92,18 +92,18 @@ namespace Umbraco.Examine //check for published content if (valueSet.Category == IndexTypes.Content && PublishedValuesOnly) { - if (!valueSet.Values.TryGetValue(UmbracoExamineIndexer.PublishedFieldName, out var published)) + if (!valueSet.Values.TryGetValue(UmbracoExamineIndex.PublishedFieldName, out var published)) return ValueSetValidationResult.Failed; if (!published[0].Equals(1)) return ValueSetValidationResult.Failed; //deal with variants, if there are unpublished variants than we need to remove them from the value set - if (valueSet.Values.TryGetValue(UmbracoContentIndexer.VariesByCultureFieldName, out var variesByCulture) + if (valueSet.Values.TryGetValue(UmbracoContentIndex.VariesByCultureFieldName, out var variesByCulture) && variesByCulture.Count > 0 && variesByCulture[0].Equals(1)) { //so this valueset is for a content that varies by culture, now check for non-published cultures and remove those values - foreach(var publishField in valueSet.Values.Where(x => x.Key.StartsWith($"{UmbracoExamineIndexer.PublishedFieldName}_")).ToList()) + foreach(var publishField in valueSet.Values.Where(x => x.Key.StartsWith($"{UmbracoExamineIndex.PublishedFieldName}_")).ToList()) { if (publishField.Value.Count <= 0 || !publishField.Value[0].Equals(1)) { diff --git a/src/Umbraco.Examine/IIndexCreator.cs b/src/Umbraco.Examine/IIndexCreator.cs new file mode 100644 index 0000000000..3b8f683990 --- /dev/null +++ b/src/Umbraco.Examine/IIndexCreator.cs @@ -0,0 +1,13 @@ +using System.Collections.Generic; +using Examine; + +namespace Umbraco.Examine +{ + /// + /// Creates 's + /// + public interface IIndexCreator + { + IEnumerable Create(); + } +} diff --git a/src/Umbraco.Examine/IIndexDiagnostics.cs b/src/Umbraco.Examine/IIndexDiagnostics.cs index 04ca4a6ab9..29d530c2d0 100644 --- a/src/Umbraco.Examine/IIndexDiagnostics.cs +++ b/src/Umbraco.Examine/IIndexDiagnostics.cs @@ -1,12 +1,9 @@ using System; using System.Collections.Generic; -using Examine; using Umbraco.Core; namespace Umbraco.Examine { - - /// /// Exposes diagnostic information about an index /// diff --git a/src/Umbraco.Examine/IValueSetBuilder.cs b/src/Umbraco.Examine/IValueSetBuilder.cs index 89aa907926..1c4890f404 100644 --- a/src/Umbraco.Examine/IValueSetBuilder.cs +++ b/src/Umbraco.Examine/IValueSetBuilder.cs @@ -5,18 +5,17 @@ using Umbraco.Core.Models; namespace Umbraco.Examine { /// - /// Creates a collection of to be indexed based on a collection of + /// Creates a collection of to be indexed based on a collection of /// - /// - public interface IValueSetBuilder - where TContent : IContentBase + /// + public interface IValueSetBuilder { /// - /// Creates a collection of to be indexed based on a collection of + /// Creates a collection of to be indexed based on a collection of /// /// /// - IEnumerable GetValueSets(params TContent[] content); + IEnumerable GetValueSets(params T[] content); } } diff --git a/src/Umbraco.Examine/LuceneIndexCreator.cs b/src/Umbraco.Examine/LuceneIndexCreator.cs new file mode 100644 index 0000000000..572de1e8a8 --- /dev/null +++ b/src/Umbraco.Examine/LuceneIndexCreator.cs @@ -0,0 +1,43 @@ +using System.Collections.Generic; +using System.IO; +using Examine; +using Examine.LuceneEngine.Directories; +using Lucene.Net.Store; +using Umbraco.Core.IO; + +namespace Umbraco.Examine +{ + /// + /// + /// Abstract class for creating Lucene based Indexes + /// + public abstract class LuceneIndexCreator : IIndexCreator + { + public abstract IEnumerable Create(); + + /// + /// Creates a file system based Lucene with the correct locking guidelines for Umbraco + /// + /// + /// + public virtual Lucene.Net.Store.Directory CreateFileSystemLuceneDirectory(string name) + { + //TODO: We should have a single AppSetting to be able to specify a default DirectoryFactory so we can have a single + //setting to configure all indexes that use this to easily swap the directory to Sync/%temp%/blog, etc... + + var dirInfo = new DirectoryInfo(Path.Combine(IOHelper.MapPath(SystemDirectories.Data), "TEMP", "ExamineIndexes", name)); + if (!dirInfo.Exists) + System.IO.Directory.CreateDirectory(dirInfo.FullName); + + var luceneDir = new SimpleFSDirectory(dirInfo); + + //we want to tell examine to use a different fs lock instead of the default NativeFSFileLock which could cause problems if the appdomain + //terminates and in some rare cases would only allow unlocking of the file if IIS is forcefully terminated. Instead we'll rely on the simplefslock + //which simply checks the existence of the lock file + // The full syntax of this is: new NoPrefixSimpleFsLockFactory(dirInfo) + // however, we are setting the DefaultLockFactory in startup so we'll use that instead since it can be managed globally. + luceneDir.SetLockFactory(DirectoryFactory.DefaultLockFactory(dirInfo)); + return luceneDir; + } + } +} diff --git a/src/Umbraco.Examine/Umbraco.Examine.csproj b/src/Umbraco.Examine/Umbraco.Examine.csproj index 9194d8332d..66b1f09068 100644 --- a/src/Umbraco.Examine/Umbraco.Examine.csproj +++ b/src/Umbraco.Examine/Umbraco.Examine.csproj @@ -66,6 +66,7 @@ + @@ -83,12 +84,13 @@ - + - + - + + Properties\SolutionInfo.cs diff --git a/src/Umbraco.Examine/UmbracoContentIndexer.cs b/src/Umbraco.Examine/UmbracoContentIndex.cs similarity index 97% rename from src/Umbraco.Examine/UmbracoContentIndexer.cs rename to src/Umbraco.Examine/UmbracoContentIndex.cs index 0501fe90c6..910941ea7b 100644 --- a/src/Umbraco.Examine/UmbracoContentIndexer.cs +++ b/src/Umbraco.Examine/UmbracoContentIndex.cs @@ -21,7 +21,7 @@ namespace Umbraco.Examine /// /// An indexer for Umbraco content and media /// - public class UmbracoContentIndexer : UmbracoExamineIndexer + public class UmbracoContentIndex : UmbracoExamineIndex { public const string VariesByCultureFieldName = SpecialFieldPrefix + "VariesByCulture"; protected ILocalizationService LanguageService { get; } @@ -32,7 +32,7 @@ namespace Umbraco.Examine /// Constructor for configuration providers /// [EditorBrowsable(EditorBrowsableState.Never)] - public UmbracoContentIndexer() + public UmbracoContentIndex() { LanguageService = Current.Services.LocalizationService; @@ -50,7 +50,7 @@ namespace Umbraco.Examine /// /// /// - public UmbracoContentIndexer( + public UmbracoContentIndex( string name, IEnumerable fieldDefinitions, Directory luceneDirectory, @@ -167,7 +167,7 @@ namespace Umbraco.Examine //since the path is not valid we need to delete this item in case it exists in the index already and has now //been moved to an invalid parent. foreach (var i in group) - QueueIndexOperation(new IndexOperation(new ValueSet(i.Id), IndexOperationType.Delete)); + base.PerformDeleteFromIndex(i.Id, args => { /*noop*/ }); } else { diff --git a/src/Umbraco.Examine/UmbracoExamineIndexer.cs b/src/Umbraco.Examine/UmbracoExamineIndex.cs similarity index 98% rename from src/Umbraco.Examine/UmbracoExamineIndexer.cs rename to src/Umbraco.Examine/UmbracoExamineIndex.cs index 84c8a7d8c5..15f1a75955 100644 --- a/src/Umbraco.Examine/UmbracoExamineIndexer.cs +++ b/src/Umbraco.Examine/UmbracoExamineIndex.cs @@ -23,7 +23,7 @@ namespace Umbraco.Examine /// An abstract provider containing the basic functionality to be able to query against /// Umbraco data. /// - public abstract class UmbracoExamineIndexer : LuceneIndex, IUmbracoIndexer, IIndexDiagnostics + public abstract class UmbracoExamineIndex : LuceneIndex, IUmbracoIndexer, IIndexDiagnostics { // note // wrapping all operations that end up calling base.SafelyProcessQueueItems in a safe call @@ -48,7 +48,7 @@ namespace Umbraco.Examine /// Constructor for config provider based indexes /// [EditorBrowsable(EditorBrowsableState.Never)] - protected UmbracoExamineIndexer() + protected UmbracoExamineIndex() : base() { ProfilingLogger = Current.ProfilingLogger; @@ -56,7 +56,7 @@ namespace Umbraco.Examine } /// - /// Create a new + /// Create a new /// /// /// @@ -65,7 +65,7 @@ namespace Umbraco.Examine /// /// /// - protected UmbracoExamineIndexer( + protected UmbracoExamineIndex( string name, IEnumerable fieldDefinitions, Directory luceneDirectory, diff --git a/src/Umbraco.Examine/UmbracoExamineIndexDiagnostics.cs b/src/Umbraco.Examine/UmbracoExamineIndexDiagnostics.cs index 0812d93931..227b52e085 100644 --- a/src/Umbraco.Examine/UmbracoExamineIndexDiagnostics.cs +++ b/src/Umbraco.Examine/UmbracoExamineIndexDiagnostics.cs @@ -9,10 +9,10 @@ namespace Umbraco.Examine { public class UmbracoExamineIndexDiagnostics : IIndexDiagnostics { - private readonly UmbracoExamineIndexer _index; + private readonly UmbracoExamineIndex _index; private readonly ILogger _logger; - public UmbracoExamineIndexDiagnostics(UmbracoExamineIndexer index, ILogger logger) + public UmbracoExamineIndexDiagnostics(UmbracoExamineIndex index, ILogger logger) { _index = index; _logger = logger; @@ -28,7 +28,7 @@ namespace Umbraco.Examine } catch (AlreadyClosedException) { - _logger.Warn(typeof(UmbracoContentIndexer), "Cannot get GetIndexDocumentCount, the writer is already closed"); + _logger.Warn(typeof(UmbracoContentIndex), "Cannot get GetIndexDocumentCount, the writer is already closed"); return 0; } } @@ -44,7 +44,7 @@ namespace Umbraco.Examine } catch (AlreadyClosedException) { - _logger.Warn(typeof(UmbracoContentIndexer), "Cannot get GetIndexFieldCount, the writer is already closed"); + _logger.Warn(typeof(UmbracoContentIndex), "Cannot get GetIndexFieldCount, the writer is already closed"); return 0; } } @@ -62,15 +62,15 @@ namespace Umbraco.Examine { var d = new Dictionary { - [nameof(UmbracoExamineIndexer.CommitCount)] = _index.CommitCount, - [nameof(UmbracoExamineIndexer.DefaultAnalyzer)] = _index.DefaultAnalyzer.GetType().Name, - [nameof(UmbracoExamineIndexer.DirectoryFactory)] = _index.DirectoryFactory, - [nameof(UmbracoExamineIndexer.EnableDefaultEventHandler)] = _index.EnableDefaultEventHandler, - [nameof(UmbracoExamineIndexer.LuceneIndexFolder)] = + [nameof(UmbracoExamineIndex.CommitCount)] = _index.CommitCount, + [nameof(UmbracoExamineIndex.DefaultAnalyzer)] = _index.DefaultAnalyzer.GetType().Name, + [nameof(UmbracoExamineIndex.DirectoryFactory)] = _index.DirectoryFactory, + [nameof(UmbracoExamineIndex.EnableDefaultEventHandler)] = _index.EnableDefaultEventHandler, + [nameof(UmbracoExamineIndex.LuceneIndexFolder)] = _index.LuceneIndexFolder == null ? string.Empty : _index.LuceneIndexFolder.ToString().ToLowerInvariant().TrimStart(IOHelper.MapPath(SystemDirectories.Root).ToLowerInvariant()).Replace("\\", "/").EnsureStartsWith('/'), - [nameof(UmbracoExamineIndexer.PublishedValuesOnly)] = _index.PublishedValuesOnly, + [nameof(UmbracoExamineIndex.PublishedValuesOnly)] = _index.PublishedValuesOnly, //There's too much info here //[nameof(UmbracoExamineIndexer.FieldDefinitionCollection)] = _index.FieldDefinitionCollection, }; diff --git a/src/Umbraco.Examine/UmbracoMemberIndexer.cs b/src/Umbraco.Examine/UmbracoMemberIndex.cs similarity index 95% rename from src/Umbraco.Examine/UmbracoMemberIndexer.cs rename to src/Umbraco.Examine/UmbracoMemberIndex.cs index 4943f49825..28b46f72dd 100644 --- a/src/Umbraco.Examine/UmbracoMemberIndexer.cs +++ b/src/Umbraco.Examine/UmbracoMemberIndex.cs @@ -22,13 +22,13 @@ namespace Umbraco.Examine /// /// Custom indexer for members /// - public class UmbracoMemberIndexer : UmbracoExamineIndexer + public class UmbracoMemberIndex : UmbracoExamineIndex { /// /// Constructor for config/provider based indexes /// [EditorBrowsable(EditorBrowsableState.Never)] - public UmbracoMemberIndexer() + public UmbracoMemberIndex() { } @@ -41,7 +41,7 @@ namespace Umbraco.Examine /// /// /// - public UmbracoMemberIndexer( + public UmbracoMemberIndex( string name, IEnumerable fieldDefinitions, Directory luceneDirectory, diff --git a/src/Umbraco.Tests.Benchmarks/CombineGuidBenchmarks.cs b/src/Umbraco.Tests.Benchmarks/CombineGuidBenchmarks.cs new file mode 100644 index 0000000000..ce55f6890d --- /dev/null +++ b/src/Umbraco.Tests.Benchmarks/CombineGuidBenchmarks.cs @@ -0,0 +1,48 @@ +using System; +using BenchmarkDotNet.Attributes; +using Umbraco.Core; +using Umbraco.Tests.Benchmarks.Config; + +namespace Umbraco.Tests.Benchmarks +{ + [QuickRunWithMemoryDiagnoserConfig] + public class CombineGuidBenchmarks + { + private static readonly Guid _a = Guid.NewGuid(); + private static readonly Guid _b = Guid.NewGuid(); + + [Benchmark] + public byte[] CombineUtils() => GuidUtils.Combine(_a, _b).ToByteArray(); + + [Benchmark] + public byte[] CombineLoop() => Combine(_a, _b); + + private static byte[] Combine(Guid guid1, Guid guid2) + { + var bytes1 = guid1.ToByteArray(); + var bytes2 = guid2.ToByteArray(); + var bytes = new byte[bytes1.Length]; + for (var i = 0; i < bytes1.Length; i++) + { + bytes[i] = (byte)(bytes1[i] ^ bytes2[i]); + } + + return bytes; + } + } + + // Nov 8 2018 + //BenchmarkDotNet=v0.11.2, OS=Windows 10.0.17763.55 (1809/October2018Update/Redstone5) + //Intel Core i7-6600U CPU 2.60GHz(Skylake), 1 CPU, 4 logical and 2 physical cores + // [Host] : .NET Framework 4.7.2 (CLR 4.0.30319.42000), 32bit LegacyJIT-v4.7.3190.0 + // Job-JIATTD : .NET Framework 4.7.2 (CLR 4.0.30319.42000), 32bit LegacyJIT-v4.7.3190.0 + + //IterationCount=3 IterationTime=100.0000 ms LaunchCount = 1 + //WarmupCount=3 + + // Method | Mean | Error | StdDev | Gen 0/1k Op | Gen 1/1k Op | Gen 2/1k Op | Allocated Memory/Op | + //------------- |---------:|----------:|----------:|------------:|------------:|------------:|--------------------:| + // CombineUtils | 33.34 ns | 8.086 ns | 0.4432 ns | 0.0133 | - | - | 28 B | + // CombineLoop | 55.03 ns | 11.311 ns | 0.6200 ns | 0.0395 | - | - | 84 B | +} + diff --git a/src/Umbraco.Tests.Benchmarks/HexStringBenchmarks.cs b/src/Umbraco.Tests.Benchmarks/HexStringBenchmarks.cs new file mode 100644 index 0000000000..e29a5a24f3 --- /dev/null +++ b/src/Umbraco.Tests.Benchmarks/HexStringBenchmarks.cs @@ -0,0 +1,69 @@ +using System; +using System.Text; +using BenchmarkDotNet.Attributes; +using Umbraco.Core; +using Umbraco.Tests.Benchmarks.Config; + +namespace Umbraco.Tests.Benchmarks +{ + [QuickRunConfig] + public class HexStringBenchmarks + { + private byte[] _buffer; + + [Params(8, 16, 32, 64, 128, 256)] + public int Count { get; set; } + + [GlobalSetup] + public void Setup() + { + this._buffer = new byte[this.Count]; + var random = new Random(); + random.NextBytes(this._buffer); + } + + [Benchmark(Baseline = true)] + public string ToHexStringBuilder() + { + var sb = new StringBuilder(this._buffer.Length * 2); + for (var i = 0; i < this._buffer.Length; i++) + { + sb.Append(this._buffer[i].ToString("X2")); + } + + return sb.ToString(); + } + + [Benchmark] + public string ToHexStringEncoder() => HexEncoder.Encode(this._buffer); + } + + // Nov 8 2018 + //BenchmarkDotNet=v0.11.2, OS=Windows 10.0.17763.55 (1809/October2018Update/Redstone5) + //Intel Core i7-6600U CPU 2.60GHz(Skylake), 1 CPU, 4 logical and 2 physical cores + // [Host] : .NET Framework 4.7.2 (CLR 4.0.30319.42000), 32bit LegacyJIT-v4.7.3190.0 + // Job-JIATTD : .NET Framework 4.7.2 (CLR 4.0.30319.42000), 32bit LegacyJIT-v4.7.3190.0 + + //IterationCount=3 IterationTime=100.0000 ms LaunchCount = 1 + //WarmupCount=3 + + // Method | Count | Mean | Error | StdDev | Ratio | + //------------------- |------ |-------------:|-------------:|-----------:|------:| + // ToHexStringBuilder | 8 | 786.49 ns | 319.92 ns | 17.536 ns | 1.00 | + // ToHexStringEncoder | 8 | 64.19 ns | 30.21 ns | 1.656 ns | 0.08 | + // | | | | | | + // ToHexStringBuilder | 16 | 1,442.43 ns | 503.00 ns | 27.571 ns | 1.00 | + // ToHexStringEncoder | 16 | 133.46 ns | 177.55 ns | 9.732 ns | 0.09 | + // | | | | | | + // ToHexStringBuilder | 32 | 2,869.23 ns | 924.35 ns | 50.667 ns | 1.00 | + // ToHexStringEncoder | 32 | 181.03 ns | 96.64 ns | 5.297 ns | 0.06 | + // | | | | | | + // ToHexStringBuilder | 64 | 5,775.33 ns | 2,825.42 ns | 154.871 ns | 1.00 | + // ToHexStringEncoder | 64 | 331.16 ns | 125.63 ns | 6.886 ns | 0.06 | + // | | | | | | + // ToHexStringBuilder | 128 | 11,662.35 ns | 4,908.03 ns | 269.026 ns | 1.00 | + // ToHexStringEncoder | 128 | 633.78 ns | 57.56 ns | 3.155 ns | 0.05 | + // | | | | | | + // ToHexStringBuilder | 256 | 22,960.11 ns | 14,111.47 ns | 773.497 ns | 1.00 | + // ToHexStringEncoder | 256 | 1,224.76 ns | 547.27 ns | 29.998 ns | 0.05 | +} diff --git a/src/Umbraco.Tests.Benchmarks/Umbraco.Tests.Benchmarks.csproj b/src/Umbraco.Tests.Benchmarks/Umbraco.Tests.Benchmarks.csproj index 15a55ab6ac..bb14fb5a77 100644 --- a/src/Umbraco.Tests.Benchmarks/Umbraco.Tests.Benchmarks.csproj +++ b/src/Umbraco.Tests.Benchmarks/Umbraco.Tests.Benchmarks.csproj @@ -46,10 +46,12 @@ + + diff --git a/src/Umbraco.Tests/Composing/TypeFinderTests.cs b/src/Umbraco.Tests/Composing/TypeFinderTests.cs index 955f6f94c8..9b52546dff 100644 --- a/src/Umbraco.Tests/Composing/TypeFinderTests.cs +++ b/src/Umbraco.Tests/Composing/TypeFinderTests.cs @@ -49,7 +49,7 @@ namespace Umbraco.Tests.Composing //typeof(TabPage).Assembly, typeof(System.Web.Mvc.ActionResult).Assembly, typeof(TypeFinder).Assembly, - typeof(global::Umbraco.Examine.UmbracoExamineIndexer).Assembly + typeof(global::Umbraco.Examine.UmbracoExamineIndex).Assembly }; } diff --git a/src/Umbraco.Tests/CoreThings/GuidUtilsTests.cs b/src/Umbraco.Tests/CoreThings/GuidUtilsTests.cs new file mode 100644 index 0000000000..5ef8cba356 --- /dev/null +++ b/src/Umbraco.Tests/CoreThings/GuidUtilsTests.cs @@ -0,0 +1,32 @@ +using System; +using NUnit.Framework; +using Umbraco.Core; + +namespace Umbraco.Tests.CoreThings +{ + public class GuidUtilsTests + { + [Test] + public void GuidCombineMethodsAreEqual() + { + var a = Guid.NewGuid(); + var b = Guid.NewGuid(); + + Assert.AreEqual(GuidUtils.Combine(a, b).ToByteArray(), Combine(a, b)); + } + + // Reference implementation taken from original code. + private static byte[] Combine(Guid guid1, Guid guid2) + { + var bytes1 = guid1.ToByteArray(); + var bytes2 = guid2.ToByteArray(); + var bytes = new byte[bytes1.Length]; + for (var i = 0; i < bytes1.Length; i++) + { + bytes[i] = (byte)(bytes1[i] ^ bytes2[i]); + } + + return bytes; + } + } +} diff --git a/src/Umbraco.Tests/CoreThings/HexEncoderTests.cs b/src/Umbraco.Tests/CoreThings/HexEncoderTests.cs new file mode 100644 index 0000000000..588fff83e8 --- /dev/null +++ b/src/Umbraco.Tests/CoreThings/HexEncoderTests.cs @@ -0,0 +1,71 @@ +using System; +using System.Text; +using NUnit.Framework; +using Umbraco.Core; + +namespace Umbraco.Tests.CoreThings +{ + public class HexEncoderTests + { + [Test] + public void ToHexStringCreatesCorrectValue() + { + var buffer = new byte[255]; + var random = new Random(); + random.NextBytes(buffer); + + var sb = new StringBuilder(buffer.Length * 2); + for (var i = 0; i < buffer.Length; i++) + { + sb.Append(buffer[i].ToString("X2")); + } + + var expected = sb.ToString(); + + var actual = HexEncoder.Encode(buffer); + Assert.AreEqual(expected, actual); + } + + [Test] + public void ToHexStringWithSeparatorCreatesCorrectValue() + { + var buffer = new byte[255]; + var random = new Random(); + random.NextBytes(buffer); + + var expected = ToHexString(buffer, '/', 2, 4); + var actual = HexEncoder.Encode(buffer, '/', 2, 4); + + Assert.AreEqual(expected, actual); + } + + private static readonly char[] _bytesToHexStringLookup = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F' }; + + // Reference implementation taken from original extension method. + private static string ToHexString(byte[] bytes, char separator, int blockSize, int blockCount) + { + int p = 0, bytesLength = bytes.Length, count = 0, size = 0; + var chars = new char[(bytesLength * 2) + blockCount]; + for (var i = 0; i < bytesLength; i++) + { + var b = bytes[i]; + chars[p++] = _bytesToHexStringLookup[b / 0x10]; + chars[p++] = _bytesToHexStringLookup[b % 0x10]; + if (count == blockCount) + { + continue; + } + + if (++size < blockSize) + { + continue; + } + + chars[p++] = separator; + size = 0; + count++; + } + return new string(chars, 0, chars.Length); + } + } +} diff --git a/src/Umbraco.Tests/PublishedContent/PublishedMediaTests.cs b/src/Umbraco.Tests/PublishedContent/PublishedMediaTests.cs index 784f534af4..508a005663 100644 --- a/src/Umbraco.Tests/PublishedContent/PublishedMediaTests.cs +++ b/src/Umbraco.Tests/PublishedContent/PublishedMediaTests.cs @@ -111,6 +111,7 @@ namespace Umbraco.Tests.PublishedContent } [Test] + [Ignore("No point testing with Examine, should refactor this test.")] public void Ensure_Children_Sorted_With_Examine() { var rebuilder = IndexInitializer.GetMediaIndexRebuilder(Container.GetInstance(), IndexInitializer.GetMockMediaService()); @@ -138,6 +139,7 @@ namespace Umbraco.Tests.PublishedContent } [Test] + [Ignore("No point testing with Examine, should refactor this test.")] public void Do_Not_Find_In_Recycle_Bin() { var rebuilder = IndexInitializer.GetMediaIndexRebuilder(Container.GetInstance(), IndexInitializer.GetMockMediaService()); @@ -185,6 +187,7 @@ namespace Umbraco.Tests.PublishedContent } [Test] + [Ignore("No point testing with Examine, should refactor this test.")] public void Children_With_Examine() { var rebuilder = IndexInitializer.GetMediaIndexRebuilder(Container.GetInstance(), IndexInitializer.GetMockMediaService()); @@ -212,6 +215,7 @@ namespace Umbraco.Tests.PublishedContent } [Test] + [Ignore("No point testing with Examine, should refactor this test.")] public void Descendants_With_Examine() { var rebuilder = IndexInitializer.GetMediaIndexRebuilder(Container.GetInstance(), IndexInitializer.GetMockMediaService()); @@ -239,6 +243,7 @@ namespace Umbraco.Tests.PublishedContent } [Test] + [Ignore("No point testing with Examine, should refactor this test.")] public void DescendantsOrSelf_With_Examine() { var rebuilder = IndexInitializer.GetMediaIndexRebuilder(Container.GetInstance(), IndexInitializer.GetMockMediaService()); @@ -266,6 +271,7 @@ namespace Umbraco.Tests.PublishedContent } [Test] + [Ignore("No point testing with Examine, should refactor this test.")] public void Ancestors_With_Examine() { var rebuilder = IndexInitializer.GetMediaIndexRebuilder(Container.GetInstance(), IndexInitializer.GetMockMediaService()); @@ -291,6 +297,7 @@ namespace Umbraco.Tests.PublishedContent } [Test] + [Ignore("No point testing with Examine, should refactor this test.")] public void AncestorsOrSelf_With_Examine() { var rebuilder = IndexInitializer.GetMediaIndexRebuilder(Container.GetInstance(), IndexInitializer.GetMockMediaService()); diff --git a/src/Umbraco.Tests/Services/ContentTypeServiceTests.cs b/src/Umbraco.Tests/Services/ContentTypeServiceTests.cs index bc0854bdb7..8ea4856861 100644 --- a/src/Umbraco.Tests/Services/ContentTypeServiceTests.cs +++ b/src/Umbraco.Tests/Services/ContentTypeServiceTests.cs @@ -148,7 +148,7 @@ namespace Umbraco.Tests.Services //change the content type to be invariant, we will also update the name here to detect the copy changes doc.SetCultureName("Hello2", "en-US"); ServiceContext.ContentService.Save(doc); - contentType.Variations = ContentVariation.Nothing; + contentType.Variations = ContentVariation.Nothing; ServiceContext.ContentTypeService.Save(contentType); doc = ServiceContext.ContentService.GetById(doc.Id); //re-get @@ -372,7 +372,7 @@ namespace Umbraco.Tests.Services doc2 = ServiceContext.ContentService.GetById(doc2.Id); //re-get //this will be null because the doc type was changed back to variant but it's property types don't get changed back - Assert.IsNull(doc.GetValue("title", "en-US")); + Assert.IsNull(doc.GetValue("title", "en-US")); Assert.IsNull(doc2.GetValue("title", "en-US")); } @@ -1714,50 +1714,65 @@ namespace Umbraco.Tests.Services // Arrange var service = ServiceContext.ContentTypeService; + // create 'page' content type with a 'Content_' group var page = MockedContentTypes.CreateSimpleContentType("page", "Page", null, false, "Content_"); + Assert.IsTrue(page.PropertyGroups.Contains("Content_")); + Assert.AreEqual(3, page.PropertyTypes.Count()); service.Save(page); + + // create 'contentPage' content type as a child of 'page' var contentPage = MockedContentTypes.CreateSimpleContentType("contentPage", "Content Page", page, true); - service.Save(contentPage); - var composition = MockedContentTypes.CreateMetaContentType(); - composition.AddPropertyGroup("Content"); - service.Save(composition); - //Adding Meta-composition to child doc type - contentPage.AddContentType(composition); + Assert.AreEqual(3, contentPage.PropertyTypes.Count()); service.Save(contentPage); - // Act - var propertyTypeOne = new PropertyType(Constants.PropertyEditors.Aliases.TextBox, ValueStorageType.Ntext, "testTextbox") + // add 'Content' group to 'meta' content type + var meta = MockedContentTypes.CreateMetaContentType(); + meta.AddPropertyGroup("Content"); + Assert.AreEqual(2, meta.PropertyTypes.Count()); + service.Save(meta); + + // add 'meta' content type to 'contentPage' composition + contentPage.AddContentType(meta); + service.Save(contentPage); + + // add property 'prop1' to 'contentPage' group 'Content_' + var prop1 = new PropertyType(Constants.PropertyEditors.Aliases.TextBox, ValueStorageType.Ntext, "testTextbox") { Name = "Test Textbox", Description = "", Mandatory = false, SortOrder = 1, DataTypeId = -88 }; - var firstOneAdded = contentPage.AddPropertyType(propertyTypeOne, "Content_"); - var propertyTypeTwo = new PropertyType(Constants.PropertyEditors.Aliases.TextBox, ValueStorageType.Ntext, "anotherTextbox") + var prop1Added = contentPage.AddPropertyType(prop1, "Content_"); + Assert.IsTrue(prop1Added); + + // add property 'prop2' to 'contentPage' group 'Content' + var prop2 = new PropertyType(Constants.PropertyEditors.Aliases.TextBox, ValueStorageType.Ntext, "anotherTextbox") { Name = "Another Test Textbox", Description = "", Mandatory = false, SortOrder = 1, DataTypeId = -88 }; - var secondOneAdded = contentPage.AddPropertyType(propertyTypeTwo, "Content"); + var prop2Added = contentPage.AddPropertyType(prop2, "Content"); + Assert.IsTrue(prop2Added); + + // save 'contentPage' content type service.Save(contentPage); - Assert.That(page.PropertyGroups.Contains("Content_"), Is.True); - var propertyGroup = page.PropertyGroups["Content_"]; - page.PropertyGroups.Add(new PropertyGroup(true) { Id = propertyGroup.Id, Name = "ContentTab", SortOrder = 0}); + var group = page.PropertyGroups["Content_"]; + group.Name = "ContentTab"; // rename the group service.Save(page); + Assert.AreEqual(3, page.PropertyTypes.Count()); - // Assert - Assert.That(firstOneAdded, Is.True); - Assert.That(secondOneAdded, Is.True); + // get 'contentPage' content type again + var contentPageAgain = service.Get("contentPage"); + Assert.IsNotNull(contentPageAgain); - var contentType = service.Get("contentPage"); - Assert.That(contentType, Is.Not.Null); + // assert that 'Content_' group is still there because we don't propagate renames + var findGroup = contentPageAgain.CompositionPropertyGroups.FirstOrDefault(x => x.Name == "Content_"); + Assert.IsNotNull(findGroup); - var compositionPropertyGroups = contentType.CompositionPropertyGroups; - - // now it is still 1, because we don't propagate renames anymore - Assert.That(compositionPropertyGroups.Count(x => x.Name.Equals("Content_")), Is.EqualTo(1)); - - var propertyTypeCount = contentType.PropertyTypes.Count(); - var compPropertyTypeCount = contentType.CompositionPropertyTypes.Count(); + // count all property types (local and composed) + var propertyTypeCount = contentPageAgain.PropertyTypes.Count(); Assert.That(propertyTypeCount, Is.EqualTo(5)); + + // count composed property types + var compPropertyTypeCount = contentPageAgain.CompositionPropertyTypes.Count(); Assert.That(compPropertyTypeCount, Is.EqualTo(10)); } diff --git a/src/Umbraco.Tests/TestHelpers/ControllerTesting/TestControllerActivatorBase.cs b/src/Umbraco.Tests/TestHelpers/ControllerTesting/TestControllerActivatorBase.cs index c65faf76c9..2cf64f04d1 100644 --- a/src/Umbraco.Tests/TestHelpers/ControllerTesting/TestControllerActivatorBase.cs +++ b/src/Umbraco.Tests/TestHelpers/ControllerTesting/TestControllerActivatorBase.cs @@ -163,11 +163,8 @@ namespace Umbraco.Tests.TestHelpers.ControllerTesting var membershipHelper = new MembershipHelper(umbCtx, Mock.Of(), Mock.Of()); - var mockedTypedContent = Mock.Of(); - var umbHelper = new UmbracoHelper(umbCtx, Mock.Of(), - mockedTypedContent, Mock.Of(), Mock.Of(), Mock.Of(), diff --git a/src/Umbraco.Tests/Testing/TestingTests/MockTests.cs b/src/Umbraco.Tests/Testing/TestingTests/MockTests.cs index d5f5778d1a..51855f7e19 100644 --- a/src/Umbraco.Tests/Testing/TestingTests/MockTests.cs +++ b/src/Umbraco.Tests/Testing/TestingTests/MockTests.cs @@ -60,7 +60,6 @@ namespace Umbraco.Tests.Testing.TestingTests // ReSharper disable once UnusedVariable var helper = new UmbracoHelper(umbracoContext, Mock.Of(), - Mock.Of(), Mock.Of(), Mock.Of(), Mock.Of(), diff --git a/src/Umbraco.Tests/Umbraco.Tests.csproj b/src/Umbraco.Tests/Umbraco.Tests.csproj index 7147cc8453..513701bf6a 100644 --- a/src/Umbraco.Tests/Umbraco.Tests.csproj +++ b/src/Umbraco.Tests/Umbraco.Tests.csproj @@ -118,6 +118,8 @@ + + diff --git a/src/Umbraco.Tests/UmbracoExamine/IndexInitializer.cs b/src/Umbraco.Tests/UmbracoExamine/IndexInitializer.cs index 1f849fc1ce..cbd335a6c4 100644 --- a/src/Umbraco.Tests/UmbracoExamine/IndexInitializer.cs +++ b/src/Umbraco.Tests/UmbracoExamine/IndexInitializer.cs @@ -146,7 +146,7 @@ namespace Umbraco.Tests.UmbracoExamine return mediaTypeServiceMock.Object; } - public static UmbracoContentIndexer GetUmbracoIndexer( + public static UmbracoContentIndex GetUmbracoIndexer( ProfilingLogger profilingLogger, Directory luceneDir, Analyzer analyzer = null, @@ -162,9 +162,9 @@ namespace Umbraco.Tests.UmbracoExamine if (validator == null) validator = new ContentValueSetValidator(true); - var i = new UmbracoContentIndexer( + var i = new UmbracoContentIndex( "testIndexer", - UmbracoExamineIndexer.UmbracoIndexFieldDefinitions, + UmbracoExamineIndex.UmbracoIndexFieldDefinitions, luceneDir, analyzer, profilingLogger, diff --git a/src/Umbraco.Tests/UmbracoExamine/IndexTest.cs b/src/Umbraco.Tests/UmbracoExamine/IndexTest.cs index 78bdb37cae..35e3524459 100644 --- a/src/Umbraco.Tests/UmbracoExamine/IndexTest.cs +++ b/src/Umbraco.Tests/UmbracoExamine/IndexTest.cs @@ -114,8 +114,8 @@ namespace Umbraco.Tests.UmbracoExamine Assert.AreEqual("value2", result.AllValues["grid.row1"][1]); Assert.IsTrue(result.Values.ContainsKey("grid")); Assert.AreEqual("value1 value2 ", result["grid"]); - Assert.IsTrue(result.Values.ContainsKey($"{UmbracoExamineIndexer.RawFieldPrefix}grid")); - Assert.AreEqual(json, result[$"{UmbracoExamineIndexer.RawFieldPrefix}grid"]); + Assert.IsTrue(result.Values.ContainsKey($"{UmbracoExamineIndex.RawFieldPrefix}grid")); + Assert.AreEqual(json, result[$"{UmbracoExamineIndex.RawFieldPrefix}grid"]); } } @@ -187,21 +187,21 @@ namespace Umbraco.Tests.UmbracoExamine [Test] public void Index_Move_Media_From_Non_Indexable_To_Indexable_ParentID() { + // create a validator with + // publishedValuesOnly false + // parentId 1116 (only content under that parent will be indexed) + var validator = new ContentValueSetValidator(false, 1116); using (var luceneDir = new RandomIdRamDirectory()) - using (var indexer = IndexInitializer.GetUmbracoIndexer(ProfilingLogger, luceneDir, - //make parent id 1116 - validator: new ContentValueSetValidator(false, 1116))) + using (var indexer = IndexInitializer.GetUmbracoIndexer(ProfilingLogger, luceneDir, validator: validator)) using (indexer.ProcessNonAsync()) { var searcher = indexer.GetSearcher(); //get a node from the data repo (this one exists underneath 2222) var node = _mediaService.GetLatestMediaByXpath("//*[string-length(@id)>0 and number(@id)>0]") - .Root - .Elements() - .Where(x => (int)x.Attribute("id") == 2112) - .First(); + .Root.Elements() + .First(x => (int) x.Attribute("id") == 2112); var currPath = (string)node.Attribute("path"); //should be : -1,1111,2222,2112 Assert.AreEqual("-1,1111,2222,2112", currPath); @@ -230,20 +230,21 @@ namespace Umbraco.Tests.UmbracoExamine [Test] public void Index_Move_Media_To_Non_Indexable_ParentID() { + // create a validator with + // publishedValuesOnly false + // parentId 2222 (only content under that parent will be indexed) + var validator = new ContentValueSetValidator(false, 2222); + using (var luceneDir = new RandomIdRamDirectory()) - using (var indexer1 = IndexInitializer.GetUmbracoIndexer(ProfilingLogger, luceneDir, - //make parent id 2222 - validator: new ContentValueSetValidator(false, 2222))) + using (var indexer1 = IndexInitializer.GetUmbracoIndexer(ProfilingLogger, luceneDir, validator: validator)) using (indexer1.ProcessNonAsync()) { var searcher = indexer1.GetSearcher(); //get a node from the data repo (this one exists underneath 2222) var node = _mediaService.GetLatestMediaByXpath("//*[string-length(@id)>0 and number(@id)>0]") - .Root - .Elements() - .Where(x => (int)x.Attribute("id") == 2112) - .First(); + .Root.Elements() + .First(x => (int) x.Attribute("id") == 2112); var currPath = (string)node.Attribute("path"); //should be : -1,1111,2222,2112 Assert.AreEqual("-1,1111,2222,2112", currPath); @@ -251,8 +252,6 @@ namespace Umbraco.Tests.UmbracoExamine //ensure it's indexed indexer1.IndexItem(node.ConvertToValueSet(IndexTypes.Media)); - - //it will exist because it exists under 2222 var results = searcher.Search(searcher.CreateCriteria().Id(2112).Compile()); Assert.AreEqual(1, results.Count()); @@ -264,8 +263,6 @@ namespace Umbraco.Tests.UmbracoExamine //now reindex the node, this should first delete it and then NOT add it because of the parent id constraint indexer1.IndexItems(new[] { node.ConvertToValueSet(IndexTypes.Media) }); - - //now ensure it's deleted results = searcher.Search(searcher.CreateCriteria().Id(2112).Compile()); Assert.AreEqual(0, results.Count()); diff --git a/src/Umbraco.Tests/UmbracoExamine/UmbracoContentValueSetValidatorTests.cs b/src/Umbraco.Tests/UmbracoExamine/UmbracoContentValueSetValidatorTests.cs index 934dd34503..8d7a446ccb 100644 --- a/src/Umbraco.Tests/UmbracoExamine/UmbracoContentValueSetValidatorTests.cs +++ b/src/Umbraco.Tests/UmbracoExamine/UmbracoContentValueSetValidatorTests.cs @@ -179,7 +179,7 @@ namespace Umbraco.Tests.UmbracoExamine { ["hello"] = "world", ["path"] = "-1,555", - [UmbracoExamineIndexer.PublishedFieldName] = 1 + [UmbracoExamineIndex.PublishedFieldName] = 1 })); Assert.AreEqual(ValueSetValidationResult.Valid, result); } @@ -213,7 +213,7 @@ namespace Umbraco.Tests.UmbracoExamine { ["hello"] = "world", ["path"] = "-1,555", - [UmbracoExamineIndexer.PublishedFieldName] = 0 + [UmbracoExamineIndex.PublishedFieldName] = 0 })); Assert.AreEqual(ValueSetValidationResult.Failed, result); @@ -222,7 +222,7 @@ namespace Umbraco.Tests.UmbracoExamine { ["hello"] = "world", ["path"] = "-1,555", - [UmbracoExamineIndexer.PublishedFieldName] = 1 + [UmbracoExamineIndex.PublishedFieldName] = 1 })); Assert.AreEqual(ValueSetValidationResult.Valid, result); } @@ -237,8 +237,8 @@ namespace Umbraco.Tests.UmbracoExamine { ["hello"] = "world", ["path"] = "-1,555", - [UmbracoContentIndexer.VariesByCultureFieldName] = 1, - [UmbracoExamineIndexer.PublishedFieldName] = 0 + [UmbracoContentIndex.VariesByCultureFieldName] = 1, + [UmbracoExamineIndex.PublishedFieldName] = 0 })); Assert.AreEqual(ValueSetValidationResult.Failed, result); @@ -247,8 +247,8 @@ namespace Umbraco.Tests.UmbracoExamine { ["hello"] = "world", ["path"] = "-1,555", - [UmbracoContentIndexer.VariesByCultureFieldName] = 1, - [UmbracoExamineIndexer.PublishedFieldName] = 1 + [UmbracoContentIndex.VariesByCultureFieldName] = 1, + [UmbracoExamineIndex.PublishedFieldName] = 1 })); Assert.AreEqual(ValueSetValidationResult.Valid, result); @@ -257,17 +257,17 @@ namespace Umbraco.Tests.UmbracoExamine { ["hello"] = "world", ["path"] = "-1,555", - [UmbracoContentIndexer.VariesByCultureFieldName] = 1, - [$"{UmbracoExamineIndexer.PublishedFieldName}_en-us"] = 1, + [UmbracoContentIndex.VariesByCultureFieldName] = 1, + [$"{UmbracoExamineIndex.PublishedFieldName}_en-us"] = 1, ["hello_en-us"] = "world", ["title_en-us"] = "my title", - [$"{UmbracoExamineIndexer.PublishedFieldName}_es-es"] = 0, + [$"{UmbracoExamineIndex.PublishedFieldName}_es-es"] = 0, ["hello_es-ES"] = "world", ["title_es-ES"] = "my title", - [UmbracoExamineIndexer.PublishedFieldName] = 1 + [UmbracoExamineIndex.PublishedFieldName] = 1 }); Assert.AreEqual(10, valueSet.Values.Count()); - Assert.IsTrue(valueSet.Values.ContainsKey($"{UmbracoExamineIndexer.PublishedFieldName}_es-es")); + Assert.IsTrue(valueSet.Values.ContainsKey($"{UmbracoExamineIndex.PublishedFieldName}_es-es")); Assert.IsTrue(valueSet.Values.ContainsKey("hello_es-ES")); Assert.IsTrue(valueSet.Values.ContainsKey("title_es-ES")); @@ -275,7 +275,7 @@ namespace Umbraco.Tests.UmbracoExamine Assert.AreEqual(ValueSetValidationResult.Filtered, result); Assert.AreEqual(7, valueSet.Values.Count()); //filtered to 7 values (removes es-es values) - Assert.IsFalse(valueSet.Values.ContainsKey($"{UmbracoExamineIndexer.PublishedFieldName}_es-es")); + Assert.IsFalse(valueSet.Values.ContainsKey($"{UmbracoExamineIndex.PublishedFieldName}_es-es")); Assert.IsFalse(valueSet.Values.ContainsKey("hello_es-ES")); Assert.IsFalse(valueSet.Values.ContainsKey("title_es-ES")); } diff --git a/src/Umbraco.Tests/Web/Mvc/SurfaceControllerTests.cs b/src/Umbraco.Tests/Web/Mvc/SurfaceControllerTests.cs index 81f338da87..dce975d0c4 100644 --- a/src/Umbraco.Tests/Web/Mvc/SurfaceControllerTests.cs +++ b/src/Umbraco.Tests/Web/Mvc/SurfaceControllerTests.cs @@ -103,6 +103,13 @@ namespace Umbraco.Tests.Web.Mvc { var publishedSnapshot = new Mock(); publishedSnapshot.Setup(x => x.Members).Returns(Mock.Of()); + var contentCache = new Mock(); + var content = new Mock(); + content.Setup(x => x.Id).Returns(2); + contentCache.Setup(x => x.GetById(It.IsAny())).Returns(content.Object); + var mediaCache = new Mock(); + publishedSnapshot.Setup(x => x.Content).Returns(contentCache.Object); + publishedSnapshot.Setup(x => x.Media).Returns(mediaCache.Object); var publishedSnapshotService = new Mock(); publishedSnapshotService.Setup(x => x.CreatePublishedSnapshot(It.IsAny())).Returns(publishedSnapshot.Object); var globalSettings = TestObjects.GetGlobalSettings(); @@ -121,9 +128,6 @@ namespace Umbraco.Tests.Web.Mvc var helper = new UmbracoHelper( umbracoContext, Mock.Of(), - Mock.Of(query => query.Content(It.IsAny()) == - //return mock of IPublishedContent for any call to GetById - Mock.Of(content => content.Id == 2)), Mock.Of(), Mock.Of(), Mock.Of(), diff --git a/src/Umbraco.Web.UI.Client/lib/markdown/markdown.css b/src/Umbraco.Web.UI.Client/lib/markdown/markdown.css index 45c9b39c48..5594f70634 100644 --- a/src/Umbraco.Web.UI.Client/lib/markdown/markdown.css +++ b/src/Umbraco.Web.UI.Client/lib/markdown/markdown.css @@ -26,6 +26,11 @@ width: 100%; } +/* unset the negative margin applied in button-groups.less to avoid flickering when hovering the button bar */ +.wmd-panel .btn-toolbar .btn-group>.btn+.btn { + margin-left: 0; +} + /* .icon-link, .icon-blockquote, diff --git a/src/Umbraco.Web.UI.Client/lib/markdown/markdown.editor.js b/src/Umbraco.Web.UI.Client/lib/markdown/markdown.editor.js index 21ad5d0a8b..e1617d8365 100644 --- a/src/Umbraco.Web.UI.Client/lib/markdown/markdown.editor.js +++ b/src/Umbraco.Web.UI.Client/lib/markdown/markdown.editor.js @@ -1390,7 +1390,7 @@ "Redo - Ctrl+Y" : "Redo - Ctrl+Shift+Z"; // mac and other non-Windows platforms - buttons.redo = makeButton("wmd-redo-button", redoTitle, "icon-share-alt", null, group4); + buttons.redo = makeButton("wmd-redo-button", redoTitle, "icon-redo", null, group4); buttons.redo.execute = function (manager) { if (manager) manager.redo(); }; if (helpOptions) { @@ -2107,4 +2107,4 @@ } -})(); \ No newline at end of file +})(); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbeditors.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbeditors.directive.js index c852228205..e68c2bbc9a 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbeditors.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbeditors.directive.js @@ -203,11 +203,14 @@ })); evts.push(eventsService.on("appState.editors.close", function (name, args) { - removeEditor(args.editor); - })); - - evts.push(eventsService.on("appState.editors.closeAll", function (name, args) { - scope.editors = []; + // remove the closed editor + if(args && args.editor) { + removeEditor(args.editor); + } + // close all editors + if(args && !args.editor && args.editors.length === 0) { + scope.editors = []; + } })); //ensure to unregister from all events! diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/events/events.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/events/events.directive.js index 21980be925..a6a5336af0 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/events/events.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/events/events.directive.js @@ -3,54 +3,6 @@ **/ angular.module('umbraco.directives') -.directive('onKeyup', function () { - return { - link: function (scope, elm, attrs) { - var f = function () { - scope.$apply(attrs.onKeyup); - }; - elm.on("keyup", f); - scope.$on("$destroy", function(){ elm.off("keyup", f);} ); - } - }; -}) - -.directive('onKeydown', function () { - return { - link: function (scope, elm, attrs) { - var f = function () { - scope.$apply(attrs.onKeydown); - }; - elm.on("keydown", f); - scope.$on("$destroy", function(){ elm.off("keydown", f);} ); - } - }; -}) - -.directive('onBlur', function () { - return { - link: function (scope, elm, attrs) { - var f = function () { - scope.$apply(attrs.onBlur); - }; - elm.on("blur", f); - scope.$on("$destroy", function(){ elm.off("blur", f);} ); - } - }; -}) - -.directive('onFocus', function () { - return { - link: function (scope, elm, attrs) { - var f = function () { - scope.$apply(attrs.onFocus); - }; - elm.on("focus", f); - scope.$on("$destroy", function(){ elm.off("focus", f);} ); - } - }; -}) - .directive('onDragEnter', function () { return { link: function (scope, elm, attrs) { diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/overlays/umboverlay.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/overlays/umboverlay.directive.js index 9f9f1aa21e..99b89bf8cf 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/overlays/umboverlay.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/overlays/umboverlay.directive.js @@ -124,58 +124,6 @@ - -

Content Picker

-Opens a content picker.
-view: contentpicker - - - - - - - - - - - - - -
ParamTypeDetails
model.multiPickerBooleanPick one or multiple items
- - - - - - - - - - - - - -
ReturnsTypeDetails
model.selectionArrayArray of content objects
- - -

Icon Picker

-Opens an icon picker.
-view: iconpicker - - - - - - - - - - - - - -
ReturnsTypeDetails
model.iconStringThe icon class
-

Item Picker

Opens an item picker.
view: itempicker @@ -220,170 +168,6 @@ Opens an item picker.
-

Macro Picker

-Opens a media picker.
-view: macropicker - - - - - - - - - - - - - - - -
ParamTypeDetails
model.dialogDataObjectObject which contains array of allowedMacros. Set to null to allow all.
- - - - - - - - - - - - - - - - - - - - -
ReturnsTypeDetails
model.macroParamsArrayArray of macro params
model.selectedMacroObjectThe selected macro
- -

Media Picker

-Opens a media picker.
-view: mediapicker - - - - - - - - - - - - - - - - - - - - - - - - - -
ParamTypeDetails
model.multiPickerBooleanPick one or multiple items
model.onlyImagesBooleanOnly display files that have an image file-extension
model.disableFolderSelectBooleanDisable folder selection
- - - - - - - - - - - - - - - -
ReturnsTypeDetails
model.selectedImagesArrayArray of selected images
- -

Member Group Picker

-Opens a member group picker.
-view: membergrouppicker - - - - - - - - - - - - - - - -
ParamTypeDetails
model.multiPickerBooleanPick one or multiple items
- - - - - - - - - - - - - - - - - - - - -
ReturnsTypeDetails
model.selectedMemberGroupStringThe selected member group
model.selectedMemberGroups (multiPicker)ArrayThe selected member groups
- -

Member Picker

-Opens a member picker.
-view: memberpicker - - - - - - - - - - - - - - - -
ParamTypeDetails
model.multiPickerBooleanPick one or multiple items
- - - - - - - - - - - - - - -
ReturnsTypeDetails
model.selectionArrayArray of selected members/td> -
-

YSOD

Opens an overlay to show a custom YSOD.
view: ysod 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 b7ea16ea19..b807a4dc31 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 @@ -990,26 +990,24 @@ function contentResource($q, $http, umbDataFormatter, umbRequestHelper) { * * * @param {Int} contentId The content Id - * @param {String} userName The name of the user that should have access (if using specific user protection) - * @param {String} password The password for the user that should have access (if using specific user protection) - * @param {Array} roles The roles that should have access (if using role based protection) + * @param {Array} groups The names of the groups that should have access (if using group based protection) + * @param {Array} usernames The usernames of the members that should have access (if using member based protection) * @param {Int} loginPageId The Id of the login page * @param {Int} errorPageId The Id of the error page * @returns {Promise} resourcePromise object containing the public access protection * */ - updatePublicAccess: function (contentId, userName, password, roles, loginPageId, errorPageId) { + updatePublicAccess: function (contentId, groups, usernames, loginPageId, errorPageId) { var publicAccess = { contentId: contentId, loginPageId: loginPageId, errorPageId: errorPageId }; - if (userName && password) { - publicAccess.userName = userName; - publicAccess.password = password; + if (angular.isArray(groups) && groups.length) { + publicAccess.groups = groups; } - else if (angular.isArray(roles) && roles.length) { - publicAccess.roles = roles; + else if (angular.isArray(usernames) && usernames.length) { + publicAccess.usernames = usernames; } else { throw "must supply either userName/password or roles"; diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/relationtype.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/relationtype.resource.js new file mode 100644 index 0000000000..7f13a46d2f --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/common/resources/relationtype.resource.js @@ -0,0 +1,122 @@ +/** + * @ngdoc service + * @name umbraco.resources.relationTypeResource + * @description Loads in data for relation types. + */ +function relationTypeResource($q, $http, umbRequestHelper, umbDataFormatter) { + return { + + /** + * @ngdoc method + * @name umbraco.resources.relationTypeResource#getById + * @methodOf umbraco.resources.relationTypeResource + * + * @description + * Gets a relation type with a given ID. + * + * ##usage + *
+         * relationTypeResource.getById(1234)
+         *    .then(function() {
+         *        alert('Found it!');
+         *    });
+         * 
+ * + * @param {Int} id of the relation type to get. + * @returns {Promise} resourcePromise containing relation type data. + */ + getById: function (id) { + return umbRequestHelper.resourcePromise( + $http.get(umbRequestHelper.getApiUrl("relationTypeApiBaseUrl", "GetById", [{ id: id }])), + "Failed to get item " + id + ); + }, + + /** + * @ngdoc method + * @name umbraco.resources.relationTypeResource#getRelationObjectTypes + * @methodOf umbraco.resources.relationTypeResource + * + * @description + * Gets a list of Umbraco object types which can be associated with a relation. + * + * @returns {Object} A collection of Umbraco object types. + */ + getRelationObjectTypes: function() { + return umbRequestHelper.resourcePromise( + $http.get( + umbRequestHelper.getApiUrl("relationTypeApiBaseUrl", "GetRelationObjectTypes") + ), + "Failed to get object types" + ); + }, + + /** + * @ngdoc method + * @name umbraco.resources.relationTypeResource#save + * @methodOf umbraco.resources.relationTypeResource + * + * @description + * Updates a relation type. + * + * @param {Object} relationType The relation type object to update. + * @returns {Promise} A resourcePromise object. + */ + save: function (relationType) { + var saveModel = umbDataFormatter.formatRelationTypePostData(relationType); + + return umbRequestHelper.resourcePromise( + $http.post(umbRequestHelper.getApiUrl("relationTypeApiBaseUrl", "PostSave"), saveModel), + "Failed to save data for relation type ID" + relationType.id + ); + }, + + /** + * @ngdoc method + * @name umbraco.resources.relationTypeResource#create + * @methodOf umbraco.resources.relationTypeResource + * + * @description + * Creates a new relation type. + * + * @param {Object} relationType The relation type object to create. + * @returns {Promise} A resourcePromise object. + */ + create: function (relationType) { + var createModel = umbDataFormatter.formatRelationTypePostData(relationType); + + return umbRequestHelper.resourcePromise( + $http.post(umbRequestHelper.getApiUrl("relationTypeApiBaseUrl", "PostCreate"), createModel), + "Failed to create new realtion" + ); + }, + + /** + * @ngdoc method + * @name umbraco.resources.relationTypeResource#deleteById + * @methodOf umbraco.resources.relationTypeResource + * + * @description + * Deletes a relation type with a given ID. + * + * * ## Usage + *
+         * relationTypeResource.deleteById(1234).then(function() {
+         *    alert('Deleted it!');
+         * });
+         * 
+ * + * @param {Int} id The ID of the relation type to delete. + * @returns {Promose} resourcePromise object. + */ + deleteById: function (id) { + return umbRequestHelper.resourcePromise( + $http.post(umbRequestHelper.getApiUrl("relationTypeApiBaseUrl", "DeleteById", [{ id: id }])), + "Failed to delete item " + id + ); + } + + }; +} + +angular.module("umbraco.resources").factory("relationTypeResource", relationTypeResource); diff --git a/src/Umbraco.Web.UI.Client/src/common/services/editor.service.js b/src/Umbraco.Web.UI.Client/src/common/services/editor.service.js index 449470f54c..0dbd27b7a5 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/editor.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/editor.service.js @@ -4,6 +4,76 @@ * * @description * Added in Umbraco 8.0. Application-wide service for handling infinite editing. + * + * +

Markup example

+
+    
+ + + +
+
+ +

Controller example

+
+    (function () {
+
+        "use strict";
+
+        function Controller() {
+
+            var vm = this;
+
+            vm.open = open;
+
+            function open() {
+                var mediaPickerOptions = {
+                    multiPicker: true,
+                    submit: function(model) {
+                        editorService.close();
+                    },
+                    close: function() {
+                        editorService.close();
+                    }
+                }
+                editorService.mediaPicker(mediaPickerOptions);
+            };
+        }
+
+        angular.module("umbraco").controller("My.Controller", Controller);
+    })();
+
+ +

Custom view example

+
+    (function () {
+
+        "use strict";
+
+        function Controller() {
+
+            var vm = this;
+
+            vm.open = open;
+
+            function open() {
+                var options = {
+                    view: "path/to/view.html"
+                    submit: function(model) {
+                        editorService.close();
+                    },
+                    close: function() {
+                        editorService.close();
+                    }
+                }
+                editorService.open(options);
+            };
+        }
+
+        angular.module("umbraco").controller("My.Controller", Controller);
+    })();
+
*/ (function () { "use strict"; @@ -43,6 +113,10 @@ * * @description * Method to open a new editor in infinite editing + * + * @param {Object} editor rendering options + * @param {String} editor.view Path to view + * @param {String} editor.size Sets the size of the editor ("Small"). If nothing is set it will use full width. */ function open(editor) { @@ -98,7 +172,7 @@ editor: null }; - eventsService.emit("appState.editors.closeAll", args); + eventsService.emit("appState.editors.close", args); } /** @@ -108,8 +182,12 @@ * * @description * Opens a media editor in infinite editing, the submit callback returns the updated content item + * @param {Object} editor rendering options * @param {String} editor.id The id of the content item * @param {Boolean} editor.create Create new content item + * @param {Function} editor.submit Callback function when the publish and close button is clicked. Returns the editor model object + * @param {Function} editor.close Callback function when the close button is clicked. + * * @returns {Object} editor object */ function contentEditor(editor) { @@ -124,6 +202,12 @@ * * @description * Opens a content picker in infinite editing, the submit callback returns an array of selected items + * + * @param {Object} editor rendering options + * @param {Boolean} editor.multiPicker Pick one or multiple items + * @param {Function} editor.submit Callback function when the submit button is clicked. Returns the editor model object + * @param {Function} editor.close Callback function when the close button is clicked. + * * @returns {Object} editor object */ function contentPicker(editor) { @@ -218,11 +302,13 @@ * * @description * Opens an embed editor in infinite editing. + * @param {Object} editor rendering options + * @param {String} editor.icon The icon class + * @param {String} editor.color The color class * @param {Callback} editor.submit Saves, submits, and closes the editor * @param {Callback} editor.close Closes the editor * @returns {Object} editor object */ - function linkPicker(editor) { editor.view = "views/common/infiniteeditors/linkpicker/linkpicker.html"; editor.size = "small"; @@ -236,6 +322,7 @@ * * @description * Opens a media editor in infinite editing, the submit callback returns the updated media item + * @param {Object} editor rendering options * @param {String} editor.id The id of the media item * @param {Boolean} editor.create Create new media item * @param {Callback} editor.submit Saves, submits, and closes the editor @@ -254,6 +341,7 @@ * * @description * Opens a media picker in infinite editing, the submit callback returns an array of selected media items + * @param {Object} editor rendering options * @param {Boolean} editor.multiPicker Pick one or multiple items * @param {Boolean} editor.onlyImages Only display files that have an image file-extension * @param {Boolean} editor.disableFolderSelect Disable folder selection @@ -276,6 +364,7 @@ * * @description * Opens an icon picker in infinite editing, the submit callback returns the selected icon + * @param {Object} editor rendering options * @param {Callback} editor.submit Submits the editor * @param {Callback} editor.close Closes the editor * @returns {Object} editor object @@ -293,6 +382,7 @@ * * @description * Opens the document type editor in infinite editing, the submit callback returns the saved document type + * @param {Object} editor rendering options * @param {Callback} editor.submit Submits the editor * @param {Callback} editor.close Closes the editor * @returns {Object} editor object @@ -309,6 +399,7 @@ * * @description * Opens the media type editor in infinite editing, the submit callback returns the saved media type + * @param {Object} editor rendering options * @param {Callback} editor.submit Submits the editor * @param {Callback} editor.close Closes the editor * @returns {Object} editor object @@ -318,24 +409,75 @@ open(editor); } + /** + * @ngdoc method + * @name umbraco.services.editorService#queryBuilder + * @methodOf umbraco.services.editorService + * + * @description + * Opens the query builder in infinite editing, the submit callback returns the generted query + * @param {Object} editor rendering options + * @param {Callback} editor.submit Submits the editor + * @param {Callback} editor.close Closes the editor + * @returns {Object} editor object + */ function queryBuilder(editor) { editor.view = "views/common/infiniteeditors/querybuilder/querybuilder.html"; editor.size = "small"; open(editor); } + /** + * @ngdoc method + * @name umbraco.services.editorService#treePicker + * @methodOf umbraco.services.editorService + * + * @description + * Opens the query builder in infinite editing, the submit callback returns the generted query + * @param {Object} editor rendering options + * @param {String} options.section tree section to display + * @param {String} options.treeAlias specific tree to display + * @param {Boolean} options.multiPicker should the tree pick one or multiple items before returning + * @param {Callback} editor.submit Submits the editor + * @param {Callback} editor.close Closes the editor + * @returns {Object} editor object + */ function treePicker(editor) { editor.view = "views/common/infiniteeditors/treepicker/treepicker.html"; editor.size = "small"; open(editor); } + /** + * @ngdoc method + * @name umbraco.services.editorService#nodePermissions + * @methodOf umbraco.services.editorService + * + * @description + * Opens the an editor to set node permissions. + * @param {Object} editor rendering options + * @param {Callback} editor.submit Submits the editor + * @param {Callback} editor.close Closes the editor + * @returns {Object} editor object + */ function nodePermissions(editor) { editor.view = "views/common/infiniteeditors/nodepermissions/nodepermissions.html"; editor.size = "small"; open(editor); } + /** + * @ngdoc method + * @name umbraco.services.editorService#insertCodeSnippet + * @methodOf umbraco.services.editorService + * + * @description + * Open an editor to insert code snippets into the code editor + * @param {Object} editor rendering options + * @param {Callback} editor.submit Submits the editor + * @param {Callback} editor.close Closes the editor + * @returns {Object} editor object + */ function insertCodeSnippet(editor) { editor.view = "views/common/infiniteeditors/insertcodesnippet/insertcodesnippet.html"; editor.size = "small"; @@ -349,6 +491,7 @@ * * @description * Opens the user group picker in infinite editing, the submit callback returns an array of the selected user groups + * @param {Object} editor rendering options * @param {Callback} editor.submit Submits the editor * @param {Callback} editor.close Closes the editor * @returns {Object} editor object @@ -366,6 +509,7 @@ * * @description * Opens the user group picker in infinite editing, the submit callback returns the saved template + * @param {Object} editor rendering options * @param {String} editor.id The template id * @param {Callback} editor.submit Submits the editor * @param {Callback} editor.close Closes the editor @@ -382,7 +526,8 @@ * @methodOf umbraco.services.editorService * * @description - * Opens the section picker in infinite editing, the submit callback returns an array of the selected sections + * Opens the section picker in infinite editing, the submit callback returns an array of the selected sections¨ + * @param {Object} editor rendering options * @param {Callback} editor.submit Submits the editor * @param {Callback} editor.close Closes the editor * @returns {Object} editor object @@ -400,6 +545,7 @@ * * @description * Opens the insert field editor in infinite editing, the submit callback returns the code snippet + * @param {Object} editor rendering options * @param {Callback} editor.submit Submits the editor * @param {Callback} editor.close Closes the editor * @returns {Object} editor object @@ -417,6 +563,7 @@ * * @description * Opens the template sections editor in infinite editing, the submit callback returns the type to insert + * @param {Object} editor rendering options * @param {Callback} editor.submit Submits the editor * @param {Callback} editor.close Closes the editor * @returns {Object} editor object @@ -429,11 +576,12 @@ /** * @ngdoc method - * @name umbraco.services.editorService#sectionPicker + * @name umbraco.services.editorService#userPicker * @methodOf umbraco.services.editorService * * @description * Opens the section picker in infinite editing, the submit callback returns an array of the selected users + * @param {Object} editor rendering options * @param {Callback} editor.submit Submits the editor * @param {Callback} editor.close Closes the editor * @returns {Object} editor object @@ -452,6 +600,7 @@ * @description * Opens the section picker in infinite editing, the submit callback returns an array of the selected items * + * @param {Object} editor rendering options * @param {Array} editor.availableItems Array of available items. * @param {Array} editor.selectedItems Array of selected items. When passed in the selected items will be filtered from the available items. * @param {Boolean} editor.filter Set to false to hide the filter. @@ -485,12 +634,14 @@ /** * @ngdoc method - * @name umbraco.services.editorService#macroPicker + * @name umbraco.services.editorService#memberGroupPicker * @methodOf umbraco.services.editorService * * @description * Opens a member group picker in infinite editing. * + * @param {Object} editor rendering options + * @param {Object} editor.multiPicker Pick one or multiple items. * @param {Callback} editor.submit Submits the editor. * @param {Callback} editor.close Closes the editor. * @returns {Object} editor object 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 668509cdf3..e31742e660 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 @@ -431,6 +431,24 @@ } return displayModel; + }, + + /** + * Formats the display model used to display the relation type to a model used to save the relation type. + * @param {Object} relationType + */ + formatRelationTypePostData : function(relationType) { + var saveModel = { + id: relationType.id, + name: relationType.name, + alias: relationType.alias, + key : relationType.key, + isBidirectional: relationType.isBidirectional, + parentObjectType: relationType.parentObjectType, + childObjectType: relationType.childObjectType + }; + + return saveModel; } }; } diff --git a/src/Umbraco.Web.UI.Client/src/less/dashboards.less b/src/Umbraco.Web.UI.Client/src/less/dashboards.less index 5fd0e25be1..cc13ad31fd 100644 --- a/src/Umbraco.Web.UI.Client/src/less/dashboards.less +++ b/src/Umbraco.Web.UI.Client/src/less/dashboards.less @@ -1,13 +1,14 @@ .umb-dashboards-forms-install { background: url('../img/forms/installer-background.png'); background-repeat: repeat-x; - position: relative; - top: -30px; + position: absolute; + top: 0; + left: 0; + right: 0; + bottom: 0; padding-top: 30px; - box-shadow: inset 0px -40px 30px 25px rgba(255,255,255,1); - -moz-border-radius: 0px 0px 200px 200px; - -webkit-border-radius: 0px 0px 200px 200px; - border-radius: 0px 0px 200px 200px; + background-color: @white; + overflow: auto; small { font-size: 14px; diff --git a/src/Umbraco.Web.UI.Client/src/less/hacks.less b/src/Umbraco.Web.UI.Client/src/less/hacks.less index cd32c64782..0bbb89a250 100644 --- a/src/Umbraco.Web.UI.Client/src/less/hacks.less +++ b/src/Umbraco.Web.UI.Client/src/less/hacks.less @@ -106,7 +106,6 @@ iframe, .content-column-body { display: flex; flex-wrap: nowrap; flex-direction: row; - justify-content: center; align-items: flex-start; margin-top: 15px; diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/embed/embed.html b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/embed/embed.html index f6a641f2af..f14fb364ab 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/embed/embed.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/embed/embed.html @@ -32,11 +32,11 @@
- + - + diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/mediapicker/mediapicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/mediapicker/mediapicker.controller.js index c7a46c4c4a..a274c25287 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/mediapicker/mediapicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/mediapicker/mediapicker.controller.js @@ -4,9 +4,12 @@ angular.module("umbraco") function($scope, mediaResource, entityResource, mediaHelper, mediaTypeHelper, eventsService, treeService, localStorageService, localizationService, editorService) { if (!$scope.model.title) { - localizationService.localize("defaultdialogs_selectMedia") - .then(function(data){ - $scope.model.title = data; + localizationService.localizeMany(["defaultdialogs_selectMedia", "general_includeFromsubFolders"]) + .then(function (data) { + $scope.labels = { + title: data[0], + includeSubFolders: data[1] + } }); } @@ -113,10 +116,10 @@ angular.module("umbraco") }; $scope.submitFolder = function() { - if ($scope.newFolderName) { + if ($scope.model.newFolderName) { $scope.creatingFolder = true; mediaResource - .addFolder($scope.newFolderName, $scope.currentFolder.id) + .addFolder($scope.model.newFolderName, $scope.currentFolder.id) .then(function(data) { //we've added a new folder so lets clear the tree cache for that specific item treeService.clearCache({ @@ -126,7 +129,7 @@ angular.module("umbraco") $scope.creatingFolder = false; $scope.gotoFolder(data); $scope.showFolderInput = false; - $scope.newFolderName = ""; + $scope.model.newFolderName = ""; }); } else { $scope.showFolderInput = false; diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/mediapicker/mediapicker.html b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/mediapicker/mediapicker.html index 5046088d28..3bb694cac6 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/mediapicker/mediapicker.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/mediapicker/mediapicker.html @@ -68,8 +68,8 @@ - +
diff --git a/src/Umbraco.Web.UI.Client/src/views/components/umb-groups-builder.html b/src/Umbraco.Web.UI.Client/src/views/components/umb-groups-builder.html index 31ea24b581..c0490a1707 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/umb-groups-builder.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/umb-groups-builder.html @@ -65,7 +65,7 @@ ng-disabled="tab.inherited" umb-auto-focus umb-auto-resize - on-focus="activateGroup(tab)" + ng-focus="activateGroup(tab)" required val-server-field="{{'Groups[' + $index + '].Name'}}" /> @@ -87,7 +87,7 @@
- +
@@ -121,7 +121,7 @@ hotkey="alt+shift+p" hotkey-when="{{tab.tabState === 'active' && property.propertyState=='init'}}" ng-click="addProperty(property, tab)" - on-focus="activateGroup(tab)" + ng-focus="activateGroup(tab)" focus-when="{{property.focus}}"> diff --git a/src/Umbraco.Web.UI.Client/src/views/components/umb-locked-field.html b/src/Umbraco.Web.UI.Client/src/views/components/umb-locked-field.html index 56c5be6fb2..d13474d221 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/umb-locked-field.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/umb-locked-field.html @@ -24,7 +24,7 @@ title="{{ngModel}}" focus-when="{{!locked}}" umb-select-when="{{!locked}}" - on-blur="lock()" /> + ng-blur="lock()" />
diff --git a/src/Umbraco.Web.UI.Client/src/views/content/content.copy.controller.js b/src/Umbraco.Web.UI.Client/src/views/content/content.copy.controller.js index 2642a79ac7..a1e74cc207 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/content.copy.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/content/content.copy.controller.js @@ -25,11 +25,11 @@ angular.module("umbraco").controller("Umbraco.Editors.Content.CopyController", $scope.treeModel.hideHeader = userData.startContentIds.length > 0 && userData.startContentIds.indexOf(-1) == -1; }); - var node = $scope.currentNode; + $scope.source = _.clone($scope.currentNode); function treeLoadedHandler(args) { - if (node && node.path) { - $scope.dialogTreeApi.syncTree({ path: node.path, activate: false }); + if ($scope.source && $scope.source.path) { + $scope.dialogTreeApi.syncTree({ path: $scope.source.path, activate: false }); } } @@ -107,7 +107,7 @@ angular.module("umbraco").controller("Umbraco.Editors.Content.CopyController", $scope.busy = true; $scope.error = false; - contentResource.copy({ parentId: $scope.target.id, id: node.id, relateToOriginal: $scope.relateToOriginal, recursive: $scope.recursive }) + contentResource.copy({ parentId: $scope.target.id, id: $scope.source.id, relateToOriginal: $scope.relateToOriginal, recursive: $scope.recursive }) .then(function (path) { $scope.error = false; $scope.success = true; diff --git a/src/Umbraco.Web.UI.Client/src/views/content/content.move.controller.js b/src/Umbraco.Web.UI.Client/src/views/content/content.move.controller.js index a593bbad24..5dceff2571 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/content.move.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/content/content.move.controller.js @@ -22,11 +22,11 @@ angular.module("umbraco").controller("Umbraco.Editors.Content.MoveController", $scope.treeModel.hideHeader = userData.startContentIds.length > 0 && userData.startContentIds.indexOf(-1) == -1; }); - var node = $scope.currentNode; + $scope.source = _.clone($scope.currentNode); function treeLoadedHandler(args) { - if (node && node.path) { - $scope.dialogTreeApi.syncTree({ path: node.path, activate: false }); + if ($scope.source && $scope.source.path) { + $scope.dialogTreeApi.syncTree({ path: $scope.source.path, activate: false }); } } @@ -84,7 +84,7 @@ angular.module("umbraco").controller("Umbraco.Editors.Content.MoveController", $scope.busy = true; $scope.error = false; - contentResource.move({ parentId: $scope.target.id, id: node.id }) + contentResource.move({ parentId: $scope.target.id, id: $scope.source.id }) .then(function (path) { $scope.error = false; $scope.success = true; diff --git a/src/Umbraco.Web.UI.Client/src/views/content/content.protect.controller.js b/src/Umbraco.Web.UI.Client/src/views/content/content.protect.controller.js index 4504ebd372..8d80f308ab 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/content.protect.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/content/content.protect.controller.js @@ -1,7 +1,7 @@ (function () { "use strict"; - function ContentProtectController($scope, $routeParams, contentResource, memberGroupResource, navigationService, localizationService, editorService) { + function ContentProtectController($scope, $q, contentResource, memberResource, memberGroupResource, navigationService, localizationService, editorService) { var vm = this; var id = $scope.currentNode.id; @@ -18,6 +18,8 @@ vm.pickErrorPage = pickErrorPage; vm.pickGroup = pickGroup; vm.removeGroup = removeGroup; + vm.pickMember = pickMember; + vm.removeMember = removeMember; vm.removeProtection = removeProtection; vm.removeProtectionConfirm = removeProtectionConfirm; @@ -29,39 +31,38 @@ // get the current public access protection contentResource.getPublicAccess(id).then(function (publicAccess) { + vm.loading = false; + // init the current settings for public access (if any) vm.loginPage = publicAccess.loginPage; vm.errorPage = publicAccess.errorPage; - vm.userName = publicAccess.userName; - vm.roles = publicAccess.roles; + vm.groups = publicAccess.groups || []; + vm.members = publicAccess.members || []; vm.canRemove = true; - if (vm.userName) { - vm.type = "user"; + if (vm.members.length) { + vm.type = "member"; next(); } - else if (vm.roles) { - vm.type = "role"; + else if (vm.groups.length) { + vm.type = "group"; next(); } else { vm.canRemove = false; - vm.loading = false; } }); } function next() { - if (vm.type === "role") { + if (vm.type === "group") { vm.loading = true; - // Get all member groups + // get all existing member groups for lookup upon selection + // NOTE: if/when member groups support infinite editing, we can't rely on using a cached lookup list of valid groups anymore memberGroupResource.getGroups().then(function (groups) { vm.step = vm.type; vm.allGroups = groups; vm.hasGroups = groups.length > 0; - vm.groups = _.filter(groups, function(group) { - return _.contains(vm.roles, group.name); - }); vm.loading = false; }); } @@ -80,16 +81,20 @@ if (!vm.loginPage || !vm.errorPage) { return false; } - if (vm.type === "role") { + if (vm.type === "group") { return vm.groups && vm.groups.length > 0; } + if (vm.type === "member") { + return vm.members && vm.members.length > 0; + } return true; } function save() { vm.buttonState = "busy"; - var roles = _.map(vm.groups, function (group) { return group.name; }); - contentResource.updatePublicAccess(id, vm.userName, vm.password, roles, vm.loginPage.id, vm.errorPage.id).then( + var groups = _.map(vm.groups, function (group) { return group.name; }); + var usernames = _.map(vm.members, function (member) { return member.username; }); + contentResource.updatePublicAccess(id, groups, usernames, vm.loginPage.id, vm.errorPage.id).then( function () { localizationService.localize("publicAccess_paIsProtected", [$scope.currentNode.name]).then(function (value) { vm.success = { @@ -123,9 +128,10 @@ ? model.selectedMemberGroups : [model.selectedMemberGroup]; _.each(selectedGroupIds, - function(groupId) { + function (groupId) { + // find the group in the lookup list and add it if it isn't already var group = _.find(vm.allGroups, function(g) { return g.id === parseInt(groupId); }); - if (group && !_.contains(vm.groups, group)) { + if (group && !_.find(vm.groups, function (g) { return g.id === group.id })) { vm.groups.push(group); } }); @@ -140,7 +146,57 @@ } function removeGroup(group) { - vm.groups = _.without(vm.groups, group); + vm.groups = _.reject(vm.groups, function(g) { return g.id === group.id }); + } + + function pickMember() { + navigationService.allowHideDialog(false); + // TODO: once editorService has a memberPicker method, use that instead + editorService.treePicker({ + multiPicker: true, + entityType: "Member", + section: "member", + treeAlias: "member", + filter: function (i) { + return i.metaData.isContainer; + }, + filterCssClass: "not-allowed", + submit: function (model) { + if (model.selection && model.selection.length) { + var promises = []; + // get the selected member usernames + _.each(model.selection, + function (member) { + // TODO: + // as-is we need to fetch all the picked members one at a time to get their usernames. + // when editorService has a memberPicker method, see if this can't be avoided - otherwise + // add a memberResource.getByKeys() method to do all this in one request + promises.push( + memberResource.getByKey(member.key).then(function(newMember) { + if (!_.find(vm.members, function (currentMember) { return currentMember.username === newMember.username })) { + vm.members.push(newMember); + } + }) + ); + }); + editorService.close(); + navigationService.allowHideDialog(true); + // wait for all the member lookups to complete + vm.loading = true; + $q.all(promises).then(function() { + vm.loading = false; + }); + } + }, + close: function () { + editorService.close(); + navigationService.allowHideDialog(true); + } + }); + } + + function removeMember(member) { + vm.members = _.without(vm.members, member); } function pickLoginPage() { diff --git a/src/Umbraco.Web.UI.Client/src/views/content/copy.html b/src/Umbraco.Web.UI.Client/src/views/content/copy.html index 03a024c439..8f6d5d3e6d 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/copy.html +++ b/src/Umbraco.Web.UI.Client/src/views/content/copy.html @@ -11,14 +11,14 @@
- {{currentNode.name}} was copied to + {{source.name}} was copied to {{target.name}}
- +

- Choose where to copy {{currentNode.name}} to in the tree structure below + Choose where to copy {{source.name}} to in the tree structure below

diff --git a/src/Umbraco.Web.UI.Client/src/views/content/move.html b/src/Umbraco.Web.UI.Client/src/views/content/move.html index 708c3d3f82..b655917f6f 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/move.html +++ b/src/Umbraco.Web.UI.Client/src/views/content/move.html @@ -11,14 +11,14 @@
- {{currentNode.name}} was moved underneath {{target.name}} + {{source.name}} was moved underneath {{target.name}}
- +

Choose where to move - {{currentNode.name}} + {{source.name}} to in the tree structure below

diff --git a/src/Umbraco.Web.UI.Client/src/views/content/protect.html b/src/Umbraco.Web.UI.Client/src/views/content/protect.html index 4b2bf54b5c..ae4a15e8c1 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/protect.html +++ b/src/Umbraco.Web.UI.Client/src/views/content/protect.html @@ -20,45 +20,47 @@
- + -
- + -
-
-

Set the login and password for this page

+
+

Select the members that should have access to this page

- - - - - - - + + + + Add +
-
-

You need to create a membergroup before you can use role-based authentication

+
+

You need to create a member group before you can use group based authentication

-
-

Pick the roles who have access to this page

+
+

Select the groups that should have access to this page

-
+

Select the pages that contain login form and error messages

diff --git a/src/Umbraco.Web.UI.Client/src/views/media/media.move.controller.js b/src/Umbraco.Web.UI.Client/src/views/media/media.move.controller.js index c5ff80b7fb..8bb1dc3ac8 100644 --- a/src/Umbraco.Web.UI.Client/src/views/media/media.move.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/media/media.move.controller.js @@ -3,7 +3,7 @@ angular.module("umbraco").controller("Umbraco.Editors.Media.MoveController", function ($scope, userService, eventsService, mediaResource, appState, treeService, navigationService) { $scope.dialogTreeApi = {}; - var node = $scope.currentNode; + $scope.source = _.clone($scope.currentNode); $scope.treeModel = { hideHeader: false @@ -13,8 +13,8 @@ angular.module("umbraco").controller("Umbraco.Editors.Media.MoveController", }); function treeLoadedHandler(args) { - if (node && node.path) { - $scope.dialogTreeApi.syncTree({ path: node.path, activate: false }); + if ($scope.source && $scope.source.path) { + $scope.dialogTreeApi.syncTree({ path: $scope.source.path, activate: false }); } } @@ -55,7 +55,7 @@ angular.module("umbraco").controller("Umbraco.Editors.Media.MoveController", $scope.move = function () { $scope.busy = true; - mediaResource.move({ parentId: $scope.target.id, id: node.id }) + mediaResource.move({ parentId: $scope.target.id, id: $scope.source.id }) .then(function (path) { $scope.busy = false; $scope.error = false; diff --git a/src/Umbraco.Web.UI.Client/src/views/media/move.html b/src/Umbraco.Web.UI.Client/src/views/media/move.html index 6d93eb4e22..451acbdb16 100644 --- a/src/Umbraco.Web.UI.Client/src/views/media/move.html +++ b/src/Umbraco.Web.UI.Client/src/views/media/move.html @@ -11,14 +11,14 @@
- {{currentNode.name}} was moved underneath {{target.name}} + {{source.name}} was moved underneath {{target.name}}
- +

Choose where to move - {{currentNode.name}} + {{source.name}} to in the tree structure below

diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/datepicker/datepicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/datepicker/datepicker.controller.js index 704fccbde1..62099734fb 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/datepicker/datepicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/datepicker/datepicker.controller.js @@ -131,6 +131,7 @@ function dateTimePickerController($scope, notificationsService, assetsService, a else { $scope.model.value = null; } + angularHelper.getCurrentForm($scope).$setDirty(); } /** Sets the value of the date picker control adn associated viewModel objects based on the model value */ diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/markdowneditor/markdowneditor.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/markdowneditor/markdowneditor.controller.js index 639c24f54c..b3c014f54a 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/markdowneditor/markdowneditor.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/markdowneditor/markdowneditor.controller.js @@ -8,6 +8,10 @@ function MarkdownEditorController($scope, $element, assetsService, editorService $scope.model.value = $scope.model.config.defaultValue; } + // create a unique ID for the markdown editor, so the button bar bindings can handle split view + // - must be bound on scope, not scope.model - otherwise it won't work, because $scope.model is used in both sides of the split view + $scope.editorId = $scope.model.alias + _.uniqueId("-"); + function openMediaPicker(callback) { var mediaPicker = { disableFolderSelect: true, @@ -40,7 +44,7 @@ function MarkdownEditorController($scope, $element, assetsService, editorService $timeout(function () { $scope.markdownEditorInitComplete = false; var converter2 = new Markdown.Converter(); - var editor2 = new Markdown.Editor(converter2, "-" + $scope.model.alias); + var editor2 = new Markdown.Editor(converter2, "-" + $scope.editorId); editor2.run(); //subscribe to the image dialog clicks diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/markdowneditor/markdowneditor.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/markdowneditor/markdowneditor.html index 9b8f44317a..68fc86f002 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/markdowneditor/markdowneditor.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/markdowneditor/markdowneditor.html @@ -1,8 +1,8 @@
-
+
- + -
+
diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/slider/slider.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/slider/slider.controller.js index c8813ee4c0..515eddf0a3 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/slider/slider.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/slider/slider.controller.js @@ -1,4 +1,4 @@ -function sliderController($scope) { +function sliderController($scope, angularHelper) { let sliderRef = null; @@ -14,13 +14,14 @@ function setModelValue(values) { $scope.model.value = values ? values.toString() : null; + angularHelper.getCurrentForm($scope).$setDirty(); } $scope.setup = function(slider) { sliderRef = slider; }; - $scope.end = function(values) { + $scope.change = function (values) { setModelValue(values); }; diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/slider/slider.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/slider/slider.html index c147b30b23..ea0c2ef7bc 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/slider/slider.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/slider/slider.html @@ -5,7 +5,7 @@ ng-model="sliderValue" options="sliderOptions" on-setup="setup(slider)" - on-end="end(values)"> + on-change="change(values)">
diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/tags/tags.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/tags/tags.html index f1d46e951e..6fcdfafdcd 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/tags/tags.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/tags/tags.html @@ -26,7 +26,7 @@ class="typeahead tags-{{model.alias}}" ng-model="$parent.tagToAdd" ng-keydown="$parent.addTagOnEnter($event)" - on-blur="$parent.addTag()" + ng-blur="$parent.addTag()" localize="placeholder" placeholder="@placeholders_enterTags" /> diff --git a/src/Umbraco.Web.UI.Client/src/views/relationtypes/create.controller.js b/src/Umbraco.Web.UI.Client/src/views/relationtypes/create.controller.js new file mode 100644 index 0000000000..2cef0bc5a4 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/relationtypes/create.controller.js @@ -0,0 +1,51 @@ +/** + * @ngdoc controller + * @name Umbraco.Editors.RelationTypes.CreateController + * @function + * + * @description + * The controller for creating relation types. + */ +function RelationTypeCreateController($scope, $location, relationTypeResource, navigationService, formHelper, appState, notificationsService) { + var vm = this; + vm.relationType = {}; + vm.objectTypes = {}; + + vm.createRelationType = createRelationType; + + init(); + + function init() { + relationTypeResource.getRelationObjectTypes().then(function (data) { + vm.objectTypes = data; + }, function (err) { + notificationsService.error("Could not load form.") + }) + } + + function createRelationType() { + if (formHelper.submitForm({ scope: $scope, formCtrl: this.createRelationTypeForm, statusMessage: "Creating relation type..." })) { + var node = $scope.currentNode; + + relationTypeResource.create(vm.relationType).then(function (data) { + navigationService.hideMenu(); + + // Set the new item as active in the tree + var currentPath = node.path ? node.path : "-1"; + navigationService.syncTree({ tree: "relationTypes", path: currentPath + "," + data, forceReload: true, activate: true }); + + formHelper.resetForm({ scope: $scope }); + + var currentSection = appState.getSectionState("currentSection"); + $location.path("/" + currentSection + "/relationTypes/edit/" + data); + }, function (err) { + if (err.data && err.data.message) { + notificationsService.error(err.data.message); + navigationService.hideMenu(); + } + }); + } + } +} + +angular.module("umbraco").controller("Umbraco.Editors.RelationTypes.CreateController", RelationTypeCreateController); diff --git a/src/Umbraco.Web.UI.Client/src/views/relationtypes/create.html b/src/Umbraco.Web.UI.Client/src/views/relationtypes/create.html new file mode 100644 index 0000000000..e5f66c9fe0 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/relationtypes/create.html @@ -0,0 +1,58 @@ + + + diff --git a/src/Umbraco.Web.UI.Client/src/views/relationtypes/delete.controller.js b/src/Umbraco.Web.UI.Client/src/views/relationtypes/delete.controller.js new file mode 100644 index 0000000000..1a32f17a46 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/relationtypes/delete.controller.js @@ -0,0 +1,41 @@ +/** + * @ngdoc controller + * @name Umbraco.Editors.RelationTypes.DeleteController + * @function + * + * @description + * The controller for deleting relation types. + */ +function RelationTypeDeleteController($scope, $location, relationTypeResource, treeService, navigationService, appState) { + + var vm = this; + + vm.cancel = cancel; + vm.performDelete = performDelete; + + function cancel() { + navigationService.hideDialog(); + } + + function performDelete() { + // stop from firing again on double-click + if ($scope.busy) { return false; } + + //mark it for deletion (used in the UI) + $scope.currentNode.loading = true; + $scope.busy = true; + + relationTypeResource.deleteById($scope.currentNode.id).then(function () { + $scope.currentNode.loading = false; + + treeService.removeNode($scope.currentNode); + + navigationService.hideMenu(); + + var currentSection = appState.getSectionState("currentSection"); + $location.path("/" + currentSection + "/"); + }); + } +} + +angular.module("umbraco").controller("Umbraco.Editors.RelationTypes.DeleteController", RelationTypeDeleteController); diff --git a/src/Umbraco.Web.UI.Client/src/views/relationtypes/delete.html b/src/Umbraco.Web.UI.Client/src/views/relationtypes/delete.html new file mode 100644 index 0000000000..e0fdbc6751 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/relationtypes/delete.html @@ -0,0 +1,12 @@ +
+
+ +

+ Are you sure you want to delete {{currentNode.name}}? +

+ + + + +
+
diff --git a/src/Umbraco.Web.UI.Client/src/views/relationtypes/edit.controller.js b/src/Umbraco.Web.UI.Client/src/views/relationtypes/edit.controller.js new file mode 100644 index 0000000000..ed0845a773 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/relationtypes/edit.controller.js @@ -0,0 +1,107 @@ +/** + * @ngdoc controller + * @name Umbraco.Editors.RelationTypes.EditController + * @function + * + * @description + * The controller for editing relation types. + */ +function RelationTypeEditController($scope, $routeParams, relationTypeResource, editorState, navigationService, dateHelper, userService, entityResource, formHelper, contentEditingHelper, localizationService) { + + var vm = this; + + vm.page = {}; + vm.page.loading = false; + vm.page.saveButtonState = "init"; + vm.page.menu = {} + + vm.save = saveRelationType; + + init(); + + function init() { + vm.page.loading = true; + + localizationService.localizeMany(["relationType_tabRelationType", "relationType_tabRelations"]).then(function (data) { + vm.page.navigation = [ + { + "name": data[0], + "alias": "relationType", + "icon": "icon-info", + "view": "views/relationTypes/views/relationType.html", + "active": true + }, + { + "name": data[1], + "alias": "relations", + "icon": "icon-trafic", + "view": "views/relationTypes/views/relations.html" + } + ]; + }); + + relationTypeResource.getById($routeParams.id) + .then(function(data) { + bindRelationType(data); + vm.page.loading = false; + }); + } + + function bindRelationType(relationType) { + formatDates(relationType.relations); + getRelationNames(relationType); + + vm.relationType = relationType; + + editorState.set(vm.relationType); + + navigationService.syncTree({ tree: "relationTypes", path: relationType.path, forceReload: true }).then(function (syncArgs) { + vm.page.menu.currentNode = syncArgs.node; + }); + } + + function formatDates(relations) { + if(relations) { + userService.getCurrentUser().then(function (currentUser) { + angular.forEach(relations, function (relation) { + relation.timestampFormatted = dateHelper.getLocalDate(relation.createDate, currentUser.locale, 'LLL'); + }); + }); + } + } + + function getRelationNames(relationType) { + if(relationType.relations) { + angular.forEach(relationType.relations, function(relation){ + entityResource.getById(relation.parentId, relationType.parentObjectTypeName).then(function(entity) { + relation.parentName = entity.name; + }); + entityResource.getById(relation.childId, relationType.childObjectTypeName).then(function(entity) { + relation.childName = entity.name; + }); + }); + } + } + + function saveRelationType() { + vm.page.saveButtonState = "busy"; + + if (formHelper.submitForm({ scope: $scope, statusMessage: "Saving..." })) { + relationTypeResource.save(vm.relationType).then(function (data) { + formHelper.resetForm({ scope: $scope, notifications: data.notifications }); + bindRelationType(data); + vm.page.saveButtonState = "success"; + }, function (error) { + contentEditingHelper.handleSaveError({ + redirectOnFailure: false, + err: error + }); + + notificationsService.error(error.data.message); + vm.page.saveButtonState = "error"; + }); + } + } +} + +angular.module("umbraco").controller("Umbraco.Editors.RelationTypes.EditController", RelationTypeEditController); diff --git a/src/Umbraco.Web.UI.Client/src/views/relationtypes/edit.html b/src/Umbraco.Web.UI.Client/src/views/relationtypes/edit.html new file mode 100644 index 0000000000..2c86161bda --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/relationtypes/edit.html @@ -0,0 +1,33 @@ +
+ + +
+ + + + + + + + + + + + + + + + +
+
diff --git a/src/Umbraco.Web.UI.Client/src/views/relationtypes/views/relationType.html b/src/Umbraco.Web.UI.Client/src/views/relationtypes/views/relationType.html new file mode 100644 index 0000000000..7f31461e69 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/relationtypes/views/relationType.html @@ -0,0 +1,40 @@ + + + + +
{{model.relationType.id}}
+ {{model.relationType.key}} +
+ + + +
    +
  • + +
  • +
  • + +
  • +
+
+ + + +
{{model.relationType.parentObjectTypeName}}
+
+ + + +
{{model.relationType.childObjectTypeName}}
+
+ + + +
{{model.relationType.relations.length}}
+
+
+
diff --git a/src/Umbraco.Web.UI.Client/src/views/relationtypes/views/relations.html b/src/Umbraco.Web.UI.Client/src/views/relationtypes/views/relations.html new file mode 100644 index 0000000000..ba8d9c00da --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/relationtypes/views/relations.html @@ -0,0 +1,23 @@ + + + + +
+ + + + + + + + + + + + + +
ParentChildCreatedComment
{{relation.parentName}}{{relation.childName}}{{relation.timestampFormatted}}{{relation.comment}}
+
+
+
+
diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/da.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/da.xml index f30b4aa7bc..fef0d325ca 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/da.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/da.xml @@ -872,9 +872,9 @@ Mange hilsner fra Umbraco robotten Indsæt, men fjern formattering som ikke bør være på en webside (Anbefales) - Rollebaseret beskyttelse - Hvis du ønsker at kontrollere adgang til siden ved hjælp af rollebaseret godkendelse via Umbracos medlemsgrupper. - Du skal oprette en medlemsgruppe før du kan bruge rollebaseret godkendelse + Gruppebaseret beskyttelse + Hvis du ønsker at give adgang til alle medlemmer af specifikke medlemsgrupper + Du skal oprette en medlemsgruppe før du kan bruge gruppebaseret beskyttelse Fejlside Brugt når folk er logget ind, men ingen adgang %0% skal beskyttes]]> @@ -885,10 +885,10 @@ Mange hilsner fra Umbraco robotten Fjern beskyttelse... %0%?]]> Vælg siderne der indeholder log ind-formularer og fejlmeddelelser - %0%]]> - %0%]]> - Enkel brugerbeskyttelse - Hvis du blot ønsker at opsætte simpel beskyttelse ved hjælp af et enkelt login og kodeord + %0%]]> + %0%]]> + Adgang til enkelte medlemmer + Hvis du ønsker at give adgang til enkelte medlemmer Udgivelsen kunne ikke udgives da publiceringsdato er sat diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml index 49111e9eab..7a353d65d2 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml @@ -1124,9 +1124,9 @@ To manage your website, simply open the Umbraco back office and start adding con Paste, but remove formatting (Recommended) - Role based protection - If you wish to control access to the page using role-based authentication, using Umbraco's member groups - You need to create a membergroup before you can use role-based authentication + Group based protection + If you want to grant access to all members of specific member groups + You need to create a member group before you can use group based authentication Error Page Used when people are logged on, but do not have access %0%]]> @@ -1137,10 +1137,10 @@ To manage your website, simply open the Umbraco back office and start adding con Remove protection... %0%?]]> Select the pages that contain login form and error messages - %0%]]> - %0%]]> - Single user protection - If you just want to setup simple protection using a single login and password + %0%]]> + %0%]]> + Specific members protection + If you wish to grant access to specific members Inherit tabs and properties from an existing document type. New tabs will be added to the current document type or merged if a tab with an identical name exists. This content type is used in a composition, and therefore cannot be composed itself. There are no content types available to use as a composition. - Available editors - Reuse + Create new + Use existing Editor settings Configuration Yes, delete @@ -1966,4 +1966,18 @@ To manage your website, simply open the Umbraco back office and start adding con There is no 'restore' relation found for this node. Use the Move menu item to move it manually. The item you want to restore it under ('%0%') is in the recycle bin. Use the Move menu item to move the item manually. + + Direction + Parent to child + Bidirectional + Parent + Child + Count + Relations + Created + Comment + Name + Relation Type + Relations + diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml index 37ea9f69f6..3380e8f92e 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml @@ -1146,9 +1146,9 @@ To manage your website, simply open the Umbraco back office and start adding con Paste, but remove formatting (Recommended) - Role based protection - If you wish to control access to the page using role-based authentication, using Umbraco's member groups - You need to create a membergroup before you can use role-based authentication + Group based protection + If you want to grant access to all members of specific member groups + You need to create a member group before you can use group based authentication Error Page Used when people are logged on, but do not have access %0%]]> @@ -1159,10 +1159,10 @@ To manage your website, simply open the Umbraco back office and start adding con Remove protection... %0%?]]> Select the pages that contain login form and error messages - %0%]]> - %0%]]> - Single user protection - If you just want to setup simple protection using a single login and password + %0%]]> + %0%]]> + Specific members protection + If you wish to grant access to specific members Insufficient user permissions to publish all descendant documents @@ -1511,8 +1511,8 @@ To manage your website, simply open the Umbraco back office and start adding con Inherit tabs and properties from an existing document type. New tabs will be added to the current document type or merged if a tab with an identical name exists. This content type is used in a composition, and therefore cannot be composed itself. There are no content types available to use as a composition. - Available editors - Reuse + Create new + Use existing Editor settings Configuration Yes, delete @@ -2021,4 +2021,18 @@ To manage your website, simply open the Umbraco back office and start adding con Select your notifications for Notification settings saved for + + Direction + Parent to child + Bidirectional + Parent + Child + Count + Relations + Created + Comment + Name + Relation Type + Relations + diff --git a/src/Umbraco.Web.UI/config/trees.Release.config b/src/Umbraco.Web.UI/config/trees.Release.config index 7b5a9e5e2a..bd75e97c38 100644 --- a/src/Umbraco.Web.UI/config/trees.Release.config +++ b/src/Umbraco.Web.UI/config/trees.Release.config @@ -30,9 +30,5 @@ - - - - - + diff --git a/src/Umbraco.Web.UI/config/trees.config b/src/Umbraco.Web.UI/config/trees.config index 2b99f8751f..7f7aeca8a7 100644 --- a/src/Umbraco.Web.UI/config/trees.config +++ b/src/Umbraco.Web.UI/config/trees.config @@ -30,9 +30,5 @@ - - - - - + diff --git a/src/Umbraco.Web/Editors/BackOfficeServerVariables.cs b/src/Umbraco.Web/Editors/BackOfficeServerVariables.cs index 23d8e2cc4e..b6448f8b74 100644 --- a/src/Umbraco.Web/Editors/BackOfficeServerVariables.cs +++ b/src/Umbraco.Web/Editors/BackOfficeServerVariables.cs @@ -299,6 +299,10 @@ namespace Umbraco.Web.Editors { "languageApiBaseUrl", _urlHelper.GetUmbracoApiServiceBaseUrl( controller => controller.GetAllLanguages()) + }, + { + "relationTypeApiBaseUrl", _urlHelper.GetUmbracoApiServiceBaseUrl( + controller => controller.GetById(1)) } } }, diff --git a/src/Umbraco.Web/Editors/ContentController.cs b/src/Umbraco.Web/Editors/ContentController.cs index b7a33c3ab8..d1cb15e76d 100644 --- a/src/Umbraco.Web/Editors/ContentController.cs +++ b/src/Umbraco.Web/Editors/ContentController.cs @@ -8,6 +8,7 @@ using System.Text; using System.Web.Http; using System.Web.Http.Controllers; using System.Web.Http.ModelBinding; +using System.Web.Security; using AutoMapper; using Umbraco.Core; using Umbraco.Core.Logging; @@ -2173,18 +2174,45 @@ namespace Umbraco.Web.Editors // unwrap the current public access setup for the client // - this API method is the single point of entry for both "modes" of public access (single user and role based) - var userName = entry.Rules - .FirstOrDefault(rule => rule.RuleType == Constants.Conventions.PublicAccess.MemberUsernameRuleType) - ?.RuleValue; - var roles = entry.Rules + var usernames = entry.Rules + .Where(rule => rule.RuleType == Constants.Conventions.PublicAccess.MemberUsernameRuleType) + .Select(rule => rule.RuleValue).ToArray(); + + MemberDisplay[] members; + switch (Services.MemberService.GetMembershipScenario()) + { + case MembershipScenario.NativeUmbraco: + members = usernames + .Select(username => Services.MemberService.GetByUsername(username)) + .Where(member => member != null) + .Select(Mapper.Map) + .ToArray(); + break; + // TODO: test support custom membership providers + case MembershipScenario.CustomProviderWithUmbracoLink: + case MembershipScenario.StandaloneCustomProvider: + default: + var provider = Core.Security.MembershipProviderExtensions.GetMembersMembershipProvider(); + members = usernames + .Select(username => provider.GetUser(username, false)) + .Where(membershipUser => membershipUser != null) + .Select(Mapper.Map) + .ToArray(); + break; + } + + var allGroups = Services.MemberGroupService.GetAll().ToArray(); + var groups = entry.Rules .Where(rule => rule.RuleType == Constants.Conventions.PublicAccess.MemberRoleRuleType) - .Select(rule => rule.RuleValue) + .Select(rule => allGroups.FirstOrDefault(g => g.Name == rule.RuleValue)) + .Where(memberGroup => memberGroup != null) + .Select(Mapper.Map) .ToArray(); return Request.CreateResponse(HttpStatusCode.OK, new PublicAccess { - UserName = userName, - Roles = roles, + Members = members, + Groups = groups, LoginPage = loginPageEntity != null ? Mapper.Map(loginPageEntity) : null, ErrorPage = errorPageEntity != null ? Mapper.Map(errorPageEntity) : null }); @@ -2193,9 +2221,9 @@ namespace Umbraco.Web.Editors // set up public access using role based access [EnsureUserPermissionForContent("contentId", ActionProtect.ActionLetter)] [HttpPost] - public HttpResponseMessage PostPublicAccess(int contentId, [FromUri]string[] roles, int loginPageId, int errorPageId) + public HttpResponseMessage PostPublicAccess(int contentId, [FromUri]string[] groups, [FromUri]string[] usernames, int loginPageId, int errorPageId) { - if (roles == null || roles.Any() == false) + if ((groups == null || groups.Any() == false) && (usernames == null || usernames.Any() == false)) { throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.BadRequest)); } @@ -2208,14 +2236,23 @@ namespace Umbraco.Web.Editors throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.BadRequest)); } + var isGroupBased = groups != null && groups.Any(); + var candidateRuleValues = isGroupBased + ? groups + : usernames; + var newRuleType = isGroupBased + ? Constants.Conventions.PublicAccess.MemberRoleRuleType + : Constants.Conventions.PublicAccess.MemberUsernameRuleType; + var entry = Services.PublicAccessService.GetEntryForContent(content); if (entry == null) { entry = new PublicAccessEntry(content, loginPage, errorPage, new List()); - foreach (var role in roles) + + foreach (var ruleValue in candidateRuleValues) { - entry.AddRule(role, Constants.Conventions.PublicAccess.MemberRoleRuleType); + entry.AddRule(ruleValue, newRuleType); } } else @@ -2225,12 +2262,12 @@ namespace Umbraco.Web.Editors var currentRules = entry.Rules.ToArray(); var obsoleteRules = currentRules.Where(rule => - rule.RuleType != Constants.Conventions.PublicAccess.MemberRoleRuleType - || roles.Contains(rule.RuleValue) == false + rule.RuleType != newRuleType + || candidateRuleValues.Contains(rule.RuleValue) == false ); - var newRoles = roles.Where(group => + var newRuleValues = candidateRuleValues.Where(group => currentRules.Any(rule => - rule.RuleType == Constants.Conventions.PublicAccess.MemberRoleRuleType + rule.RuleType == newRuleType && rule.RuleValue == group ) == false ); @@ -2238,9 +2275,9 @@ namespace Umbraco.Web.Editors { entry.RemoveRule(rule); } - foreach (var role in newRoles) + foreach (var ruleValue in newRuleValues) { - entry.AddRule(role, Constants.Conventions.PublicAccess.MemberRoleRuleType); + entry.AddRule(ruleValue, newRuleType); } } @@ -2249,30 +2286,6 @@ namespace Umbraco.Web.Editors : Request.CreateResponse(HttpStatusCode.InternalServerError); } - // set up public access using username and password - [EnsureUserPermissionForContent("contentId", ActionProtect.ActionLetter)] - [HttpPost] - public HttpResponseMessage PostPublicAccess(int contentId, string userName, string password, int loginPageId, int errorPageId) - { - // TODO KJAC: validate password regex - if (string.IsNullOrEmpty(userName) || string.IsNullOrEmpty(password)) - { - throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.BadRequest)); - } - - var content = Services.ContentService.GetById(contentId); - var loginPage = Services.ContentService.GetById(loginPageId); - var errorPage = Services.ContentService.GetById(errorPageId); - if (content == null || loginPage == null || errorPage == null) - { - throw new HttpResponseException(Request.CreateResponse(HttpStatusCode.BadRequest)); - } - - // TODO KJAC: implement - - return Request.CreateResponse(HttpStatusCode.OK); - } - [EnsureUserPermissionForContent("contentId", ActionProtect.ActionLetter)] [HttpPost] public HttpResponseMessage RemovePublicAccess(int contentId) diff --git a/src/Umbraco.Web/Editors/DashboardController.cs b/src/Umbraco.Web/Editors/DashboardController.cs index 72b7acc9e7..d8cbc34938 100644 --- a/src/Umbraco.Web/Editors/DashboardController.cs +++ b/src/Umbraco.Web/Editors/DashboardController.cs @@ -118,6 +118,7 @@ namespace Umbraco.Web.Editors } [ValidateAngularAntiForgeryToken] + [OutgoingEditorModelEvent] public IEnumerable> GetDashboard(string section) { return _dashboards.GetDashboards(section, Security.CurrentUser); diff --git a/src/Umbraco.Web/Editors/EditorModelEventArgs.cs b/src/Umbraco.Web/Editors/EditorModelEventArgs.cs index 153a2d8786..daf262fce5 100644 --- a/src/Umbraco.Web/Editors/EditorModelEventArgs.cs +++ b/src/Umbraco.Web/Editors/EditorModelEventArgs.cs @@ -4,9 +4,13 @@ namespace Umbraco.Web.Editors { public sealed class EditorModelEventArgs : EditorModelEventArgs { + private readonly EditorModelEventArgs _baseArgs; + private T _model; + public EditorModelEventArgs(EditorModelEventArgs baseArgs) : base(baseArgs.Model, baseArgs.UmbracoContext) { + _baseArgs = baseArgs; Model = (T)baseArgs.Model; } @@ -16,7 +20,16 @@ namespace Umbraco.Web.Editors Model = model; } - public new T Model { get; private set; } + public new T Model + { + get => _model; + set + { + _model = value; + if (_baseArgs != null) + _baseArgs.Model = _model; + } + } } public class EditorModelEventArgs : EventArgs @@ -27,7 +40,7 @@ namespace Umbraco.Web.Editors UmbracoContext = umbracoContext; } - public object Model { get; private set; } - public UmbracoContext UmbracoContext { get; private set; } + public object Model { get; set; } + public UmbracoContext UmbracoContext { get; } } -} \ No newline at end of file +} diff --git a/src/Umbraco.Web/Editors/EditorModelEventManager.cs b/src/Umbraco.Web/Editors/EditorModelEventManager.cs index e2a248cb88..2225f5c577 100644 --- a/src/Umbraco.Web/Editors/EditorModelEventManager.cs +++ b/src/Umbraco.Web/Editors/EditorModelEventManager.cs @@ -1,4 +1,5 @@ -using System.Web.Http.Filters; +using System.Collections.Generic; +using System.Web.Http.Filters; using Umbraco.Core.Events; using Umbraco.Web.Models.ContentEditing; @@ -13,6 +14,13 @@ namespace Umbraco.Web.Editors public static event TypedEventHandler> SendingMediaModel; public static event TypedEventHandler> SendingMemberModel; public static event TypedEventHandler> SendingUserModel; + public static event TypedEventHandler>>> SendingDashboardModel; + + private static void OnSendingDashboardModel(HttpActionExecutedContext sender, EditorModelEventArgs>> e) + { + var handler = SendingDashboardModel; + handler?.Invoke(sender, e); + } private static void OnSendingUserModel(HttpActionExecutedContext sender, EditorModelEventArgs e) { @@ -56,6 +64,9 @@ namespace Umbraco.Web.Editors if (e.Model is UserDisplay) OnSendingUserModel(sender, new EditorModelEventArgs(e)); + + if (e.Model is IEnumerable>) + OnSendingDashboardModel(sender, new EditorModelEventArgs>>(e)); } } } diff --git a/src/Umbraco.Web/Editors/RelationController.cs b/src/Umbraco.Web/Editors/RelationController.cs index c287e8a429..430f1af690 100644 --- a/src/Umbraco.Web/Editors/RelationController.cs +++ b/src/Umbraco.Web/Editors/RelationController.cs @@ -6,40 +6,40 @@ using System.Web.Http; using AutoMapper; using Umbraco.Core; using Umbraco.Core.Models; +using Umbraco.Web.Models.ContentEditing; using Umbraco.Web.Mvc; using Umbraco.Web.WebApi.Filters; using Constants = Umbraco.Core.Constants; -using Relation = Umbraco.Web.Models.ContentEditing.Relation; namespace Umbraco.Web.Editors { [PluginController("UmbracoApi")] - [UmbracoApplicationAuthorizeAttribute(Constants.Applications.Content)] + [UmbracoApplicationAuthorize(Constants.Applications.Content)] public class RelationController : UmbracoAuthorizedJsonController { - public Relation GetById(int id) + public RelationDisplay GetById(int id) { - return Mapper.Map(Services.RelationService.GetById(id)); + return Mapper.Map(Services.RelationService.GetById(id)); } //[EnsureUserPermissionForContent("childId")] - public IEnumerable GetByChildId(int childId, string relationTypeAlias = "") + public IEnumerable GetByChildId(int childId, string relationTypeAlias = "") { var relations = Services.RelationService.GetByChildId(childId).ToArray(); if (relations.Any() == false) { - return Enumerable.Empty(); + return Enumerable.Empty(); } if (string.IsNullOrWhiteSpace(relationTypeAlias) == false) { return - Mapper.Map, IEnumerable>( + Mapper.Map, IEnumerable>( relations.Where(x => x.RelationType.Alias.InvariantEquals(relationTypeAlias))); } - return Mapper.Map, IEnumerable>(relations); + return Mapper.Map, IEnumerable>(relations); } [HttpDelete] diff --git a/src/Umbraco.Web/Editors/RelationTypeController.cs b/src/Umbraco.Web/Editors/RelationTypeController.cs new file mode 100644 index 0000000000..ef3def1889 --- /dev/null +++ b/src/Umbraco.Web/Editors/RelationTypeController.cs @@ -0,0 +1,147 @@ +using System; +using System.Collections.Generic; +using System.Net; +using System.Net.Http; +using System.Web.Http; +using AutoMapper; +using Umbraco.Core; +using Umbraco.Core.Models; +using Umbraco.Web.Models.ContentEditing; +using Umbraco.Web.Mvc; +using Umbraco.Web.WebApi; +using Umbraco.Web.WebApi.Filters; +using Constants = Umbraco.Core.Constants; + +namespace Umbraco.Web.Editors +{ + /// + /// The API controller for editing relation types. + /// + [PluginController("UmbracoApi")] + [UmbracoTreeAuthorize(Constants.Trees.RelationTypes)] + [EnableOverrideAuthorization] + public class RelationTypeController : BackOfficeNotificationsController + { + /// + /// Gets a relation type by ID. + /// + /// The relation type ID. + /// Returns the . + public RelationTypeDisplay GetById(int id) + { + var relationType = Services.RelationService.GetRelationTypeById(id); + + if (relationType == null) + { + throw new HttpResponseException(HttpStatusCode.NotFound); + } + + var relations = Services.RelationService.GetByRelationTypeId(relationType.Id); + + var display = Mapper.Map(relationType); + display.Relations = Mapper.Map, IEnumerable>(relations); + + return display; + } + + /// + /// Gets a list of object types which can be associated via relations. + /// + /// A list of available object types. + public List GetRelationObjectTypes() + { + var objectTypes = new List + { + new ObjectType{Id = UmbracoObjectTypes.Document.GetGuid(), Name = UmbracoObjectTypes.Document.GetFriendlyName()}, + new ObjectType{Id = UmbracoObjectTypes.Media.GetGuid(), Name = UmbracoObjectTypes.Media.GetFriendlyName()}, + new ObjectType{Id = UmbracoObjectTypes.Member.GetGuid(), Name = UmbracoObjectTypes.Member.GetFriendlyName()}, + new ObjectType{Id = UmbracoObjectTypes.DocumentType.GetGuid(), Name = UmbracoObjectTypes.DocumentType.GetFriendlyName()}, + new ObjectType{Id = UmbracoObjectTypes.MediaType.GetGuid(), Name = UmbracoObjectTypes.MediaType.GetFriendlyName()}, + new ObjectType{Id = UmbracoObjectTypes.MemberType.GetGuid(), Name = UmbracoObjectTypes.MemberType.GetFriendlyName()}, + new ObjectType{Id = UmbracoObjectTypes.DataType.GetGuid(), Name = UmbracoObjectTypes.DataType.GetFriendlyName()}, + new ObjectType{Id = UmbracoObjectTypes.MemberGroup.GetGuid(), Name = UmbracoObjectTypes.MemberGroup.GetFriendlyName()}, + new ObjectType{Id = UmbracoObjectTypes.Stylesheet.GetGuid(), Name = UmbracoObjectTypes.Stylesheet.GetFriendlyName()}, + new ObjectType{Id = UmbracoObjectTypes.ROOT.GetGuid(), Name = UmbracoObjectTypes.ROOT.GetFriendlyName()}, + new ObjectType{Id = UmbracoObjectTypes.RecycleBin.GetGuid(), Name = UmbracoObjectTypes.RecycleBin.GetFriendlyName()}, + }; + + return objectTypes; + } + + /// + /// Creates a new relation type. + /// + /// The relation type to create. + /// A containing the persisted relation type's ID. + public HttpResponseMessage PostCreate(RelationTypeSave relationType) + { + var relationTypePersisted = new RelationType(relationType.ChildObjectType, relationType.ParentObjectType, relationType.Name.ToSafeAlias(true)) + { + Name = relationType.Name, + IsBidirectional = relationType.IsBidirectional + }; + + try + { + Services.RelationService.Save(relationTypePersisted); + + return Request.CreateResponse(HttpStatusCode.OK, relationTypePersisted.Id); + } + catch (Exception ex) + { + Logger.Error(GetType(), ex, "Error creating relation type with {Name}", relationType.Name); + return Request.CreateNotificationValidationErrorResponse("Error creating relation type."); + } + } + + /// + /// Updates an existing relation type. + /// + /// The relation type to update. + /// A display object containing the updated relation type. + public RelationTypeDisplay PostSave(RelationTypeSave relationType) + { + var relationTypePersisted = Services.RelationService.GetRelationTypeById(relationType.Key); + + if (relationTypePersisted == null) + { + throw new HttpResponseException(Request.CreateNotificationValidationErrorResponse("Relation type does not exist")); + } + + Mapper.Map(relationType, relationTypePersisted); + + try + { + Services.RelationService.Save(relationTypePersisted); + var display = Mapper.Map(relationTypePersisted); + display.AddSuccessNotification("Relation type saved", ""); + + return display; + } + catch (Exception ex) + { + Logger.Error(GetType(), ex, "Error saving relation type with {Id}", relationType.Id); + throw new HttpResponseException(Request.CreateNotificationValidationErrorResponse("Something went wrong when saving the relation type")); + } + } + + /// + /// Deletes a relation type with a given ID. + /// + /// The ID of the relation type to delete. + /// A . + [HttpPost] + [HttpDelete] + public HttpResponseMessage DeleteById(int id) + { + var relationType = Services.RelationService.GetRelationTypeById(id); + + if(relationType == null) + throw new HttpResponseException(HttpStatusCode.NotFound); + + Services.RelationService.Delete(relationType); + + return Request.CreateResponse(HttpStatusCode.OK); + } + } +} diff --git a/src/Umbraco.Web/Models/ContentEditing/ObjectType.cs b/src/Umbraco.Web/Models/ContentEditing/ObjectType.cs new file mode 100644 index 0000000000..522b0c666b --- /dev/null +++ b/src/Umbraco.Web/Models/ContentEditing/ObjectType.cs @@ -0,0 +1,15 @@ +using System; +using System.Runtime.Serialization; + +namespace Umbraco.Web.Models.ContentEditing +{ + [DataContract(Name = "objectType", Namespace = "")] + public class ObjectType + { + [DataMember(Name = "name")] + public string Name { get; set; } + + [DataMember(Name = "id")] + public Guid Id { get; set; } + } +} diff --git a/src/Umbraco.Web/Models/ContentEditing/PublicAccess.cs b/src/Umbraco.Web/Models/ContentEditing/PublicAccess.cs index 68236d5934..dcf2dcae92 100644 --- a/src/Umbraco.Web/Models/ContentEditing/PublicAccess.cs +++ b/src/Umbraco.Web/Models/ContentEditing/PublicAccess.cs @@ -5,16 +5,16 @@ namespace Umbraco.Web.Models.ContentEditing [DataContract(Name = "publicAccess", Namespace = "")] public class PublicAccess { - [DataMember(Name = "userName")] - public string UserName { get; set; } - - [DataMember(Name = "roles")] - public string[] Roles { get; set; } + [DataMember(Name = "groups")] + public MemberGroupDisplay[] Groups { get; set; } [DataMember(Name = "loginPage")] public EntityBasic LoginPage { get; set; } [DataMember(Name = "errorPage")] public EntityBasic ErrorPage { get; set; } + + [DataMember(Name = "members")] + public MemberDisplay[] Members { get; set; } } } diff --git a/src/Umbraco.Web/Models/ContentEditing/Relation.cs b/src/Umbraco.Web/Models/ContentEditing/Relation.cs deleted file mode 100644 index b166c67f55..0000000000 --- a/src/Umbraco.Web/Models/ContentEditing/Relation.cs +++ /dev/null @@ -1,43 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using System.Runtime.Serialization; -using System.Text; -using System.Threading.Tasks; - -namespace Umbraco.Web.Models.ContentEditing -{ - [DataContract(Name = "relation", Namespace = "")] - public class Relation - { - - public Relation() - { - RelationType = new RelationType(); - } - - /// - /// Gets or sets the Parent Id of the Relation (Source) - /// - [DataMember(Name = "parentId")] - public int ParentId { get; set; } - - /// - /// Gets or sets the Child Id of the Relation (Destination) - /// - [DataMember(Name = "childId")] - public int ChildId { get; set; } - - /// - /// Gets or sets the for the Relation - /// - [DataMember(Name = "relationType", IsRequired = true)] - public RelationType RelationType { get; set; } - - /// - /// Gets or sets a comment for the Relation - /// - [DataMember(Name = "comment")] - public string Comment { get; set; } - - } -} diff --git a/src/Umbraco.Web/Models/ContentEditing/RelationDisplay.cs b/src/Umbraco.Web/Models/ContentEditing/RelationDisplay.cs new file mode 100644 index 0000000000..24ebabc615 --- /dev/null +++ b/src/Umbraco.Web/Models/ContentEditing/RelationDisplay.cs @@ -0,0 +1,52 @@ +using System; +using System.ComponentModel; +using System.Runtime.Serialization; + +namespace Umbraco.Web.Models.ContentEditing +{ + [DataContract(Name = "relation", Namespace = "")] + public class RelationDisplay + { + /// + /// Gets or sets the Parent Id of the Relation (Source). + /// + [DataMember(Name = "parentId")] + [ReadOnly(true)] + public int ParentId { get; set; } + + /// + /// Gets or sets the Parent Name of the relation (Source). + /// + [DataMember(Name = "parentName")] + [ReadOnly(true)] + public string ParentName { get; set; } + + /// + /// Gets or sets the Child Id of the Relation (Destination). + /// + [DataMember(Name = "childId")] + [ReadOnly(true)] + public int ChildId { get; set; } + + /// + /// Gets or sets the Child Name of the relation (Destination). + /// + [DataMember(Name = "childName")] + [ReadOnly(true)] + public string ChildName { get; set; } + + /// + /// Gets or sets the date when the Relation was created. + /// + [DataMember(Name = "createDate")] + [ReadOnly(true)] + public DateTime CreateDate { get; set; } + + /// + /// Gets or sets a comment for the Relation. + /// + [DataMember(Name = "comment")] + [ReadOnly(true)] + public string Comment { get; set; } + } +} diff --git a/src/Umbraco.Web/Models/ContentEditing/RelationType.cs b/src/Umbraco.Web/Models/ContentEditing/RelationType.cs deleted file mode 100644 index 129376da5c..0000000000 --- a/src/Umbraco.Web/Models/ContentEditing/RelationType.cs +++ /dev/null @@ -1,42 +0,0 @@ -using System; -using System.Runtime.Serialization; - -namespace Umbraco.Web.Models.ContentEditing -{ - [DataContract(Name = "relationType", Namespace = "")] - public class RelationType - { - - /// - /// Gets or sets the Name of the RelationType - /// - [DataMember(Name = "name", IsRequired = true)] - public string Name { get; set; } - - /// - /// Gets or sets the Alias of the RelationType - /// - [DataMember(Name = "alias", IsRequired = true)] - public string Alias { get; set; } - - /// - /// Gets or sets a boolean indicating whether the RelationType is Bidirectional (true) or Parent to Child (false) - /// - [DataMember(Name = "isBidirectional", IsRequired = true)] - public bool IsBidirectional { get; set; } - - /// - /// Gets or sets the Parents object type id - /// - /// Corresponds to the NodeObjectType in the umbracoNode table - [DataMember(Name = "parentObjectType", IsRequired = true)] - public Guid ParentObjectType { get; set; } - - /// - /// Gets or sets the Childs object type id - /// - /// Corresponds to the NodeObjectType in the umbracoNode table - [DataMember(Name = "childObjectType", IsRequired = true)] - public Guid ChildObjectType { get; set; } - } -} diff --git a/src/Umbraco.Web/Models/ContentEditing/RelationTypeDisplay.cs b/src/Umbraco.Web/Models/ContentEditing/RelationTypeDisplay.cs new file mode 100644 index 0000000000..c443175260 --- /dev/null +++ b/src/Umbraco.Web/Models/ContentEditing/RelationTypeDisplay.cs @@ -0,0 +1,63 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Runtime.Serialization; + +namespace Umbraco.Web.Models.ContentEditing +{ + [DataContract(Name = "relationType", Namespace = "")] + public class RelationTypeDisplay : EntityBasic, INotificationModel + { + public RelationTypeDisplay() + { + Notifications = new List(); + } + + /// + /// Gets or sets a boolean indicating whether the RelationType is Bidirectional (true) or Parent to Child (false) + /// + [DataMember(Name = "isBidirectional", IsRequired = true)] + public bool IsBidirectional { get; set; } + + /// + /// Gets or sets the Parents object type id + /// + /// Corresponds to the NodeObjectType in the umbracoNode table + [DataMember(Name = "parentObjectType", IsRequired = true)] + public Guid ParentObjectType { get; set; } + + /// + /// Gets or sets the Parent's object type name. + /// + [DataMember(Name = "parentObjectTypeName")] + [ReadOnly(true)] + public string ParentObjectTypeName { get; set; } + + /// + /// Gets or sets the Childs object type id + /// + /// Corresponds to the NodeObjectType in the umbracoNode table + [DataMember(Name = "childObjectType", IsRequired = true)] + public Guid ChildObjectType { get; set; } + + /// + /// Gets or sets the Child's object type name. + /// + [DataMember(Name = "childObjectTypeName")] + [ReadOnly(true)] + public string ChildObjectTypeName { get; set; } + + /// + /// Gets or sets the relations associated with this relation type. + /// + [DataMember(Name = "relations")] + [ReadOnly(true)] + public IEnumerable Relations { get; set; } + + /// + /// This is used to add custom localized messages/strings to the response for the app to use for localized UI purposes. + /// + [DataMember(Name = "notifications")] + public List Notifications { get; private set; } + } +} diff --git a/src/Umbraco.Web/Models/ContentEditing/RelationTypeSave.cs b/src/Umbraco.Web/Models/ContentEditing/RelationTypeSave.cs new file mode 100644 index 0000000000..e7e8d6d2ba --- /dev/null +++ b/src/Umbraco.Web/Models/ContentEditing/RelationTypeSave.cs @@ -0,0 +1,27 @@ +using System; +using System.Runtime.Serialization; + +namespace Umbraco.Web.Models.ContentEditing +{ + [DataContract(Name = "relationType", Namespace = "")] + public class RelationTypeSave : EntityBasic + { + /// + /// Gets or sets a boolean indicating whether the RelationType is Bidirectional (true) or Parent to Child (false) + /// + [DataMember(Name = "isBidirectional", IsRequired = true)] + public bool IsBidirectional { get; set; } + + /// + /// Gets or sets the parent object type ID. + /// + [DataMember(Name = "parentObjectType", IsRequired = false)] + public Guid ParentObjectType { get; set; } + + /// + /// Gets or sets the child object type ID. + /// + [DataMember(Name = "childObjectType", IsRequired = false)] + public Guid ChildObjectType { get; set; } + } +} diff --git a/src/Umbraco.Web/Models/Mapping/EntityMapperProfile.cs b/src/Umbraco.Web/Models/Mapping/EntityMapperProfile.cs index 3e42178fbd..ea76293df5 100644 --- a/src/Umbraco.Web/Models/Mapping/EntityMapperProfile.cs +++ b/src/Umbraco.Web/Models/Mapping/EntityMapperProfile.cs @@ -123,15 +123,15 @@ namespace Umbraco.Web.Models.Mapping .AfterMap((src, dest) => { //get the icon if there is one - dest.Icon = src.Values.ContainsKey(UmbracoExamineIndexer.IconFieldName) - ? src.Values[UmbracoExamineIndexer.IconFieldName] + dest.Icon = src.Values.ContainsKey(UmbracoExamineIndex.IconFieldName) + ? src.Values[UmbracoExamineIndex.IconFieldName] : "icon-document"; dest.Name = src.Values.ContainsKey("nodeName") ? src.Values["nodeName"] : "[no name]"; - if (src.Values.ContainsKey(UmbracoExamineIndexer.NodeKeyFieldName)) + if (src.Values.ContainsKey(UmbracoExamineIndex.NodeKeyFieldName)) { Guid key; - if (Guid.TryParse(src.Values[UmbracoExamineIndexer.NodeKeyFieldName], out key)) + if (Guid.TryParse(src.Values[UmbracoExamineIndex.NodeKeyFieldName], out key)) { dest.Key = key; @@ -166,7 +166,7 @@ namespace Umbraco.Web.Models.Mapping dest.ParentId = -1; } } - dest.Path = src.Values.ContainsKey(UmbracoExamineIndexer.IndexPathFieldName) ? src.Values[UmbracoExamineIndexer.IndexPathFieldName] : ""; + dest.Path = src.Values.ContainsKey(UmbracoExamineIndex.IndexPathFieldName) ? src.Values[UmbracoExamineIndex.IndexPathFieldName] : ""; if (src.Values.ContainsKey(LuceneIndex.ItemTypeFieldName)) { diff --git a/src/Umbraco.Web/Models/Mapping/RelationMapperProfile.cs b/src/Umbraco.Web/Models/Mapping/RelationMapperProfile.cs index 31acf4e5e1..e31b1877d3 100644 --- a/src/Umbraco.Web/Models/Mapping/RelationMapperProfile.cs +++ b/src/Umbraco.Web/Models/Mapping/RelationMapperProfile.cs @@ -1,7 +1,7 @@ using AutoMapper; +using Umbraco.Core; using Umbraco.Core.Models; -using Relation = Umbraco.Web.Models.ContentEditing.Relation; -using RelationType = Umbraco.Web.Models.ContentEditing.RelationType; +using Umbraco.Web.Models.ContentEditing; namespace Umbraco.Web.Models.Mapping { @@ -9,11 +9,39 @@ namespace Umbraco.Web.Models.Mapping { public RelationMapperProfile() { - //FROM IRelationType TO RelationType - CreateMap(); + // FROM IRelationType to RelationTypeDisplay + CreateMap() + .ForMember(dest => dest.Icon, opt => opt.Ignore()) + .ForMember(dest => dest.Trashed, opt => opt.Ignore()) + .ForMember(dest => dest.Alias, opt => opt.Ignore()) + .ForMember(dest => dest.Path, opt => opt.Ignore()) + .ForMember(dest => dest.AdditionalData, opt => opt.Ignore()) + .ForMember(dest => dest.ChildObjectTypeName, opt => opt.Ignore()) + .ForMember(dest => dest.ParentObjectTypeName, opt => opt.Ignore()) + .ForMember(dest => dest.Relations, opt => opt.Ignore()) + .ForMember(dest => dest.ParentId, opt => opt.Ignore()) + .ForMember(dest => dest.Notifications, opt => opt.Ignore()) + .ForMember(dest => dest.Udi, opt => opt.MapFrom(content => Udi.Create(Constants.UdiEntityType.RelationType, content.Key))) + .AfterMap((src, dest) => + { + // Build up the path + dest.Path = "-1," + src.Id; - //FROM IRelation TO Relation - CreateMap(); + // Set the "friendly" names for the parent and child object types + dest.ParentObjectTypeName = ObjectTypes.GetUmbracoObjectType(src.ParentObjectType).GetFriendlyName(); + dest.ChildObjectTypeName = ObjectTypes.GetUmbracoObjectType(src.ChildObjectType).GetFriendlyName(); + }); + + // FROM IRelation to RelationDisplay + CreateMap() + .ForMember(dest => dest.ParentName, opt => opt.Ignore()) + .ForMember(dest => dest.ChildName, opt => opt.Ignore()); + + // FROM RelationTypeSave to IRelationType + CreateMap() + .ForMember(dest => dest.CreateDate, opt => opt.Ignore()) + .ForMember(dest => dest.UpdateDate, opt => opt.Ignore()) + .ForMember(dest => dest.DeleteDate, opt => opt.Ignore()); } } } diff --git a/src/Umbraco.Web/Models/Trees/MenuItemList.cs b/src/Umbraco.Web/Models/Trees/MenuItemList.cs index b34f0b4444..70b35e25bd 100644 --- a/src/Umbraco.Web/Models/Trees/MenuItemList.cs +++ b/src/Umbraco.Web/Models/Trees/MenuItemList.cs @@ -44,10 +44,11 @@ namespace Umbraco.Web.Models.Trees /// /// /// The text to display for the menu item, will default to the IAction alias if not specified - public MenuItem Add(string name, bool hasSeparator = false) + /// Whether or not this action opens a dialog + public MenuItem Add(string name, bool hasSeparator = false, bool opensDialog = false) where T : IAction { - var item = CreateMenuItem(name, hasSeparator); + var item = CreateMenuItem(name, hasSeparator, opensDialog); if (item != null) { Add(item); @@ -62,11 +63,11 @@ namespace Umbraco.Web.Models.Trees /// /// /// The used to localize the action name based on it's alias - /// + /// Whether or not this action opens a dialog public MenuItem Add(ILocalizedTextService textService, bool hasSeparator = false, bool opensDialog = false) where T : IAction { - var item = CreateMenuItem(textService, hasSeparator); + var item = CreateMenuItem(textService, hasSeparator, opensDialog); if (item != null) { Add(item); @@ -75,14 +76,15 @@ namespace Umbraco.Web.Models.Trees return null; } - internal MenuItem CreateMenuItem(string name, bool hasSeparator = false) + internal MenuItem CreateMenuItem(string name, bool hasSeparator = false, bool opensDialog = false) where T : IAction { var item = Current.Actions.GetAction(); if (item == null) return null; var menuItem = new MenuItem(item, name) { - SeperatorBefore = hasSeparator + SeperatorBefore = hasSeparator, + OpensDialog = opensDialog }; return menuItem; diff --git a/src/Umbraco.Web/PropertyEditors/GridPropertyIndexValueFactory.cs b/src/Umbraco.Web/PropertyEditors/GridPropertyIndexValueFactory.cs index c8235d1d11..83279ae975 100644 --- a/src/Umbraco.Web/PropertyEditors/GridPropertyIndexValueFactory.cs +++ b/src/Umbraco.Web/PropertyEditors/GridPropertyIndexValueFactory.cs @@ -59,7 +59,7 @@ namespace Umbraco.Web.PropertyEditors if (sb.Length > 0) { //First save the raw value to a raw field - result.Add(new KeyValuePair>($"{UmbracoExamineIndexer.RawFieldPrefix}{property.Alias}", new[] { rawVal })); + result.Add(new KeyValuePair>($"{UmbracoExamineIndex.RawFieldPrefix}{property.Alias}", new[] { rawVal })); //index the property with the combined/cleaned value result.Add(new KeyValuePair>(property.Alias, new[] { sb.ToString() })); diff --git a/src/Umbraco.Web/PropertyEditors/RichTextPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/RichTextPropertyEditor.cs index 2deb8b8444..31b65c6357 100644 --- a/src/Umbraco.Web/PropertyEditors/RichTextPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/RichTextPropertyEditor.cs @@ -104,7 +104,7 @@ namespace Umbraco.Web.PropertyEditors //index the stripped html values yield return new KeyValuePair>(property.Alias, new object[] { strVal.StripHtml() }); //store the raw value - yield return new KeyValuePair>($"{UmbracoExamineIndexer.RawFieldPrefix}{property.Alias}", new object[] { strVal }); + yield return new KeyValuePair>($"{UmbracoExamineIndex.RawFieldPrefix}{property.Alias}", new object[] { strVal }); } } } diff --git a/src/Umbraco.Web/PublishedCache/XmlPublishedCache/PublishedMediaCache.cs b/src/Umbraco.Web/PublishedCache/XmlPublishedCache/PublishedMediaCache.cs index d681d9296e..ac6b425e27 100644 --- a/src/Umbraco.Web/PublishedCache/XmlPublishedCache/PublishedMediaCache.cs +++ b/src/Umbraco.Web/PublishedCache/XmlPublishedCache/PublishedMediaCache.cs @@ -108,7 +108,7 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache // +(+parentID:-1) +__IndexType:media var criteria = searchProvider.CreateCriteria("media"); - var filter = criteria.ParentId(-1).Not().Field(UmbracoExamineIndexer.IndexPathFieldName, "-1,-21,".MultipleCharacterWildcard()); + var filter = criteria.ParentId(-1).Not().Field(UmbracoExamineIndex.IndexPathFieldName, "-1,-21,".MultipleCharacterWildcard()); var result = searchProvider.Search(filter.Compile()); if (result != null) @@ -294,7 +294,7 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache // note that since the use of the wildcard, it automatically escapes it in Lucene. var criteria = searchProvider.CreateCriteria("media"); - var filter = criteria.Id(id.ToInvariantString()).Not().Field(UmbracoExamineIndexer.IndexPathFieldName, "-1,-21,".MultipleCharacterWildcard()); + var filter = criteria.Id(id.ToInvariantString()).Not().Field(UmbracoExamineIndex.IndexPathFieldName, "-1,-21,".MultipleCharacterWildcard()); var result = searchProvider.Search(filter.Compile()).FirstOrDefault(); if (result != null) return ConvertFromSearchResult(result); @@ -476,7 +476,7 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache { //We are going to check for a special field however, that is because in some cases we store a 'Raw' //value in the index such as for xml/html. - var rawValue = dd.Properties.FirstOrDefault(x => x.Alias.InvariantEquals(UmbracoExamineIndexer.RawFieldPrefix + alias)); + var rawValue = dd.Properties.FirstOrDefault(x => x.Alias.InvariantEquals(UmbracoExamineIndex.RawFieldPrefix + alias)); return rawValue ?? dd.Properties.FirstOrDefault(x => x.Alias.InvariantEquals(alias)); } @@ -509,7 +509,7 @@ namespace Umbraco.Web.PublishedCache.XmlPublishedCache //first check in Examine as this is WAY faster var criteria = searchProvider.CreateCriteria("media"); - var filter = criteria.ParentId(parentId).Not().Field(UmbracoExamineIndexer.IndexPathFieldName, "-1,-21,".MultipleCharacterWildcard()); + var filter = criteria.ParentId(parentId).Not().Field(UmbracoExamineIndex.IndexPathFieldName, "-1,-21,".MultipleCharacterWildcard()); //the above filter will create a query like this, NOTE: That since the use of the wildcard, it automatically escapes it in Lucene. //+(+parentId:3113 -__Path:-1,-21,*) +__IndexType:media diff --git a/src/Umbraco.Web/Routing/PublishedRouter.cs b/src/Umbraco.Web/Routing/PublishedRouter.cs index 06c23406ab..1122aaa11a 100644 --- a/src/Umbraco.Web/Routing/PublishedRouter.cs +++ b/src/Umbraco.Web/Routing/PublishedRouter.cs @@ -637,7 +637,7 @@ namespace Umbraco.Web.Routing if (loginPageId != request.PublishedContent.Id) request.PublishedContent = request.UmbracoContext.PublishedSnapshot.Content.GetById(loginPageId); } - else if (_services.PublicAccessService.HasAccess(request.PublishedContent.Id, _services.ContentService, GetRolesForLogin(membershipHelper.CurrentUserName)) == false) + else if (_services.PublicAccessService.HasAccess(request.PublishedContent.Id, _services.ContentService, membershipHelper.CurrentUserName, GetRolesForLogin(membershipHelper.CurrentUserName)) == false) { _logger.Debug("EnsurePublishedContentAccess: Current member has not access, redirect to error page"); var errorPageId = publicAccessAttempt.Result.NoAccessNodeId; diff --git a/src/Umbraco.Web/Search/IUmbracoIndexesCreator.cs b/src/Umbraco.Web/Search/IUmbracoIndexesCreator.cs index 58014597d2..d654e4effd 100644 --- a/src/Umbraco.Web/Search/IUmbracoIndexesCreator.cs +++ b/src/Umbraco.Web/Search/IUmbracoIndexesCreator.cs @@ -1,13 +1,14 @@ using System.Collections.Generic; using Examine; +using Umbraco.Examine; namespace Umbraco.Web.Search { + /// /// /// Used to create the Umbraco indexes /// - public interface IUmbracoIndexesCreator + public interface IUmbracoIndexesCreator : IIndexCreator { - IEnumerable Create(); } } diff --git a/src/Umbraco.Web/Search/UmbracoIndexesCreator.cs b/src/Umbraco.Web/Search/UmbracoIndexesCreator.cs index 623bd5b75f..3723dedc48 100644 --- a/src/Umbraco.Web/Search/UmbracoIndexesCreator.cs +++ b/src/Umbraco.Web/Search/UmbracoIndexesCreator.cs @@ -21,7 +21,7 @@ namespace Umbraco.Web.Search /// /// Creates the indexes used by Umbraco /// - public class UmbracoIndexesCreator : IUmbracoIndexesCreator + public class UmbracoIndexesCreator : LuceneIndexCreator, IUmbracoIndexesCreator { //TODO: we should inject the different IValueSetValidator so devs can just register them instead of overriding this class? @@ -45,7 +45,7 @@ namespace Umbraco.Web.Search /// Creates the Umbraco indexes /// /// - public IEnumerable Create() + public override IEnumerable Create() { return new [] { @@ -57,11 +57,11 @@ namespace Umbraco.Web.Search private IIndex CreateInternalIndex() { - var index = new UmbracoContentIndexer( + var index = new UmbracoContentIndex( Constants.UmbracoIndexes.InternalIndexName, //fixme - how to deal with languages like in UmbracoContentIndexer.CreateFieldValueTypes - UmbracoExamineIndexer.UmbracoIndexFieldDefinitions, - GetFileSystemLuceneDirectory(Constants.UmbracoIndexes.InternalIndexPath), + UmbracoExamineIndex.UmbracoIndexFieldDefinitions, + CreateFileSystemLuceneDirectory(Constants.UmbracoIndexes.InternalIndexPath), new CultureInvariantWhitespaceAnalyzer(), ProfilingLogger, LanguageService, @@ -71,11 +71,11 @@ namespace Umbraco.Web.Search private IIndex CreateExternalIndex() { - var index = new UmbracoContentIndexer( + var index = new UmbracoContentIndex( Constants.UmbracoIndexes.ExternalIndexName, //fixme - how to deal with languages like in UmbracoContentIndexer.CreateFieldValueTypes - UmbracoExamineIndexer.UmbracoIndexFieldDefinitions, - GetFileSystemLuceneDirectory(Constants.UmbracoIndexes.ExternalIndexPath), + UmbracoExamineIndex.UmbracoIndexFieldDefinitions, + CreateFileSystemLuceneDirectory(Constants.UmbracoIndexes.ExternalIndexPath), new StandardAnalyzer(Lucene.Net.Util.Version.LUCENE_30), ProfilingLogger, LanguageService, @@ -85,31 +85,17 @@ namespace Umbraco.Web.Search private IIndex CreateMemberIndex() { - var index = new UmbracoMemberIndexer( + var index = new UmbracoMemberIndex( Constants.UmbracoIndexes.MembersIndexName, //fixme - how to deal with languages like in UmbracoContentIndexer.CreateFieldValueTypes - UmbracoExamineIndexer.UmbracoIndexFieldDefinitions, - GetFileSystemLuceneDirectory(Constants.UmbracoIndexes.MembersIndexPath), + UmbracoExamineIndex.UmbracoIndexFieldDefinitions, + CreateFileSystemLuceneDirectory(Constants.UmbracoIndexes.MembersIndexPath), new CultureInvariantWhitespaceAnalyzer(), ProfilingLogger, GetMemberValueSetValidator()); return index; } - - public virtual Lucene.Net.Store.Directory GetFileSystemLuceneDirectory(string name) - { - var dirInfo = new DirectoryInfo(Path.Combine(IOHelper.MapPath(SystemDirectories.Data), "TEMP", "ExamineIndexes", name)); - if (!dirInfo.Exists) - System.IO.Directory.CreateDirectory(dirInfo.FullName); - - var luceneDir = new SimpleFSDirectory(dirInfo); - //we want to tell examine to use a different fs lock instead of the default NativeFSFileLock which could cause problems if the appdomain - //terminates and in some rare cases would only allow unlocking of the file if IIS is forcefully terminated. Instead we'll rely on the simplefslock - //which simply checks the existence of the lock file - luceneDir.SetLockFactory(new NoPrefixSimpleFsLockFactory(dirInfo)); - return luceneDir; - } - + public virtual IContentValueSetValidator GetContentValueSetValidator() { return new ContentValueSetValidator(false, true, PublicAccessService); diff --git a/src/Umbraco.Web/Trees/ApplicationTreeController.cs b/src/Umbraco.Web/Trees/ApplicationTreeController.cs index c1192b6909..d824f32f4b 100644 --- a/src/Umbraco.Web/Trees/ApplicationTreeController.cs +++ b/src/Umbraco.Web/Trees/ApplicationTreeController.cs @@ -78,10 +78,20 @@ namespace Umbraco.Web.Trees } } - var multiTree = TreeRootNode.CreateMultiTreeRoot(collection); - multiTree.Name = Services.TextService.Localize("sections/" + application); + if(collection.Count > 0) + { + var multiTree = TreeRootNode.CreateMultiTreeRoot(collection); + multiTree.Name = Services.TextService.Localize("sections/" + application); - return multiTree; + return multiTree; + } + + //Otherwise its a application/section with no trees (aka a full screen app) + //For example we do not have a Forms tree definied in C# & can not attribute with [Tree(isSingleNodeTree:true0] + var rootId = Constants.System.Root.ToString(CultureInfo.InvariantCulture); + var section = Services.TextService.Localize("sections/" + application); + + return TreeRootNode.CreateSingleTreeRoot(rootId, null, null, section, TreeNodeCollection.Empty, true); } var rootNodeGroups = new List(); diff --git a/src/Umbraco.Web/Trees/RelationTypeTreeController.cs b/src/Umbraco.Web/Trees/RelationTypeTreeController.cs index 33ccc152c5..1ce319b6ac 100644 --- a/src/Umbraco.Web/Trees/RelationTypeTreeController.cs +++ b/src/Umbraco.Web/Trees/RelationTypeTreeController.cs @@ -1,13 +1,9 @@ -using System; -using System.Linq; +using System.Linq; using System.Net.Http.Formatting; using Umbraco.Web.Models.Trees; using Umbraco.Web.WebApi.Filters; using Umbraco.Core; - using Umbraco.Core.Services; -using Umbraco.Core.Models; -using Umbraco.Core.Models.Entities; using Umbraco.Web.Actions; namespace Umbraco.Web.Trees @@ -25,8 +21,8 @@ namespace Umbraco.Web.Trees if (id == Constants.System.Root.ToInvariantString()) { //Create the normal create action - var addMenuItem = menu.Items.Add(Services.TextService, opensDialog: true); - addMenuItem.LaunchDialogUrl("developer/RelationTypes/NewRelationType.aspx", "Create New RelationType"); + menu.Items.Add(Services.TextService.Localize("actions", ActionNew.ActionAlias)); + //refresh action menu.Items.Add(new RefreshNode(Services.TextService, true)); @@ -36,17 +32,7 @@ namespace Umbraco.Web.Trees var relationType = Services.RelationService.GetRelationTypeById(int.Parse(id)); if (relationType == null) return new MenuItemCollection(); - //add delete option for all macros - menu.Items.Add(Services.TextService, opensDialog: true) - //Since we haven't implemented anything for relationtypes in angular, this needs to be converted to - //use the legacy format - .ConvertLegacyMenuItem(new EntitySlim - { - Id = relationType.Id, - Level = 1, - ParentId = -1, - Name = relationType.Name - }, "relationTypes", queryStrings.GetValue("application")); + menu.Items.Add(Services.TextService.Localize("actions", ActionDelete.ActionAlias)); return menu; } @@ -57,18 +43,9 @@ namespace Umbraco.Web.Trees if (id == Constants.System.Root.ToInvariantString()) { - nodes.AddRange(Services.RelationService - .GetAllRelationTypes().Select(rt => CreateTreeNode( - rt.Id.ToString(), - id, - queryStrings, - rt.Name, - "icon-trafic", - false, - //TODO: Rebuild the macro editor in angular, then we dont need to have this at all (which is just a path to the legacy editor) - "/" + queryStrings.GetValue("application") + "/framed/" + - Uri.EscapeDataString("/umbraco/developer/RelationTypes/EditRelationType.aspx?id=" + rt.Id) - ))); + nodes.AddRange(Services.RelationService.GetAllRelationTypes() + .Select(rt => CreateTreeNode(rt.Id.ToString(), id, queryStrings, rt.Name, + "icon-trafic", false))); } return nodes; } diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index 79ee2dbbb5..4835b654e2 100755 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -112,6 +112,7 @@ + @@ -152,6 +153,10 @@ + + + + @@ -628,9 +633,7 @@ - - @@ -1237,13 +1240,6 @@ FeedProxy.aspx - - NewRelationType.aspx - ASPXCodeBehind - - - NewRelationType.aspx - insertMasterpageContent.aspx ASPXCodeBehind @@ -1315,9 +1311,6 @@ - - ASPXCodeBehind - ASPXCodeBehind diff --git a/src/Umbraco.Web/UmbracoHelper.cs b/src/Umbraco.Web/UmbracoHelper.cs index fbb739b5c2..3bfa433987 100644 --- a/src/Umbraco.Web/UmbracoHelper.cs +++ b/src/Umbraco.Web/UmbracoHelper.cs @@ -11,7 +11,6 @@ using Umbraco.Core.Models.PublishedContent; using Umbraco.Core.Services; using Umbraco.Core.Xml; using Umbraco.Web.Composing; -using Umbraco.Core.Cache; using Umbraco.Web.Routing; using Umbraco.Web.Security; @@ -28,7 +27,6 @@ namespace Umbraco.Web private readonly UmbracoContext _umbracoContext; private readonly IPublishedContent _currentPage; - private readonly IPublishedContentQuery _iQuery; private readonly ServiceContext _services; private IUmbracoComponentRenderer _componentRenderer; @@ -44,22 +42,18 @@ namespace Umbraco.Web /// /// For tests. internal UmbracoHelper(UmbracoContext umbracoContext, IPublishedContent content, - IPublishedContentQuery query, ITagQuery tagQuery, ICultureDictionary cultureDictionary, IUmbracoComponentRenderer componentRenderer, MembershipHelper membershipHelper, ServiceContext services) { - if (tagQuery == null) throw new ArgumentNullException(nameof(tagQuery)); - _umbracoContext = umbracoContext ?? throw new ArgumentNullException(nameof(umbracoContext)); - _tag = new TagQuery(tagQuery); + _tag = tagQuery ?? throw new ArgumentNullException(nameof(tagQuery)); _cultureDictionary = cultureDictionary ?? throw new ArgumentNullException(nameof(cultureDictionary)); _componentRenderer = componentRenderer ?? throw new ArgumentNullException(nameof(componentRenderer)); _membershipHelper = membershipHelper ?? throw new ArgumentNullException(nameof(membershipHelper)); _currentPage = content ?? throw new ArgumentNullException(nameof(content)); - _iQuery = query ?? throw new ArgumentNullException(nameof(query)); _services = services ?? throw new ArgumentNullException(nameof(services)); } @@ -105,15 +99,13 @@ namespace Umbraco.Web /// Gets the tag context. /// public ITagQuery TagQuery => _tag ?? - (_tag = new TagQuery(_services.TagService, _iQuery ?? ContentQuery)); + (_tag = new TagQuery(_services.TagService, ContentQuery)); /// /// Gets the query context. /// public IPublishedContentQuery ContentQuery => _query ?? - (_query = _iQuery != null - ? new PublishedContentQuery(_iQuery) - : new PublishedContentQuery(UmbracoContext.ContentCache, UmbracoContext.MediaCache)); + (_query = new PublishedContentQuery(UmbracoContext.ContentCache, UmbracoContext.MediaCache)); /// /// Gets the Umbraco context. diff --git a/src/Umbraco.Web/WebApi/Filters/OutgoingEditorModelEventAttribute.cs b/src/Umbraco.Web/WebApi/Filters/OutgoingEditorModelEventAttribute.cs index ec32b61bca..8410891a5d 100644 --- a/src/Umbraco.Web/WebApi/Filters/OutgoingEditorModelEventAttribute.cs +++ b/src/Umbraco.Web/WebApi/Filters/OutgoingEditorModelEventAttribute.cs @@ -19,16 +19,17 @@ namespace Umbraco.Web.WebApi.Filters var user = UmbracoContext.Current.Security.CurrentUser; if (user == null) return; - var objectContent = actionExecutedContext.Response.Content as ObjectContent; - if (objectContent != null) + if (actionExecutedContext.Response.Content is ObjectContent objectContent) { var model = objectContent.Value; if (model != null) { - EditorModelEventManager.EmitEvent(actionExecutedContext, new EditorModelEventArgs( - (dynamic)model, - UmbracoContext.Current)); + var args = new EditorModelEventArgs( + model, + UmbracoContext.Current); + EditorModelEventManager.EmitEvent(actionExecutedContext, args); + objectContent.Value = args.Model; } } diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/developer/RelationTypes/NewRelationType.aspx b/src/Umbraco.Web/umbraco.presentation/umbraco/developer/RelationTypes/NewRelationType.aspx deleted file mode 100644 index 5939549fa4..0000000000 --- a/src/Umbraco.Web/umbraco.presentation/umbraco/developer/RelationTypes/NewRelationType.aspx +++ /dev/null @@ -1,54 +0,0 @@ -<%@ Page Language="C#" AutoEventWireup="true" CodeBehind="NewRelationType.aspx.cs" Inherits="umbraco.cms.presentation.developer.RelationTypes.NewRelationType" MasterPageFile="../../masterpages/umbracoPage.Master"%> -<%@ Register TagPrefix="umb" Namespace="Umbraco.Web._Legacy.Controls" %> - - - - - - - - - - - - - - - - - - - - - - - - - - - - <% ///* */ %> - - - - - - - - - - - - - - - -
- - or - Cancel -
- - -
- diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/developer/RelationTypes/NewRelationType.aspx.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/developer/RelationTypes/NewRelationType.aspx.cs deleted file mode 100644 index cffe2157e3..0000000000 --- a/src/Umbraco.Web/umbraco.presentation/umbraco/developer/RelationTypes/NewRelationType.aspx.cs +++ /dev/null @@ -1,89 +0,0 @@ -using System; -using System.Web.UI.WebControls; -using Umbraco.Core; -using Umbraco.Web.UI.Pages; -using Umbraco.Core.Models; - -namespace umbraco.cms.presentation.developer.RelationTypes -{ - /// - /// Add a new Relation Type - /// - [WebformsPageTreeAuthorize(Constants.Trees.RelationTypes)] - public partial class NewRelationType : UmbracoEnsuredPage - { - /// - /// On Load event - /// - /// this aspx page - /// EventArgs (expect empty) - protected void Page_Load(object sender, EventArgs e) - { - if (!this.Page.IsPostBack) - { - this.Form.DefaultFocus = this.descriptionTextBox.ClientID; - } - - this.AppendUmbracoObjectTypes(this.parentDropDownList); - this.AppendUmbracoObjectTypes(this.childDropDownList); - } - - /// - /// Server side validation to ensure there are no existing relationshipTypes with the alias of - /// the relation type being added - /// - /// the aliasCustomValidator control - /// to set validation respose - protected void AliasCustomValidator_ServerValidate(object source, ServerValidateEventArgs args) - { - var relationService = Services.RelationService; - args.IsValid = relationService.GetRelationTypeByAlias(this.aliasTextBox.Text.Trim()) == null; - } - - /// - /// Add a new relation type into the database, and redirects to it's editing page. - /// - /// expects the addButton control - /// expects EventArgs for addButton - protected void AddButton_Click(object sender, EventArgs e) - { - if (Page.IsValid) - { - var newRelationTypeAlias = this.aliasTextBox.Text.Trim(); - - var relationService = Services.RelationService; - var relationType = new RelationType(new Guid(this.childDropDownList.SelectedValue), - new Guid(this.parentDropDownList.SelectedValue), newRelationTypeAlias, this.descriptionTextBox.Text) - { - IsBidirectional = this.dualRadioButtonList.SelectedValue == "1" - }; - - relationService.Save(relationType); - - var newRelationTypeId = relationService.GetRelationTypeByAlias(newRelationTypeAlias).Id; - - ClientTools.ChangeContentFrameUrl("developer/RelationTypes/EditRelationType.aspx?id=" + newRelationTypeId).CloseModalWindow().ChildNodeCreated(); - } - } - - /// - /// Adds the Umbraco Object types to a drop down list - /// - /// control for which to add the Umbraco object types - private void AppendUmbracoObjectTypes(ListControl dropDownList) - { - dropDownList.Items.Add(new ListItem(UmbracoObjectTypes.Document.GetFriendlyName(), Constants.ObjectTypes.Strings.Document)); - dropDownList.Items.Add(new ListItem(UmbracoObjectTypes.Media.GetFriendlyName(), Constants.ObjectTypes.Strings.Media)); - dropDownList.Items.Add(new ListItem(UmbracoObjectTypes.Member.GetFriendlyName(), Constants.ObjectTypes.Strings.Member)); - dropDownList.Items.Add(new ListItem(UmbracoObjectTypes.MediaType.GetFriendlyName(), Constants.ObjectTypes.Strings.MediaType)); - dropDownList.Items.Add(new ListItem(UmbracoObjectTypes.DocumentType.GetFriendlyName(), Constants.ObjectTypes.Strings.DocumentType)); - dropDownList.Items.Add(new ListItem(UmbracoObjectTypes.MemberType.GetFriendlyName(), Constants.ObjectTypes.Strings.MemberType)); - dropDownList.Items.Add(new ListItem(UmbracoObjectTypes.DataType.GetFriendlyName(), Constants.ObjectTypes.Strings.DataType)); - dropDownList.Items.Add(new ListItem(UmbracoObjectTypes.MemberGroup.GetFriendlyName(), Constants.ObjectTypes.Strings.MemberGroup)); - dropDownList.Items.Add(new ListItem(UmbracoObjectTypes.Stylesheet.GetFriendlyName(), Constants.ObjectTypes.Strings.Stylesheet)); - dropDownList.Items.Add(new ListItem(UmbracoObjectTypes.Template.GetFriendlyName(), Constants.ObjectTypes.Strings.Template)); - dropDownList.Items.Add(new ListItem(UmbracoObjectTypes.ROOT.GetFriendlyName(), Constants.ObjectTypes.Strings.SystemRoot)); - dropDownList.Items.Add(new ListItem(UmbracoObjectTypes.RecycleBin.GetFriendlyName(), Constants.ObjectTypes.Strings.ContentRecycleBin)); - } - } -} diff --git a/src/Umbraco.Web/umbraco.presentation/umbraco/developer/RelationTypes/NewRelationType.aspx.designer.cs b/src/Umbraco.Web/umbraco.presentation/umbraco/developer/RelationTypes/NewRelationType.aspx.designer.cs deleted file mode 100644 index 5f463c7ad8..0000000000 --- a/src/Umbraco.Web/umbraco.presentation/umbraco/developer/RelationTypes/NewRelationType.aspx.designer.cs +++ /dev/null @@ -1,168 +0,0 @@ -//------------------------------------------------------------------------------ -// -// This code was generated by a tool. -// -// Changes to this file may cause incorrect behavior and will be lost if -// the code is regenerated. -// -//------------------------------------------------------------------------------ - -namespace umbraco.cms.presentation.developer.RelationTypes { - - - public partial class NewRelationType { - - /// - /// nameAliasPane control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::Umbraco.Web._Legacy.Controls.Pane nameAliasPane; - - /// - /// nameProperyPanel control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::Umbraco.Web._Legacy.Controls.PropertyPanel nameProperyPanel; - - /// - /// descriptionTextBox control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::System.Web.UI.WebControls.TextBox descriptionTextBox; - - /// - /// descriptionRequiredFieldValidator control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::System.Web.UI.WebControls.RequiredFieldValidator descriptionRequiredFieldValidator; - - /// - /// aliasPropertyPanel control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::Umbraco.Web._Legacy.Controls.PropertyPanel aliasPropertyPanel; - - /// - /// aliasTextBox control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::System.Web.UI.WebControls.TextBox aliasTextBox; - - /// - /// aliasRequiredFieldValidator control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::System.Web.UI.WebControls.RequiredFieldValidator aliasRequiredFieldValidator; - - /// - /// aliasCustomValidator control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::System.Web.UI.WebControls.CustomValidator aliasCustomValidator; - - /// - /// directionPane control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::Umbraco.Web._Legacy.Controls.Pane directionPane; - - /// - /// PropertyPanel1 control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::Umbraco.Web._Legacy.Controls.PropertyPanel PropertyPanel1; - - /// - /// dualRadioButtonList control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::System.Web.UI.WebControls.RadioButtonList dualRadioButtonList; - - /// - /// objectTypePane control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::Umbraco.Web._Legacy.Controls.Pane objectTypePane; - - /// - /// PropertyPanel2 control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::Umbraco.Web._Legacy.Controls.PropertyPanel PropertyPanel2; - - /// - /// parentDropDownList control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::System.Web.UI.WebControls.DropDownList parentDropDownList; - - /// - /// PropertyPanel3 control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::Umbraco.Web._Legacy.Controls.PropertyPanel PropertyPanel3; - - /// - /// childDropDownList control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::System.Web.UI.WebControls.DropDownList childDropDownList; - - /// - /// addButton control. - /// - /// - /// Auto-generated field. - /// To modify move field declaration from designer file to code-behind file. - /// - protected global::System.Web.UI.WebControls.Button addButton; - } -}