diff --git a/build/NuSpecs/UmbracoCms.Core.nuspec b/build/NuSpecs/UmbracoCms.Core.nuspec index 4abb26377b..ce837f7189 100644 --- a/build/NuSpecs/UmbracoCms.Core.nuspec +++ b/build/NuSpecs/UmbracoCms.Core.nuspec @@ -44,37 +44,37 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/build/UmbracoVersion.txt b/build/UmbracoVersion.txt index cb641006d1..0a909cc729 100644 --- a/build/UmbracoVersion.txt +++ b/build/UmbracoVersion.txt @@ -1,3 +1,3 @@ # Usage: on line 2 put the release version, on line 3 put the version comment (example: beta) 7.6.0 -alpha060 +alpha062 \ No newline at end of file diff --git a/src/SolutionInfo.cs b/src/SolutionInfo.cs index 8288c293e4..e7d7b208fc 100644 --- a/src/SolutionInfo.cs +++ b/src/SolutionInfo.cs @@ -12,4 +12,4 @@ using System.Resources; [assembly: AssemblyVersion("1.0.*")] [assembly: AssemblyFileVersion("7.6.0")] -[assembly: AssemblyInformationalVersion("7.6.0-alpha060")] \ No newline at end of file +[assembly: AssemblyInformationalVersion("7.6.0-alpha062")] \ No newline at end of file diff --git a/src/Umbraco.Core/Configuration/UmbracoVersion.cs b/src/Umbraco.Core/Configuration/UmbracoVersion.cs index a21c9b7bab..9e325c9afd 100644 --- a/src/Umbraco.Core/Configuration/UmbracoVersion.cs +++ b/src/Umbraco.Core/Configuration/UmbracoVersion.cs @@ -24,7 +24,7 @@ namespace Umbraco.Core.Configuration /// Gets the version comment (like beta or RC). /// /// The version comment. - public static string CurrentComment { get { return "alpha060"; } } + public static string CurrentComment { get { return "alpha062"; } } // Get the version of the umbraco.dll by looking at a class in that dll // Had to do it like this due to medium trust issues, see: http://haacked.com/archive/2010/11/04/assembly-location-and-medium-trust.aspx diff --git a/src/Umbraco.Core/Constants-DeployEntityType.cs b/src/Umbraco.Core/Constants-DeployEntityType.cs index 0f91a3c086..a62ca3d8ef 100644 --- a/src/Umbraco.Core/Constants-DeployEntityType.cs +++ b/src/Umbraco.Core/Constants-DeployEntityType.cs @@ -40,8 +40,8 @@ namespace Umbraco.Core // forms public const string FormsForm = "forms-form"; - public const string FormsWorkflow = "forms-workflow"; - public const string FormsRecord = "forms-record"; + public const string FormsPreValue = "forms-prevalue"; + public const string FormsDataSource = "forms-datasource"; // string entity types @@ -89,10 +89,10 @@ namespace Umbraco.Core return RelationType; case UmbracoObjectTypes.FormsForm: return FormsForm; - case UmbracoObjectTypes.FormsWorkflow: - return FormsWorkflow; - case UmbracoObjectTypes.FormsRecord: - return FormsRecord; + case UmbracoObjectTypes.FormsPreValue: + return FormsPreValue; + case UmbracoObjectTypes.FormsDataSource: + return FormsDataSource; } throw new NotSupportedException(string.Format("UmbracoObjectType \"{0}\" does not have a matching EntityType.", umbracoObjectType)); } @@ -131,10 +131,10 @@ namespace Umbraco.Core return UmbracoObjectTypes.RelationType; case FormsForm: return UmbracoObjectTypes.FormsForm; - case FormsWorkflow: - return UmbracoObjectTypes.FormsWorkflow; - case FormsRecord: - return UmbracoObjectTypes.FormsRecord; + case FormsPreValue: + return UmbracoObjectTypes.FormsPreValue; + case FormsDataSource: + return UmbracoObjectTypes.FormsDataSource; } throw new NotSupportedException( string.Format("EntityType \"{0}\" does not have a matching UmbracoObjectType.", entityType)); diff --git a/src/Umbraco.Core/Constants-ObjectTypes.cs b/src/Umbraco.Core/Constants-ObjectTypes.cs index 7bba427f12..d364c1379c 100644 --- a/src/Umbraco.Core/Constants-ObjectTypes.cs +++ b/src/Umbraco.Core/Constants-ObjectTypes.cs @@ -174,24 +174,24 @@ namespace Umbraco.Core public static readonly Guid FormsFormGuid = new Guid(FormsForm); /// - /// Guid for a Forms Workflow. + /// Guid for a Forms PreValue Source. /// - public const string FormsWorkflow = "42D7BF9B-A362-4FEE-B45A-674D5C064B70"; + public const string FormsPreValue = "42D7BF9B-A362-4FEE-B45A-674D5C064B70"; /// - /// Guid for a Forms Workflow. + /// Guid for a Forms PreValue Source. /// - public static readonly Guid FormsWorkflowGuid = new Guid(FormsWorkflow); + public static readonly Guid FormsPreValueGuid = new Guid(FormsPreValue); /// - /// Guid for a Forms Record. + /// Guid for a Forms DataSource. /// - public const string FormsRecord = "CFED6CE4-9359-443E-9977-9956FEB1D867"; + public const string FormsDataSource = "CFED6CE4-9359-443E-9977-9956FEB1D867"; /// - /// Guid for a Forms Record. + /// Guid for a Forms DataSource. /// - public static readonly Guid FormsRecordGuid = new Guid(FormsRecord); + public static readonly Guid FormsDataSourceGuid = new Guid(FormsDataSource); } } } \ No newline at end of file diff --git a/src/Umbraco.Core/Events/ImportPackageEventArgs.cs b/src/Umbraco.Core/Events/ImportPackageEventArgs.cs index 506481a79e..231e58c07e 100644 --- a/src/Umbraco.Core/Events/ImportPackageEventArgs.cs +++ b/src/Umbraco.Core/Events/ImportPackageEventArgs.cs @@ -23,6 +23,11 @@ namespace Umbraco.Core.Events { get { return _packageMetaData; } } + + public IEnumerable InstallationSummary + { + get { return EventObject; } + } public bool Equals(ImportPackageEventArgs other) { diff --git a/src/Umbraco.Core/Events/UninstallPackageEventArgs.cs b/src/Umbraco.Core/Events/UninstallPackageEventArgs.cs new file mode 100644 index 0000000000..324867a8f7 --- /dev/null +++ b/src/Umbraco.Core/Events/UninstallPackageEventArgs.cs @@ -0,0 +1,31 @@ +using System.Collections.Generic; +using Umbraco.Core.Packaging.Models; + +namespace Umbraco.Core.Events +{ + internal class UninstallPackageEventArgs : CancellableObjectEventArgs> + { + private readonly MetaData _packageMetaData; + + public UninstallPackageEventArgs(TEntity eventObject, bool canCancel) + : base(new[] { eventObject }, canCancel) + { + } + + public UninstallPackageEventArgs(TEntity eventObject, MetaData packageMetaData) + : base(new[] { eventObject }) + { + _packageMetaData = packageMetaData; + } + + public MetaData PackageMetaData + { + get { return _packageMetaData; } + } + + public IEnumerable UninstallationSummary + { + get { return EventObject; } + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/IO/IOHelper.cs b/src/Umbraco.Core/IO/IOHelper.cs index 286acf0285..2ee0463435 100644 --- a/src/Umbraco.Core/IO/IOHelper.cs +++ b/src/Umbraco.Core/IO/IOHelper.cs @@ -4,11 +4,11 @@ using System.Globalization; using System.Reflection; using System.IO; using System.Configuration; +using System.Linq; using System.Web; using System.Text.RegularExpressions; using System.Web.Hosting; using Umbraco.Core.Configuration; -using Umbraco.Core.Logging; namespace Umbraco.Core.IO { @@ -351,7 +351,54 @@ namespace Umbraco.Core.IO writer.Write(contents); } } - - } + + } + + /// + /// Checks if a given path is a full path including drive letter + /// + /// + /// + // From: http://stackoverflow.com/a/35046453/5018 + internal static bool IsFullPath(this string path) + { + return string.IsNullOrWhiteSpace(path) == false + && path.IndexOfAny(Path.GetInvalidPathChars().ToArray()) == -1 + && Path.IsPathRooted(path) + && Path.GetPathRoot(path).Equals(Path.DirectorySeparatorChar.ToString(), StringComparison.Ordinal) == false; + } + + /// + /// Get properly formatted relative path from an existing absolute or relative path + /// + /// + /// + internal static string GetRelativePath(this string path) + { + if (path.IsFullPath()) + { + var rootDirectory = GetRootDirectorySafe(); + var relativePath = path.ToLowerInvariant().Replace(rootDirectory.ToLowerInvariant(), string.Empty); + path = relativePath; + } + + return path.EnsurePathIsApplicationRootPrefixed(); + } + + /// + /// Ensures that a path has `~/` as prefix + /// + /// + /// + internal static string EnsurePathIsApplicationRootPrefixed(this string path) + { + if (path.StartsWith("~/")) + return path; + if (path.StartsWith("/") == false && path.StartsWith("\\") == false) + path = string.Format("/{0}", path); + if (path.StartsWith("~") == false) + path = string.Format("~{0}", path); + return path; + } } } diff --git a/src/Umbraco.Core/Models/Rdbms/MemberDto.cs b/src/Umbraco.Core/Models/Rdbms/MemberDto.cs index e5f7b3f17c..cbe9f909f8 100644 --- a/src/Umbraco.Core/Models/Rdbms/MemberDto.cs +++ b/src/Umbraco.Core/Models/Rdbms/MemberDto.cs @@ -22,6 +22,7 @@ namespace Umbraco.Core.Models.Rdbms [Column("LoginName")] [Length(1000)] [Constraint(Default = "''")] + [Index(IndexTypes.NonClustered, Name = "IX_cmsMember_LoginName")] public string LoginName { get; set; } [Column("Password")] diff --git a/src/Umbraco.Core/Models/Rdbms/RelationDto.cs b/src/Umbraco.Core/Models/Rdbms/RelationDto.cs index 368904a5cb..d3d741a191 100644 --- a/src/Umbraco.Core/Models/Rdbms/RelationDto.cs +++ b/src/Umbraco.Core/Models/Rdbms/RelationDto.cs @@ -16,6 +16,7 @@ namespace Umbraco.Core.Models.Rdbms [Column("parentId")] [ForeignKey(typeof(NodeDto), Name = "FK_umbracoRelation_umbracoNode")] + [Index(IndexTypes.UniqueNonClustered, Name = "IX_umbracoRelation_parentChildType", ForColumns = "parentId,childId,relType")] public int ParentId { get; set; } [Column("childId")] diff --git a/src/Umbraco.Core/Models/Rdbms/RelationTypeDto.cs b/src/Umbraco.Core/Models/Rdbms/RelationTypeDto.cs index 54052f58a3..d13ce33520 100644 --- a/src/Umbraco.Core/Models/Rdbms/RelationTypeDto.cs +++ b/src/Umbraco.Core/Models/Rdbms/RelationTypeDto.cs @@ -29,11 +29,13 @@ namespace Umbraco.Core.Models.Rdbms public Guid ChildObjectType { get; set; } [Column("name")] + [Index(IndexTypes.UniqueNonClustered, Name = "IX_umbracoRelationType_name")] public string Name { get; set; } [Column("alias")] [NullSetting(NullSetting = NullSettings.Null)] [Length(100)] + [Index(IndexTypes.UniqueNonClustered, Name = "IX_umbracoRelationType_alias")] public string Alias { get; set; } } } \ No newline at end of file diff --git a/src/Umbraco.Core/Models/UmbracoObjectTypes.cs b/src/Umbraco.Core/Models/UmbracoObjectTypes.cs index b7bf07af34..68e6e1a815 100644 --- a/src/Umbraco.Core/Models/UmbracoObjectTypes.cs +++ b/src/Umbraco.Core/Models/UmbracoObjectTypes.cs @@ -153,17 +153,17 @@ namespace Umbraco.Core.Models FormsForm, /// - /// Forms Workflow + /// Forms PreValue /// - [UmbracoObjectType(Constants.ObjectTypes.FormsWorkflow)] - [FriendlyName("Workflow")] - FormsWorkflow, + [UmbracoObjectType(Constants.ObjectTypes.FormsPreValue)] + [FriendlyName("PreValue")] + FormsPreValue, /// - /// Forms Record + /// Forms DataSource /// - [UmbracoObjectType(Constants.ObjectTypes.FormsRecord)] - [FriendlyName("Record")] - FormsRecord + [UmbracoObjectType(Constants.ObjectTypes.FormsDataSource)] + [FriendlyName("DataSource")] + FormsDataSource } } \ No newline at end of file diff --git a/src/Umbraco.Core/Packaging/Models/UninstallationSummary.cs b/src/Umbraco.Core/Packaging/Models/UninstallationSummary.cs new file mode 100644 index 0000000000..13bb4bfc77 --- /dev/null +++ b/src/Umbraco.Core/Packaging/Models/UninstallationSummary.cs @@ -0,0 +1,42 @@ +using System; +using System.Collections.Generic; +using System.Runtime.Serialization; +using Umbraco.Core.Models; + +namespace Umbraco.Core.Packaging.Models +{ + [Serializable] + [DataContract(IsReference = true)] + internal class UninstallationSummary + { + public MetaData MetaData { get; set; } + public IEnumerable DataTypesUninstalled { get; set; } + public IEnumerable LanguagesUninstalled { get; set; } + public IEnumerable DictionaryItemsUninstalled { get; set; } + public IEnumerable MacrosUninstalled { get; set; } + public IEnumerable FilesUninstalled { get; set; } + public IEnumerable TemplatesUninstalled { get; set; } + public IEnumerable ContentTypesUninstalled { get; set; } + public IEnumerable StylesheetsUninstalled { get; set; } + public IEnumerable ContentUninstalled { get; set; } + public bool PackageUninstalled { get; set; } + } + + internal static class UninstallationSummaryExtentions + { + public static UninstallationSummary InitEmpty(this UninstallationSummary summary) + { + summary.ContentUninstalled = new List(); + summary.ContentTypesUninstalled = new List(); + summary.DataTypesUninstalled = new List(); + summary.DictionaryItemsUninstalled = new List(); + summary.FilesUninstalled = new List(); + summary.LanguagesUninstalled = new List(); + summary.MacrosUninstalled = new List(); + summary.MetaData = new MetaData(); + summary.TemplatesUninstalled = new List(); + summary.PackageUninstalled = false; + return summary; + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Factories/ContentFactory.cs b/src/Umbraco.Core/Persistence/Factories/ContentFactory.cs index 88de5a1d6c..0532eab6b1 100644 --- a/src/Umbraco.Core/Persistence/Factories/ContentFactory.cs +++ b/src/Umbraco.Core/Persistence/Factories/ContentFactory.cs @@ -27,7 +27,20 @@ namespace Umbraco.Core.Persistence.Factories #region Implementation of IEntityFactory - public static IContent BuildEntity(DocumentDto dto, IContentType contentType) + /// + /// Builds a IContent item from the dto(s) and content type + /// + /// + /// This DTO can contain all of the information to build an IContent item, however in cases where multiple entities are being built, + /// a separate publishedDto entity will be supplied in place of the 's own + /// ResultColumn DocumentPublishedReadOnlyDto + /// + /// + /// + /// When querying for multiple content items the main DTO will not contain the ResultColumn DocumentPublishedReadOnlyDto and a separate publishedDto instance will be supplied + /// + /// + public static IContent BuildEntity(DocumentDto dto, IContentType contentType, DocumentPublishedReadOnlyDto publishedDto = null) { var content = new Content(dto.Text, dto.ContentVersionDto.ContentDto.NodeDto.ParentId, contentType); @@ -52,8 +65,13 @@ namespace Umbraco.Core.Persistence.Factories content.ExpireDate = dto.ExpiresDate.HasValue ? dto.ExpiresDate.Value : (DateTime?)null; content.ReleaseDate = dto.ReleaseDate.HasValue ? dto.ReleaseDate.Value : (DateTime?)null; content.Version = dto.ContentVersionDto.VersionId; + content.PublishedState = dto.Published ? PublishedState.Published : PublishedState.Unpublished; - content.PublishedVersionGuid = dto.DocumentPublishedReadOnlyDto == null ? default(Guid) : dto.DocumentPublishedReadOnlyDto.VersionId; + + //Check if the publishedDto has been supplied, if not the use the dto's own DocumentPublishedReadOnlyDto value + content.PublishedVersionGuid = publishedDto == null + ? (dto.DocumentPublishedReadOnlyDto == null ? default(Guid) : dto.DocumentPublishedReadOnlyDto.VersionId) + : publishedDto.VersionId; //on initial construction we don't want to have dirty properties tracked // http://issues.umbraco.org/issue/U4-1946 diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenSixZero/AddIndexToCmsMemberLoginName.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenSixZero/AddIndexToCmsMemberLoginName.cs new file mode 100644 index 0000000000..beff4b3210 --- /dev/null +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenSixZero/AddIndexToCmsMemberLoginName.cs @@ -0,0 +1,42 @@ +using System.Linq; +using Umbraco.Core.Logging; +using Umbraco.Core.Persistence.DatabaseModelDefinitions; +using Umbraco.Core.Persistence.SqlSyntax; + +namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSevenSixZero +{ + [Migration("7.6.0", 0, Constants.System.UmbracoMigrationName)] + public class AddIndexToCmsMemberLoginName : MigrationBase + { + public AddIndexToCmsMemberLoginName(ISqlSyntaxProvider sqlSyntax, ILogger logger) + : base(sqlSyntax, logger) + { } + + public override void Up() + { + var dbIndexes = SqlSyntax.GetDefinedIndexes(Context.Database) + .Select(x => new DbIndexDefinition() + { + TableName = x.Item1, + IndexName = x.Item2, + ColumnName = x.Item3, + IsUnique = x.Item4 + }).ToArray(); + + //make sure it doesn't already exist + if (dbIndexes.Any(x => x.IndexName.InvariantEquals("IX_cmsMember_LoginName")) == false) + { + Create.Index("IX_cmsMember_LoginName").OnTable("cmsMember") + .OnColumn("LoginName") + .Ascending() + .WithOptions() + .NonClustered(); + } + } + + public override void Down() + { + Delete.Index("IX_cmsMember_LoginName").OnTable("cmsMember"); + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenSixZero/AddIndexToUmbracoNodePath.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenSixZero/AddIndexToUmbracoNodePath.cs index 229311dd9f..e59252390a 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenSixZero/AddIndexToUmbracoNodePath.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenSixZero/AddIndexToUmbracoNodePath.cs @@ -14,14 +14,7 @@ namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSevenSixZero public override void Up() { - var dbIndexes = SqlSyntax.GetDefinedIndexes(Context.Database) - .Select(x => new DbIndexDefinition() - { - TableName = x.Item1, - IndexName = x.Item2, - ColumnName = x.Item3, - IsUnique = x.Item4 - }).ToArray(); + var dbIndexes = SqlSyntax.GetDefinedIndexesDefinitions(Context.Database); //make sure it doesn't already exist if (dbIndexes.Any(x => x.IndexName.InvariantEquals("IX_umbracoNodePath")) == false) diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenSixZero/AddIndexesToUmbracoRelationTables.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenSixZero/AddIndexesToUmbracoRelationTables.cs new file mode 100644 index 0000000000..b8c0d78ef1 --- /dev/null +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenSixZero/AddIndexesToUmbracoRelationTables.cs @@ -0,0 +1,91 @@ +using System; +using System.Linq; +using Umbraco.Core.Logging; +using Umbraco.Core.Persistence.SqlSyntax; + +namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSevenSixZero +{ + [Migration("7.6.0", 0, Constants.System.UmbracoMigrationName)] + public class AddIndexesToUmbracoRelationTables : MigrationBase + { + public AddIndexesToUmbracoRelationTables(ISqlSyntaxProvider sqlSyntax, ILogger logger) + : base(sqlSyntax, logger) + { } + + public override void Up() + { + var dbIndexes = SqlSyntax.GetDefinedIndexesDefinitions(Context.Database).ToArray(); + + //make sure it doesn't already exist + if (dbIndexes.Any(x => x.IndexName.InvariantEquals("IX_umbracoRelation_parentChildType")) == false) + { + //This will remove any corrupt/duplicate data in the relation table before the index is applied + //Ensure this executes in a defered block which will be done inside of the migration transaction + this.Execute.Code(database => + { + //We need to check if this index has corrupted data and then clear that data + var duplicates = database.Fetch("SELECT parentId,childId,relType FROM umbracoRelation GROUP BY parentId,childId,relType HAVING COUNT(*) > 1"); + if (duplicates.Count > 0) + { + //need to fix this there cannot be duplicates so we'll take the latest entries, it's really not going to matter though + foreach (var duplicate in duplicates) + { + var ids = database.Fetch("SELECT id FROM umbracoRelation WHERE parentId=@parentId AND childId=@childId AND relType=@relType ORDER BY datetime DESC", + new { parentId = duplicate.parentId, childId = duplicate.childId, relType = duplicate.relType }); + + if (ids.Count == 1) + { + //this is just a safety check, this should absolutely never happen + throw new InvalidOperationException("Duplicates were detected but could not be discovered"); + } + + //delete the others + ids = ids.Skip(0).ToList(); + + //iterate in groups of 2000 to avoid the max sql parameter limit + foreach (var idGroup in ids.InGroupsOf(2000)) + { + database.Execute("DELETE FROM umbracoRelation WHERE id IN (@ids)", new { ids = idGroup }); + } + } + } + return ""; + }); + + //unique index to prevent duplicates - and for better perf + Create.Index("IX_umbracoRelation_parentChildType").OnTable("umbracoRelation") + .OnColumn("parentId").Ascending() + .OnColumn("childId").Ascending() + .OnColumn("relType").Ascending() + .WithOptions() + .Unique(); + } + + //need indexes on alias and name for relation type since these are queried against + + //make sure it doesn't already exist + if (dbIndexes.Any(x => x.IndexName.InvariantEquals("IX_umbracoRelationType_alias")) == false) + { + Create.Index("IX_umbracoRelationType_alias").OnTable("umbracoRelationType") + .OnColumn("alias") + .Ascending() + .WithOptions() + .NonClustered(); + } + if (dbIndexes.Any(x => x.IndexName.InvariantEquals("IX_umbracoRelationType_name")) == false) + { + Create.Index("IX_umbracoRelationType_name").OnTable("umbracoRelationType") + .OnColumn("name") + .Ascending() + .WithOptions() + .NonClustered(); + } + + } + + public override void Down() + { + Delete.Index("IX_umbracoNodePath").OnTable("umbracoNode"); + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Repositories/BaseQueryType.cs b/src/Umbraco.Core/Persistence/Repositories/BaseQueryType.cs index 4579fd98fb..05061c47af 100644 --- a/src/Umbraco.Core/Persistence/Repositories/BaseQueryType.cs +++ b/src/Umbraco.Core/Persistence/Repositories/BaseQueryType.cs @@ -2,8 +2,30 @@ namespace Umbraco.Core.Persistence.Repositories { internal enum BaseQueryType { - Full, + /// + /// A query to return all information for a single item + /// + /// + /// In some cases this will be the same as + /// + FullSingle, + + /// + /// A query to return all information for multiple items + /// + /// + /// In some cases this will be the same as + /// + FullMultiple, + + /// + /// A query to return the ids for items + /// Ids, + + /// + /// A query to return the count for items + /// Count } } \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs b/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs index 044e2a95e8..78128d2b7d 100644 --- a/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/ContentRepository.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Collections.ObjectModel; using System.Globalization; using System.Linq; using System.Xml; @@ -53,7 +54,7 @@ namespace Umbraco.Core.Persistence.Repositories protected override IContent PerformGet(int id) { - var sql = GetBaseQuery(false) + var sql = GetBaseQuery(BaseQueryType.FullSingle) .Where(GetBaseWhereClause(), new { Id = id }) .Where(x => x.Newest, SqlSyntax) .OrderByDescending(x => x.VersionDate, SqlSyntax); @@ -81,15 +82,15 @@ namespace Umbraco.Core.Persistence.Repositories return s; }; - var sqlBaseFull = GetBaseQuery(BaseQueryType.Full); + var sqlBaseFull = GetBaseQuery(BaseQueryType.FullMultiple); var sqlBaseIds = GetBaseQuery(BaseQueryType.Ids); - return ProcessQuery(translate(sqlBaseFull), translate(sqlBaseIds)); + return ProcessQuery(translate(sqlBaseFull), new PagingSqlQuery(translate(sqlBaseIds))); } protected override IEnumerable PerformGetByQuery(IQuery query) { - var sqlBaseFull = GetBaseQuery(BaseQueryType.Full); + var sqlBaseFull = GetBaseQuery(BaseQueryType.FullMultiple); var sqlBaseIds = GetBaseQuery(BaseQueryType.Ids); Func, Sql> translate = (translator) => @@ -103,13 +104,25 @@ namespace Umbraco.Core.Persistence.Repositories var translatorFull = new SqlTranslator(sqlBaseFull, query); var translatorIds = new SqlTranslator(sqlBaseIds, query); - return ProcessQuery(translate(translatorFull), translate(translatorIds)); + return ProcessQuery(translate(translatorFull), new PagingSqlQuery(translate(translatorIds))); } #endregion #region Overrides of PetaPocoRepositoryBase + /// + /// Returns the base query to return Content + /// + /// + /// + /// + /// Content queries will differ depending on what needs to be returned: + /// * FullSingle: When querying for a single document, this will include the Outer join to fetch the content item's published version info + /// * FullMultiple: When querying for multiple documents, this will exclude the Outer join to fetch the content item's published version info - this info would need to be fetched separately + /// * Ids: This would essentially be the same as FullMultiple however the columns specified will only return the Ids for the documents + /// * Count: A query to return the count for documents + /// protected override Sql GetBaseQuery(BaseQueryType queryType) { var sql = new Sql(); @@ -122,14 +135,14 @@ namespace Umbraco.Core.Persistence.Repositories .InnerJoin(SqlSyntax) .On(SqlSyntax, left => left.NodeId, right => right.NodeId); - if (queryType == BaseQueryType.Full) + if (queryType == BaseQueryType.FullSingle) { //The only reason we apply this left outer join is to be able to pull back the DocumentPublishedReadOnlyDto //information with the entire data set, so basically this will get both the latest document and also it's published - //version if it has one. When performing a count or when just retrieving Ids like in paging, this is unecessary + //version if it has one. When performing a count or when retrieving Ids like in paging, this is unecessary //and causes huge performance overhead for the SQL server, especially when sorting the result. - //To fix this perf overhead we'd need another index on : - // CREATE NON CLUSTERED INDEX ON cmsDocument.node + cmsDocument.published + //We also don't include this outer join when querying for multiple entities since it is much faster to fetch this information + //in a separate query. For a single entity this is ok. var sqlx = string.Format("LEFT OUTER JOIN {0} {1} ON ({1}.{2}={0}.{2} AND {1}.{3}=1)", SqlSyntax.GetQuotedTableName("cmsDocument"), @@ -151,7 +164,7 @@ namespace Umbraco.Core.Persistence.Repositories protected override Sql GetBaseQuery(bool isCount) { - return GetBaseQuery(isCount ? BaseQueryType.Count : BaseQueryType.Full); + return GetBaseQuery(isCount ? BaseQueryType.Count : BaseQueryType.FullSingle); } protected override string GetBaseWhereClause() @@ -222,10 +235,10 @@ namespace Umbraco.Core.Persistence.Repositories while (true) { // get the next group of nodes - var sqlFull = translate(baseId, GetBaseQuery(BaseQueryType.Full)); + var sqlFull = translate(baseId, GetBaseQuery(BaseQueryType.FullMultiple)); var sqlIds = translate(baseId, GetBaseQuery(BaseQueryType.Ids)); - var xmlItems = ProcessQuery(SqlSyntax.SelectTop(sqlFull, groupSize), SqlSyntax.SelectTop(sqlIds, groupSize)) + var xmlItems = ProcessQuery(SqlSyntax.SelectTop(sqlFull, groupSize), new PagingSqlQuery(SqlSyntax.SelectTop(sqlIds, groupSize))) .Select(x => new ContentXmlDto { NodeId = x.Id, Xml = serializer(x).ToString() }) .ToList(); @@ -245,7 +258,7 @@ namespace Umbraco.Core.Persistence.Repositories Logger.Error("Could not rebuild XML for nodeId=" + xmlItem.NodeId, e); } } - baseId = xmlItems.Last().NodeId; + baseId = xmlItems[xmlItems.Count - 1].NodeId; } } @@ -257,15 +270,15 @@ namespace Umbraco.Core.Persistence.Repositories .OrderByDescending(x => x.VersionDate, SqlSyntax); }; - var sqlFull = translate(GetBaseQuery(BaseQueryType.Full)); + var sqlFull = translate(GetBaseQuery(BaseQueryType.FullMultiple)); var sqlIds = translate(GetBaseQuery(BaseQueryType.Ids)); - return ProcessQuery(sqlFull, sqlIds, true); + return ProcessQuery(sqlFull, new PagingSqlQuery(sqlIds), true); } public override IContent GetByVersion(Guid versionId) { - var sql = GetBaseQuery(false); + var sql = GetBaseQuery(BaseQueryType.FullSingle); sql.Where("cmsContentVersion.VersionId = @VersionId", new { VersionId = versionId }); sql.OrderByDescending(x => x.VersionDate, SqlSyntax); @@ -675,12 +688,12 @@ namespace Umbraco.Core.Persistence.Repositories // ORDER BY substring(path, 1, len(path) - charindex(',', reverse(path))), sortOrder // but that's probably an overkill - sorting by level,sortOrder should be enough - var sqlFull = GetBaseQuery(BaseQueryType.Full); + var sqlFull = GetBaseQuery(BaseQueryType.FullMultiple); var translatorFull = new SqlTranslator(sqlFull, query); var sqlIds = GetBaseQuery(BaseQueryType.Ids); var translatorIds = new SqlTranslator(sqlIds, query); - return ProcessQuery(translate(translatorFull), translate(translatorIds), true); + return ProcessQuery(translate(translatorFull), new PagingSqlQuery(translate(translatorIds)), true); } /// @@ -860,7 +873,7 @@ order by umbracoNode.{2}, umbracoNode.parentID, umbracoNode.sortOrder", return GetPagedResultsByQuery(query, pageIndex, pageSize, out totalRecords, new Tuple("cmsDocument", "nodeId"), - (sqlFull, sqlIds) => ProcessQuery(sqlFull, sqlIds), orderBy, orderDirection, orderBySystemField, + (sqlFull, pagingSqlQuery) => ProcessQuery(sqlFull, pagingSqlQuery), orderBy, orderDirection, orderBySystemField, filterCallback); } @@ -890,23 +903,58 @@ order by umbracoNode.{2}, umbracoNode.parentID, umbracoNode.sortOrder", return base.GetDatabaseFieldNameForOrderBy(orderBy); } - + /// /// This is the underlying method that processes most queries for this repository /// /// - /// The full SQL with the outer join to return all data required to create an IContent + /// The FullMultiple SQL without the outer join to return all data required to create an IContent excluding it's published state data which this will query separately /// - /// + /// /// The Id SQL without the outer join to just return all document ids - used to process the properties for the content item /// /// /// - private IEnumerable ProcessQuery(Sql sqlFull, Sql sqlIds, bool withCache = false) + private IEnumerable ProcessQuery(Sql sqlFull, PagingSqlQuery pagingSqlQuery, bool withCache = false) { // fetch returns a list so it's ok to iterate it in this method - var dtos = Database.Fetch(sqlFull); + var dtos = Database.Fetch(sqlFull); if (dtos.Count == 0) return Enumerable.Empty(); + + //Go and get all of the published version data separately for this data, this is because when we are querying + //for multiple content items we don't include the outer join to fetch this data in the same query because + //it is insanely slow. Instead we just fetch the published version data separately in one query. + + //we need to parse the original SQL statement and reduce the columns to just cmsDocument.nodeId so that we can use + // the statement to go get the published data for all of the items by using an inner join + var parsedOriginalSql = "SELECT cmsDocument.nodeId " + sqlFull.SQL.Substring(sqlFull.SQL.IndexOf("FROM", StringComparison.Ordinal)); + //now remove everything from an Orderby clause and beyond + if (parsedOriginalSql.InvariantContains("ORDER BY ")) + { + parsedOriginalSql = parsedOriginalSql.Substring(0, parsedOriginalSql.LastIndexOf("ORDER BY ", StringComparison.Ordinal)); + } + + var publishedSql = new Sql(@"SELECT * +FROM cmsDocument AS doc2 +INNER JOIN + (" + parsedOriginalSql + @") as docData +ON doc2.nodeId = docData.nodeId +WHERE doc2.published = 1 +ORDER BY doc2.nodeId +", sqlFull.Arguments); + + //go and get the published version data, we do a Query here and not a Fetch so we are + //not allocating a whole list to memory just to allocate another list in memory since + //we are assigning this data to a keyed collection for fast lookup below + var publishedData = Database.Query(publishedSql); + var publishedDataCollection = new DocumentPublishedReadOnlyDtoCollection(); + foreach (var publishedDto in publishedData) + { + //double check that there's no corrupt db data, there should only be a single published item + if (publishedDataCollection.Contains(publishedDto.NodeId) == false) + publishedDataCollection.Add(publishedDto); + } + var content = new IContent[dtos.Count]; var defs = new List(); @@ -920,6 +968,8 @@ order by umbracoNode.{2}, umbracoNode.parentID, umbracoNode.sortOrder", for (var i = 0; i < dtos.Count; i++) { var dto = dtos[i]; + DocumentPublishedReadOnlyDto publishedDto; + publishedDataCollection.TryGetValue(dto.NodeId, out publishedDto); // if the cache contains the published version, use it if (withCache) @@ -947,7 +997,7 @@ order by umbracoNode.{2}, umbracoNode.parentID, umbracoNode.sortOrder", contentTypes[dto.ContentVersionDto.ContentDto.ContentTypeId] = contentType; } - content[i] = ContentFactory.BuildEntity(dto, contentType); + content[i] = ContentFactory.BuildEntity(dto, contentType, publishedDto); // need template if (dto.TemplateId.HasValue && dto.TemplateId.Value > 0) @@ -968,7 +1018,7 @@ order by umbracoNode.{2}, umbracoNode.parentID, umbracoNode.sortOrder", .ToDictionary(x => x.Id, x => x); // load all properties for all documents from database in 1 query - var propertyData = GetPropertyCollection(sqlIds, defs); + var propertyData = GetPropertyCollection(pagingSqlQuery, defs); // assign var dtoIndex = 0; @@ -1056,7 +1106,7 @@ order by umbracoNode.{2}, umbracoNode.parentID, umbracoNode.sortOrder", return currentName; } - + /// /// Dispose disposable properties /// @@ -1071,5 +1121,26 @@ order by umbracoNode.{2}, umbracoNode.parentID, umbracoNode.sortOrder", _contentPreviewRepository.Dispose(); _contentXmlRepository.Dispose(); } + + /// + /// A keyed collection for fast lookup when retrieving a separate list of published version data + /// + private class DocumentPublishedReadOnlyDtoCollection : KeyedCollection + { + protected override int GetKeyForItem(DocumentPublishedReadOnlyDto item) + { + return item.NodeId; + } + + public bool TryGetValue(int key, out DocumentPublishedReadOnlyDto val) + { + if (Dictionary == null) + { + val = null; + return false; + } + return Dictionary.TryGetValue(key, out val); + } + } } } \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Repositories/MediaRepository.cs b/src/Umbraco.Core/Persistence/Repositories/MediaRepository.cs index 22b9b3c2e4..4b7afe9000 100644 --- a/src/Umbraco.Core/Persistence/Repositories/MediaRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/MediaRepository.cs @@ -68,7 +68,7 @@ namespace Umbraco.Core.Persistence.Repositories sql.Where("umbracoNode.id in (@ids)", new { ids = ids }); } - return ProcessQuery(sql); + return ProcessQuery(sql, new PagingSqlQuery(sql)); } protected override IEnumerable PerformGetByQuery(IQuery query) @@ -78,7 +78,7 @@ namespace Umbraco.Core.Persistence.Repositories var sql = translator.Translate() .OrderBy(x => x.SortOrder, SqlSyntax); - return ProcessQuery(sql); + return ProcessQuery(sql, new PagingSqlQuery(sql)); } #endregion @@ -100,7 +100,7 @@ namespace Umbraco.Core.Persistence.Repositories protected override Sql GetBaseQuery(bool isCount) { - return GetBaseQuery(isCount ? BaseQueryType.Count : BaseQueryType.Full); + return GetBaseQuery(isCount ? BaseQueryType.Count : BaseQueryType.FullSingle); } protected override string GetBaseWhereClause() @@ -143,13 +143,24 @@ namespace Umbraco.Core.Persistence.Repositories var sql = GetBaseQuery(false) .Where(GetBaseWhereClause(), new { Id = id }) .OrderByDescending(x => x.VersionDate, SqlSyntax); - return ProcessQuery(sql, true); + return ProcessQuery(sql, new PagingSqlQuery(sql), true); } - private IEnumerable ProcessQuery(Sql sql, bool withCache = false) + /// + /// This is the underlying method that processes most queries for this repository + /// + /// + /// The full SQL to select all media data + /// + /// + /// The Id SQL to just return all media ids - used to process the properties for the media item + /// + /// + /// + private IEnumerable ProcessQuery(Sql sqlFull, PagingSqlQuery pagingSqlQuery, bool withCache = false) { // fetch returns a list so it's ok to iterate it in this method - var dtos = Database.Fetch(sql); + var dtos = Database.Fetch(sqlFull); var content = new IMedia[dtos.Count]; var defs = new List(); @@ -200,7 +211,7 @@ namespace Umbraco.Core.Persistence.Repositories } // load all properties for all documents from database in 1 query - var propertyData = GetPropertyCollection(sql, defs); + var propertyData = GetPropertyCollection(pagingSqlQuery, defs); // assign var dtoIndex = 0; @@ -257,7 +268,8 @@ namespace Umbraco.Core.Persistence.Repositories query = query .Where(x => x.NodeId > baseId, SqlSyntax) .OrderBy(x => x.NodeId, SqlSyntax); - var xmlItems = ProcessQuery(SqlSyntax.SelectTop(query, groupSize)) + var sql = SqlSyntax.SelectTop(query, groupSize); + var xmlItems = ProcessQuery(sql, new PagingSqlQuery(sql)) .Select(x => new ContentXmlDto { NodeId = x.Id, Xml = serializer(x).ToString() }) .ToList(); @@ -505,7 +517,7 @@ namespace Umbraco.Core.Persistence.Repositories return GetPagedResultsByQuery(query, pageIndex, pageSize, out totalRecords, new Tuple("cmsContentVersion", "contentId"), - (sqlFull, sqlIds) => ProcessQuery(sqlFull), orderBy, orderDirection, orderBySystemField, + (sqlFull, pagingSqlQuery) => ProcessQuery(sqlFull, pagingSqlQuery), orderBy, orderDirection, orderBySystemField, filterCallback); } @@ -513,7 +525,7 @@ namespace Umbraco.Core.Persistence.Repositories /// /// Private method to create a media object from a ContentDto /// - /// + /// /// /// /// @@ -525,7 +537,7 @@ namespace Umbraco.Core.Persistence.Repositories var docDef = new DocumentDefinition(dto.NodeId, versionId, media.UpdateDate, media.CreateDate, contentType); - var properties = GetPropertyCollection(docSql, new[] { docDef }); + var properties = GetPropertyCollection(new PagingSqlQuery(docSql), new[] { docDef }); media.Properties = properties[dto.NodeId]; diff --git a/src/Umbraco.Core/Persistence/Repositories/MemberRepository.cs b/src/Umbraco.Core/Persistence/Repositories/MemberRepository.cs index d824deae5a..95028f99cd 100644 --- a/src/Umbraco.Core/Persistence/Repositories/MemberRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/MemberRepository.cs @@ -68,7 +68,7 @@ namespace Umbraco.Core.Persistence.Repositories sql.Where("umbracoNode.id in (@ids)", new { ids = ids }); } - return ProcessQuery(sql); + return ProcessQuery(sql, new PagingSqlQuery(sql)); } @@ -90,7 +90,7 @@ namespace Umbraco.Core.Persistence.Repositories baseQuery.Append(new Sql("WHERE umbracoNode.id IN (" + sql.SQL + ")", sql.Arguments)) .OrderBy(x => x.SortOrder); - return ProcessQuery(baseQuery); + return ProcessQuery(baseQuery, new PagingSqlQuery(baseQuery)); } else { @@ -98,7 +98,7 @@ namespace Umbraco.Core.Persistence.Repositories var sql = translator.Translate() .OrderBy(x => x.SortOrder); - return ProcessQuery(sql); + return ProcessQuery(sql, new PagingSqlQuery(sql)); } } @@ -129,7 +129,7 @@ namespace Umbraco.Core.Persistence.Repositories protected override Sql GetBaseQuery(bool isCount) { - return GetBaseQuery(isCount ? BaseQueryType.Count : BaseQueryType.Full); + return GetBaseQuery(isCount ? BaseQueryType.Count : BaseQueryType.FullSingle); } protected override string GetBaseWhereClause() @@ -385,7 +385,7 @@ namespace Umbraco.Core.Persistence.Repositories var sql = GetBaseQuery(false) .Where(GetBaseWhereClause(), new { Id = id }) .OrderByDescending(x => x.VersionDate, SqlSyntax); - return ProcessQuery(sql, true); + return ProcessQuery(sql, new PagingSqlQuery(sql), true); } public void RebuildXmlStructures(Func serializer, int groupSize = 200, IEnumerable contentTypeIds = null) @@ -408,7 +408,8 @@ namespace Umbraco.Core.Persistence.Repositories query = query .Where(x => x.NodeId > baseId) .OrderBy(x => x.NodeId, SqlSyntax); - var xmlItems = ProcessQuery(SqlSyntax.SelectTop(query, groupSize)) + var sql = SqlSyntax.SelectTop(query, groupSize); + var xmlItems = ProcessQuery(sql, new PagingSqlQuery(sql)) .Select(x => new ContentXmlDto { NodeId = x.Id, Xml = serializer(x).ToString() }) .ToList(); @@ -449,7 +450,7 @@ namespace Umbraco.Core.Persistence.Repositories var factory = new MemberFactory(memberType, NodeObjectTypeId, dto.NodeId); var media = factory.BuildEntity(dto); - var properties = GetPropertyCollection(sql, new[] { new DocumentDefinition(dto.NodeId, dto.ContentVersionDto.VersionId, media.UpdateDate, media.CreateDate, memberType) }); + var properties = GetPropertyCollection(new PagingSqlQuery(sql), new[] { new DocumentDefinition(dto.NodeId, dto.ContentVersionDto.VersionId, media.UpdateDate, media.CreateDate, memberType) }); media.Properties = properties[dto.NodeId]; @@ -540,7 +541,7 @@ namespace Umbraco.Core.Persistence.Repositories .OrderByDescending(x => x.VersionDate) .OrderBy(x => x.SortOrder); - return ProcessQuery(sql); + return ProcessQuery(sql, new PagingSqlQuery(sql)); } @@ -601,7 +602,7 @@ namespace Umbraco.Core.Persistence.Repositories return GetPagedResultsByQuery(query, pageIndex, pageSize, out totalRecords, new Tuple("cmsMember", "nodeId"), - (sqlFull, sqlIds) => ProcessQuery(sqlFull), orderBy, orderDirection, orderBySystemField, + (sqlFull, sqlIds) => ProcessQuery(sqlFull, sqlIds), orderBy, orderDirection, orderBySystemField, filterCallback); } @@ -641,10 +642,21 @@ namespace Umbraco.Core.Persistence.Repositories return base.GetEntityPropertyNameForOrderBy(orderBy); } - private IEnumerable ProcessQuery(Sql sql, bool withCache = false) + /// + /// This is the underlying method that processes most queries for this repository + /// + /// + /// The full SQL to select all member data + /// + /// + /// The Id SQL to just return all member ids - used to process the properties for the member item + /// + /// + /// + private IEnumerable ProcessQuery(Sql sqlFull, PagingSqlQuery pagingSqlQuery, bool withCache = false) { // fetch returns a list so it's ok to iterate it in this method - var dtos = Database.Fetch(sql); + var dtos = Database.Fetch(sqlFull); var content = new IMember[dtos.Count]; var defs = new List(); @@ -681,7 +693,7 @@ namespace Umbraco.Core.Persistence.Repositories } // load all properties for all documents from database in 1 query - var propertyData = GetPropertyCollection(sql, defs); + var propertyData = GetPropertyCollection(pagingSqlQuery, defs); // assign var dtoIndex = 0; diff --git a/src/Umbraco.Core/Persistence/Repositories/VersionableRepositoryBase.cs b/src/Umbraco.Core/Persistence/Repositories/VersionableRepositoryBase.cs index e17de65786..d802d10c66 100644 --- a/src/Umbraco.Core/Persistence/Repositories/VersionableRepositoryBase.cs +++ b/src/Umbraco.Core/Persistence/Repositories/VersionableRepositoryBase.cs @@ -432,7 +432,7 @@ namespace Umbraco.Core.Persistence.Repositories /// orderBy protected IEnumerable GetPagedResultsByQuery(IQuery query, long pageIndex, int pageSize, out long totalRecords, Tuple nodeIdSelect, - Func> processQuery, + Func, IEnumerable> processQuery, string orderBy, Direction orderDirection, bool orderBySystemField, @@ -443,7 +443,7 @@ namespace Umbraco.Core.Persistence.Repositories // Get base query for returning IDs var sqlBaseIds = GetBaseQuery(BaseQueryType.Ids); // Get base query for returning all data - var sqlBaseFull = GetBaseQuery(BaseQueryType.Full); + var sqlBaseFull = GetBaseQuery(BaseQueryType.FullMultiple); if (query == null) query = new Query(); var translatorIds = new SqlTranslator(sqlBaseIds, query); @@ -466,7 +466,7 @@ namespace Umbraco.Core.Persistence.Repositories // the pageResult, then the GetAll will actually return ALL records in the db. if (pagedResult.Items.Any()) { - //Crete the inner paged query that was used above to get the paged result, we'll use that as the inner sub query + //Create the inner paged query that was used above to get the paged result, we'll use that as the inner sub query var args = sqlNodeIdsWithSort.Arguments; string sqlStringCount, sqlStringPage; Database.BuildPageQueries(pageIndex * pageSize, pageSize, sqlNodeIdsWithSort.SQL, ref args, out sqlStringCount, out sqlStringPage); @@ -486,8 +486,8 @@ namespace Umbraco.Core.Persistence.Repositories var fullQuery = GetSortedSqlForPagedResults( GetFilteredSqlForPagedResults(fullQueryWithPagedInnerJoin, defaultFilter), orderDirection, orderBy, orderBySystemField, nodeIdSelect); - - return processQuery(fullQuery, sqlNodeIdsWithSort); + + return processQuery(fullQuery, new PagingSqlQuery(Database, sqlNodeIdsWithSort, pageIndex, pageSize)); } else { @@ -497,18 +497,47 @@ namespace Umbraco.Core.Persistence.Repositories return result; } + /// + /// Gets the property collection for a non-paged query + /// + /// + /// + /// protected IDictionary GetPropertyCollection( - Sql docSql, + Sql sql, + IReadOnlyCollection documentDefs) + { + return GetPropertyCollection(new PagingSqlQuery(sql), documentDefs); + } + + /// + /// Gets the property collection for a query + /// + /// + /// + /// + protected IDictionary GetPropertyCollection( + PagingSqlQuery pagingSqlQuery, IReadOnlyCollection documentDefs) { if (documentDefs.Count == 0) return new Dictionary(); + //initialize to the query passed in + var docSql = pagingSqlQuery.PrePagedSql; + //we need to parse the original SQL statement and reduce the columns to just cmsContent.nodeId, cmsContentVersion.VersionId so that we can use // the statement to go get the property data for all of the items by using an inner join var parsedOriginalSql = "SELECT {0} " + docSql.SQL.Substring(docSql.SQL.IndexOf("FROM", StringComparison.Ordinal)); - //now remove everything from an Orderby clause and beyond - if (parsedOriginalSql.InvariantContains("ORDER BY ")) + + if (pagingSqlQuery.HasPaging) { + //if this is a paged query, build the paged query with the custom column substitution, then re-assign + docSql = pagingSqlQuery.BuildPagedQuery("{0}"); + parsedOriginalSql = docSql.SQL; + } + else if (parsedOriginalSql.InvariantContains("ORDER BY ")) + { + //now remove everything from an Orderby clause and beyond if this is unpaged data parsedOriginalSql = parsedOriginalSql.Substring(0, parsedOriginalSql.LastIndexOf("ORDER BY ", StringComparison.Ordinal)); } @@ -525,7 +554,7 @@ WHERE EXISTS( INNER JOIN cmsPropertyType ON b.datatypeNodeId = cmsPropertyType.dataTypeId INNER JOIN - (" + string.Format(parsedOriginalSql, "DISTINCT cmsContent.contentType") + @") as docData + (" + string.Format(parsedOriginalSql, "cmsContent.contentType") + @") as docData ON cmsPropertyType.contentTypeId = docData.contentType WHERE a.id = b.id)", docSql.Arguments); @@ -646,28 +675,7 @@ ORDER BY contentNodeId, propertytypeid return result; } - - public class DocumentDefinition - { - /// - /// Initializes a new instance of the class. - /// - public DocumentDefinition(int id, Guid version, DateTime versionDate, DateTime createDate, IContentTypeComposition composition) - { - Id = id; - Version = version; - VersionDate = versionDate; - CreateDate = createDate; - Composition = composition; - } - - public int Id { get; set; } - public Guid Version { get; set; } - public DateTime VersionDate { get; set; } - public DateTime CreateDate { get; set; } - public IContentTypeComposition Composition { get; set; } - } - + protected virtual string GetDatabaseFieldNameForOrderBy(string orderBy) { // Translate the passed order by field (which were originally defined for in-memory object sorting @@ -763,5 +771,92 @@ ORDER BY contentNodeId, propertytypeid /// /// protected abstract Sql GetBaseQuery(BaseQueryType queryType); + + internal class DocumentDefinition + { + /// + /// Initializes a new instance of the class. + /// + public DocumentDefinition(int id, Guid version, DateTime versionDate, DateTime createDate, IContentTypeComposition composition) + { + Id = id; + Version = version; + VersionDate = versionDate; + CreateDate = createDate; + Composition = composition; + } + + public int Id { get; set; } + public Guid Version { get; set; } + public DateTime VersionDate { get; set; } + public DateTime CreateDate { get; set; } + public IContentTypeComposition Composition { get; set; } + } + + /// + /// An object representing a query that may contain paging information + /// + internal class PagingSqlQuery + { + public Sql PrePagedSql { get; private set; } + + public PagingSqlQuery(Sql prePagedSql) + { + PrePagedSql = prePagedSql; + } + + public virtual bool HasPaging + { + get { return false; } + } + + public virtual Sql BuildPagedQuery(string selectColumns) + { + throw new InvalidOperationException("This query has no paging information"); + } + } + + /// + /// An object representing a query that contains paging information + /// + /// + internal class PagingSqlQuery : PagingSqlQuery + { + private readonly Database _db; + private readonly long _pageIndex; + private readonly int _pageSize; + + public PagingSqlQuery(Database db, Sql prePagedSql, long pageIndex, int pageSize) : base(prePagedSql) + { + _db = db; + _pageIndex = pageIndex; + _pageSize = pageSize; + } + + public override bool HasPaging + { + get { return _pageSize > 0; } + } + + /// + /// Creates a paged query based on the original query and subtitutes the selectColumns specified + /// + /// + /// + public override Sql BuildPagedQuery(string selectColumns) + { + if (HasPaging == false) throw new InvalidOperationException("This query has no paging information"); + + var resultSql = string.Format("SELECT {0} {1}", selectColumns, PrePagedSql.SQL.Substring(PrePagedSql.SQL.IndexOf("FROM", StringComparison.Ordinal))); + + //this query is meant to be paged so we need to generate the paging syntax + //Create the inner paged query that was used above to get the paged result, we'll use that as the inner sub query + var args = PrePagedSql.Arguments; + string sqlStringCount, sqlStringPage; + _db.BuildPageQueries(_pageIndex * _pageSize, _pageSize, resultSql, ref args, out sqlStringCount, out sqlStringPage); + + return new Sql(sqlStringPage, args); + } + } } } \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/SqlSyntax/SqlSyntaxProviderExtensions.cs b/src/Umbraco.Core/Persistence/SqlSyntax/SqlSyntaxProviderExtensions.cs index 1231765f20..f0bafdacf7 100644 --- a/src/Umbraco.Core/Persistence/SqlSyntax/SqlSyntaxProviderExtensions.cs +++ b/src/Umbraco.Core/Persistence/SqlSyntax/SqlSyntaxProviderExtensions.cs @@ -1,7 +1,23 @@ -namespace Umbraco.Core.Persistence.SqlSyntax +using System.Collections.Generic; +using System.Linq; +using Umbraco.Core.Persistence.DatabaseModelDefinitions; + +namespace Umbraco.Core.Persistence.SqlSyntax { internal static class SqlSyntaxProviderExtensions { + public static IEnumerable GetDefinedIndexesDefinitions(this ISqlSyntaxProvider sql, Database db) + { + return sql.GetDefinedIndexes(db) + .Select(x => new DbIndexDefinition() + { + TableName = x.Item1, + IndexName = x.Item2, + ColumnName = x.Item3, + IsUnique = x.Item4 + }).ToArray(); + } + /// /// Returns the quotes tableName.columnName combo /// diff --git a/src/Umbraco.Core/Services/FileService.cs b/src/Umbraco.Core/Services/FileService.cs index f4f634d974..ec886d788d 100644 --- a/src/Umbraco.Core/Services/FileService.cs +++ b/src/Umbraco.Core/Services/FileService.cs @@ -735,6 +735,26 @@ namespace Umbraco.Core.Services return empty.Union(files.Except(empty)); } + public void CreatePartialViewFolder(string folderPath) + { + var uow = _fileUowProvider.GetUnitOfWork(); + using (var repository = RepositoryFactory.CreatePartialViewRepository(uow)) + { + ((PartialViewRepository)repository).AddFolder(folderPath); + uow.Commit(); + } + } + + public void CreatePartialViewMacroFolder(string folderPath) + { + var uow = _fileUowProvider.GetUnitOfWork(); + using (var repository = RepositoryFactory.CreatePartialViewMacroRepository(uow)) + { + ((PartialViewMacroRepository)repository).AddFolder(folderPath); + uow.Commit(); + } + } + public void DeletePartialViewFolder(string folderPath) { using (var uow = _fileUowProvider.GetUnitOfWork()) diff --git a/src/Umbraco.Core/Services/IFileService.cs b/src/Umbraco.Core/Services/IFileService.cs index 453d199bfe..e8f1065219 100644 --- a/src/Umbraco.Core/Services/IFileService.cs +++ b/src/Umbraco.Core/Services/IFileService.cs @@ -11,6 +11,8 @@ namespace Umbraco.Core.Services public interface IFileService : IService { IEnumerable GetPartialViewSnippetNames(params string[] filterNames); + void CreatePartialViewFolder(string folderPath); + void CreatePartialViewMacroFolder(string folderPath); void DeletePartialViewFolder(string folderPath); void DeletePartialViewMacroFolder(string folderPath); IPartialView GetPartialView(string path); diff --git a/src/Umbraco.Core/Services/PackagingService.cs b/src/Umbraco.Core/Services/PackagingService.cs index f2530a3319..d5f1ffea4c 100644 --- a/src/Umbraco.Core/Services/PackagingService.cs +++ b/src/Umbraco.Core/Services/PackagingService.cs @@ -1716,6 +1716,24 @@ namespace Umbraco.Core.Services #region Package Building #endregion + /// + /// This method can be used to trigger the 'ImportedPackage' event when a package is installed by something else but this service. + /// + /// + internal static void OnImportedPackage(ImportPackageEventArgs args) + { + ImportedPackage.RaiseEvent(args, null); + } + + /// + /// This method can be used to trigger the 'UninstalledPackage' event when a package is uninstalled by something else but this service. + /// + /// + internal static void OnUninstalledPackage(UninstallPackageEventArgs args) + { + UninstalledPackage.RaiseEvent(args, null); + } + #region Event Handlers /// /// Occurs before Importing Content @@ -1876,10 +1894,15 @@ namespace Umbraco.Core.Services internal static event TypedEventHandler> ImportingPackage; /// - /// Occurs after a apckage is imported + /// Occurs after a package is imported /// internal static event TypedEventHandler> ImportedPackage; + /// + /// Occurs after a package is uninstalled + /// + internal static event TypedEventHandler> UninstalledPackage; + #endregion } } diff --git a/src/Umbraco.Core/Services/RelationService.cs b/src/Umbraco.Core/Services/RelationService.cs index ec42d4bd74..76222810d2 100644 --- a/src/Umbraco.Core/Services/RelationService.cs +++ b/src/Umbraco.Core/Services/RelationService.cs @@ -215,7 +215,7 @@ namespace Umbraco.Core.Services using (var uow = UowProvider.GetUnitOfWork(commit: true)) { var repository = RepositoryFactory.CreateRelationRepository(uow); - var query = new Query().Where(x => x.ChildId == id || x.ParentId == id); + var query = new Query().Where(x => x.ParentId == id || x.ChildId == id); return repository.GetByQuery(query); } } @@ -230,7 +230,7 @@ namespace Umbraco.Core.Services if (relationType == null) return Enumerable.Empty(); var relationRepo = RepositoryFactory.CreateRelationRepository(uow); - var query = new Query().Where(x => (x.ChildId == id || x.ParentId == id) && x.RelationTypeId == relationType.Id); + var query = new Query().Where(x => (x.ParentId == id || x.ChildId == id) && x.RelationTypeId == relationType.Id); return relationRepo.GetByQuery(query); } } diff --git a/src/Umbraco.Core/UdiGetterExtensions.cs b/src/Umbraco.Core/UdiGetterExtensions.cs index 1bd018a754..a9750fe464 100644 --- a/src/Umbraco.Core/UdiGetterExtensions.cs +++ b/src/Umbraco.Core/UdiGetterExtensions.cs @@ -285,18 +285,30 @@ namespace Umbraco.Core var member = entity as IMember; if (member != null) return member.GetUdi(); - var contentBase = entity as IContentBase; - if (contentBase != null) return contentBase.GetUdi(); + var stylesheet = entity as Stylesheet; + if (stylesheet != null) return stylesheet.GetUdi(); + + var script = entity as Script; + if (script != null) return script.GetUdi(); + + var dictionaryItem = entity as IDictionaryItem; + if (dictionaryItem != null) return dictionaryItem.GetUdi(); var macro = entity as IMacro; if (macro != null) return macro.GetUdi(); + var partialView = entity as IPartialView; + if (partialView != null) return partialView.GetUdi(); + + var xsltFile = entity as IXsltFile; + if (xsltFile != null) return xsltFile.GetUdi(); + + var contentBase = entity as IContentBase; + if (contentBase != null) return contentBase.GetUdi(); + var relationType = entity as IRelationType; if (relationType != null) return relationType.GetUdi(); - var dictionaryItem = entity as IDictionaryItem; - if (dictionaryItem != null) return dictionaryItem.GetUdi(); - throw new NotSupportedException(string.Format("Entity type {0} is not supported.", entity.GetType().FullName)); } } diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index 738634c858..c8ef5347fe 100644 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -316,6 +316,7 @@ + @@ -451,6 +452,7 @@ + @@ -480,6 +482,8 @@ + + diff --git a/src/Umbraco.Tests/IO/IOHelperTest.cs b/src/Umbraco.Tests/IO/IOHelperTest.cs index 559ba1a2f3..e42491454b 100644 --- a/src/Umbraco.Tests/IO/IOHelperTest.cs +++ b/src/Umbraco.Tests/IO/IOHelperTest.cs @@ -56,5 +56,14 @@ namespace Umbraco.Tests.IO Assert.AreEqual(IOHelper.MapPath(SystemDirectories.WebServices, true), IOHelper.MapPath(SystemDirectories.WebServices, false)); Assert.AreEqual(IOHelper.MapPath(SystemDirectories.Xslt, true), IOHelper.MapPath(SystemDirectories.Xslt, false)); } + + [Test] + public void EnsurePathIsApplicationRootPrefixed() + { + //Assert + Assert.AreEqual("~/Views/Template.cshtml", IOHelper.EnsurePathIsApplicationRootPrefixed("Views/Template.cshtml")); + Assert.AreEqual("~/Views/Template.cshtml", IOHelper.EnsurePathIsApplicationRootPrefixed("/Views/Template.cshtml")); + Assert.AreEqual("~/Views/Template.cshtml", IOHelper.EnsurePathIsApplicationRootPrefixed("~/Views/Template.cshtml")); + } } } diff --git a/src/Umbraco.Tests/Persistence/SyntaxProvider/SqlCeSyntaxProviderTests.cs b/src/Umbraco.Tests/Persistence/SyntaxProvider/SqlCeSyntaxProviderTests.cs index fafddb8dfd..105b3d0c11 100644 --- a/src/Umbraco.Tests/Persistence/SyntaxProvider/SqlCeSyntaxProviderTests.cs +++ b/src/Umbraco.Tests/Persistence/SyntaxProvider/SqlCeSyntaxProviderTests.cs @@ -121,6 +121,19 @@ WHERE (([umbracoNode].[nodeObjectType] = @0))) x)".Replace(Environment.NewLine, Assert.AreEqual("CREATE UNIQUE NONCLUSTERED INDEX [IX_A] ON [TheTable] ([A])", createExpression.ToString()); } + [Test] + public void CreateIndexBuilder_SqlServer_Unique_CreatesUniqueNonClusteredIndex_Multi_Columnn() + { + var sqlSyntax = new SqlServerSyntaxProvider(); + var createExpression = new CreateIndexExpression(DatabaseProviders.SqlServer, new[] { DatabaseProviders.SqlServer }, sqlSyntax) + { + Index = { Name = "IX_AB" } + }; + var builder = new CreateIndexBuilder(createExpression); + builder.OnTable("TheTable").OnColumn("A").Ascending().OnColumn("B").Ascending().WithOptions().Unique(); + Assert.AreEqual("CREATE UNIQUE NONCLUSTERED INDEX [IX_AB] ON [TheTable] ([A],[B])", createExpression.ToString()); + } + [Test] public void CreateIndexBuilder_SqlServer_Clustered_CreatesClusteredIndex() { diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbbreadcrumbs.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbbreadcrumbs.directive.js index f2797425c5..61906bbf74 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbbreadcrumbs.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbbreadcrumbs.directive.js @@ -40,27 +40,50 @@ Use this directive to generate a list of breadcrumbs. @param {array} ancestors Array of ancestors @param {string} entityType The content entity type (member, media, content). +@param {callback} Callback when an ancestor is clicked. It will override the default link behaviour. **/ -(function() { - 'use strict'; +(function () { + 'use strict'; - function BreadcrumbsDirective() { + function BreadcrumbsDirective() { - var directive = { - restrict: 'E', - replace: true, - templateUrl: 'views/components/editor/umb-breadcrumbs.html', - scope: { - ancestors: "=", - entityType: "@" - } - }; + function link(scope, el, attr, ctrl) { - return directive; + scope.allowOnOpen = false; - } + scope.open = function(ancestor) { + if(scope.onOpen && scope.allowOnOpen) { + scope.onOpen({'ancestor': ancestor}); + } + }; - angular.module('umbraco.directives').directive('umbBreadcrumbs', BreadcrumbsDirective); + function onInit() { + if ("onOpen" in attr) { + scope.allowOnOpen = true; + } + } + + onInit(); + + } + + var directive = { + restrict: 'E', + replace: true, + templateUrl: 'views/components/editor/umb-breadcrumbs.html', + scope: { + ancestors: "=", + entityType: "@", + onOpen: "&" + }, + link: link + }; + + return directive; + + } + + angular.module('umbraco.directives').directive('umbBreadcrumbs', BreadcrumbsDirective); })(); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/tree/umbtree.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/tree/umbtree.directive.js index 75d0144982..7a74716ea1 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/tree/umbtree.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/tree/umbtree.directive.js @@ -21,7 +21,8 @@ function umbTreeDirective($compile, $log, $q, $rootScope, treeService, notificat customtreeparams: '@', eventhandler: '=', enablecheckboxes: '@', - enablelistviewsearch: '@' + enablelistviewsearch: '@', + enablelistviewexpand: '@' }, compile: function(element, attrs) { @@ -35,7 +36,7 @@ function umbTreeDirective($compile, $log, $q, $rootScope, treeService, notificat '' + ''; template += '
    ' + - '' + + '' + '
