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 <nikolajlauridsen@protonmail.ch>

* 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 <nikolajlauridsen@protonmail.ch>
This commit is contained in:
Kenn Jacobsen
2022-09-28 13:37:59 +02:00
committed by GitHub
parent 3752f51625
commit 134b193c74
103 changed files with 5976 additions and 255 deletions

View File

@@ -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<FolderTreeItemViewModel>), StatusCodes.Status200OK)]
public async Task<ActionResult<PagedViewModel<FolderTreeItemViewModel>>> Children(Guid parentKey, int skip = 0, int take = 100, bool foldersOnly = false)
{
RenderFoldersOnly(foldersOnly);
return await GetChildren(parentKey, skip, take);
}
}

View File

@@ -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<FolderTreeItemViewModel>
{
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();
}
}

View File

@@ -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<FolderTreeItemViewModel>), StatusCodes.Status200OK)]
public async Task<ActionResult<IEnumerable<FolderTreeItemViewModel>>> Items([FromQuery(Name = "key")] Guid[] keys)
=> await GetItems(keys);
}

View File

@@ -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<FolderTreeItemViewModel>), StatusCodes.Status200OK)]
public async Task<ActionResult<PagedViewModel<FolderTreeItemViewModel>>> Root(int skip = 0, int take = 100, bool foldersOnly = false)
{
RenderFoldersOnly(foldersOnly);
return await GetRoot(skip, take);
}
}

View File

@@ -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<EntityTreeItemViewModel>), StatusCodes.Status200OK)]
public async Task<ActionResult<PagedViewModel<EntityTreeItemViewModel>>> 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<EntityTreeItemViewModel> result = PagedViewModel(viewModels, totalItems);
return await Task.FromResult(Ok(result));
}
}

View File

@@ -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<EntityTreeItemViewModel>
{
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<IDictionaryItem> allDictionaryItems, out long totalItems)
{
IDictionaryItem[] allDictionaryItemsAsArray = allDictionaryItems.ToArray();
totalItems = allDictionaryItemsAsArray.Length;
return allDictionaryItemsAsArray
.OrderBy(item => item.ItemKey)
.Skip((int)pageNumber * pageSize)
.Take(pageSize)
.ToArray();
}
}

View File

@@ -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<FolderTreeItemViewModel>), StatusCodes.Status200OK)]
public async Task<ActionResult<IEnumerable<FolderTreeItemViewModel>>> Items([FromQuery(Name = "key")] Guid[] keys)
{
IDictionaryItem[] dictionaryItems = LocalizationService.GetDictionaryItemsByIds(keys).ToArray();
EntityTreeItemViewModel[] viewModels = MapTreeItemViewModels(null, dictionaryItems);
return await Task.FromResult(Ok(viewModels));
}
}

View File

@@ -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<EntityTreeItemViewModel>), StatusCodes.Status200OK)]
public async Task<ActionResult<PagedViewModel<EntityTreeItemViewModel>>> 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<EntityTreeItemViewModel> result = PagedViewModel(viewModels, totalItems);
return await Task.FromResult(Ok(result));
}
}

View File

@@ -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<RecycleBinItemViewModel>), StatusCodes.Status200OK)]
public async Task<ActionResult<PagedViewModel<RecycleBinItemViewModel>>> Children(Guid parentKey, int skip = 0, int take = 100)
=> await GetChildren(parentKey, skip, take);
}

View File

@@ -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<RecycleBinItemViewModel>
{
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;
}
}

View File

@@ -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<RecycleBinItemViewModel>), StatusCodes.Status200OK)]
public async Task<ActionResult<PagedViewModel<RecycleBinItemViewModel>>> Root(int skip = 0, int take = 100)
=> await GetRoot(skip, take);
}

View File

@@ -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<DocumentTreeItemViewModel>), StatusCodes.Status200OK)]
public async Task<ActionResult<PagedViewModel<DocumentTreeItemViewModel>>> 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);
}
}

View File

@@ -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<DocumentTreeItemViewModel>
{
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<string>();
// 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<int>();
//
// protected override string[] GetUserStartNodePaths()
// => _backofficeSecurityAccessor
// .BackOfficeSecurity?
// .CurrentUser?
// .GetContentStartNodePaths(EntityService, _appCaches)
// ?? Array.Empty<string>();
}

