Add ancestor endpoints and remove explicit parent context (#15746)

* Remove explicit parent context in API outputs

* Add ancestor endpoints for document and data type (experimental for now)

* Add ancestor endpoints for doctypes, media, mediatypes, partial views, scripts, static files, stylesheets and templates

* Add unit tests for ancestor ID parsing

* Add ancestor endpoint for dictionary items

* Update OpenApi.json

* Fix merge and regenerate OpenApi.json

* Regenerate OpenApi.json

* Rename "folder" to "parent" for consistency

* Fix merge

* Fix merge

* Include "self" in ancestor endpoints

* Handle ancestors for root items correctly

* Remove "type" from recycle bin items

* Tests against fixed values instead of calculated ones.

---------

Co-authored-by: Sven Geusens <sge@umbraco.dk>
This commit is contained in:
Kenn Jacobsen
2024-03-25 12:15:50 +01:00
committed by GitHub
parent e441639786
commit f6f868e463
24 changed files with 1308 additions and 53 deletions

View File

@@ -0,0 +1,22 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Management.ViewModels.Tree;
using Umbraco.Cms.Core.Services;
namespace Umbraco.Cms.Api.Management.Controllers.DataType.Tree;
[ApiVersion("1.0")]
public class AncestorsDataTypeTreeController : DataTypeTreeControllerBase
{
public AncestorsDataTypeTreeController(IEntityService entityService, IDataTypeService dataTypeService)
: base(entityService, dataTypeService)
{
}
[HttpGet("ancestors")]
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(IEnumerable<DataTypeTreeItemResponseModel>), StatusCodes.Status200OK)]
public async Task<ActionResult<IEnumerable<DataTypeTreeItemResponseModel>>> Ancestors(Guid descendantId)
=> await GetAncestors(descendantId);
}

View File

@@ -0,0 +1,22 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Management.ViewModels.Tree;
using Umbraco.Cms.Core.Services;
namespace Umbraco.Cms.Api.Management.Controllers.Dictionary.Tree;
[ApiVersion("1.0")]
public class AncestorsDictionaryTreeController : DictionaryTreeControllerBase
{
public AncestorsDictionaryTreeController(IEntityService entityService, IDictionaryItemService dictionaryItemService)
: base(entityService, dictionaryItemService)
{
}
[HttpGet("ancestors")]
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(IEnumerable<NamedEntityTreeItemResponseModel>), StatusCodes.Status200OK)]
public async Task<ActionResult<IEnumerable<NamedEntityTreeItemResponseModel>>> Ancestors(Guid descendantId)
=> await GetAncestors(descendantId);
}

View File

@@ -23,6 +23,6 @@ public class ChildrenDictionaryTreeController : DictionaryTreeControllerBase
{
PagedModel<IDictionaryItem> paginatedItems = await DictionaryItemService.GetPagedAsync(parentId, skip, take);
return Ok(PagedViewModel(await MapTreeItemViewModels(parentId, paginatedItems.Items), paginatedItems.Total));
return Ok(PagedViewModel(await MapTreeItemViewModels(paginatedItems.Items), paginatedItems.Total));
}
}

View File

@@ -27,9 +27,38 @@ public class DictionaryTreeControllerBase : NamedEntityTreeControllerBase<NamedE
protected IDictionaryItemService DictionaryItemService { get; }
protected async Task<IEnumerable<NamedEntityTreeItemResponseModel>> MapTreeItemViewModels(Guid? parentKey, IEnumerable<IDictionaryItem> dictionaryItems)
protected async Task<IEnumerable<NamedEntityTreeItemResponseModel>> MapTreeItemViewModels(IEnumerable<IDictionaryItem> dictionaryItems)
=> await Task.WhenAll(dictionaryItems.Select(CreateEntityTreeItemViewModelAsync));
protected override async Task<ActionResult<IEnumerable<NamedEntityTreeItemResponseModel>>> GetAncestors(Guid descendantKey, bool includeSelf = true)
{
async Task<NamedEntityTreeItemResponseModel> CreateEntityTreeItemViewModelAsync(IDictionaryItem dictionaryItem)
IDictionaryItem? dictionaryItem = await DictionaryItemService.GetAsync(descendantKey);
if (dictionaryItem is null)
{
// this looks weird - but we actually mimic how the rest of the ancestor (and children) endpoints actually work
return Ok(Enumerable.Empty<NamedEntityTreeItemResponseModel>());
}
var ancestors = new List<IDictionaryItem>();
if (includeSelf)
{
ancestors.Add(dictionaryItem);
}
while (dictionaryItem?.ParentId is not null)
{
dictionaryItem = await DictionaryItemService.GetAsync(dictionaryItem.ParentId.Value);
if (dictionaryItem is not null)
{
ancestors.Add(dictionaryItem);
}
}
NamedEntityTreeItemResponseModel[] viewModels = await Task.WhenAll(ancestors.Select(CreateEntityTreeItemViewModelAsync));
return Ok(viewModels.Reverse());
}
private async Task<NamedEntityTreeItemResponseModel> CreateEntityTreeItemViewModelAsync(IDictionaryItem dictionaryItem)
{
var hasChildren = await DictionaryItemService.CountChildrenAsync(dictionaryItem.Key) > 0;
return new NamedEntityTreeItemResponseModel
@@ -37,15 +66,12 @@ public class DictionaryTreeControllerBase : NamedEntityTreeControllerBase<NamedE
Name = dictionaryItem.ItemKey,
Id = dictionaryItem.Key,
HasChildren = hasChildren,
Parent = parentKey.HasValue
Parent = dictionaryItem.ParentId.HasValue
? new ReferenceByIdModel
{
Id = parentKey.Value
Id = dictionaryItem.ParentId.Value
}
: null
};
}
return await Task.WhenAll(dictionaryItems.Select(CreateEntityTreeItemViewModelAsync));
}
}