' + '' + ''; diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/tree/umbtreeitem.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/tree/umbtreeitem.directive.js index f4fe0db4e6..b32942791c 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/tree/umbtreeitem.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/tree/umbtreeitem.directive.js @@ -27,6 +27,7 @@ angular.module("umbraco.directives") section: '@', eventhandler: '=', currentNode: '=', + enablelistviewexpand: '@', node: '=', tree: '=' }, @@ -38,7 +39,7 @@ angular.module("umbraco.directives") '
' + //NOTE: This ins element is used to display the search icon if the node is a container/listview and the tree is currently in dialog //'' + - ' ' + + ' ' + '' + '' + //NOTE: These are the 'option' elipses @@ -74,11 +75,12 @@ angular.module("umbraco.directives") //toggle visibility of last 'ins' depending on children //visibility still ensure the space is "reserved", so both nodes with and without children are aligned. - if (!node.hasChildren) { - element.find("ins").last().css("visibility", "hidden"); + + if (node.hasChildren || node.metaData.isContainer && scope.enablelistviewexpand === "true") { + element.find("ins").last().css("visibility", "visible"); } else { - element.find("ins").last().css("visibility", "visible"); + element.find("ins").last().css("visibility", "hidden"); } var icon = element.find("i:first"); @@ -192,7 +194,7 @@ angular.module("umbraco.directives") emits treeNodeCollapsing event if already expanded and treeNodeExpanding if collapsed */ scope.load = function (node) { - if (node.expanded) { + if (node.expanded && !node.metaData.isContainer) { deleteAnimations = false; emitEvent("treeNodeCollapsing", { tree: scope.tree, node: node, element: element }); node.expanded = false; @@ -227,7 +229,7 @@ angular.module("umbraco.directives") setupNodeDom(scope.node, scope.tree); - var template = '
'; + var template = '
'; var newElement = angular.element(template); $compile(newElement)(scope); element.append(newElement); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbminilistview.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbminilistview.directive.js new file mode 100644 index 0000000000..281242621a --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbminilistview.directive.js @@ -0,0 +1,209 @@ +(function () { + 'use strict'; + + function MiniListViewDirective(contentResource, memberResource, mediaResource) { + + function link(scope, el, attr, ctrl) { + + scope.search = ""; + scope.miniListViews = []; + scope.breadcrumb = []; + + var miniListViewsHistory = []; + var goingForward = true; + var skipAnimation = true; + + function onInit() { + open(scope.node); + } + + function open(node) { + + goingForward = true; + + var miniListView = { + node: node, + loading: true, + pagination: { + pageSize: 10, + pageNumber: 1, + filter: '', + orderDirection: "Ascending", + orderBy: "SortOrder", + orderBySystemField: true + } + }; + + // clear and push mini list view in dom so we only render 1 view + scope.miniListViews = []; + scope.miniListViews.push(miniListView); + + // store in history so we quickly can navigate back + miniListViewsHistory.push(miniListView); + + // get children + getChildrenForMiniListView(miniListView); + + makeBreadcrumb(); + + } + + function getChildrenForMiniListView(miniListView) { + + // start loading animation list view + miniListView.loading = true; + + // setup the correct resource depending on section + var resource = ""; + + if (scope.entityType === "Member") { + resource = memberResource.getPagedResults; + } else if (scope.entityType === "Media") { + resource = mediaResource.getChildren; + } else { + resource = contentResource.getChildren; + } + + resource(miniListView.node.id, miniListView.pagination) + .then(function (data) { + // update children + miniListView.children = data.items; + // update pagination + miniListView.pagination.totalItems = data.totalItems; + miniListView.pagination.totalPages = data.totalPages; + // stop load indicator + miniListView.loading = false; + }); + } + + scope.openNode = function(event, node) { + open(node); + event.stopPropagation(); + }; + + scope.selectNode = function(node) { + if(scope.onSelect) { + scope.onSelect({'node': node}); + } + }; + + /* Pagination */ + scope.goToPage = function(pageNumber, miniListView) { + // set new page number + miniListView.pagination.pageNumber = pageNumber; + // get children + getChildrenForMiniListView(miniListView); + }; + + /* Breadcrumb */ + scope.clickBreadcrumb = function(ancestor) { + + var found = false; + goingForward = false; + + angular.forEach(miniListViewsHistory, function(historyItem, index){ + // We need to make sure we can compare the two id's. + // Some id's are integers and others are strings. + // Members have string ids like "all-members". + if(historyItem.node.id.toString() === ancestor.id.toString()) { + // load the list view from history + scope.miniListViews = []; + scope.miniListViews.push(historyItem); + // clean up history - remove all children after + miniListViewsHistory.splice(index + 1, miniListViewsHistory.length); + found = true; + } + }); + + if(!found) { + // if we can't find the view in the history - close the list view + scope.exitMiniListView(); + } + + // update the breadcrumb + makeBreadcrumb(); + + }; + + scope.showBackButton = function() { + // don't show the back button if the start node is a list view + if(scope.node.metaData && scope.node.metaData.IsContainer || scope.node.isContainer) { + return false; + } else { + return true; + } + }; + + scope.exitMiniListView = function() { + miniListViewsHistory = []; + scope.miniListViews = []; + if(scope.onClose) { + scope.onClose(); + } + }; + + function makeBreadcrumb() { + scope.breadcrumb = []; + angular.forEach(miniListViewsHistory, function(historyItem){ + scope.breadcrumb.push(historyItem.node); + }); + } + + /* Search */ + scope.searchMiniListView = function(search, miniListView) { + // set search value + miniListView.pagination.filter = search; + // reset pagination + miniListView.pagination.pageNumber = 1; + // start loading animation list view + miniListView.loading = true; + searchMiniListView(miniListView); + }; + + var searchMiniListView = _.debounce(function (miniListView) { + scope.$apply(function () { + getChildrenForMiniListView(miniListView); + }); + }, 500); + + /* Animation */ + scope.getMiniListViewAnimation = function() { + + // disable the first "slide-in-animation"" if the start node is a list view + if(scope.node.metaData && scope.node.metaData.IsContainer && skipAnimation || scope.node.isContainer && skipAnimation) { + skipAnimation = false; + return; + } + + if(goingForward) { + return 'umb-mini-list-view--forward'; + } else { + return 'umb-mini-list-view--backwards'; + } + }; + + onInit(); + + } + + var directive = { + restrict: 'E', + replace: true, + templateUrl: 'views/components/umb-mini-list-view.html', + scope: { + node: "=", + entityType: "@", + startNodeId: "=", + onSelect: "&", + onClose: "&" + }, + link: link + }; + + return directive; + + } + + angular.module('umbraco.directives').directive('umbMiniListView', MiniListViewDirective); + +})(); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbpagination.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbpagination.directive.js index fe753171e1..610f8546a3 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbpagination.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbpagination.directive.js @@ -134,25 +134,40 @@ Use this directive to generate a pagination. } - scope.next = function() { - if (scope.onNext && scope.pageNumber < scope.totalPages) { - scope.pageNumber++; - scope.onNext(scope.pageNumber); - } + scope.next = function () { + if (scope.pageNumber < scope.totalPages) { + scope.pageNumber++; + if (scope.onNext) { + scope.onNext(scope.pageNumber); + } + if (scope.onChange) { + scope.onChange({ "pageNumber": scope.pageNumber }); + } + } }; - scope.prev = function(pageNumber) { - if (scope.onPrev && scope.pageNumber > 1) { - scope.pageNumber--; - scope.onPrev(scope.pageNumber); - } + scope.prev = function (pageNumber) { + if (scope.pageNumber > 1) { + scope.pageNumber--; + if (scope.onPrev) { + scope.onPrev(scope.pageNumber); + } + if (scope.onChange) { + scope.onChange({ "pageNumber": scope.pageNumber }); + } + } }; - scope.goToPage = function(pageNumber) { - if(scope.onGoToPage) { - scope.pageNumber = pageNumber + 1; - scope.onGoToPage(scope.pageNumber); - } + scope.goToPage = function (pageNumber) { + scope.pageNumber = pageNumber + 1; + if (scope.onGoToPage) { + scope.onGoToPage(scope.pageNumber); + } + if (scope.onChange) { + if (scope.onChange) { + scope.onChange({ "pageNumber": scope.pageNumber }); + } + } }; var unbindPageNumberWatcher = scope.$watch('pageNumber', function(newValue, oldValue){ @@ -176,7 +191,8 @@ Use this directive to generate a pagination. totalPages: "=", onNext: "=", onPrev: "=", - onGoToPage: "=" + onGoToPage: "=", + onChange: "&" }, link: link }; diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/codefile.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/codefile.resource.js index e83bb4f14e..44454f21e3 100644 --- a/src/Umbraco.Web.UI.Client/src/common/resources/codefile.resource.js +++ b/src/Umbraco.Web.UI.Client/src/common/resources/codefile.resource.js @@ -205,6 +205,38 @@ function codefileResource($q, $http, umbDataFormatter, umbRequestHelper) { "codeFileApiBaseUrl", "GetScaffold?type=" + type + "&id=" + id + "&snippetName=" + snippetName)), "Failed to get scaffold for" + type); + }, + + /** + * @ngdoc method + * @name umbraco.resources.codefileResource#createContainer + * @methodOf umbraco.resources.codefileResource + * + * @description + * Creates a container/folder + * + * ##usage + *
+         * codefileResource.createContainer("partialViews", "folder%2ffolder", "folder")
+         *    .then(function(data) {
+         *        alert('its here!');
+         *    });
+         * 
+ * + * @param {string} File type: (scripts, partialViews, partialViewMacros). + * @param {string} Parent Id: url encoded path + * @param {string} Container name + * @returns {Promise} resourcePromise object. + * + */ + + createContainer: function(type, parentId, name) { + return umbRequestHelper.resourcePromise( + $http.post(umbRequestHelper.getApiUrl( + "codeFileApiBaseUrl", + "PostCreateContainer", + { type: type, parentId: parentId, name: encodeURIComponent(name) })), + 'Failed to create a folder under parent id ' + parentId); } }; diff --git a/src/Umbraco.Web.UI.Client/src/less/belle.less b/src/Umbraco.Web.UI.Client/src/less/belle.less index 6fbf12e815..be55d136da 100644 --- a/src/Umbraco.Web.UI.Client/src/less/belle.less +++ b/src/Umbraco.Web.UI.Client/src/less/belle.less @@ -118,6 +118,8 @@ @import "components/umb-avatar.less"; @import "components/umb-progress-bar.less"; @import "components/umb-querybuilder.less"; +@import "components/umb-pagination.less"; +@import "components/umb-mini-list-view.less"; @import "components/buttons/umb-button.less"; @import "components/buttons/umb-button-group.less"; diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-mini-list-view.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-mini-list-view.less new file mode 100644 index 0000000000..1804b45260 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-mini-list-view.less @@ -0,0 +1,97 @@ +.umb-mini-list-view__title { + display: flex; + align-items: center; +} + +.umb-mini-list-view__title-text { + font-size: 16px; + font-weight: bold; +} + +.umb-mini-list-view__title-icon { + font-size: 20px; + margin-right: 5px; +} + +.umb-mini-list-view__back { + font-size: 11px; + margin-right: 5px; + color: @gray; + display: flex; + align-items: center; +} + +.umb-mini-list-view__back-icon { + margin-right: 4px; + height: 11px; + line-height: 11px; +} + +.umb-mini-list-view__back-text { + text-decoration: underline; + margin-right: 5px; +} + +.umb-mini-list-view__back:hover { + opacity: 1; + text-decoration: none; + color: @black; +} + +/* Animations */ + +/* Forward */ + +.umb-mini-list-view--forward-enter, +.umb-mini-list-view--forward-leave +{ + transition: 120ms cubic-bezier(0.250, 0.250, 0.750, 0.750) all; + position: relative; + display: block; +} + +.umb-mini-list-view--forward-enter { + left: 100%; +} + +.umb-mini-list-view--forward-enter.umb-mini-list-view--forward-enter-active { + left: 0; + opacity: 1; +} + +.umb-mini-list-view--forward-leave { + left: 0; +} + +.umb-mini-list-view--forward-leave.umb-mini-list-view--forward-leave-active{ + left: -100%; + opacity: 0; +} + +/* Backwards */ + +.umb-mini-list-view--backwards-enter, +.umb-mini-list-view--backwards-leave +{ + transition: 120ms cubic-bezier(0.250, 0.250, 0.750, 0.750) all; + position: relative; + display: block; +} + +.umb-mini-list-view--backwards-enter { + right: 100%; +} + +.umb-mini-list-view--backwards-enter.umb-mini-list-view--backwards-enter-active { + right: 0; + opacity: 1; +} + +.umb-mini-list-view--backwards-leave { + left: 0; +} + +.umb-mini-list-view--backwards-leave.umb-mini-list-view--backwards-leave-active{ + right: -100%; + opacity: 0; +} \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-pagination.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-pagination.less new file mode 100644 index 0000000000..8d3d563cab --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-pagination.less @@ -0,0 +1,3 @@ +.umb-pagination ul { + box-shadow: none; +} \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-table.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-table.less index bc8e19cf2b..9cf1b3cfc8 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-table.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-table.less @@ -10,7 +10,7 @@ flex-wrap: nowrap; justify-content: space-between; - min-width: 640px; + min-width: auto; } .umb-table.umb-table-inactive { @@ -103,11 +103,17 @@ input.umb-table__input { // Table Body Styles +.umb-table-body { + position: relative; +} + .umb-table-body .umb-table-row { color: fade(@gray, 75%); border-top: 1px solid @grayLight; cursor: pointer; font-size: 13px; + position: relative; + min-height: 32px; &:hover { background-color: fade(@grayLighter, 90%); @@ -198,30 +204,33 @@ input.umb-table__input { } } - - - - - // Table Row Styles .umb-table-row { display: flex; flex-flow: row nowrap; align-items: center; - user-select: none; } +.umb-table-body .umb-table-row--empty { + flex: 1 1 auto; + display: flex; + justify-content: center; + padding: 5px 0; + cursor: auto; + user-select: auto; +} +.umb-table-body .umb-table-row--empty:hover { + background-color: transparent; + cursor: auto; +} .umb-table-row.-selected, .umb-table-row.-selected:hover { background-color: fade(@blueDark, 4%); } - - - // Table Cell Styles .umb-table-cell { display: flex; @@ -250,9 +259,44 @@ input.umb-table__input { padding: 15px 0; } +.umb-table-cell--auto-width { + flex: 0 0 auto !important; +} + +.umb-table-cell--faded { + opacity: 0.4; +} + // Increases the space for the name cell .umb-table__name { flex: 1 1 25%; max-width: 25%; } + +.umb-table__loading-overlay { + position: absolute; + width: 100%; + height: 100%; + background-color: rgba(255, 255, 255, 0.7); + z-index: 1; +} + +.umb-table__row-expand { + font-size: 12px; + text-decoration: none; + color: @black; +} + +.umb-table--condensed { + + .umb-table-cell:first-of-type:not(.not-fixed) { + padding-top: 10px; + padding-bottom: 10px; + } + + .umb-table-body__icon { + font-size: 20px; + } + +} \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/less/variables.less b/src/Umbraco.Web.UI.Client/src/less/variables.less index 8ebb0f08ef..e7b8517447 100644 --- a/src/Umbraco.Web.UI.Client/src/less/variables.less +++ b/src/Umbraco.Web.UI.Client/src/less/variables.less @@ -36,6 +36,7 @@ .red{color: @red;} .blue{color: @blue;} +.black{color: @black;} //icon colors for tree icons @@ -351,4 +352,4 @@ // SORTABLE // -------------------------------------------------- @sortableHelperBg: rgba(4, 156, 219, 0.5); -@sortablePlaceholderBg : @blue; \ No newline at end of file +@sortablePlaceholderBg : @blue; diff --git a/src/Umbraco.Web.UI.Client/src/views/common/overlays/contentpicker/contentpicker.html b/src/Umbraco.Web.UI.Client/src/views/common/overlays/contentpicker/contentpicker.html index 8e70ad163c..43eab532d4 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/overlays/contentpicker/contentpicker.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/overlays/contentpicker/contentpicker.html @@ -1,34 +1,48 @@
-
- - -
+
- - +
+ + +
-
- - -
+ + -
+
+ + +
+ +
+ + + + +
\ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/views/common/overlays/memberpicker/memberpicker.html b/src/Umbraco.Web.UI.Client/src/views/common/overlays/memberpicker/memberpicker.html index e4a8d36263..00475dddfe 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/overlays/memberpicker/memberpicker.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/overlays/memberpicker/memberpicker.html @@ -1,34 +1,47 @@
-
- - -
+
- - +
+ + +
-
- - -
+ + + +
+ + +
+
+ + +
diff --git a/src/Umbraco.Web.UI.Client/src/views/common/overlays/treepicker/treepicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/overlays/treepicker/treepicker.controller.js index e69b5ee3ce..1caff3ab31 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/overlays/treepicker/treepicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/overlays/treepicker/treepicker.controller.js @@ -1,6 +1,6 @@ //used for the media picker dialog angular.module("umbraco").controller("Umbraco.Overlays.TreePickerController", - function ($scope, entityResource, eventsService, $log, searchService, angularHelper, $timeout, localizationService, treeService) { + function ($scope, $q, entityResource, eventsService, $log, searchService, angularHelper, $timeout, localizationService, treeService, contentResource, mediaResource, memberResource) { var tree = null; var dialogOptions = $scope.model; @@ -22,27 +22,22 @@ angular.module("umbraco").controller("Umbraco.Overlays.TreePickerController", $scope.init = function(contentType) { if(contentType === "content") { - entityType = "Document"; + $scope.entityType = "Document"; if(!$scope.model.title) { $scope.model.title = localizationService.localize("defaultdialogs_selectContent"); } } else if(contentType === "member") { - entityType = "Member"; + $scope.entityType = "Member"; if(!$scope.model.title) { $scope.model.title = localizationService.localize("defaultdialogs_selectMember"); } } else if(contentType === "media") { - entityType = "Media"; + $scope.entityType = "Media"; if(!$scope.model.title) { $scope.model.title = localizationService.localize("defaultdialogs_selectMedia"); } } } - - // Search is only working for content, media and member section so we will remove it from everything else - if($scope.section === "content" || $scope.section === "media" || $scope.section === "member" ) { - $scope.enableSearh = true; - } //create the custom query string param for this tree $scope.customTreeParams = dialogOptions.startNodeId ? "startNodeId=" + dialogOptions.startNodeId : ""; @@ -54,7 +49,7 @@ angular.module("umbraco").controller("Umbraco.Overlays.TreePickerController", }); // Allow the entity type to be passed in but defaults to Document for backwards compatibility. - var entityType = dialogOptions.entityType ? dialogOptions.entityType : "Document"; + $scope.entityType = dialogOptions.entityType ? dialogOptions.entityType : "Document"; //min / max values @@ -66,10 +61,24 @@ angular.module("umbraco").controller("Umbraco.Overlays.TreePickerController", } if (dialogOptions.section === "member") { - entityType = "Member"; + $scope.entityType = "Member"; } else if (dialogOptions.section === "media") { - entityType = "Media"; + $scope.entityType = "Media"; + } + + // Search and listviews is only working for content, media and member section so we will remove it from everything else + if ($scope.section === "content" || $scope.section === "media" || $scope.section === "member") { + $scope.enableSearh = true; + + //if a alternative startnode is used, we need to check if it is a container + if (dialogOptions.startNodeId && dialogOptions.startNodeId !== -1) { + entityResource.getById(dialogOptions.startNodeId, $scope.entityType).then(function (node) { + if (node.metaData.IsContainer) { + openMiniListView(node); + } + }); + } } //Configures filtering @@ -101,54 +110,17 @@ angular.module("umbraco").controller("Umbraco.Overlays.TreePickerController", } function nodeExpandedHandler(ev, args) { + + // open mini list view for list views + if (args.node.metaData.isContainer) { + openMiniListView(args.node); + } + if (angular.isArray(args.children)) { //iterate children _.each(args.children, function (child) { - //check if any of the items are list views, if so we need to add some custom - // children: A node to activate the search, any nodes that have already been - // selected in the search - if (child.metaData.isContainer) { - child.hasChildren = true; - child.children = [ - { - level: child.level + 1, - hasChildren: false, - parent: function () { - return child; - }, - name: searchText, - metaData: { - listViewNode: child, - }, - cssClass: "icon-search", - cssClasses: ["not-published"] - } - ]; - //add base transition classes to this node - child.cssClasses.push("tree-node-slide-up"); - - var listViewResults = _.filter($scope.searchInfo.selectedSearchResults, function(i) { - return i.parentId == child.id; - }); - _.each(listViewResults, function(item) { - child.children.unshift({ - id: item.id, - name: item.name, - cssClass: "icon umb-tree-icon sprTree " + item.icon, - level: child.level + 1, - metaData: { - isSearchResult: true - }, - hasChildren: false, - parent: function () { - return child; - } - }); - }); - } - //now we need to look in the already selected search results and // toggle the check boxes for those ones that are listed var exists = _.find($scope.searchInfo.selectedSearchResults, function (selected) { @@ -174,18 +146,7 @@ angular.module("umbraco").controller("Umbraco.Overlays.TreePickerController", args.event.preventDefault(); args.event.stopPropagation(); - if (args.node.metaData.listViewNode) { - //check if list view 'search' node was selected - - $scope.searchInfo.showSearch = true; - $scope.searchInfo.searchFromId = args.node.metaData.listViewNode.id; - $scope.searchInfo.searchFromName = args.node.metaData.listViewNode.name; - - //add transition classes - var listViewNode = args.node.parent(); - listViewNode.cssClasses.push('tree-node-slide-up-hide-active'); - } - else if (args.node.metaData.isSearchResult) { + if (args.node.metaData.isSearchResult) { //check if the item selected was a search result from a list view //unselect @@ -232,7 +193,7 @@ angular.module("umbraco").controller("Umbraco.Overlays.TreePickerController", multiSelectItem(entity); } else { //otherwise we have to get it from the server - entityResource.getById(id, entityType).then(function (ent) { + entityResource.getById(id, $scope.entityType).then(function (ent) { multiSelectItem(ent); }); } @@ -257,7 +218,7 @@ angular.module("umbraco").controller("Umbraco.Overlays.TreePickerController", multiSelectItem(entity); } else { //otherwise we have to get it from the server - entityResource.getById(id, entityType).then(function (ent) { + entityResource.getById(id, $scope.entityType).then(function (ent) { multiSelectItem(ent); }); } @@ -274,7 +235,7 @@ angular.module("umbraco").controller("Umbraco.Overlays.TreePickerController", $scope.model.submit($scope.model); } else { //otherwise we have to get it from the server - entityResource.getById(id, entityType).then(function (ent) { + entityResource.getById(id, $scope.entityType).then(function (ent) { $scope.model.selection.push(ent); $scope.model.submit($scope.model); }); @@ -355,7 +316,7 @@ angular.module("umbraco").controller("Umbraco.Overlays.TreePickerController", } $scope.multiSubmit = function (result) { - entityResource.getByIds(result, entityType).then(function (ents) { + entityResource.getByIds(result, $scope.entityType).then(function (ents) { $scope.submit(ents); }); }; @@ -504,4 +465,19 @@ angular.module("umbraco").controller("Umbraco.Overlays.TreePickerController", $scope.dialogTreeEventHandler.unbind("treeNodeExpanded", nodeExpandedHandler); $scope.dialogTreeEventHandler.unbind("treeNodeSelect", nodeSelectHandler); }); + + $scope.selectListViewNode = function(node) { + select(node.name, node.id); + //toggle checked state + node.selected = node.selected === true ? false : true; + }; + + $scope.closeMiniListView = function() { + $scope.miniListView = undefined; + }; + + function openMiniListView(node) { + $scope.miniListView = node; + } + }); diff --git a/src/Umbraco.Web.UI.Client/src/views/common/overlays/treepicker/treepicker.html b/src/Umbraco.Web.UI.Client/src/views/common/overlays/treepicker/treepicker.html index b009b4d45f..c9785ed9e1 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/overlays/treepicker/treepicker.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/overlays/treepicker/treepicker.html @@ -1,35 +1,49 @@
-
- - -
+
- - +
+ + +
-
- - -
+ + -
+
+ + +
+ +
+ + + + + \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-breadcrumbs.html b/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-breadcrumbs.html index 219e90fc4c..a9e42d01d6 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-breadcrumbs.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-breadcrumbs.html @@ -1,10 +1,15 @@ + {{ancestor.name}} + + + \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/views/components/umb-mini-list-view.html b/src/Umbraco.Web.UI.Client/src/views/components/umb-mini-list-view.html new file mode 100644 index 0000000000..97884a6d9e --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/components/umb-mini-list-view.html @@ -0,0 +1,101 @@ +
+ +
+ +
+ +

{{ miniListView.node.name }}

+
+ +
+ + + + Back / + + + + + +
+ +
+ + +
+
+ +
+ +
+
+
+ + +
+ + +
+ +
+ + +
+
+
+   + + +
+
+
{{ child.name }}
+
+ + +
+ + +
+ + +
+ +
+ +
+ +
+ +
+ + +
+ +
+ +
\ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/views/components/umb-pagination.html b/src/Umbraco.Web.UI.Client/src/views/components/umb-pagination.html index 9e4a1697a3..e8effcd761 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/umb-pagination.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/umb-pagination.html @@ -1,4 +1,4 @@ -