View File

@@ -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<DocumentTreeItemViewModel>), StatusCodes.Status200OK)]
public async Task<ActionResult<IEnumerable<DocumentTreeItemViewModel>>> Items([FromQuery(Name = "key")] Guid[] keys, Guid? dataTypeKey = null, string? culture = null)
{
IgnoreUserStartNodesForDataType(dataTypeKey);
RenderForClientCulture(culture);
return await GetItems(keys);
}
}

View File

@@ -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<DocumentTreeItemViewModel>), StatusCodes.Status200OK)]
public async Task<ActionResult<PagedViewModel<DocumentTreeItemViewModel>>> Root(int skip = 0, int take = 100, Guid? dataTypeKey = null, string? culture = null)
{
IgnoreUserStartNodesForDataType(dataTypeKey);
RenderForClientCulture(culture);
return await GetRoot(skip, take);
}
}

View File

@@ -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<DocumentBlueprintTreeItemViewModel>
{
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<IDocumentEntitySlim>()
.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();
}
}

View File

@@ -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<DocumentBlueprintTreeItemViewModel>), StatusCodes.Status200OK)]
public async Task<ActionResult<IEnumerable<DocumentBlueprintTreeItemViewModel>>> Items([FromQuery(Name = "key")] Guid[] keys)
=> await GetItems(keys);
}

View File

@@ -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<DocumentBlueprintTreeItemViewModel>), StatusCodes.Status200OK)]
public async Task<ActionResult<PagedViewModel<DocumentBlueprintTreeItemViewModel>>> Root(int skip = 0, int take = 100)
=> await GetRoot(skip, take);
}

View File

@@ -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<DocumentTypeTreeItemViewModel>), StatusCodes.Status200OK)]
public async Task<ActionResult<PagedViewModel<DocumentTypeTreeItemViewModel>>> Children(Guid parentKey, int skip = 0, int take = 100, bool foldersOnly = false)
{
RenderFoldersOnly(foldersOnly);
return await GetChildren(parentKey, skip, take);
}
}

View File

@@ -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<DocumentTypeTreeItemViewModel>
{
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();
}
}

View File

@@ -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<DocumentTypeTreeItemViewModel>), StatusCodes.Status200OK)]
public async Task<ActionResult<IEnumerable<DocumentTypeTreeItemViewModel>>> Items([FromQuery(Name = "key")] Guid[] keys)
=> await GetItems(keys);
}

View File

@@ -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<DocumentTypeTreeItemViewModel>), StatusCodes.Status200OK)]
public async Task<ActionResult<PagedViewModel<DocumentTypeTreeItemViewModel>>> Root(int skip = 0, int take = 100, bool foldersOnly = false)
{
RenderFoldersOnly(foldersOnly);
return await GetRoot(skip, take);
}
}

View File

@@ -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

View File

@@ -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<RecycleBinItemViewModel>), StatusCodes.Status200OK)]
public async Task<ActionResult<PagedViewModel<RecycleBinItemViewModel>>> Children(Guid parentKey, int skip = 0, int take = 100)
=> await GetChildren(parentKey, skip, take);
}

View File

@@ -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<RecycleBinItemViewModel>
{
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;
}
}

View File

@@ -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<RecycleBinItemViewModel>), StatusCodes.Status200OK)]
public async Task<ActionResult<PagedViewModel<RecycleBinItemViewModel>>> Root(int skip = 0, int take = 100)
=> await GetRoot(skip, take);
}

View File

@@ -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<ContentTreeItemViewModel>), StatusCodes.Status200OK)]
public async Task<ActionResult<PagedViewModel<ContentTreeItemViewModel>>> Children(Guid parentKey, int skip = 0, int take = 100, Guid? dataTypeKey = null)
{
IgnoreUserStartNodesForDataType(dataTypeKey);
return await GetChildren(parentKey, skip, take);
}
}

View File

@@ -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<ContentTreeItemViewModel>), StatusCodes.Status200OK)]
public async Task<ActionResult<IEnumerable<ContentTreeItemViewModel>>> Items([FromQuery(Name = "key")] Guid[] keys, Guid? dataTypeKey = null)
{
IgnoreUserStartNodesForDataType(dataTypeKey);
return await GetItems(keys);
}
}