View File

@@ -23,6 +23,6 @@ public class RootDictionaryTreeController : DictionaryTreeControllerBase
{
PagedModel<IDictionaryItem> paginatedItems = await DictionaryItemService.GetPagedAsync(null, skip, take);
return Ok(PagedViewModel(await MapTreeItemViewModels(null, paginatedItems.Items), paginatedItems.Total));
return Ok(PagedViewModel(await MapTreeItemViewModels(paginatedItems.Items), paginatedItems.Total));
}
}

View File

@@ -0,0 +1,40 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Management.Factories;
using Umbraco.Cms.Api.Management.Services.Entities;
using Umbraco.Cms.Api.Management.ViewModels.Tree;
using Umbraco.Cms.Core.Cache;
using Umbraco.Cms.Core.Security;
using Umbraco.Cms.Core.Services;
namespace Umbraco.Cms.Api.Management.Controllers.Document.Tree;
[ApiVersion("1.0")]
public class AncestorsDocumentTreeController : DocumentTreeControllerBase
{
public AncestorsDocumentTreeController(
IEntityService entityService,
IUserStartNodeEntitiesService userStartNodeEntitiesService,
IDataTypeService dataTypeService,
IPublicAccessService publicAccessService,
AppCaches appCaches,
IBackOfficeSecurityAccessor backofficeSecurityAccessor,
IDocumentPresentationFactory documentPresentationFactory)
: base(
entityService,
userStartNodeEntitiesService,
dataTypeService,
publicAccessService,
appCaches,
backofficeSecurityAccessor,
documentPresentationFactory)
{
}
[HttpGet("ancestors")]
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(IEnumerable<DocumentTreeItemResponseModel>), StatusCodes.Status200OK)]
public async Task<ActionResult<IEnumerable<DocumentTreeItemResponseModel>>> Ancestors(Guid descendantId)
=> await GetAncestors(descendantId);
}

View File

@@ -0,0 +1,22 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Management.ViewModels.Tree;
using Umbraco.Cms.Core.Services;
namespace Umbraco.Cms.Api.Management.Controllers.DocumentType.Tree;
[ApiVersion("1.0")]
public class AncestorsDocumentTypeTreeController : DocumentTypeTreeControllerBase
{
public AncestorsDocumentTypeTreeController(IEntityService entityService, IContentTypeService contentTypeService)
: base(entityService, contentTypeService)
{
}
[HttpGet("ancestors")]
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(IEnumerable<DocumentTypeTreeItemResponseModel>), StatusCodes.Status200OK)]
public async Task<ActionResult<IEnumerable<DocumentTypeTreeItemResponseModel>>> Ancestors(Guid descendantId)
=> await GetAncestors(descendantId);
}

View File

@@ -0,0 +1,32 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Management.Factories;
using Umbraco.Cms.Api.Management.Services.Entities;
using Umbraco.Cms.Api.Management.ViewModels.Tree;
using Umbraco.Cms.Core.Cache;
using Umbraco.Cms.Core.Security;
using Umbraco.Cms.Core.Services;
namespace Umbraco.Cms.Api.Management.Controllers.Media.Tree;
[ApiVersion("1.0")]
public class AncestorsMediaTreeController : MediaTreeControllerBase
{
public AncestorsMediaTreeController(
IEntityService entityService,
IUserStartNodeEntitiesService userStartNodeEntitiesService,
IDataTypeService dataTypeService,
AppCaches appCaches,
IBackOfficeSecurityAccessor backofficeSecurityAccessor,
IMediaPresentationFactory mediaPresentationFactory)
: base(entityService, userStartNodeEntitiesService, dataTypeService, appCaches, backofficeSecurityAccessor, mediaPresentationFactory)
{
}
[HttpGet("ancestors")]
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(IEnumerable<MediaTreeItemResponseModel>), StatusCodes.Status200OK)]
public async Task<ActionResult<IEnumerable<MediaTreeItemResponseModel>>> Ancestors(Guid descendantId)
=> await GetAncestors(descendantId);
}

View File

@@ -0,0 +1,22 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Management.ViewModels.Tree;
using Umbraco.Cms.Core.Services;
namespace Umbraco.Cms.Api.Management.Controllers.MediaType.Tree;
[ApiVersion("1.0")]
public class AncestorsMediaTypeTreeController : MediaTypeTreeControllerBase
{
public AncestorsMediaTypeTreeController(IEntityService entityService, IMediaTypeService mediaTypeService)
: base(entityService, mediaTypeService)
{
}
[HttpGet("ancestors")]
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(IEnumerable<MediaTypeTreeItemResponseModel>), StatusCodes.Status200OK)]
public async Task<ActionResult<IEnumerable<MediaTypeTreeItemResponseModel>>> Ancestors(Guid descendantId)
=> await GetAncestors(descendantId);
}

View File

