diff --git a/src/Umbraco.Cms.Api.Management/Controllers/DataType/Tree/DataTypeTreeControllerBase.cs b/src/Umbraco.Cms.Api.Management/Controllers/DataType/Tree/DataTypeTreeControllerBase.cs index 452a321d9b1d..90d2669b7f40 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/DataType/Tree/DataTypeTreeControllerBase.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/DataType/Tree/DataTypeTreeControllerBase.cs @@ -27,6 +27,17 @@ public DataTypeTreeControllerBase(IEntityService entityService, IDataTypeService protected override UmbracoObjectTypes FolderObjectType => UmbracoObjectTypes.DataTypeContainer; + protected override Ordering ItemOrdering + { + get + { + var ordering = Ordering.By(nameof(Infrastructure.Persistence.Dtos.NodeDto.NodeObjectType), Direction.Descending); // We need to override to change direction + ordering.Next = Ordering.By(nameof(Infrastructure.Persistence.Dtos.NodeDto.Text)); + + return ordering; + } + } + protected override DataTypeTreeItemResponseModel[] MapTreeItemViewModels(Guid? parentId, IEntitySlim[] entities) { var dataTypes = _dataTypeService diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Tree/FolderTreeControllerBase.cs b/src/Umbraco.Cms.Api.Management/Controllers/Tree/FolderTreeControllerBase.cs index 0dee74bea3ff..2a33d95aa313 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Tree/FolderTreeControllerBase.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Tree/FolderTreeControllerBase.cs @@ -14,6 +14,20 @@ public abstract class FolderTreeControllerBase : NamedEntityTreeControlle private readonly Guid _folderObjectTypeId; private bool _foldersOnly; + + + protected override Ordering ItemOrdering + { + get + { + // Override to order by type (folder vs item) before the text + var ordering = Ordering.By(nameof(Infrastructure.Persistence.Dtos.NodeDto.NodeObjectType)); + ordering.Next = Ordering.By(nameof(Infrastructure.Persistence.Dtos.NodeDto.Text)); + + return ordering; + } + } + protected FolderTreeControllerBase(IEntityService entityService) : base(entityService) => // ReSharper disable once VirtualMemberCallInConstructor @@ -76,38 +90,19 @@ private IEntitySlim[] GetEntities(Guid? parentKey, int skip, int take, out long { totalItems = 0; - if (take == 0) - { - totalItems = _foldersOnly - ? EntityService.CountChildren(parentKey, FolderObjectType) - : EntityService.CountChildren(parentKey, FolderObjectType) - + EntityService.CountChildren(parentKey, ItemObjectType); - return Array.Empty(); - } + UmbracoObjectTypes[] childObjectTypes = _foldersOnly ? [FolderObjectType] : [FolderObjectType, ItemObjectType]; - // EntityService is not able to paginate children of multiple item types, so we will only paginate the - // item type entities and always return all folders as part of the the first result "page" i.e. when skip is 0 - IEntitySlim[] folderEntities = skip == 0 - ? EntityService.GetChildren(parentKey, FolderObjectType).OrderBy(c => c.Name).ToArray() - : Array.Empty(); - IEntitySlim[] itemEntities = _foldersOnly - ? Array.Empty() - : EntityService.GetPagedChildren( + IEntitySlim[] itemEntities = EntityService.GetPagedChildren( parentKey, new [] { FolderObjectType, ItemObjectType }, - ItemObjectType, + childObjectTypes, skip, take, + false, out totalItems, ordering: ItemOrdering) .ToArray(); - // the GetChildren for folders does not return an amount and does not get executed when beyond the first page - // but the items still count towards the total, so add these to either 0 when only folders, or the out param from paged - totalItems += skip == 0 - ? folderEntities.Length - : EntityService.CountChildren(parentKey, FolderObjectType); - - return folderEntities.Union(itemEntities).ToArray(); + return itemEntities; } } diff --git a/src/Umbraco.Core/Persistence/Repositories/IEntityRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IEntityRepository.cs index d225651eef9f..477f3e5c500b 100644 --- a/src/Umbraco.Core/Persistence/Repositories/IEntityRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/IEntityRepository.cs @@ -84,7 +84,19 @@ IEnumerable GetPagedResultsByQuery( int pageSize, out long totalRecords, IQuery? filter, + Ordering? ordering) => + GetPagedResultsByQuery(query, new HashSet(){objectType}, pageIndex, pageSize, out totalRecords, filter, ordering); + + IEnumerable GetPagedResultsByQuery( + IQuery query, + ISet objectTypes, + long pageIndex, + int pageSize, + out long totalRecords, + IQuery? filter, Ordering? ordering); - int CountByQuery(IQuery query, Guid objectType, IQuery? filter); + int CountByQuery(IQuery query, Guid objectType, IQuery? filter) => + CountByQuery(query, new HashSet() { objectType }, filter); + int CountByQuery(IQuery query, IEnumerable objectTypes, IQuery? filter); } diff --git a/src/Umbraco.Core/Services/EntityService.cs b/src/Umbraco.Core/Services/EntityService.cs index d2941e919cf1..7f2ba473b7a1 100644 --- a/src/Umbraco.Core/Services/EntityService.cs +++ b/src/Umbraco.Core/Services/EntityService.cs @@ -396,6 +396,12 @@ public IEnumerable GetPagedChildren( return Enumerable.Empty(); } + if (take == 0) + { + totalRecords = CountChildren(parentId, childObjectType, filter); + return Enumerable.Empty(); + } + PaginationHelper.ConvertSkipTakeToPaging(skip, take, out var pageNumber, out var pageSize); IEnumerable children = GetPagedChildren( @@ -645,34 +651,23 @@ public int ReserveId(Guid key) } } - public int CountChildren( + private int CountChildren(int id, UmbracoObjectTypes objectType, IQuery? filter = null) => + CountChildren(id, new HashSet() { objectType }, filter); + + private int CountChildren( int id, - UmbracoObjectTypes objectType, + IEnumerable objectTypes, IQuery? filter = null) { using (ScopeProvider.CreateCoreScope(autoComplete: true)) { IQuery query = Query().Where(x => x.ParentId == id && x.Trashed == false); - return _entityRepository.CountByQuery(query, objectType.GetGuid(), filter); + var objectTypeGuids = objectTypes.Select(x => x.GetGuid()).ToHashSet(); + return _entityRepository.CountByQuery(query, objectTypeGuids, filter); } } - public int CountChildren(Guid? key, UmbracoObjectTypes objectType, IQuery? filter = null) - { - using ICoreScope scope = ScopeProvider.CreateCoreScope(); - - if (ResolveKey(key, objectType, out var parentId) is false) - { - return 0; - } - - var count = CountChildren(parentId, objectType, filter); - - scope.Complete(); - return count; - } - private bool ResolveKey(Guid? key, UmbracoObjectTypes objectType, out int id) { // We have to explicitly check for "root key" since this value is null, and GetId does not accept null. @@ -721,5 +716,47 @@ private IEnumerable GetPagedChildren( return _entityRepository.GetPagedResultsByQuery(query, objectType.GetGuid(), pageIndex, pageSize, out totalRecords, filter, ordering); } } + + public IEnumerable GetPagedChildren( + Guid? parentKey, + IEnumerable parentObjectTypes, + IEnumerable childObjectTypes, + int skip, + int take, + bool trashed, + out long totalRecords, + IQuery? filter = null, + Ordering? ordering = null) + { + using (ScopeProvider.CreateCoreScope(autoComplete: true)) + { + var parentId = 0; + var parentIdResolved = parentObjectTypes.Any(parentObjectType => ResolveKey(parentKey, parentObjectType, out parentId)); + if (parentIdResolved is false) + { + totalRecords = 0; + return Enumerable.Empty(); + } + + if (take == 0) + { + totalRecords = CountChildren(parentId, childObjectTypes, filter); + return Array.Empty(); + } + + IQuery query = Query().Where(x => x.ParentId == parentId && x.Trashed == trashed); + + PaginationHelper.ConvertSkipTakeToPaging(skip, take, out var pageNumber, out var pageSize); + + var objectTypeGuids = childObjectTypes.Select(x => x.GetGuid()).ToHashSet(); + if (pageSize == 0) + { + totalRecords = _entityRepository.CountByQuery(query, objectTypeGuids, filter); + return Enumerable.Empty(); + } + + return _entityRepository.GetPagedResultsByQuery(query, objectTypeGuids, pageNumber, pageSize, out totalRecords, filter, ordering); + } + } } diff --git a/src/Umbraco.Core/Services/IEntityService.cs b/src/Umbraco.Core/Services/IEntityService.cs index a2ee9f11a3a2..08ff2feb8c46 100644 --- a/src/Umbraco.Core/Services/IEntityService.cs +++ b/src/Umbraco.Core/Services/IEntityService.cs @@ -233,6 +233,21 @@ IEnumerable GetPagedChildren( return Array.Empty(); } + IEnumerable GetPagedChildren( + Guid? parentKey, + IEnumerable parentObjectTypes, + IEnumerable childObjectTypes, + int skip, + int take, + bool trashed, + out long totalRecords, + IQuery? filter = null, + Ordering? ordering = null) + { + totalRecords = 0; + return Array.Empty(); + } + /// /// Gets children of an entity. /// @@ -367,11 +382,4 @@ IEnumerable GetPagedDescendants( /// The identifier. /// When a new content or a media is saved with the key, it will have the reserved identifier. int ReserveId(Guid key); - - /// - /// Counts the children of an entity - /// - int CountChildren(int id, UmbracoObjectTypes objectType, IQuery? filter = null); - - public int CountChildren(Guid? key, UmbracoObjectTypes objectType, IQuery? filter = null) => 0; } diff --git a/src/Umbraco.Core/Services/Ordering.cs b/src/Umbraco.Core/Services/Ordering.cs index 39c89e5c4a43..6ab42426175a 100644 --- a/src/Umbraco.Core/Services/Ordering.cs +++ b/src/Umbraco.Core/Services/Ordering.cs @@ -34,6 +34,7 @@ public Ordering(string? orderBy, Direction direction = Direction.Ascending, stri IsCustomField = isCustomField; } + public Ordering? Next { get; set; } = null; /// /// Gets the name of the ordering field. /// diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/EntityRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/EntityRepository.cs index 96b455d208ab..ec0b06a45174 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/EntityRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/EntityRepository.cs @@ -30,13 +30,13 @@ public EntityRepository(IScopeAccessor scopeAccessor, AppCaches appCaches) #region Repository - public int CountByQuery(IQuery query, Guid objectType, IQuery? filter) + public int CountByQuery(IQuery query, IEnumerable objectTypes, IQuery? filter) { Sql sql = Sql(); sql.SelectCount(); sql .From(); - sql.WhereIn(x => x.NodeObjectType, new[] { objectType } ); + sql.WhereIn(x => x.NodeObjectType, objectTypes ); foreach (Tuple queryClause in query.GetWhereClauses()) { @@ -54,10 +54,11 @@ public int CountByQuery(IQuery query, Guid objectType, IQuery(sql); } - public IEnumerable GetPagedResultsByQuery(IQuery query, Guid objectType, + public IEnumerable GetPagedResultsByQuery(IQuery query, ISet objectTypes, long pageIndex, int pageSize, out long totalRecords, IQuery? filter, Ordering? ordering) => - GetPagedResultsByQuery(query, new[] {objectType}, pageIndex, pageSize, out totalRecords, filter, ordering); + GetPagedResultsByQuery(query, objectTypes.ToArray(), pageIndex, pageSize, out totalRecords, filter, ordering); + // get a page of entities public IEnumerable GetPagedResultsByQuery(IQuery query, Guid[] objectTypes, @@ -635,25 +636,40 @@ private void ApplyOrdering(ref Sql sql, Ordering ordering) // TODO: although the default ordering string works for name, it wont work for others without a table or an alias of some sort // As more things are attempted to be sorted we'll prob have to add more expressions here string orderBy; - switch (ordering.OrderBy?.ToUpperInvariant()) - { - case "PATH": - orderBy = SqlSyntax.GetQuotedColumn(NodeDto.TableName, "path"); - break; - default: - orderBy = ordering.OrderBy ?? string.Empty; - break; - } + Ordering? runner = ordering; - if (ordering.Direction == Direction.Ascending) + do { - sql.OrderBy(orderBy); - } - else - { - sql.OrderByDescending(orderBy); + + switch (runner.OrderBy?.ToUpperInvariant()) + { + case "NODEOBJECTTYPE": + orderBy = $"UPPER({SqlSyntax.GetQuotedColumn(NodeDto.TableName, "nodeObjectType")})"; + break; + case "PATH": + orderBy = SqlSyntax.GetQuotedColumn(NodeDto.TableName, "path"); + break; + + default: + orderBy = runner.OrderBy ?? string.Empty; + break; + } + + if (runner.Direction == Direction.Ascending) + { + sql.OrderBy(orderBy); + } + else + { + sql.OrderByDescending(orderBy); + } + + runner = runner.Next; } + while (runner is not null); + + } #endregion