View File

@@ -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<ContentTreeItemViewModel>
{
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<string>();
// 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<int>();
//
// protected override string[] GetUserStartNodePaths()
// => _backofficeSecurityAccessor
// .BackOfficeSecurity?
// .CurrentUser?
// .GetMediaStartNodePaths(EntityService, _appCaches)
// ?? Array.Empty<string>();
}

View File

@@ -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<ContentTreeItemViewModel>), StatusCodes.Status200OK)]
public async Task<ActionResult<PagedViewModel<ContentTreeItemViewModel>>> Root(int skip = 0, int take = 100, Guid? dataTypeKey = null)
{
IgnoreUserStartNodesForDataType(dataTypeKey);
return await GetRoot(skip, take);
}
}

View File

@@ -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<FolderTreeItemViewModel>), StatusCodes.Status200OK)]
public async Task<ActionResult<PagedViewModel<FolderTreeItemViewModel>>> Children(Guid parentKey, int skip = 0, int take = 100, bool foldersOnly = false)
{
RenderFoldersOnly(foldersOnly);
return await GetChildren(parentKey, skip, take);
}
}

View File

@@ -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<FolderTreeItemViewModel>), StatusCodes.Status200OK)]
public async Task<ActionResult<IEnumerable<FolderTreeItemViewModel>>> Items([FromQuery(Name = "key")] Guid[] keys)
=> await GetItems(keys);
}

View File

@@ -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<FolderTreeItemViewModel>
{
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();
}
}

View File

@@ -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<FolderTreeItemViewModel>), StatusCodes.Status200OK)]
public async Task<ActionResult<PagedViewModel<FolderTreeItemViewModel>>> Root(int skip = 0, int take = 100, bool foldersOnly = false)
{
RenderFoldersOnly(foldersOnly);
return await GetRoot(skip, take);
}
}

View File

@@ -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<EntityTreeItemViewModel>), StatusCodes.Status200OK)]
public async Task<ActionResult<IEnumerable<EntityTreeItemViewModel>>> Items([FromQuery(Name = "key")] Guid[] keys)
=> await GetItems(keys);
}

View File

@@ -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<EntityTreeItemViewModel>
{
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;
}
}

View File

@@ -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<EntityTreeItemViewModel>), StatusCodes.Status200OK)]
public async Task<ActionResult<PagedViewModel<EntityTreeItemViewModel>>> Root(int skip = 0, int take = 100)
=> await GetRoot(skip, take);
}

View File

@@ -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<EntityTreeItemViewModel>), StatusCodes.Status200OK)]
public async Task<ActionResult<IEnumerable<EntityTreeItemViewModel>>> Items([FromQuery(Name = "key")] Guid[] keys)
=> await GetItems(keys);
}

View File

@@ -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<EntityTreeItemViewModel>
{
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;
}
}

View File

@@ -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<EntityTreeItemViewModel>), StatusCodes.Status200OK)]
public async Task<ActionResult<PagedViewModel<EntityTreeItemViewModel>>> Root(int skip = 0, int take = 100)
=> await GetRoot(skip, take);
}

View File

@@ -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<FileSystemTreeItemViewModel>), StatusCodes.Status200OK)]
public async Task<ActionResult<PagedViewModel<FileSystemTreeItemViewModel>>> Children(string path, int skip = 0, int take = 100)
=> await GetChildren(path, skip, take);
}

View File

@@ -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<FileSystemTreeItemViewModel>), StatusCodes.Status200OK)]
public async Task<ActionResult<IEnumerable<FileSystemTreeItemViewModel>>> Items([FromQuery(Name = "path")] string[] paths)
=> await GetItems(paths);
}

View File

@@ -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;
}

View File

@@ -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<FileSystemTreeItemViewModel>), StatusCodes.Status200OK)]
public async Task<ActionResult<PagedViewModel<FileSystemTreeItemViewModel>>> Root(int skip = 0, int take = 100)
=> await GetRoot(skip, take);
}

View File