@@ -0,0 +1,22 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Management.ViewModels.Tree;
using Umbraco.Cms.Core.IO;
namespace Umbraco.Cms.Api.Management.Controllers.PartialView.Tree;
[ApiVersion("1.0")]
public class AncestorsPartialViewTreeController : PartialViewTreeControllerBase
{
public AncestorsPartialViewTreeController(FileSystems fileSystems)
: base(fileSystems)
{
}
[HttpGet("ancestors")]
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(IEnumerable<FileSystemTreeItemPresentationModel>), StatusCodes.Status200OK)]
public async Task<ActionResult<IEnumerable<FileSystemTreeItemPresentationModel>>> Ancestors(string descendantPath)
=> await GetAncestors(descendantPath);
}

View File

@@ -15,14 +15,9 @@ public abstract class RecycleBinControllerBase<TItem> : ContentControllerBase
where TItem : RecycleBinItemResponseModelBase, new()
{
private readonly IEntityService _entityService;
private readonly string _itemUdiType;
protected RecycleBinControllerBase(IEntityService entityService)
{
_entityService = entityService;
// ReSharper disable once VirtualMemberCallInConstructor
_itemUdiType = ItemObjectType.GetUdiType();
}
=> _entityService = entityService;
protected abstract UmbracoObjectTypes ItemObjectType { get; }
@@ -59,7 +54,6 @@ public abstract class RecycleBinControllerBase<TItem> : ContentControllerBase
var viewModel = new TItem
{
Id = entity.Key,
Type = _itemUdiType,
HasChildren = entity.HasChildren,
Parent = parentKey.HasValue
? new ItemReferenceByIdResponseModel

View File

@@ -0,0 +1,22 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Management.ViewModels.Tree;
using Umbraco.Cms.Core.IO;
namespace Umbraco.Cms.Api.Management.Controllers.Script.Tree;
[ApiVersion("1.0")]
public class AncestorsScriptTreeController : ScriptTreeControllerBase
{
public AncestorsScriptTreeController(FileSystems fileSystems)
: base(fileSystems)
{
}
[HttpGet("ancestors")]
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(IEnumerable<FileSystemTreeItemPresentationModel>), StatusCodes.Status200OK)]
public async Task<ActionResult<IEnumerable<FileSystemTreeItemPresentationModel>>> Ancestors(string descendantPath)
=> await GetAncestors(descendantPath);
}

View File

@@ -0,0 +1,22 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Management.ViewModels.Tree;
using Umbraco.Cms.Core.IO;
namespace Umbraco.Cms.Api.Management.Controllers.StaticFile.Tree;
[ApiVersion("1.0")]
public class AncestorsStaticFileTreeController : StaticFileTreeControllerBase
{
public AncestorsStaticFileTreeController(IPhysicalFileSystem physicalFileSystem)
: base(physicalFileSystem)
{
}
[HttpGet("ancestors")]
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(IEnumerable<FileSystemTreeItemPresentationModel>), StatusCodes.Status200OK)]
public async Task<ActionResult<IEnumerable<FileSystemTreeItemPresentationModel>>> Ancestors(string descendantPath)
=> await GetAncestors(descendantPath);
}

View File

@@ -1,6 +1,7 @@
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Management.Controllers.Tree;
using Umbraco.Cms.Api.Management.Routing;
using Umbraco.Cms.Api.Management.ViewModels.Tree;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.IO;
@@ -29,7 +30,12 @@ public class StaticFileTreeControllerBase : FileSystemTreeControllerBase
? Array.Empty<string>()
: base.GetFiles(path);
protected override FileSystemTreeItemPresentationModel[] GetAncestorModels(string path, bool includeSelf)
=> IsAllowedPath(path)
? base.GetAncestorModels(path, includeSelf)
: Array.Empty<FileSystemTreeItemPresentationModel>();
private bool IsTreeRootPath(string path) => string.IsNullOrWhiteSpace(path);
private bool IsAllowedPath(string path) => _allowedRootFolders.Contains(path) || _allowedRootFolders.Any(folder => path.StartsWith($"{folder}/"));
private bool IsAllowedPath(string path) => _allowedRootFolders.Contains(path) || _allowedRootFolders.Any(folder => path.StartsWith($"{folder}{Path.DirectorySeparatorChar}"));
}

View File

@@ -0,0 +1,22 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Management.ViewModels.Tree;
using Umbraco.Cms.Core.IO;
namespace Umbraco.Cms.Api.Management.Controllers.Stylesheet.Tree;
[ApiVersion("1.0")]
public class AncestorsStylesheetTreeController : StylesheetTreeControllerBase
{
public AncestorsStylesheetTreeController(FileSystems fileSystems)
: base(fileSystems)
{
}
[HttpGet("ancestors")]
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(IEnumerable<FileSystemTreeItemPresentationModel>), StatusCodes.Status200OK)]
public async Task<ActionResult<IEnumerable<FileSystemTreeItemPresentationModel>>> Ancestors(string descendantPath)
=> await GetAncestors(descendantPath);
}

View File

@@ -0,0 +1,22 @@
using Asp.Versioning;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Management.ViewModels.Tree;
using Umbraco.Cms.Core.Services;
namespace Umbraco.Cms.Api.Management.Controllers.Template.Tree;
[ApiVersion("1.0")]
public class AncestorsTemplateTreeController : TemplateTreeControllerBase
{
public AncestorsTemplateTreeController(IEntityService entityService)
: base(entityService)
{
}
[HttpGet("ancestors")]
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(IEnumerable<NamedEntityTreeItemResponseModel>), StatusCodes.Status200OK)]
public async Task<ActionResult<IEnumerable<NamedEntityTreeItemResponseModel>>> Ancestors(Guid descendantId)
=> await GetAncestors(descendantId);
}

