diff --git a/src/Umbraco.Core/Configuration/UmbracoSettings/SecurityElement.cs b/src/Umbraco.Core/Configuration/UmbracoSettings/SecurityElement.cs index d3160a216b..33a8a8584b 100644 --- a/src/Umbraco.Core/Configuration/UmbracoSettings/SecurityElement.cs +++ b/src/Umbraco.Core/Configuration/UmbracoSettings/SecurityElement.cs @@ -12,7 +12,7 @@ namespace Umbraco.Core.Configuration.UmbracoSettings return new OptionalInnerTextConfigurationElement( (InnerTextConfigurationElement)this["keepUserLoggedIn"], //set the default - false); + true); } } diff --git a/src/Umbraco.Core/Models/UmbracoObjectTypes.cs b/src/Umbraco.Core/Models/UmbracoObjectTypes.cs index e04e20ff0a..c9967c0688 100644 --- a/src/Umbraco.Core/Models/UmbracoObjectTypes.cs +++ b/src/Umbraco.Core/Models/UmbracoObjectTypes.cs @@ -61,6 +61,7 @@ namespace Umbraco.Core.Models [FriendlyName("Member Group")] MemberGroup, + //TODO: What is a 'Content Item' supposed to be??? /// /// Content Item /// diff --git a/src/Umbraco.Core/Persistence/Factories/UmbracoEntityFactory.cs b/src/Umbraco.Core/Persistence/Factories/UmbracoEntityFactory.cs index ec3f48a0ca..db1a99bc45 100644 --- a/src/Umbraco.Core/Persistence/Factories/UmbracoEntityFactory.cs +++ b/src/Umbraco.Core/Persistence/Factories/UmbracoEntityFactory.cs @@ -1,15 +1,33 @@ using System; using System.Collections.Generic; using System.Globalization; +using System.Linq; +using System.Reflection; using Umbraco.Core.Models; +using Umbraco.Core.Models.EntityBase; using Umbraco.Core.Persistence.Repositories; namespace Umbraco.Core.Persistence.Factories { internal class UmbracoEntityFactory : IEntityFactory { + internal void AddAdditionalData(UmbracoEntity entity, IDictionary originalEntityProperties) + { + var entityProps = TypeHelper.GetPublicProperties(typeof(IUmbracoEntity)).Select(x => x.Name).ToArray(); + + //figure out what extra properties we have that are not on the IUmbracoEntity and add them to additional data + foreach (var k in originalEntityProperties.Keys + .Select(x => new { orig = x, title = x.ConvertCase(StringAliasCaseType.PascalCase) }) + .Where(x => entityProps.InvariantContains(x.title) == false)) + { + entity.AdditionalData[k.title] = originalEntityProperties[k.orig]; + } + } + internal UmbracoEntity BuildEntityFromDynamic(dynamic d) { + var asDictionary = (IDictionary)d; + var entity = new UmbracoEntity(d.trashed) { CreateDate = d.createDate, @@ -23,14 +41,12 @@ namespace Umbraco.Core.Persistence.Factories Path = d.path, SortOrder = d.sortOrder, HasChildren = d.children > 0, - ContentTypeAlias = d.alias ?? string.Empty, - ContentTypeIcon = d.icon ?? string.Empty, - ContentTypeThumbnail = d.thumbnail ?? string.Empty, + ContentTypeAlias = asDictionary.ContainsKey("alias") ? (d.alias ?? string.Empty) : string.Empty, + ContentTypeIcon = asDictionary.ContainsKey("icon") ? (d.icon ?? string.Empty) : string.Empty, + ContentTypeThumbnail = asDictionary.ContainsKey("thumbnail") ? (d.thumbnail ?? string.Empty) : string.Empty, UmbracoProperties = new List() }; - var asDictionary = (IDictionary)d; - var publishedVersion = default(Guid); //some content items don't have a published version if (asDictionary.ContainsKey("publishedVersion") && asDictionary["publishedVersion"] != null) @@ -38,12 +54,18 @@ namespace Umbraco.Core.Persistence.Factories Guid.TryParse(d.publishedVersion.ToString(), out publishedVersion); } var newestVersion = default(Guid); - Guid.TryParse(d.newestVersion.ToString(), out newestVersion); + if (asDictionary.ContainsKey("newestVersion") && d.newestVersion != null) + { + Guid.TryParse(d.newestVersion.ToString(), out newestVersion); + } entity.IsPublished = publishedVersion != default(Guid) || (newestVersion != default(Guid) && publishedVersion == newestVersion); entity.IsDraft = newestVersion != default(Guid) && (publishedVersion == default(Guid) || publishedVersion != newestVersion); entity.HasPendingChanges = (publishedVersion != default(Guid) && newestVersion != default(Guid)) && publishedVersion != newestVersion; - + + //Now we can assign the additional data! + AddAdditionalData(entity, asDictionary); + return entity; } diff --git a/src/Umbraco.Core/Persistence/Migrations/Syntax/Create/Index/CreateIndexBuilder.cs b/src/Umbraco.Core/Persistence/Migrations/Syntax/Create/Index/CreateIndexBuilder.cs index c91c1de469..4d1396ff79 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Syntax/Create/Index/CreateIndexBuilder.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Syntax/Create/Index/CreateIndexBuilder.cs @@ -57,6 +57,7 @@ namespace Umbraco.Core.Persistence.Migrations.Syntax.Create.Index public ICreateIndexOnColumnSyntax NonClustered() { + Expression.Index.IndexType = IndexTypes.NonClustered; Expression.Index.IsClustered = false; Expression.Index.IndexType = IndexTypes.NonClustered; Expression.Index.IsUnique = false; @@ -65,6 +66,7 @@ namespace Umbraco.Core.Persistence.Migrations.Syntax.Create.Index public ICreateIndexOnColumnSyntax Clustered() { + Expression.Index.IndexType = IndexTypes.Clustered; Expression.Index.IsClustered = true; //if it is clustered then we have to change the index type set the other flags Expression.Index.IndexType = IndexTypes.Clustered; @@ -75,6 +77,7 @@ namespace Umbraco.Core.Persistence.Migrations.Syntax.Create.Index ICreateIndexOnColumnSyntax ICreateIndexOptionsSyntax.Unique() { + Expression.Index.IndexType = IndexTypes.UniqueNonClustered; Expression.Index.IsUnique = true; return this; } diff --git a/src/Umbraco.Core/Persistence/Repositories/EntityRepository.cs b/src/Umbraco.Core/Persistence/Repositories/EntityRepository.cs index 0fd0fc4e76..7336e36ab7 100644 --- a/src/Umbraco.Core/Persistence/Repositories/EntityRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/EntityRepository.cs @@ -52,12 +52,12 @@ namespace Umbraco.Core.Persistence.Repositories public IUmbracoEntity GetByKey(Guid key) { var sql = GetBaseWhere(GetBase, false, false, key); - var nodeDto = _work.Database.FirstOrDefault(sql); + var nodeDto = _work.Database.FirstOrDefault(sql); if (nodeDto == null) return null; var factory = new UmbracoEntityFactory(); - var entity = factory.BuildEntity(nodeDto); + var entity = factory.BuildEntityFromDynamic(nodeDto); return entity; } @@ -67,12 +67,12 @@ namespace Umbraco.Core.Persistence.Repositories bool isContent = objectTypeId == new Guid(Constants.ObjectTypes.Document); bool isMedia = objectTypeId == new Guid(Constants.ObjectTypes.Media); var sql = GetBaseWhere(GetBase, isContent, isMedia, objectTypeId, key).Append(GetGroupBy(isContent, isMedia)); - var nodeDto = _work.Database.FirstOrDefault(sql); + var nodeDto = _work.Database.FirstOrDefault(sql); if (nodeDto == null) return null; var factory = new UmbracoEntityFactory(); - var entity = factory.BuildEntity(nodeDto); + var entity = factory.BuildEntityFromDynamic(nodeDto); return entity; } @@ -80,12 +80,12 @@ namespace Umbraco.Core.Persistence.Repositories public virtual IUmbracoEntity Get(int id) { var sql = GetBaseWhere(GetBase, false, false, id); - var nodeDto = _work.Database.FirstOrDefault(sql); + var nodeDto = _work.Database.FirstOrDefault(sql); if (nodeDto == null) return null; var factory = new UmbracoEntityFactory(); - var entity = factory.BuildEntity(nodeDto); + var entity = factory.BuildEntityFromDynamic(nodeDto); return entity; } @@ -95,12 +95,13 @@ namespace Umbraco.Core.Persistence.Repositories bool isContent = objectTypeId == new Guid(Constants.ObjectTypes.Document); bool isMedia = objectTypeId == new Guid(Constants.ObjectTypes.Media); var sql = GetBaseWhere(GetBase, isContent, isMedia, objectTypeId, id).Append(GetGroupBy(isContent, isMedia)); - var nodeDto = _work.Database.FirstOrDefault(sql); + + var nodeDto = _work.Database.FirstOrDefault(sql); if (nodeDto == null) return null; var factory = new UmbracoEntityFactory(); - var entity = factory.BuildEntity(nodeDto); + var entity = factory.BuildEntityFromDynamic(nodeDto); return entity; } @@ -119,16 +120,27 @@ namespace Umbraco.Core.Persistence.Repositories bool isContent = objectTypeId == new Guid(Constants.ObjectTypes.Document); bool isMedia = objectTypeId == new Guid(Constants.ObjectTypes.Media); var sql = GetBaseWhere(GetBase, isContent, isMedia, string.Empty, objectTypeId).Append(GetGroupBy(isContent, isMedia)); - var dtos = isMedia - ? _work.Database.Fetch( - new UmbracoEntityRelator().Map, sql) - : _work.Database.Fetch(sql); + var factory = new UmbracoEntityFactory(); - foreach (var dto in dtos) + if (isMedia) { - var entity = factory.BuildEntity(dto); - yield return entity; + //for now treat media differently + //TODO: We should really use this methodology for Content/Members too!! since it includes properties and ALL of the dynamic db fields + var entities = _work.Database.Fetch( + new UmbracoEntityRelator().Map, sql); + foreach (var entity in entities) + { + yield return entity; + } + } + else + { + var dtos = _work.Database.Fetch(sql); + foreach (var entity in dtos.Select(dto => factory.BuildEntityFromDynamic(dto))) + { + yield return entity; + } } } } @@ -140,10 +152,10 @@ namespace Umbraco.Core.Persistence.Repositories var translator = new SqlTranslator(sqlClause, query); var sql = translator.Translate().Append(GetGroupBy(false, false)); - var dtos = _work.Database.Fetch(sql); + var dtos = _work.Database.Fetch(sql); var factory = new UmbracoEntityFactory(); - var list = dtos.Select(factory.BuildEntity).Cast().ToList(); + var list = dtos.Select(factory.BuildEntityFromDynamic).Cast().ToList(); return list; } @@ -162,42 +174,22 @@ namespace Umbraco.Core.Persistence.Repositories if (isMedia) { - //treat media differently for now - var dtos = _work.Database.Fetch( + //treat media differently for now + //TODO: We should really use this methodology for Content/Members too!! since it includes properties and ALL of the dynamic db fields + var entities = _work.Database.Fetch( new UmbracoEntityRelator().Map, sql); - return dtos.Select(factory.BuildEntity).Cast().ToArray(); + return entities; } else { - //use dynamic so that we can get ALL properties from the SQL - //we'll have to stitch stuff together manually but we can get our - //additional data to put in the dictionary. + //use dynamic so that we can get ALL properties from the SQL so we can chuck that data into our AdditionalData var dtos = _work.Database.Fetch(sql); - var entityProps = TypeHelper.GetPublicProperties(typeof (IUmbracoEntity)).Select(x => x.Name).ToArray(); - var result = new List(); - foreach (var d in dtos) - { - //build the initial entity - IUmbracoEntity entity = factory.BuildEntityFromDynamic(d); - - //convert the dynamic row to dictionary - var asDictionary = (IDictionary) d; - - //figure out what extra properties we have that are not on the IUmbracoEntity and add them to additional data - foreach (var k in asDictionary.Keys - .Select(x => new {orig = x, title = x.ConvertCase(StringAliasCaseType.PascalCase)}) - .Where(x => entityProps.InvariantContains(x.title) == false)) - { - entity.AdditionalData[k.title] = asDictionary[k.orig]; - } - - result.Add(entity); - } - return result; + return dtos.Select(factory.BuildEntityFromDynamic).Cast().ToList(); } } #endregion + #region Sql Statements @@ -396,11 +388,20 @@ namespace Umbraco.Core.Persistence.Repositories public string UmbracoFile { get; set; } } + /// + /// This is a special relator in that it is not returning a DTO but a real resolved entity and that it accepts + /// a dynamic instance. + /// + /// + /// We're doing this because when we query the db, we want to use dynamic so that it returns all available fields not just the ones + /// defined on the entity so we can them to additional data + /// internal class UmbracoEntityRelator { - internal UmbracoEntityDto Current; + internal UmbracoEntity Current; + private readonly UmbracoEntityFactory _factory = new UmbracoEntityFactory(); - internal UmbracoEntityDto Map(UmbracoEntityDto a, UmbracoPropertyDto p) + internal UmbracoEntity Map(dynamic a, UmbracoPropertyDto p) { // Terminating call. Since we can return null from this function // we need to be ready for PetaPoco to callback later with null @@ -408,28 +409,44 @@ namespace Umbraco.Core.Persistence.Repositories if (a == null) return Current; - // Is this the same UmbracoEntityDto as the current one we're processing - if (Current != null && Current.UniqueId == a.UniqueId) + // Is this the same UmbracoEntity as the current one we're processing + if (Current != null && Current.Key == a.uniqueID) { - // Yes, just add this UmbracoPropertyDto to the current UmbracoEntityDto's collection - Current.UmbracoPropertyDtos.Add(p); - - // Return null to indicate we're not done with this UmbracoEntityDto yet + // Yes, just add this UmbracoProperty to the current UmbracoEntity's collection + if (Current.UmbracoProperties == null) + { + Current.UmbracoProperties = new List(); + } + Current.UmbracoProperties.Add(new UmbracoEntity.UmbracoProperty + { + PropertyEditorAlias = p.PropertyEditorAlias, + Value = p.UmbracoFile + }); + // Return null to indicate we're not done with this UmbracoEntity yet return null; } - // This is a different UmbracoEntityDto to the current one, or this is the + // This is a different UmbracoEntity to the current one, or this is the // first time through and we don't have a Tab yet // Save the current UmbracoEntityDto var prev = Current; - // Setup the new current UmbracoEntityDto - Current = a; - Current.UmbracoPropertyDtos = new List(); - Current.UmbracoPropertyDtos.Add(p); + // Setup the new current UmbracoEntity + + Current = _factory.BuildEntityFromDynamic(a); - // Return the now populated previous UmbracoEntityDto (or null if first time through) + //add the property/create the prop list if null + Current.UmbracoProperties = new List + { + new UmbracoEntity.UmbracoProperty + { + PropertyEditorAlias = p.PropertyEditorAlias, + Value = p.UmbracoFile + } + }; + + // Return the now populated previous UmbracoEntity (or null if first time through) return prev; } } diff --git a/src/Umbraco.Core/Services/ContentService.cs b/src/Umbraco.Core/Services/ContentService.cs index 93bd9a6408..2dd8287a7c 100644 --- a/src/Umbraco.Core/Services/ContentService.cs +++ b/src/Umbraco.Core/Services/ContentService.cs @@ -1170,14 +1170,11 @@ namespace Umbraco.Core.Services } } - //Special case for the Tags DataType - if (content.Properties.Any(x => x.PropertyType.PropertyEditorAlias == Constants.PropertyEditors.TagsAlias)) + //Special case for the associated tags + var tags = uow.Database.Fetch("WHERE nodeId = @Id", new { Id = content.Id }); + foreach (var tag in tags) { - var tags = uow.Database.Fetch("WHERE nodeId = @Id", new { Id = content.Id }); - foreach (var tag in tags) - { - uow.Database.Insert(new TagRelationshipDto { NodeId = copy.Id, TagId = tag.TagId }); - } + uow.Database.Insert(new TagRelationshipDto { NodeId = copy.Id, TagId = tag.TagId, PropertyTypeId = tag.PropertyTypeId }); } } diff --git a/src/Umbraco.Tests/Configurations/UmbracoSettings/PackageRepositoriesElementTests.cs b/src/Umbraco.Tests/Configurations/UmbracoSettings/PackageRepositoriesElementTests.cs index cb82978c72..fb2dcff4a6 100644 --- a/src/Umbraco.Tests/Configurations/UmbracoSettings/PackageRepositoriesElementTests.cs +++ b/src/Umbraco.Tests/Configurations/UmbracoSettings/PackageRepositoriesElementTests.cs @@ -15,13 +15,13 @@ namespace Umbraco.Tests.Configurations.UmbracoSettings Assert.IsTrue(SettingsSection.PackageRepositories.Repositories.ElementAt(0).Name == "Umbraco package Repository"); Assert.IsTrue(SettingsSection.PackageRepositories.Repositories.ElementAt(0).HasCustomWebServiceUrl == false); Assert.IsTrue(SettingsSection.PackageRepositories.Repositories.ElementAt(0).WebServiceUrl == "/umbraco/webservices/api/repository.asmx"); - Assert.IsTrue(SettingsSection.PackageRepositories.Repositories.ElementAt(0).RepositoryUrl == "http://packages.umbraco.org"); + Assert.IsTrue(SettingsSection.PackageRepositories.Repositories.ElementAt(0).RepositoryUrl == "http://our.umbraco.org"); Assert.IsTrue(SettingsSection.PackageRepositories.Repositories.ElementAt(1).Id == Guid.Parse("163245E0-CD22-44B6-841A-1B9B9D2E955F")); Assert.IsTrue(SettingsSection.PackageRepositories.Repositories.ElementAt(1).Name == "Test Repo"); Assert.IsTrue(SettingsSection.PackageRepositories.Repositories.ElementAt(1).HasCustomWebServiceUrl == false); Assert.IsTrue(SettingsSection.PackageRepositories.Repositories.ElementAt(0).WebServiceUrl == "/umbraco/webservices/api/repository.asmx"); - Assert.IsTrue(SettingsSection.PackageRepositories.Repositories.ElementAt(0).RepositoryUrl == "http://packages.umbraco.org"); + Assert.IsTrue(SettingsSection.PackageRepositories.Repositories.ElementAt(0).RepositoryUrl == "http://our.umbraco.org"); } } } \ No newline at end of file diff --git a/src/Umbraco.Tests/Models/Mapping/ContentWebModelMappingTests.cs b/src/Umbraco.Tests/Models/Mapping/ContentWebModelMappingTests.cs index 02849050bb..4d62dcf809 100644 --- a/src/Umbraco.Tests/Models/Mapping/ContentWebModelMappingTests.cs +++ b/src/Umbraco.Tests/Models/Mapping/ContentWebModelMappingTests.cs @@ -30,13 +30,13 @@ namespace Umbraco.Tests.Models.Mapping get { return DatabaseBehavior.NewSchemaPerFixture; } } - protected override void FreezeResolution() - { - //PropertyEditorResolver.Current = new PropertyEditorResolver( - // () => new List {typeof (TestPropertyEditor)}); + //protected override void FreezeResolution() + //{ + // PropertyEditorResolver.Current = new PropertyEditorResolver( + // () => PluginManager.Current.ResolvePropertyEditors()); - base.FreezeResolution(); - } + // base.FreezeResolution(); + //} [Test] public void To_Media_Item_Simple() diff --git a/src/Umbraco.Tests/Persistence/SyntaxProvider/SqlSyntaxProviderTests.cs b/src/Umbraco.Tests/Persistence/SyntaxProvider/SqlSyntaxProviderTests.cs index 3909a17847..cc94620476 100644 --- a/src/Umbraco.Tests/Persistence/SyntaxProvider/SqlSyntaxProviderTests.cs +++ b/src/Umbraco.Tests/Persistence/SyntaxProvider/SqlSyntaxProviderTests.cs @@ -1,7 +1,11 @@ using System; +using System.Collections.Generic; using NUnit.Framework; using Umbraco.Core.Models.Rdbms; +using Umbraco.Core.Persistence.DatabaseAnnotations; using Umbraco.Core.Persistence.DatabaseModelDefinitions; +using Umbraco.Core.Persistence.Migrations.Syntax.Create.Index; +using Umbraco.Core.Persistence.Migrations.Syntax.Expressions; using Umbraco.Core.Persistence.SqlSyntax; namespace Umbraco.Tests.Persistence.SyntaxProvider @@ -39,6 +43,71 @@ namespace Umbraco.Tests.Persistence.SyntaxProvider } } + [Test] + public void Format_SqlServer_NonClusteredIndexDefinition_AddsNonClusteredDirective() + { + SqlSyntaxContext.SqlSyntaxProvider = SqlServerSyntax.Provider; + + var indexDefinition = CreateIndexDefinition(); + indexDefinition.IndexType = IndexTypes.NonClustered; + + var actual = SqlSyntaxContext.SqlSyntaxProvider.Format(indexDefinition); + Assert.AreEqual("CREATE NONCLUSTERED INDEX [IX_A] ON [TheTable] ([A])", actual); + } + + [Test] + public void Format_SqlServer_NonClusteredIndexDefinition_UsingIsClusteredFalse_AddsClusteredDirective() + { + SqlSyntaxContext.SqlSyntaxProvider = SqlServerSyntax.Provider; + + var indexDefinition = CreateIndexDefinition(); + indexDefinition.IsClustered = false; + + var actual = SqlSyntaxContext.SqlSyntaxProvider.Format(indexDefinition); + Assert.AreEqual("CREATE CLUSTERED INDEX [IX_A] ON [TheTable] ([A])", actual); + } + + [Test] + public void CreateIndexBuilder_SqlServer_NonClustered_CreatesNonClusteredIndex() + { + SqlSyntaxContext.SqlSyntaxProvider = SqlServerSyntax.Provider; + var createExpression = new CreateIndexExpression { Index = { Name = "IX_A" } }; + var builder = new CreateIndexBuilder(createExpression); + builder.OnTable("TheTable").OnColumn("A").Ascending().WithOptions().NonClustered(); + Assert.AreEqual("CREATE NONCLUSTERED INDEX [IX_A] ON [TheTable] ([A])", createExpression.ToString()); + } + + [Test] + public void CreateIndexBuilder_SqlServer_Unique_CreatesUniqueNonClusteredIndex() + { + SqlSyntaxContext.SqlSyntaxProvider = SqlServerSyntax.Provider; + var createExpression = new CreateIndexExpression { Index = { Name = "IX_A" } }; + var builder = new CreateIndexBuilder(createExpression); + builder.OnTable("TheTable").OnColumn("A").Ascending().WithOptions().Unique(); + Assert.AreEqual("CREATE UNIQUE NONCLUSTERED INDEX [IX_A] ON [TheTable] ([A])", createExpression.ToString()); + } + + [Test] + public void CreateIndexBuilder_SqlServer_Clustered_CreatesClusteredIndex() + { + SqlSyntaxContext.SqlSyntaxProvider = SqlServerSyntax.Provider; + var createExpression = new CreateIndexExpression { Index = { Name = "IX_A" } }; + var builder = new CreateIndexBuilder(createExpression); + builder.OnTable("TheTable").OnColumn("A").Ascending().WithOptions().Clustered(); + Assert.AreEqual("CREATE CLUSTERED INDEX [IX_A] ON [TheTable] ([A])", createExpression.ToString()); + } + + private static IndexDefinition CreateIndexDefinition() + { + return new IndexDefinition + { + ColumnName = "A", + Name = "IX_A", + TableName = "TheTable", + SchemaName = "dbo" + }; + } + [TearDown] public void TearDown() { diff --git a/src/Umbraco.Tests/Services/ContentServiceTests.cs b/src/Umbraco.Tests/Services/ContentServiceTests.cs index cf1847ac60..fd5349a0a9 100644 --- a/src/Umbraco.Tests/Services/ContentServiceTests.cs +++ b/src/Umbraco.Tests/Services/ContentServiceTests.cs @@ -865,6 +865,27 @@ namespace Umbraco.Tests.Services //Assert.AreNotEqual(content.Name, copy.Name); } + [Test] + public void Can_Copy_Content_With_Tags() + { + // Arrange + var contentService = ServiceContext.ContentService; + var contentType = ServiceContext.ContentTypeService.GetContentType("umbTextpage"); + var temp = MockedContent.CreateSimpleContent(contentType, "Simple Text Page", -1); + var prop = temp.Properties.First(); + temp.SetTags(prop.Alias, new[] {"hello", "world"}, true); + var status = contentService.PublishWithStatus(temp); + + // Act + var copy = contentService.Copy(temp, temp.ParentId, false, 0); + + // Assert + var copiedTags = ServiceContext.TagService.GetTagsForEntity(copy.Id).ToArray(); + Assert.AreEqual(2, copiedTags.Count()); + Assert.AreEqual("hello", copiedTags[0].Text); + Assert.AreEqual("world", copiedTags[1].Text); + } + [Test, NUnit.Framework.Ignore] public void Can_Send_To_Publication() { } diff --git a/src/Umbraco.Tests/Services/EntityServiceTests.cs b/src/Umbraco.Tests/Services/EntityServiceTests.cs index c6bf01245a..cc2316c349 100644 --- a/src/Umbraco.Tests/Services/EntityServiceTests.cs +++ b/src/Umbraco.Tests/Services/EntityServiceTests.cs @@ -3,6 +3,7 @@ using System.Linq; using NUnit.Framework; using Umbraco.Core; using Umbraco.Core.Models; +using Umbraco.Tests.TestHelpers; using Umbraco.Tests.TestHelpers.Entities; namespace Umbraco.Tests.Services @@ -25,6 +26,11 @@ namespace Umbraco.Tests.Services base.TearDown(); } + protected override DatabaseBehavior DatabaseTestBehavior + { + get { return DatabaseBehavior.NewSchemaPerFixture; } + } + [Test] public void EntityService_Can_Find_All_Content_By_UmbracoObjectTypes() { @@ -136,24 +142,32 @@ namespace Umbraco.Tests.Services y => y.PropertyEditorAlias == Constants.PropertyEditors.UploadFieldAlias)), Is.True); } + private static bool _isSetup = false; + public override void CreateTestData() { - base.CreateTestData(); + if (_isSetup == false) + { + _isSetup = true; - //Create and Save folder-Media -> 1050 - var folderMediaType = ServiceContext.ContentTypeService.GetMediaType(1031); - var folder = MockedMedia.CreateMediaFolder(folderMediaType, -1); - ServiceContext.MediaService.Save(folder, 0); + base.CreateTestData(); - //Create and Save image-Media -> 1051 - var imageMediaType = ServiceContext.ContentTypeService.GetMediaType(1032); - var image = MockedMedia.CreateMediaImage(imageMediaType, folder.Id); - ServiceContext.MediaService.Save(image, 0); + //Create and Save folder-Media -> 1050 + var folderMediaType = ServiceContext.ContentTypeService.GetMediaType(1031); + var folder = MockedMedia.CreateMediaFolder(folderMediaType, -1); + ServiceContext.MediaService.Save(folder, 0); - //Create and Save file-Media -> 1052 - var fileMediaType = ServiceContext.ContentTypeService.GetMediaType(1033); - var file = MockedMedia.CreateMediaFile(fileMediaType, folder.Id); - ServiceContext.MediaService.Save(file, 0); + //Create and Save image-Media -> 1051 + var imageMediaType = ServiceContext.ContentTypeService.GetMediaType(1032); + var image = MockedMedia.CreateMediaImage(imageMediaType, folder.Id); + ServiceContext.MediaService.Save(image, 0); + + //Create and Save file-Media -> 1052 + var fileMediaType = ServiceContext.ContentTypeService.GetMediaType(1033); + var file = MockedMedia.CreateMediaFile(fileMediaType, folder.Id); + ServiceContext.MediaService.Save(file, 0); + } + } } } \ No newline at end of file diff --git a/src/Umbraco.Tests/TestHelpers/BaseDatabaseFactoryTest.cs b/src/Umbraco.Tests/TestHelpers/BaseDatabaseFactoryTest.cs index c0bc7a5c79..46caba40c5 100644 --- a/src/Umbraco.Tests/TestHelpers/BaseDatabaseFactoryTest.cs +++ b/src/Umbraco.Tests/TestHelpers/BaseDatabaseFactoryTest.cs @@ -55,10 +55,10 @@ namespace Umbraco.Tests.TestHelpers public override void Initialize() { InitializeFirstRunFlags(); - + var path = TestHelper.CurrentAssemblyDirectory; AppDomain.CurrentDomain.SetData("DataDirectory", path); - + //disable cache var cacheHelper = CacheHelper.CreateDisabledCacheHelper(); @@ -67,25 +67,26 @@ namespace Umbraco.Tests.TestHelpers GetDbProviderName()); _appContext = new ApplicationContext( - //assign the db context - new DatabaseContext(dbFactory), - //assign the service context - new ServiceContext(new PetaPocoUnitOfWorkProvider(dbFactory), new FileUnitOfWorkProvider(), new PublishingStrategy(), cacheHelper), - cacheHelper) - { - IsReady = true - }; + //assign the db context + new DatabaseContext(dbFactory), + //assign the service context + new ServiceContext(new PetaPocoUnitOfWorkProvider(dbFactory), new FileUnitOfWorkProvider(), new PublishingStrategy(), cacheHelper), + cacheHelper) + { + IsReady = true + }; base.Initialize(); - DatabaseContext.Initialize(dbFactory.ProviderName, dbFactory.ConnectionString); + using (DisposableTimer.TraceDuration("init")) + { + DatabaseContext.Initialize(dbFactory.ProviderName, dbFactory.ConnectionString); + CreateSqlCeDatabase(); + InitializeDatabase(); - CreateSqlCeDatabase(); - - InitializeDatabase(); - - //ensure the configuration matches the current version for tests - SettingsForTests.ConfigurationStatus = UmbracoVersion.Current.ToString(3); + //ensure the configuration matches the current version for tests + SettingsForTests.ConfigurationStatus = UmbracoVersion.Current.ToString(3); + } } protected override void SetupApplicationContext() diff --git a/src/Umbraco.Tests/TestHelpers/BaseUmbracoApplicationTest.cs b/src/Umbraco.Tests/TestHelpers/BaseUmbracoApplicationTest.cs index 403a285209..48b7c0b17f 100644 --- a/src/Umbraco.Tests/TestHelpers/BaseUmbracoApplicationTest.cs +++ b/src/Umbraco.Tests/TestHelpers/BaseUmbracoApplicationTest.cs @@ -20,22 +20,31 @@ namespace Umbraco.Tests.TestHelpers [TestFixture] public abstract class BaseUmbracoApplicationTest : BaseUmbracoConfigurationTest { + private static bool _mappersInitialized = false; + private static readonly object Locker = new object(); + [SetUp] public override void Initialize() { base.Initialize(); - TestHelper.InitializeContentDirectories(); - TestHelper.EnsureUmbracoSettingsConfig(); + using (DisposableTimer.TraceDuration("init")) + { + TestHelper.InitializeContentDirectories(); + TestHelper.EnsureUmbracoSettingsConfig(); + + //Create the legacy prop-eds mapping + LegacyPropertyEditorIdToAliasConverter.CreateMappingsForCoreEditors(); + + SetupPluginManager(); + + SetupApplicationContext(); + + InitializeMappers(); + + FreezeResolution(); + } - //Create the legacy prop-eds mapping - LegacyPropertyEditorIdToAliasConverter.CreateMappingsForCoreEditors(); - - SetupPluginManager(); - SetupApplicationContext(); - InitializeMappers(); - - FreezeResolution(); } [TearDown] @@ -57,18 +66,24 @@ namespace Umbraco.Tests.TestHelpers private void InitializeMappers() { - Mapper.Initialize(configuration => + lock (Locker) { - var mappers = PluginManager.Current.FindAndCreateInstances(); - foreach (var mapper in mappers) + //only need to initialize one time + if (_mappersInitialized == false) { - mapper.ConfigureMappings(configuration, ApplicationContext); - } - }); + _mappersInitialized = true; + Mapper.Initialize(configuration => + { + var mappers = PluginManager.Current.FindAndCreateInstances(); + foreach (var mapper in mappers) + { + mapper.ConfigureMappings(configuration, ApplicationContext); + } + }); + } + } } - - /// /// By default this returns false which means the plugin manager will not be reset so it doesn't need to re-scan /// all of the assemblies. Inheritors can override this if plugin manager resetting is required, generally needs diff --git a/src/Umbraco.Tests/TestHelpers/BaseUmbracoConfigurationTest.cs b/src/Umbraco.Tests/TestHelpers/BaseUmbracoConfigurationTest.cs index b5524cad0f..64769511b0 100644 --- a/src/Umbraco.Tests/TestHelpers/BaseUmbracoConfigurationTest.cs +++ b/src/Umbraco.Tests/TestHelpers/BaseUmbracoConfigurationTest.cs @@ -1,4 +1,5 @@ using NUnit.Framework; +using Umbraco.Core; using Umbraco.Core.Configuration.UmbracoSettings; namespace Umbraco.Tests.TestHelpers @@ -9,17 +10,25 @@ namespace Umbraco.Tests.TestHelpers [TestFixture] public abstract class BaseUmbracoConfigurationTest { + [TestFixtureSetUp] + public void InitializeFixture() + { + TestHelper.SetupLog4NetForTests(); + } + [SetUp] public virtual void Initialize() { - TestHelper.SetupLog4NetForTests(); + using (DisposableTimer.TraceDuration < BaseUmbracoConfigurationTest>("init")) + { + //mock the Umbraco settings that we need + var settings = SettingsForTests.GetMockSettings(); + //sets the global singleton to use the mocked format + SettingsForTests.ConfigureSettings(settings); + //set our local variable for tests to use (preferably) + UmbracoSettings = settings; + } - //mock the Umbraco settings that we need - var settings = SettingsForTests.GetMockSettings(); - //sets the global singleton to use the mocked format - SettingsForTests.ConfigureSettings(settings); - //set our local variable for tests to use (preferably) - UmbracoSettings = settings; } [TearDown] diff --git a/src/Umbraco.Tests/unit-test-log4net.config b/src/Umbraco.Tests/unit-test-log4net.config index 0922e4d932..1b4fc414e2 100644 --- a/src/Umbraco.Tests/unit-test-log4net.config +++ b/src/Umbraco.Tests/unit-test-log4net.config @@ -14,7 +14,14 @@ - + + + + + + + + diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/datepicker/bootstrap-datetimepicker.js b/src/Umbraco.Web.UI.Client/lib/datetimepicker/bootstrap-datetimepicker.js similarity index 100% rename from src/Umbraco.Web.UI.Client/src/views/propertyeditors/datepicker/bootstrap-datetimepicker.js rename to src/Umbraco.Web.UI.Client/lib/datetimepicker/bootstrap-datetimepicker.js diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/datepicker/bootstrap-datetimepicker.min.css b/src/Umbraco.Web.UI.Client/lib/datetimepicker/bootstrap-datetimepicker.min.css similarity index 99% rename from src/Umbraco.Web.UI.Client/src/views/propertyeditors/datepicker/bootstrap-datetimepicker.min.css rename to src/Umbraco.Web.UI.Client/lib/datetimepicker/bootstrap-datetimepicker.min.css index 37449fc396..36394e276b 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/datepicker/bootstrap-datetimepicker.min.css +++ b/src/Umbraco.Web.UI.Client/lib/datetimepicker/bootstrap-datetimepicker.min.css @@ -1,8 +1,8 @@ -/*! - * Datepicker for Bootstrap - * - * Copyright 2012 Stefan Petre - * Licensed under the Apache License v2.0 - * http://www.apache.org/licenses/LICENSE-2.0 - * +/*! + * Datepicker for Bootstrap + * + * Copyright 2012 Stefan Petre + * Licensed under the Apache License v2.0 + * http://www.apache.org/licenses/LICENSE-2.0 + * */.clearfix{*zoom:1}.clearfix:before,.clearfix:after{display:table;content:"";line-height:0}.clearfix:after{clear:both}.hide-text{font:0/0 a;color:transparent;text-shadow:none;background-color:transparent;border:0}.input-block-level{display:block;width:100%;min-height:30px;-webkit-box-sizing:border-box;-moz-box-sizing:border-box;box-sizing:border-box}.bootstrap-datetimepicker-widget{top:0;left:0;width:250px;padding:4px;margin-top:1px;z-index:3000;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px}.bootstrap-datetimepicker-widget:before{content:'';display:inline-block;border-left:7px solid transparent;border-right:7px solid transparent;border-bottom:7px solid #ccc;border-bottom-color:rgba(0,0,0,0.2);position:absolute;top:-7px;left:6px}.bootstrap-datetimepicker-widget:after{content:'';display:inline-block;border-left:6px solid transparent;border-right:6px solid transparent;border-bottom:6px solid #fff;position:absolute;top:-6px;left:7px}.bootstrap-datetimepicker-widget.pull-right:before{left:auto;right:6px}.bootstrap-datetimepicker-widget.pull-right:after{left:auto;right:7px}.bootstrap-datetimepicker-widget>ul{list-style-type:none;margin:0}.bootstrap-datetimepicker-widget .timepicker-hour,.bootstrap-datetimepicker-widget .timepicker-minute,.bootstrap-datetimepicker-widget .timepicker-second{width:100%;font-weight:bold;font-size:1.2em}.bootstrap-datetimepicker-widget table[data-hour-format="12"] .separator{width:4px;padding:0;margin:0}.bootstrap-datetimepicker-widget .datepicker>div{display:none}.bootstrap-datetimepicker-widget .picker-switch{text-align:center}.bootstrap-datetimepicker-widget table{width:100%;margin:0}.bootstrap-datetimepicker-widget td,.bootstrap-datetimepicker-widget th{text-align:center;width:20px;height:20px;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px}.bootstrap-datetimepicker-widget td.day:hover,.bootstrap-datetimepicker-widget td.hour:hover,.bootstrap-datetimepicker-widget td.minute:hover,.bootstrap-datetimepicker-widget td.second:hover{background:#eee;cursor:pointer}.bootstrap-datetimepicker-widget td.old,.bootstrap-datetimepicker-widget td.new{color:#999}.bootstrap-datetimepicker-widget td.active,.bootstrap-datetimepicker-widget td.active:hover{color:#fff;background-color:#006dcc;background-image:-moz-linear-gradient(top,#08c,#04c);background-image:-webkit-gradient(linear,0 0,0 100%,from(#08c),to(#04c));background-image:-webkit-linear-gradient(top,#08c,#04c);background-image:-o-linear-gradient(top,#08c,#04c);background-image:linear-gradient(to bottom,#08c,#04c);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff0088cc',endColorstr='#ff0044cc',GradientType=0);border-color:#04c #04c #002a80;border-color:rgba(0,0,0,0.1) rgba(0,0,0,0.1) rgba(0,0,0,0.25);*background-color:#04c;filter:progid:DXImageTransform.Microsoft.gradient(enabled = false);color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,0.25)}.bootstrap-datetimepicker-widget td.active:hover,.bootstrap-datetimepicker-widget td.active:hover:hover,.bootstrap-datetimepicker-widget td.active:active,.bootstrap-datetimepicker-widget td.active:hover:active,.bootstrap-datetimepicker-widget td.active.active,.bootstrap-datetimepicker-widget td.active:hover.active,.bootstrap-datetimepicker-widget td.active.disabled,.bootstrap-datetimepicker-widget td.active:hover.disabled,.bootstrap-datetimepicker-widget td.active[disabled],.bootstrap-datetimepicker-widget td.active:hover[disabled]{color:#fff;background-color:#04c;*background-color:#003bb3}.bootstrap-datetimepicker-widget td.active:active,.bootstrap-datetimepicker-widget td.active:hover:active,.bootstrap-datetimepicker-widget td.active.active,.bootstrap-datetimepicker-widget td.active:hover.active{background-color:#039 \9}.bootstrap-datetimepicker-widget td.disabled,.bootstrap-datetimepicker-widget td.disabled:hover{background:0;color:#999;cursor:not-allowed}.bootstrap-datetimepicker-widget td span{display:block;width:47px;height:54px;line-height:54px;float:left;margin:2px;cursor:pointer;-webkit-border-radius:4px;-moz-border-radius:4px;border-radius:4px}.bootstrap-datetimepicker-widget td span:hover{background:#eee}.bootstrap-datetimepicker-widget td span.active{color:#fff;background-color:#006dcc;background-image:-moz-linear-gradient(top,#08c,#04c);background-image:-webkit-gradient(linear,0 0,0 100%,from(#08c),to(#04c));background-image:-webkit-linear-gradient(top,#08c,#04c);background-image:-o-linear-gradient(top,#08c,#04c);background-image:linear-gradient(to bottom,#08c,#04c);background-repeat:repeat-x;filter:progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff0088cc',endColorstr='#ff0044cc',GradientType=0);border-color:#04c #04c #002a80;border-color:rgba(0,0,0,0.1) rgba(0,0,0,0.1) rgba(0,0,0,0.25);*background-color:#04c;filter:progid:DXImageTransform.Microsoft.gradient(enabled = false);color:#fff;text-shadow:0 -1px 0 rgba(0,0,0,0.25)}.bootstrap-datetimepicker-widget td span.active:hover,.bootstrap-datetimepicker-widget td span.active:active,.bootstrap-datetimepicker-widget td span.active.active,.bootstrap-datetimepicker-widget td span.active.disabled,.bootstrap-datetimepicker-widget td span.active[disabled]{color:#fff;background-color:#04c;*background-color:#003bb3}.bootstrap-datetimepicker-widget td span.active:active,.bootstrap-datetimepicker-widget td span.active.active{background-color:#039 \9}.bootstrap-datetimepicker-widget td span.old{color:#999}.bootstrap-datetimepicker-widget td span.disabled,.bootstrap-datetimepicker-widget td span.disabled:hover{background:0;color:#999;cursor:not-allowed}.bootstrap-datetimepicker-widget th.switch{width:145px}.bootstrap-datetimepicker-widget th.next,.bootstrap-datetimepicker-widget th.prev{font-size:21px}.bootstrap-datetimepicker-widget th.disabled,.bootstrap-datetimepicker-widget th.disabled:hover{background:0;color:#999;cursor:not-allowed}.bootstrap-datetimepicker-widget thead tr:first-child th{cursor:pointer}.bootstrap-datetimepicker-widget thead tr:first-child th:hover{background:#eee}.input-append.date .add-on i,.input-prepend.date .add-on i{display:block;cursor:pointer;width:16px;height:16px}.bootstrap-datetimepicker-widget.left-oriented:before{left:auto;right:6px}.bootstrap-datetimepicker-widget.left-oriented:after{left:auto;right:7px} \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/datepicker/bootstrap-datetimepicker.min.js b/src/Umbraco.Web.UI.Client/lib/datetimepicker/bootstrap-datetimepicker.min.js similarity index 100% rename from src/Umbraco.Web.UI.Client/src/views/propertyeditors/datepicker/bootstrap-datetimepicker.min.js rename to src/Umbraco.Web.UI.Client/lib/datetimepicker/bootstrap-datetimepicker.min.js diff --git a/src/Umbraco.Web.UI.Client/lib/datetimepicker/langs/datetimepicker.pt-BR.js b/src/Umbraco.Web.UI.Client/lib/datetimepicker/langs/datetimepicker.pt-BR.js new file mode 100644 index 0000000000..2455b93da6 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/lib/datetimepicker/langs/datetimepicker.pt-BR.js @@ -0,0 +1,14 @@ +/** + * Brazilian translation for bootstrap-datetimepicker + * Cauan Cabral + */ +; (function ($) { + $.fn.datetimepicker.dates['pt-BR'] = { + days: ["Domingo", "Segunda", "Terça", "Quarta", "Quinta", "Sexta", "Sábado", "Domingo"], + daysShort: ["Dom", "Seg", "Ter", "Qua", "Qui", "Sex", "Sáb", "Dom"], + daysMin: ["Do", "Se", "Te", "Qu", "Qu", "Se", "Sa", "Do"], + months: ["Janeiro", "Fevereiro", "Março", "Abril", "Maio", "Junho", "Julho", "Agosto", "Setembro", "Outubro", "Novembro", "Dezembro"], + monthsShort: ["Jan", "Fev", "Mar", "Abr", "Mai", "Jun", "Jul", "Ago", "Set", "Out", "Nov", "Dez"], + today: "Hoje" + }; +}(jQuery)); \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/common/services/appstate.service.js b/src/Umbraco.Web.UI.Client/src/common/services/appstate.service.js index f0563d5900..81e3fff441 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/appstate.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/appstate.service.js @@ -16,7 +16,8 @@ function appState($rootScope) { touchDevice: null, showTray: null, stickyNavigation: null, - navMode: null + navMode: null, + editingEntity: null }; var sectionState = { diff --git a/src/Umbraco.Web.UI.Client/src/common/services/contenteditinghelper.service.js b/src/Umbraco.Web.UI.Client/src/common/services/contenteditinghelper.service.js index 0a2a74d006..5ed9212d3c 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/contenteditinghelper.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/contenteditinghelper.service.js @@ -2,11 +2,13 @@ /** * @ngdoc service * @name umbraco.services.contentEditingHelper -* @description A helper service for content/media/member controllers when editing/creating/saving content. +* @description A helper service for most editors, some methods are specific to content/media/member model types but most are used by +* all editors to share logic and reduce the amount of replicated code among editors. **/ -function contentEditingHelper($location, $routeParams, notificationsService, serverValidationManager, dialogService, formHelper) { +function contentEditingHelper($location, $routeParams, notificationsService, serverValidationManager, dialogService, formHelper, appState) { return { + /** * @ngdoc method @@ -36,15 +38,15 @@ function contentEditingHelper($location, $routeParams, notificationsService, ser * @function * * @description - * re-binds all changed property values to the origContent object from the newContent object and returns an array of changed properties. + * re-binds all changed property values to the origContent object from the savedContent object and returns an array of changed properties. */ - reBindChangedProperties: function (origContent, newContent) { + reBindChangedProperties: function (origContent, savedContent) { var changed = []; //get a list of properties since they are contained in tabs var allOrigProps = this.getAllProps(origContent); - var allNewProps = this.getAllProps(newContent); + var allNewProps = this.getAllProps(savedContent); function getNewProp(alias) { return _.find(allNewProps, function (item) { @@ -66,8 +68,8 @@ function contentEditingHelper($location, $routeParams, notificationsService, ser continue; } - if (!_.isEqual(origContent[o], newContent[o])) { - origContent[o] = newContent[o]; + if (!_.isEqual(origContent[o], savedContent[o])) { + origContent[o] = savedContent[o]; } } @@ -109,9 +111,6 @@ function contentEditingHelper($location, $routeParams, notificationsService, ser if (!args.err) { throw "args.err cannot be null"; } - if (!args.allNewProps && !angular.isArray(args.allNewProps)) { - throw "args.allNewProps must be a valid array"; - } if (args.redirectOnFailure === undefined || args.redirectOnFailure === null) { throw "args.redirectOnFailure must be set to true or false"; } @@ -168,11 +167,11 @@ function contentEditingHelper($location, $routeParams, notificationsService, ser if (!args) { throw "args cannot be null"; } - if (!args.newContent) { - throw "args.newContent cannot be null"; + if (!args.savedContent) { + throw "args.savedContent cannot be null"; } - if (!this.redirectToCreatedContent(args.redirectId ? args.redirectId : args.newContent.id)) { + if (!this.redirectToCreatedContent(args.redirectId ? args.redirectId : args.savedContent.id)) { //we are not redirecting because this is not new content, it is existing content. In this case // we need to detect what properties have changed and re-bind them with the server data. diff --git a/src/Umbraco.Web.UI.Client/src/common/services/editorContext.service.js b/src/Umbraco.Web.UI.Client/src/common/services/editorContext.service.js deleted file mode 100644 index 4f69a3011a..0000000000 --- a/src/Umbraco.Web.UI.Client/src/common/services/editorContext.service.js +++ /dev/null @@ -1,2 +0,0 @@ -angular.module("umbraco.services") - .value('editorContext', undefined); \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/common/services/navigation.service.js b/src/Umbraco.Web.UI.Client/src/common/services/navigation.service.js index c619f37b92..79cf1a75b7 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/navigation.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/navigation.service.js @@ -176,15 +176,11 @@ function navigationService($rootScope, $routeParams, $log, $location, $q, $timeo appState.setGlobalState("showTray", false); }, - //adding this to get clean global access to the main tree directive - //there will only ever be one main tree event handler - //we need to pass in the current scope for binding these actions - - //TODO: How many places are we assigning a currentNode?? Now we're assigning a currentNode arbitrarily to this - // scope - which looks to be the scope of the navigation controller - but then we are assigning a global current - // node on the ui object?? This is a mess. - - setupTreeEvents: function(treeEventHandler, scope) { + /** + Called to assign the main tree event handler - this is called by the navigation controller. + TODO: Potentially another dev could call this which would kind of mung the whole app so potentially there's a better way. + */ + setupTreeEvents: function(treeEventHandler) { mainTreeEventHandler = treeEventHandler; //when a tree is loaded into a section, we need to put it into appState @@ -199,12 +195,6 @@ function navigationService($rootScope, $routeParams, $log, $location, $q, $timeo //set the current selected node appState.setTreeState("selectedNode", args.node); } - - //TODO: what the heck is going on here? - this seems really zany, allowing us to modify the - // navigationController.scope from within the navigationService to assign back to the args - // so that we can change the navigationController.scope from within the umbTree directive. Hrm. - args.scope = scope; - }); //this reacts to the options item in the tree @@ -215,11 +205,6 @@ function navigationService($rootScope, $routeParams, $log, $location, $q, $timeo //Set the current action node (this is not the same as the current selected node!) appState.setMenuState("currentNode", args.node); - //TODO: what the heck is going on here? - this seems really zany, allowing us to modify the - // navigationController.scope from within the navigationService to assign back to the args - // so that we can change the navigationController.scope from within the umbTree directive. Hrm. - args.scope = scope; - if (args.event && args.event.altKey) { args.skipDefault = true; } @@ -231,11 +216,6 @@ function navigationService($rootScope, $routeParams, $log, $location, $q, $timeo ev.stopPropagation(); ev.preventDefault(); - //TODO: what the heck is going on here? - this seems really zany, allowing us to modify the - // navigationController.scope from within the navigationService to assign back to the args - // so that we can change the navigationController.scope from within the umbTree directive. Hrm. - args.scope = scope; - args.skipDefault = true; service.showMenu(ev, args); }); @@ -457,8 +437,19 @@ function navigationService($rootScope, $routeParams, $log, $location, $q, $timeo setMode("tree"); }, + /** Executes a given menu action */ executeMenuAction: function (action, node, section) { + if (!action) { + throw "action cannot be null"; + } + if (!node) { + throw "node cannot be null"; + } + if (!section) { + throw "section cannot be null"; + } + if (action.metaData && action.metaData["jsAction"] && angular.isString(action.metaData["jsAction"])) { //we'll try to get the jsAction from the injector @@ -471,12 +462,12 @@ function navigationService($rootScope, $routeParams, $log, $location, $q, $timeo eval(js); } else { - var _service = $injector.get(menuAction[0]); - if (!_service) { + var menuActionService = $injector.get(menuAction[0]); + if (!menuActionService) { throw "The angular service " + menuAction[0] + " could not be found"; } - var method = _service[menuAction[1]]; + var method = menuActionService[menuAction[1]]; if (!method) { throw "The method " + menuAction[1] + " on the angular service " + menuAction[0] + " could not be found"; diff --git a/src/Umbraco.Web.UI.Client/src/common/services/tree.service.js b/src/Umbraco.Web.UI.Client/src/common/services/tree.service.js index f8266168b1..ba62feff60 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/tree.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/tree.service.js @@ -227,12 +227,13 @@ function treeService($q, treeResource, iconHelper, notificationsService, $rootSc return this.getChildren(args) .then(function(data) { - //set state to done and expand + //set state to done and expand (only if there actually are children!) args.node.loading = false; args.node.children = data; - args.node.expanded = true; - args.node.hasChildren = true; - + if (args.node.children && args.node.children.length > 0) { + args.node.expanded = true; + args.node.hasChildren = true; + } return data; }, function(reason) { diff --git a/src/Umbraco.Web.UI.Client/src/less/hacks.less b/src/Umbraco.Web.UI.Client/src/less/hacks.less index b4d5280db1..cd11c1ed69 100644 --- a/src/Umbraco.Web.UI.Client/src/less/hacks.less +++ b/src/Umbraco.Web.UI.Client/src/less/hacks.less @@ -70,4 +70,15 @@ iframe, .content-column-body { .legacy-custom-file{ width: 16px; height: 16px; margin-right: 11px; display: inline-block; background-position: center center; +} + +/* + missing icon names in helveticons that are in font-awesome - used by the datepicker, + basically making them equivalent to their helviton icon +*/ +.icon-chevron-up:before { + content: "\e128"; +} +.icon-chevron-down:before { + content: "\e0c9"; } \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/views/common/navigation.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/navigation.controller.js index 095ec8c3d8..435f6d844e 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/navigation.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/navigation.controller.js @@ -14,7 +14,7 @@ function NavigationController($scope, $rootScope, $location, $log, $routeParams, //TODO: Need to think about this and an nicer way to acheive what this is doing. //the tree event handler i used to subscribe to the main tree click events $scope.treeEventHandler = $({}); - navigationService.setupTreeEvents($scope.treeEventHandler, $scope); + navigationService.setupTreeEvents($scope.treeEventHandler); //Put the navigation service on this scope so we can use it's methods/properties in the view. // IMPORTANT: all properties assigned to this scope are generally available on the scope object on dialogs since diff --git a/src/Umbraco.Web.UI.Client/src/views/content/content.edit.controller.js b/src/Umbraco.Web.UI.Client/src/views/content/content.edit.controller.js index f47b98f5a5..487ba611db 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/content.edit.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/content/content.edit.controller.js @@ -6,7 +6,7 @@ * @description * The controller for the content editor */ -function ContentEditController($scope, $routeParams, $q, $timeout, $window, appState, contentResource, navigationService, notificationsService, angularHelper, serverValidationManager, contentEditingHelper, editorContext, treeService, fileManager, formHelper, umbRequestHelper, keyboardService) { +function ContentEditController($scope, $routeParams, $q, $timeout, $window, appState, contentResource, navigationService, notificationsService, angularHelper, serverValidationManager, contentEditingHelper, treeService, fileManager, formHelper, umbRequestHelper, keyboardService, umbModelMapper) { //setup scope vars $scope.defaultButton = null; @@ -15,9 +15,6 @@ function ContentEditController($scope, $routeParams, $q, $timeout, $window, appS $scope.currentSection = appState.getSectionState("currentSection"); $scope.currentNode = null; //the editors affiliated node - //we need this to share our content object with property editors - editorContext = $scope.content; - //This sets up the action buttons based on what permissions the user has. //The allowedActions parameter contains a list of chars, each represents a button by permission so //here we'll build the buttons according to the chars of the user. @@ -123,11 +120,15 @@ function ContentEditController($scope, $routeParams, $q, $timeout, $window, appS contentEditingHelper.handleSuccessfulSave({ scope: $scope, - newContent: data, + savedContent: data, rebindCallback: contentEditingHelper.reBindChangedProperties($scope.content, data) }); + //update appState + appState.setGlobalState("editingEntity", umbModelMapper.convertToEntityBasic($scope.content)); + configureButtons(data); + navigationService.syncTree({ tree: "content", path: data.path.split(","), forceReload: true }).then(function (syncArgs) { $scope.currentNode = syncArgs.node; }); @@ -139,10 +140,12 @@ function ContentEditController($scope, $routeParams, $q, $timeout, $window, appS contentEditingHelper.handleSaveError({ redirectOnFailure: true, err: err, - allNewProps: contentEditingHelper.getAllProps(err.data), - allOrigProps: contentEditingHelper.getAllProps($scope.content) + rebindCallback: contentEditingHelper.reBindChangedProperties($scope.content, err.data) }); + //update appState + appState.setGlobalState("editingEntity", umbModelMapper.convertToEntityBasic($scope.content)); + deferred.reject(err); }); } @@ -159,6 +162,8 @@ function ContentEditController($scope, $routeParams, $q, $timeout, $window, appS .then(function(data) { $scope.loaded = true; $scope.content = data; + //put this into appState + appState.setGlobalState("editingEntity", umbModelMapper.convertToEntityBasic($scope.content)); configureButtons($scope.content); }); } @@ -168,6 +173,8 @@ function ContentEditController($scope, $routeParams, $q, $timeout, $window, appS .then(function(data) { $scope.loaded = true; $scope.content = data; + //put this into appState + appState.setGlobalState("editingEntity", umbModelMapper.convertToEntityBasic($scope.content)); configureButtons($scope.content); //in one particular special case, after we've created a new item we redirect back to the edit @@ -194,7 +201,7 @@ function ContentEditController($scope, $routeParams, $q, $timeout, $window, appS contentEditingHelper.handleSuccessfulSave({ scope: $scope, - newContent: data, + savedContent: data, rebindCallback: contentEditingHelper.reBindChangedProperties($scope.content, data) }); diff --git a/src/Umbraco.Web.UI.Client/src/views/datatype/datatype.edit.controller.js b/src/Umbraco.Web.UI.Client/src/views/datatype/datatype.edit.controller.js index 5a83003c46..7f82dd8fec 100644 --- a/src/Umbraco.Web.UI.Client/src/views/datatype/datatype.edit.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/datatype/datatype.edit.controller.js @@ -45,8 +45,8 @@ function DataTypeEditController($scope, $routeParams, $location, appState, navig $scope.preValues = []; if ($routeParams.create) { - //we are creating so get an empty content item - dataTypeResource.getScaffold($routeParams.id, $routeParams.doctype) + //we are creating so get an empty data type item + dataTypeResource.getScaffold($routeParams.id) .then(function(data) { $scope.loaded = true; $scope.preValuesLoaded = true; @@ -101,7 +101,7 @@ function DataTypeEditController($scope, $routeParams, $location, appState, navig contentEditingHelper.handleSuccessfulSave({ scope: $scope, - newContent: data, + savedContent: data, rebindCallback: function() { createPreValueProps(data.preValues); } @@ -117,9 +117,7 @@ function DataTypeEditController($scope, $routeParams, $location, appState, navig // to be the same thing since that only really matters for content/media. contentEditingHelper.handleSaveError({ redirectOnFailure: false, - err: err, - allNewProps: $scope.preValues, - allOrigProps: $scope.preValues + err: err }); }); } diff --git a/src/Umbraco.Web.UI.Client/src/views/media/media.edit.controller.js b/src/Umbraco.Web.UI.Client/src/views/media/media.edit.controller.js index 99f21c3fb9..1d3e226fd8 100644 --- a/src/Umbraco.Web.UI.Client/src/views/media/media.edit.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/media/media.edit.controller.js @@ -6,7 +6,7 @@ * @description * The controller for the media editor */ -function mediaEditController($scope, $routeParams, appState, mediaResource, navigationService, notificationsService, angularHelper, serverValidationManager, contentEditingHelper, fileManager, treeService, formHelper) { +function mediaEditController($scope, $routeParams, appState, mediaResource, navigationService, notificationsService, angularHelper, serverValidationManager, contentEditingHelper, fileManager, treeService, formHelper, umbModelMapper) { //setup scope vars $scope.nav = navigationService; @@ -19,7 +19,8 @@ function mediaEditController($scope, $routeParams, appState, mediaResource, navi .then(function (data) { $scope.loaded = true; $scope.content = data; - + //put this into appState + appState.setGlobalState("editingEntity", umbModelMapper.convertToEntityBasic($scope.content)); }); } else { @@ -27,7 +28,9 @@ function mediaEditController($scope, $routeParams, appState, mediaResource, navi .then(function (data) { $scope.loaded = true; $scope.content = data; - + //put this into appState + appState.setGlobalState("editingEntity", umbModelMapper.convertToEntityBasic($scope.content)); + //in one particular special case, after we've created a new item we redirect back to the edit // route but there might be server validation errors in the collection which we need to display // after the redirect, so we will bind all subscriptions which will show the server validation errors @@ -52,10 +55,13 @@ function mediaEditController($scope, $routeParams, appState, mediaResource, navi contentEditingHelper.handleSuccessfulSave({ scope: $scope, - newContent: data, + savedContent: data, rebindCallback: contentEditingHelper.reBindChangedProperties($scope.content, data) }); + //update appState + appState.setGlobalState("editingEntity", umbModelMapper.convertToEntityBasic($scope.content)); + navigationService.syncTree({ tree: "media", path: data.path, forceReload: true }).then(function (syncArgs) { $scope.currentNode = syncArgs.node; }); @@ -65,9 +71,11 @@ function mediaEditController($scope, $routeParams, appState, mediaResource, navi contentEditingHelper.handleSaveError({ err: err, redirectOnFailure: true, - allNewProps: contentEditingHelper.getAllProps(err.data), rebindCallback: contentEditingHelper.reBindChangedProperties($scope.content, err.data) }); + + //update appState + appState.setGlobalState("editingEntity", umbModelMapper.convertToEntityBasic($scope.content)); }); } diff --git a/src/Umbraco.Web.UI.Client/src/views/member/member.edit.controller.js b/src/Umbraco.Web.UI.Client/src/views/member/member.edit.controller.js index f1ade8b374..4a20db9c43 100644 --- a/src/Umbraco.Web.UI.Client/src/views/member/member.edit.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/member/member.edit.controller.js @@ -6,19 +6,29 @@ * @description * The controller for the member editor */ -function MemberEditController($scope, $routeParams, $location, $q, $window, appState, memberResource, entityResource, navigationService, notificationsService, angularHelper, serverValidationManager, contentEditingHelper, fileManager, formHelper, treeService) { +function MemberEditController($scope, $routeParams, $location, $q, $window, appState, memberResource, entityResource, navigationService, notificationsService, angularHelper, serverValidationManager, contentEditingHelper, fileManager, formHelper, umbModelMapper) { //setup scope vars $scope.nav = navigationService; $scope.currentSection = appState.getSectionState("currentSection"); $scope.currentNode = null; //the editors affiliated node + //build a path to sync the tree with + function buildTreePath(data) { + //TODO: Will this work for the 'other' list ? + var path = data.name[0] + "," + data.key; + path = path.replace(/-/g, ''); + return path; + } + if ($routeParams.create) { //we are creating so get an empty member item memberResource.getScaffold($routeParams.doctype) .then(function(data) { $scope.loaded = true; $scope.content = data; + //put this into appState + appState.setGlobalState("editingEntity", umbModelMapper.convertToEntityBasic($scope.content)); }); } else { @@ -40,9 +50,10 @@ function MemberEditController($scope, $routeParams, $location, $q, $window, appS $scope.loaded = true; $scope.content = data; - //build a path to sync the tree with - var path = data.name[0]+"," + data.key; - path = path.replace(/-/g,''); + //put this into appState + appState.setGlobalState("editingEntity", umbModelMapper.convertToEntityBasic($scope.content)); + + var path = buildTreePath(data); navigationService.syncTree({ tree: "member", path: path.split(",") }).then(function (syncArgs) { $scope.currentNode = syncArgs.node; @@ -69,13 +80,18 @@ function MemberEditController($scope, $routeParams, $location, $q, $window, appS contentEditingHelper.handleSuccessfulSave({ scope: $scope, - newContent: data, + savedContent: data, //specify a custom id to redirect to since we want to use the GUID redirectId: data.key, rebindCallback: contentEditingHelper.reBindChangedProperties($scope.content, data) }); - navigationService.syncTree({ tree: "member", path: path.split(",") }).then(function (syncArgs) { + //update appState + appState.setGlobalState("editingEntity", umbModelMapper.convertToEntityBasic($scope.content)); + + var path = buildTreePath(data); + + navigationService.syncTree({ tree: "member", path: path.split(","), forceReload: true }).then(function (syncArgs) { $scope.currentNode = syncArgs.node; }); @@ -84,9 +100,11 @@ function MemberEditController($scope, $routeParams, $location, $q, $window, appS contentEditingHelper.handleSaveError({ redirectOnFailure: false, err: err, - allNewProps: contentEditingHelper.getAllProps(err.data), - allOrigProps: contentEditingHelper.getAllProps($scope.content) + rebindCallback: contentEditingHelper.reBindChangedProperties($scope.content, err.data) }); + + //update appState + appState.setGlobalState("editingEntity", umbModelMapper.convertToEntityBasic($scope.content)); }); } 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 111b03bff1..b8f12ac7b7 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 @@ -1,54 +1,67 @@ -angular.module("umbraco").controller("Umbraco.Editors.DatepickerController", - function ($scope, notificationsService, assetsService, userService) { +function dateTimePickerController($scope, notificationsService, assetsService, angularHelper, userService) { - function applyDate(e){ - // when a date is changed, update the model - if (e.localDate) { - if ($scope.model.config.format == "yyyy-MM-dd HH:mm:ss") { - $scope.$apply(function(){ - $scope.model.value = e.localDate.toIsoDateTimeString(); - }); - }else{ - $scope.model.value = e.localDate.toIsoDateString(); - } + //lists the custom language files that we currently support + var customLangs = ["pt-BR"]; + + //setup the default config + var config = { + pickDate: true, + pickTime: true, + pick12HourFormat: false, + format: "yyyy-MM-dd hh:mm:ss" + }; + + //map the user config + $scope.model.config = angular.extend(config, $scope.model.config); + + //handles the date changing via the api + function applyDate(e) { + angularHelper.safeApply($scope, function() { + // when a date is changed, update the model + if (e.localDate) { + if ($scope.model.config.format == "yyyy-MM-dd hh:mm:ss") { + $scope.model.value = e.localDate.toIsoDateTimeString(); } - } + else { + $scope.model.value = e.localDate.toIsoDateString(); + } + } + }); + } - function initEditor(){ - // Get the id of the datepicker button that was clicked - var pickerId = $scope.model.alias; - // Open the datepicker and add a changeDate eventlistener - - $("#" + pickerId) - .datetimepicker($scope.model.config) - .on("changeDate", applyDate) - .on("hide", applyDate); + //get the current user to see if we can localize this picker + userService.getCurrentUser().then(function (user) { + + var filesToLoad = ["lib/datetimepicker/bootstrap-datetimepicker.min.js"]; + + //if we support this custom culture, set it, then we'll need to load in that lang file + if (_.contains(customLangs, user.locale)) { + $scope.model.config.language = user.locale; + filesToLoad.push("lib/datetimepicker/langs/datetimepicker." + user.locale + ".js"); } - userService.getCurrentUser().then(function(user){ + assetsService.load(filesToLoad).then( + function() { + //The Datepicker js and css files are available and all components are ready to use. - //setup the default config - var config = { - pickDate: true, - pickTime: true, - language: user.locale, - format: "yyyy-MM-dd HH:mm:ss" - }; + // Get the id of the datepicker button that was clicked + var pickerId = $scope.model.alias; + // Open the datepicker and add a changeDate eventlistener + $("#datepicker" + pickerId) + //.datetimepicker(config); + .datetimepicker($scope.model.config) + .on("changeDate", applyDate); - //format:"yyyy-MM-dd HH:mm:ss" + //now assign the date + $("#datepicker" + pickerId).val($scope.model.value); - //map the user config - angular.extend(config, $scope.model.config); - //map back to the model - $scope.model.config = config; - $scope.model.viewvalue = $scope.model.value; + }); - assetsService.loadJs( - 'views/propertyeditors/datepicker/bootstrap-datetimepicker.js' - ).then(initEditor); - }); - - assetsService.loadCss( - 'views/propertyeditors/datepicker/bootstrap-datetimepicker.min.css' - ); - }); \ No newline at end of file + }); + + assetsService.loadCss( + 'views/propertyeditors/datepicker/bootstrap-datetimepicker.min.css' + ); +} + +angular.module("umbraco").controller("Umbraco.PropertyEditors.DatepickerController", dateTimePickerController); diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/datepicker/datepicker.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/datepicker/datepicker.html index d0f8ce915f..c5ddf7e967 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/datepicker/datepicker.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/datepicker/datepicker.html @@ -1,7 +1,7 @@ -
-
+
+
diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/listview.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/listview.controller.js index bb70bd2a15..4b8b0b99ef 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/listview.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/listview.controller.js @@ -1,213 +1,244 @@ -angular.module("umbraco") - .controller("Umbraco.PropertyEditors.ListViewController", - function ($rootScope, $scope, $routeParams, contentResource, contentTypeResource, notificationsService, iconHelper, dialogService) { +function listViewController($rootScope, $scope, $routeParams, $injector, notificationsService, iconHelper, dialogService) { - $scope.actionInProgress = false; - $scope.listViewResultSet = { - totalPages: 0, - items: [] - }; + //this is a quick check to see if we're in create mode, if so just exit - we cannot show children for content + // that isn't created yet, if we continue this will use the parent id in the route params which isn't what + // we want. NOTE: This is just a safety check since when we scaffold an empty model on the server we remove + // the list view tab entirely when it's new. + if ($routeParams.create) { + $scope.isNew = true; + return; + } - $scope.options = { - pageSize: 10, - pageNumber: 1, - filter: '', - orderBy: 'Id', - orderDirection: "desc" - }; + //Now we need to check if this is for media or content because that will depend on the resources we use + var contentResource, contentTypeResource; + if ($scope.model.config.entityType && $scope.model.config.entityType === "media") { + contentResource = $injector.get('mediaResource'); + contentTypeResource = $injector.get('mediaTypeResource'); + $scope.entityType = "media"; + } + else { + contentResource = $injector.get('contentResource'); + contentTypeResource = $injector.get('contentTypeResource'); + $scope.entityType = "content"; + } + + $scope.isNew = false; + $scope.actionInProgress = false; + $scope.listViewResultSet = { + totalPages: 0, + items: [] + }; + + $scope.options = { + pageSize: 10, + pageNumber: 1, + filter: '', + orderBy: 'Id', + orderDirection: "desc" + }; - $scope.next = function () { - if ($scope.options.pageNumber < $scope.listViewResultSet.totalPages) { - $scope.options.pageNumber++; - $scope.reloadView($scope.contentId); - } - }; + $scope.next = function () { + if ($scope.options.pageNumber < $scope.listViewResultSet.totalPages) { + $scope.options.pageNumber++; + $scope.reloadView($scope.contentId); + } + }; - $scope.goToPage = function (pageNumber) { - $scope.options.pageNumber = pageNumber + 1; - $scope.reloadView($scope.contentId); - }; + $scope.goToPage = function (pageNumber) { + $scope.options.pageNumber = pageNumber + 1; + $scope.reloadView($scope.contentId); + }; - $scope.sort = function (field) { + $scope.sort = function (field) { - $scope.options.orderBy = field; + $scope.options.orderBy = field; - if ($scope.options.orderDirection === "desc") { - $scope.options.orderDirection = "asc"; - } else { - $scope.options.orderDirection = "desc"; - } + if ($scope.options.orderDirection === "desc") { + $scope.options.orderDirection = "asc"; + } else { + $scope.options.orderDirection = "desc"; + } - $scope.reloadView($scope.contentId); - }; + $scope.reloadView($scope.contentId); + }; - $scope.prev = function () { - if ($scope.options.pageNumber > 1) { - $scope.options.pageNumber--; - $scope.reloadView($scope.contentId); - } - }; + $scope.prev = function () { + if ($scope.options.pageNumber > 1) { + $scope.options.pageNumber--; + $scope.reloadView($scope.contentId); + } + }; - /*Loads the search results, based on parameters set in prev,next,sort and so on*/ - /*Pagination is done by an array of objects, due angularJS's funky way of monitoring state - with simple values */ + /*Loads the search results, based on parameters set in prev,next,sort and so on*/ + /*Pagination is done by an array of objects, due angularJS's funky way of monitoring state + with simple values */ - $scope.reloadView = function (id) { - contentResource.getChildren(id, $scope.options).then(function (data) { + $scope.reloadView = function (id) { + contentResource.getChildren(id, $scope.options).then(function (data) { - $scope.listViewResultSet = data; - $scope.pagination = []; + $scope.listViewResultSet = data; + $scope.pagination = []; - for (var i = $scope.listViewResultSet.totalPages - 1; i >= 0; i--) { - $scope.pagination[i] = { index: i, name: i + 1 }; - } - - if ($scope.options.pageNumber > $scope.listViewResultSet.totalPages) { - $scope.options.pageNumber = $scope.listViewResultSet.totalPages; - } - - }); - }; - - $scope.selectAll = function ($event) { - var checkbox = $event.target; - - for (var i = 0; i < $scope.listViewResultSet.items.length; i++) { - var entity = $scope.listViewResultSet.items[i]; - entity.selected = checkbox.checked; - } - }; - - $scope.isSelectedAll = function () { - return _.every($scope.listViewResultSet.items, function (item) { - return item.selected; - }); - }; - - $scope.isAnythingSelected = function () { - return _.some($scope.listViewResultSet.items, function (item) { - return item.selected; - }); - }; - - $scope.getIcon = function (entry) { - return iconHelper.convertFromLegacyIcon(entry.icon); - }; - - $scope.delete = function () { - var selected = _.filter($scope.listViewResultSet.items, function (item) { - return item.selected; - }); - var total = selected.length; - if (total === 0) { - return; - } - - if (confirm("Sure you want to delete?") == true) { - $scope.actionInProgress = true; - $scope.bulkStatus = "Starting with delete"; - var current = 1; - - for (var i = 0; i < selected.length; i++) { - $scope.bulkStatus = "Deleted doc " + current + " out of " + total + " documents"; - contentResource.deleteById(selected[i].id).then(function (data) { - if (current === total) { - notificationsService.success("Bulk action", "Deleted " + total + "documents"); - $scope.bulkStatus = ""; - $scope.reloadView($scope.contentId); - $scope.actionInProgress = false; - } - current++; - }); - } - } - - }; - - $scope.publish = function () { - var selected = _.filter($scope.listViewResultSet.items, function (item) { - return item.selected; - }); - var total = selected.length; - if (total === 0) { - return; - } - - $scope.actionInProgress = true; - $scope.bulkStatus = "Starting with publish"; - var current = 1; - - for (var i = 0; i < selected.length; i++) { - $scope.bulkStatus = "Publishing " + current + " out of " + total + " documents"; - - contentResource.publishById(selected[i].id) - .then(function (content) { - if (current == total) { - notificationsService.success("Bulk action", "Published " + total + "documents"); - $scope.bulkStatus = ""; - $scope.reloadView($scope.contentId); - $scope.actionInProgress = false; - } - current++; - }, function (err) { - - $scope.bulkStatus = ""; - $scope.reloadView($scope.contentId); - $scope.actionInProgress = false; - - //if there are validation errors for publishing then we need to show them - if (err.status === 400 && err.data && err.data.Message) { - notificationsService.error("Publish error", err.data.Message); - } - else { - dialogService.ysodDialog(err); - } - }); - - } - }; - - $scope.unpublish = function () { - var selected = _.filter($scope.listViewResultSet.items, function (item) { - return item.selected; - }); - var total = selected.length; - if (total === 0) { - return; - } - - $scope.actionInProgress = true; - $scope.bulkStatus = "Starting with publish"; - var current = 1; - - for (var i = 0; i < selected.length; i++) { - $scope.bulkStatus = "Unpublishing " + current + " out of " + total + " documents"; - - contentResource.unPublish(selected[i].id) - .then(function (content) { - - if (current == total) { - notificationsService.success("Bulk action", "Published " + total + "documents"); - $scope.bulkStatus = ""; - $scope.reloadView($scope.contentId); - $scope.actionInProgress = false; - } - - current++; - }); - } - }; - - if ($routeParams.id) { - $scope.pagination = new Array(10); - $scope.listViewAllowedTypes = contentTypeResource.getAllowedTypes($routeParams.id); - $scope.reloadView($routeParams.id); - - $scope.contentId = $routeParams.id; + for (var i = $scope.listViewResultSet.totalPages - 1; i >= 0; i--) { + $scope.pagination[i] = { index: i, name: i + 1 }; + } + if ($scope.options.pageNumber > $scope.listViewResultSet.totalPages) { + $scope.options.pageNumber = $scope.listViewResultSet.totalPages; } }); + }; + + $scope.selectAll = function ($event) { + var checkbox = $event.target; + if (!angular.isArray($scope.listViewResultSet.items)) { + return; + } + for (var i = 0; i < $scope.listViewResultSet.items.length; i++) { + var entity = $scope.listViewResultSet.items[i]; + entity.selected = checkbox.checked; + } + }; + + $scope.isSelectedAll = function () { + if (!angular.isArray($scope.listViewResultSet.items)) { + return false; + } + return _.every($scope.listViewResultSet.items, function (item) { + return item.selected; + }); + }; + + $scope.isAnythingSelected = function () { + if (!angular.isArray($scope.listViewResultSet.items)) { + return false; + } + return _.some($scope.listViewResultSet.items, function (item) { + return item.selected; + }); + }; + + $scope.getIcon = function (entry) { + return iconHelper.convertFromLegacyIcon(entry.icon); + }; + + $scope.delete = function () { + var selected = _.filter($scope.listViewResultSet.items, function (item) { + return item.selected; + }); + var total = selected.length; + if (total === 0) { + return; + } + + if (confirm("Sure you want to delete?") == true) { + $scope.actionInProgress = true; + $scope.bulkStatus = "Starting with delete"; + var current = 1; + + for (var i = 0; i < selected.length; i++) { + $scope.bulkStatus = "Deleted doc " + current + " out of " + total + " documents"; + contentResource.deleteById(selected[i].id).then(function (data) { + if (current === total) { + notificationsService.success("Bulk action", "Deleted " + total + "documents"); + $scope.bulkStatus = ""; + $scope.reloadView($scope.contentId); + $scope.actionInProgress = false; + } + current++; + }); + } + } + + }; + + $scope.publish = function () { + var selected = _.filter($scope.listViewResultSet.items, function (item) { + return item.selected; + }); + var total = selected.length; + if (total === 0) { + return; + } + + $scope.actionInProgress = true; + $scope.bulkStatus = "Starting with publish"; + var current = 1; + + for (var i = 0; i < selected.length; i++) { + $scope.bulkStatus = "Publishing " + current + " out of " + total + " documents"; + + contentResource.publishById(selected[i].id) + .then(function (content) { + if (current == total) { + notificationsService.success("Bulk action", "Published " + total + "documents"); + $scope.bulkStatus = ""; + $scope.reloadView($scope.contentId); + $scope.actionInProgress = false; + } + current++; + }, function (err) { + + $scope.bulkStatus = ""; + $scope.reloadView($scope.contentId); + $scope.actionInProgress = false; + + //if there are validation errors for publishing then we need to show them + if (err.status === 400 && err.data && err.data.Message) { + notificationsService.error("Publish error", err.data.Message); + } + else { + dialogService.ysodDialog(err); + } + }); + + } + }; + + $scope.unpublish = function () { + var selected = _.filter($scope.listViewResultSet.items, function (item) { + return item.selected; + }); + var total = selected.length; + if (total === 0) { + return; + } + + $scope.actionInProgress = true; + $scope.bulkStatus = "Starting with publish"; + var current = 1; + + for (var i = 0; i < selected.length; i++) { + $scope.bulkStatus = "Unpublishing " + current + " out of " + total + " documents"; + + contentResource.unPublish(selected[i].id) + .then(function (content) { + + if (current == total) { + notificationsService.success("Bulk action", "Published " + total + "documents"); + $scope.bulkStatus = ""; + $scope.reloadView($scope.contentId); + $scope.actionInProgress = false; + } + + current++; + }); + } + }; + + if ($routeParams.id) { + $scope.pagination = new Array(10); + $scope.listViewAllowedTypes = contentTypeResource.getAllowedTypes($routeParams.id); + $scope.reloadView($routeParams.id); + + $scope.contentId = $routeParams.id; + + } + +} + +angular.module("umbraco").controller("Umbraco.PropertyEditors.ListViewController", listViewController); \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/listview.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/listview.html index 23feb61dc3..4cb351f149 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/listview.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/listview/listview.html @@ -1,103 +1,114 @@ - -
-
- +
+ +
- - - - - - - - - - + - - +
+
-
- - - - - - +
- Name - - Last edited - - Updated by -
- - -
- + + + + + + + - - {{result.name}}{{result.updateDate|date:'medium'}} - - {{result.owner.name}}
+ + + + + + + + + - - - + -
  • - {{$index + 1}} -
  • + + + + + + + - - - -
    + + Name + + Last edited + + Updated by + +
    + + +
    +
    -
    - -
    -
    + -
  • - Next -
  • - - +
    + {{result.name}}{{result.updateDate|date:'medium'}} + + {{result.owner.name}} + +
    -
    \ No newline at end of file + + + +
    +
    + + + + + + +
    +
    \ No newline at end of file diff --git a/src/Umbraco.Web.UI/umbraco/config/lang/en.xml b/src/Umbraco.Web.UI/umbraco/config/lang/en.xml index c45c0d9170..1c7f930e53 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/en.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/en.xml @@ -676,6 +676,11 @@ To manage your website, simply open the umbraco back office and start adding con If you just want to setup simple protection using a single login and password + + + diff --git a/src/Umbraco.Web.UI/umbraco_client/Dialogs/SortDialog.js b/src/Umbraco.Web.UI/umbraco_client/Dialogs/SortDialog.js index 75a84292c3..a7bb1d9f29 100644 --- a/src/Umbraco.Web.UI/umbraco_client/Dialogs/SortDialog.js +++ b/src/Umbraco.Web.UI/umbraco_client/Dialogs/SortDialog.js @@ -43,18 +43,17 @@ }, _saveSort: function() { - var rows = jQuery('#sortableNodes tbody tr'); + var rows = $('#sortableNodes tbody tr'); var sortOrder = ""; $.each(rows, function () { sortOrder += $(this).attr("id").replace("node_", "") + ","; }); - document.getElementById("sortingDone").style.display = 'none'; - document.getElementById("sortArea").style.display = 'none'; - - document.getElementById("loading").style.display = 'block'; - + $("#sortingDone").hide(); + $("#sortArea").hide(); + $("#loading").show(); + var self = this; $.ajax({ @@ -69,9 +68,10 @@ }); }, - _showConfirm: function() { - document.getElementById("loading").style.display = 'none'; - document.getElementById("sortingDone").style.display = 'block'; + _showConfirm: function () { + $(".umb-dialog-footer").hide(); + $("#loading").hide(); + $("#sortingDone").show(); UmbClientMgr.mainTree().reloadActionNode(); }, @@ -101,7 +101,7 @@ }); //setup the drag/drop sorting - $("#sortableNodes").tableDnD({ containment: jQuery("#sortableFrame") }); + $("#sortableNodes").tableDnD({ containment: $("#sortableFrame") }); //wire up the submit button self._opts.submitButton.click(function() { diff --git a/src/Umbraco.Web/Editors/ContentController.cs b/src/Umbraco.Web/Editors/ContentController.cs index 6b3fdb1649..65cc5cf6b6 100644 --- a/src/Umbraco.Web/Editors/ContentController.cs +++ b/src/Umbraco.Web/Editors/ContentController.cs @@ -95,7 +95,10 @@ namespace Umbraco.Web.Editors ///
    /// /// - /// + /// + /// If this is a container type, we'll remove the umbContainerView tab for a new item since + /// it cannot actually list children if it doesn't exist yet. + /// public ContentItemDisplay GetEmpty(string contentTypeAlias, int parentId) { var contentType = Services.ContentTypeService.GetContentType(contentTypeAlias); @@ -105,7 +108,12 @@ namespace Umbraco.Web.Editors } var emptyContent = new Content("", parentId, contentType); - return Mapper.Map(emptyContent); + var mapped = Mapper.Map(emptyContent); + + //remove this tab if it exists: umbContainerView + var containerTab = mapped.Tabs.FirstOrDefault(x => x.Alias == "umbContainerView"); + mapped.Tabs = mapped.Tabs.Except(new[] {containerTab}); + return mapped; } /// @@ -594,10 +602,21 @@ namespace Umbraco.Web.Editors display.AddWarningNotification( ui.Text("publish"), ui.Text("speechBubbles", "contentPublishedFailedByEvent")); + break; + case PublishStatusType.FailedAwaitingRelease: + display.AddWarningNotification( + ui.Text("publish"), + ui.Text("publish", "contentPublishedFailedAwaitingRelease", + new[] + { + string.Format("{0} ({1})", status.ContentItem.Name, status.ContentItem.Id) + }, + UmbracoUser).Trim()); break; case PublishStatusType.FailedHasExpired: - case PublishStatusType.FailedAwaitingRelease: + //TODO: We should add proper error messaging for this! case PublishStatusType.FailedIsTrashed: + //TODO: We should add proper error messaging for this! case PublishStatusType.FailedContentInvalid: display.AddWarningNotification( ui.Text("publish"), diff --git a/src/Umbraco.Web/Editors/MediaController.cs b/src/Umbraco.Web/Editors/MediaController.cs index 844d8d5fc8..321704e828 100644 --- a/src/Umbraco.Web/Editors/MediaController.cs +++ b/src/Umbraco.Web/Editors/MediaController.cs @@ -152,7 +152,12 @@ namespace Umbraco.Web.Editors } var emptyContent = new Core.Models.Media("", parentId, contentType); - return Mapper.Map(emptyContent); + var mapped = Mapper.Map(emptyContent); + + //remove this tab if it exists: umbContainerView + var containerTab = mapped.Tabs.FirstOrDefault(x => x.Alias == "umbContainerView"); + mapped.Tabs = mapped.Tabs.Except(new[] { containerTab }); + return mapped; } /// diff --git a/src/Umbraco.Web/Models/ContentEditing/UserDetail.cs b/src/Umbraco.Web/Models/ContentEditing/UserDetail.cs index 32b890d9fd..1ab322e3f3 100644 --- a/src/Umbraco.Web/Models/ContentEditing/UserDetail.cs +++ b/src/Umbraco.Web/Models/ContentEditing/UserDetail.cs @@ -13,7 +13,7 @@ namespace Umbraco.Web.Models.ContentEditing [DataMember(Name = "locale", IsRequired = true)] [Required] - public string Language { get; set; } + public string Culture { get; set; } /// /// The MD5 lowercase hash of the email which can be used by gravatar diff --git a/src/Umbraco.Web/Models/Mapping/ContentModelMapper.cs b/src/Umbraco.Web/Models/Mapping/ContentModelMapper.cs index a0398328ca..d1bd00d2f3 100644 --- a/src/Umbraco.Web/Models/Mapping/ContentModelMapper.cs +++ b/src/Umbraco.Web/Models/Mapping/ContentModelMapper.cs @@ -100,7 +100,7 @@ namespace Umbraco.Web.Models.Mapping if (content.ContentType.IsContainer) { - TabsAndPropertiesResolver.AddContainerView(display); + TabsAndPropertiesResolver.AddContainerView(display, "content"); } TabsAndPropertiesResolver.MapGenericProperties( diff --git a/src/Umbraco.Web/Models/Mapping/MediaModelMapper.cs b/src/Umbraco.Web/Models/Mapping/MediaModelMapper.cs index 75e3d0098e..a105048f65 100644 --- a/src/Umbraco.Web/Models/Mapping/MediaModelMapper.cs +++ b/src/Umbraco.Web/Models/Mapping/MediaModelMapper.cs @@ -61,12 +61,11 @@ namespace Umbraco.Web.Models.Mapping private static void MapGenericCustomProperties(IMedia media, MediaItemDisplay display) { - /* - * Should this be added? if so we need some changes in the UI tho. + if (media.ContentType.IsContainer) { - TabsAndPropertiesResolver.AddContainerView(display); - }*/ + TabsAndPropertiesResolver.AddContainerView(display, "media"); + } TabsAndPropertiesResolver.MapGenericProperties(media, display); } diff --git a/src/Umbraco.Web/Models/Mapping/TabsAndPropertiesResolver.cs b/src/Umbraco.Web/Models/Mapping/TabsAndPropertiesResolver.cs index 62485daae6..b4fcb412c4 100644 --- a/src/Umbraco.Web/Models/Mapping/TabsAndPropertiesResolver.cs +++ b/src/Umbraco.Web/Models/Mapping/TabsAndPropertiesResolver.cs @@ -108,11 +108,17 @@ namespace Umbraco.Web.Models.Mapping } - internal static void AddContainerView(TabbedContentItem display) + /// + /// Adds the container (listview) tab to the document + /// + /// + /// + /// This must be either 'content' or 'media' + internal static void AddContainerView(TabbedContentItem display, string entityType) where TPersisted : IContentBase { var listViewTab = new Tab(); - listViewTab.Alias = "containerView"; + listViewTab.Alias = "umbContainerView"; listViewTab.Label = ui.Text("content", "childItems"); listViewTab.Id = 25; listViewTab.IsActive = true; @@ -124,7 +130,11 @@ namespace Umbraco.Web.Models.Mapping Label = "", Value = null, View = "listview", - HideLabel = true + HideLabel = true, + Config = new Dictionary + { + {"entityType", entityType} + } }); listViewTab.Properties = listViewProperties; diff --git a/src/Umbraco.Web/Models/Mapping/UserModelMapper.cs b/src/Umbraco.Web/Models/Mapping/UserModelMapper.cs index 0648bba260..a3a3fa1a80 100644 --- a/src/Umbraco.Web/Models/Mapping/UserModelMapper.cs +++ b/src/Umbraco.Web/Models/Mapping/UserModelMapper.cs @@ -4,6 +4,7 @@ using Umbraco.Core; using Umbraco.Core.Models.Mapping; using Umbraco.Core.Models.Membership; using Umbraco.Web.Models.ContentEditing; +using umbraco; namespace Umbraco.Web.Models.Mapping { @@ -14,6 +15,7 @@ namespace Umbraco.Web.Models.Mapping config.CreateMap() .ForMember(detail => detail.UserId, opt => opt.MapFrom(user => GetIntId(user.Id))) .ForMember(detail => detail.UserType, opt => opt.MapFrom(user => user.UserType.Alias)) + .ForMember(detail => detail.Culture, opt => opt.MapFrom(user => ui.Culture(user))) .ForMember( detail => detail.EmailHash, opt => opt.MapFrom(user => user.Email.ToLowerInvariant().Trim().ToMd5())); diff --git a/src/Umbraco.Web/Models/Trees/TreeNodeExtensions.cs b/src/Umbraco.Web/Models/Trees/TreeNodeExtensions.cs index 13f36aa2f8..8e97d527b4 100644 --- a/src/Umbraco.Web/Models/Trees/TreeNodeExtensions.cs +++ b/src/Umbraco.Web/Models/Trees/TreeNodeExtensions.cs @@ -14,6 +14,18 @@ treeNode.AdditionalData[LegacyJsCallbackKey] = jsCallback; } + /// + /// Sets the node style to show that it is currently protected publicly + /// + /// + public static void SetContainerStyle(this TreeNode treeNode) + { + if (treeNode.CssClasses.Contains("is-container") == false) + { + treeNode.CssClasses.Add("is-container"); + } + } + /// /// Sets the node style to show that it is currently protected publicly /// diff --git a/src/Umbraco.Web/PropertyEditors/DateTimePropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/DateTimePropertyEditor.cs index 5e91323585..ee416604d8 100644 --- a/src/Umbraco.Web/PropertyEditors/DateTimePropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/DateTimePropertyEditor.cs @@ -12,7 +12,9 @@ namespace Umbraco.Web.PropertyEditors { _defaultPreVals = new Dictionary { - {"format", "yyyy-MM-dd HH:mm:ss"} + //NOTE: This is very important that we do not use .Net format's there, this format + // is the correct format for the JS picker we are using so you cannot capitalize the HH, they need to be 'hh' + {"format", "yyyy-MM-dd hh:mm:ss"} }; } diff --git a/src/Umbraco.Web/Trees/ApplicationTreeController.cs b/src/Umbraco.Web/Trees/ApplicationTreeController.cs index b16ee8bd00..5b18e5698b 100644 --- a/src/Umbraco.Web/Trees/ApplicationTreeController.cs +++ b/src/Umbraco.Web/Trees/ApplicationTreeController.cs @@ -75,8 +75,12 @@ namespace Umbraco.Web.Trees foreach (var apptree in appTrees) { //return the root nodes for each tree in the app - var rootNode = GetRootForMultipleAppTree(apptree, queryStrings); - collection.Add(rootNode); + var rootNode = GetRootForMultipleAppTree(apptree, queryStrings); + //This could be null if the tree decides not to return it's root (i.e. the member type tree does this when not in umbraco membership mode) + if (rootNode != null) + { + collection.Add(rootNode); + } } var multiTree = SectionRootNode.CreateMultiTreeSectionRoot(rootId, collection); diff --git a/src/Umbraco.Web/Trees/ApplicationTreeExtensions.cs b/src/Umbraco.Web/Trees/ApplicationTreeExtensions.cs index f39c79fe8b..72c58f7e4d 100644 --- a/src/Umbraco.Web/Trees/ApplicationTreeExtensions.cs +++ b/src/Umbraco.Web/Trees/ApplicationTreeExtensions.cs @@ -82,6 +82,12 @@ namespace Umbraco.Web.Trees return Attempt.Fail(xmlTreeNodeAttempt.Exception); } + //the root can potentially be null, in that case we'll just return a null success which means it won't be included + if (xmlTreeNodeAttempt.Result == null) + { + return Attempt.Succeed(null); + } + var legacyController = new LegacyTreeController(xmlTreeNodeAttempt.Result, appTree.Alias, currentSection, urlHelper); var newRoot = legacyController.GetRootNode(formCollection); diff --git a/src/Umbraco.Web/Trees/ContentTreeController.cs b/src/Umbraco.Web/Trees/ContentTreeController.cs index 9be4a93311..2203a8ef89 100644 --- a/src/Umbraco.Web/Trees/ContentTreeController.cs +++ b/src/Umbraco.Web/Trees/ContentTreeController.cs @@ -52,12 +52,21 @@ namespace Umbraco.Web.Trees get { return Security.CurrentUser.StartContentId; } } + /// + /// Gets the tree nodes for the given id + /// + /// + /// + /// + /// + /// If the content item is a container node then we will not return anything + /// protected override TreeNodeCollection PerformGetTreeNodes(string id, FormDataCollection queryStrings) { + var nodes = new TreeNodeCollection(); + var entities = GetChildEntities(id); - var nodes = new TreeNodeCollection(); - foreach (var entity in entities) { var e = (UmbracoEntity)entity; @@ -65,24 +74,25 @@ namespace Umbraco.Web.Trees var allowedUserOptions = GetAllowedUserMenuItemsForNode(e); if (CanUserAccessNode(e, allowedUserOptions)) { - var hasChildren = e.HasChildren; //Special check to see if it ia a container, if so then we'll hide children. - if (entity.AdditionalData.ContainsKey("IsContainer") && entity.AdditionalData["IsContainer"] is bool && (bool) entity.AdditionalData["IsContainer"]) - { - hasChildren = false; - } - + var isContainer = entity.AdditionalData.ContainsKey("IsContainer") + && entity.AdditionalData["IsContainer"] is bool + && (bool) entity.AdditionalData["IsContainer"]; + var node = CreateTreeNode( e.Id.ToInvariantString(), id, queryStrings, e.Name, e.ContentTypeIcon, - hasChildren); + e.HasChildren && (isContainer == false)); node.AdditionalData.Add("contentType", e.ContentTypeAlias); + if (isContainer) + node.SetContainerStyle(); + if (e.IsPublished == false) node.SetNotPublishedStyle(); diff --git a/src/Umbraco.Web/Trees/ContentTreeControllerBase.cs b/src/Umbraco.Web/Trees/ContentTreeControllerBase.cs index 5b9a6181f3..82d9fcb9b2 100644 --- a/src/Umbraco.Web/Trees/ContentTreeControllerBase.cs +++ b/src/Umbraco.Web/Trees/ContentTreeControllerBase.cs @@ -83,6 +83,14 @@ namespace Umbraco.Web.Trees return nodes; } + //before we get the children we need to see if this is a container node + var current = Services.EntityService.Get(int.Parse(id), UmbracoObjectType); + if (current != null && current.AdditionalData.ContainsKey("IsContainer") && current.AdditionalData["IsContainer"] is bool && (bool)current.AdditionalData["IsContainer"]) + { + //no children! + return new TreeNodeCollection(); + } + return PerformGetTreeNodes(id, queryStrings); } diff --git a/src/Umbraco.Web/Trees/MediaTreeController.cs b/src/Umbraco.Web/Trees/MediaTreeController.cs index fa247eb87d..b091fbd89c 100644 --- a/src/Umbraco.Web/Trees/MediaTreeController.cs +++ b/src/Umbraco.Web/Trees/MediaTreeController.cs @@ -55,9 +55,25 @@ namespace Umbraco.Web.Trees foreach (var entity in entities) { var e = (UmbracoEntity)entity; - var node = CreateTreeNode(e.Id.ToInvariantString(), id, queryStrings, e.Name, e.ContentTypeIcon, e.HasChildren); + + //Special check to see if it ia a container, if so then we'll hide children. + var isContainer = entity.AdditionalData.ContainsKey("IsContainer") + && entity.AdditionalData["IsContainer"] is bool + && (bool)entity.AdditionalData["IsContainer"]; + var node = CreateTreeNode( + e.Id.ToInvariantString(), + id, + queryStrings, + e.Name, + e.ContentTypeIcon, + e.HasChildren && (isContainer == false)); + node.AdditionalData.Add("contentType", e.ContentTypeAlias); + + if (isContainer) + node.SetContainerStyle(); + nodes.Add(node); } return nodes; diff --git a/src/Umbraco.Web/UI/Controls/InsertMacroSplitButton.cs b/src/Umbraco.Web/UI/Controls/InsertMacroSplitButton.cs index 4fa9146dbc..5c6db74513 100644 --- a/src/Umbraco.Web/UI/Controls/InsertMacroSplitButton.cs +++ b/src/Umbraco.Web/UI/Controls/InsertMacroSplitButton.cs @@ -137,7 +137,7 @@ namespace Umbraco.Web.UI.Controls private bool DoesMacroHaveParameters(int macroId) { - return ApplicationContext.DatabaseContext.Database.ExecuteScalar(string.Format("select 1 from cmsMacroProperty where macro = {0}", macroId)) == 1; + return ApplicationContext.DatabaseContext.Database.ExecuteScalar(string.Format("SELECT COUNT(*) from cmsMacroProperty where macro = {0}", macroId)) > 0; } } }