@@ -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<TItem> : 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<ActionResult<PagedViewModel<TItem>>> 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<TItem> result = PagedViewModel(treeItemViewModels, totalItems);
return await Task.FromResult(Ok(result));
}
protected async Task<ActionResult<PagedViewModel<TItem>>> 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<TItem> 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>();
}
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<TItem> PagedViewModel(IEnumerable<TItem> treeItemViewModels, long totalItems)
=> new() { Total = totalItems, Items = treeItemViewModels };
}

View File

@@ -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<FolderTreeItemViewModel>), StatusCodes.Status200OK)]
public async Task<ActionResult<IEnumerable<FolderTreeItemViewModel>>> 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));
}
}

View File

@@ -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<EntityTreeItemViewModel>
{
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();
}

View File

@@ -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<EntityTreeItemViewModel>), StatusCodes.Status200OK)]
public async Task<ActionResult<PagedViewModel<EntityTreeItemViewModel>>> 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<EntityTreeItemViewModel> result = PagedViewModel(viewModels, allRelationTypes.Length);
return await Task.FromResult(Ok(result));
}
}

View File

@@ -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<FileSystemTreeItemViewModel>), StatusCodes.Status200OK)]
public async Task<ActionResult<PagedViewModel<FileSystemTreeItemViewModel>>> Children(string path, int skip = 0, int take = 100)
=> await GetChildren(path, skip, take);
}

View File

@@ -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<FileSystemTreeItemViewModel>), StatusCodes.Status200OK)]
public async Task<ActionResult<IEnumerable<FileSystemTreeItemViewModel>>> Items([FromQuery(Name = "path")] string[] paths)
=> await GetItems(paths);
}

View File

@@ -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<FileSystemTreeItemViewModel>), StatusCodes.Status200OK)]
public async Task<ActionResult<PagedViewModel<FileSystemTreeItemViewModel>>> Root(int skip = 0, int take = 100)
=> await GetRoot(skip, take);
}

View File

@@ -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;
}

View File

@@ -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
{

View File

@@ -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<FileSystemTreeItemViewModel>), StatusCodes.Status200OK)]
public async Task<ActionResult<PagedViewModel<FileSystemTreeItemViewModel>>> Children(string path, int skip = 0, int take = 100)
=> await GetChildren(path, skip, take);
}

View File

@@ -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<FileSystemTreeItemViewModel>), StatusCodes.Status200OK)]
public async Task<ActionResult<IEnumerable<FileSystemTreeItemViewModel>>> Items([FromQuery(Name = "path")] string[] paths)
=> await GetItems(paths);
}

View File

@@ -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<FileSystemTreeItemViewModel>), StatusCodes.Status200OK)]
public async Task<ActionResult<PagedViewModel<FileSystemTreeItemViewModel>>> Root(int skip = 0, int take = 100)
=> await GetRoot(skip, take);
}

View File

@@ -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<string>();
protected override string[] GetFiles(string path)
=> IsTreeRootPath(path) || IsAllowedPath(path) == false
? Array.Empty<string>()
: 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}/"));
}

View File

@@ -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<FileSystemTreeItemViewModel>), StatusCodes.Status200OK)]
public async Task<ActionResult<PagedViewModel<FileSystemTreeItemViewModel>>> Children(string path, int skip = 0, int take = 100)
=> await GetChildren(path, skip, take);
}

View File

@@ -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<FileSystemTreeItemViewModel>), StatusCodes.Status200OK)]
public async Task<ActionResult<IEnumerable<FileSystemTreeItemViewModel>>> Items([FromQuery(Name = "path")] string[] paths)
=> await GetItems(paths);
}

View File

@@ -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<FileSystemTreeItemViewModel>), StatusCodes.Status200OK)]
public async Task<ActionResult<PagedViewModel<FileSystemTreeItemViewModel>>> Root(int skip = 0, int take = 100)
=> await GetRoot(skip, take);
}

View File

@@ -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;
}

View File

@@ -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<EntityTreeItemViewModel>), StatusCodes.Status200OK)]
public async Task<ActionResult<PagedViewModel<EntityTreeItemViewModel>>> Children(Guid parentKey, int skip = 0, int take = 100)
=> await GetChildren(parentKey, skip, take);
}

View File