View File

@@ -3,10 +3,10 @@ using Umbraco.Cms.Api.Common.ViewModels.Pagination;
using Umbraco.Cms.Api.Management.ViewModels;
using Umbraco.Cms.Api.Management.ViewModels.Tree;
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Extensions;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.Entities;
using Umbraco.Cms.Core.Services;
using Umbraco.Extensions;
namespace Umbraco.Cms.Api.Management.Controllers.Tree;
@@ -42,18 +42,41 @@ public abstract class EntityTreeControllerBase<TItem> : ManagementApiControllerB
return await Task.FromResult(Ok(result));
}
protected async Task<ActionResult<IEnumerable<TItem>>> GetItems(Guid[] ids)
protected virtual async Task<ActionResult<IEnumerable<TItem>>> GetAncestors(Guid descendantKey, bool includeSelf = true)
{
if (ids.IsCollectionEmpty())
IEntitySlim[] ancestorEntities = await GetAncestorEntitiesAsync(descendantKey, includeSelf);
TItem[] result = ancestorEntities
.Select(ancestor =>
{
return await Task.FromResult(Ok(PagedViewModel(Array.Empty<TItem>(), 0)));
IEntitySlim? parent = ancestor.ParentId > 0
? ancestorEntities.Single(a => a.Id == ancestor.ParentId)
: null;
return MapTreeItemViewModel(parent?.Key, ancestor);
})
.ToArray();
return Ok(result);
}
IEntitySlim[] itemEntities = GetEntities(ids);
protected virtual async Task<IEntitySlim[]> GetAncestorEntitiesAsync(Guid descendantKey, bool includeSelf)
{
IEntitySlim? entity = EntityService.Get(descendantKey, ItemObjectType);
if (entity is null)
{
// not much else we can do here but return nothing
return await Task.FromResult(Array.Empty<IEntitySlim>());
}
TItem[] treeItemViewModels = MapTreeItemViewModels(null, itemEntities);
var ancestorIds = entity.AncestorIds();
return await Task.FromResult(Ok(treeItemViewModels));
IEnumerable<IEntitySlim> ancestors = ancestorIds.Any()
? EntityService.GetAll(ItemObjectType, ancestorIds)
: Array.Empty<IEntitySlim>();
ancestors = ancestors.Union(includeSelf ? new[] { entity } : Array.Empty<IEntitySlim>());
return ancestors.OrderBy(item => item.Level).ToArray();
}
protected virtual IEntitySlim[] GetPagedRootEntities(int skip, int take, out long totalItems)
@@ -77,8 +100,6 @@ public abstract class EntityTreeControllerBase<TItem> : ManagementApiControllerB
ordering: ItemOrdering)
.ToArray();
protected virtual IEntitySlim[] GetEntities(Guid[] ids) => EntityService.GetAll(ItemObjectType, ids).ToArray();
protected virtual TItem[] MapTreeItemViewModels(Guid? parentKey, IEntitySlim[] entities)
=> entities.Select(entity => MapTreeItemViewModel(parentKey, entity)).ToArray();

View File

@@ -28,6 +28,30 @@ public abstract class FileSystemTreeControllerBase : ManagementApiControllerBase
return await Task.FromResult(Ok(result));
}
protected virtual async Task<ActionResult<IEnumerable<FileSystemTreeItemPresentationModel>>> GetAncestors(string path, bool includeSelf = true)
{
path = path.VirtualPathToSystemPath();
FileSystemTreeItemPresentationModel[] models = GetAncestorModels(path, includeSelf);
return await Task.FromResult(Ok(models));
}
protected virtual FileSystemTreeItemPresentationModel[] GetAncestorModels(string path, bool includeSelf)
{
var directories = path.Split(Path.DirectorySeparatorChar).Take(Range.EndAt(Index.FromEnd(1))).ToArray();
var result = directories
.Select((directory, index) => MapViewModel(string.Join(Path.DirectorySeparatorChar, directories.Take(index + 1)), directory, true))
.ToList();
if (includeSelf)
{
var selfIsFolder = FileSystem.FileExists(path) is false;
result.Add(MapViewModel(path, GetFileSystemItemName(selfIsFolder, path), selfIsFolder));
}
return result.ToArray();
}
protected virtual string[] GetDirectories(string path) => FileSystem
.GetDirectories(path)
.OrderBy(directory => directory)
@@ -54,7 +78,7 @@ public abstract class FileSystemTreeControllerBase : ManagementApiControllerBase
FileSystemTreeItemPresentationModel ViewModel(string itemPath, bool isFolder)
=> MapViewModel(
itemPath,
isFolder ? Path.GetFileName(itemPath) : FileSystem.GetFileName(itemPath),
GetFileSystemItemName(isFolder, itemPath),
isFolder);
return allItems
@@ -64,6 +88,10 @@ public abstract class FileSystemTreeControllerBase : ManagementApiControllerBase
.ToArray();
}
private string GetFileSystemItemName(bool isFolder, string itemPath) => isFolder
? Path.GetFileName(itemPath)
: FileSystem.GetFileName(itemPath);
private PagedViewModel<FileSystemTreeItemPresentationModel> PagedViewModel(IEnumerable<FileSystemTreeItemPresentationModel> viewModels, long totalItems)
=> new() { Total = totalItems, Items = viewModels };

