From 134b193c748c6fa0a6a383ee5f7bc1bc52c55474 Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Wed, 28 Sep 2022 13:37:59 +0200 Subject: [PATCH] New backoffice - trees design (#12963) * Refactor: Add default versioned back office route attribute * Tree controller bases and first draft implementations for document, media and doctype * Move tree item view models to appropriate location * Fix missing parent * Refactor user entity access for testability * A bit of clean-up + handle user start nodes for items endpoint * Implement foldersOnly for folder tree * Items endpoint for document type tree * Strongly typed action results * Content + media recycle bin * Correct return type for swagger * Member type tree * Rename user start node handling to make a little more sense * Revert to faked admin start nodes in document tree * Media type tree * Data type tree * Relation type tree * Remove unused dependency from member type tree * Correct documentation for member type tree endpoint response types * Use icon constants * Add templates tree * Member group tree * Document blueprint tree * Partial views, scripts and stylesheets trees * Static files tree * Clarify "folders only" state * Comments and improved readability * Rename TreeControllerBase and TreeItemViewModel * Move recycle bin controller base to its own namespace * Moved tree base controllers to their own namespace * Common base class for tree view models * Remove ProblemDetails response type declaration from all actions * Add OpenApiTag * Various review comments * Dictionary item tree * Renamed all tree controllers to follow action/feature naming convention * Handle client culture state for document tree * Support "ignore user start nodes" for content and media + refactor how tree states work to make things more explicit * Fix or postpone a few TODOs * Make entity service able to paginate trashed children * Handle sorting explicitly * Re-apply VersionedApiBackOfficeRoute to install and upgrade controllers after merge * Use PagedViewModel instead of PagedResult for all trees * Explain the usage of UmbracoObjectTypes.Unknown * Introduce and apply GetMany pattern for dictionary items * Add a note about relation type caching * Fix broken test build + add unit tests for new localization service methods * Use new management API controller base * Entity repository should build document entities for document blueprints when getting paged entities (same as it does when getting specific entities) * Use Media type for Media recycle bin Co-authored-by: Mole * Move shared relation service to concrete implementations * Use inclusive language * Add 401 response type documentation to applicable trees * Refactor entity load for folder tree controller base + ensure that folders are only included in the first result page * Add (in-memory) pagination to dictionary tree * Make file system controller honor paging parameters * Support pagination in relation type tree * Clarify method name a bit for detecting tree root path requests * Update Open API schema to match new trees * Move from page number and page size to skip/take (with temporary workaround for lack of property skip/take pagination in current DB implementation) * Update OpenAPI schema to match skip/take * Update OpenAPI schema * Don't return paginated view models from "items" endpoints * Update OpenApi schema Co-authored-by: Mole --- .../Tree/ChildrenDataTypeTreeController.cs | 24 + .../Tree/DataTypeTreeControllerBase.cs | 46 + .../Tree/ItemsDataTypeTreeController.cs | 20 + .../Tree/RootDataTypeTreeController.cs | 24 + .../ChildrenDictionaryItemTreeController.cs | 39 + .../Tree/DictionaryItemTreeControllerBase.cs | 53 + .../Tree/ItemsDictionaryItemTreeController.cs | 27 + .../Tree/RootDictionaryItemTreeController.cs | 39 + .../ChildrenDocumentRecycleBinController.cs | 21 + .../DocumentRecycleBinControllerBase.cs | 43 + .../RootDocumentRecycleBinController.cs | 21 + .../Tree/ChildrenDocumentTreeController.cs | 34 + .../Tree/DocumentTreeControllerBase.cs | 94 + .../Tree/ItemsDocumentTreeController.cs | 33 + .../Tree/RootDocumentTreeController.cs | 34 + .../DocumentBlueprintTreeControllerBase.cs | 56 + .../ItemsDocumentBlueprintTreeController.cs | 21 + .../RootDocumentBlueprintTreeController.cs | 21 + .../ChildrenDocumentTypeTreeController.cs | 24 + .../Tree/DocumentTypeTreeControllerBase.cs | 47 + .../Tree/ItemsDocumentTypeTreeController.cs | 20 + .../Tree/RootDocumentTypeTreeController.cs | 24 + .../Install/InstallControllerBase.cs | 2 +- .../ChildrenMediaRecycleBinController.cs | 21 + .../MediaRecycleBinControllerBase.cs | 43 + .../RootMediaRecycleBinController.cs | 21 + .../Media/Tree/ChildrenMediaTreeController.cs | 32 + .../Media/Tree/ItemsMediaTreeController.cs | 31 + .../Media/Tree/MediaTreeControllerBase.cs | 72 + .../Media/Tree/RootMediaTreeController.cs | 32 + .../Tree/ChildrenMediaTypeTreeController.cs | 24 + .../Tree/ItemsMediaTypeTreeController.cs | 20 + .../Tree/MediaTypeTreeControllerBase.cs | 46 + .../Tree/RootMediaTypeTreeController.cs | 24 + .../Tree/ItemsMemberGroupTreeController.cs | 20 + .../Tree/MemberGroupTreeControllerBase.cs | 32 + .../Tree/RootMemberGroupTreeController.cs | 21 + .../Tree/ItemsMemberTypeTreeController.cs | 20 + .../Tree/MemberTypeTreeControllerBase.cs | 32 + .../Tree/RootMemberTypeTreeController.cs | 21 + .../Tree/ChildrenPartialViewTreeController.cs | 21 + .../Tree/ItemsPartialViewTreeController.cs | 20 + .../Tree/PartialViewTreeControllerBase.cs | 25 + .../Tree/RootPartialViewTreeController.cs | 21 + .../RecycleBin/RecycleBinControllerBase.cs | 111 + .../Tree/ItemsRelationTypeTreeController.cs | 32 + .../Tree/RelationTypeTreeControllerBase.cs | 38 + .../Tree/RootRelationTypeTreeController.cs | 44 + .../Tree/ChildrenScriptTreeController.cs | 21 + .../Script/Tree/ItemsScriptTreeController.cs | 20 + .../Script/Tree/RootScriptTreeController.cs | 21 + .../Script/Tree/ScriptTreeControllerBase.cs | 25 + .../Server/ServerControllerBase.cs | 2 +- .../Tree/ChildrenStaticFileTreeController.cs | 21 + .../Tree/ItemsStaticFileTreeController.cs | 20 + .../Tree/RootStaticFileTreeController.cs | 21 + .../Tree/StaticFileTreeControllerBase.cs | 42 + .../Tree/ChildrenStylesheetTreeController.cs | 21 + .../Tree/ItemsStylesheetTreeController.cs | 20 + .../Tree/RootStylesheetTreeController.cs | 21 + .../Tree/StylesheetTreeControllerBase.cs | 25 + .../Tree/ChildrenTemplateTreeController.cs | 21 + .../Tree/ItemsTemplateTreeController.cs | 20 + .../Tree/RootTemplateTreeController.cs | 21 + .../Tree/TemplateTreeControllerBase.cs | 32 + .../Tree/EntityTreeControllerBase.cs | 132 + .../Tree/FileSystemTreeControllerBase.cs | 110 + .../Tree/FolderTreeControllerBase.cs | 88 + .../Tree/UserStartNodeTreeControllerBase.cs | 117 + .../Upgrade/UpgradeControllerBase.cs | 2 +- .../InstallerBuilderExtensions.cs | 8 + .../RequireDocumentTreeRootAccessAttribute.cs | 19 + .../RequireMediaTreeRootAccessAttribute.cs | 19 + .../Filters/RequireTreeRootAccessAttribute.cs | 40 + .../ManagementApiComposer.cs | 3 +- .../Models/Entities/UserAccessEntity.cs | 16 + src/Umbraco.Cms.ManagementApi/OpenApi.json | 3151 +++++++++++++++-- .../Entities/IUserStartNodeEntitiesService.cs | 42 + .../Entities/UserStartNodeEntitiesService.cs | 76 + .../Services/Paging/PaginationService.cs | 39 + .../RecycleBin/RecycleBinItemViewModel.cs | 19 + .../Tree/ContentTreeItemViewModel.cs | 6 + .../DocumentBlueprintTreeItemViewModel.cs | 10 + .../Tree/DocumentTreeItemViewModel.cs | 10 + .../Tree/DocumentTypeTreeItemViewModel.cs | 6 + .../Tree/EntityTreeItemViewModel.cs | 10 + .../Tree/FileSystemTreeItemViewModel.cs | 8 + .../Tree/FolderTreeItemViewModel.cs | 6 + .../ViewModels/Tree/TreeItemViewModel.cs | 12 + src/Umbraco.Core/Constants-Icons.cs | 20 + src/Umbraco.Core/Models/UmbracoObjectTypes.cs | 2 +- .../Repositories/IDictionaryRepository.cs | 4 + src/Umbraco.Core/Services/EntityService.cs | 39 +- src/Umbraco.Core/Services/IEntityService.cs | 16 + .../Services/ILocalizationService.cs | 18 + .../Services/LocalizationService.cs | 45 + .../Implement/DictionaryRepository.cs | 14 + .../Implement/EntityRepository.cs | 3 +- .../Implement/SimpleGetRepository.cs | 2 +- .../VersionedApiBackOfficeRouteAttribute.cs | 9 + .../Repositories/DictionaryRepositoryTest.cs | 40 + .../Services/EntityServiceTests.cs | 87 + .../Services/LocalizationServiceTests.cs | 18 + 103 files changed, 5976 insertions(+), 255 deletions(-) create mode 100644 src/Umbraco.Cms.ManagementApi/Controllers/DataType/Tree/ChildrenDataTypeTreeController.cs create mode 100644 src/Umbraco.Cms.ManagementApi/Controllers/DataType/Tree/DataTypeTreeControllerBase.cs create mode 100644 src/Umbraco.Cms.ManagementApi/Controllers/DataType/Tree/ItemsDataTypeTreeController.cs create mode 100644 src/Umbraco.Cms.ManagementApi/Controllers/DataType/Tree/RootDataTypeTreeController.cs create mode 100644 src/Umbraco.Cms.ManagementApi/Controllers/DictionaryItem/Tree/ChildrenDictionaryItemTreeController.cs create mode 100644 src/Umbraco.Cms.ManagementApi/Controllers/DictionaryItem/Tree/DictionaryItemTreeControllerBase.cs create mode 100644 src/Umbraco.Cms.ManagementApi/Controllers/DictionaryItem/Tree/ItemsDictionaryItemTreeController.cs create mode 100644 src/Umbraco.Cms.ManagementApi/Controllers/DictionaryItem/Tree/RootDictionaryItemTreeController.cs create mode 100644 src/Umbraco.Cms.ManagementApi/Controllers/Document/RecycleBin/ChildrenDocumentRecycleBinController.cs create mode 100644 src/Umbraco.Cms.ManagementApi/Controllers/Document/RecycleBin/DocumentRecycleBinControllerBase.cs create mode 100644 src/Umbraco.Cms.ManagementApi/Controllers/Document/RecycleBin/RootDocumentRecycleBinController.cs create mode 100644 src/Umbraco.Cms.ManagementApi/Controllers/Document/Tree/ChildrenDocumentTreeController.cs create mode 100644 src/Umbraco.Cms.ManagementApi/Controllers/Document/Tree/DocumentTreeControllerBase.cs create mode 100644 src/Umbraco.Cms.ManagementApi/Controllers/Document/Tree/ItemsDocumentTreeController.cs create mode 100644 src/Umbraco.Cms.ManagementApi/Controllers/Document/Tree/RootDocumentTreeController.cs create mode 100644 src/Umbraco.Cms.ManagementApi/Controllers/DocumentBlueprint/Tree/DocumentBlueprintTreeControllerBase.cs create mode 100644 src/Umbraco.Cms.ManagementApi/Controllers/DocumentBlueprint/Tree/ItemsDocumentBlueprintTreeController.cs create mode 100644 src/Umbraco.Cms.ManagementApi/Controllers/DocumentBlueprint/Tree/RootDocumentBlueprintTreeController.cs create mode 100644 src/Umbraco.Cms.ManagementApi/Controllers/DocumentType/Tree/ChildrenDocumentTypeTreeController.cs create mode 100644 src/Umbraco.Cms.ManagementApi/Controllers/DocumentType/Tree/DocumentTypeTreeControllerBase.cs create mode 100644 src/Umbraco.Cms.ManagementApi/Controllers/DocumentType/Tree/ItemsDocumentTypeTreeController.cs create mode 100644 src/Umbraco.Cms.ManagementApi/Controllers/DocumentType/Tree/RootDocumentTypeTreeController.cs create mode 100644 src/Umbraco.Cms.ManagementApi/Controllers/Media/RecycleBin/ChildrenMediaRecycleBinController.cs create mode 100644 src/Umbraco.Cms.ManagementApi/Controllers/Media/RecycleBin/MediaRecycleBinControllerBase.cs create mode 100644 src/Umbraco.Cms.ManagementApi/Controllers/Media/RecycleBin/RootMediaRecycleBinController.cs create mode 100644 src/Umbraco.Cms.ManagementApi/Controllers/Media/Tree/ChildrenMediaTreeController.cs create mode 100644 src/Umbraco.Cms.ManagementApi/Controllers/Media/Tree/ItemsMediaTreeController.cs create mode 100644 src/Umbraco.Cms.ManagementApi/Controllers/Media/Tree/MediaTreeControllerBase.cs create mode 100644 src/Umbraco.Cms.ManagementApi/Controllers/Media/Tree/RootMediaTreeController.cs create mode 100644 src/Umbraco.Cms.ManagementApi/Controllers/MediaType/Tree/ChildrenMediaTypeTreeController.cs create mode 100644 src/Umbraco.Cms.ManagementApi/Controllers/MediaType/Tree/ItemsMediaTypeTreeController.cs create mode 100644 src/Umbraco.Cms.ManagementApi/Controllers/MediaType/Tree/MediaTypeTreeControllerBase.cs create mode 100644 src/Umbraco.Cms.ManagementApi/Controllers/MediaType/Tree/RootMediaTypeTreeController.cs create mode 100644 src/Umbraco.Cms.ManagementApi/Controllers/MemberGroup/Tree/ItemsMemberGroupTreeController.cs create mode 100644 src/Umbraco.Cms.ManagementApi/Controllers/MemberGroup/Tree/MemberGroupTreeControllerBase.cs create mode 100644 src/Umbraco.Cms.ManagementApi/Controllers/MemberGroup/Tree/RootMemberGroupTreeController.cs create mode 100644 src/Umbraco.Cms.ManagementApi/Controllers/MemberType/Tree/ItemsMemberTypeTreeController.cs create mode 100644 src/Umbraco.Cms.ManagementApi/Controllers/MemberType/Tree/MemberTypeTreeControllerBase.cs create mode 100644 src/Umbraco.Cms.ManagementApi/Controllers/MemberType/Tree/RootMemberTypeTreeController.cs create mode 100644 src/Umbraco.Cms.ManagementApi/Controllers/PartialView/Tree/ChildrenPartialViewTreeController.cs create mode 100644 src/Umbraco.Cms.ManagementApi/Controllers/PartialView/Tree/ItemsPartialViewTreeController.cs create mode 100644 src/Umbraco.Cms.ManagementApi/Controllers/PartialView/Tree/PartialViewTreeControllerBase.cs create mode 100644 src/Umbraco.Cms.ManagementApi/Controllers/PartialView/Tree/RootPartialViewTreeController.cs create mode 100644 src/Umbraco.Cms.ManagementApi/Controllers/RecycleBin/RecycleBinControllerBase.cs create mode 100644 src/Umbraco.Cms.ManagementApi/Controllers/RelationType/Tree/ItemsRelationTypeTreeController.cs create mode 100644 src/Umbraco.Cms.ManagementApi/Controllers/RelationType/Tree/RelationTypeTreeControllerBase.cs create mode 100644 src/Umbraco.Cms.ManagementApi/Controllers/RelationType/Tree/RootRelationTypeTreeController.cs create mode 100644 src/Umbraco.Cms.ManagementApi/Controllers/Script/Tree/ChildrenScriptTreeController.cs create mode 100644 src/Umbraco.Cms.ManagementApi/Controllers/Script/Tree/ItemsScriptTreeController.cs create mode 100644 src/Umbraco.Cms.ManagementApi/Controllers/Script/Tree/RootScriptTreeController.cs create mode 100644 src/Umbraco.Cms.ManagementApi/Controllers/Script/Tree/ScriptTreeControllerBase.cs create mode 100644 src/Umbraco.Cms.ManagementApi/Controllers/StaticFile/Tree/ChildrenStaticFileTreeController.cs create mode 100644 src/Umbraco.Cms.ManagementApi/Controllers/StaticFile/Tree/ItemsStaticFileTreeController.cs create mode 100644 src/Umbraco.Cms.ManagementApi/Controllers/StaticFile/Tree/RootStaticFileTreeController.cs create mode 100644 src/Umbraco.Cms.ManagementApi/Controllers/StaticFile/Tree/StaticFileTreeControllerBase.cs create mode 100644 src/Umbraco.Cms.ManagementApi/Controllers/Stylesheet/Tree/ChildrenStylesheetTreeController.cs create mode 100644 src/Umbraco.Cms.ManagementApi/Controllers/Stylesheet/Tree/ItemsStylesheetTreeController.cs create mode 100644 src/Umbraco.Cms.ManagementApi/Controllers/Stylesheet/Tree/RootStylesheetTreeController.cs create mode 100644 src/Umbraco.Cms.ManagementApi/Controllers/Stylesheet/Tree/StylesheetTreeControllerBase.cs create mode 100644 src/Umbraco.Cms.ManagementApi/Controllers/Template/Tree/ChildrenTemplateTreeController.cs create mode 100644 src/Umbraco.Cms.ManagementApi/Controllers/Template/Tree/ItemsTemplateTreeController.cs create mode 100644 src/Umbraco.Cms.ManagementApi/Controllers/Template/Tree/RootTemplateTreeController.cs create mode 100644 src/Umbraco.Cms.ManagementApi/Controllers/Template/Tree/TemplateTreeControllerBase.cs create mode 100644 src/Umbraco.Cms.ManagementApi/Controllers/Tree/EntityTreeControllerBase.cs create mode 100644 src/Umbraco.Cms.ManagementApi/Controllers/Tree/FileSystemTreeControllerBase.cs create mode 100644 src/Umbraco.Cms.ManagementApi/Controllers/Tree/FolderTreeControllerBase.cs create mode 100644 src/Umbraco.Cms.ManagementApi/Controllers/Tree/UserStartNodeTreeControllerBase.cs create mode 100644 src/Umbraco.Cms.ManagementApi/Filters/RequireDocumentTreeRootAccessAttribute.cs create mode 100644 src/Umbraco.Cms.ManagementApi/Filters/RequireMediaTreeRootAccessAttribute.cs create mode 100644 src/Umbraco.Cms.ManagementApi/Filters/RequireTreeRootAccessAttribute.cs create mode 100644 src/Umbraco.Cms.ManagementApi/Models/Entities/UserAccessEntity.cs create mode 100644 src/Umbraco.Cms.ManagementApi/Services/Entities/IUserStartNodeEntitiesService.cs create mode 100644 src/Umbraco.Cms.ManagementApi/Services/Entities/UserStartNodeEntitiesService.cs create mode 100644 src/Umbraco.Cms.ManagementApi/Services/Paging/PaginationService.cs create mode 100644 src/Umbraco.Cms.ManagementApi/ViewModels/RecycleBin/RecycleBinItemViewModel.cs create mode 100644 src/Umbraco.Cms.ManagementApi/ViewModels/Tree/ContentTreeItemViewModel.cs create mode 100644 src/Umbraco.Cms.ManagementApi/ViewModels/Tree/DocumentBlueprintTreeItemViewModel.cs create mode 100644 src/Umbraco.Cms.ManagementApi/ViewModels/Tree/DocumentTreeItemViewModel.cs create mode 100644 src/Umbraco.Cms.ManagementApi/ViewModels/Tree/DocumentTypeTreeItemViewModel.cs create mode 100644 src/Umbraco.Cms.ManagementApi/ViewModels/Tree/EntityTreeItemViewModel.cs create mode 100644 src/Umbraco.Cms.ManagementApi/ViewModels/Tree/FileSystemTreeItemViewModel.cs create mode 100644 src/Umbraco.Cms.ManagementApi/ViewModels/Tree/FolderTreeItemViewModel.cs create mode 100644 src/Umbraco.Cms.ManagementApi/ViewModels/Tree/TreeItemViewModel.cs create mode 100644 src/Umbraco.New.Cms.Web.Common/Routing/VersionedApiBackOfficeRouteAttribute.cs diff --git a/src/Umbraco.Cms.ManagementApi/Controllers/DataType/Tree/ChildrenDataTypeTreeController.cs b/src/Umbraco.Cms.ManagementApi/Controllers/DataType/Tree/ChildrenDataTypeTreeController.cs new file mode 100644 index 0000000000..7aee93d03c --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Controllers/DataType/Tree/ChildrenDataTypeTreeController.cs @@ -0,0 +1,24 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.ManagementApi.ViewModels.Pagination; +using Umbraco.Cms.ManagementApi.ViewModels.Tree; + +namespace Umbraco.Cms.ManagementApi.Controllers.DataType.Tree; + +public class ChildrenDataTypeTreeController : DataTypeTreeControllerBase +{ + public ChildrenDataTypeTreeController(IEntityService entityService, IDataTypeService dataTypeService) + : base(entityService, dataTypeService) + { + } + + [HttpGet("children")] + [MapToApiVersion("1.0")] + [ProducesResponseType(typeof(PagedViewModel), StatusCodes.Status200OK)] + public async Task>> Children(Guid parentKey, int skip = 0, int take = 100, bool foldersOnly = false) + { + RenderFoldersOnly(foldersOnly); + return await GetChildren(parentKey, skip, take); + } +} diff --git a/src/Umbraco.Cms.ManagementApi/Controllers/DataType/Tree/DataTypeTreeControllerBase.cs b/src/Umbraco.Cms.ManagementApi/Controllers/DataType/Tree/DataTypeTreeControllerBase.cs new file mode 100644 index 0000000000..4bdae5a2fa --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Controllers/DataType/Tree/DataTypeTreeControllerBase.cs @@ -0,0 +1,46 @@ +using Microsoft.AspNetCore.Mvc; +using NSwag.Annotations; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.Entities; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.ManagementApi.Controllers.Tree; +using Umbraco.Cms.ManagementApi.ViewModels.Tree; +using Umbraco.New.Cms.Web.Common.Routing; + +namespace Umbraco.Cms.ManagementApi.Controllers.DataType.Tree; + +[ApiVersion("1.0")] +[ApiController] +[VersionedApiBackOfficeRoute($"{Constants.UdiEntityType.DataType}/tree")] +[OpenApiTag(nameof(Constants.UdiEntityType.DataType))] +public class DataTypeTreeControllerBase : FolderTreeControllerBase +{ + private readonly IDataTypeService _dataTypeService; + + public DataTypeTreeControllerBase(IEntityService entityService, IDataTypeService dataTypeService) + : base(entityService) => + _dataTypeService = dataTypeService; + + protected override UmbracoObjectTypes ItemObjectType => UmbracoObjectTypes.DataType; + + protected override UmbracoObjectTypes FolderObjectType => UmbracoObjectTypes.DataTypeContainer; + + protected override FolderTreeItemViewModel[] MapTreeItemViewModels(Guid? parentKey, IEntitySlim[] entities) + { + var dataTypes = _dataTypeService + .GetAll(entities.Select(entity => entity.Id).ToArray()) + .ToDictionary(contentType => contentType.Id); + + return entities.Select(entity => + { + FolderTreeItemViewModel viewModel = MapTreeItemViewModel(parentKey, entity); + if (dataTypes.TryGetValue(entity.Id, out IDataType? dataType)) + { + viewModel.Icon = dataType.Editor?.Icon ?? viewModel.Icon; + } + + return viewModel; + }).ToArray(); + } +} diff --git a/src/Umbraco.Cms.ManagementApi/Controllers/DataType/Tree/ItemsDataTypeTreeController.cs b/src/Umbraco.Cms.ManagementApi/Controllers/DataType/Tree/ItemsDataTypeTreeController.cs new file mode 100644 index 0000000000..82eabcb6f4 --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Controllers/DataType/Tree/ItemsDataTypeTreeController.cs @@ -0,0 +1,20 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.ManagementApi.ViewModels.Tree; + +namespace Umbraco.Cms.ManagementApi.Controllers.DataType.Tree; + +public class ItemsDataTypeTreeController : DataTypeTreeControllerBase +{ + public ItemsDataTypeTreeController(IEntityService entityService, IDataTypeService dataTypeService) + : base(entityService, dataTypeService) + { + } + + [HttpGet("items")] + [MapToApiVersion("1.0")] + [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)] + public async Task>> Items([FromQuery(Name = "key")] Guid[] keys) + => await GetItems(keys); +} diff --git a/src/Umbraco.Cms.ManagementApi/Controllers/DataType/Tree/RootDataTypeTreeController.cs b/src/Umbraco.Cms.ManagementApi/Controllers/DataType/Tree/RootDataTypeTreeController.cs new file mode 100644 index 0000000000..adae969985 --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Controllers/DataType/Tree/RootDataTypeTreeController.cs @@ -0,0 +1,24 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.ManagementApi.ViewModels.Pagination; +using Umbraco.Cms.ManagementApi.ViewModels.Tree; + +namespace Umbraco.Cms.ManagementApi.Controllers.DataType.Tree; + +public class RootDataTypeTreeController : DataTypeTreeControllerBase +{ + public RootDataTypeTreeController(IEntityService entityService, IDataTypeService dataTypeService) + : base(entityService, dataTypeService) + { + } + + [HttpGet("root")] + [MapToApiVersion("1.0")] + [ProducesResponseType(typeof(PagedViewModel), StatusCodes.Status200OK)] + public async Task>> Root(int skip = 0, int take = 100, bool foldersOnly = false) + { + RenderFoldersOnly(foldersOnly); + return await GetRoot(skip, take); + } +} diff --git a/src/Umbraco.Cms.ManagementApi/Controllers/DictionaryItem/Tree/ChildrenDictionaryItemTreeController.cs b/src/Umbraco.Cms.ManagementApi/Controllers/DictionaryItem/Tree/ChildrenDictionaryItemTreeController.cs new file mode 100644 index 0000000000..0969b808c9 --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Controllers/DictionaryItem/Tree/ChildrenDictionaryItemTreeController.cs @@ -0,0 +1,39 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.ManagementApi.Services.Paging; +using Umbraco.Cms.ManagementApi.ViewModels.Pagination; +using Umbraco.Cms.ManagementApi.ViewModels.Tree; + +namespace Umbraco.Cms.ManagementApi.Controllers.DictionaryItem.Tree; + +public class ChildrenDictionaryItemTreeController : DictionaryItemTreeControllerBase +{ + public ChildrenDictionaryItemTreeController(IEntityService entityService, ILocalizationService localizationService) + : base(entityService, localizationService) + { + } + + [HttpGet("children")] + [MapToApiVersion("1.0")] + [ProducesResponseType(typeof(PagedViewModel), StatusCodes.Status200OK)] + public async Task>> Children(Guid parentKey, int skip = 0, int take = 100) + { + if (PaginationService.ConvertSkipTakeToPaging(skip, take, out var pageNumber, out var pageSize, out ProblemDetails? error) == false) + { + return BadRequest(error); + } + + IDictionaryItem[] dictionaryItems = PaginatedDictionaryItems( + pageNumber, + pageSize, + LocalizationService.GetDictionaryItemChildren(parentKey), + out var totalItems); + + EntityTreeItemViewModel[] viewModels = MapTreeItemViewModels(null, dictionaryItems); + + PagedViewModel result = PagedViewModel(viewModels, totalItems); + return await Task.FromResult(Ok(result)); + } +} diff --git a/src/Umbraco.Cms.ManagementApi/Controllers/DictionaryItem/Tree/DictionaryItemTreeControllerBase.cs b/src/Umbraco.Cms.ManagementApi/Controllers/DictionaryItem/Tree/DictionaryItemTreeControllerBase.cs new file mode 100644 index 0000000000..0d6a513e2d --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Controllers/DictionaryItem/Tree/DictionaryItemTreeControllerBase.cs @@ -0,0 +1,53 @@ +using Microsoft.AspNetCore.Mvc; +using NSwag.Annotations; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.ManagementApi.Controllers.Tree; +using Umbraco.Cms.ManagementApi.ViewModels.Tree; +using Umbraco.New.Cms.Web.Common.Routing; + +namespace Umbraco.Cms.ManagementApi.Controllers.DictionaryItem.Tree; + +[ApiVersion("1.0")] +[ApiController] +[VersionedApiBackOfficeRoute($"{Constants.UdiEntityType.DictionaryItem}/tree")] +[OpenApiTag(nameof(Constants.UdiEntityType.DictionaryItem))] +// NOTE: at the moment dictionary items aren't supported by EntityService, so we have little use of the +// tree controller base. We'll keep it though, in the hope that we can mend EntityService. +public class DictionaryItemTreeControllerBase : EntityTreeControllerBase +{ + public DictionaryItemTreeControllerBase(IEntityService entityService, ILocalizationService localizationService) + : base(entityService) => + LocalizationService = localizationService; + + // dictionary items do not currently have a known UmbracoObjectType, so we'll settle with Unknown for now + protected override UmbracoObjectTypes ItemObjectType => UmbracoObjectTypes.Unknown; + + protected ILocalizationService LocalizationService { get; } + + protected EntityTreeItemViewModel[] MapTreeItemViewModels(Guid? parentKey, IDictionaryItem[] dictionaryItems) + => dictionaryItems.Select(dictionaryItem => new EntityTreeItemViewModel + { + Icon = Constants.Icons.RelationType, + Name = dictionaryItem.ItemKey, + Key = dictionaryItem.Key, + Type = Constants.UdiEntityType.DictionaryItem, + HasChildren = false, + IsContainer = LocalizationService.GetDictionaryItemChildren(dictionaryItem.Key).Any(), + ParentKey = parentKey + }).ToArray(); + + // localization service does not (yet) allow pagination of dictionary items, we have to do it in memory for now + protected IDictionaryItem[] PaginatedDictionaryItems(long pageNumber, int pageSize, IEnumerable allDictionaryItems, out long totalItems) + { + IDictionaryItem[] allDictionaryItemsAsArray = allDictionaryItems.ToArray(); + + totalItems = allDictionaryItemsAsArray.Length; + return allDictionaryItemsAsArray + .OrderBy(item => item.ItemKey) + .Skip((int)pageNumber * pageSize) + .Take(pageSize) + .ToArray(); + } +} diff --git a/src/Umbraco.Cms.ManagementApi/Controllers/DictionaryItem/Tree/ItemsDictionaryItemTreeController.cs b/src/Umbraco.Cms.ManagementApi/Controllers/DictionaryItem/Tree/ItemsDictionaryItemTreeController.cs new file mode 100644 index 0000000000..69d2fda33d --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Controllers/DictionaryItem/Tree/ItemsDictionaryItemTreeController.cs @@ -0,0 +1,27 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.ManagementApi.ViewModels.Tree; + +namespace Umbraco.Cms.ManagementApi.Controllers.DictionaryItem.Tree; + +public class ItemsDictionaryItemTreeController : DictionaryItemTreeControllerBase +{ + public ItemsDictionaryItemTreeController(IEntityService entityService, ILocalizationService localizationService) + : base(entityService, localizationService) + { + } + + [HttpGet("items")] + [MapToApiVersion("1.0")] + [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)] + public async Task>> Items([FromQuery(Name = "key")] Guid[] keys) + { + IDictionaryItem[] dictionaryItems = LocalizationService.GetDictionaryItemsByIds(keys).ToArray(); + + EntityTreeItemViewModel[] viewModels = MapTreeItemViewModels(null, dictionaryItems); + + return await Task.FromResult(Ok(viewModels)); + } +} diff --git a/src/Umbraco.Cms.ManagementApi/Controllers/DictionaryItem/Tree/RootDictionaryItemTreeController.cs b/src/Umbraco.Cms.ManagementApi/Controllers/DictionaryItem/Tree/RootDictionaryItemTreeController.cs new file mode 100644 index 0000000000..18abf7a728 --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Controllers/DictionaryItem/Tree/RootDictionaryItemTreeController.cs @@ -0,0 +1,39 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.ManagementApi.Services.Paging; +using Umbraco.Cms.ManagementApi.ViewModels.Pagination; +using Umbraco.Cms.ManagementApi.ViewModels.Tree; + +namespace Umbraco.Cms.ManagementApi.Controllers.DictionaryItem.Tree; + +public class RootDictionaryItemTreeController : DictionaryItemTreeControllerBase +{ + public RootDictionaryItemTreeController(IEntityService entityService, ILocalizationService localizationService) + : base(entityService, localizationService) + { + } + + [HttpGet("root")] + [MapToApiVersion("1.0")] + [ProducesResponseType(typeof(PagedViewModel), StatusCodes.Status200OK)] + public async Task>> Root(int skip = 0, int take = 100) + { + if (PaginationService.ConvertSkipTakeToPaging(skip, take, out var pageNumber, out var pageSize, out ProblemDetails? error) == false) + { + return BadRequest(error); + } + + IDictionaryItem[] dictionaryItems = PaginatedDictionaryItems( + pageNumber, + pageSize, + LocalizationService.GetRootDictionaryItems(), + out var totalItems); + + EntityTreeItemViewModel[] viewModels = MapTreeItemViewModels(null, dictionaryItems); + + PagedViewModel result = PagedViewModel(viewModels, totalItems); + return await Task.FromResult(Ok(result)); + } +} diff --git a/src/Umbraco.Cms.ManagementApi/Controllers/Document/RecycleBin/ChildrenDocumentRecycleBinController.cs b/src/Umbraco.Cms.ManagementApi/Controllers/Document/RecycleBin/ChildrenDocumentRecycleBinController.cs new file mode 100644 index 0000000000..3127382588 --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Controllers/Document/RecycleBin/ChildrenDocumentRecycleBinController.cs @@ -0,0 +1,21 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.ManagementApi.ViewModels.Pagination; +using Umbraco.Cms.ManagementApi.ViewModels.RecycleBin; + +namespace Umbraco.Cms.ManagementApi.Controllers.Document.RecycleBin; + +public class ChildrenDocumentRecycleBinController : DocumentRecycleBinControllerBase +{ + public ChildrenDocumentRecycleBinController(IEntityService entityService) + : base(entityService) + { + } + + [HttpGet("children")] + [MapToApiVersion("1.0")] + [ProducesResponseType(typeof(PagedViewModel), StatusCodes.Status200OK)] + public async Task>> Children(Guid parentKey, int skip = 0, int take = 100) + => await GetChildren(parentKey, skip, take); +} diff --git a/src/Umbraco.Cms.ManagementApi/Controllers/Document/RecycleBin/DocumentRecycleBinControllerBase.cs b/src/Umbraco.Cms.ManagementApi/Controllers/Document/RecycleBin/DocumentRecycleBinControllerBase.cs new file mode 100644 index 0000000000..747b6b3296 --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Controllers/Document/RecycleBin/DocumentRecycleBinControllerBase.cs @@ -0,0 +1,43 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using NSwag.Annotations; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.Entities; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.ManagementApi.Controllers.RecycleBin; +using Umbraco.Cms.ManagementApi.Filters; +using Umbraco.Cms.ManagementApi.ViewModels.RecycleBin; +using Umbraco.New.Cms.Web.Common.Routing; + +namespace Umbraco.Cms.ManagementApi.Controllers.Document.RecycleBin; + +[ApiVersion("1.0")] +[ApiController] +[VersionedApiBackOfficeRoute($"{Constants.UdiEntityType.Document}/recycle-bin")] +[RequireDocumentTreeRootAccess] +[ProducesResponseType(StatusCodes.Status401Unauthorized)] +[OpenApiTag(nameof(Constants.UdiEntityType.Document))] +public class DocumentRecycleBinControllerBase : RecycleBinControllerBase +{ + public DocumentRecycleBinControllerBase(IEntityService entityService) + : base(entityService) + { + } + + protected override UmbracoObjectTypes ItemObjectType => UmbracoObjectTypes.Document; + + protected override int RecycleBinRootId => Constants.System.RecycleBinContent; + + protected override RecycleBinItemViewModel MapRecycleBinViewModel(Guid? parentKey, IEntitySlim entity) + { + RecycleBinItemViewModel viewModel = base.MapRecycleBinViewModel(parentKey, entity); + + if (entity is IDocumentEntitySlim documentEntitySlim) + { + viewModel.Icon = documentEntitySlim.ContentTypeIcon ?? viewModel.Icon; + } + + return viewModel; + } +} diff --git a/src/Umbraco.Cms.ManagementApi/Controllers/Document/RecycleBin/RootDocumentRecycleBinController.cs b/src/Umbraco.Cms.ManagementApi/Controllers/Document/RecycleBin/RootDocumentRecycleBinController.cs new file mode 100644 index 0000000000..21b79eb745 --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Controllers/Document/RecycleBin/RootDocumentRecycleBinController.cs @@ -0,0 +1,21 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.ManagementApi.ViewModels.Pagination; +using Umbraco.Cms.ManagementApi.ViewModels.RecycleBin; + +namespace Umbraco.Cms.ManagementApi.Controllers.Document.RecycleBin; + +public class RootDocumentRecycleBinController : DocumentRecycleBinControllerBase +{ + public RootDocumentRecycleBinController(IEntityService entityService) + : base(entityService) + { + } + + [HttpGet("root")] + [MapToApiVersion("1.0")] + [ProducesResponseType(typeof(PagedViewModel), StatusCodes.Status200OK)] + public async Task>> Root(int skip = 0, int take = 100) + => await GetRoot(skip, take); +} diff --git a/src/Umbraco.Cms.ManagementApi/Controllers/Document/Tree/ChildrenDocumentTreeController.cs b/src/Umbraco.Cms.ManagementApi/Controllers/Document/Tree/ChildrenDocumentTreeController.cs new file mode 100644 index 0000000000..1a72e9a41f --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Controllers/Document/Tree/ChildrenDocumentTreeController.cs @@ -0,0 +1,34 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Core.Cache; +using Umbraco.Cms.Core.Security; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.ManagementApi.Services.Entities; +using Umbraco.Cms.ManagementApi.ViewModels.Pagination; +using Umbraco.Cms.ManagementApi.ViewModels.Tree; + +namespace Umbraco.Cms.ManagementApi.Controllers.Document.Tree; + +public class ChildrenDocumentTreeController : DocumentTreeControllerBase +{ + public ChildrenDocumentTreeController( + IEntityService entityService, + IUserStartNodeEntitiesService userStartNodeEntitiesService, + IDataTypeService dataTypeService, + IPublicAccessService publicAccessService, + AppCaches appCaches, + IBackOfficeSecurityAccessor backofficeSecurityAccessor) + : base(entityService, userStartNodeEntitiesService, dataTypeService, publicAccessService, appCaches, backofficeSecurityAccessor) + { + } + + [HttpGet("children")] + [MapToApiVersion("1.0")] + [ProducesResponseType(typeof(PagedViewModel), StatusCodes.Status200OK)] + public async Task>> Children(Guid parentKey, int skip = 0, int take = 100, Guid? dataTypeKey = null, string? culture = null) + { + IgnoreUserStartNodesForDataType(dataTypeKey); + RenderForClientCulture(culture); + return await GetChildren(parentKey, skip, take); + } +} diff --git a/src/Umbraco.Cms.ManagementApi/Controllers/Document/Tree/DocumentTreeControllerBase.cs b/src/Umbraco.Cms.ManagementApi/Controllers/Document/Tree/DocumentTreeControllerBase.cs new file mode 100644 index 0000000000..a20237537d --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Controllers/Document/Tree/DocumentTreeControllerBase.cs @@ -0,0 +1,94 @@ +using Microsoft.AspNetCore.Mvc; +using NSwag.Annotations; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Cache; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.Entities; +using Umbraco.Cms.Core.Security; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.ManagementApi.Controllers.Tree; +using Umbraco.Cms.ManagementApi.Services.Entities; +using Umbraco.Cms.ManagementApi.ViewModels.Tree; +using Umbraco.Extensions; +using Umbraco.New.Cms.Web.Common.Routing; + +namespace Umbraco.Cms.ManagementApi.Controllers.Document.Tree; + +[ApiVersion("1.0")] +[ApiController] +[VersionedApiBackOfficeRoute($"{Constants.UdiEntityType.Document}/tree")] +[OpenApiTag(nameof(Constants.UdiEntityType.Document))] +public abstract class DocumentTreeControllerBase : UserStartNodeTreeControllerBase +{ + private readonly IPublicAccessService _publicAccessService; + private readonly AppCaches _appCaches; + private readonly IBackOfficeSecurityAccessor _backofficeSecurityAccessor; + private string? _culture; + + protected DocumentTreeControllerBase( + IEntityService entityService, + IUserStartNodeEntitiesService userStartNodeEntitiesService, + IDataTypeService dataTypeService, + IPublicAccessService publicAccessService, + AppCaches appCaches, + IBackOfficeSecurityAccessor backofficeSecurityAccessor) + : base(entityService, userStartNodeEntitiesService, dataTypeService) + { + _publicAccessService = publicAccessService; + _appCaches = appCaches; + _backofficeSecurityAccessor = backofficeSecurityAccessor; + } + + protected override UmbracoObjectTypes ItemObjectType => UmbracoObjectTypes.Document; + + protected override Ordering ItemOrdering => Ordering.By(nameof(Infrastructure.Persistence.Dtos.NodeDto.SortOrder)); + + protected void RenderForClientCulture(string? culture) => _culture = culture; + + protected override DocumentTreeItemViewModel MapTreeItemViewModel(Guid? parentKey, IEntitySlim entity) + { + DocumentTreeItemViewModel viewModel = base.MapTreeItemViewModel(parentKey, entity); + + if (entity is IDocumentEntitySlim documentEntitySlim) + { + viewModel.IsPublished = documentEntitySlim.Published; + viewModel.IsEdited = documentEntitySlim.Edited; + viewModel.Icon = documentEntitySlim.ContentTypeIcon ?? viewModel.Icon; + viewModel.IsProtected = _publicAccessService.IsProtected(entity.Path); + + if (_culture != null && documentEntitySlim.Variations.VariesByCulture()) + { + viewModel.Name = documentEntitySlim.CultureNames.TryGetValue(_culture, out var cultureName) + ? cultureName + : $"({viewModel.Name})"; + + viewModel.IsPublished = documentEntitySlim.PublishedCultures.Contains(_culture); + viewModel.IsEdited = documentEntitySlim.EditedCultures.Contains(_culture); + } + + viewModel.IsEdited &= viewModel.IsPublished; + } + + return viewModel; + } + + // TODO: delete these (faking start node setup for unlimited editor) + protected override int[] GetUserStartNodeIds() => new[] { -1 }; + + protected override string[] GetUserStartNodePaths() => Array.Empty(); + + // TODO: use these implementations instead of the dummy ones above once we have backoffice auth in place + // protected override int[] GetUserStartNodeIds() + // => _backofficeSecurityAccessor + // .BackOfficeSecurity? + // .CurrentUser? + // .CalculateContentStartNodeIds(EntityService, _appCaches) + // ?? Array.Empty(); + // + // protected override string[] GetUserStartNodePaths() + // => _backofficeSecurityAccessor + // .BackOfficeSecurity? + // .CurrentUser? + // .GetContentStartNodePaths(EntityService, _appCaches) + // ?? Array.Empty(); +} diff --git a/src/Umbraco.Cms.ManagementApi/Controllers/Document/Tree/ItemsDocumentTreeController.cs b/src/Umbraco.Cms.ManagementApi/Controllers/Document/Tree/ItemsDocumentTreeController.cs new file mode 100644 index 0000000000..a18dfea069 --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Controllers/Document/Tree/ItemsDocumentTreeController.cs @@ -0,0 +1,33 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Core.Cache; +using Umbraco.Cms.Core.Security; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.ManagementApi.Services.Entities; +using Umbraco.Cms.ManagementApi.ViewModels.Tree; + +namespace Umbraco.Cms.ManagementApi.Controllers.Document.Tree; + +public class ItemsDocumentTreeController : DocumentTreeControllerBase +{ + public ItemsDocumentTreeController( + IEntityService entityService, + IUserStartNodeEntitiesService userStartNodeEntitiesService, + IDataTypeService dataTypeService, + IPublicAccessService publicAccessService, + AppCaches appCaches, + IBackOfficeSecurityAccessor backofficeSecurityAccessor) + : base(entityService, userStartNodeEntitiesService, dataTypeService, publicAccessService, appCaches, backofficeSecurityAccessor) + { + } + + [HttpGet("items")] + [MapToApiVersion("1.0")] + [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)] + public async Task>> Items([FromQuery(Name = "key")] Guid[] keys, Guid? dataTypeKey = null, string? culture = null) + { + IgnoreUserStartNodesForDataType(dataTypeKey); + RenderForClientCulture(culture); + return await GetItems(keys); + } +} diff --git a/src/Umbraco.Cms.ManagementApi/Controllers/Document/Tree/RootDocumentTreeController.cs b/src/Umbraco.Cms.ManagementApi/Controllers/Document/Tree/RootDocumentTreeController.cs new file mode 100644 index 0000000000..1091292162 --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Controllers/Document/Tree/RootDocumentTreeController.cs @@ -0,0 +1,34 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Core.Cache; +using Umbraco.Cms.Core.Security; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.ManagementApi.Services.Entities; +using Umbraco.Cms.ManagementApi.ViewModels.Pagination; +using Umbraco.Cms.ManagementApi.ViewModels.Tree; + +namespace Umbraco.Cms.ManagementApi.Controllers.Document.Tree; + +public class RootDocumentTreeController : DocumentTreeControllerBase +{ + public RootDocumentTreeController( + IEntityService entityService, + IUserStartNodeEntitiesService userStartNodeEntitiesService, + IDataTypeService dataTypeService, + IPublicAccessService publicAccessService, + AppCaches appCaches, + IBackOfficeSecurityAccessor backofficeSecurityAccessor) + : base(entityService, userStartNodeEntitiesService, dataTypeService, publicAccessService, appCaches, backofficeSecurityAccessor) + { + } + + [HttpGet("root")] + [MapToApiVersion("1.0")] + [ProducesResponseType(typeof(PagedViewModel), StatusCodes.Status200OK)] + public async Task>> Root(int skip = 0, int take = 100, Guid? dataTypeKey = null, string? culture = null) + { + IgnoreUserStartNodesForDataType(dataTypeKey); + RenderForClientCulture(culture); + return await GetRoot(skip, take); + } +} diff --git a/src/Umbraco.Cms.ManagementApi/Controllers/DocumentBlueprint/Tree/DocumentBlueprintTreeControllerBase.cs b/src/Umbraco.Cms.ManagementApi/Controllers/DocumentBlueprint/Tree/DocumentBlueprintTreeControllerBase.cs new file mode 100644 index 0000000000..c6247da3a9 --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Controllers/DocumentBlueprint/Tree/DocumentBlueprintTreeControllerBase.cs @@ -0,0 +1,56 @@ +using Microsoft.AspNetCore.Mvc; +using NSwag.Annotations; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.Entities; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.ManagementApi.Controllers.Tree; +using Umbraco.Cms.ManagementApi.ViewModels.Tree; +using Umbraco.New.Cms.Web.Common.Routing; + +namespace Umbraco.Cms.ManagementApi.Controllers.DocumentBlueprint.Tree; + +[ApiVersion("1.0")] +[ApiController] +[VersionedApiBackOfficeRoute($"{Constants.UdiEntityType.DocumentBlueprint}/tree")] +[OpenApiTag(nameof(Constants.UdiEntityType.DocumentBlueprint))] +public class DocumentBlueprintTreeControllerBase : EntityTreeControllerBase +{ + private readonly IContentTypeService _contentTypeService; + + public DocumentBlueprintTreeControllerBase(IEntityService entityService, IContentTypeService contentTypeService) + : base(entityService) => + _contentTypeService = contentTypeService; + + protected override UmbracoObjectTypes ItemObjectType => UmbracoObjectTypes.DocumentBlueprint; + + protected override DocumentBlueprintTreeItemViewModel[] MapTreeItemViewModels(Guid? parentKey, IEntitySlim[] entities) + { + var contentTypeAliases = entities + .OfType() + .Select(entity => entity.ContentTypeAlias) + .ToArray(); + + var contentTypeIds = _contentTypeService.GetAllContentTypeIds(contentTypeAliases).ToArray(); + var contentTypeByAlias = _contentTypeService + .GetAll(contentTypeIds) + .ToDictionary(contentType => contentType.Alias); + + return entities.Select(entity => + { + DocumentBlueprintTreeItemViewModel viewModel = base.MapTreeItemViewModel(parentKey, entity); + viewModel.Icon = Constants.Icons.Blueprint; + viewModel.HasChildren = false; + + if (entity is IDocumentEntitySlim documentEntitySlim + && contentTypeByAlias.TryGetValue(documentEntitySlim.ContentTypeAlias, out IContentType? contentType)) + { + viewModel.DocumentTypeKey = contentType.Key; + viewModel.DocumentTypeAlias = contentType.Alias; + viewModel.DocumentTypeName = contentType.Name; + } + + return viewModel; + }).ToArray(); + } +} diff --git a/src/Umbraco.Cms.ManagementApi/Controllers/DocumentBlueprint/Tree/ItemsDocumentBlueprintTreeController.cs b/src/Umbraco.Cms.ManagementApi/Controllers/DocumentBlueprint/Tree/ItemsDocumentBlueprintTreeController.cs new file mode 100644 index 0000000000..6b7edb6fab --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Controllers/DocumentBlueprint/Tree/ItemsDocumentBlueprintTreeController.cs @@ -0,0 +1,21 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.ManagementApi.ViewModels.Pagination; +using Umbraco.Cms.ManagementApi.ViewModels.Tree; + +namespace Umbraco.Cms.ManagementApi.Controllers.DocumentBlueprint.Tree; + +public class ItemsDocumentBlueprintTreeController : DocumentBlueprintTreeControllerBase +{ + public ItemsDocumentBlueprintTreeController(IEntityService entityService, IContentTypeService contentTypeService) + : base(entityService, contentTypeService) + { + } + + [HttpGet("items")] + [MapToApiVersion("1.0")] + [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)] + public async Task>> Items([FromQuery(Name = "key")] Guid[] keys) + => await GetItems(keys); +} diff --git a/src/Umbraco.Cms.ManagementApi/Controllers/DocumentBlueprint/Tree/RootDocumentBlueprintTreeController.cs b/src/Umbraco.Cms.ManagementApi/Controllers/DocumentBlueprint/Tree/RootDocumentBlueprintTreeController.cs new file mode 100644 index 0000000000..b9dd33d3bf --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Controllers/DocumentBlueprint/Tree/RootDocumentBlueprintTreeController.cs @@ -0,0 +1,21 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.ManagementApi.ViewModels.Pagination; +using Umbraco.Cms.ManagementApi.ViewModels.Tree; + +namespace Umbraco.Cms.ManagementApi.Controllers.DocumentBlueprint.Tree; + +public class RootDocumentBlueprintTreeController : DocumentBlueprintTreeControllerBase +{ + public RootDocumentBlueprintTreeController(IEntityService entityService, IContentTypeService contentTypeService) + : base(entityService, contentTypeService) + { + } + + [HttpGet("root")] + [MapToApiVersion("1.0")] + [ProducesResponseType(typeof(PagedViewModel), StatusCodes.Status200OK)] + public async Task>> Root(int skip = 0, int take = 100) + => await GetRoot(skip, take); +} diff --git a/src/Umbraco.Cms.ManagementApi/Controllers/DocumentType/Tree/ChildrenDocumentTypeTreeController.cs b/src/Umbraco.Cms.ManagementApi/Controllers/DocumentType/Tree/ChildrenDocumentTypeTreeController.cs new file mode 100644 index 0000000000..b6deb6e3f6 --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Controllers/DocumentType/Tree/ChildrenDocumentTypeTreeController.cs @@ -0,0 +1,24 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.ManagementApi.ViewModels.Pagination; +using Umbraco.Cms.ManagementApi.ViewModels.Tree; + +namespace Umbraco.Cms.ManagementApi.Controllers.DocumentType.Tree; + +public class ChildrenDocumentTypeTreeController : DocumentTypeTreeControllerBase +{ + public ChildrenDocumentTypeTreeController(IEntityService entityService, IContentTypeService contentTypeService) + : base(entityService, contentTypeService) + { + } + + [HttpGet("children")] + [MapToApiVersion("1.0")] + [ProducesResponseType(typeof(PagedViewModel), StatusCodes.Status200OK)] + public async Task>> Children(Guid parentKey, int skip = 0, int take = 100, bool foldersOnly = false) + { + RenderFoldersOnly(foldersOnly); + return await GetChildren(parentKey, skip, take); + } +} diff --git a/src/Umbraco.Cms.ManagementApi/Controllers/DocumentType/Tree/DocumentTypeTreeControllerBase.cs b/src/Umbraco.Cms.ManagementApi/Controllers/DocumentType/Tree/DocumentTypeTreeControllerBase.cs new file mode 100644 index 0000000000..cc4c224ef5 --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Controllers/DocumentType/Tree/DocumentTypeTreeControllerBase.cs @@ -0,0 +1,47 @@ +using Microsoft.AspNetCore.Mvc; +using NSwag.Annotations; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.Entities; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.ManagementApi.Controllers.Tree; +using Umbraco.Cms.ManagementApi.ViewModels.Tree; +using Umbraco.New.Cms.Web.Common.Routing; + +namespace Umbraco.Cms.ManagementApi.Controllers.DocumentType.Tree; + +[ApiVersion("1.0")] +[ApiController] +[VersionedApiBackOfficeRoute($"{Constants.UdiEntityType.DocumentType}/tree")] +[OpenApiTag(nameof(Constants.UdiEntityType.DocumentType))] +public class DocumentTypeTreeControllerBase : FolderTreeControllerBase +{ + private readonly IContentTypeService _contentTypeService; + + public DocumentTypeTreeControllerBase(IEntityService entityService, IContentTypeService contentTypeService) + : base(entityService) => + _contentTypeService = contentTypeService; + + protected override UmbracoObjectTypes ItemObjectType => UmbracoObjectTypes.DocumentType; + + protected override UmbracoObjectTypes FolderObjectType => UmbracoObjectTypes.DocumentTypeContainer; + + protected override DocumentTypeTreeItemViewModel[] MapTreeItemViewModels(Guid? parentKey, IEntitySlim[] entities) + { + var contentTypes = _contentTypeService + .GetAll(entities.Select(entity => entity.Id).ToArray()) + .ToDictionary(contentType => contentType.Id); + + return entities.Select(entity => + { + DocumentTypeTreeItemViewModel viewModel = MapTreeItemViewModel(parentKey, entity); + if (contentTypes.TryGetValue(entity.Id, out IContentType? contentType)) + { + viewModel.Icon = contentType.Icon ?? viewModel.Icon; + viewModel.IsElement = contentType.IsElement; + } + + return viewModel; + }).ToArray(); + } +} diff --git a/src/Umbraco.Cms.ManagementApi/Controllers/DocumentType/Tree/ItemsDocumentTypeTreeController.cs b/src/Umbraco.Cms.ManagementApi/Controllers/DocumentType/Tree/ItemsDocumentTypeTreeController.cs new file mode 100644 index 0000000000..e19bf249c6 --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Controllers/DocumentType/Tree/ItemsDocumentTypeTreeController.cs @@ -0,0 +1,20 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.ManagementApi.ViewModels.Tree; + +namespace Umbraco.Cms.ManagementApi.Controllers.DocumentType.Tree; + +public class ItemsDocumentTypeTreeController : DocumentTypeTreeControllerBase +{ + public ItemsDocumentTypeTreeController(IEntityService entityService, IContentTypeService contentTypeService) + : base(entityService, contentTypeService) + { + } + + [HttpGet("items")] + [MapToApiVersion("1.0")] + [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)] + public async Task>> Items([FromQuery(Name = "key")] Guid[] keys) + => await GetItems(keys); +} diff --git a/src/Umbraco.Cms.ManagementApi/Controllers/DocumentType/Tree/RootDocumentTypeTreeController.cs b/src/Umbraco.Cms.ManagementApi/Controllers/DocumentType/Tree/RootDocumentTypeTreeController.cs new file mode 100644 index 0000000000..9ffbfb6bf1 --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Controllers/DocumentType/Tree/RootDocumentTypeTreeController.cs @@ -0,0 +1,24 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.ManagementApi.ViewModels.Pagination; +using Umbraco.Cms.ManagementApi.ViewModels.Tree; + +namespace Umbraco.Cms.ManagementApi.Controllers.DocumentType.Tree; + +public class RootDocumentTypeTreeController : DocumentTypeTreeControllerBase +{ + public RootDocumentTypeTreeController(IEntityService entityService, IContentTypeService contentTypeService) + : base(entityService, contentTypeService) + { + } + + [HttpGet("root")] + [MapToApiVersion("1.0")] + [ProducesResponseType(typeof(PagedViewModel), StatusCodes.Status200OK)] + public async Task>> Root(int skip = 0, int take = 100, bool foldersOnly = false) + { + RenderFoldersOnly(foldersOnly); + return await GetRoot(skip, take); + } +} diff --git a/src/Umbraco.Cms.ManagementApi/Controllers/Install/InstallControllerBase.cs b/src/Umbraco.Cms.ManagementApi/Controllers/Install/InstallControllerBase.cs index 275a5cd7b7..e07af21c74 100644 --- a/src/Umbraco.Cms.ManagementApi/Controllers/Install/InstallControllerBase.cs +++ b/src/Umbraco.Cms.ManagementApi/Controllers/Install/InstallControllerBase.cs @@ -7,7 +7,7 @@ using Umbraco.New.Cms.Web.Common.Routing; namespace Umbraco.Cms.ManagementApi.Controllers.Install; [ApiController] -[BackOfficeRoute("api/v{version:apiVersion}/install")] +[VersionedApiBackOfficeRoute("install")] [OpenApiTag("Install")] [RequireRuntimeLevel(RuntimeLevel.Install)] public abstract class InstallControllerBase : Controller diff --git a/src/Umbraco.Cms.ManagementApi/Controllers/Media/RecycleBin/ChildrenMediaRecycleBinController.cs b/src/Umbraco.Cms.ManagementApi/Controllers/Media/RecycleBin/ChildrenMediaRecycleBinController.cs new file mode 100644 index 0000000000..48ef9b4227 --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Controllers/Media/RecycleBin/ChildrenMediaRecycleBinController.cs @@ -0,0 +1,21 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.ManagementApi.ViewModels.Pagination; +using Umbraco.Cms.ManagementApi.ViewModels.RecycleBin; + +namespace Umbraco.Cms.ManagementApi.Controllers.Media.RecycleBin; + +public class ChildrenMediaRecycleBinController : MediaRecycleBinControllerBase +{ + public ChildrenMediaRecycleBinController(IEntityService entityService) + : base(entityService) + { + } + + [HttpGet("children")] + [MapToApiVersion("1.0")] + [ProducesResponseType(typeof(PagedViewModel), StatusCodes.Status200OK)] + public async Task>> Children(Guid parentKey, int skip = 0, int take = 100) + => await GetChildren(parentKey, skip, take); +} diff --git a/src/Umbraco.Cms.ManagementApi/Controllers/Media/RecycleBin/MediaRecycleBinControllerBase.cs b/src/Umbraco.Cms.ManagementApi/Controllers/Media/RecycleBin/MediaRecycleBinControllerBase.cs new file mode 100644 index 0000000000..157e1099de --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Controllers/Media/RecycleBin/MediaRecycleBinControllerBase.cs @@ -0,0 +1,43 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using NSwag.Annotations; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.Entities; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.ManagementApi.Controllers.RecycleBin; +using Umbraco.Cms.ManagementApi.Filters; +using Umbraco.Cms.ManagementApi.ViewModels.RecycleBin; +using Umbraco.New.Cms.Web.Common.Routing; + +namespace Umbraco.Cms.ManagementApi.Controllers.Media.RecycleBin; + +[ApiVersion("1.0")] +[ApiController] +[VersionedApiBackOfficeRoute($"{Constants.UdiEntityType.Media}/recycle-bin")] +[RequireMediaTreeRootAccess] +[ProducesResponseType(StatusCodes.Status401Unauthorized)] +[OpenApiTag(nameof(Constants.UdiEntityType.Media))] +public class MediaRecycleBinControllerBase : RecycleBinControllerBase +{ + public MediaRecycleBinControllerBase(IEntityService entityService) + : base(entityService) + { + } + + protected override UmbracoObjectTypes ItemObjectType => UmbracoObjectTypes.Media; + + protected override int RecycleBinRootId => Constants.System.RecycleBinMedia; + + protected override RecycleBinItemViewModel MapRecycleBinViewModel(Guid? parentKey, IEntitySlim entity) + { + RecycleBinItemViewModel viewModel = base.MapRecycleBinViewModel(parentKey, entity); + + if (entity is IMediaEntitySlim mediaEntitySlim) + { + viewModel.Icon = mediaEntitySlim.ContentTypeIcon ?? viewModel.Icon; + } + + return viewModel; + } +} diff --git a/src/Umbraco.Cms.ManagementApi/Controllers/Media/RecycleBin/RootMediaRecycleBinController.cs b/src/Umbraco.Cms.ManagementApi/Controllers/Media/RecycleBin/RootMediaRecycleBinController.cs new file mode 100644 index 0000000000..9ae1330d58 --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Controllers/Media/RecycleBin/RootMediaRecycleBinController.cs @@ -0,0 +1,21 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.ManagementApi.ViewModels.Pagination; +using Umbraco.Cms.ManagementApi.ViewModels.RecycleBin; + +namespace Umbraco.Cms.ManagementApi.Controllers.Media.RecycleBin; + +public class RootMediaRecycleBinController : MediaRecycleBinControllerBase +{ + public RootMediaRecycleBinController(IEntityService entityService) + : base(entityService) + { + } + + [HttpGet("root")] + [MapToApiVersion("1.0")] + [ProducesResponseType(typeof(PagedViewModel), StatusCodes.Status200OK)] + public async Task>> Root(int skip = 0, int take = 100) + => await GetRoot(skip, take); +} diff --git a/src/Umbraco.Cms.ManagementApi/Controllers/Media/Tree/ChildrenMediaTreeController.cs b/src/Umbraco.Cms.ManagementApi/Controllers/Media/Tree/ChildrenMediaTreeController.cs new file mode 100644 index 0000000000..18d7b924af --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Controllers/Media/Tree/ChildrenMediaTreeController.cs @@ -0,0 +1,32 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Core.Cache; +using Umbraco.Cms.Core.Security; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.ManagementApi.Services.Entities; +using Umbraco.Cms.ManagementApi.ViewModels.Pagination; +using Umbraco.Cms.ManagementApi.ViewModels.Tree; + +namespace Umbraco.Cms.ManagementApi.Controllers.Media.Tree; + +public class ChildrenMediaTreeController : MediaTreeControllerBase +{ + public ChildrenMediaTreeController( + IEntityService entityService, + IUserStartNodeEntitiesService userStartNodeEntitiesService, + IDataTypeService dataTypeService, + AppCaches appCaches, + IBackOfficeSecurityAccessor backofficeSecurityAccessor) + : base(entityService, userStartNodeEntitiesService, dataTypeService, appCaches, backofficeSecurityAccessor) + { + } + + [HttpGet("children")] + [MapToApiVersion("1.0")] + [ProducesResponseType(typeof(PagedViewModel), StatusCodes.Status200OK)] + public async Task>> Children(Guid parentKey, int skip = 0, int take = 100, Guid? dataTypeKey = null) + { + IgnoreUserStartNodesForDataType(dataTypeKey); + return await GetChildren(parentKey, skip, take); + } +} diff --git a/src/Umbraco.Cms.ManagementApi/Controllers/Media/Tree/ItemsMediaTreeController.cs b/src/Umbraco.Cms.ManagementApi/Controllers/Media/Tree/ItemsMediaTreeController.cs new file mode 100644 index 0000000000..2ebf1a559f --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Controllers/Media/Tree/ItemsMediaTreeController.cs @@ -0,0 +1,31 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Core.Cache; +using Umbraco.Cms.Core.Security; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.ManagementApi.Services.Entities; +using Umbraco.Cms.ManagementApi.ViewModels.Tree; + +namespace Umbraco.Cms.ManagementApi.Controllers.Media.Tree; + +public class ItemsMediaTreeController : MediaTreeControllerBase +{ + public ItemsMediaTreeController( + IEntityService entityService, + IUserStartNodeEntitiesService userStartNodeEntitiesService, + IDataTypeService dataTypeService, + AppCaches appCaches, + IBackOfficeSecurityAccessor backofficeSecurityAccessor) + : base(entityService, userStartNodeEntitiesService, dataTypeService, appCaches, backofficeSecurityAccessor) + { + } + + [HttpGet("items")] + [MapToApiVersion("1.0")] + [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)] + public async Task>> Items([FromQuery(Name = "key")] Guid[] keys, Guid? dataTypeKey = null) + { + IgnoreUserStartNodesForDataType(dataTypeKey); + return await GetItems(keys); + } +} diff --git a/src/Umbraco.Cms.ManagementApi/Controllers/Media/Tree/MediaTreeControllerBase.cs b/src/Umbraco.Cms.ManagementApi/Controllers/Media/Tree/MediaTreeControllerBase.cs new file mode 100644 index 0000000000..c03f05c71d --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Controllers/Media/Tree/MediaTreeControllerBase.cs @@ -0,0 +1,72 @@ +using Microsoft.AspNetCore.Mvc; +using NSwag.Annotations; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Cache; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.Entities; +using Umbraco.Cms.Core.Security; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.ManagementApi.Controllers.Tree; +using Umbraco.Cms.ManagementApi.Services.Entities; +using Umbraco.Cms.ManagementApi.ViewModels.Tree; +using Umbraco.New.Cms.Web.Common.Routing; + +namespace Umbraco.Cms.ManagementApi.Controllers.Media.Tree; + +[ApiVersion("1.0")] +[ApiController] +[VersionedApiBackOfficeRoute($"{Constants.UdiEntityType.Media}/tree")] +[OpenApiTag(nameof(Constants.UdiEntityType.Media))] +public class MediaTreeControllerBase : UserStartNodeTreeControllerBase +{ + private readonly AppCaches _appCaches; + private readonly IBackOfficeSecurityAccessor _backofficeSecurityAccessor; + + public MediaTreeControllerBase( + IEntityService entityService, + IUserStartNodeEntitiesService userStartNodeEntitiesService, + IDataTypeService dataTypeService, + AppCaches appCaches, + IBackOfficeSecurityAccessor backofficeSecurityAccessor) + : base(entityService, userStartNodeEntitiesService, dataTypeService) + { + _appCaches = appCaches; + _backofficeSecurityAccessor = backofficeSecurityAccessor; + } + + protected override UmbracoObjectTypes ItemObjectType => UmbracoObjectTypes.Media; + + protected override Ordering ItemOrdering => Ordering.By(nameof(Infrastructure.Persistence.Dtos.NodeDto.SortOrder)); + + protected override ContentTreeItemViewModel MapTreeItemViewModel(Guid? parentKey, IEntitySlim entity) + { + ContentTreeItemViewModel viewModel = base.MapTreeItemViewModel(parentKey, entity); + + if (entity is IMediaEntitySlim mediaEntitySlim) + { + viewModel.Icon = mediaEntitySlim.ContentTypeIcon ?? viewModel.Icon; + } + + return viewModel; + } + + // TODO: delete these (faking start node setup for unlimited editor) + protected override int[] GetUserStartNodeIds() => new[] { -1 }; + + protected override string[] GetUserStartNodePaths() => Array.Empty(); + + // TODO: use these implementations instead of the dummy ones above once we have backoffice auth in place + // protected override int[] GetUserStartNodeIds() + // => _backofficeSecurityAccessor + // .BackOfficeSecurity? + // .CurrentUser? + // .CalculateMediaStartNodeIds(EntityService, _appCaches) + // ?? Array.Empty(); + // + // protected override string[] GetUserStartNodePaths() + // => _backofficeSecurityAccessor + // .BackOfficeSecurity? + // .CurrentUser? + // .GetMediaStartNodePaths(EntityService, _appCaches) + // ?? Array.Empty(); +} diff --git a/src/Umbraco.Cms.ManagementApi/Controllers/Media/Tree/RootMediaTreeController.cs b/src/Umbraco.Cms.ManagementApi/Controllers/Media/Tree/RootMediaTreeController.cs new file mode 100644 index 0000000000..59723a6ffd --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Controllers/Media/Tree/RootMediaTreeController.cs @@ -0,0 +1,32 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Core.Cache; +using Umbraco.Cms.Core.Security; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.ManagementApi.Services.Entities; +using Umbraco.Cms.ManagementApi.ViewModels.Pagination; +using Umbraco.Cms.ManagementApi.ViewModels.Tree; + +namespace Umbraco.Cms.ManagementApi.Controllers.Media.Tree; + +public class RootMediaTreeController : MediaTreeControllerBase +{ + public RootMediaTreeController( + IEntityService entityService, + IUserStartNodeEntitiesService userStartNodeEntitiesService, + IDataTypeService dataTypeService, + AppCaches appCaches, + IBackOfficeSecurityAccessor backofficeSecurityAccessor) + : base(entityService, userStartNodeEntitiesService, dataTypeService, appCaches, backofficeSecurityAccessor) + { + } + + [HttpGet("root")] + [MapToApiVersion("1.0")] + [ProducesResponseType(typeof(PagedViewModel), StatusCodes.Status200OK)] + public async Task>> Root(int skip = 0, int take = 100, Guid? dataTypeKey = null) + { + IgnoreUserStartNodesForDataType(dataTypeKey); + return await GetRoot(skip, take); + } +} diff --git a/src/Umbraco.Cms.ManagementApi/Controllers/MediaType/Tree/ChildrenMediaTypeTreeController.cs b/src/Umbraco.Cms.ManagementApi/Controllers/MediaType/Tree/ChildrenMediaTypeTreeController.cs new file mode 100644 index 0000000000..ee416938d3 --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Controllers/MediaType/Tree/ChildrenMediaTypeTreeController.cs @@ -0,0 +1,24 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.ManagementApi.ViewModels.Pagination; +using Umbraco.Cms.ManagementApi.ViewModels.Tree; + +namespace Umbraco.Cms.ManagementApi.Controllers.MediaType.Tree; + +public class ChildrenMediaTypeTreeController : MediaTypeTreeControllerBase +{ + public ChildrenMediaTypeTreeController(IEntityService entityService, IMediaTypeService mediaTypeService) + : base(entityService, mediaTypeService) + { + } + + [HttpGet("children")] + [MapToApiVersion("1.0")] + [ProducesResponseType(typeof(PagedViewModel), StatusCodes.Status200OK)] + public async Task>> Children(Guid parentKey, int skip = 0, int take = 100, bool foldersOnly = false) + { + RenderFoldersOnly(foldersOnly); + return await GetChildren(parentKey, skip, take); + } +} diff --git a/src/Umbraco.Cms.ManagementApi/Controllers/MediaType/Tree/ItemsMediaTypeTreeController.cs b/src/Umbraco.Cms.ManagementApi/Controllers/MediaType/Tree/ItemsMediaTypeTreeController.cs new file mode 100644 index 0000000000..363751fee7 --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Controllers/MediaType/Tree/ItemsMediaTypeTreeController.cs @@ -0,0 +1,20 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.ManagementApi.ViewModels.Tree; + +namespace Umbraco.Cms.ManagementApi.Controllers.MediaType.Tree; + +public class ItemsMediaTypeTreeController : MediaTypeTreeControllerBase +{ + public ItemsMediaTypeTreeController(IEntityService entityService, IMediaTypeService mediaTypeService) + : base(entityService, mediaTypeService) + { + } + + [HttpGet("items")] + [MapToApiVersion("1.0")] + [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)] + public async Task>> Items([FromQuery(Name = "key")] Guid[] keys) + => await GetItems(keys); +} diff --git a/src/Umbraco.Cms.ManagementApi/Controllers/MediaType/Tree/MediaTypeTreeControllerBase.cs b/src/Umbraco.Cms.ManagementApi/Controllers/MediaType/Tree/MediaTypeTreeControllerBase.cs new file mode 100644 index 0000000000..5b06c46439 --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Controllers/MediaType/Tree/MediaTypeTreeControllerBase.cs @@ -0,0 +1,46 @@ +using Microsoft.AspNetCore.Mvc; +using NSwag.Annotations; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.Entities; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.ManagementApi.Controllers.Tree; +using Umbraco.Cms.ManagementApi.ViewModels.Tree; +using Umbraco.New.Cms.Web.Common.Routing; + +namespace Umbraco.Cms.ManagementApi.Controllers.MediaType.Tree; + +[ApiVersion("1.0")] +[ApiController] +[VersionedApiBackOfficeRoute($"{Constants.UdiEntityType.MediaType}/tree")] +[OpenApiTag(nameof(Constants.UdiEntityType.MediaType))] +public class MediaTypeTreeControllerBase : FolderTreeControllerBase +{ + private readonly IMediaTypeService _mediaTypeService; + + public MediaTypeTreeControllerBase(IEntityService entityService, IMediaTypeService mediaTypeService) + : base(entityService) => + _mediaTypeService = mediaTypeService; + + protected override UmbracoObjectTypes ItemObjectType => UmbracoObjectTypes.MediaType; + + protected override UmbracoObjectTypes FolderObjectType => UmbracoObjectTypes.MediaTypeContainer; + + protected override FolderTreeItemViewModel[] MapTreeItemViewModels(Guid? parentKey, IEntitySlim[] entities) + { + var mediaTypes = _mediaTypeService + .GetAll(entities.Select(entity => entity.Id).ToArray()) + .ToDictionary(contentType => contentType.Id); + + return entities.Select(entity => + { + FolderTreeItemViewModel viewModel = MapTreeItemViewModel(parentKey, entity); + if (mediaTypes.TryGetValue(entity.Id, out IMediaType? mediaType)) + { + viewModel.Icon = mediaType.Icon ?? viewModel.Icon; + } + + return viewModel; + }).ToArray(); + } +} diff --git a/src/Umbraco.Cms.ManagementApi/Controllers/MediaType/Tree/RootMediaTypeTreeController.cs b/src/Umbraco.Cms.ManagementApi/Controllers/MediaType/Tree/RootMediaTypeTreeController.cs new file mode 100644 index 0000000000..902fc1fcc5 --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Controllers/MediaType/Tree/RootMediaTypeTreeController.cs @@ -0,0 +1,24 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.ManagementApi.ViewModels.Pagination; +using Umbraco.Cms.ManagementApi.ViewModels.Tree; + +namespace Umbraco.Cms.ManagementApi.Controllers.MediaType.Tree; + +public class RootMediaTypeTreeController : MediaTypeTreeControllerBase +{ + public RootMediaTypeTreeController(IEntityService entityService, IMediaTypeService mediaTypeService) + : base(entityService, mediaTypeService) + { + } + + [HttpGet("root")] + [MapToApiVersion("1.0")] + [ProducesResponseType(typeof(PagedViewModel), StatusCodes.Status200OK)] + public async Task>> Root(int skip = 0, int take = 100, bool foldersOnly = false) + { + RenderFoldersOnly(foldersOnly); + return await GetRoot(skip, take); + } +} diff --git a/src/Umbraco.Cms.ManagementApi/Controllers/MemberGroup/Tree/ItemsMemberGroupTreeController.cs b/src/Umbraco.Cms.ManagementApi/Controllers/MemberGroup/Tree/ItemsMemberGroupTreeController.cs new file mode 100644 index 0000000000..94db46be4e --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Controllers/MemberGroup/Tree/ItemsMemberGroupTreeController.cs @@ -0,0 +1,20 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.ManagementApi.ViewModels.Tree; + +namespace Umbraco.Cms.ManagementApi.Controllers.MemberGroup.Tree; + +public class ItemsMemberGroupTreeController : MemberGroupTreeControllerBase +{ + public ItemsMemberGroupTreeController(IEntityService entityService) + : base(entityService) + { + } + + [HttpGet("items")] + [MapToApiVersion("1.0")] + [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)] + public async Task>> Items([FromQuery(Name = "key")] Guid[] keys) + => await GetItems(keys); +} diff --git a/src/Umbraco.Cms.ManagementApi/Controllers/MemberGroup/Tree/MemberGroupTreeControllerBase.cs b/src/Umbraco.Cms.ManagementApi/Controllers/MemberGroup/Tree/MemberGroupTreeControllerBase.cs new file mode 100644 index 0000000000..b3ee033a25 --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Controllers/MemberGroup/Tree/MemberGroupTreeControllerBase.cs @@ -0,0 +1,32 @@ +using Microsoft.AspNetCore.Mvc; +using NSwag.Annotations; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.Entities; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.ManagementApi.Controllers.Tree; +using Umbraco.Cms.ManagementApi.ViewModels.Tree; +using Umbraco.New.Cms.Web.Common.Routing; + +namespace Umbraco.Cms.ManagementApi.Controllers.MemberGroup.Tree; + +[ApiVersion("1.0")] +[ApiController] +[VersionedApiBackOfficeRoute($"{Constants.UdiEntityType.MemberGroup}/tree")] +[OpenApiTag(nameof(Constants.UdiEntityType.MemberGroup))] +public class MemberGroupTreeControllerBase : EntityTreeControllerBase +{ + public MemberGroupTreeControllerBase(IEntityService entityService) + : base(entityService) + { + } + + protected override UmbracoObjectTypes ItemObjectType => UmbracoObjectTypes.MemberGroup; + + protected override EntityTreeItemViewModel MapTreeItemViewModel(Guid? parentKey, IEntitySlim entity) + { + EntityTreeItemViewModel viewModel = base.MapTreeItemViewModel(parentKey, entity); + viewModel.Icon = Constants.Icons.MemberGroup; + return viewModel; + } +} diff --git a/src/Umbraco.Cms.ManagementApi/Controllers/MemberGroup/Tree/RootMemberGroupTreeController.cs b/src/Umbraco.Cms.ManagementApi/Controllers/MemberGroup/Tree/RootMemberGroupTreeController.cs new file mode 100644 index 0000000000..4b9e7a35ab --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Controllers/MemberGroup/Tree/RootMemberGroupTreeController.cs @@ -0,0 +1,21 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.ManagementApi.ViewModels.Pagination; +using Umbraco.Cms.ManagementApi.ViewModels.Tree; + +namespace Umbraco.Cms.ManagementApi.Controllers.MemberGroup.Tree; + +public class RootMemberGroupTreeController : MemberGroupTreeControllerBase +{ + public RootMemberGroupTreeController(IEntityService entityService) + : base(entityService) + { + } + + [HttpGet("root")] + [MapToApiVersion("1.0")] + [ProducesResponseType(typeof(PagedViewModel), StatusCodes.Status200OK)] + public async Task>> Root(int skip = 0, int take = 100) + => await GetRoot(skip, take); +} diff --git a/src/Umbraco.Cms.ManagementApi/Controllers/MemberType/Tree/ItemsMemberTypeTreeController.cs b/src/Umbraco.Cms.ManagementApi/Controllers/MemberType/Tree/ItemsMemberTypeTreeController.cs new file mode 100644 index 0000000000..249cdd5d67 --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Controllers/MemberType/Tree/ItemsMemberTypeTreeController.cs @@ -0,0 +1,20 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.ManagementApi.ViewModels.Tree; + +namespace Umbraco.Cms.ManagementApi.Controllers.MemberType.Tree; + +public class ItemsMemberTypeTreeController : MemberTypeTreeControllerBase +{ + public ItemsMemberTypeTreeController(IEntityService entityService) + : base(entityService) + { + } + + [HttpGet("items")] + [MapToApiVersion("1.0")] + [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)] + public async Task>> Items([FromQuery(Name = "key")] Guid[] keys) + => await GetItems(keys); +} diff --git a/src/Umbraco.Cms.ManagementApi/Controllers/MemberType/Tree/MemberTypeTreeControllerBase.cs b/src/Umbraco.Cms.ManagementApi/Controllers/MemberType/Tree/MemberTypeTreeControllerBase.cs new file mode 100644 index 0000000000..88183dfd58 --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Controllers/MemberType/Tree/MemberTypeTreeControllerBase.cs @@ -0,0 +1,32 @@ +using Microsoft.AspNetCore.Mvc; +using NSwag.Annotations; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.Entities; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.ManagementApi.Controllers.Tree; +using Umbraco.Cms.ManagementApi.ViewModels.Tree; +using Umbraco.New.Cms.Web.Common.Routing; + +namespace Umbraco.Cms.ManagementApi.Controllers.MemberType.Tree; + +[ApiVersion("1.0")] +[ApiController] +[VersionedApiBackOfficeRoute($"{Constants.UdiEntityType.MemberType}/tree")] +[OpenApiTag(nameof(Constants.UdiEntityType.MemberType))] +public class MemberTypeTreeControllerBase : EntityTreeControllerBase +{ + public MemberTypeTreeControllerBase(IEntityService entityService) + : base(entityService) + { + } + + protected override UmbracoObjectTypes ItemObjectType => UmbracoObjectTypes.MemberType; + + protected override EntityTreeItemViewModel MapTreeItemViewModel(Guid? parentKey, IEntitySlim entity) + { + EntityTreeItemViewModel viewModel = base.MapTreeItemViewModel(parentKey, entity); + viewModel.Icon = Constants.Icons.User; + return viewModel; + } +} diff --git a/src/Umbraco.Cms.ManagementApi/Controllers/MemberType/Tree/RootMemberTypeTreeController.cs b/src/Umbraco.Cms.ManagementApi/Controllers/MemberType/Tree/RootMemberTypeTreeController.cs new file mode 100644 index 0000000000..92e3dc1f7c --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Controllers/MemberType/Tree/RootMemberTypeTreeController.cs @@ -0,0 +1,21 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.ManagementApi.ViewModels.Pagination; +using Umbraco.Cms.ManagementApi.ViewModels.Tree; + +namespace Umbraco.Cms.ManagementApi.Controllers.MemberType.Tree; + +public class RootMemberTypeTreeController : MemberTypeTreeControllerBase +{ + public RootMemberTypeTreeController(IEntityService entityService) + : base(entityService) + { + } + + [HttpGet("root")] + [MapToApiVersion("1.0")] + [ProducesResponseType(typeof(PagedViewModel), StatusCodes.Status200OK)] + public async Task>> Root(int skip = 0, int take = 100) + => await GetRoot(skip, take); +} diff --git a/src/Umbraco.Cms.ManagementApi/Controllers/PartialView/Tree/ChildrenPartialViewTreeController.cs b/src/Umbraco.Cms.ManagementApi/Controllers/PartialView/Tree/ChildrenPartialViewTreeController.cs new file mode 100644 index 0000000000..91e9354d2c --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Controllers/PartialView/Tree/ChildrenPartialViewTreeController.cs @@ -0,0 +1,21 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Core.IO; +using Umbraco.Cms.ManagementApi.ViewModels.Pagination; +using Umbraco.Cms.ManagementApi.ViewModels.Tree; + +namespace Umbraco.Cms.ManagementApi.Controllers.PartialView.Tree; + +public class ChildrenPartialViewTreeController : PartialViewTreeControllerBase +{ + public ChildrenPartialViewTreeController(FileSystems fileSystems) + : base(fileSystems) + { + } + + [HttpGet("children")] + [MapToApiVersion("1.0")] + [ProducesResponseType(typeof(PagedViewModel), StatusCodes.Status200OK)] + public async Task>> Children(string path, int skip = 0, int take = 100) + => await GetChildren(path, skip, take); +} diff --git a/src/Umbraco.Cms.ManagementApi/Controllers/PartialView/Tree/ItemsPartialViewTreeController.cs b/src/Umbraco.Cms.ManagementApi/Controllers/PartialView/Tree/ItemsPartialViewTreeController.cs new file mode 100644 index 0000000000..d6107a844a --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Controllers/PartialView/Tree/ItemsPartialViewTreeController.cs @@ -0,0 +1,20 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Core.IO; +using Umbraco.Cms.ManagementApi.ViewModels.Tree; + +namespace Umbraco.Cms.ManagementApi.Controllers.PartialView.Tree; + +public class ItemsPartialViewTreeController : PartialViewTreeControllerBase +{ + public ItemsPartialViewTreeController(FileSystems fileSystems) + : base(fileSystems) + { + } + + [HttpGet("items")] + [MapToApiVersion("1.0")] + [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)] + public async Task>> Items([FromQuery(Name = "path")] string[] paths) + => await GetItems(paths); +} diff --git a/src/Umbraco.Cms.ManagementApi/Controllers/PartialView/Tree/PartialViewTreeControllerBase.cs b/src/Umbraco.Cms.ManagementApi/Controllers/PartialView/Tree/PartialViewTreeControllerBase.cs new file mode 100644 index 0000000000..95ad0eb6cf --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Controllers/PartialView/Tree/PartialViewTreeControllerBase.cs @@ -0,0 +1,25 @@ +using Microsoft.AspNetCore.Mvc; +using NSwag.Annotations; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.IO; +using Umbraco.Cms.ManagementApi.Controllers.Tree; +using Umbraco.New.Cms.Web.Common.Routing; + +namespace Umbraco.Cms.ManagementApi.Controllers.PartialView.Tree; + +[ApiVersion("1.0")] +[ApiController] +[VersionedApiBackOfficeRoute($"{Constants.UdiEntityType.PartialView}/tree")] +[OpenApiTag(nameof(Constants.UdiEntityType.PartialView))] +public class PartialViewTreeControllerBase : FileSystemTreeControllerBase +{ + public PartialViewTreeControllerBase(FileSystems fileSystems) + => FileSystem = fileSystems.PartialViewsFileSystem ?? + throw new ArgumentException("Missing partial views file system", nameof(fileSystems)); + + protected override IFileSystem FileSystem { get; } + + protected override string FileIcon(string path) => Constants.Icons.PartialView; + + protected override string ItemType(string path) => Constants.UdiEntityType.PartialView; +} diff --git a/src/Umbraco.Cms.ManagementApi/Controllers/PartialView/Tree/RootPartialViewTreeController.cs b/src/Umbraco.Cms.ManagementApi/Controllers/PartialView/Tree/RootPartialViewTreeController.cs new file mode 100644 index 0000000000..536ff007a1 --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Controllers/PartialView/Tree/RootPartialViewTreeController.cs @@ -0,0 +1,21 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Core.IO; +using Umbraco.Cms.ManagementApi.ViewModels.Pagination; +using Umbraco.Cms.ManagementApi.ViewModels.Tree; + +namespace Umbraco.Cms.ManagementApi.Controllers.PartialView.Tree; + +public class RootPartialViewTreeController : PartialViewTreeControllerBase +{ + public RootPartialViewTreeController(FileSystems fileSystems) + : base(fileSystems) + { + } + + [HttpGet("root")] + [MapToApiVersion("1.0")] + [ProducesResponseType(typeof(PagedViewModel), StatusCodes.Status200OK)] + public async Task>> Root(int skip = 0, int take = 100) + => await GetRoot(skip, take); +} diff --git a/src/Umbraco.Cms.ManagementApi/Controllers/RecycleBin/RecycleBinControllerBase.cs b/src/Umbraco.Cms.ManagementApi/Controllers/RecycleBin/RecycleBinControllerBase.cs new file mode 100644 index 0000000000..041ba9e9af --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Controllers/RecycleBin/RecycleBinControllerBase.cs @@ -0,0 +1,111 @@ +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.Entities; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.ManagementApi.Services.Paging; +using Umbraco.Cms.ManagementApi.ViewModels.Pagination; +using Umbraco.Cms.ManagementApi.ViewModels.RecycleBin; + +namespace Umbraco.Cms.ManagementApi.Controllers.RecycleBin; + +public abstract class RecycleBinControllerBase : Controller + where TItem : RecycleBinItemViewModel, new() +{ + private readonly IEntityService _entityService; + private readonly string _itemUdiType; + + protected RecycleBinControllerBase(IEntityService entityService) + { + _entityService = entityService; + // ReSharper disable once VirtualMemberCallInConstructor + _itemUdiType = ItemObjectType.GetUdiType(); + } + + protected abstract UmbracoObjectTypes ItemObjectType { get; } + + protected abstract int RecycleBinRootId { get; } + + protected async Task>> GetRoot(int skip, int take) + { + if (PaginationService.ConvertSkipTakeToPaging(skip, take, out var pageNumber, out var pageSize, out ProblemDetails? error) == false) + { + return BadRequest(error); + } + + IEntitySlim[] rootEntities = GetPagedRootEntities(pageNumber, pageSize, out var totalItems); + + TItem[] treeItemViewModels = MapRecycleBinViewModels(null, rootEntities); + + PagedViewModel result = PagedViewModel(treeItemViewModels, totalItems); + return await Task.FromResult(Ok(result)); + } + + protected async Task>> GetChildren(Guid parentKey, int skip, int take) + { + if (PaginationService.ConvertSkipTakeToPaging(skip, take, out var pageNumber, out var pageSize, out ProblemDetails? error) == false) + { + return BadRequest(error); + } + + IEntitySlim[] children = GetPagedChildEntities(parentKey, pageNumber, pageSize, out var totalItems); + + TItem[] treeItemViewModels = MapRecycleBinViewModels(parentKey, children); + + PagedViewModel result = PagedViewModel(treeItemViewModels, totalItems); + + return await Task.FromResult(Ok(result)); + } + + protected virtual TItem MapRecycleBinViewModel(Guid? parentKey, IEntitySlim entity) + { + if (entity == null) + { + throw new ArgumentNullException(nameof(entity)); + } + + var viewModel = new TItem + { + Icon = _itemUdiType, + Name = entity.Name!, + Key = entity.Key, + Type = _itemUdiType, + HasChildren = entity.HasChildren, + IsContainer = entity.IsContainer, + ParentKey = parentKey + }; + + return viewModel; + } + + private IEntitySlim[] GetPagedRootEntities(long pageNumber, int pageSize, out long totalItems) + { + IEntitySlim[] rootEntities = _entityService + .GetPagedTrashedChildren(RecycleBinRootId, ItemObjectType, pageNumber, pageSize, out totalItems) + .ToArray(); + + return rootEntities; + } + + private IEntitySlim[] GetPagedChildEntities(Guid parentKey, long pageNumber, int pageSize, out long totalItems) + { + IEntitySlim? parent = _entityService.Get(parentKey, ItemObjectType); + if (parent == null || parent.Trashed == false) + { + // not much else we can do here but return nothing + totalItems = 0; + return Array.Empty(); + } + + IEntitySlim[] children = _entityService + .GetPagedTrashedChildren(parent.Id, ItemObjectType, pageNumber, pageSize, out totalItems) + .ToArray(); + + return children; + } + + private TItem[] MapRecycleBinViewModels(Guid? parentKey, IEntitySlim[] entities) + => entities.Select(entity => MapRecycleBinViewModel(parentKey, entity)).ToArray(); + + private PagedViewModel PagedViewModel(IEnumerable treeItemViewModels, long totalItems) + => new() { Total = totalItems, Items = treeItemViewModels }; +} diff --git a/src/Umbraco.Cms.ManagementApi/Controllers/RelationType/Tree/ItemsRelationTypeTreeController.cs b/src/Umbraco.Cms.ManagementApi/Controllers/RelationType/Tree/ItemsRelationTypeTreeController.cs new file mode 100644 index 0000000000..7e2054a594 --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Controllers/RelationType/Tree/ItemsRelationTypeTreeController.cs @@ -0,0 +1,32 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.ManagementApi.ViewModels.Tree; + +namespace Umbraco.Cms.ManagementApi.Controllers.RelationType.Tree; + +public class ItemsRelationTypeTreeController : RelationTypeTreeControllerBase +{ + private readonly IRelationService _relationService; + + public ItemsRelationTypeTreeController(IEntityService entityService, IRelationService relationService) + : base(entityService) => + _relationService = relationService; + + [HttpGet("items")] + [MapToApiVersion("1.0")] + [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)] + public async Task>> Items([FromQuery(Name = "key")] Guid[] keys) + { + // relation service does not allow fetching a collection of relation types by their ids; instead it relies + // heavily on caching, which means this is as fast as it gets - even if it looks less than performant + IRelationType[] relationTypes = _relationService + .GetAllRelationTypes() + .Where(relationType => keys.Contains(relationType.Key)).ToArray(); + + EntityTreeItemViewModel[] viewModels = MapTreeItemViewModels(null, relationTypes); + + return await Task.FromResult(Ok(viewModels)); + } +} diff --git a/src/Umbraco.Cms.ManagementApi/Controllers/RelationType/Tree/RelationTypeTreeControllerBase.cs b/src/Umbraco.Cms.ManagementApi/Controllers/RelationType/Tree/RelationTypeTreeControllerBase.cs new file mode 100644 index 0000000000..c90c124686 --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Controllers/RelationType/Tree/RelationTypeTreeControllerBase.cs @@ -0,0 +1,38 @@ +using Microsoft.AspNetCore.Mvc; +using NSwag.Annotations; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.ManagementApi.Controllers.Tree; +using Umbraco.Cms.ManagementApi.ViewModels.Tree; +using Umbraco.New.Cms.Web.Common.Routing; + +namespace Umbraco.Cms.ManagementApi.Controllers.RelationType.Tree; + +[ApiVersion("1.0")] +[ApiController] +[VersionedApiBackOfficeRoute($"{Constants.UdiEntityType.RelationType}/tree")] +[OpenApiTag(nameof(Constants.UdiEntityType.RelationType))] +// NOTE: at the moment relation types aren't supported by EntityService, so we have little use of the +// tree controller base. We'll keep it though, in the hope that we can mend EntityService. +public class RelationTypeTreeControllerBase : EntityTreeControllerBase +{ + public RelationTypeTreeControllerBase(IEntityService entityService) + : base(entityService) + { + } + + protected override UmbracoObjectTypes ItemObjectType => UmbracoObjectTypes.RelationType; + + protected EntityTreeItemViewModel[] MapTreeItemViewModels(Guid? parentKey, IRelationType[] relationTypes) + => relationTypes.Select(relationType => new EntityTreeItemViewModel + { + Icon = Constants.Icons.RelationType, + Name = relationType.Name!, + Key = relationType.Key, + Type = Constants.UdiEntityType.RelationType, + HasChildren = false, + IsContainer = false, + ParentKey = parentKey + }).ToArray(); +} diff --git a/src/Umbraco.Cms.ManagementApi/Controllers/RelationType/Tree/RootRelationTypeTreeController.cs b/src/Umbraco.Cms.ManagementApi/Controllers/RelationType/Tree/RootRelationTypeTreeController.cs new file mode 100644 index 0000000000..2c49282147 --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Controllers/RelationType/Tree/RootRelationTypeTreeController.cs @@ -0,0 +1,44 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.ManagementApi.Services.Paging; +using Umbraco.Cms.ManagementApi.ViewModels.Pagination; +using Umbraco.Cms.ManagementApi.ViewModels.Tree; + +namespace Umbraco.Cms.ManagementApi.Controllers.RelationType.Tree; + +public class RootRelationTypeTreeController : RelationTypeTreeControllerBase +{ + private readonly IRelationService _relationService; + + public RootRelationTypeTreeController(IEntityService entityService, IRelationService relationService) + : base(entityService) => + _relationService = relationService; + + [HttpGet("root")] + [MapToApiVersion("1.0")] + [ProducesResponseType(typeof(PagedViewModel), StatusCodes.Status200OK)] + public async Task>> Root(int skip = 0, int take = 100) + { + if (PaginationService.ConvertSkipTakeToPaging(skip, take, out var pageNumber, out var pageSize, out ProblemDetails? error) == false) + { + return BadRequest(error); + } + + // pagination is not supported (yet) by relation service, so we do it in memory for now + // - chances are we won't have many relation types, so it won't be an actual issue + IRelationType[] allRelationTypes = _relationService.GetAllRelationTypes().ToArray(); + + EntityTreeItemViewModel[] viewModels = MapTreeItemViewModels( + null, + allRelationTypes + .OrderBy(relationType => relationType.Name) + .Skip((int)(pageNumber * pageSize)) + .Take(pageSize) + .ToArray()); + + PagedViewModel result = PagedViewModel(viewModels, allRelationTypes.Length); + return await Task.FromResult(Ok(result)); + } +} diff --git a/src/Umbraco.Cms.ManagementApi/Controllers/Script/Tree/ChildrenScriptTreeController.cs b/src/Umbraco.Cms.ManagementApi/Controllers/Script/Tree/ChildrenScriptTreeController.cs new file mode 100644 index 0000000000..d84958ba80 --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Controllers/Script/Tree/ChildrenScriptTreeController.cs @@ -0,0 +1,21 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Core.IO; +using Umbraco.Cms.ManagementApi.ViewModels.Pagination; +using Umbraco.Cms.ManagementApi.ViewModels.Tree; + +namespace Umbraco.Cms.ManagementApi.Controllers.Script.Tree; + +public class ChildrenScriptTreeController : ScriptTreeControllerBase +{ + public ChildrenScriptTreeController(FileSystems fileSystems) + : base(fileSystems) + { + } + + [HttpGet("children")] + [MapToApiVersion("1.0")] + [ProducesResponseType(typeof(PagedViewModel), StatusCodes.Status200OK)] + public async Task>> Children(string path, int skip = 0, int take = 100) + => await GetChildren(path, skip, take); +} diff --git a/src/Umbraco.Cms.ManagementApi/Controllers/Script/Tree/ItemsScriptTreeController.cs b/src/Umbraco.Cms.ManagementApi/Controllers/Script/Tree/ItemsScriptTreeController.cs new file mode 100644 index 0000000000..99cd6d990e --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Controllers/Script/Tree/ItemsScriptTreeController.cs @@ -0,0 +1,20 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Core.IO; +using Umbraco.Cms.ManagementApi.ViewModels.Tree; + +namespace Umbraco.Cms.ManagementApi.Controllers.Script.Tree; + +public class ItemsScriptTreeController : ScriptTreeControllerBase +{ + public ItemsScriptTreeController(FileSystems fileSystems) + : base(fileSystems) + { + } + + [HttpGet("items")] + [MapToApiVersion("1.0")] + [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)] + public async Task>> Items([FromQuery(Name = "path")] string[] paths) + => await GetItems(paths); +} diff --git a/src/Umbraco.Cms.ManagementApi/Controllers/Script/Tree/RootScriptTreeController.cs b/src/Umbraco.Cms.ManagementApi/Controllers/Script/Tree/RootScriptTreeController.cs new file mode 100644 index 0000000000..775a42f248 --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Controllers/Script/Tree/RootScriptTreeController.cs @@ -0,0 +1,21 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Core.IO; +using Umbraco.Cms.ManagementApi.ViewModels.Pagination; +using Umbraco.Cms.ManagementApi.ViewModels.Tree; + +namespace Umbraco.Cms.ManagementApi.Controllers.Script.Tree; + +public class RootScriptTreeController : ScriptTreeControllerBase +{ + public RootScriptTreeController(FileSystems fileSystems) + : base(fileSystems) + { + } + + [HttpGet("root")] + [MapToApiVersion("1.0")] + [ProducesResponseType(typeof(PagedViewModel), StatusCodes.Status200OK)] + public async Task>> Root(int skip = 0, int take = 100) + => await GetRoot(skip, take); +} diff --git a/src/Umbraco.Cms.ManagementApi/Controllers/Script/Tree/ScriptTreeControllerBase.cs b/src/Umbraco.Cms.ManagementApi/Controllers/Script/Tree/ScriptTreeControllerBase.cs new file mode 100644 index 0000000000..4e204da4ee --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Controllers/Script/Tree/ScriptTreeControllerBase.cs @@ -0,0 +1,25 @@ +using Microsoft.AspNetCore.Mvc; +using NSwag.Annotations; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.IO; +using Umbraco.Cms.ManagementApi.Controllers.Tree; +using Umbraco.New.Cms.Web.Common.Routing; + +namespace Umbraco.Cms.ManagementApi.Controllers.Script.Tree; + +[ApiVersion("1.0")] +[ApiController] +[VersionedApiBackOfficeRoute($"{Constants.UdiEntityType.Script}/tree")] +[OpenApiTag(nameof(Constants.UdiEntityType.Script))] +public class ScriptTreeControllerBase : FileSystemTreeControllerBase +{ + public ScriptTreeControllerBase(FileSystems fileSystems) + => FileSystem = fileSystems.ScriptsFileSystem ?? + throw new ArgumentException("Missing scripts file system", nameof(fileSystems)); + + protected override IFileSystem FileSystem { get; } + + protected override string FileIcon(string path) => Constants.Icons.Script; + + protected override string ItemType(string path) => Constants.UdiEntityType.Script; +} diff --git a/src/Umbraco.Cms.ManagementApi/Controllers/Server/ServerControllerBase.cs b/src/Umbraco.Cms.ManagementApi/Controllers/Server/ServerControllerBase.cs index cdb4921ba3..d8755bf0ec 100644 --- a/src/Umbraco.Cms.ManagementApi/Controllers/Server/ServerControllerBase.cs +++ b/src/Umbraco.Cms.ManagementApi/Controllers/Server/ServerControllerBase.cs @@ -5,7 +5,7 @@ using Umbraco.New.Cms.Web.Common.Routing; namespace Umbraco.Cms.ManagementApi.Controllers.Server; [ApiController] -[BackOfficeRoute("api/v{version:apiVersion}/server")] +[VersionedApiBackOfficeRoute("server")] [OpenApiTag("Server")] public abstract class ServerControllerBase : Controller { diff --git a/src/Umbraco.Cms.ManagementApi/Controllers/StaticFile/Tree/ChildrenStaticFileTreeController.cs b/src/Umbraco.Cms.ManagementApi/Controllers/StaticFile/Tree/ChildrenStaticFileTreeController.cs new file mode 100644 index 0000000000..71a659f336 --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Controllers/StaticFile/Tree/ChildrenStaticFileTreeController.cs @@ -0,0 +1,21 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Core.IO; +using Umbraco.Cms.ManagementApi.ViewModels.Pagination; +using Umbraco.Cms.ManagementApi.ViewModels.Tree; + +namespace Umbraco.Cms.ManagementApi.Controllers.StaticFile.Tree; + +public class ChildrenStaticFileTreeController : StaticFileTreeControllerBase +{ + public ChildrenStaticFileTreeController(IPhysicalFileSystem physicalFileSystem) + : base(physicalFileSystem) + { + } + + [HttpGet("children")] + [MapToApiVersion("1.0")] + [ProducesResponseType(typeof(PagedViewModel), StatusCodes.Status200OK)] + public async Task>> Children(string path, int skip = 0, int take = 100) + => await GetChildren(path, skip, take); +} diff --git a/src/Umbraco.Cms.ManagementApi/Controllers/StaticFile/Tree/ItemsStaticFileTreeController.cs b/src/Umbraco.Cms.ManagementApi/Controllers/StaticFile/Tree/ItemsStaticFileTreeController.cs new file mode 100644 index 0000000000..205f92d94f --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Controllers/StaticFile/Tree/ItemsStaticFileTreeController.cs @@ -0,0 +1,20 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Core.IO; +using Umbraco.Cms.ManagementApi.ViewModels.Tree; + +namespace Umbraco.Cms.ManagementApi.Controllers.StaticFile.Tree; + +public class ItemsStaticFileTreeController : StaticFileTreeControllerBase +{ + public ItemsStaticFileTreeController(IPhysicalFileSystem physicalFileSystem) + : base(physicalFileSystem) + { + } + + [HttpGet("items")] + [MapToApiVersion("1.0")] + [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)] + public async Task>> Items([FromQuery(Name = "path")] string[] paths) + => await GetItems(paths); +} diff --git a/src/Umbraco.Cms.ManagementApi/Controllers/StaticFile/Tree/RootStaticFileTreeController.cs b/src/Umbraco.Cms.ManagementApi/Controllers/StaticFile/Tree/RootStaticFileTreeController.cs new file mode 100644 index 0000000000..925f7f1ecf --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Controllers/StaticFile/Tree/RootStaticFileTreeController.cs @@ -0,0 +1,21 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Core.IO; +using Umbraco.Cms.ManagementApi.ViewModels.Pagination; +using Umbraco.Cms.ManagementApi.ViewModels.Tree; + +namespace Umbraco.Cms.ManagementApi.Controllers.StaticFile.Tree; + +public class RootStaticFileTreeController : StaticFileTreeControllerBase +{ + public RootStaticFileTreeController(IPhysicalFileSystem physicalFileSystem) + : base(physicalFileSystem) + { + } + + [HttpGet("root")] + [MapToApiVersion("1.0")] + [ProducesResponseType(typeof(PagedViewModel), StatusCodes.Status200OK)] + public async Task>> Root(int skip = 0, int take = 100) + => await GetRoot(skip, take); +} diff --git a/src/Umbraco.Cms.ManagementApi/Controllers/StaticFile/Tree/StaticFileTreeControllerBase.cs b/src/Umbraco.Cms.ManagementApi/Controllers/StaticFile/Tree/StaticFileTreeControllerBase.cs new file mode 100644 index 0000000000..ec50a54495 --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Controllers/StaticFile/Tree/StaticFileTreeControllerBase.cs @@ -0,0 +1,42 @@ +using Microsoft.AspNetCore.Mvc; +using NSwag.Annotations; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.IO; +using Umbraco.Cms.ManagementApi.Controllers.Tree; +using Umbraco.New.Cms.Web.Common.Routing; + +namespace Umbraco.Cms.ManagementApi.Controllers.StaticFile.Tree; + +[ApiVersion("1.0")] +[ApiController] +[VersionedApiBackOfficeRoute("static-file/tree")] +[OpenApiTag("StaticFile")] +public class StaticFileTreeControllerBase : FileSystemTreeControllerBase +{ + private static readonly string[] _allowedRootFolders = { "App_Plugins", "wwwroot" }; + + public StaticFileTreeControllerBase(IPhysicalFileSystem physicalFileSystem) + => FileSystem = physicalFileSystem; + + protected override IFileSystem FileSystem { get; } + + protected override string FileIcon(string path) => Constants.Icons.DefaultIcon; + + protected override string ItemType(string path) => "static-file"; + + protected override string[] GetDirectories(string path) => + IsTreeRootPath(path) + ? _allowedRootFolders + : IsAllowedPath(path) + ? base.GetDirectories(path) + : Array.Empty(); + + protected override string[] GetFiles(string path) + => IsTreeRootPath(path) || IsAllowedPath(path) == false + ? Array.Empty() + : base.GetFiles(path); + + private bool IsTreeRootPath(string path) => string.IsNullOrWhiteSpace(path); + + private bool IsAllowedPath(string path) => _allowedRootFolders.Contains(path) || _allowedRootFolders.Any(folder => path.StartsWith($"{folder}/")); +} diff --git a/src/Umbraco.Cms.ManagementApi/Controllers/Stylesheet/Tree/ChildrenStylesheetTreeController.cs b/src/Umbraco.Cms.ManagementApi/Controllers/Stylesheet/Tree/ChildrenStylesheetTreeController.cs new file mode 100644 index 0000000000..abd50401d5 --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Controllers/Stylesheet/Tree/ChildrenStylesheetTreeController.cs @@ -0,0 +1,21 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Core.IO; +using Umbraco.Cms.ManagementApi.ViewModels.Pagination; +using Umbraco.Cms.ManagementApi.ViewModels.Tree; + +namespace Umbraco.Cms.ManagementApi.Controllers.Stylesheet.Tree; + +public class ChildrenStylesheetTreeController : StylesheetTreeControllerBase +{ + public ChildrenStylesheetTreeController(FileSystems fileSystems) + : base(fileSystems) + { + } + + [HttpGet("children")] + [MapToApiVersion("1.0")] + [ProducesResponseType(typeof(PagedViewModel), StatusCodes.Status200OK)] + public async Task>> Children(string path, int skip = 0, int take = 100) + => await GetChildren(path, skip, take); +} diff --git a/src/Umbraco.Cms.ManagementApi/Controllers/Stylesheet/Tree/ItemsStylesheetTreeController.cs b/src/Umbraco.Cms.ManagementApi/Controllers/Stylesheet/Tree/ItemsStylesheetTreeController.cs new file mode 100644 index 0000000000..de2e779ba1 --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Controllers/Stylesheet/Tree/ItemsStylesheetTreeController.cs @@ -0,0 +1,20 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Core.IO; +using Umbraco.Cms.ManagementApi.ViewModels.Tree; + +namespace Umbraco.Cms.ManagementApi.Controllers.Stylesheet.Tree; + +public class ItemsStylesheetTreeController : StylesheetTreeControllerBase +{ + public ItemsStylesheetTreeController(FileSystems fileSystems) + : base(fileSystems) + { + } + + [HttpGet("items")] + [MapToApiVersion("1.0")] + [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)] + public async Task>> Items([FromQuery(Name = "path")] string[] paths) + => await GetItems(paths); +} diff --git a/src/Umbraco.Cms.ManagementApi/Controllers/Stylesheet/Tree/RootStylesheetTreeController.cs b/src/Umbraco.Cms.ManagementApi/Controllers/Stylesheet/Tree/RootStylesheetTreeController.cs new file mode 100644 index 0000000000..ca5138befc --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Controllers/Stylesheet/Tree/RootStylesheetTreeController.cs @@ -0,0 +1,21 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Core.IO; +using Umbraco.Cms.ManagementApi.ViewModels.Pagination; +using Umbraco.Cms.ManagementApi.ViewModels.Tree; + +namespace Umbraco.Cms.ManagementApi.Controllers.Stylesheet.Tree; + +public class RootStylesheetTreeController : StylesheetTreeControllerBase +{ + public RootStylesheetTreeController(FileSystems fileSystems) + : base(fileSystems) + { + } + + [HttpGet("root")] + [MapToApiVersion("1.0")] + [ProducesResponseType(typeof(PagedViewModel), StatusCodes.Status200OK)] + public async Task>> Root(int skip = 0, int take = 100) + => await GetRoot(skip, take); +} diff --git a/src/Umbraco.Cms.ManagementApi/Controllers/Stylesheet/Tree/StylesheetTreeControllerBase.cs b/src/Umbraco.Cms.ManagementApi/Controllers/Stylesheet/Tree/StylesheetTreeControllerBase.cs new file mode 100644 index 0000000000..b529752293 --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Controllers/Stylesheet/Tree/StylesheetTreeControllerBase.cs @@ -0,0 +1,25 @@ +using Microsoft.AspNetCore.Mvc; +using NSwag.Annotations; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.IO; +using Umbraco.Cms.ManagementApi.Controllers.Tree; +using Umbraco.New.Cms.Web.Common.Routing; + +namespace Umbraco.Cms.ManagementApi.Controllers.Stylesheet.Tree; + +[ApiVersion("1.0")] +[ApiController] +[VersionedApiBackOfficeRoute($"{Constants.UdiEntityType.Stylesheet}/tree")] +[OpenApiTag(nameof(Constants.UdiEntityType.Stylesheet))] +public class StylesheetTreeControllerBase : FileSystemTreeControllerBase +{ + public StylesheetTreeControllerBase(FileSystems fileSystems) + => FileSystem = fileSystems.StylesheetsFileSystem ?? + throw new ArgumentException("Missing stylesheets file system", nameof(fileSystems)); + + protected override IFileSystem FileSystem { get; } + + protected override string FileIcon(string path) => Constants.Icons.Stylesheet; + + protected override string ItemType(string path) => Constants.UdiEntityType.Stylesheet; +} diff --git a/src/Umbraco.Cms.ManagementApi/Controllers/Template/Tree/ChildrenTemplateTreeController.cs b/src/Umbraco.Cms.ManagementApi/Controllers/Template/Tree/ChildrenTemplateTreeController.cs new file mode 100644 index 0000000000..6ae058a7b0 --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Controllers/Template/Tree/ChildrenTemplateTreeController.cs @@ -0,0 +1,21 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.ManagementApi.ViewModels.Pagination; +using Umbraco.Cms.ManagementApi.ViewModels.Tree; + +namespace Umbraco.Cms.ManagementApi.Controllers.Template.Tree; + +public class ChildrenTemplateTreeController : TemplateTreeControllerBase +{ + public ChildrenTemplateTreeController(IEntityService entityService) + : base(entityService) + { + } + + [HttpGet("children")] + [MapToApiVersion("1.0")] + [ProducesResponseType(typeof(PagedViewModel), StatusCodes.Status200OK)] + public async Task>> Children(Guid parentKey, int skip = 0, int take = 100) + => await GetChildren(parentKey, skip, take); +} diff --git a/src/Umbraco.Cms.ManagementApi/Controllers/Template/Tree/ItemsTemplateTreeController.cs b/src/Umbraco.Cms.ManagementApi/Controllers/Template/Tree/ItemsTemplateTreeController.cs new file mode 100644 index 0000000000..fb4a29c621 --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Controllers/Template/Tree/ItemsTemplateTreeController.cs @@ -0,0 +1,20 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.ManagementApi.ViewModels.Tree; + +namespace Umbraco.Cms.ManagementApi.Controllers.Template.Tree; + +public class ItemsTemplateTreeController : TemplateTreeControllerBase +{ + public ItemsTemplateTreeController(IEntityService entityService) + : base(entityService) + { + } + + [HttpGet("items")] + [MapToApiVersion("1.0")] + [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)] + public async Task>> Items([FromQuery(Name = "key")] Guid[] keys) + => await GetItems(keys); +} diff --git a/src/Umbraco.Cms.ManagementApi/Controllers/Template/Tree/RootTemplateTreeController.cs b/src/Umbraco.Cms.ManagementApi/Controllers/Template/Tree/RootTemplateTreeController.cs new file mode 100644 index 0000000000..af03a8b316 --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Controllers/Template/Tree/RootTemplateTreeController.cs @@ -0,0 +1,21 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.ManagementApi.ViewModels.Pagination; +using Umbraco.Cms.ManagementApi.ViewModels.Tree; + +namespace Umbraco.Cms.ManagementApi.Controllers.Template.Tree; + +public class RootTemplateTreeController : TemplateTreeControllerBase +{ + public RootTemplateTreeController(IEntityService entityService) + : base(entityService) + { + } + + [HttpGet("root")] + [MapToApiVersion("1.0")] + [ProducesResponseType(typeof(PagedViewModel), StatusCodes.Status200OK)] + public async Task>> Root(int skip = 0, int take = 100) + => await GetRoot(skip, take); +} diff --git a/src/Umbraco.Cms.ManagementApi/Controllers/Template/Tree/TemplateTreeControllerBase.cs b/src/Umbraco.Cms.ManagementApi/Controllers/Template/Tree/TemplateTreeControllerBase.cs new file mode 100644 index 0000000000..be885f26be --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Controllers/Template/Tree/TemplateTreeControllerBase.cs @@ -0,0 +1,32 @@ +using Microsoft.AspNetCore.Mvc; +using NSwag.Annotations; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.Entities; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.ManagementApi.Controllers.Tree; +using Umbraco.Cms.ManagementApi.ViewModels.Tree; +using Umbraco.New.Cms.Web.Common.Routing; + +namespace Umbraco.Cms.ManagementApi.Controllers.Template.Tree; + +[ApiVersion("1.0")] +[ApiController] +[VersionedApiBackOfficeRoute($"{Constants.UdiEntityType.Template}/tree")] +[OpenApiTag(nameof(Constants.UdiEntityType.Template))] +public class TemplateTreeControllerBase : EntityTreeControllerBase +{ + public TemplateTreeControllerBase(IEntityService entityService) + : base(entityService) + { + } + + protected override UmbracoObjectTypes ItemObjectType => UmbracoObjectTypes.Template; + + protected override EntityTreeItemViewModel MapTreeItemViewModel(Guid? parentKey, IEntitySlim entity) + { + EntityTreeItemViewModel viewModel = base.MapTreeItemViewModel(parentKey, entity); + viewModel.Icon = Constants.Icons.Template; + return viewModel; + } +} diff --git a/src/Umbraco.Cms.ManagementApi/Controllers/Tree/EntityTreeControllerBase.cs b/src/Umbraco.Cms.ManagementApi/Controllers/Tree/EntityTreeControllerBase.cs new file mode 100644 index 0000000000..3cd05e5eb1 --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Controllers/Tree/EntityTreeControllerBase.cs @@ -0,0 +1,132 @@ +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.Entities; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.ManagementApi.Services.Paging; +using Umbraco.Cms.ManagementApi.ViewModels.Pagination; +using Umbraco.Cms.ManagementApi.ViewModels.Tree; +using Umbraco.Extensions; + +namespace Umbraco.Cms.ManagementApi.Controllers.Tree; + +public abstract class EntityTreeControllerBase : ManagementApiControllerBase + where TItem : EntityTreeItemViewModel, new() +{ + private readonly string _itemUdiType; + + protected EntityTreeControllerBase(IEntityService entityService) + { + EntityService = entityService; + + // ReSharper disable once VirtualMemberCallInConstructor + _itemUdiType = ItemObjectType.GetUdiType(); + } + + protected IEntityService EntityService { get; } + + protected abstract UmbracoObjectTypes ItemObjectType { get; } + + protected virtual Ordering ItemOrdering => Ordering.By(nameof(Infrastructure.Persistence.Dtos.NodeDto.Text)); + + protected async Task>> GetRoot(int skip, int take) + { + if (PaginationService.ConvertSkipTakeToPaging(skip, take, out var pageNumber, out var pageSize, out ProblemDetails? error) == false) + { + return BadRequest(error); + } + + IEntitySlim[] rootEntities = GetPagedRootEntities(pageNumber, pageSize, out var totalItems); + + TItem[] treeItemViewModels = MapTreeItemViewModels(null, rootEntities); + + PagedViewModel result = PagedViewModel(treeItemViewModels, totalItems); + return await Task.FromResult(Ok(result)); + } + + protected async Task>> GetChildren(Guid parentKey, int skip, int take) + { + if (PaginationService.ConvertSkipTakeToPaging(skip, take, out var pageNumber, out var pageSize, out ProblemDetails? error) == false) + { + return BadRequest(error); + } + + IEntitySlim[] children = GetPagedChildEntities(parentKey, pageNumber, pageSize, out var totalItems); + + TItem[] treeItemViewModels = MapTreeItemViewModels(parentKey, children); + + PagedViewModel result = PagedViewModel(treeItemViewModels, totalItems); + return await Task.FromResult(Ok(result)); + } + + protected async Task>> GetItems(Guid[] keys) + { + if (keys.IsCollectionEmpty()) + { + return await Task.FromResult(Ok(PagedViewModel(Array.Empty(), 0))); + } + + IEntitySlim[] itemEntities = GetEntities(keys); + + TItem[] treeItemViewModels = MapTreeItemViewModels(null, itemEntities); + + return await Task.FromResult(Ok(treeItemViewModels)); + } + + protected virtual IEntitySlim[] GetPagedRootEntities(long pageNumber, int pageSize, out long totalItems) + => EntityService + .GetPagedChildren( + Constants.System.Root, + ItemObjectType, + pageNumber, + pageSize, + out totalItems, + ordering: ItemOrdering) + .ToArray(); + + protected virtual IEntitySlim[] GetPagedChildEntities(Guid parentKey, long pageNumber, int pageSize, out long totalItems) + { + // EntityService is only able to get paged children by parent ID, so we must first map parent key to parent ID + Attempt parentId = EntityService.GetId(parentKey, ItemObjectType); + if (parentId.Success == false) + { + // not much else we can do here but return nothing + totalItems = 0; + return Array.Empty(); + } + + IEntitySlim[] children = EntityService.GetPagedChildren( + parentId.Result, + ItemObjectType, + pageNumber, + pageSize, + out totalItems, + ordering: ItemOrdering) + .ToArray(); + return children; + } + + protected virtual IEntitySlim[] GetEntities(Guid[] keys) => EntityService.GetAll(ItemObjectType, keys).ToArray(); + + protected virtual TItem[] MapTreeItemViewModels(Guid? parentKey, IEntitySlim[] entities) + => entities.Select(entity => MapTreeItemViewModel(parentKey, entity)).ToArray(); + + protected virtual TItem MapTreeItemViewModel(Guid? parentKey, IEntitySlim entity) + { + var viewModel = new TItem + { + Icon = _itemUdiType, + Name = entity.Name!, + Key = entity.Key, + Type = _itemUdiType, + HasChildren = entity.HasChildren, + IsContainer = entity.IsContainer, + ParentKey = parentKey + }; + + return viewModel; + } + + protected PagedViewModel PagedViewModel(IEnumerable treeItemViewModels, long totalItems) + => new() { Total = totalItems, Items = treeItemViewModels }; +} diff --git a/src/Umbraco.Cms.ManagementApi/Controllers/Tree/FileSystemTreeControllerBase.cs b/src/Umbraco.Cms.ManagementApi/Controllers/Tree/FileSystemTreeControllerBase.cs new file mode 100644 index 0000000000..76113312a7 --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Controllers/Tree/FileSystemTreeControllerBase.cs @@ -0,0 +1,110 @@ +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.IO; +using Umbraco.Cms.ManagementApi.Services.Paging; +using Umbraco.Cms.ManagementApi.ViewModels.Pagination; +using Umbraco.Cms.ManagementApi.ViewModels.Tree; +using Umbraco.Extensions; + +namespace Umbraco.Cms.ManagementApi.Controllers.Tree; + +public abstract class FileSystemTreeControllerBase : ManagementApiControllerBase +{ + protected abstract IFileSystem FileSystem { get; } + + protected abstract string FileIcon(string path); + + protected abstract string ItemType(string path); + + protected async Task>> GetRoot(int skip, int take) + { + if (PaginationService.ConvertSkipTakeToPaging(skip, take, out var pageNumber, out var pageSize, out ProblemDetails? error) == false) + { + return BadRequest(error); + } + + FileSystemTreeItemViewModel[] viewModels = GetPathViewModels(string.Empty, pageNumber, pageSize, out var totalItems); + + PagedViewModel result = PagedViewModel(viewModels, totalItems); + return await Task.FromResult(Ok(result)); + } + + protected async Task>> GetChildren(string path, int skip, int take) + { + if (PaginationService.ConvertSkipTakeToPaging(skip, take, out var pageNumber, out var pageSize, out ProblemDetails? error) == false) + { + return BadRequest(error); + } + + FileSystemTreeItemViewModel[] viewModels = GetPathViewModels(path, pageNumber, pageSize, out var totalItems); + + PagedViewModel result = PagedViewModel(viewModels, totalItems); + return await Task.FromResult(Ok(result)); + } + + protected async Task>> GetItems(string[] paths) + { + FileSystemTreeItemViewModel[] viewModels = paths + .Where(FileSystem.FileExists) + .Select(path => + { + var fileName = GetFileName(path); + return fileName.IsNullOrWhiteSpace() + ? null + : MapViewModel(path, fileName, false); + }).WhereNotNull().ToArray(); + + return await Task.FromResult(Ok(viewModels)); + } + + protected virtual string[] GetDirectories(string path) => FileSystem + .GetDirectories(path) + .OrderBy(directory => directory) + .ToArray(); + + protected virtual string[] GetFiles(string path) => FileSystem + .GetFiles(path) + .OrderBy(file => file) + .ToArray(); + + protected virtual string GetFileName(string path) => FileSystem.GetFileName(path); + + protected virtual bool DirectoryHasChildren(string path) + => FileSystem.GetFiles(path).Any() || FileSystem.GetDirectories(path).Any(); + + private FileSystemTreeItemViewModel[] GetPathViewModels(string path, long pageNumber, int pageSize, out long totalItems) + { + var allItems = GetDirectories(path) + .Select(directory => new { Path = directory, IsFolder = true }) + .Union(GetFiles(path).Select(file => new { Path = file, IsFolder = false })) + .ToArray(); + + totalItems = allItems.Length; + + FileSystemTreeItemViewModel ViewModel(string itemPath, bool isFolder) + => MapViewModel( + itemPath, + isFolder ? Path.GetFileName(itemPath) : FileSystem.GetFileName(itemPath), + isFolder); + + return allItems + .Skip((int)(pageNumber * pageSize)) + .Take(pageSize) + .Select(item => ViewModel(item.Path, item.IsFolder)) + .ToArray(); + } + + private PagedViewModel PagedViewModel(IEnumerable viewModels, long totalItems) + => new() { Total = totalItems, Items = viewModels }; + + private FileSystemTreeItemViewModel MapViewModel(string path, string name, bool isFolder) + => new() + { + Path = path, + Name = name, + Icon = isFolder ? Constants.Icons.Folder : FileIcon(path), + HasChildren = isFolder && DirectoryHasChildren(path), + Type = ItemType(path), + IsFolder = isFolder + }; +} diff --git a/src/Umbraco.Cms.ManagementApi/Controllers/Tree/FolderTreeControllerBase.cs b/src/Umbraco.Cms.ManagementApi/Controllers/Tree/FolderTreeControllerBase.cs new file mode 100644 index 0000000000..778dd1dd8e --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Controllers/Tree/FolderTreeControllerBase.cs @@ -0,0 +1,88 @@ +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.Entities; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.ManagementApi.ViewModels.Tree; + +namespace Umbraco.Cms.ManagementApi.Controllers.Tree; + +public abstract class FolderTreeControllerBase : EntityTreeControllerBase + where TItem : FolderTreeItemViewModel, new() +{ + private readonly Guid _folderObjectTypeId; + private bool _foldersOnly; + + protected FolderTreeControllerBase(IEntityService entityService) + : base(entityService) => + // ReSharper disable once VirtualMemberCallInConstructor + _folderObjectTypeId = FolderObjectType.GetGuid(); + + protected abstract UmbracoObjectTypes FolderObjectType { get; } + + protected void RenderFoldersOnly(bool foldersOnly) => _foldersOnly = foldersOnly; + + protected override IEntitySlim[] GetPagedRootEntities(long pageNumber, int pageSize, out long totalItems) + => GetEntities( + Constants.System.Root, + pageNumber, + pageSize, + out totalItems); + + protected override IEntitySlim[] GetPagedChildEntities(Guid parentKey, long pageNumber, int pageSize, out long totalItems) + { + // EntityService is only able to get paged children by parent ID, so we must first map parent key to parent ID + Attempt parentId = EntityService.GetId(parentKey, FolderObjectType); + if (parentId.Success == false) + { + parentId = EntityService.GetId(parentKey, ItemObjectType); + if (parentId.Success == false) + { + // not much else we can do here but return nothing + totalItems = 0; + return Array.Empty(); + } + } + + return GetEntities( + parentId.Result, + pageNumber, + pageSize, + out totalItems); + } + + protected override TItem MapTreeItemViewModel(Guid? parentKey, IEntitySlim entity) + { + TItem viewModel = base.MapTreeItemViewModel(parentKey, entity); + + if (entity.NodeObjectType == _folderObjectTypeId) + { + viewModel.IsFolder = true; + viewModel.Icon = Constants.Icons.Folder; + } + + return viewModel; + } + + private IEntitySlim[] GetEntities(int parentId, long pageNumber, int pageSize, out long totalItems) + { + totalItems = 0; + + // 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 + IEntitySlim[] folderEntities = pageNumber == 0 + ? EntityService.GetChildren(parentId, FolderObjectType).OrderBy(c => c.Name).ToArray() + : Array.Empty(); + IEntitySlim[] itemEntities = _foldersOnly + ? Array.Empty() + : EntityService.GetPagedChildren( + parentId, + ItemObjectType, + pageNumber, + pageSize, + out totalItems, + ordering: ItemOrdering) + .ToArray(); + + return folderEntities.Union(itemEntities).ToArray(); + } +} diff --git a/src/Umbraco.Cms.ManagementApi/Controllers/Tree/UserStartNodeTreeControllerBase.cs b/src/Umbraco.Cms.ManagementApi/Controllers/Tree/UserStartNodeTreeControllerBase.cs new file mode 100644 index 0000000000..63cf90dc5c --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Controllers/Tree/UserStartNodeTreeControllerBase.cs @@ -0,0 +1,117 @@ +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Models.Entities; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.ManagementApi.Models.Entities; +using Umbraco.Cms.ManagementApi.Services.Entities; +using Umbraco.Cms.ManagementApi.ViewModels.Tree; +using Umbraco.Extensions; + +namespace Umbraco.Cms.ManagementApi.Controllers.Tree; + +public abstract class UserStartNodeTreeControllerBase : EntityTreeControllerBase + where TItem : ContentTreeItemViewModel, new() +{ + private readonly IUserStartNodeEntitiesService _userStartNodeEntitiesService; + private readonly IDataTypeService _dataTypeService; + + private int[]? _userStartNodeIds; + private string[]? _userStartNodePaths; + private Dictionary _accessMap = new(); + private Guid? _dataTypeKey; + + protected UserStartNodeTreeControllerBase( + IEntityService entityService, + IUserStartNodeEntitiesService userStartNodeEntitiesService, + IDataTypeService dataTypeService) + : base(entityService) + { + _userStartNodeEntitiesService = userStartNodeEntitiesService; + _dataTypeService = dataTypeService; + } + + protected abstract int[] GetUserStartNodeIds(); + + protected abstract string[] GetUserStartNodePaths(); + + protected void IgnoreUserStartNodesForDataType(Guid? dataTypeKey) => _dataTypeKey = dataTypeKey; + + protected override IEntitySlim[] GetPagedRootEntities(long pageNumber, int pageSize, out long totalItems) + => UserHasRootAccess() || IgnoreUserStartNodes() + ? base.GetPagedRootEntities(pageNumber, pageSize, out totalItems) + : CalculateAccessMap(() => _userStartNodeEntitiesService.RootUserAccessEntities(ItemObjectType, UserStartNodeIds), out totalItems); + + protected override IEntitySlim[] GetPagedChildEntities(Guid parentKey, long pageNumber, int pageSize, out long totalItems) + { + IEntitySlim[] children = base.GetPagedChildEntities(parentKey, pageNumber, pageSize, out totalItems); + return UserHasRootAccess() || IgnoreUserStartNodes() + ? children + : 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()) + { + return base.MapTreeItemViewModels(parentKey, entities); + } + + // for users with no root access, only add items for the entities contained within the calculated access map. + // the access map may contain entities that the user does not have direct access to, but need still to see, + // because it has descendants that the user *does* have access to. these entities are added as "no access" items. + TItem[] contentTreeItemViewModels = entities.Select(entity => + { + if (_accessMap.TryGetValue(entity.Key, out var hasAccess) == false) + { + // entity is not a part of the calculated access map + return null; + } + + // direct access => return a regular item + // no direct access => return a "no access" item + return hasAccess + ? MapTreeItemViewModel(parentKey, entity) + : MapTreeItemViewModelAsNoAccess(parentKey, entity); + }) + .WhereNotNull() + .ToArray(); + + return contentTreeItemViewModels; + } + + private int[] UserStartNodeIds => _userStartNodeIds ??= GetUserStartNodeIds(); + + private string[] UserStartNodePaths => _userStartNodePaths ??= GetUserStartNodePaths(); + + private bool UserHasRootAccess() => UserStartNodeIds.Contains(Constants.System.Root); + + private bool IgnoreUserStartNodes() + => _dataTypeKey.HasValue + && _dataTypeService.IsDataTypeIgnoringUserStartNodes(_dataTypeKey.Value); + + private IEntitySlim[] CalculateAccessMap(Func> getUserAccessEntities, out long totalItems) + { + UserAccessEntity[] userAccessEntities = getUserAccessEntities().ToArray(); + + _accessMap = userAccessEntities.ToDictionary(uae => uae.Entity.Key, uae => uae.HasAccess); + + IEntitySlim[] entities = userAccessEntities.Select(uae => uae.Entity).ToArray(); + totalItems = entities.Length; + + return entities; + } + + private TItem MapTreeItemViewModelAsNoAccess(Guid? parentKey, IEntitySlim entity) + { + TItem viewModel = MapTreeItemViewModel(parentKey, entity); + viewModel.NoAccess = true; + return viewModel; + } +} diff --git a/src/Umbraco.Cms.ManagementApi/Controllers/Upgrade/UpgradeControllerBase.cs b/src/Umbraco.Cms.ManagementApi/Controllers/Upgrade/UpgradeControllerBase.cs index 2b489501ec..1d10aa6dda 100644 --- a/src/Umbraco.Cms.ManagementApi/Controllers/Upgrade/UpgradeControllerBase.cs +++ b/src/Umbraco.Cms.ManagementApi/Controllers/Upgrade/UpgradeControllerBase.cs @@ -10,7 +10,7 @@ namespace Umbraco.Cms.ManagementApi.Controllers.Upgrade; [ApiController] [RequireRuntimeLevel(RuntimeLevel.Upgrade)] -[BackOfficeRoute("api/v{version:apiVersion}/upgrade")] +[VersionedApiBackOfficeRoute("upgrade")] [OpenApiTag("Upgrade")] public abstract class UpgradeControllerBase : Controller { diff --git a/src/Umbraco.Cms.ManagementApi/DependencyInjection/InstallerBuilderExtensions.cs b/src/Umbraco.Cms.ManagementApi/DependencyInjection/InstallerBuilderExtensions.cs index 385cd1ff51..b1a1b9731e 100644 --- a/src/Umbraco.Cms.ManagementApi/DependencyInjection/InstallerBuilderExtensions.cs +++ b/src/Umbraco.Cms.ManagementApi/DependencyInjection/InstallerBuilderExtensions.cs @@ -2,6 +2,8 @@ using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.Mapping; using Umbraco.Cms.ManagementApi.Mapping.Installer; +using Umbraco.Cms.ManagementApi.Services.Entities; +using Umbraco.Cms.ManagementApi.Services.Paging; using Umbraco.New.Cms.Core.Factories; using Umbraco.New.Cms.Core.Installer; using Umbraco.New.Cms.Core.Installer.Steps; @@ -76,4 +78,10 @@ public static class InstallerBuilderExtensions public static UpgradeStepCollectionBuilder UpgradeSteps(this IUmbracoBuilder builder) => builder.WithCollectionBuilder(); + + internal static IUmbracoBuilder AddTrees(this IUmbracoBuilder builder) + { + builder.Services.AddTransient(); + return builder; + } } diff --git a/src/Umbraco.Cms.ManagementApi/Filters/RequireDocumentTreeRootAccessAttribute.cs b/src/Umbraco.Cms.ManagementApi/Filters/RequireDocumentTreeRootAccessAttribute.cs new file mode 100644 index 0000000000..8426884716 --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Filters/RequireDocumentTreeRootAccessAttribute.cs @@ -0,0 +1,19 @@ +using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.Extensions.DependencyInjection; +using Umbraco.Cms.Core.Cache; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.Membership; +using Umbraco.Cms.Core.Services; + +namespace Umbraco.Cms.ManagementApi.Filters; + +public class RequireDocumentTreeRootAccessAttribute : RequireTreeRootAccessAttribute +{ + protected override int[] GetUserStartNodeIds(IUser user, ActionExecutingContext context) + { + AppCaches appCaches = context.HttpContext.RequestServices.GetRequiredService(); + IEntityService entityService = context.HttpContext.RequestServices.GetRequiredService(); + + return user.CalculateContentStartNodeIds(entityService, appCaches) ?? Array.Empty(); + } +} diff --git a/src/Umbraco.Cms.ManagementApi/Filters/RequireMediaTreeRootAccessAttribute.cs b/src/Umbraco.Cms.ManagementApi/Filters/RequireMediaTreeRootAccessAttribute.cs new file mode 100644 index 0000000000..42dd62c5f2 --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Filters/RequireMediaTreeRootAccessAttribute.cs @@ -0,0 +1,19 @@ +using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.Extensions.DependencyInjection; +using Umbraco.Cms.Core.Cache; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.Membership; +using Umbraco.Cms.Core.Services; + +namespace Umbraco.Cms.ManagementApi.Filters; + +public class RequireMediaTreeRootAccessAttribute : RequireTreeRootAccessAttribute +{ + protected override int[] GetUserStartNodeIds(IUser user, ActionExecutingContext context) + { + AppCaches appCaches = context.HttpContext.RequestServices.GetRequiredService(); + IEntityService entityService = context.HttpContext.RequestServices.GetRequiredService(); + + return user.CalculateMediaStartNodeIds(entityService, appCaches) ?? Array.Empty(); + } +} diff --git a/src/Umbraco.Cms.ManagementApi/Filters/RequireTreeRootAccessAttribute.cs b/src/Umbraco.Cms.ManagementApi/Filters/RequireTreeRootAccessAttribute.cs new file mode 100644 index 0000000000..60e98ed565 --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Filters/RequireTreeRootAccessAttribute.cs @@ -0,0 +1,40 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Filters; +using Microsoft.Extensions.DependencyInjection; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Models.Membership; +using Umbraco.Cms.Core.Security; + +namespace Umbraco.Cms.ManagementApi.Filters; + +public abstract class RequireTreeRootAccessAttribute : ActionFilterAttribute +{ + public override void OnActionExecuting(ActionExecutingContext context) + { + IBackOfficeSecurityAccessor backOfficeSecurityAccessor = context.HttpContext.RequestServices.GetRequiredService(); + IUser? user = backOfficeSecurityAccessor.BackOfficeSecurity?.CurrentUser; + + var startNodeIds = user != null ? GetUserStartNodeIds(user, context) : Array.Empty(); + + // TODO: remove this once we have backoffice auth in place + startNodeIds = new[] { Constants.System.Root }; + + if (startNodeIds.Contains(Constants.System.Root)) + { + return; + } + + var problemDetails = new ProblemDetails + { + Title = "Unauthorized user", + Detail = "The current backoffice user should have access to the tree root", + Status = StatusCodes.Status401Unauthorized, + Type = "Error", + }; + + context.Result = new ObjectResult(problemDetails) { StatusCode = StatusCodes.Status401Unauthorized }; + } + + protected abstract int[] GetUserStartNodeIds(IUser user, ActionExecutingContext context); +} diff --git a/src/Umbraco.Cms.ManagementApi/ManagementApiComposer.cs b/src/Umbraco.Cms.ManagementApi/ManagementApiComposer.cs index de013756aa..e336a18363 100644 --- a/src/Umbraco.Cms.ManagementApi/ManagementApiComposer.cs +++ b/src/Umbraco.Cms.ManagementApi/ManagementApiComposer.cs @@ -36,7 +36,8 @@ public class ManagementApiComposer : IComposer builder .AddNewInstaller() .AddUpgrader() - .AddExamineManagement(); + .AddExamineManagement() + .AddTrees(); services.AddApiVersioning(options => { diff --git a/src/Umbraco.Cms.ManagementApi/Models/Entities/UserAccessEntity.cs b/src/Umbraco.Cms.ManagementApi/Models/Entities/UserAccessEntity.cs new file mode 100644 index 0000000000..7f7981e693 --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Models/Entities/UserAccessEntity.cs @@ -0,0 +1,16 @@ +using Umbraco.Cms.Core.Models.Entities; + +namespace Umbraco.Cms.ManagementApi.Models.Entities; + +public class UserAccessEntity +{ + public UserAccessEntity(IEntitySlim entity, bool hasAccess) + { + Entity = entity; + HasAccess = hasAccess; + } + + public IEntitySlim Entity { get; } + + public bool HasAccess { get; } +} diff --git a/src/Umbraco.Cms.ManagementApi/OpenApi.json b/src/Umbraco.Cms.ManagementApi/OpenApi.json index 99f6333fc5..f56114f5ef 100644 --- a/src/Umbraco.Cms.ManagementApi/OpenApi.json +++ b/src/Umbraco.Cms.ManagementApi/OpenApi.json @@ -11,235 +11,1550 @@ } ], "paths": { - "/umbraco/api/v1/examineManagement/index": { - "get": { - "operationId": "IndexExamineManagement_Index", - "parameters": [ - { - "in": "query", - "name": "indexName", - "schema": { - "nullable": true, - "type": "string" - }, - "x-position": 1 - } + "/umbraco/api/v1/upgrade/authorize": { + "post": { + "tags": [ + "Upgrade" ], + "operationId": "AuthorizeUpgrade_Authorize", "responses": { "200": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ExamineIndexViewModel" - } - } - }, "description": "" }, - "400": { + "428": { + "description": "", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/ProblemDetails" } } - }, - "description": "" + } + }, + "500": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } } - }, - "tags": [ - "ExamineManagement" - ] + } } }, - "/umbraco/api/v1/examineManagement/indexes": { + "/umbraco/api/v1/upgrade/settings": { "get": { - "operationId": "IndexesExamineManagement_Indexes", + "tags": [ + "Upgrade" + ], + "operationId": "SettingsUpgrade_Settings", + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UpgradeSettingsViewModel" + } + } + } + }, + "428": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + } + } + } + }, + "/umbraco/api/v1/template/tree/children": { + "get": { + "tags": [ + "Template" + ], + "operationId": "ChildrenTemplateTree_Children", "parameters": [ { + "name": "parentKey", "in": "query", - "name": "skip", "schema": { - "format": "int32", - "type": "integer" + "type": "string", + "format": "guid" }, "x-position": 1 }, { + "name": "skip", "in": "query", - "name": "take", "schema": { + "type": "integer", "format": "int32", - "type": "integer" + "default": 0 + }, + "x-position": 2 + }, + { + "name": "take", + "in": "query", + "schema": { + "type": "integer", + "format": "int32", + "default": 100 + }, + "x-position": 3 + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PagedViewModelOfEntityTreeItemViewModel" + } + } + } + } + } + } + }, + "/umbraco/api/v1/template/tree/items": { + "get": { + "tags": [ + "Template" + ], + "operationId": "ItemsTemplateTree_Items", + "parameters": [ + { + "name": "key", + "x-originalName": "keys", + "in": "query", + "style": "form", + "explode": true, + "schema": { + "type": "array", + "nullable": true, + "items": { + "type": "string", + "format": "guid" + } + }, + "x-position": 1 + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/EntityTreeItemViewModel" + } + } + } + } + } + } + } + }, + "/umbraco/api/v1/template/tree/root": { + "get": { + "tags": [ + "Template" + ], + "operationId": "RootTemplateTree_Root", + "parameters": [ + { + "name": "skip", + "in": "query", + "schema": { + "type": "integer", + "format": "int32", + "default": 0 + }, + "x-position": 1 + }, + { + "name": "take", + "in": "query", + "schema": { + "type": "integer", + "format": "int32", + "default": 100 }, "x-position": 2 } ], "responses": { "200": { + "description": "", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/PagedViewModelOfExamineIndexViewModel" + "$ref": "#/components/schemas/PagedViewModelOfEntityTreeItemViewModel" } } - }, - "description": "" + } } - }, - "tags": [ - "ExamineManagement" - ] + } } }, - "/umbraco/api/v1/examineManagement/rebuild": { - "post": { - "operationId": "RebuildExamineManagement_Rebuild", + "/umbraco/api/v1/stylesheet/tree/children": { + "get": { + "tags": [ + "Stylesheet" + ], + "operationId": "ChildrenStylesheetTree_Children", "parameters": [ { + "name": "path", "in": "query", - "name": "indexName", "schema": { + "type": "string", + "nullable": true + }, + "x-position": 1 + }, + { + "name": "skip", + "in": "query", + "schema": { + "type": "integer", + "format": "int32", + "default": 0 + }, + "x-position": 2 + }, + { + "name": "take", + "in": "query", + "schema": { + "type": "integer", + "format": "int32", + "default": 100 + }, + "x-position": 3 + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PagedViewModelOfFileSystemTreeItemViewModel" + } + } + } + } + } + } + }, + "/umbraco/api/v1/stylesheet/tree/items": { + "get": { + "tags": [ + "Stylesheet" + ], + "operationId": "ItemsStylesheetTree_Items", + "parameters": [ + { + "name": "path", + "x-originalName": "paths", + "in": "query", + "style": "form", + "explode": true, + "schema": { + "type": "array", "nullable": true, - "type": "string" + "items": { + "type": "string" + } }, "x-position": 1 } ], "responses": { "200": { + "description": "", "content": { - "application/octet-stream": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/FileSystemTreeItemViewModel" + } + } + } + } + } + } + } + }, + "/umbraco/api/v1/stylesheet/tree/root": { + "get": { + "tags": [ + "Stylesheet" + ], + "operationId": "RootStylesheetTree_Root", + "parameters": [ + { + "name": "skip", + "in": "query", + "schema": { + "type": "integer", + "format": "int32", + "default": 0 + }, + "x-position": 1 + }, + { + "name": "take", + "in": "query", + "schema": { + "type": "integer", + "format": "int32", + "default": 100 + }, + "x-position": 2 + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PagedViewModelOfFileSystemTreeItemViewModel" + } + } + } + } + } + } + }, + "/umbraco/api/v1/static-file/tree/children": { + "get": { + "tags": [ + "StaticFile" + ], + "operationId": "ChildrenStaticFileTree_Children", + "parameters": [ + { + "name": "path", + "in": "query", + "schema": { + "type": "string", + "nullable": true + }, + "x-position": 1 + }, + { + "name": "skip", + "in": "query", + "schema": { + "type": "integer", + "format": "int32", + "default": 0 + }, + "x-position": 2 + }, + { + "name": "take", + "in": "query", + "schema": { + "type": "integer", + "format": "int32", + "default": 100 + }, + "x-position": 3 + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PagedViewModelOfFileSystemTreeItemViewModel" + } + } + } + } + } + } + }, + "/umbraco/api/v1/static-file/tree/items": { + "get": { + "tags": [ + "StaticFile" + ], + "operationId": "ItemsStaticFileTree_Items", + "parameters": [ + { + "name": "path", + "x-originalName": "paths", + "in": "query", + "style": "form", + "explode": true, + "schema": { + "type": "array", + "nullable": true, + "items": { + "type": "string" + } + }, + "x-position": 1 + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/FileSystemTreeItemViewModel" + } + } + } + } + } + } + } + }, + "/umbraco/api/v1/static-file/tree/root": { + "get": { + "tags": [ + "StaticFile" + ], + "operationId": "RootStaticFileTree_Root", + "parameters": [ + { + "name": "skip", + "in": "query", + "schema": { + "type": "integer", + "format": "int32", + "default": 0 + }, + "x-position": 1 + }, + { + "name": "take", + "in": "query", + "schema": { + "type": "integer", + "format": "int32", + "default": 100 + }, + "x-position": 2 + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PagedViewModelOfFileSystemTreeItemViewModel" + } + } + } + } + } + } + }, + "/umbraco/api/v1/server/status": { + "get": { + "tags": [ + "Server" + ], + "operationId": "StatusServer_Get", + "responses": { + "400": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ServerStatusViewModel" + } + } + } + } + } + } + }, + "/umbraco/api/v1/server/version": { + "get": { + "tags": [ + "Server" + ], + "operationId": "VersionServer_Get", + "responses": { + "400": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/VersionViewModel" + } + } + } + } + } + } + }, + "/umbraco/api/v1/script/tree/children": { + "get": { + "tags": [ + "Script" + ], + "operationId": "ChildrenScriptTree_Children", + "parameters": [ + { + "name": "path", + "in": "query", + "schema": { + "type": "string", + "nullable": true + }, + "x-position": 1 + }, + { + "name": "skip", + "in": "query", + "schema": { + "type": "integer", + "format": "int32", + "default": 0 + }, + "x-position": 2 + }, + { + "name": "take", + "in": "query", + "schema": { + "type": "integer", + "format": "int32", + "default": 100 + }, + "x-position": 3 + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PagedViewModelOfFileSystemTreeItemViewModel" + } + } + } + } + } + } + }, + "/umbraco/api/v1/script/tree/items": { + "get": { + "tags": [ + "Script" + ], + "operationId": "ItemsScriptTree_Items", + "parameters": [ + { + "name": "path", + "x-originalName": "paths", + "in": "query", + "style": "form", + "explode": true, + "schema": { + "type": "array", + "nullable": true, + "items": { + "type": "string" + } + }, + "x-position": 1 + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/FileSystemTreeItemViewModel" + } + } + } + } + } + } + } + }, + "/umbraco/api/v1/script/tree/root": { + "get": { + "tags": [ + "Script" + ], + "operationId": "RootScriptTree_Root", + "parameters": [ + { + "name": "skip", + "in": "query", + "schema": { + "type": "integer", + "format": "int32", + "default": 0 + }, + "x-position": 1 + }, + { + "name": "take", + "in": "query", + "schema": { + "type": "integer", + "format": "int32", + "default": 100 + }, + "x-position": 2 + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PagedViewModelOfFileSystemTreeItemViewModel" + } + } + } + } + } + } + }, + "/umbraco/api/v1/relation-type/tree/items": { + "get": { + "tags": [ + "RelationType" + ], + "operationId": "ItemsRelationTypeTree_Items", + "parameters": [ + { + "name": "key", + "x-originalName": "keys", + "in": "query", + "style": "form", + "explode": true, + "schema": { + "type": "array", + "nullable": true, + "items": { + "type": "string", + "format": "guid" + } + }, + "x-position": 1 + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/FolderTreeItemViewModel" + } + } + } + } + } + } + } + }, + "/umbraco/api/v1/relation-type/tree/root": { + "get": { + "tags": [ + "RelationType" + ], + "operationId": "RootRelationTypeTree_Root", + "parameters": [ + { + "name": "skip", + "in": "query", + "schema": { + "type": "integer", + "format": "int32", + "default": 0 + }, + "x-position": 1 + }, + { + "name": "take", + "in": "query", + "schema": { + "type": "integer", + "format": "int32", + "default": 100 + }, + "x-position": 2 + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PagedViewModelOfEntityTreeItemViewModel" + } + } + } + } + } + } + }, + "/umbraco/api/v1/published-cache/collect": { + "post": { + "tags": [ + "PublishedCache" + ], + "operationId": "CollectPublishedCache_Collect", + "responses": { + "200": { + "description": "" + } + } + } + }, + "/umbraco/api/v1/published-cache/rebuild": { + "post": { + "tags": [ + "PublishedCache" + ], + "operationId": "RebuildPublishedCache_Collect", + "responses": { + "200": { + "description": "" + } + } + } + }, + "/umbraco/api/v1/published-cache/reload": { + "post": { + "tags": [ + "PublishedCache" + ], + "operationId": "ReloadPublishedCache_Reload", + "responses": { + "200": { + "description": "" + } + } + } + }, + "/umbraco/api/v1/published-cache/status": { + "get": { + "tags": [ + "PublishedCache" + ], + "operationId": "StatusPublishedCache_Status", + "responses": { + "200": { + "description": "", + "content": { + "application/json": { "schema": { - "format": "binary", "type": "string" } } - }, - "description": "" - }, - "400": { + } + } + } + } + }, + "/umbraco/api/v1/profiling/status": { + "get": { + "tags": [ + "Profiling" + ], + "operationId": "StatusProfiling_Status", + "responses": { + "200": { + "description": "", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ProblemDetails" + "$ref": "#/components/schemas/ProfilingStatusViewModel" } } - }, - "description": "" + } } - }, - "tags": [ - "ExamineManagement" - ] + } } }, - "/umbraco/api/v1/examineManagement/search": { + "/umbraco/api/v1/partial-view/tree/children": { "get": { - "operationId": "SearchExamineManagement_GetSearchResults", + "tags": [ + "PartialView" + ], + "operationId": "ChildrenPartialViewTree_Children", "parameters": [ { + "name": "path", "in": "query", - "name": "searcherName", "schema": { - "nullable": true, - "type": "string" + "type": "string", + "nullable": true }, "x-position": 1 }, { + "name": "skip", "in": "query", - "name": "query", "schema": { - "nullable": true, - "type": "string" + "type": "integer", + "format": "int32", + "default": 0 }, "x-position": 2 }, { + "name": "take", "in": "query", - "name": "skip", "schema": { + "type": "integer", "format": "int32", - "type": "integer" + "default": 100 + }, + "x-position": 3 + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PagedViewModelOfFileSystemTreeItemViewModel" + } + } + } + } + } + } + }, + "/umbraco/api/v1/partial-view/tree/items": { + "get": { + "tags": [ + "PartialView" + ], + "operationId": "ItemsPartialViewTree_Items", + "parameters": [ + { + "name": "path", + "x-originalName": "paths", + "in": "query", + "style": "form", + "explode": true, + "schema": { + "type": "array", + "nullable": true, + "items": { + "type": "string" + } + }, + "x-position": 1 + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/FileSystemTreeItemViewModel" + } + } + } + } + } + } + } + }, + "/umbraco/api/v1/partial-view/tree/root": { + "get": { + "tags": [ + "PartialView" + ], + "operationId": "RootPartialViewTree_Root", + "parameters": [ + { + "name": "skip", + "in": "query", + "schema": { + "type": "integer", + "format": "int32", + "default": 0 + }, + "x-position": 1 + }, + { + "name": "take", + "in": "query", + "schema": { + "type": "integer", + "format": "int32", + "default": 100 + }, + "x-position": 2 + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PagedViewModelOfFileSystemTreeItemViewModel" + } + } + } + } + } + } + }, + "/umbraco/api/v1/member-type/tree/items": { + "get": { + "tags": [ + "MemberType" + ], + "operationId": "ItemsMemberTypeTree_Items", + "parameters": [ + { + "name": "key", + "x-originalName": "keys", + "in": "query", + "style": "form", + "explode": true, + "schema": { + "type": "array", + "nullable": true, + "items": { + "type": "string", + "format": "guid" + } + }, + "x-position": 1 + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/EntityTreeItemViewModel" + } + } + } + } + } + } + } + }, + "/umbraco/api/v1/member-type/tree/root": { + "get": { + "tags": [ + "MemberType" + ], + "operationId": "RootMemberTypeTree_Root", + "parameters": [ + { + "name": "skip", + "in": "query", + "schema": { + "type": "integer", + "format": "int32", + "default": 0 + }, + "x-position": 1 + }, + { + "name": "take", + "in": "query", + "schema": { + "type": "integer", + "format": "int32", + "default": 100 + }, + "x-position": 2 + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PagedViewModelOfEntityTreeItemViewModel" + } + } + } + } + } + } + }, + "/umbraco/api/v1/member-group/tree/items": { + "get": { + "tags": [ + "MemberGroup" + ], + "operationId": "ItemsMemberGroupTree_Items", + "parameters": [ + { + "name": "key", + "x-originalName": "keys", + "in": "query", + "style": "form", + "explode": true, + "schema": { + "type": "array", + "nullable": true, + "items": { + "type": "string", + "format": "guid" + } + }, + "x-position": 1 + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/EntityTreeItemViewModel" + } + } + } + } + } + } + } + }, + "/umbraco/api/v1/member-group/tree/root": { + "get": { + "tags": [ + "MemberGroup" + ], + "operationId": "RootMemberGroupTree_Root", + "parameters": [ + { + "name": "skip", + "in": "query", + "schema": { + "type": "integer", + "format": "int32", + "default": 0 + }, + "x-position": 1 + }, + { + "name": "take", + "in": "query", + "schema": { + "type": "integer", + "format": "int32", + "default": 100 + }, + "x-position": 2 + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PagedViewModelOfEntityTreeItemViewModel" + } + } + } + } + } + } + }, + "/umbraco/api/v1/media/tree/children": { + "get": { + "tags": [ + "Media" + ], + "operationId": "ChildrenMediaTree_Children", + "parameters": [ + { + "name": "parentKey", + "in": "query", + "schema": { + "type": "string", + "format": "guid" + }, + "x-position": 1 + }, + { + "name": "skip", + "in": "query", + "schema": { + "type": "integer", + "format": "int32", + "default": 0 + }, + "x-position": 2 + }, + { + "name": "take", + "in": "query", + "schema": { + "type": "integer", + "format": "int32", + "default": 100 }, "x-position": 3 }, { + "name": "dataTypeKey", "in": "query", - "name": "take", "schema": { - "format": "int32", - "type": "integer" + "type": "string", + "format": "guid", + "nullable": true }, "x-position": 4 } ], "responses": { "200": { + "description": "", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/PagedViewModelOfPagedViewModelOfSearchResultViewModel" + "$ref": "#/components/schemas/PagedViewModelOfContentTreeItemViewModel" } } - }, - "description": "" - }, - "404": { - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ProblemDetails" - } - } - }, - "description": "" + } } - }, - "tags": [ - "ExamineManagement" - ] + } } }, - "/umbraco/api/v1/examineManagement/searchers": { + "/umbraco/api/v1/media/tree/items": { "get": { - "operationId": "SearchersExamineManagement_Searchers", + "tags": [ + "Media" + ], + "operationId": "ItemsMediaTree_Items", "parameters": [ { + "name": "key", + "x-originalName": "keys", "in": "query", - "name": "skip", + "style": "form", + "explode": true, "schema": { - "format": "int32", - "type": "integer" + "type": "array", + "nullable": true, + "items": { + "type": "string", + "format": "guid" + } }, "x-position": 1 }, { + "name": "dataTypeKey", "in": "query", - "name": "take", "schema": { - "format": "int32", - "type": "integer" + "type": "string", + "format": "guid", + "nullable": true }, "x-position": 2 } ], "responses": { "200": { + "description": "", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/PagedViewModelOfSearcherViewModel" + "type": "array", + "items": { + "$ref": "#/components/schemas/ContentTreeItemViewModel" + } } } - }, - "description": "" + } } - }, + } + } + }, + "/umbraco/api/v1/media/tree/root": { + "get": { "tags": [ - "ExamineManagement" - ] + "Media" + ], + "operationId": "RootMediaTree_Root", + "parameters": [ + { + "name": "skip", + "in": "query", + "schema": { + "type": "integer", + "format": "int32", + "default": 0 + }, + "x-position": 1 + }, + { + "name": "take", + "in": "query", + "schema": { + "type": "integer", + "format": "int32", + "default": 100 + }, + "x-position": 2 + }, + { + "name": "dataTypeKey", + "in": "query", + "schema": { + "type": "string", + "format": "guid", + "nullable": true + }, + "x-position": 3 + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PagedViewModelOfContentTreeItemViewModel" + } + } + } + } + } + } + }, + "/umbraco/api/v1/media/recycle-bin/children": { + "get": { + "tags": [ + "Media" + ], + "operationId": "ChildrenMediaRecycleBin_Children", + "parameters": [ + { + "name": "parentKey", + "in": "query", + "schema": { + "type": "string", + "format": "guid" + }, + "x-position": 1 + }, + { + "name": "skip", + "in": "query", + "schema": { + "type": "integer", + "format": "int32", + "default": 0 + }, + "x-position": 2 + }, + { + "name": "take", + "in": "query", + "schema": { + "type": "integer", + "format": "int32", + "default": 100 + }, + "x-position": 3 + } + ], + "responses": { + "401": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PagedViewModelOfRecycleBinItemViewModel" + } + } + } + } + } + } + }, + "/umbraco/api/v1/media/recycle-bin/root": { + "get": { + "tags": [ + "Media" + ], + "operationId": "RootMediaRecycleBin_Root", + "parameters": [ + { + "name": "skip", + "in": "query", + "schema": { + "type": "integer", + "format": "int32", + "default": 0 + }, + "x-position": 1 + }, + { + "name": "take", + "in": "query", + "schema": { + "type": "integer", + "format": "int32", + "default": 100 + }, + "x-position": 2 + } + ], + "responses": { + "401": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PagedViewModelOfRecycleBinItemViewModel" + } + } + } + } + } + } + }, + "/umbraco/api/v1/media-type/tree/children": { + "get": { + "tags": [ + "MediaType" + ], + "operationId": "ChildrenMediaTypeTree_Children", + "parameters": [ + { + "name": "parentKey", + "in": "query", + "schema": { + "type": "string", + "format": "guid" + }, + "x-position": 1 + }, + { + "name": "skip", + "in": "query", + "schema": { + "type": "integer", + "format": "int32", + "default": 0 + }, + "x-position": 2 + }, + { + "name": "take", + "in": "query", + "schema": { + "type": "integer", + "format": "int32", + "default": 100 + }, + "x-position": 3 + }, + { + "name": "foldersOnly", + "in": "query", + "schema": { + "type": "boolean", + "default": false + }, + "x-position": 4 + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PagedViewModelOfFolderTreeItemViewModel" + } + } + } + } + } + } + }, + "/umbraco/api/v1/media-type/tree/items": { + "get": { + "tags": [ + "MediaType" + ], + "operationId": "ItemsMediaTypeTree_Items", + "parameters": [ + { + "name": "key", + "x-originalName": "keys", + "in": "query", + "style": "form", + "explode": true, + "schema": { + "type": "array", + "nullable": true, + "items": { + "type": "string", + "format": "guid" + } + }, + "x-position": 1 + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/FolderTreeItemViewModel" + } + } + } + } + } + } + } + }, + "/umbraco/api/v1/media-type/tree/root": { + "get": { + "tags": [ + "MediaType" + ], + "operationId": "RootMediaTypeTree_Root", + "parameters": [ + { + "name": "skip", + "in": "query", + "schema": { + "type": "integer", + "format": "int32", + "default": 0 + }, + "x-position": 1 + }, + { + "name": "take", + "in": "query", + "schema": { + "type": "integer", + "format": "int32", + "default": 100 + }, + "x-position": 2 + }, + { + "name": "foldersOnly", + "in": "query", + "schema": { + "type": "boolean", + "default": false + }, + "x-position": 3 + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PagedViewModelOfFolderTreeItemViewModel" + } + } + } + } + } } }, "/umbraco/api/v1/install/settings": { @@ -362,32 +1677,39 @@ } } }, - "/umbraco/api/v1/upgrade/authorize": { - "post": { + "/umbraco/api/v1/examineManagement/indexes": { + "get": { "tags": [ - "Upgrade" + "ExamineManagement" + ], + "operationId": "IndexesExamineManagement_Indexes", + "parameters": [ + { + "name": "skip", + "in": "query", + "schema": { + "type": "integer", + "format": "int32" + }, + "x-position": 1 + }, + { + "name": "take", + "in": "query", + "schema": { + "type": "integer", + "format": "int32" + }, + "x-position": 2 + } ], - "operationId": "AuthorizeUpgrade_Authorize", "responses": { "200": { - "description": "" - }, - "428": { "description": "", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ProblemDetails" - } - } - } - }, - "500": { - "description": "", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ProblemDetails" + "$ref": "#/components/schemas/PagedViewModelOfExamineIndexViewModel" } } } @@ -395,42 +1717,23 @@ } } }, - "/umbraco/api/v1/upgrade/settings": { + "/umbraco/api/v1/examineManagement/index": { "get": { "tags": [ - "Upgrade" + "ExamineManagement" ], - "operationId": "SettingsUpgrade_Settings", - "responses": { - "200": { - "description": "", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/UpgradeSettingsViewModel" - } - } - } - }, - "428": { - "description": "", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/ProblemDetails" - } - } - } + "operationId": "IndexExamineManagement_Index", + "parameters": [ + { + "name": "indexName", + "in": "query", + "schema": { + "type": "string", + "nullable": true + }, + "x-position": 1 } - } - } - }, - "/umbraco/api/v1/server/status": { - "get": { - "tags": [ - "Server" ], - "operationId": "StatusServer_Get", "responses": { "400": { "description": "", @@ -447,7 +1750,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ServerStatusViewModel" + "$ref": "#/components/schemas/ExamineIndexViewModel" } } } @@ -455,12 +1758,23 @@ } } }, - "/umbraco/api/v1/server/version": { - "get": { + "/umbraco/api/v1/examineManagement/rebuild": { + "post": { "tags": [ - "Server" + "ExamineManagement" + ], + "operationId": "RebuildExamineManagement_Rebuild", + "parameters": [ + { + "name": "indexName", + "in": "query", + "schema": { + "type": "string", + "nullable": true + }, + "x-position": 1 + } ], - "operationId": "VersionServer_Get", "responses": { "400": { "description": "", @@ -475,9 +1789,10 @@ "200": { "description": "", "content": { - "application/json": { + "application/octet-stream": { "schema": { - "$ref": "#/components/schemas/VersionViewModel" + "type": "string", + "format": "binary" } } } @@ -485,19 +1800,39 @@ } } }, - "/umbraco/api/v1/profiling/status": { + "/umbraco/api/v1/examineManagement/searchers": { "get": { "tags": [ - "Profiling" + "ExamineManagement" + ], + "operationId": "SearchersExamineManagement_Searchers", + "parameters": [ + { + "name": "skip", + "in": "query", + "schema": { + "type": "integer", + "format": "int32" + }, + "x-position": 1 + }, + { + "name": "take", + "in": "query", + "schema": { + "type": "integer", + "format": "int32" + }, + "x-position": 2 + } ], - "operationId": "StatusProfiling_Status", "responses": { "200": { "description": "", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ProfilingStatusViewModel" + "$ref": "#/components/schemas/PagedViewModelOfSearcherViewModel" } } } @@ -505,58 +1840,892 @@ } } }, - "/umbraco/api/v1/published-cache/collect": { - "post": { - "tags": [ - "PublishedCache" - ], - "operationId": "CollectPublishedCache_Collect", - "responses": { - "200": { - "description": "" - } - } - } - }, - "/umbraco/api/v1/published-cache/rebuild": { - "post": { - "tags": [ - "PublishedCache" - ], - "operationId": "RebuildPublishedCache_Collect", - "responses": { - "200": { - "description": "" - } - } - } - }, - "/umbraco/api/v1/published-cache/reload": { - "post": { - "tags": [ - "PublishedCache" - ], - "operationId": "ReloadPublishedCache_Reload", - "responses": { - "200": { - "description": "" - } - } - } - }, - "/umbraco/api/v1/published-cache/status": { + "/umbraco/api/v1/examineManagement/search": { "get": { "tags": [ - "PublishedCache" + "ExamineManagement" + ], + "operationId": "SearchExamineManagement_GetSearchResults", + "parameters": [ + { + "name": "searcherName", + "in": "query", + "schema": { + "type": "string", + "nullable": true + }, + "x-position": 1 + }, + { + "name": "query", + "in": "query", + "schema": { + "type": "string", + "nullable": true + }, + "x-position": 2 + }, + { + "name": "skip", + "in": "query", + "schema": { + "type": "integer", + "format": "int32" + }, + "x-position": 3 + }, + { + "name": "take", + "in": "query", + "schema": { + "type": "integer", + "format": "int32" + }, + "x-position": 4 + } ], - "operationId": "StatusPublishedCache_Status", "responses": { "200": { "description": "", "content": { "application/json": { "schema": { - "type": "string" + "$ref": "#/components/schemas/PagedViewModelOfPagedViewModelOfSearchResultViewModel" + } + } + } + }, + "404": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + } + } + } + }, + "/umbraco/api/v1/document/tree/children": { + "get": { + "tags": [ + "Document" + ], + "operationId": "ChildrenDocumentTree_Children", + "parameters": [ + { + "name": "parentKey", + "in": "query", + "schema": { + "type": "string", + "format": "guid" + }, + "x-position": 1 + }, + { + "name": "skip", + "in": "query", + "schema": { + "type": "integer", + "format": "int32", + "default": 0 + }, + "x-position": 2 + }, + { + "name": "take", + "in": "query", + "schema": { + "type": "integer", + "format": "int32", + "default": 100 + }, + "x-position": 3 + }, + { + "name": "dataTypeKey", + "in": "query", + "schema": { + "type": "string", + "format": "guid", + "nullable": true + }, + "x-position": 4 + }, + { + "name": "culture", + "in": "query", + "schema": { + "type": "string", + "nullable": true + }, + "x-position": 5 + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PagedViewModelOfDocumentTreeItemViewModel" + } + } + } + } + } + } + }, + "/umbraco/api/v1/document/tree/items": { + "get": { + "tags": [ + "Document" + ], + "operationId": "ItemsDocumentTree_Items", + "parameters": [ + { + "name": "key", + "x-originalName": "keys", + "in": "query", + "style": "form", + "explode": true, + "schema": { + "type": "array", + "nullable": true, + "items": { + "type": "string", + "format": "guid" + } + }, + "x-position": 1 + }, + { + "name": "dataTypeKey", + "in": "query", + "schema": { + "type": "string", + "format": "guid", + "nullable": true + }, + "x-position": 2 + }, + { + "name": "culture", + "in": "query", + "schema": { + "type": "string", + "nullable": true + }, + "x-position": 3 + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/DocumentTreeItemViewModel" + } + } + } + } + } + } + } + }, + "/umbraco/api/v1/document/tree/root": { + "get": { + "tags": [ + "Document" + ], + "operationId": "RootDocumentTree_Root", + "parameters": [ + { + "name": "skip", + "in": "query", + "schema": { + "type": "integer", + "format": "int32", + "default": 0 + }, + "x-position": 1 + }, + { + "name": "take", + "in": "query", + "schema": { + "type": "integer", + "format": "int32", + "default": 100 + }, + "x-position": 2 + }, + { + "name": "dataTypeKey", + "in": "query", + "schema": { + "type": "string", + "format": "guid", + "nullable": true + }, + "x-position": 3 + }, + { + "name": "culture", + "in": "query", + "schema": { + "type": "string", + "nullable": true + }, + "x-position": 4 + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PagedViewModelOfDocumentTreeItemViewModel" + } + } + } + } + } + } + }, + "/umbraco/api/v1/document/recycle-bin/children": { + "get": { + "tags": [ + "Document" + ], + "operationId": "ChildrenDocumentRecycleBin_Children", + "parameters": [ + { + "name": "parentKey", + "in": "query", + "schema": { + "type": "string", + "format": "guid" + }, + "x-position": 1 + }, + { + "name": "skip", + "in": "query", + "schema": { + "type": "integer", + "format": "int32", + "default": 0 + }, + "x-position": 2 + }, + { + "name": "take", + "in": "query", + "schema": { + "type": "integer", + "format": "int32", + "default": 100 + }, + "x-position": 3 + } + ], + "responses": { + "401": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PagedViewModelOfRecycleBinItemViewModel" + } + } + } + } + } + } + }, + "/umbraco/api/v1/document/recycle-bin/root": { + "get": { + "tags": [ + "Document" + ], + "operationId": "RootDocumentRecycleBin_Root", + "parameters": [ + { + "name": "skip", + "in": "query", + "schema": { + "type": "integer", + "format": "int32", + "default": 0 + }, + "x-position": 1 + }, + { + "name": "take", + "in": "query", + "schema": { + "type": "integer", + "format": "int32", + "default": 100 + }, + "x-position": 2 + } + ], + "responses": { + "401": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ProblemDetails" + } + } + } + }, + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PagedViewModelOfRecycleBinItemViewModel" + } + } + } + } + } + } + }, + "/umbraco/api/v1/document-type/tree/children": { + "get": { + "tags": [ + "DocumentType" + ], + "operationId": "ChildrenDocumentTypeTree_Children", + "parameters": [ + { + "name": "parentKey", + "in": "query", + "schema": { + "type": "string", + "format": "guid" + }, + "x-position": 1 + }, + { + "name": "skip", + "in": "query", + "schema": { + "type": "integer", + "format": "int32", + "default": 0 + }, + "x-position": 2 + }, + { + "name": "take", + "in": "query", + "schema": { + "type": "integer", + "format": "int32", + "default": 100 + }, + "x-position": 3 + }, + { + "name": "foldersOnly", + "in": "query", + "schema": { + "type": "boolean", + "default": false + }, + "x-position": 4 + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PagedViewModelOfDocumentTypeTreeItemViewModel" + } + } + } + } + } + } + }, + "/umbraco/api/v1/document-type/tree/items": { + "get": { + "tags": [ + "DocumentType" + ], + "operationId": "ItemsDocumentTypeTree_Items", + "parameters": [ + { + "name": "key", + "x-originalName": "keys", + "in": "query", + "style": "form", + "explode": true, + "schema": { + "type": "array", + "nullable": true, + "items": { + "type": "string", + "format": "guid" + } + }, + "x-position": 1 + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/DocumentTypeTreeItemViewModel" + } + } + } + } + } + } + } + }, + "/umbraco/api/v1/document-type/tree/root": { + "get": { + "tags": [ + "DocumentType" + ], + "operationId": "RootDocumentTypeTree_Root", + "parameters": [ + { + "name": "skip", + "in": "query", + "schema": { + "type": "integer", + "format": "int32", + "default": 0 + }, + "x-position": 1 + }, + { + "name": "take", + "in": "query", + "schema": { + "type": "integer", + "format": "int32", + "default": 100 + }, + "x-position": 2 + }, + { + "name": "foldersOnly", + "in": "query", + "schema": { + "type": "boolean", + "default": false + }, + "x-position": 3 + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PagedViewModelOfDocumentTypeTreeItemViewModel" + } + } + } + } + } + } + }, + "/umbraco/api/v1/document-blueprint/tree/items": { + "get": { + "tags": [ + "DocumentBlueprint" + ], + "operationId": "ItemsDocumentBlueprintTree_Items", + "parameters": [ + { + "name": "key", + "x-originalName": "keys", + "in": "query", + "style": "form", + "explode": true, + "schema": { + "type": "array", + "nullable": true, + "items": { + "type": "string", + "format": "guid" + } + }, + "x-position": 1 + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/DocumentBlueprintTreeItemViewModel" + } + } + } + } + } + } + } + }, + "/umbraco/api/v1/document-blueprint/tree/root": { + "get": { + "tags": [ + "DocumentBlueprint" + ], + "operationId": "RootDocumentBlueprintTree_Root", + "parameters": [ + { + "name": "skip", + "in": "query", + "schema": { + "type": "integer", + "format": "int32", + "default": 0 + }, + "x-position": 1 + }, + { + "name": "take", + "in": "query", + "schema": { + "type": "integer", + "format": "int32", + "default": 100 + }, + "x-position": 2 + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PagedViewModelOfDocumentBlueprintTreeItemViewModel" + } + } + } + } + } + } + }, + "/umbraco/api/v1/dictionary-item/tree/children": { + "get": { + "tags": [ + "DictionaryItem" + ], + "operationId": "ChildrenDictionaryItemTree_Children", + "parameters": [ + { + "name": "parentKey", + "in": "query", + "schema": { + "type": "string", + "format": "guid" + }, + "x-position": 1 + }, + { + "name": "skip", + "in": "query", + "schema": { + "type": "integer", + "format": "int32", + "default": 0 + }, + "x-position": 2 + }, + { + "name": "take", + "in": "query", + "schema": { + "type": "integer", + "format": "int32", + "default": 100 + }, + "x-position": 3 + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PagedViewModelOfEntityTreeItemViewModel" + } + } + } + } + } + } + }, + "/umbraco/api/v1/dictionary-item/tree/items": { + "get": { + "tags": [ + "DictionaryItem" + ], + "operationId": "ItemsDictionaryItemTree_Items", + "parameters": [ + { + "name": "key", + "x-originalName": "keys", + "in": "query", + "style": "form", + "explode": true, + "schema": { + "type": "array", + "nullable": true, + "items": { + "type": "string", + "format": "guid" + } + }, + "x-position": 1 + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/FolderTreeItemViewModel" + } + } + } + } + } + } + } + }, + "/umbraco/api/v1/dictionary-item/tree/root": { + "get": { + "tags": [ + "DictionaryItem" + ], + "operationId": "RootDictionaryItemTree_Root", + "parameters": [ + { + "name": "skip", + "in": "query", + "schema": { + "type": "integer", + "format": "int32", + "default": 0 + }, + "x-position": 1 + }, + { + "name": "take", + "in": "query", + "schema": { + "type": "integer", + "format": "int32", + "default": 100 + }, + "x-position": 2 + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PagedViewModelOfEntityTreeItemViewModel" + } + } + } + } + } + } + }, + "/umbraco/api/v1/data-type/tree/children": { + "get": { + "tags": [ + "DataType" + ], + "operationId": "ChildrenDataTypeTree_Children", + "parameters": [ + { + "name": "parentKey", + "in": "query", + "schema": { + "type": "string", + "format": "guid" + }, + "x-position": 1 + }, + { + "name": "skip", + "in": "query", + "schema": { + "type": "integer", + "format": "int32", + "default": 0 + }, + "x-position": 2 + }, + { + "name": "take", + "in": "query", + "schema": { + "type": "integer", + "format": "int32", + "default": 100 + }, + "x-position": 3 + }, + { + "name": "foldersOnly", + "in": "query", + "schema": { + "type": "boolean", + "default": false + }, + "x-position": 4 + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PagedViewModelOfFolderTreeItemViewModel" + } + } + } + } + } + } + }, + "/umbraco/api/v1/data-type/tree/items": { + "get": { + "tags": [ + "DataType" + ], + "operationId": "ItemsDataTypeTree_Items", + "parameters": [ + { + "name": "key", + "x-originalName": "keys", + "in": "query", + "style": "form", + "explode": true, + "schema": { + "type": "array", + "nullable": true, + "items": { + "type": "string", + "format": "guid" + } + }, + "x-position": 1 + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/FolderTreeItemViewModel" + } + } + } + } + } + } + } + }, + "/umbraco/api/v1/data-type/tree/root": { + "get": { + "tags": [ + "DataType" + ], + "operationId": "RootDataTypeTree_Root", + "parameters": [ + { + "name": "skip", + "in": "query", + "schema": { + "type": "integer", + "format": "int32", + "default": 0 + }, + "x-position": 1 + }, + { + "name": "take", + "in": "query", + "schema": { + "type": "integer", + "format": "int32", + "default": 100 + }, + "x-position": 2 + }, + { + "name": "foldersOnly", + "in": "query", + "schema": { + "type": "boolean", + "default": false + }, + "x-position": 3 + } + ], + "responses": { + "200": { + "description": "", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PagedViewModelOfFolderTreeItemViewModel" } } } @@ -596,6 +2765,278 @@ } } }, + "UpgradeSettingsViewModel": { + "type": "object", + "additionalProperties": false, + "properties": { + "currentState": { + "type": "string" + }, + "newState": { + "type": "string" + }, + "newVersion": { + "type": "string" + }, + "oldVersion": { + "type": "string" + }, + "reportUrl": { + "type": "string" + } + } + }, + "PagedViewModelOfEntityTreeItemViewModel": { + "type": "object", + "additionalProperties": false, + "properties": { + "total": { + "type": "integer", + "format": "int64" + }, + "items": { + "type": "array", + "items": { + "$ref": "#/components/schemas/EntityTreeItemViewModel" + } + } + } + }, + "EntityTreeItemViewModel": { + "allOf": [ + { + "$ref": "#/components/schemas/TreeItemViewModel" + }, + { + "type": "object", + "additionalProperties": false, + "properties": { + "key": { + "type": "string", + "format": "guid" + }, + "isContainer": { + "type": "boolean" + }, + "parentKey": { + "type": "string", + "format": "guid", + "nullable": true + } + } + } + ] + }, + "TreeItemViewModel": { + "type": "object", + "additionalProperties": false, + "properties": { + "name": { + "type": "string" + }, + "type": { + "type": "string" + }, + "icon": { + "type": "string" + }, + "hasChildren": { + "type": "boolean" + } + } + }, + "PagedViewModelOfFileSystemTreeItemViewModel": { + "type": "object", + "additionalProperties": false, + "properties": { + "total": { + "type": "integer", + "format": "int64" + }, + "items": { + "type": "array", + "items": { + "$ref": "#/components/schemas/FileSystemTreeItemViewModel" + } + } + } + }, + "FileSystemTreeItemViewModel": { + "allOf": [ + { + "$ref": "#/components/schemas/TreeItemViewModel" + }, + { + "type": "object", + "additionalProperties": false, + "properties": { + "path": { + "type": "string" + }, + "isFolder": { + "type": "boolean" + } + } + } + ] + }, + "ServerStatusViewModel": { + "type": "object", + "additionalProperties": false, + "properties": { + "serverStatus": { + "$ref": "#/components/schemas/RuntimeLevel" + } + } + }, + "RuntimeLevel": { + "type": "string", + "description": "Describes the levels in which the runtime can run.\n ", + "x-enumNames": [ + "Unknown", + "Boot", + "Install", + "Upgrade", + "Run", + "BootFailed" + ], + "enum": [ + "Unknown", + "Boot", + "Install", + "Upgrade", + "Run", + "BootFailed" + ] + }, + "VersionViewModel": { + "type": "object", + "additionalProperties": false, + "properties": { + "version": { + "type": "string" + } + } + }, + "FolderTreeItemViewModel": { + "allOf": [ + { + "$ref": "#/components/schemas/EntityTreeItemViewModel" + }, + { + "type": "object", + "additionalProperties": false, + "properties": { + "isFolder": { + "type": "boolean" + } + } + } + ] + }, + "ProfilingStatusViewModel": { + "type": "object", + "additionalProperties": false, + "properties": { + "enabled": { + "type": "boolean" + } + } + }, + "PagedViewModelOfContentTreeItemViewModel": { + "type": "object", + "additionalProperties": false, + "properties": { + "total": { + "type": "integer", + "format": "int64" + }, + "items": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ContentTreeItemViewModel" + } + } + } + }, + "ContentTreeItemViewModel": { + "allOf": [ + { + "$ref": "#/components/schemas/EntityTreeItemViewModel" + }, + { + "type": "object", + "additionalProperties": false, + "properties": { + "noAccess": { + "type": "boolean" + } + } + } + ] + }, + "PagedViewModelOfRecycleBinItemViewModel": { + "type": "object", + "additionalProperties": false, + "properties": { + "total": { + "type": "integer", + "format": "int64" + }, + "items": { + "type": "array", + "items": { + "$ref": "#/components/schemas/RecycleBinItemViewModel" + } + } + } + }, + "RecycleBinItemViewModel": { + "type": "object", + "additionalProperties": false, + "properties": { + "key": { + "type": "string", + "format": "guid" + }, + "name": { + "type": "string" + }, + "type": { + "type": "string" + }, + "icon": { + "type": "string" + }, + "hasChildren": { + "type": "boolean" + }, + "isContainer": { + "type": "boolean" + }, + "parentKey": { + "type": "string", + "format": "guid", + "nullable": true + } + } + }, + "PagedViewModelOfFolderTreeItemViewModel": { + "type": "object", + "additionalProperties": false, + "properties": { + "total": { + "type": "integer", + "format": "int64" + }, + "items": { + "type": "array", + "items": { + "$ref": "#/components/schemas/FolderTreeItemViewModel" + } + } + } + }, "InstallSettingsViewModel": { "type": "object", "additionalProperties": false, @@ -787,92 +3228,320 @@ } } }, - "UpgradeSettingsViewModel": { + "PagedViewModelOfExamineIndexViewModel": { "type": "object", "additionalProperties": false, "properties": { - "currentState": { + "total": { + "type": "integer", + "format": "int64" + }, + "items": { + "type": "array", + "items": { + "$ref": "#/components/schemas/ExamineIndexViewModel" + } + } + } + }, + "ExamineIndexViewModel": { + "type": "object", + "additionalProperties": false, + "properties": { + "name": { "type": "string" }, - "newState": { + "healthStatus": { + "type": "string", + "nullable": true + }, + "isHealthy": { + "type": "boolean" + }, + "canRebuild": { + "type": "boolean" + }, + "searcherName": { "type": "string" }, - "newVersion": { - "type": "string" + "documentCount": { + "type": "integer", + "format": "int64" }, - "oldVersion": { - "type": "string" + "fieldCount": { + "type": "integer", + "format": "int32" + } + } + }, + "PagedViewModelOfSearcherViewModel": { + "type": "object", + "additionalProperties": false, + "properties": { + "total": { + "type": "integer", + "format": "int64" }, - "reportUrl": { + "items": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SearcherViewModel" + } + } + } + }, + "SearcherViewModel": { + "type": "object", + "additionalProperties": false, + "properties": { + "name": { "type": "string" } } }, - "ServerStatusViewModel": { + "PagedViewModelOfPagedViewModelOfSearchResultViewModel": { "type": "object", "additionalProperties": false, "properties": { - "serverStatus": { - "$ref": "#/components/schemas/RuntimeLevel" + "total": { + "type": "integer", + "format": "int64" + }, + "items": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PagedViewModelOfSearchResultViewModel" + } } } }, - "RuntimeLevel": { - "type": "string", - "description": "Describes the levels in which the runtime can run.\n ", - "x-enumNames": [ - "Unknown", - "Boot", - "Install", - "Upgrade", - "Run", - "BootFailed" - ], - "enum": [ - "Unknown", - "Boot", - "Install", - "Upgrade", - "Run", - "BootFailed" + "PagedViewModelOfSearchResultViewModel": { + "type": "object", + "additionalProperties": false, + "properties": { + "total": { + "type": "integer", + "format": "int64" + }, + "items": { + "type": "array", + "items": { + "$ref": "#/components/schemas/SearchResultViewModel" + } + } + } + }, + "SearchResultViewModel": { + "type": "object", + "additionalProperties": false, + "properties": { + "id": { + "type": "string" + }, + "score": { + "type": "number", + "format": "float" + }, + "fieldCount": { + "type": "integer", + "format": "int32" + }, + "fields": { + "type": "array", + "items": { + "$ref": "#/components/schemas/FieldViewModel" + } + } + } + }, + "FieldViewModel": { + "type": "object", + "additionalProperties": false, + "properties": { + "name": { + "type": "string" + }, + "values": { + "type": "array", + "items": { + "type": "string" + } + } + } + }, + "PagedViewModelOfDocumentTreeItemViewModel": { + "type": "object", + "additionalProperties": false, + "properties": { + "total": { + "type": "integer", + "format": "int64" + }, + "items": { + "type": "array", + "items": { + "$ref": "#/components/schemas/DocumentTreeItemViewModel" + } + } + } + }, + "DocumentTreeItemViewModel": { + "allOf": [ + { + "$ref": "#/components/schemas/ContentTreeItemViewModel" + }, + { + "type": "object", + "additionalProperties": false, + "properties": { + "isProtected": { + "type": "boolean" + }, + "isPublished": { + "type": "boolean" + }, + "isEdited": { + "type": "boolean" + } + } + } ] }, - "VersionViewModel": { + "PagedViewModelOfDocumentTypeTreeItemViewModel": { "type": "object", "additionalProperties": false, "properties": { - "version": { - "type": "string" + "total": { + "type": "integer", + "format": "int64" + }, + "items": { + "type": "array", + "items": { + "$ref": "#/components/schemas/DocumentTypeTreeItemViewModel" + } } } }, - "ProfilingStatusViewModel": { + "DocumentTypeTreeItemViewModel": { + "allOf": [ + { + "$ref": "#/components/schemas/FolderTreeItemViewModel" + }, + { + "type": "object", + "additionalProperties": false, + "properties": { + "isElement": { + "type": "boolean" + } + } + } + ] + }, + "DocumentBlueprintTreeItemViewModel": { + "allOf": [ + { + "$ref": "#/components/schemas/EntityTreeItemViewModel" + }, + { + "type": "object", + "additionalProperties": false, + "properties": { + "documentTypeKey": { + "type": "string", + "format": "guid" + }, + "documentTypeAlias": { + "type": "string" + }, + "documentTypeName": { + "type": "string", + "nullable": true + } + } + } + ] + }, + "PagedViewModelOfDocumentBlueprintTreeItemViewModel": { "type": "object", "additionalProperties": false, "properties": { - "enabled": { - "type": "boolean" + "total": { + "type": "integer", + "format": "int64" + }, + "items": { + "type": "array", + "items": { + "$ref": "#/components/schemas/DocumentBlueprintTreeItemViewModel" + } } } } } }, "tags": [ + { + "name": "DataType" + }, + { + "name": "DictionaryItem" + }, + { + "name": "Document" + }, + { + "name": "DocumentBlueprint" + }, + { + "name": "DocumentType" + }, { "name": "ExamineManagement" }, { "name": "Install" }, + { + "name": "Media" + }, + { + "name": "MediaType" + }, + { + "name": "MemberGroup" + }, + { + "name": "MemberType" + }, + { + "name": "PartialView" + }, { "name": "Profiling" }, { "name": "PublishedCache" }, + { + "name": "RelationType" + }, + { + "name": "Script" + }, { "name": "Server" }, + { + "name": "StaticFile" + }, + { + "name": "Stylesheet" + }, + { + "name": "Template" + }, { "name": "Upgrade" } diff --git a/src/Umbraco.Cms.ManagementApi/Services/Entities/IUserStartNodeEntitiesService.cs b/src/Umbraco.Cms.ManagementApi/Services/Entities/IUserStartNodeEntitiesService.cs new file mode 100644 index 0000000000..2f05759b42 --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Services/Entities/IUserStartNodeEntitiesService.cs @@ -0,0 +1,42 @@ +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.Entities; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.ManagementApi.Models.Entities; + +namespace Umbraco.Cms.ManagementApi.Services.Entities; + +public interface IUserStartNodeEntitiesService +{ + /// + /// Calculates the applicable root entities for a given object type for users without root access. + /// + /// The object type. + /// The calculated start node IDs for the user. + /// A list of root entities for the user. + /// + /// The returned entities may include entities that outside of the user start node scope, but are needed to + /// for browsing to the actual user start nodes. These entities will be marked as "no access" entities. + /// + IEnumerable RootUserAccessEntities(UmbracoObjectTypes umbracoObjectType, int[] userStartNodeIds); + + /// + /// Calculates the applicable child entities from a list of candidate child entities for users without root access. + /// + /// The candidate child entities to filter (i.e. entities fetched with ). + /// The calculated start node paths for the user. + /// A list of child entities applicable entities for the user. + /// + /// The returned entities may include entities that outside of the user start node scope, but are needed to + /// for browsing to the actual user start nodes. These entities will be marked as "no access" entities. + /// Some candidate entities may be filtered out if they are not applicable for the user scope. + /// + IEnumerable ChildUserAccessEntities(IEnumerable candidateChildren, string[] userStartNodePaths); + + /// + /// Calculates the access level of a collection of entities for users without root access. + /// + /// The entities. + /// The calculated start node paths for the user. + /// The access level for each entity. + IEnumerable UserAccessEntities(IEnumerable entities, string[] userStartNodePaths); +} diff --git a/src/Umbraco.Cms.ManagementApi/Services/Entities/UserStartNodeEntitiesService.cs b/src/Umbraco.Cms.ManagementApi/Services/Entities/UserStartNodeEntitiesService.cs new file mode 100644 index 0000000000..70c01308d8 --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Services/Entities/UserStartNodeEntitiesService.cs @@ -0,0 +1,76 @@ +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Models.Entities; +using Umbraco.Cms.Core.Services; +using Umbraco.Cms.ManagementApi.Models.Entities; +using Umbraco.Extensions; + +namespace Umbraco.Cms.ManagementApi.Services.Entities; + +public class UserStartNodeEntitiesService : IUserStartNodeEntitiesService +{ + private readonly IEntityService _entityService; + + public UserStartNodeEntitiesService(IEntityService entityService) => _entityService = entityService; + + /// + public IEnumerable RootUserAccessEntities(UmbracoObjectTypes umbracoObjectType, int[] userStartNodeIds) + { + // root entities for users without root access should include: + // - the start nodes that are actual root entities (level == 1) + // - the root level ancestors to the rest of the start nodes (required for browsing to the actual start nodes - will be marked as "no access") + IEntitySlim[] userStartEntities = _entityService.GetAll(umbracoObjectType, userStartNodeIds).ToArray(); + + // find the start nodes that are at root level (level == 1) + IEntitySlim[] allowedTopmostEntities = userStartEntities.Where(entity => entity.Level == 1).ToArray(); + + // find the root level ancestors of the rest of the start nodes, and add those as well + var nonAllowedTopmostEntityIds = userStartEntities.Except(allowedTopmostEntities) + .Select(entity => int.TryParse(entity.Path.Split(Constants.CharArrays.Comma).Skip(1).FirstOrDefault(), out var id) ? id : 0) + .Where(id => id > 0) + .ToArray(); + IEntitySlim[] nonAllowedTopmostEntities = nonAllowedTopmostEntityIds.Any() + ? _entityService.GetAll(umbracoObjectType, nonAllowedTopmostEntityIds).ToArray() + : Array.Empty(); + + return allowedTopmostEntities + .Select(entity => new UserAccessEntity(entity, true)) + .Union( + nonAllowedTopmostEntities + .Select(entity => new UserAccessEntity(entity, false))) + .ToArray(); + } + + /// + public IEnumerable ChildUserAccessEntities(IEnumerable candidateChildren, string[] userStartNodePaths) + // child entities for users without root access should include: + // - children that are descendant-or-self of a user start node + // - children that are ancestors of a user start node (required for browsing to the actual start nodes - will be marked as "no access") + // all other candidate children should be discarded + => candidateChildren.Select(child => + { + // is descendant-or-self of a start node? + if (IsDescendantOrSelf(child, userStartNodePaths)) + { + return new UserAccessEntity(child, true); + } + + // is ancestor of a start node? + if (userStartNodePaths.Any(path => path.StartsWith(child.Path))) + { + return new UserAccessEntity(child, false); + } + + return null; + }).WhereNotNull().ToArray(); + + /// + public IEnumerable UserAccessEntities(IEnumerable entities, string[] userStartNodePaths) + // entities for users without root access should include: + // - entities that are descendant-or-self of a user start node as regular entities + // - all other entities as "no access" entities + => entities.Select(entity => new UserAccessEntity(entity, IsDescendantOrSelf(entity, userStartNodePaths))).ToArray(); + + private static bool IsDescendantOrSelf(IEntitySlim child, string[] userStartNodePaths) + => userStartNodePaths.Any(path => child.Path.StartsWith(path)); +} diff --git a/src/Umbraco.Cms.ManagementApi/Services/Paging/PaginationService.cs b/src/Umbraco.Cms.ManagementApi/Services/Paging/PaginationService.cs new file mode 100644 index 0000000000..34488a51c3 --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/Services/Paging/PaginationService.cs @@ -0,0 +1,39 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; + +namespace Umbraco.Cms.ManagementApi.Services.Paging; + +// TODO: remove this class once EF core is in place with proper skip/take pagination implementation +// this service is used for converting skip/take to classic pagination with page number and page size. +// it is a temporary solution that should be removed once EF core is in place, thus we'll live +// with this code being statically referenced across multiple controllers. the alternative would be +// an injectable service, but that would require a greater clean-up effort later on. +internal static class PaginationService +{ + internal static bool ConvertSkipTakeToPaging(int skip, int take, out long pageNumber, out int pageSize, out ProblemDetails? error) + { + if (take <= 0) + { + throw new ArgumentException("Must be greater than zero", nameof(take)); + } + + if (skip % take != 0) + { + pageSize = 0; + pageNumber = 0; + error = new ProblemDetails + { + Title = "Invalid skip/take", + Detail = "Skip must be a multiple of take - i.e. skip = 10, take = 5", + Status = StatusCodes.Status400BadRequest, + Type = "Error", + }; + return false; + } + + pageSize = take; + pageNumber = skip / take; + error = null; + return true; + } +} diff --git a/src/Umbraco.Cms.ManagementApi/ViewModels/RecycleBin/RecycleBinItemViewModel.cs b/src/Umbraco.Cms.ManagementApi/ViewModels/RecycleBin/RecycleBinItemViewModel.cs new file mode 100644 index 0000000000..1fe649132a --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/ViewModels/RecycleBin/RecycleBinItemViewModel.cs @@ -0,0 +1,19 @@ +namespace Umbraco.Cms.ManagementApi.ViewModels.RecycleBin; + +public class RecycleBinItemViewModel +{ + public Guid Key { get; set; } + + public string Name { get; set; } = string.Empty; + + public string Type { get; set; } = string.Empty; + + public string Icon { get; set; } = string.Empty; + + public bool HasChildren { get; set; } + + public bool IsContainer { get; set; } + + public Guid? ParentKey { get; set; } +} + diff --git a/src/Umbraco.Cms.ManagementApi/ViewModels/Tree/ContentTreeItemViewModel.cs b/src/Umbraco.Cms.ManagementApi/ViewModels/Tree/ContentTreeItemViewModel.cs new file mode 100644 index 0000000000..b958b0bff5 --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/ViewModels/Tree/ContentTreeItemViewModel.cs @@ -0,0 +1,6 @@ +namespace Umbraco.Cms.ManagementApi.ViewModels.Tree; + +public class ContentTreeItemViewModel : EntityTreeItemViewModel +{ + public bool NoAccess { get; set; } +} diff --git a/src/Umbraco.Cms.ManagementApi/ViewModels/Tree/DocumentBlueprintTreeItemViewModel.cs b/src/Umbraco.Cms.ManagementApi/ViewModels/Tree/DocumentBlueprintTreeItemViewModel.cs new file mode 100644 index 0000000000..24760f7f1b --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/ViewModels/Tree/DocumentBlueprintTreeItemViewModel.cs @@ -0,0 +1,10 @@ +namespace Umbraco.Cms.ManagementApi.ViewModels.Tree; + +public class DocumentBlueprintTreeItemViewModel : EntityTreeItemViewModel +{ + public Guid DocumentTypeKey { get; set; } + + public string DocumentTypeAlias { get; set; } = string.Empty; + + public string? DocumentTypeName { get; set; } = string.Empty; +} diff --git a/src/Umbraco.Cms.ManagementApi/ViewModels/Tree/DocumentTreeItemViewModel.cs b/src/Umbraco.Cms.ManagementApi/ViewModels/Tree/DocumentTreeItemViewModel.cs new file mode 100644 index 0000000000..bba2c90f15 --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/ViewModels/Tree/DocumentTreeItemViewModel.cs @@ -0,0 +1,10 @@ +namespace Umbraco.Cms.ManagementApi.ViewModels.Tree; + +public class DocumentTreeItemViewModel : ContentTreeItemViewModel +{ + public bool IsProtected { get; set; } + + public bool IsPublished { get; set; } + + public bool IsEdited { get; set; } +} diff --git a/src/Umbraco.Cms.ManagementApi/ViewModels/Tree/DocumentTypeTreeItemViewModel.cs b/src/Umbraco.Cms.ManagementApi/ViewModels/Tree/DocumentTypeTreeItemViewModel.cs new file mode 100644 index 0000000000..0fbd3abb4c --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/ViewModels/Tree/DocumentTypeTreeItemViewModel.cs @@ -0,0 +1,6 @@ +namespace Umbraco.Cms.ManagementApi.ViewModels.Tree; + +public class DocumentTypeTreeItemViewModel : FolderTreeItemViewModel +{ + public bool IsElement { get; set; } +} diff --git a/src/Umbraco.Cms.ManagementApi/ViewModels/Tree/EntityTreeItemViewModel.cs b/src/Umbraco.Cms.ManagementApi/ViewModels/Tree/EntityTreeItemViewModel.cs new file mode 100644 index 0000000000..fd9c3abd48 --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/ViewModels/Tree/EntityTreeItemViewModel.cs @@ -0,0 +1,10 @@ +namespace Umbraco.Cms.ManagementApi.ViewModels.Tree; + +public class EntityTreeItemViewModel : TreeItemViewModel +{ + public Guid Key { get; set; } + + public bool IsContainer { get; set; } + + public Guid? ParentKey { get; set; } +} diff --git a/src/Umbraco.Cms.ManagementApi/ViewModels/Tree/FileSystemTreeItemViewModel.cs b/src/Umbraco.Cms.ManagementApi/ViewModels/Tree/FileSystemTreeItemViewModel.cs new file mode 100644 index 0000000000..50973df0c0 --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/ViewModels/Tree/FileSystemTreeItemViewModel.cs @@ -0,0 +1,8 @@ +namespace Umbraco.Cms.ManagementApi.ViewModels.Tree; + +public class FileSystemTreeItemViewModel : TreeItemViewModel +{ + public string Path { get; set; } = string.Empty; + + public bool IsFolder { get; set; } +} diff --git a/src/Umbraco.Cms.ManagementApi/ViewModels/Tree/FolderTreeItemViewModel.cs b/src/Umbraco.Cms.ManagementApi/ViewModels/Tree/FolderTreeItemViewModel.cs new file mode 100644 index 0000000000..4e435372d1 --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/ViewModels/Tree/FolderTreeItemViewModel.cs @@ -0,0 +1,6 @@ +namespace Umbraco.Cms.ManagementApi.ViewModels.Tree; + +public class FolderTreeItemViewModel : EntityTreeItemViewModel +{ + public bool IsFolder { get; set; } +} diff --git a/src/Umbraco.Cms.ManagementApi/ViewModels/Tree/TreeItemViewModel.cs b/src/Umbraco.Cms.ManagementApi/ViewModels/Tree/TreeItemViewModel.cs new file mode 100644 index 0000000000..ac8fdc2cb9 --- /dev/null +++ b/src/Umbraco.Cms.ManagementApi/ViewModels/Tree/TreeItemViewModel.cs @@ -0,0 +1,12 @@ +namespace Umbraco.Cms.ManagementApi.ViewModels.Tree; + +public class TreeItemViewModel +{ + public string Name { get; set; } = string.Empty; + + public string Type { get; set; } = string.Empty; + + public string Icon { get; set; } = string.Empty; + + public bool HasChildren { get; set; } +} diff --git a/src/Umbraco.Core/Constants-Icons.cs b/src/Umbraco.Core/Constants-Icons.cs index 40ab52aaa5..5cfc2808fc 100644 --- a/src/Umbraco.Core/Constants-Icons.cs +++ b/src/Umbraco.Core/Constants-Icons.cs @@ -119,11 +119,31 @@ public static partial class Constants /// public const string Packages = "icon-box"; + /// + /// System property editor icon + /// + public const string PartialView = "icon-article"; + /// /// System property editor icon /// public const string PropertyEditor = "icon-autofill"; + /// + /// Relation type icon + /// + public const string RelationType = "icon-trafic"; + + /// + /// Script type icon + /// + public const string Script = "icon-script"; + + /// + /// Stylesheet type icon + /// + public const string Stylesheet = "icon-brackets"; + /// /// System member icon /// diff --git a/src/Umbraco.Core/Models/UmbracoObjectTypes.cs b/src/Umbraco.Core/Models/UmbracoObjectTypes.cs index 600927db84..acc9888bf4 100644 --- a/src/Umbraco.Core/Models/UmbracoObjectTypes.cs +++ b/src/Umbraco.Core/Models/UmbracoObjectTypes.cs @@ -54,7 +54,7 @@ public enum UmbracoObjectTypes /// /// Member Group /// - [UmbracoObjectType(Constants.ObjectTypes.Strings.MemberGroup)] + [UmbracoObjectType(Constants.ObjectTypes.Strings.MemberGroup, typeof(IMemberGroup))] [FriendlyName("Member Group")] [UmbracoUdiType(Constants.UdiEntityType.MemberGroup)] MemberGroup, diff --git a/src/Umbraco.Core/Persistence/Repositories/IDictionaryRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IDictionaryRepository.cs index db2347e925..9d75bc1030 100644 --- a/src/Umbraco.Core/Persistence/Repositories/IDictionaryRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/IDictionaryRepository.cs @@ -6,6 +6,10 @@ public interface IDictionaryRepository : IReadWriteQueryRepository GetMany(params Guid[] uniqueIds) => Array.Empty(); + + IEnumerable GetManyByKeys(params string[] keys) => Array.Empty(); + IDictionaryItem? Get(string key); IEnumerable GetDictionaryItemDescendants(Guid? parentId); diff --git a/src/Umbraco.Core/Services/EntityService.cs b/src/Umbraco.Core/Services/EntityService.cs index 591fa17909..b6fc244bd0 100644 --- a/src/Umbraco.Core/Services/EntityService.cs +++ b/src/Umbraco.Core/Services/EntityService.cs @@ -37,6 +37,8 @@ public class EntityService : RepositoryService, IEntityService { typeof(IMediaType).FullName!, UmbracoObjectTypes.MediaType }, { typeof(IMember).FullName!, UmbracoObjectTypes.Member }, { typeof(IMemberType).FullName!, UmbracoObjectTypes.MemberType }, + { typeof(IMemberGroup).FullName!, UmbracoObjectTypes.MemberGroup }, + { typeof(ITemplate).FullName!, UmbracoObjectTypes.Template }, }; } @@ -314,14 +316,18 @@ public class EntityService : RepositoryService, IEntityService out long totalRecords, IQuery? filter = null, Ordering? ordering = null) - { - using (ScopeProvider.CreateCoreScope(autoComplete: true)) - { - IQuery query = Query().Where(x => x.ParentId == id && x.Trashed == false); + => GetPagedChildren(id, objectType, pageIndex, pageSize, false, filter, ordering, out totalRecords); - return _entityRepository.GetPagedResultsByQuery(query, objectType.GetGuid(), pageIndex, pageSize, out totalRecords, filter, ordering); - } - } + /// + public IEnumerable GetPagedTrashedChildren( + int id, + UmbracoObjectTypes objectType, + long pageIndex, + int pageSize, + out long totalRecords, + IQuery? filter = null, + Ordering? ordering = null) + => GetPagedChildren(id, objectType, pageIndex, pageSize, true, filter, ordering, out totalRecords); /// public IEnumerable GetPagedDescendants( @@ -523,4 +529,23 @@ public class EntityService : RepositoryService, IEntityService return objType; } + + private IEnumerable GetPagedChildren( + int id, + UmbracoObjectTypes objectType, + long pageIndex, + int pageSize, + bool trashed, + IQuery? filter, + Ordering? ordering, + out long totalRecords) + { + using (ScopeProvider.CreateCoreScope(autoComplete: true)) + { + IQuery query = Query().Where(x => x.ParentId == id && x.Trashed == trashed); + + return _entityRepository.GetPagedResultsByQuery(query, objectType.GetGuid(), pageIndex, pageSize, out totalRecords, filter, ordering); + } + } } + diff --git a/src/Umbraco.Core/Services/IEntityService.cs b/src/Umbraco.Core/Services/IEntityService.cs index 74a416a8fe..5151d9ed1f 100644 --- a/src/Umbraco.Core/Services/IEntityService.cs +++ b/src/Umbraco.Core/Services/IEntityService.cs @@ -186,6 +186,22 @@ public interface IEntityService IQuery? filter = null, Ordering? ordering = null); + /// + /// Gets children of an entity. + /// + IEnumerable GetPagedTrashedChildren( + int id, + UmbracoObjectTypes objectType, + long pageIndex, + int pageSize, + out long totalRecords, + IQuery? filter = null, + Ordering? ordering = null) + { + totalRecords = 0; + return Array.Empty(); + } + /// /// Gets descendants of an entity. /// diff --git a/src/Umbraco.Core/Services/ILocalizationService.cs b/src/Umbraco.Core/Services/ILocalizationService.cs index 7a1b1b6fd1..f96ed9af0b 100644 --- a/src/Umbraco.Core/Services/ILocalizationService.cs +++ b/src/Umbraco.Core/Services/ILocalizationService.cs @@ -49,6 +49,15 @@ public interface ILocalizationService : IService /// IDictionaryItem? GetDictionaryItemById(Guid id); + /// + /// Gets a collection of by their ids + /// + /// Ids of the + /// + /// A collection of + /// + IEnumerable GetDictionaryItemsByIds(params Guid[] ids) => Array.Empty(); + /// /// Gets a by its key /// @@ -58,6 +67,15 @@ public interface ILocalizationService : IService /// IDictionaryItem? GetDictionaryItemByKey(string key); + /// + /// Gets a collection of by their keys + /// + /// Keys of the + /// + /// A collection of + /// + IEnumerable GetDictionaryItemsByKeys(params string[] keys) => Array.Empty(); + /// /// Gets a list of children for a /// diff --git a/src/Umbraco.Core/Services/LocalizationService.cs b/src/Umbraco.Core/Services/LocalizationService.cs index 3046ddafb5..824aed8cf5 100644 --- a/src/Umbraco.Core/Services/LocalizationService.cs +++ b/src/Umbraco.Core/Services/LocalizationService.cs @@ -170,6 +170,29 @@ internal class LocalizationService : RepositoryService, ILocalizationService } } + /// + /// Gets a collection by their ids + /// + /// Ids of the + /// + /// A collection of + /// + public IEnumerable GetDictionaryItemsByIds(params Guid[] ids) + { + using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true)) + { + IEnumerable items = _dictionaryRepository.GetMany(ids).ToArray(); + + // ensure the lazy Language callback is assigned + foreach (IDictionaryItem item in items) + { + EnsureDictionaryItemLanguageCallback(item); + } + + return items; + } + } + /// /// Gets a by its key /// @@ -189,6 +212,28 @@ internal class LocalizationService : RepositoryService, ILocalizationService } } + /// + /// Gets a collection of by their keys + /// + /// Keys of the + /// + /// A collection of + /// + public IEnumerable GetDictionaryItemsByKeys(params string[] keys) + { + using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true)) + { + IEnumerable items = _dictionaryRepository.GetManyByKeys(keys).ToArray(); + + // ensure the lazy Language callback is assigned + foreach (IDictionaryItem item in items) + { + EnsureDictionaryItemLanguageCallback(item); + } + return items; + } + } + /// /// Gets a list of children for a /// diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DictionaryRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DictionaryRepository.cs index 6cd2d8989b..909c9cfec2 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DictionaryRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/DictionaryRepository.cs @@ -33,6 +33,13 @@ internal class DictionaryRepository : EntityRepositoryBase return uniqueIdRepo.Get(uniqueId); } + public IEnumerable GetMany(params Guid[] uniqueIds) + { + var uniqueIdRepo = new DictionaryByUniqueIdRepository(this, ScopeAccessor, AppCaches, + _loggerFactory.CreateLogger()); + return uniqueIdRepo.GetMany(uniqueIds); + } + public IDictionaryItem? Get(string key) { var keyRepo = new DictionaryByKeyRepository(this, ScopeAccessor, AppCaches, @@ -40,6 +47,13 @@ internal class DictionaryRepository : EntityRepositoryBase return keyRepo.Get(key); } + public IEnumerable GetManyByKeys(string[] keys) + { + var keyRepo = new DictionaryByKeyRepository(this, ScopeAccessor, AppCaches, + _loggerFactory.CreateLogger()); + return keyRepo.GetMany(keys); + } + public Dictionary GetDictionaryItemKeyMap() { var columns = new[] { "key", "id" }.Select(x => (object)SqlSyntax.GetQuotedColumnName(x)).ToArray(); diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/EntityRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/EntityRepository.cs index c904b5b440..9841ae9d0c 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/EntityRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/EntityRepository.cs @@ -680,7 +680,8 @@ internal class EntityRepository : RepositoryBase, IEntityRepositoryExtended private EntitySlim BuildEntity(BaseDto dto) { - if (dto.NodeObjectType == Constants.ObjectTypes.Document) + if (dto.NodeObjectType == Constants.ObjectTypes.Document + || dto.NodeObjectType == Constants.ObjectTypes.DocumentBlueprint) { return BuildDocumentEntity(dto); } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/SimpleGetRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/SimpleGetRepository.cs index 1fe1f1e82a..ad509afd5a 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/SimpleGetRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/SimpleGetRepository.cs @@ -55,7 +55,7 @@ internal abstract class SimpleGetRepository : EntityReposito protected override IEnumerable PerformGetAll(params TId[]? ids) { - Sql sql = Sql().From(); + Sql sql = Sql().From(); if (ids?.Any() ?? false) { diff --git a/src/Umbraco.New.Cms.Web.Common/Routing/VersionedApiBackOfficeRouteAttribute.cs b/src/Umbraco.New.Cms.Web.Common/Routing/VersionedApiBackOfficeRouteAttribute.cs new file mode 100644 index 0000000000..a7abacdb03 --- /dev/null +++ b/src/Umbraco.New.Cms.Web.Common/Routing/VersionedApiBackOfficeRouteAttribute.cs @@ -0,0 +1,9 @@ +namespace Umbraco.New.Cms.Web.Common.Routing; + +public class VersionedApiBackOfficeRouteAttribute : BackOfficeRouteAttribute +{ + public VersionedApiBackOfficeRouteAttribute(string template) + : base($"api/v{{version:apiVersion}}/{template.TrimStart('/')}") + { + } +} diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/DictionaryRepositoryTest.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/DictionaryRepositoryTest.cs index 4d71da076b..628fa419c2 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/DictionaryRepositoryTest.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Persistence/Repositories/DictionaryRepositoryTest.cs @@ -158,6 +158,26 @@ public class DictionaryRepositoryTest : UmbracoIntegrationTest } } + [Test] + public void Can_Perform_GetAll_ByKeys_On_DictionaryRepository() + { + // Arrange + var provider = ScopeProvider; + using (provider.CreateScope()) + { + var repository = CreateRepository(); + + // Act + var dictionaryItems = repository.GetManyByKeys().ToArray(); + + // Assert + Assert.That(dictionaryItems, Is.Not.Null); + Assert.That(dictionaryItems.Any(), Is.True); + Assert.That(dictionaryItems.Any(x => x == null), Is.False); + Assert.That(dictionaryItems.Count(), Is.EqualTo(2)); + } + } + [Test] public void Can_Perform_GetAll_With_Params_On_DictionaryRepository() { @@ -178,6 +198,26 @@ public class DictionaryRepositoryTest : UmbracoIntegrationTest } } + [Test] + public void Can_Perform_GetAll_ByKeys_With_Params_On_DictionaryRepository() + { + // Arrange + var provider = ScopeProvider; + using (provider.CreateScope()) + { + var repository = CreateRepository(); + + // Act + var dictionaryItems = repository.GetManyByKeys("Read More", "Article").ToArray(); + + // Assert + Assert.That(dictionaryItems, Is.Not.Null); + Assert.That(dictionaryItems.Any(), Is.True); + Assert.That(dictionaryItems.Any(x => x == null), Is.False); + Assert.That(dictionaryItems.Count(), Is.EqualTo(2)); + } + } + [Test] public void Can_Perform_GetByQuery_On_DictionaryRepository() { diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/EntityServiceTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/EntityServiceTests.cs index 642cafcd49..3003d28f4c 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/EntityServiceTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/EntityServiceTests.cs @@ -273,6 +273,49 @@ public class EntityServiceTests : UmbracoIntegrationTest } } + [Test] + public void EntityService_Can_Get_Paged_Trashed_Content_Children() + { + var contentType = ContentTypeService.Get("umbTextpage"); + + var root = ContentBuilder.CreateSimpleContent(contentType); + ContentService.Save(root); + var toDelete = new List(); + for (var i = 0; i < 10; i++) + { + var c1 = ContentBuilder.CreateSimpleContent(contentType, Guid.NewGuid().ToString(), root); + ContentService.Save(c1); + + if (i % 2 == 0) + { + toDelete.Add(c1); + } + + for (var j = 0; j < 5; j++) + { + var c2 = ContentBuilder.CreateSimpleContent(contentType, Guid.NewGuid().ToString(), c1); + ContentService.Save(c2); + } + } + + foreach (var content in toDelete) + { + ContentService.MoveToRecycleBin(content); + } + + // get paged entities at recycle bin root + var entities = EntityService + .GetPagedTrashedChildren(Constants.System.RecycleBinContent, UmbracoObjectTypes.Document, 0, 1000, out var total) + .Select(x => x.Id) + .ToArray(); + + Assert.True(total > 0); + foreach (var c in toDelete) + { + Assert.IsTrue(entities.Contains(c.Id)); + } + } + [Test] public void EntityService_Can_Get_Paged_Content_Descendants_With_Search() { @@ -452,6 +495,50 @@ public class EntityServiceTests : UmbracoIntegrationTest } } + [Test] + public void EntityService_Can_Get_Paged_Trashed_Media_Children() + { + var folderType = MediaTypeService.Get(1031); + var imageMediaType = MediaTypeService.Get(1032); + + var root = MediaBuilder.CreateMediaFolder(folderType, -1); + MediaService.Save(root); + var toDelete = new List(); + for (var i = 0; i < 10; i++) + { + var c1 = MediaBuilder.CreateMediaImage(imageMediaType, root.Id); + MediaService.Save(c1); + + if (i % 2 == 0) + { + toDelete.Add(c1); + } + + for (var j = 0; j < 5; j++) + { + var c2 = MediaBuilder.CreateMediaImage(imageMediaType, c1.Id); + MediaService.Save(c2); + } + } + + foreach (var content in toDelete) + { + MediaService.MoveToRecycleBin(content); + } + + // get paged entities at recycle bin root + var entities = EntityService + .GetPagedTrashedChildren(Constants.System.RecycleBinMedia, UmbracoObjectTypes.Media, 0, 1000, out var total) + .Select(x => x.Id) + .ToArray(); + + Assert.True(total > 0); + foreach (var media in toDelete) + { + Assert.IsTrue(entities.Contains(media.Id)); + } + } + [Test] public void EntityService_Can_Get_Paged_Media_Descendants_With_Search() { diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/LocalizationServiceTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/LocalizationServiceTests.cs index 4621b9b63a..2ca4af5fae 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/LocalizationServiceTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/LocalizationServiceTests.cs @@ -82,6 +82,15 @@ public class LocalizationServiceTests : UmbracoIntegrationTest Assert.NotNull(childItem); } + [Test] + public void Can_Get_Dictionary_Items_By_Guid_Ids() + { + var items = LocalizationService.GetDictionaryItemsByIds(_parentItemGuidId, _childItemGuidId); + Assert.AreEqual(2, items.Count()); + Assert.NotNull(items.FirstOrDefault(i => i.Key == _parentItemGuidId)); + Assert.NotNull(items.FirstOrDefault(i => i.Key == _childItemGuidId)); + } + [Test] public void Can_Get_Dictionary_Item_By_Key() { @@ -92,6 +101,15 @@ public class LocalizationServiceTests : UmbracoIntegrationTest Assert.NotNull(childItem); } + [Test] + public void Can_Get_Dictionary_Items_By_Keys() + { + var items = LocalizationService.GetDictionaryItemsByKeys("Parent", "Child"); + Assert.AreEqual(2, items.Count()); + Assert.NotNull(items.FirstOrDefault(i => i.ItemKey == "Parent")); + Assert.NotNull(items.FirstOrDefault(i => i.ItemKey == "Child")); + } + [Test] public void Can_Get_Dictionary_Item_Children() {