@@ -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<EntityTreeItemViewModel>), StatusCodes.Status200OK)]
public async Task<ActionResult<IEnumerable<EntityTreeItemViewModel>>> Items([FromQuery(Name = "key")] Guid[] keys)
=> await GetItems(keys);
}

View File

@@ -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<EntityTreeItemViewModel>), StatusCodes.Status200OK)]
public async Task<ActionResult<PagedViewModel<EntityTreeItemViewModel>>> Root(int skip = 0, int take = 100)
=> await GetRoot(skip, take);
}

View File

@@ -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<EntityTreeItemViewModel>
{
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;
}
}

View File

@@ -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<TItem> : 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<ActionResult<PagedViewModel<TItem>>> 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<TItem> result = PagedViewModel(treeItemViewModels, totalItems);
return await Task.FromResult(Ok(result));
}
protected async Task<ActionResult<PagedViewModel<TItem>>> 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<TItem> result = PagedViewModel(treeItemViewModels, totalItems);
return await Task.FromResult(Ok(result));
}
protected async Task<ActionResult<IEnumerable<TItem>>> GetItems(Guid[] keys)
{
if (keys.IsCollectionEmpty())
{
return await Task.FromResult(Ok(PagedViewModel(Array.Empty<TItem>(), 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<int> 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>();
}
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<TItem> PagedViewModel(IEnumerable<TItem> treeItemViewModels, long totalItems)
=> new() { Total = totalItems, Items = treeItemViewModels };
}

View File

@@ -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<ActionResult<PagedViewModel<FileSystemTreeItemViewModel>>> 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<FileSystemTreeItemViewModel> result = PagedViewModel(viewModels, totalItems);
return await Task.FromResult(Ok(result));
}
protected async Task<ActionResult<PagedViewModel<FileSystemTreeItemViewModel>>> 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<FileSystemTreeItemViewModel> result = PagedViewModel(viewModels, totalItems);
return await Task.FromResult(Ok(result));
}
protected async Task<ActionResult<IEnumerable<FileSystemTreeItemViewModel>>> 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<FileSystemTreeItemViewModel> PagedViewModel(IEnumerable<FileSystemTreeItemViewModel> 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
};
}

View File

@@ -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<TItem> : EntityTreeControllerBase<TItem>
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<int> 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<IEntitySlim>();
}
}
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>();
IEntitySlim[] itemEntities = _foldersOnly
? Array.Empty<IEntitySlim>()
: EntityService.GetPagedChildren(
parentId,
ItemObjectType,
pageNumber,
pageSize,
out totalItems,
ordering: ItemOrdering)
.ToArray();
return folderEntities.Union(itemEntities).ToArray();
}
}

View File

@@ -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<TItem> : EntityTreeControllerBase<TItem>
where TItem : ContentTreeItemViewModel, new()
{
private readonly IUserStartNodeEntitiesService _userStartNodeEntitiesService;
private readonly IDataTypeService _dataTypeService;
private int[]? _userStartNodeIds;
private string[]? _userStartNodePaths;
private Dictionary<Guid, bool> _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<IEnumerable<UserAccessEntity>> 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;
}
}

View File

@@ -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
{

View File

@@ -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<UpgradeStepCollectionBuilder>();
internal static IUmbracoBuilder AddTrees(this IUmbracoBuilder builder)
{
builder.Services.AddTransient<IUserStartNodeEntitiesService, UserStartNodeEntitiesService>();
return builder;
}
}

View File

@@ -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<AppCaches>();
IEntityService entityService = context.HttpContext.RequestServices.GetRequiredService<IEntityService>();
return user.CalculateContentStartNodeIds(entityService, appCaches) ?? Array.Empty<int>();
}
}

View File

@@ -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<AppCaches>();
IEntityService entityService = context.HttpContext.RequestServices.GetRequiredService<IEntityService>();
return user.CalculateMediaStartNodeIds(entityService, appCaches) ?? Array.Empty<int>();
}
}

View File

@@ -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<IBackOfficeSecurityAccessor>();
IUser? user = backOfficeSecurityAccessor.BackOfficeSecurity?.CurrentUser;
var startNodeIds = user != null ? GetUserStartNodeIds(user, context) : Array.Empty<int>();
// 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);
}

