From f6f868e463eb6a9ccce97427f34e1898f4cf38a4 Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Mon, 25 Mar 2024 12:15:50 +0100 Subject: [PATCH] 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 --- .../Tree/AncestorsDataTypeTreeController.cs | 22 + .../Tree/AncestorsDictionaryTreeController.cs | 22 + .../Tree/ChildrenDictionaryTreeController.cs | 2 +- .../Tree/DictionaryTreeControllerBase.cs | 58 +- .../Tree/RootDictionaryTreeController.cs | 2 +- .../Tree/AncestorsDocumentTreeController.cs | 40 + .../AncestorsDocumentTypeTreeController.cs | 22 + .../Tree/AncestorsMediaTreeController.cs | 32 + .../Tree/AncestorsMediaTypeTreeController.cs | 22 + .../AncestorsPartialViewTreeController.cs | 22 + .../RecycleBin/RecycleBinControllerBase.cs | 8 +- .../Tree/AncestorsScriptTreeController.cs | 22 + .../Tree/AncestorsStaticFileTreeController.cs | 22 + .../Tree/StaticFileTreeControllerBase.cs | 8 +- .../Tree/AncestorsStylesheetTreeController.cs | 22 + .../Tree/AncestorsTemplateTreeController.cs | 22 + .../Tree/EntityTreeControllerBase.cs | 39 +- .../Tree/FileSystemTreeControllerBase.cs | 30 +- .../Tree/FolderTreeControllerBase.cs | 25 + .../Tree/UserStartNodeTreeControllerBase.cs | 8 - src/Umbraco.Cms.Api.Management/OpenApi.json | 870 +++++++++++++++++- .../RecycleBinItemResponseModelBase.cs | 3 - .../Extensions/TreeEntityExtensions.cs | 13 + .../Extensions/TreeEntityExtensionsTests.cs | 25 + 24 files changed, 1308 insertions(+), 53 deletions(-) create mode 100644 src/Umbraco.Cms.Api.Management/Controllers/DataType/Tree/AncestorsDataTypeTreeController.cs create mode 100644 src/Umbraco.Cms.Api.Management/Controllers/Dictionary/Tree/AncestorsDictionaryTreeController.cs create mode 100644 src/Umbraco.Cms.Api.Management/Controllers/Document/Tree/AncestorsDocumentTreeController.cs create mode 100644 src/Umbraco.Cms.Api.Management/Controllers/DocumentType/Tree/AncestorsDocumentTypeTreeController.cs create mode 100644 src/Umbraco.Cms.Api.Management/Controllers/Media/Tree/AncestorsMediaTreeController.cs create mode 100644 src/Umbraco.Cms.Api.Management/Controllers/MediaType/Tree/AncestorsMediaTypeTreeController.cs create mode 100644 src/Umbraco.Cms.Api.Management/Controllers/PartialView/Tree/AncestorsPartialViewTreeController.cs create mode 100644 src/Umbraco.Cms.Api.Management/Controllers/Script/Tree/AncestorsScriptTreeController.cs create mode 100644 src/Umbraco.Cms.Api.Management/Controllers/StaticFile/Tree/AncestorsStaticFileTreeController.cs create mode 100644 src/Umbraco.Cms.Api.Management/Controllers/Stylesheet/Tree/AncestorsStylesheetTreeController.cs create mode 100644 src/Umbraco.Cms.Api.Management/Controllers/Template/Tree/AncestorsTemplateTreeController.cs create mode 100644 src/Umbraco.Core/Extensions/TreeEntityExtensions.cs create mode 100644 tests/Umbraco.Tests.UnitTests/Umbraco.Core/Extensions/TreeEntityExtensionsTests.cs diff --git a/src/Umbraco.Cms.Api.Management/Controllers/DataType/Tree/AncestorsDataTypeTreeController.cs b/src/Umbraco.Cms.Api.Management/Controllers/DataType/Tree/AncestorsDataTypeTreeController.cs new file mode 100644 index 0000000000..1bb501bb12 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Controllers/DataType/Tree/AncestorsDataTypeTreeController.cs @@ -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), StatusCodes.Status200OK)] + public async Task>> Ancestors(Guid descendantId) + => await GetAncestors(descendantId); +} diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Dictionary/Tree/AncestorsDictionaryTreeController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Dictionary/Tree/AncestorsDictionaryTreeController.cs new file mode 100644 index 0000000000..ad15c0c9ad --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Controllers/Dictionary/Tree/AncestorsDictionaryTreeController.cs @@ -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), StatusCodes.Status200OK)] + public async Task>> Ancestors(Guid descendantId) + => await GetAncestors(descendantId); +} diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Dictionary/Tree/ChildrenDictionaryTreeController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Dictionary/Tree/ChildrenDictionaryTreeController.cs index 7b4f9fb363..8bf1d6ba14 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Dictionary/Tree/ChildrenDictionaryTreeController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Dictionary/Tree/ChildrenDictionaryTreeController.cs @@ -23,6 +23,6 @@ public class ChildrenDictionaryTreeController : DictionaryTreeControllerBase { PagedModel 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)); } } diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Dictionary/Tree/DictionaryTreeControllerBase.cs b/src/Umbraco.Cms.Api.Management/Controllers/Dictionary/Tree/DictionaryTreeControllerBase.cs index f746d3cf28..5e55924079 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Dictionary/Tree/DictionaryTreeControllerBase.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Dictionary/Tree/DictionaryTreeControllerBase.cs @@ -27,25 +27,51 @@ public class DictionaryTreeControllerBase : NamedEntityTreeControllerBase> MapTreeItemViewModels(Guid? parentKey, IEnumerable dictionaryItems) + protected async Task> MapTreeItemViewModels(IEnumerable dictionaryItems) + => await Task.WhenAll(dictionaryItems.Select(CreateEntityTreeItemViewModelAsync)); + + protected override async Task>> GetAncestors(Guid descendantKey, bool includeSelf = true) { - async Task CreateEntityTreeItemViewModelAsync(IDictionaryItem dictionaryItem) + IDictionaryItem? dictionaryItem = await DictionaryItemService.GetAsync(descendantKey); + if (dictionaryItem is null) { - var hasChildren = await DictionaryItemService.CountChildrenAsync(dictionaryItem.Key) > 0; - return new NamedEntityTreeItemResponseModel - { - Name = dictionaryItem.ItemKey, - Id = dictionaryItem.Key, - HasChildren = hasChildren, - Parent = parentKey.HasValue - ? new ReferenceByIdModel - { - Id = parentKey.Value - } - : null - }; + // this looks weird - but we actually mimic how the rest of the ancestor (and children) endpoints actually work + return Ok(Enumerable.Empty()); } - return await Task.WhenAll(dictionaryItems.Select(CreateEntityTreeItemViewModelAsync)); + var ancestors = new List(); + 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 CreateEntityTreeItemViewModelAsync(IDictionaryItem dictionaryItem) + { + var hasChildren = await DictionaryItemService.CountChildrenAsync(dictionaryItem.Key) > 0; + return new NamedEntityTreeItemResponseModel + { + Name = dictionaryItem.ItemKey, + Id = dictionaryItem.Key, + HasChildren = hasChildren, + Parent = dictionaryItem.ParentId.HasValue + ? new ReferenceByIdModel + { + Id = dictionaryItem.ParentId.Value + } + : null + }; } } diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Dictionary/Tree/RootDictionaryTreeController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Dictionary/Tree/RootDictionaryTreeController.cs index fcb60db818..40596122da 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Dictionary/Tree/RootDictionaryTreeController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Dictionary/Tree/RootDictionaryTreeController.cs @@ -23,6 +23,6 @@ public class RootDictionaryTreeController : DictionaryTreeControllerBase { PagedModel 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)); } } diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Document/Tree/AncestorsDocumentTreeController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Document/Tree/AncestorsDocumentTreeController.cs new file mode 100644 index 0000000000..413b3d0656 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Controllers/Document/Tree/AncestorsDocumentTreeController.cs @@ -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), StatusCodes.Status200OK)] + public async Task>> Ancestors(Guid descendantId) + => await GetAncestors(descendantId); +} diff --git a/src/Umbraco.Cms.Api.Management/Controllers/DocumentType/Tree/AncestorsDocumentTypeTreeController.cs b/src/Umbraco.Cms.Api.Management/Controllers/DocumentType/Tree/AncestorsDocumentTypeTreeController.cs new file mode 100644 index 0000000000..3d8fc278cf --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Controllers/DocumentType/Tree/AncestorsDocumentTypeTreeController.cs @@ -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), StatusCodes.Status200OK)] + public async Task>> Ancestors(Guid descendantId) + => await GetAncestors(descendantId); +} diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Media/Tree/AncestorsMediaTreeController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Media/Tree/AncestorsMediaTreeController.cs new file mode 100644 index 0000000000..ed3c027a22 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Controllers/Media/Tree/AncestorsMediaTreeController.cs @@ -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), StatusCodes.Status200OK)] + public async Task>> Ancestors(Guid descendantId) + => await GetAncestors(descendantId); +} diff --git a/src/Umbraco.Cms.Api.Management/Controllers/MediaType/Tree/AncestorsMediaTypeTreeController.cs b/src/Umbraco.Cms.Api.Management/Controllers/MediaType/Tree/AncestorsMediaTypeTreeController.cs new file mode 100644 index 0000000000..b02fceb9cb --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Controllers/MediaType/Tree/AncestorsMediaTypeTreeController.cs @@ -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), StatusCodes.Status200OK)] + public async Task>> Ancestors(Guid descendantId) + => await GetAncestors(descendantId); +} diff --git a/src/Umbraco.Cms.Api.Management/Controllers/PartialView/Tree/AncestorsPartialViewTreeController.cs b/src/Umbraco.Cms.Api.Management/Controllers/PartialView/Tree/AncestorsPartialViewTreeController.cs new file mode 100644 index 0000000000..6ad15fd86c --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Controllers/PartialView/Tree/AncestorsPartialViewTreeController.cs @@ -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), StatusCodes.Status200OK)] + public async Task>> Ancestors(string descendantPath) + => await GetAncestors(descendantPath); +} diff --git a/src/Umbraco.Cms.Api.Management/Controllers/RecycleBin/RecycleBinControllerBase.cs b/src/Umbraco.Cms.Api.Management/Controllers/RecycleBin/RecycleBinControllerBase.cs index 02911daa97..e26e77c405 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/RecycleBin/RecycleBinControllerBase.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/RecycleBin/RecycleBinControllerBase.cs @@ -15,14 +15,9 @@ public abstract class RecycleBinControllerBase : 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 : ContentControllerBase var viewModel = new TItem { Id = entity.Key, - Type = _itemUdiType, HasChildren = entity.HasChildren, Parent = parentKey.HasValue ? new ItemReferenceByIdResponseModel diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Script/Tree/AncestorsScriptTreeController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Script/Tree/AncestorsScriptTreeController.cs new file mode 100644 index 0000000000..a2f2f43b7c --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Controllers/Script/Tree/AncestorsScriptTreeController.cs @@ -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), StatusCodes.Status200OK)] + public async Task>> Ancestors(string descendantPath) + => await GetAncestors(descendantPath); +} diff --git a/src/Umbraco.Cms.Api.Management/Controllers/StaticFile/Tree/AncestorsStaticFileTreeController.cs b/src/Umbraco.Cms.Api.Management/Controllers/StaticFile/Tree/AncestorsStaticFileTreeController.cs new file mode 100644 index 0000000000..d52d10b14d --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Controllers/StaticFile/Tree/AncestorsStaticFileTreeController.cs @@ -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), StatusCodes.Status200OK)] + public async Task>> Ancestors(string descendantPath) + => await GetAncestors(descendantPath); +} diff --git a/src/Umbraco.Cms.Api.Management/Controllers/StaticFile/Tree/StaticFileTreeControllerBase.cs b/src/Umbraco.Cms.Api.Management/Controllers/StaticFile/Tree/StaticFileTreeControllerBase.cs index 728aef345e..7f3acfbda4 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/StaticFile/Tree/StaticFileTreeControllerBase.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/StaticFile/Tree/StaticFileTreeControllerBase.cs @@ -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() : base.GetFiles(path); + protected override FileSystemTreeItemPresentationModel[] GetAncestorModels(string path, bool includeSelf) + => IsAllowedPath(path) + ? base.GetAncestorModels(path, includeSelf) + : Array.Empty(); + 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}")); } diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Stylesheet/Tree/AncestorsStylesheetTreeController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Stylesheet/Tree/AncestorsStylesheetTreeController.cs new file mode 100644 index 0000000000..86c924dd4f --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Controllers/Stylesheet/Tree/AncestorsStylesheetTreeController.cs @@ -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), StatusCodes.Status200OK)] + public async Task>> Ancestors(string descendantPath) + => await GetAncestors(descendantPath); +} diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Template/Tree/AncestorsTemplateTreeController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Template/Tree/AncestorsTemplateTreeController.cs new file mode 100644 index 0000000000..4cb3650b1e --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Controllers/Template/Tree/AncestorsTemplateTreeController.cs @@ -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), StatusCodes.Status200OK)] + public async Task>> Ancestors(Guid descendantId) + => await GetAncestors(descendantId); +} diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Tree/EntityTreeControllerBase.cs b/src/Umbraco.Cms.Api.Management/Controllers/Tree/EntityTreeControllerBase.cs index b9dfd90776..57f575fd8a 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Tree/EntityTreeControllerBase.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Tree/EntityTreeControllerBase.cs @@ -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 : ManagementApiControllerB return await Task.FromResult(Ok(result)); } - protected async Task>> GetItems(Guid[] ids) + protected virtual async Task>> GetAncestors(Guid descendantKey, bool includeSelf = true) { - if (ids.IsCollectionEmpty()) + IEntitySlim[] ancestorEntities = await GetAncestorEntitiesAsync(descendantKey, includeSelf); + + TItem[] result = ancestorEntities + .Select(ancestor => + { + IEntitySlim? parent = ancestor.ParentId > 0 + ? ancestorEntities.Single(a => a.Id == ancestor.ParentId) + : null; + + return MapTreeItemViewModel(parent?.Key, ancestor); + }) + .ToArray(); + + return Ok(result); + } + + protected virtual async Task GetAncestorEntitiesAsync(Guid descendantKey, bool includeSelf) + { + IEntitySlim? entity = EntityService.Get(descendantKey, ItemObjectType); + if (entity is null) { - return await Task.FromResult(Ok(PagedViewModel(Array.Empty(), 0))); + // not much else we can do here but return nothing + return await Task.FromResult(Array.Empty()); } - IEntitySlim[] itemEntities = GetEntities(ids); + var ancestorIds = entity.AncestorIds(); - TItem[] treeItemViewModels = MapTreeItemViewModels(null, itemEntities); + IEnumerable ancestors = ancestorIds.Any() + ? EntityService.GetAll(ItemObjectType, ancestorIds) + : Array.Empty(); + ancestors = ancestors.Union(includeSelf ? new[] { entity } : Array.Empty()); - return await Task.FromResult(Ok(treeItemViewModels)); + 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 : 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(); diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Tree/FileSystemTreeControllerBase.cs b/src/Umbraco.Cms.Api.Management/Controllers/Tree/FileSystemTreeControllerBase.cs index b717ef96a3..80b1edffeb 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Tree/FileSystemTreeControllerBase.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Tree/FileSystemTreeControllerBase.cs @@ -28,6 +28,30 @@ public abstract class FileSystemTreeControllerBase : ManagementApiControllerBase return await Task.FromResult(Ok(result)); } + protected virtual async Task>> 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 PagedViewModel(IEnumerable viewModels, long totalItems) => new() { Total = totalItems, Items = viewModels }; diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Tree/FolderTreeControllerBase.cs b/src/Umbraco.Cms.Api.Management/Controllers/Tree/FolderTreeControllerBase.cs index 9db5248069..0dee74bea3 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Tree/FolderTreeControllerBase.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Tree/FolderTreeControllerBase.cs @@ -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 : NamedEntityTreeControlle return viewModel; } + protected override async Task 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()); + } + + 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 ancestors = ancestorIds.Any() + ? EntityService + .GetAll(ItemObjectType, ancestorIds) + .Union(containers) + : Array.Empty(); + ancestors = ancestors.Union(includeSelf ? new[] { entity } : Array.Empty()); + + return ancestors.OrderBy(item => item.Level).ToArray(); + } + private IEntitySlim[] GetEntities(Guid? parentKey, int skip, int take, out long totalItems) { totalItems = 0; diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Tree/UserStartNodeTreeControllerBase.cs b/src/Umbraco.Cms.Api.Management/Controllers/Tree/UserStartNodeTreeControllerBase.cs index 1e82b6da35..4c4d6211ff 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Tree/UserStartNodeTreeControllerBase.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Tree/UserStartNodeTreeControllerBase.cs @@ -48,14 +48,6 @@ public abstract class UserStartNodeTreeControllerBase : 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()) diff --git a/src/Umbraco.Cms.Api.Management/OpenApi.json b/src/Umbraco.Cms.Api.Management/OpenApi.json index 33b4e2207f..930878dc0e 100644 --- a/src/Umbraco.Cms.Api.Management/OpenApi.json +++ b/src/Umbraco.Cms.Api.Management/OpenApi.json @@ -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" }, diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/RecycleBin/RecycleBinItemResponseModelBase.cs b/src/Umbraco.Cms.Api.Management/ViewModels/RecycleBin/RecycleBinItemResponseModelBase.cs index 2e77b259bd..0d17903493 100644 --- a/src/Umbraco.Cms.Api.Management/ViewModels/RecycleBin/RecycleBinItemResponseModelBase.cs +++ b/src/Umbraco.Cms.Api.Management/ViewModels/RecycleBin/RecycleBinItemResponseModelBase.cs @@ -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; } diff --git a/src/Umbraco.Core/Extensions/TreeEntityExtensions.cs b/src/Umbraco.Core/Extensions/TreeEntityExtensions.cs new file mode 100644 index 0000000000..8aa5d3c23d --- /dev/null +++ b/src/Umbraco.Core/Extensions/TreeEntityExtensions.cs @@ -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(); +} diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Extensions/TreeEntityExtensionsTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Extensions/TreeEntityExtensionsTests.cs new file mode 100644 index 0000000000..e3adf6288f --- /dev/null +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Extensions/TreeEntityExtensionsTests.cs @@ -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(); + entityMock.SetupGet(m => m.Path).Returns(path); + + var result = entityMock.Object.AncestorIds(); + Assert.AreEqual(expectedIds.Length, result.Length); + Assert.That(expectedIds, Is.EquivalentTo(result)); + } +}