View File

@@ -3,6 +3,8 @@ using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Core.Models.Entities;
using Umbraco.Cms.Core.Services;
using Umbraco.Cms.Core.Extensions;
using Umbraco.Extensions;
namespace Umbraco.Cms.Api.Management.Controllers.Tree;
@@ -47,6 +49,29 @@ public abstract class FolderTreeControllerBase<TItem> : NamedEntityTreeControlle
return viewModel;
}
protected override async Task<IEntitySlim[]> GetAncestorEntitiesAsync(Guid descendantKey, bool includeSelf = true)
{
IEntitySlim? entity = EntityService.Get(descendantKey, ItemObjectType)
?? EntityService.Get(descendantKey, FolderObjectType);
if (entity is null)
{
// not much else we can do here but return nothing
return await Task.FromResult(Array.Empty<IEntitySlim>());
}
var ancestorIds = entity.AncestorIds();
// annoyingly we can't use EntityService.GetAll() with container object types, so we have to get them one by one
IEntitySlim[] containers = ancestorIds.Select(id => EntityService.Get(id, FolderObjectType)).WhereNotNull().ToArray();
IEnumerable<IEntitySlim> ancestors = ancestorIds.Any()
? EntityService
.GetAll(ItemObjectType, ancestorIds)
.Union(containers)
: Array.Empty<IEntitySlim>();
ancestors = ancestors.Union(includeSelf ? new[] { entity } : Array.Empty<IEntitySlim>());
return ancestors.OrderBy(item => item.Level).ToArray();
}
private IEntitySlim[] GetEntities(Guid? parentKey, int skip, int take, out long totalItems)
{
totalItems = 0;

View File

@@ -48,14 +48,6 @@ public abstract class UserStartNodeTreeControllerBase<TItem> : EntityTreeControl
: CalculateAccessMap(() => _userStartNodeEntitiesService.ChildUserAccessEntities(children, UserStartNodePaths), out totalItems);
}
protected override IEntitySlim[] GetEntities(Guid[] keys)
{
IEntitySlim[] entities = base.GetEntities(keys);
return UserHasRootAccess() || IgnoreUserStartNodes()
? entities
: CalculateAccessMap(() => _userStartNodeEntitiesService.UserAccessEntities(entities, UserStartNodePaths), out _);
}
protected override TItem[] MapTreeItemViewModels(Guid? parentKey, IEntitySlim[] entities)
{
if (UserHasRootAccess() || IgnoreUserStartNodes())

View File

@@ -1818,6 +1818,75 @@
]
}
},
"/umbraco/management/api/v1/tree/data-type/ancestors": {
"get": {
"tags": [
"Data Type"
],
"operationId": "GetTreeDataTypeAncestors",
"parameters": [
{
"name": "descendantId",
"in": "query",
"schema": {
"type": "string",
"format": "uuid"
}
}
],
"responses": {
"200": {
"description": "Success",
"content": {
"application/json": {
"schema": {
"type": "array",
"items": {
"oneOf": [
{
"$ref": "#/components/schemas/DataTypeTreeItemResponseModel"
}
]
}
}
},
"text/json": {
"schema": {
"type": "array",
"items": {
"oneOf": [
{
"$ref": "#/components/schemas/DataTypeTreeItemResponseModel"
}
]
}
}
},
"text/plain": {
"schema": {
"type": "array",
"items": {
"oneOf": [
{
"$ref": "#/components/schemas/DataTypeTreeItemResponseModel"
}
]
}
}
}
}
},
"401": {
"description": "The resource is protected and requires an authentication token"
}
},
"security": [
{
"Backoffice User": [ ]
}
]
}
},
"/umbraco/management/api/v1/tree/data-type/children": {
"get": {
"tags": [
@@ -2985,6 +3054,129 @@
]
}
},
"/umbraco/management/api/v1/tree/dictionary/ancestors": {
"get": {
"tags": [
"Dictionary"
],
"operationId": "GetTreeDictionaryAncestors",
"parameters": [
{
"name": "descendantId",
"in": "query",
"schema": {
"type": "string",
"format": "uuid"
}
}
],
"responses": {
"200": {
"description": "Success",
"content": {
"application/json": {
"schema": {
"type": "array",
"items": {
"oneOf": [
{
"$ref": "#/components/schemas/NamedEntityTreeItemResponseModel"
},
{
"$ref": "#/components/schemas/DataTypeTreeItemResponseModel"
},
{
"$ref": "#/components/schemas/DocumentBlueprintTreeItemResponseModel"
},
{
"$ref": "#/components/schemas/DocumentTypeTreeItemResponseModel"
},
{
"$ref": "#/components/schemas/FolderTreeItemResponseModel"
},
{
"$ref": "#/components/schemas/MediaTypeTreeItemResponseModel"
},
{
"$ref": "#/components/schemas/RelationTypeTreeItemResponseModel"
}
]
}
}
},
"text/json": {
"schema": {
"type": "array",
"items": {
"oneOf": [
{
"$ref": "#/components/schemas/NamedEntityTreeItemResponseModel"
},
{
"$ref": "#/components/schemas/DataTypeTreeItemResponseModel"
},
{
"$ref": "#/components/schemas/DocumentBlueprintTreeItemResponseModel"
},
{
"$ref": "#/components/schemas/DocumentTypeTreeItemResponseModel"
},
{
"$ref": "#/components/schemas/FolderTreeItemResponseModel"
},
{
"$ref": "#/components/schemas/MediaTypeTreeItemResponseModel"
},
{
"$ref": "#/components/schemas/RelationTypeTreeItemResponseModel"
}
]
}
}
},
"text/plain": {
"schema": {
"type": "array",
"items": {
"oneOf": [
{
"$ref": "#/components/schemas/NamedEntityTreeItemResponseModel"
},
{
"$ref": "#/components/schemas/DataTypeTreeItemResponseModel"
},
{
"$ref": "#/components/schemas/DocumentBlueprintTreeItemResponseModel"
},
{
"$ref": "#/components/schemas/DocumentTypeTreeItemResponseModel"
},
{
"$ref": "#/components/schemas/FolderTreeItemResponseModel"
},
{
"$ref": "#/components/schemas/MediaTypeTreeItemResponseModel"
},
{
"$ref": "#/components/schemas/RelationTypeTreeItemResponseModel"
}
]
}
}
}
}
},
"401": {
"description": "The resource is protected and requires an authentication token"
}
},
"security": [
{
"Backoffice User": [ ]
}
]
}
},
"/umbraco/management/api/v1/tree/dictionary/children": {
"get": {
"tags": [
@@ -4915,6 +5107,75 @@
]
}
},
"/umbraco/management/api/v1/tree/document-type/ancestors": {
"get": {
"tags": [
"Document Type"
],
"operationId": "GetTreeDocumentTypeAncestors",
"parameters": [
{
"name": "descendantId",
"in": "query",
"schema": {
"type": "string",
"format": "uuid"
}
}
],
"responses": {
"200": {
"description": "Success",
"content": {
"application/json": {
"schema": {
"type": "array",
"items": {
"oneOf": [
{
"$ref": "#/components/schemas/DocumentTypeTreeItemResponseModel"
}
]
}
}
},
"text/json": {
"schema": {
"type": "array",
"items": {
"oneOf": [
{
"$ref": "#/components/schemas/DocumentTypeTreeItemResponseModel"
}
]
}
}
},
"text/plain": {
"schema": {
"type": "array",
"items": {
"oneOf": [
{
"$ref": "#/components/schemas/DocumentTypeTreeItemResponseModel"
}
]
}
}
}
}
},
"401": {
"description": "The resource is protected and requires an authentication token"
}
},
"security": [
{
"Backoffice User": [ ]
}
]
}
},
"/umbraco/management/api/v1/tree/document-type/children": {
"get": {
"tags": [
@@ -8535,6 +8796,75 @@
]
}
},
"/umbraco/management/api/v1/tree/document/ancestors": {
"get": {
"tags": [
"Document"
],
"operationId": "GetTreeDocumentAncestors",
"parameters": [
{
"name": "descendantId",
"in": "query",
"schema": {
"type": "string",
"format": "uuid"
}
}
],
"responses": {
"200": {
"description": "Success",
"content": {
"application/json": {
"schema": {
"type": "array",
"items": {
"oneOf": [
{
"$ref": "#/components/schemas/DocumentTreeItemResponseModel"
}
]
}
}
},
"text/json": {
"schema": {
"type": "array",
"items": {
"oneOf": [
{
"$ref": "#/components/schemas/DocumentTreeItemResponseModel"
}
]
}
}
},
"text/plain": {
"schema": {
"type": "array",
"items": {
"oneOf": [
{
"$ref": "#/components/schemas/DocumentTreeItemResponseModel"
}
]
}
}
}
}
},
"401": {
"description": "The resource is protected and requires an authentication token"
}
},
"security": [
{
"Backoffice User": [ ]
}
]
}
},
"/umbraco/management/api/v1/tree/document/children": {
"get": {
"tags": [
@@ -12748,6 +13078,75 @@
]
}
},
"/umbraco/management/api/v1/tree/media-type/ancestors": {
"get": {
"tags": [
"Media Type"
],
"operationId": "GetTreeMediaTypeAncestors",
"parameters": [
{
"name": "descendantId",
"in": "query",
"schema": {
"type": "string",
"format": "uuid"
}
}
],
"responses": {
"200": {
"description": "Success",
"content": {
"application/json": {
"schema": {
"type": "array",
"items": {
"oneOf": [
{
"$ref": "#/components/schemas/MediaTypeTreeItemResponseModel"
}
]
}
}
},
"text/json": {
"schema": {
"type": "array",
"items": {
"oneOf": [
{
"$ref": "#/components/schemas/MediaTypeTreeItemResponseModel"
}
]
}
}
},
"text/plain": {
"schema": {
"type": "array",
"items": {
"oneOf": [
{
"$ref": "#/components/schemas/MediaTypeTreeItemResponseModel"
}
]
}
}
}
}
},
"401": {
"description": "The resource is protected and requires an authentication token"
}
},
"security": [
{
"Backoffice User": [ ]
}
]
}
},
"/umbraco/management/api/v1/tree/media-type/children": {
"get": {
"tags": [
@@ -14932,6 +15331,75 @@
]
}
},
"/umbraco/management/api/v1/tree/media/ancestors": {
"get": {
"tags": [
"Media"
],
"operationId": "GetTreeMediaAncestors",
"parameters": [
{
"name": "descendantId",
"in": "query",
"schema": {
"type": "string",
"format": "uuid"
}
}
],
"responses": {
"200": {
"description": "Success",
"content": {
"application/json": {
"schema": {
"type": "array",
"items": {
"oneOf": [
{
"$ref": "#/components/schemas/MediaTreeItemResponseModel"
}
]
}
}
},
"text/json": {
"schema": {
"type": "array",
"items": {
"oneOf": [
{
"$ref": "#/components/schemas/MediaTreeItemResponseModel"
}
]
}
}
},
"text/plain": {
"schema": {
"type": "array",
"items": {
"oneOf": [
{
"$ref": "#/components/schemas/MediaTreeItemResponseModel"
}
]
}
}
}
}
},
"401": {
"description": "The resource is protected and requires an authentication token"
}
},
"security": [
{
"Backoffice User": [ ]
}
]
}
},
"/umbraco/management/api/v1/tree/media/children": {
"get": {
"tags": [
@@ -19841,6 +20309,74 @@
]
}
},
"/umbraco/management/api/v1/tree/partial-view/ancestors": {
"get": {
"tags": [
"Partial View"
],
"operationId": "GetTreePartialViewAncestors",
"parameters": [
{
"name": "descendantPath",
"in": "query",
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "Success",
"content": {
"application/json": {
"schema": {
"type": "array",
"items": {
"oneOf": [
{
"$ref": "#/components/schemas/FileSystemTreeItemPresentationModel"
}
]
}
}
},
"text/json": {
"schema": {
"type": "array",
"items": {
"oneOf": [
{
"$ref": "#/components/schemas/FileSystemTreeItemPresentationModel"
}
]
}
}
},
"text/plain": {
"schema": {
"type": "array",
"items": {
"oneOf": [
{
"$ref": "#/components/schemas/FileSystemTreeItemPresentationModel"
}
]
}
}
}
}
},
"401": {
"description": "The resource is protected and requires an authentication token"
}
},
"security": [
{
"Backoffice User": [ ]
}
]
}
},
"/umbraco/management/api/v1/tree/partial-view/children": {
"get": {
"tags": [
@@ -22378,6 +22914,74 @@
]
}
},
"/umbraco/management/api/v1/tree/script/ancestors": {
"get": {
"tags": [
"Script"
],
"operationId": "GetTreeScriptAncestors",
"parameters": [
{
"name": "descendantPath",
"in": "query",
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "Success",
"content": {
"application/json": {
"schema": {
"type": "array",
"items": {
"oneOf": [
{
"$ref": "#/components/schemas/FileSystemTreeItemPresentationModel"
}
]
}
}
},
"text/json": {
"schema": {
"type": "array",
"items": {
"oneOf": [
{
"$ref": "#/components/schemas/FileSystemTreeItemPresentationModel"
}
]
}
}
},
"text/plain": {
"schema": {
"type": "array",
"items": {
"oneOf": [
{
"$ref": "#/components/schemas/FileSystemTreeItemPresentationModel"
}
]
}
}
}
}
},
"401": {
"description": "The resource is protected and requires an authentication token"
}
},
"security": [
{
"Backoffice User": [ ]
}
]
}
},
"/umbraco/management/api/v1/tree/script/children": {
"get": {
"tags": [
@@ -23387,6 +23991,74 @@
]
}
},
"/umbraco/management/api/v1/tree/static-file/ancestors": {
"get": {
"tags": [
"Static File"
],
"operationId": "GetTreeStaticFileAncestors",
"parameters": [
{
"name": "descendantPath",
"in": "query",
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "Success",
"content": {
"application/json": {
"schema": {
"type": "array",
"items": {
"oneOf": [
{
"$ref": "#/components/schemas/FileSystemTreeItemPresentationModel"
}
]
}
}
},
"text/json": {
"schema": {
"type": "array",
"items": {
"oneOf": [
{
"$ref": "#/components/schemas/FileSystemTreeItemPresentationModel"
}
]
}
}
},
"text/plain": {
"schema": {
"type": "array",
"items": {
"oneOf": [
{
"$ref": "#/components/schemas/FileSystemTreeItemPresentationModel"
}
]
}
}
}
}
},
"401": {
"description": "The resource is protected and requires an authentication token"
}
},
"security": [
{
"Backoffice User": [ ]
}
]
}
},
"/umbraco/management/api/v1/tree/static-file/children": {
"get": {
"tags": [
@@ -24527,6 +25199,74 @@
]
}
},
"/umbraco/management/api/v1/tree/stylesheet/ancestors": {
"get": {
"tags": [
"Stylesheet"
],
"operationId": "GetTreeStylesheetAncestors",
"parameters": [
{
"name": "descendantPath",
"in": "query",
"schema": {
"type": "string"
}
}
],
"responses": {
"200": {
"description": "Success",
"content": {
"application/json": {
"schema": {
"type": "array",
"items": {
"oneOf": [
{
"$ref": "#/components/schemas/FileSystemTreeItemPresentationModel"
}
]
}
}
},
"text/json": {
"schema": {
"type": "array",
"items": {
"oneOf": [
{
"$ref": "#/components/schemas/FileSystemTreeItemPresentationModel"
}
]
}
}
},
"text/plain": {
"schema": {
"type": "array",
"items": {
"oneOf": [
{
"$ref": "#/components/schemas/FileSystemTreeItemPresentationModel"
}
]
}
}
}
}
},
"401": {
"description": "The resource is protected and requires an authentication token"
}
},
"security": [
{
"Backoffice User": [ ]
}
]
}
},
"/umbraco/management/api/v1/tree/stylesheet/children": {
"get": {
"tags": [
@@ -25664,6 +26404,129 @@
]
}
},
"/umbraco/management/api/v1/tree/template/ancestors": {
"get": {
"tags": [
"Template"
],
"operationId": "GetTreeTemplateAncestors",
"parameters": [
{
"name": "descendantId",
"in": "query",
"schema": {
"type": "string",
"format": "uuid"
}
}
],
"responses": {
"200": {
"description": "Success",
"content": {
"application/json": {
"schema": {
"type": "array",
"items": {
"oneOf": [
{
"$ref": "#/components/schemas/NamedEntityTreeItemResponseModel"
},
{
"$ref": "#/components/schemas/DataTypeTreeItemResponseModel"
},
{
"$ref": "#/components/schemas/DocumentBlueprintTreeItemResponseModel"
},
{
"$ref": "#/components/schemas/DocumentTypeTreeItemResponseModel"
},
{
"$ref": "#/components/schemas/FolderTreeItemResponseModel"
},
{
"$ref": "#/components/schemas/MediaTypeTreeItemResponseModel"
},
{
"$ref": "#/components/schemas/RelationTypeTreeItemResponseModel"
}
]
}
}
},
"text/json": {
"schema": {
"type": "array",
"items": {
"oneOf": [
{
"$ref": "#/components/schemas/NamedEntityTreeItemResponseModel"
},
{
"$ref": "#/components/schemas/DataTypeTreeItemResponseModel"
},
{
"$ref": "#/components/schemas/DocumentBlueprintTreeItemResponseModel"
},
{
"$ref": "#/components/schemas/DocumentTypeTreeItemResponseModel"
},
{
"$ref": "#/components/schemas/FolderTreeItemResponseModel"
},
{
"$ref": "#/components/schemas/MediaTypeTreeItemResponseModel"
},
{
"$ref": "#/components/schemas/RelationTypeTreeItemResponseModel"
}
]
}
}
},
"text/plain": {
"schema": {
"type": "array",
"items": {
"oneOf": [
{
"$ref": "#/components/schemas/NamedEntityTreeItemResponseModel"
},
{
"$ref": "#/components/schemas/DataTypeTreeItemResponseModel"
},
{
"$ref": "#/components/schemas/DocumentBlueprintTreeItemResponseModel"
},
{
"$ref": "#/components/schemas/DocumentTypeTreeItemResponseModel"
},
{
"$ref": "#/components/schemas/FolderTreeItemResponseModel"
},
{
"$ref": "#/components/schemas/MediaTypeTreeItemResponseModel"
},
{
"$ref": "#/components/schemas/RelationTypeTreeItemResponseModel"
}
]
}
}
}
}
},
"401": {
"description": "The resource is protected and requires an authentication token"
}
},
"security": [
{
"Backoffice User": [ ]
}
]
}
},
"/umbraco/management/api/v1/tree/template/children": {
"get": {
"tags": [
@@ -38786,8 +39649,7 @@
"RecycleBinItemResponseModelBaseModel": {
"required": [
"hasChildren",
"id",
"type"
"id"
],
"type": "object",
"properties": {
@@ -38795,10 +39657,6 @@
"type": "string",
"format": "uuid"
},
"type": {
"minLength": 1,
"type": "string"
},
"hasChildren": {
"type": "boolean"
},

View File

@@ -8,9 +8,6 @@ public abstract class RecycleBinItemResponseModelBase
[Required]
public Guid Id { get; set; }
[Required]
public string Type { get; set; } = string.Empty;
[Required]
public bool HasChildren { get; set; }

View File

@@ -0,0 +1,13 @@
using System.Globalization;
using Umbraco.Cms.Core.Models.Entities;
namespace Umbraco.Cms.Core.Extensions;
public static class TreeEntityExtensions
{
public static int[] AncestorIds(this ITreeEntity entity) => entity.Path
.Split(Constants.CharArrays.Comma)
.Select(item => int.Parse(item, CultureInfo.InvariantCulture))
.Take(new Range(Index.FromStart(1), Index.FromEnd(1)))
.ToArray();
}

View File

@@ -0,0 +1,25 @@
using Moq;
using NUnit.Framework;
using Umbraco.Cms.Core.Extensions;
using Umbraco.Cms.Core.Models.Entities;
using Range = System.Range;
namespace Umbraco.Cms.Tests.UnitTests.Umbraco.Core.Extensions;
[TestFixture]
public class TreeEntityExtensionsTests
{
[TestCase("-1,1234", new int[] { })]
[TestCase("-1,1234,5678", new int[] { 1234 })]
[TestCase("-1,1234,5678,9012", new int[] { 5678, 1234 })]
[TestCase("-1,1234,5678,9012,2345", new int[] { 9012, 5678, 1234 })]
public void Parse_Ancestor_Ids_Excludes_Root_And_Self(string path, int[] expectedIds)
{
var entityMock = new Mock<ITreeEntity>();
entityMock.SetupGet(m => m.Path).Returns(path);
var result = entityMock.Object.AncestorIds();
Assert.AreEqual(expectedIds.Length, result.Length);
Assert.That(expectedIds, Is.EquivalentTo(result));
}
}