View File

@@ -36,7 +36,8 @@ public class ManagementApiComposer : IComposer
builder
.AddNewInstaller()
.AddUpgrader()
.AddExamineManagement();
.AddExamineManagement()
.AddTrees();
services.AddApiVersioning(options =>
{

View File

@@ -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; }
}

File diff suppressed because it is too large Load Diff

View File

@@ -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
{
/// <summary>
/// Calculates the applicable root entities for a given object type for users without root access.
/// </summary>
/// <param name="umbracoObjectType">The object type.</param>
/// <param name="userStartNodeIds">The calculated start node IDs for the user.</param>
/// <returns>A list of root entities for the user.</returns>
/// <remarks>
/// 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.
/// </remarks>
IEnumerable<UserAccessEntity> RootUserAccessEntities(UmbracoObjectTypes umbracoObjectType, int[] userStartNodeIds);
/// <summary>
/// Calculates the applicable child entities from a list of candidate child entities for users without root access.
/// </summary>
/// <param name="candidateChildren">The candidate child entities to filter (i.e. entities fetched with <see cref="EntityService.GetPagedChildren"/>).</param>
/// <param name="userStartNodePaths">The calculated start node paths for the user.</param>
/// <returns>A list of child entities applicable entities for the user.</returns>
/// <remarks>
/// 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.
/// </remarks>
IEnumerable<UserAccessEntity> ChildUserAccessEntities(IEnumerable<IEntitySlim> candidateChildren, string[] userStartNodePaths);
/// <summary>
/// Calculates the access level of a collection of entities for users without root access.
/// </summary>
/// <param name="entities">The entities.</param>
/// <param name="userStartNodePaths">The calculated start node paths for the user.</param>
/// <returns> The access level for each entity.</returns>
IEnumerable<UserAccessEntity> UserAccessEntities(IEnumerable<IEntitySlim> entities, string[] userStartNodePaths);
}

View File

@@ -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;
/// <inheritdoc />
public IEnumerable<UserAccessEntity> 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<IEntitySlim>();
return allowedTopmostEntities
.Select(entity => new UserAccessEntity(entity, true))
.Union(
nonAllowedTopmostEntities
.Select(entity => new UserAccessEntity(entity, false)))
.ToArray();
}
/// <inheritdoc />
public IEnumerable<UserAccessEntity> ChildUserAccessEntities(IEnumerable<IEntitySlim> 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();
/// <inheritdoc />
public IEnumerable<UserAccessEntity> UserAccessEntities(IEnumerable<IEntitySlim> 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));
}

View File

@@ -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;
}
}

View File

@@ -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; }
}

View File

@@ -0,0 +1,6 @@
namespace Umbraco.Cms.ManagementApi.ViewModels.Tree;
public class ContentTreeItemViewModel : EntityTreeItemViewModel
{
public bool NoAccess { get; set; }
}

View File

@@ -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;
}

View File

@@ -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; }
}

View File

@@ -0,0 +1,6 @@
namespace Umbraco.Cms.ManagementApi.ViewModels.Tree;
public class DocumentTypeTreeItemViewModel : FolderTreeItemViewModel
{
public bool IsElement { get; set; }
}

View File

@@ -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; }
}

View File

@@ -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; }
}

View File

@@ -0,0 +1,6 @@
namespace Umbraco.Cms.ManagementApi.ViewModels.Tree;
public class FolderTreeItemViewModel : EntityTreeItemViewModel
{
public bool IsFolder { get; set; }
}

View File

@@ -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; }
}

View File

@@ -119,11 +119,31 @@ public static partial class Constants
/// </summary>
public const string Packages = "icon-box";
/// <summary>
/// System property editor icon
/// </summary>
public const string PartialView = "icon-article";
/// <summary>
/// System property editor icon
/// </summary>
public const string PropertyEditor = "icon-autofill";
/// <summary>
/// Relation type icon
/// </summary>
public const string RelationType = "icon-trafic";
/// <summary>
/// Script type icon
/// </summary>
public const string Script = "icon-script";
/// <summary>
/// Stylesheet type icon
/// </summary>
public const string Stylesheet = "icon-brackets";
/// <summary>
/// System member icon
/// </summary>

View File

@@ -54,7 +54,7 @@ public enum UmbracoObjectTypes
/// <summary>
/// Member Group
/// </summary>
[UmbracoObjectType(Constants.ObjectTypes.Strings.MemberGroup)]
[UmbracoObjectType(Constants.ObjectTypes.Strings.MemberGroup, typeof(IMemberGroup))]
[FriendlyName("Member Group")]
[UmbracoUdiType(Constants.UdiEntityType.MemberGroup)]
MemberGroup,

View File

@@ -6,6 +6,10 @@ public interface IDictionaryRepository : IReadWriteQueryRepository<int, IDiction
{
IDictionaryItem? Get(Guid uniqueId);
IEnumerable<IDictionaryItem> GetMany(params Guid[] uniqueIds) => Array.Empty<IDictionaryItem>();
IEnumerable<IDictionaryItem> GetManyByKeys(params string[] keys) => Array.Empty<IDictionaryItem>();
IDictionaryItem? Get(string key);
IEnumerable<IDictionaryItem> GetDictionaryItemDescendants(Guid? parentId);

View File

@@ -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<IUmbracoEntity>? filter = null,
Ordering? ordering = null)
{
using (ScopeProvider.CreateCoreScope(autoComplete: true))
{
IQuery<IUmbracoEntity> query = Query<IUmbracoEntity>().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);
}
}
/// <inheritdoc />
public IEnumerable<IEntitySlim> GetPagedTrashedChildren(
int id,
UmbracoObjectTypes objectType,
long pageIndex,
int pageSize,
out long totalRecords,
IQuery<IUmbracoEntity>? filter = null,
Ordering? ordering = null)
=> GetPagedChildren(id, objectType, pageIndex, pageSize, true, filter, ordering, out totalRecords);
/// <inheritdoc />
public IEnumerable<IEntitySlim> GetPagedDescendants(
@@ -523,4 +529,23 @@ public class EntityService : RepositoryService, IEntityService
return objType;
}
private IEnumerable<IEntitySlim> GetPagedChildren(
int id,
UmbracoObjectTypes objectType,
long pageIndex,
int pageSize,
bool trashed,
IQuery<IUmbracoEntity>? filter,
Ordering? ordering,
out long totalRecords)
{
using (ScopeProvider.CreateCoreScope(autoComplete: true))
{
IQuery<IUmbracoEntity> query = Query<IUmbracoEntity>().Where(x => x.ParentId == id && x.Trashed == trashed);
return _entityRepository.GetPagedResultsByQuery(query, objectType.GetGuid(), pageIndex, pageSize, out totalRecords, filter, ordering);
}
}
}

View File

@@ -186,6 +186,22 @@ public interface IEntityService
IQuery<IUmbracoEntity>? filter = null,
Ordering? ordering = null);
/// <summary>
/// Gets children of an entity.
/// </summary>
IEnumerable<IEntitySlim> GetPagedTrashedChildren(
int id,
UmbracoObjectTypes objectType,
long pageIndex,
int pageSize,
out long totalRecords,
IQuery<IUmbracoEntity>? filter = null,
Ordering? ordering = null)
{
totalRecords = 0;
return Array.Empty<IEntitySlim>();
}
/// <summary>
/// Gets descendants of an entity.
/// </summary>

View File

@@ -49,6 +49,15 @@ public interface ILocalizationService : IService
/// </returns>
IDictionaryItem? GetDictionaryItemById(Guid id);
/// <summary>
/// Gets a collection of <see cref="IDictionaryItem" /> by their <see cref="Guid" /> ids
/// </summary>
/// <param name="ids">Ids of the <see cref="IDictionaryItem" /></param>
/// <returns>
/// A collection of <see cref="IDictionaryItem" />
/// </returns>
IEnumerable<IDictionaryItem> GetDictionaryItemsByIds(params Guid[] ids) => Array.Empty<IDictionaryItem>();
/// <summary>
/// Gets a <see cref="IDictionaryItem" /> by its key
/// </summary>
@@ -58,6 +67,15 @@ public interface ILocalizationService : IService
/// </returns>
IDictionaryItem? GetDictionaryItemByKey(string key);
/// <summary>
/// Gets a collection of <see cref="IDictionaryItem" /> by their keys
/// </summary>
/// <param name="keys">Keys of the <see cref="IDictionaryItem" /></param>
/// <returns>
/// A collection of <see cref="IDictionaryItem" />
/// </returns>
IEnumerable<IDictionaryItem> GetDictionaryItemsByKeys(params string[] keys) => Array.Empty<IDictionaryItem>();
/// <summary>
/// Gets a list of children for a <see cref="IDictionaryItem" />
/// </summary>

View File

@@ -170,6 +170,29 @@ internal class LocalizationService : RepositoryService, ILocalizationService
}
}
/// <summary>
/// Gets a collection <see cref="IDictionaryItem" /> by their <see cref="Guid" /> ids
/// </summary>
/// <param name="ids">Ids of the <see cref="IDictionaryItem" /></param>
/// <returns>
/// A collection of <see cref="IDictionaryItem" />
/// </returns>
public IEnumerable<IDictionaryItem> GetDictionaryItemsByIds(params Guid[] ids)
{
using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
{
IEnumerable<IDictionaryItem> items = _dictionaryRepository.GetMany(ids).ToArray();
// ensure the lazy Language callback is assigned
foreach (IDictionaryItem item in items)
{
EnsureDictionaryItemLanguageCallback(item);
}
return items;
}
}
/// <summary>
/// Gets a <see cref="IDictionaryItem" /> by its key
/// </summary>
@@ -189,6 +212,28 @@ internal class LocalizationService : RepositoryService, ILocalizationService
}
}
/// <summary>
/// Gets a collection of <see cref="IDictionaryItem" /> by their keys
/// </summary>
/// <param name="keys">Keys of the <see cref="IDictionaryItem" /></param>
/// <returns>
/// A collection of <see cref="IDictionaryItem" />
/// </returns>
public IEnumerable<IDictionaryItem> GetDictionaryItemsByKeys(params string[] keys)
{
using (ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true))
{
IEnumerable<IDictionaryItem> items = _dictionaryRepository.GetManyByKeys(keys).ToArray();
// ensure the lazy Language callback is assigned
foreach (IDictionaryItem item in items)
{
EnsureDictionaryItemLanguageCallback(item);
}
return items;
}
}
/// <summary>
/// Gets a list of children for a <see cref="IDictionaryItem"/>
/// </summary>

View File

@@ -33,6 +33,13 @@ internal class DictionaryRepository : EntityRepositoryBase<int, IDictionaryItem>
return uniqueIdRepo.Get(uniqueId);
}
public IEnumerable<IDictionaryItem> GetMany(params Guid[] uniqueIds)
{
var uniqueIdRepo = new DictionaryByUniqueIdRepository(this, ScopeAccessor, AppCaches,
_loggerFactory.CreateLogger<DictionaryByUniqueIdRepository>());
return uniqueIdRepo.GetMany(uniqueIds);
}
public IDictionaryItem? Get(string key)
{
var keyRepo = new DictionaryByKeyRepository(this, ScopeAccessor, AppCaches,
@@ -40,6 +47,13 @@ internal class DictionaryRepository : EntityRepositoryBase<int, IDictionaryItem>
return keyRepo.Get(key);
}
public IEnumerable<IDictionaryItem> GetManyByKeys(string[] keys)
{
var keyRepo = new DictionaryByKeyRepository(this, ScopeAccessor, AppCaches,
_loggerFactory.CreateLogger<DictionaryByKeyRepository>());
return keyRepo.GetMany(keys);
}
public Dictionary<string, Guid> GetDictionaryItemKeyMap()
{
var columns = new[] { "key", "id" }.Select(x => (object)SqlSyntax.GetQuotedColumnName(x)).ToArray();

View File

@@ -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);
}

View File

@@ -55,7 +55,7 @@ internal abstract class SimpleGetRepository<TId, TEntity, TDto> : EntityReposito
protected override IEnumerable<TEntity> PerformGetAll(params TId[]? ids)
{
Sql<ISqlContext> sql = Sql().From<TEntity>();
Sql<ISqlContext> sql = Sql().From<TDto>();
if (ids?.Any() ?? false)
{

View File

@@ -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('/')}")
{
}
}