From 8213da1b7781c1936b3e25e7dd709846839c1878 Mon Sep 17 00:00:00 2001 From: Nicklas Kramer Date: Tue, 23 Sep 2025 11:17:25 +0200 Subject: [PATCH] Trees: Expanding sibling endpoints to include all entities with trees (#20150) * Adding member types sibling endpoints * Introducing sibling endpoint for Partial Views and logic. * Introducing sibling endpoint for stylesheets * Introducing sibling endpoint for scripts * Introducing FileSystemTreeServiceBase.cs * Introducing interfaces for implementation specific services * Introducing services for specific trees * Modifying controller bases to fit new interface and logic. * Obsoleting old constructors related to PartialView * Obsoleting ctors related to Stylesheets * Obsoleting ctors related to scripts * Adding tests for scriptsTreeService * Adding tests for siblings * Removing unused dependencies * Removing signs and replacing it with flags * Fixing breaking changes by obsoletion * Fixing more breaking changes * Registering missing service * Fixing breaking changes again * Changing name of method GetSiblingsViewModels * Rewritten tests for less bloat and less duplicate code * Expanding tests to include other methods from service * Test refactoring: avoided populating file systems that weren't under test, updated encapsulation, renaming, further re-use. * Management API: Expanding the existing sibling endpoints to support trashed entities (#20154) * Refactoring existing logic to include trashed items * Including tests for trashed entities * Groundwork for trashed siblings * Documents trashed siblings endpoint * Controller for Media trashed items * Expanding tests to include a test for trashed siblings * Code review corrections * Resolving code review --------- Co-authored-by: Andy Butland --- .../SiblingsDocumentRecycleBinController.cs | 24 +++++ .../SiblingsMediaRecycleBinController.cs | 24 +++++ .../Tree/SiblingMemberTypeTreeController.cs | 28 +++++ .../AncestorsPartialViewTreeController.cs | 20 +++- .../Tree/ChildrenPartialViewTreeController.cs | 19 +++- .../Tree/PartialViewTreeControllerBase.cs | 30 +++++- .../Tree/RootPartialViewTreeController.cs | 19 +++- .../Tree/SiblingsPartialViewTreeController.cs | 41 +++++++ .../RecycleBin/RecycleBinControllerBase.cs | 42 ++++++++ .../Tree/AncestorsScriptTreeController.cs | 19 +++- .../Tree/ChildrenScriptTreeController.cs | 19 +++- .../Script/Tree/RootScriptTreeController.cs | 19 +++- .../Script/Tree/ScriptTreeControllerBase.cs | 24 +++++ .../Tree/SiblingsScriptTreeController.cs | 41 +++++++ .../Tree/StaticFileTreeControllerBase.cs | 28 +++-- .../Tree/AncestorsStylesheetTreeController.cs | 19 +++- .../Tree/ChildrenStylesheetTreeController.cs | 20 +++- .../Tree/RootStylesheetTreeController.cs | 19 +++- .../Tree/SiblingsStylesheetTreeController.cs | 41 +++++++ .../Tree/StylesheetTreeControllerBase.cs | 30 +++++- .../Tree/FileSystemTreeControllerBase.cs | 72 ++++++++----- .../TreeBuilderExtensions.cs | 5 + .../FileSystem/FileSystemTreeServiceBase.cs | 102 ++++++++++++++++++ .../FileSystem/IFileSystemTreeService.cs | 18 ++++ .../FileSystem/IPartialViewTreeService.cs | 5 + .../Services/FileSystem/IScriptTreeService.cs | 5 + .../FileSystem/IStyleSheetTreeService.cs | 7 ++ .../FileSystem/PartialViewTreeService.cs | 14 +++ .../Services/FileSystem/ScriptTreeService.cs | 14 +++ .../FileSystem/StyleSheetTreeService.cs | 14 +++ .../Repositories/IEntityRepository.cs | 27 +++++ src/Umbraco.Core/Services/EntityService.cs | 41 +++++++ src/Umbraco.Core/Services/IEntityService.cs | 31 +++++- .../Implement/EntityRepository.cs | 87 ++++++++++++--- .../Trees/FileSystemTreeServiceTestsBase.cs | 83 ++++++++++++++ .../Trees/PartialViewTreeServiceTests.cs | 54 ++++++++++ .../Services/Trees/ScriptTreeServiceTests.cs | 53 +++++++++ .../Trees/StyleSheetTreeServiceTests.cs | 53 +++++++++ .../Services/EntityServiceTests.cs | 21 ++++ 39 files changed, 1163 insertions(+), 69 deletions(-) create mode 100644 src/Umbraco.Cms.Api.Management/Controllers/Document/RecycleBin/SiblingsDocumentRecycleBinController.cs create mode 100644 src/Umbraco.Cms.Api.Management/Controllers/Media/RecycleBin/SiblingsMediaRecycleBinController.cs create mode 100644 src/Umbraco.Cms.Api.Management/Controllers/MemberType/Tree/SiblingMemberTypeTreeController.cs create mode 100644 src/Umbraco.Cms.Api.Management/Controllers/PartialView/Tree/SiblingsPartialViewTreeController.cs create mode 100644 src/Umbraco.Cms.Api.Management/Controllers/Script/Tree/SiblingsScriptTreeController.cs create mode 100644 src/Umbraco.Cms.Api.Management/Controllers/Stylesheet/Tree/SiblingsStylesheetTreeController.cs create mode 100644 src/Umbraco.Cms.Api.Management/Services/FileSystem/FileSystemTreeServiceBase.cs create mode 100644 src/Umbraco.Cms.Api.Management/Services/FileSystem/IFileSystemTreeService.cs create mode 100644 src/Umbraco.Cms.Api.Management/Services/FileSystem/IPartialViewTreeService.cs create mode 100644 src/Umbraco.Cms.Api.Management/Services/FileSystem/IScriptTreeService.cs create mode 100644 src/Umbraco.Cms.Api.Management/Services/FileSystem/IStyleSheetTreeService.cs create mode 100644 src/Umbraco.Cms.Api.Management/Services/FileSystem/PartialViewTreeService.cs create mode 100644 src/Umbraco.Cms.Api.Management/Services/FileSystem/ScriptTreeService.cs create mode 100644 src/Umbraco.Cms.Api.Management/Services/FileSystem/StyleSheetTreeService.cs create mode 100644 tests/Umbraco.Tests.Integration/ManagementApi/Services/Trees/FileSystemTreeServiceTestsBase.cs create mode 100644 tests/Umbraco.Tests.Integration/ManagementApi/Services/Trees/PartialViewTreeServiceTests.cs create mode 100644 tests/Umbraco.Tests.Integration/ManagementApi/Services/Trees/ScriptTreeServiceTests.cs create mode 100644 tests/Umbraco.Tests.Integration/ManagementApi/Services/Trees/StyleSheetTreeServiceTests.cs diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Document/RecycleBin/SiblingsDocumentRecycleBinController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Document/RecycleBin/SiblingsDocumentRecycleBinController.cs new file mode 100644 index 0000000000..06f2f9284f --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Controllers/Document/RecycleBin/SiblingsDocumentRecycleBinController.cs @@ -0,0 +1,24 @@ +using Asp.Versioning; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Api.Common.ViewModels.Pagination; +using Umbraco.Cms.Api.Management.Factories; +using Umbraco.Cms.Api.Management.ViewModels.Document.RecycleBin; +using Umbraco.Cms.Core.Services; + +namespace Umbraco.Cms.Api.Management.Controllers.Document.RecycleBin; + +[ApiVersion("1.0")] +public class SiblingsDocumentRecycleBinController : DocumentRecycleBinControllerBase +{ + public SiblingsDocumentRecycleBinController(IEntityService entityService, IDocumentPresentationFactory documentPresentationFactory) + : base(entityService, documentPresentationFactory) + { + } + + [HttpGet("siblings")] + [MapToApiVersion("1.0")] + [ProducesResponseType(typeof(SubsetViewModel), StatusCodes.Status200OK)] + public async Task>> Siblings(CancellationToken cancellationToken, Guid target, int before, int after, Guid? dataTypeId = null) + => await GetSiblings(target, before, after); +} diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Media/RecycleBin/SiblingsMediaRecycleBinController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Media/RecycleBin/SiblingsMediaRecycleBinController.cs new file mode 100644 index 0000000000..5628aa551e --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Controllers/Media/RecycleBin/SiblingsMediaRecycleBinController.cs @@ -0,0 +1,24 @@ +using Asp.Versioning; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Api.Common.ViewModels.Pagination; +using Umbraco.Cms.Api.Management.Factories; +using Umbraco.Cms.Api.Management.ViewModels.Media.RecycleBin; +using Umbraco.Cms.Core.Services; + +namespace Umbraco.Cms.Api.Management.Controllers.Media.RecycleBin; + +[ApiVersion("1.0")] +public class SiblingsMediaRecycleBinController : MediaRecycleBinControllerBase +{ + public SiblingsMediaRecycleBinController(IEntityService entityService, IMediaPresentationFactory mediaPresentationFactory) + : base(entityService, mediaPresentationFactory) + { + } + + [HttpGet("siblings")] + [MapToApiVersion("1.0")] + [ProducesResponseType(typeof(SubsetViewModel), StatusCodes.Status200OK)] + public async Task>> Siblings(CancellationToken cancellationToken, Guid target, int before, int after, Guid? dataTypeId = null) + => await GetSiblings(target, before, after); +} diff --git a/src/Umbraco.Cms.Api.Management/Controllers/MemberType/Tree/SiblingMemberTypeTreeController.cs b/src/Umbraco.Cms.Api.Management/Controllers/MemberType/Tree/SiblingMemberTypeTreeController.cs new file mode 100644 index 0000000000..4ed8e9f949 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Controllers/MemberType/Tree/SiblingMemberTypeTreeController.cs @@ -0,0 +1,28 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Api.Common.ViewModels.Pagination; +using Umbraco.Cms.Api.Management.Services.Flags; +using Umbraco.Cms.Api.Management.ViewModels.Tree; +using Umbraco.Cms.Core.Services; + +namespace Umbraco.Cms.Api.Management.Controllers.MemberType.Tree; + +public class SiblingMemberTypeTreeController : MemberTypeTreeControllerBase +{ + public SiblingMemberTypeTreeController( + IEntityService entityService, + FlagProviderCollection flagProviders, + IMemberTypeService memberTypeService) + : base(entityService, flagProviders, memberTypeService) + { + } + + [HttpGet("siblings")] + [ProducesResponseType(typeof(SubsetViewModel), StatusCodes.Status200OK)] + public async Task>> Siblings( + CancellationToken cancellationToken, + Guid target, + int before, + int after) + => await GetSiblings(target, before, after); +} diff --git a/src/Umbraco.Cms.Api.Management/Controllers/PartialView/Tree/AncestorsPartialViewTreeController.cs b/src/Umbraco.Cms.Api.Management/Controllers/PartialView/Tree/AncestorsPartialViewTreeController.cs index 4048e1b9e7..3f79545e39 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/PartialView/Tree/AncestorsPartialViewTreeController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/PartialView/Tree/AncestorsPartialViewTreeController.cs @@ -1,16 +1,34 @@ using Asp.Versioning; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.DependencyInjection; +using Umbraco.Cms.Api.Management.Services.FileSystem; using Umbraco.Cms.Api.Management.ViewModels.Tree; +using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.IO; +using Umbraco.Cms.Core.Services; namespace Umbraco.Cms.Api.Management.Controllers.PartialView.Tree; [ApiVersion("1.0")] public class AncestorsPartialViewTreeController : PartialViewTreeControllerBase { + private readonly IPartialViewTreeService _partialViewTreeService; + + // TODO Remove the static service provider, and replace with base when the other constructors are obsoleted. + public AncestorsPartialViewTreeController(IPartialViewTreeService partialViewTreeService) + : this(partialViewTreeService, StaticServiceProvider.Instance.GetRequiredService()) + => _partialViewTreeService = partialViewTreeService; + + [ActivatorUtilitiesConstructor] + [Obsolete("Please use the other constructor. Scheduled to be removed in Umbraco 19")] + public AncestorsPartialViewTreeController(IPartialViewTreeService partialViewTreeService, FileSystems fileSystems) + : base(partialViewTreeService, fileSystems) => + _partialViewTreeService = partialViewTreeService; + + [Obsolete("Please use the other constructor. Scheduled to be removed in Umbraco 19")] public AncestorsPartialViewTreeController(FileSystems fileSystems) - : base(fileSystems) + : this(StaticServiceProvider.Instance.GetRequiredService(), fileSystems) { } diff --git a/src/Umbraco.Cms.Api.Management/Controllers/PartialView/Tree/ChildrenPartialViewTreeController.cs b/src/Umbraco.Cms.Api.Management/Controllers/PartialView/Tree/ChildrenPartialViewTreeController.cs index 2877248e41..099f01f342 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/PartialView/Tree/ChildrenPartialViewTreeController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/PartialView/Tree/ChildrenPartialViewTreeController.cs @@ -1,17 +1,34 @@ using Asp.Versioning; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Core.IO; using Umbraco.Cms.Api.Common.ViewModels.Pagination; +using Umbraco.Cms.Api.Management.Services.FileSystem; using Umbraco.Cms.Api.Management.ViewModels.Tree; +using Umbraco.Cms.Core.DependencyInjection; namespace Umbraco.Cms.Api.Management.Controllers.PartialView.Tree; [ApiVersion("1.0")] public class ChildrenPartialViewTreeController : PartialViewTreeControllerBase { + private readonly IPartialViewTreeService _partialViewTreeService; + + // TODO Remove the static service provider, and replace with base when the other constructors are obsoleted. + public ChildrenPartialViewTreeController(IPartialViewTreeService partialViewTreeService) + : this(partialViewTreeService, StaticServiceProvider.Instance.GetRequiredService()) + => _partialViewTreeService = partialViewTreeService; + + [ActivatorUtilitiesConstructor] + [Obsolete("Please use the other constructor. Scheduled to be removed in Umbraco 19")] + public ChildrenPartialViewTreeController(IPartialViewTreeService partialViewTreeService, FileSystems fileSystems) + : base(partialViewTreeService, fileSystems) => + _partialViewTreeService = partialViewTreeService; + + [Obsolete("Please use the other constructor. Scheduled to be removed in Umbraco 19")] public ChildrenPartialViewTreeController(FileSystems fileSystems) - : base(fileSystems) + : this(StaticServiceProvider.Instance.GetRequiredService(), fileSystems) { } diff --git a/src/Umbraco.Cms.Api.Management/Controllers/PartialView/Tree/PartialViewTreeControllerBase.cs b/src/Umbraco.Cms.Api.Management/Controllers/PartialView/Tree/PartialViewTreeControllerBase.cs index 11e6a946ea..b45aff4616 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/PartialView/Tree/PartialViewTreeControllerBase.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/PartialView/Tree/PartialViewTreeControllerBase.cs @@ -1,8 +1,11 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Api.Management.Controllers.Tree; using Umbraco.Cms.Api.Management.Routing; +using Umbraco.Cms.Api.Management.Services.FileSystem; using Umbraco.Cms.Core; +using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.IO; using Umbraco.Cms.Web.Common.Authorization; @@ -13,9 +16,30 @@ namespace Umbraco.Cms.Api.Management.Controllers.PartialView.Tree; [Authorize(Policy = AuthorizationPolicies.TreeAccessPartialViews)] public class PartialViewTreeControllerBase : FileSystemTreeControllerBase { - public PartialViewTreeControllerBase(FileSystems fileSystems) - => FileSystem = fileSystems.PartialViewsFileSystem ?? - throw new ArgumentException("Missing partial views file system", nameof(fileSystems)); + private readonly IPartialViewTreeService _partialViewTreeService; + // TODO Remove the static service provider, and replace with base when the other constructors are obsoleted. + public PartialViewTreeControllerBase(IPartialViewTreeService partialViewTreeService) + : this(partialViewTreeService, StaticServiceProvider.Instance.GetRequiredService()) => + _partialViewTreeService = partialViewTreeService; + + // FileSystem is required therefore, we can't remove it without some wizadry. When obsoletion is due, remove this. + [ActivatorUtilitiesConstructor] + [Obsolete("Scheduled for removal in Umbraco 19")] + public PartialViewTreeControllerBase(IPartialViewTreeService partialViewTreeService, FileSystems fileSystems) + : base(partialViewTreeService) + { + _partialViewTreeService = partialViewTreeService; + FileSystem = fileSystems.PartialViewsFileSystem ?? + throw new ArgumentException("Missing scripts file system", nameof(fileSystems)); + } + + [Obsolete("Please use the other constructor. Scheduled to be removed in Umbraco 19")] + public PartialViewTreeControllerBase(FileSystems fileSystems) + : this(StaticServiceProvider.Instance.GetRequiredService()) + => FileSystem = fileSystems.PartialViewsFileSystem ?? + throw new ArgumentException("Missing scripts file system", nameof(fileSystems)); + + [Obsolete("Included in the service class. Scheduled to be removed in Umbraco 19")] protected override IFileSystem FileSystem { get; } } diff --git a/src/Umbraco.Cms.Api.Management/Controllers/PartialView/Tree/RootPartialViewTreeController.cs b/src/Umbraco.Cms.Api.Management/Controllers/PartialView/Tree/RootPartialViewTreeController.cs index 4247ded602..4e42266389 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/PartialView/Tree/RootPartialViewTreeController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/PartialView/Tree/RootPartialViewTreeController.cs @@ -1,17 +1,34 @@ using Asp.Versioning; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Core.IO; using Umbraco.Cms.Api.Common.ViewModels.Pagination; +using Umbraco.Cms.Api.Management.Services.FileSystem; using Umbraco.Cms.Api.Management.ViewModels.Tree; +using Umbraco.Cms.Core.DependencyInjection; namespace Umbraco.Cms.Api.Management.Controllers.PartialView.Tree; [ApiVersion("1.0")] public class RootPartialViewTreeController : PartialViewTreeControllerBase { + private readonly IPartialViewTreeService _partialViewTreeService; + + // TODO Remove the static service provider, and replace with base when the other constructors are obsoleted. + public RootPartialViewTreeController(IPartialViewTreeService partialViewTreeService) + : this(partialViewTreeService, StaticServiceProvider.Instance.GetRequiredService()) + => _partialViewTreeService = partialViewTreeService; + + [ActivatorUtilitiesConstructor] + [Obsolete("Please use the other constructor. Scheduled to be removed in Umbraco 19")] + public RootPartialViewTreeController(IPartialViewTreeService partialViewTreeService, FileSystems fileSystems) + : base(partialViewTreeService, fileSystems) => + _partialViewTreeService = partialViewTreeService; + + [Obsolete("Please use the other constructor. Scheduled to be removed in Umbraco 19")] public RootPartialViewTreeController(FileSystems fileSystems) - : base(fileSystems) + : this(StaticServiceProvider.Instance.GetRequiredService(), fileSystems) { } diff --git a/src/Umbraco.Cms.Api.Management/Controllers/PartialView/Tree/SiblingsPartialViewTreeController.cs b/src/Umbraco.Cms.Api.Management/Controllers/PartialView/Tree/SiblingsPartialViewTreeController.cs new file mode 100644 index 0000000000..af1e3171a6 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Controllers/PartialView/Tree/SiblingsPartialViewTreeController.cs @@ -0,0 +1,41 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.DependencyInjection; +using Umbraco.Cms.Api.Common.ViewModels.Pagination; +using Umbraco.Cms.Api.Management.Services.FileSystem; +using Umbraco.Cms.Api.Management.ViewModels.Tree; +using Umbraco.Cms.Core.DependencyInjection; +using Umbraco.Cms.Core.IO; + +namespace Umbraco.Cms.Api.Management.Controllers.PartialView.Tree; + +public class SiblingsPartialViewTreeController : PartialViewTreeControllerBase +{ + private readonly IPartialViewTreeService _partialViewTreeService; + + // TODO Remove the static service provider, and replace with base when the other constructors are obsoleted. + public SiblingsPartialViewTreeController(IPartialViewTreeService partialViewTreeService) + : this(partialViewTreeService, StaticServiceProvider.Instance.GetRequiredService()) + => _partialViewTreeService = partialViewTreeService; + + [ActivatorUtilitiesConstructor] + [Obsolete("Please use the other constructor. Scheduled to be removed in Umbraco 19")] + public SiblingsPartialViewTreeController(IPartialViewTreeService partialViewTreeService, FileSystems fileSystems) + : base(partialViewTreeService, fileSystems) => + _partialViewTreeService = partialViewTreeService; + + [Obsolete("Please use the other constructor. Scheduled to be removed in Umbraco 19")] + public SiblingsPartialViewTreeController(FileSystems fileSystems) + : this(StaticServiceProvider.Instance.GetRequiredService(), fileSystems) + { + } + + [HttpGet("siblings")] + [ProducesResponseType(typeof(SubsetViewModel), StatusCodes.Status200OK)] + public async Task>> Siblings( + CancellationToken cancellationToken, + string path, + int before, + int after) + => await GetSiblings(path, before, after); +} diff --git a/src/Umbraco.Cms.Api.Management/Controllers/RecycleBin/RecycleBinControllerBase.cs b/src/Umbraco.Cms.Api.Management/Controllers/RecycleBin/RecycleBinControllerBase.cs index 850d7240d0..59e10df454 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/RecycleBin/RecycleBinControllerBase.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/RecycleBin/RecycleBinControllerBase.cs @@ -4,6 +4,7 @@ using Umbraco.Cms.Api.Common.ViewModels.Pagination; using Umbraco.Cms.Api.Management.Controllers.Content; using Umbraco.Cms.Api.Management.ViewModels.Item; using Umbraco.Cms.Api.Management.ViewModels.RecycleBin; +using Umbraco.Cms.Core; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models.Entities; using Umbraco.Cms.Core.Services; @@ -45,6 +46,24 @@ public abstract class RecycleBinControllerBase : ContentControllerBase return Task.FromResult>>(Ok(result)); } + protected async Task>> GetSiblings(Guid target, int before, int after) + { + IEntitySlim[] siblings = GetSiblingEntities(target, before, after, out var totalBefore, out var totalAfter); + if (siblings.Length == 0) + { + return NotFound(); + } + + IEntitySlim entity = siblings.First(); + Guid? parentKey = GetParentKey(entity); + + TItem[] treeItemViewModels = MapRecycleBinViewModels(parentKey, siblings); + + SubsetViewModel result = SubsetViewModel(treeItemViewModels, totalBefore, totalAfter); + + return Ok(result); + } + protected virtual TItem MapRecycleBinViewModel(Guid? parentKey, IEntitySlim entity) { if (entity == null) @@ -136,4 +155,27 @@ public abstract class RecycleBinControllerBase : ContentControllerBase private PagedViewModel PagedViewModel(IEnumerable treeItemViewModels, long totalItems) => new() { Total = totalItems, Items = treeItemViewModels }; + + protected SubsetViewModel SubsetViewModel(IEnumerable treeItemViewModels, long totalBefore, long totalAfter) + => new() { TotalBefore = totalBefore, TotalAfter = totalAfter, Items = treeItemViewModels }; + + protected virtual IEntitySlim[] GetSiblingEntities(Guid target, int before, int after, out long totalBefore, out long totalAfter) => + _entityService + .GetTrashedSiblings( + target, + [ItemObjectType], + before, + after, + out totalBefore, + out totalAfter, + ordering: Ordering.By(nameof(Infrastructure.Persistence.Dtos.NodeDto.Text))) + .ToArray(); + + /// + /// Gets the parent key for an entity, or root if null or no parent. + /// + protected virtual Guid? GetParentKey(IEntitySlim entity) => + entity.ParentId > 0 + ? _entityService.GetKey(entity.ParentId, ItemObjectType).Result + : Constants.System.RootKey; } diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Script/Tree/AncestorsScriptTreeController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Script/Tree/AncestorsScriptTreeController.cs index ce3ae4189c..ed5acbc0c3 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Script/Tree/AncestorsScriptTreeController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Script/Tree/AncestorsScriptTreeController.cs @@ -1,7 +1,10 @@ using Asp.Versioning; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.DependencyInjection; +using Umbraco.Cms.Api.Management.Services.FileSystem; using Umbraco.Cms.Api.Management.ViewModels.Tree; +using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.IO; namespace Umbraco.Cms.Api.Management.Controllers.Script.Tree; @@ -9,8 +12,22 @@ namespace Umbraco.Cms.Api.Management.Controllers.Script.Tree; [ApiVersion("1.0")] public class AncestorsScriptTreeController : ScriptTreeControllerBase { + private readonly IScriptTreeService _scriptTreeService; + + // TODO Remove the static service provider, and replace with base when the other constructors are obsoleted. + public AncestorsScriptTreeController(IScriptTreeService scriptTreeService) + : this(scriptTreeService, StaticServiceProvider.Instance.GetRequiredService()) + => _scriptTreeService = scriptTreeService; + + [ActivatorUtilitiesConstructor] + [Obsolete("Please use the other constructor. Scheduled to be removed in Umbraco 19")] + public AncestorsScriptTreeController(IScriptTreeService scriptTreeService, FileSystems fileSystems) + : base(scriptTreeService, fileSystems) => + _scriptTreeService = scriptTreeService; + + [Obsolete("Please use the other constructor. Scheduled to be removed in Umbraco 19")] public AncestorsScriptTreeController(FileSystems fileSystems) - : base(fileSystems) + : this(StaticServiceProvider.Instance.GetRequiredService(), fileSystems) { } diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Script/Tree/ChildrenScriptTreeController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Script/Tree/ChildrenScriptTreeController.cs index 73f028eab2..ba40037841 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Script/Tree/ChildrenScriptTreeController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Script/Tree/ChildrenScriptTreeController.cs @@ -1,17 +1,34 @@ using Asp.Versioning; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Core.IO; using Umbraco.Cms.Api.Common.ViewModels.Pagination; +using Umbraco.Cms.Api.Management.Services.FileSystem; using Umbraco.Cms.Api.Management.ViewModels.Tree; +using Umbraco.Cms.Core.DependencyInjection; namespace Umbraco.Cms.Api.Management.Controllers.Script.Tree; [ApiVersion("1.0")] public class ChildrenScriptTreeController : ScriptTreeControllerBase { + private readonly IScriptTreeService _scriptTreeService; + + // TODO Remove the static service provider, and replace with base when the other constructors are obsoleted. + public ChildrenScriptTreeController(IScriptTreeService scriptTreeService) + : this(scriptTreeService, StaticServiceProvider.Instance.GetRequiredService()) + => _scriptTreeService = scriptTreeService; + + [ActivatorUtilitiesConstructor] + [Obsolete("Please use the other constructor. Scheduled to be removed in Umbraco 19")] + public ChildrenScriptTreeController(IScriptTreeService scriptTreeService, FileSystems fileSystems) + : base(scriptTreeService, fileSystems) => + _scriptTreeService = scriptTreeService; + + [Obsolete("Please use the other constructor. Scheduled to be removed in Umbraco 19")] public ChildrenScriptTreeController(FileSystems fileSystems) - : base(fileSystems) + : this(StaticServiceProvider.Instance.GetRequiredService(), fileSystems) { } diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Script/Tree/RootScriptTreeController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Script/Tree/RootScriptTreeController.cs index 3eff3b5f50..f29d3bdb44 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Script/Tree/RootScriptTreeController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Script/Tree/RootScriptTreeController.cs @@ -1,17 +1,34 @@ using Asp.Versioning; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Core.IO; using Umbraco.Cms.Api.Common.ViewModels.Pagination; +using Umbraco.Cms.Api.Management.Services.FileSystem; using Umbraco.Cms.Api.Management.ViewModels.Tree; +using Umbraco.Cms.Core.DependencyInjection; namespace Umbraco.Cms.Api.Management.Controllers.Script.Tree; [ApiVersion("1.0")] public class RootScriptTreeController : ScriptTreeControllerBase { + private readonly IScriptTreeService _scriptTreeService; + + // TODO Remove the static service provider, and replace with base when the other constructors are obsoleted. + public RootScriptTreeController(IScriptTreeService scriptTreeService) + : this(scriptTreeService, StaticServiceProvider.Instance.GetRequiredService()) + => _scriptTreeService = scriptTreeService; + + [ActivatorUtilitiesConstructor] + [Obsolete("Please use the other constructor. Scheduled to be removed in Umbraco 19")] + public RootScriptTreeController(IScriptTreeService scriptTreeService, FileSystems fileSystems) + : base(scriptTreeService, fileSystems) => + _scriptTreeService = scriptTreeService; + + [Obsolete("Please use the other constructor. Scheduled to be removed in Umbraco 19")] public RootScriptTreeController(FileSystems fileSystems) - : base(fileSystems) + : this(StaticServiceProvider.Instance.GetRequiredService(), fileSystems) { } diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Script/Tree/ScriptTreeControllerBase.cs b/src/Umbraco.Cms.Api.Management/Controllers/Script/Tree/ScriptTreeControllerBase.cs index e8bda7446d..ba302fc920 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Script/Tree/ScriptTreeControllerBase.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Script/Tree/ScriptTreeControllerBase.cs @@ -1,8 +1,11 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Api.Management.Controllers.Tree; using Umbraco.Cms.Api.Management.Routing; +using Umbraco.Cms.Api.Management.Services.FileSystem; using Umbraco.Cms.Core; +using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.IO; using Umbraco.Cms.Web.Common.Authorization; @@ -13,9 +16,30 @@ namespace Umbraco.Cms.Api.Management.Controllers.Script.Tree; [Authorize(Policy = AuthorizationPolicies.TreeAccessScripts)] public class ScriptTreeControllerBase : FileSystemTreeControllerBase { + private readonly IScriptTreeService _scriptTreeService; + + // TODO Remove the static service provider, and replace with base when the other constructors are obsoleted. + public ScriptTreeControllerBase(IScriptTreeService scriptTreeService) + : this(scriptTreeService, StaticServiceProvider.Instance.GetRequiredService()) => + _scriptTreeService = scriptTreeService; + + // FileSystem is required therefore, we can't remove it without some wizadry. When obsoletion is due, remove this. + [ActivatorUtilitiesConstructor] + [Obsolete("Scheduled for removal in Umbraco 19")] + public ScriptTreeControllerBase(IScriptTreeService scriptTreeService, FileSystems fileSystems) + : base(scriptTreeService) + { + _scriptTreeService = scriptTreeService; + FileSystem = fileSystems.ScriptsFileSystem ?? + throw new ArgumentException("Missing scripts file system", nameof(fileSystems)); + } + + [Obsolete("Please use the other constructor. Scheduled to be removed in Umbraco 19")] public ScriptTreeControllerBase(FileSystems fileSystems) + : this(StaticServiceProvider.Instance.GetRequiredService()) => FileSystem = fileSystems.ScriptsFileSystem ?? throw new ArgumentException("Missing scripts file system", nameof(fileSystems)); + [Obsolete("Included in the service class. Scheduled to be removed in Umbraco 19")] protected override IFileSystem FileSystem { get; } } diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Script/Tree/SiblingsScriptTreeController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Script/Tree/SiblingsScriptTreeController.cs new file mode 100644 index 0000000000..deec60cacb --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Controllers/Script/Tree/SiblingsScriptTreeController.cs @@ -0,0 +1,41 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.DependencyInjection; +using Umbraco.Cms.Api.Common.ViewModels.Pagination; +using Umbraco.Cms.Api.Management.Services.FileSystem; +using Umbraco.Cms.Api.Management.ViewModels.Tree; +using Umbraco.Cms.Core.DependencyInjection; +using Umbraco.Cms.Core.IO; + +namespace Umbraco.Cms.Api.Management.Controllers.Script.Tree; + +public class SiblingsScriptTreeController : ScriptTreeControllerBase +{ + private readonly IScriptTreeService _scriptTreeService; + + // TODO Remove the static service provider, and replace with base when the other constructors are obsoleted. + public SiblingsScriptTreeController(IScriptTreeService scriptTreeService) + : this(scriptTreeService, StaticServiceProvider.Instance.GetRequiredService()) + => _scriptTreeService = scriptTreeService; + + [ActivatorUtilitiesConstructor] + [Obsolete("Please use the other constructor. Scheduled to be removed in Umbraco 19")] + public SiblingsScriptTreeController(IScriptTreeService scriptTreeService, FileSystems fileSystems) + : base(scriptTreeService, fileSystems) => + _scriptTreeService = scriptTreeService; + + [Obsolete("Please use the other constructor. Scheduled to be removed in Umbraco 19")] + public SiblingsScriptTreeController(FileSystems fileSystems) + : this(StaticServiceProvider.Instance.GetRequiredService(), fileSystems) + { + } + + [HttpGet("siblings")] + [ProducesResponseType(typeof(SubsetViewModel), StatusCodes.Status200OK)] + public async Task>> Siblings( + CancellationToken cancellationToken, + string path, + int before, + int after) + => await GetSiblings(path, before, after); +} diff --git a/src/Umbraco.Cms.Api.Management/Controllers/StaticFile/Tree/StaticFileTreeControllerBase.cs b/src/Umbraco.Cms.Api.Management/Controllers/StaticFile/Tree/StaticFileTreeControllerBase.cs index e4ee568150..0695f0b0ed 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/StaticFile/Tree/StaticFileTreeControllerBase.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/StaticFile/Tree/StaticFileTreeControllerBase.cs @@ -1,8 +1,11 @@ using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Api.Management.Controllers.Tree; using Umbraco.Cms.Api.Management.Routing; +using Umbraco.Cms.Api.Management.Services.FileSystem; using Umbraco.Cms.Api.Management.ViewModels.Tree; using Umbraco.Cms.Core; +using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.IO; namespace Umbraco.Cms.Api.Management.Controllers.StaticFile.Tree; @@ -11,28 +14,39 @@ namespace Umbraco.Cms.Api.Management.Controllers.StaticFile.Tree; [ApiExplorerSettings(GroupName = "Static File")] public class StaticFileTreeControllerBase : FileSystemTreeControllerBase { + private readonly IFileSystemTreeService _fileSystemTreeService; private static readonly string[] _allowedRootFolders = { $"{Path.DirectorySeparatorChar}App_Plugins", $"{Path.DirectorySeparatorChar}wwwroot" }; + public StaticFileTreeControllerBase(IPhysicalFileSystem physicalFileSystem, IFileSystemTreeService fileSystemTreeService) + : base (fileSystemTreeService) + { + FileSystem = physicalFileSystem; + _fileSystemTreeService = fileSystemTreeService; + } + + [Obsolete("Please use the other constructor. Scheduled for removal in Umbraco 19")] public StaticFileTreeControllerBase(IPhysicalFileSystem physicalFileSystem) - => FileSystem = physicalFileSystem; + : this(physicalFileSystem, StaticServiceProvider.Instance.GetRequiredService()) + { + } protected override IFileSystem FileSystem { get; } - protected override string[] GetDirectories(string path) => + protected string[] GetDirectories(string path) => IsTreeRootPath(path) ? _allowedRootFolders : IsAllowedPath(path) - ? base.GetDirectories(path) + ? _fileSystemTreeService.GetDirectories(path) : Array.Empty(); - protected override string[] GetFiles(string path) + protected string[] GetFiles(string path) => IsTreeRootPath(path) || IsAllowedPath(path) == false ? Array.Empty() - : base.GetFiles(path); + : _fileSystemTreeService.GetFiles(path); - protected override FileSystemTreeItemPresentationModel[] GetAncestorModels(string path, bool includeSelf) + protected FileSystemTreeItemPresentationModel[] GetAncestorModels(string path, bool includeSelf) => IsAllowedPath(path) - ? base.GetAncestorModels(path, includeSelf) + ? _fileSystemTreeService.GetAncestorModels(path, includeSelf) : Array.Empty(); private bool IsTreeRootPath(string path) => path == Path.DirectorySeparatorChar.ToString(); diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Stylesheet/Tree/AncestorsStylesheetTreeController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Stylesheet/Tree/AncestorsStylesheetTreeController.cs index 3863389125..3760808263 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Stylesheet/Tree/AncestorsStylesheetTreeController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Stylesheet/Tree/AncestorsStylesheetTreeController.cs @@ -1,7 +1,10 @@ using Asp.Versioning; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.DependencyInjection; +using Umbraco.Cms.Api.Management.Services.FileSystem; using Umbraco.Cms.Api.Management.ViewModels.Tree; +using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.IO; namespace Umbraco.Cms.Api.Management.Controllers.Stylesheet.Tree; @@ -9,8 +12,22 @@ namespace Umbraco.Cms.Api.Management.Controllers.Stylesheet.Tree; [ApiVersion("1.0")] public class AncestorsStylesheetTreeController : StylesheetTreeControllerBase { + private readonly IStyleSheetTreeService _styleSheetTreeService; + + // TODO Remove the static service provider, and replace with base when the other constructors are obsoleted. + public AncestorsStylesheetTreeController(IStyleSheetTreeService styleSheetTreeService) + : this(styleSheetTreeService, StaticServiceProvider.Instance.GetRequiredService()) + => _styleSheetTreeService = styleSheetTreeService; + + [ActivatorUtilitiesConstructor] + [Obsolete("Please use the other constructor. Scheduled to be removed in Umbraco 19")] + public AncestorsStylesheetTreeController(IStyleSheetTreeService styleSheetTreeService, FileSystems fileSystems) + : base(styleSheetTreeService, fileSystems) => + _styleSheetTreeService = styleSheetTreeService; + + [Obsolete("Please use the other constructor. Scheduled to be removed in Umbraco 19")] public AncestorsStylesheetTreeController(FileSystems fileSystems) - : base(fileSystems) + : this(StaticServiceProvider.Instance.GetRequiredService(), fileSystems) { } diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Stylesheet/Tree/ChildrenStylesheetTreeController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Stylesheet/Tree/ChildrenStylesheetTreeController.cs index 3435f67225..41484bce50 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Stylesheet/Tree/ChildrenStylesheetTreeController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Stylesheet/Tree/ChildrenStylesheetTreeController.cs @@ -1,20 +1,36 @@ using Asp.Versioning; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Core.IO; using Umbraco.Cms.Api.Common.ViewModels.Pagination; +using Umbraco.Cms.Api.Management.Services.FileSystem; using Umbraco.Cms.Api.Management.ViewModels.Tree; +using Umbraco.Cms.Core.DependencyInjection; namespace Umbraco.Cms.Api.Management.Controllers.Stylesheet.Tree; [ApiVersion("1.0")] public class ChildrenStylesheetTreeController : StylesheetTreeControllerBase { + private readonly IStyleSheetTreeService _styleSheetTreeService; + + // TODO Remove the static service provider, and replace with base when the other constructors are obsoleted. + public ChildrenStylesheetTreeController(IStyleSheetTreeService styleSheetTreeService) + : this(styleSheetTreeService, StaticServiceProvider.Instance.GetRequiredService()) + => _styleSheetTreeService = styleSheetTreeService; + + [ActivatorUtilitiesConstructor] + [Obsolete("Please use the other constructor. Scheduled to be removed in Umbraco 19")] + public ChildrenStylesheetTreeController(IStyleSheetTreeService styleSheetTreeService, FileSystems fileSystems) + : base(styleSheetTreeService, fileSystems) => + _styleSheetTreeService = styleSheetTreeService; + + [Obsolete("Please use the other constructor. Scheduled to be removed in Umbraco 19")] public ChildrenStylesheetTreeController(FileSystems fileSystems) - : base(fileSystems) + : this(StaticServiceProvider.Instance.GetRequiredService(), fileSystems) { } - [HttpGet("children")] [MapToApiVersion("1.0")] [ProducesResponseType(typeof(PagedViewModel), StatusCodes.Status200OK)] diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Stylesheet/Tree/RootStylesheetTreeController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Stylesheet/Tree/RootStylesheetTreeController.cs index c01f15ea59..417c636a37 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Stylesheet/Tree/RootStylesheetTreeController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Stylesheet/Tree/RootStylesheetTreeController.cs @@ -1,17 +1,34 @@ using Asp.Versioning; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Core.IO; using Umbraco.Cms.Api.Common.ViewModels.Pagination; +using Umbraco.Cms.Api.Management.Services.FileSystem; using Umbraco.Cms.Api.Management.ViewModels.Tree; +using Umbraco.Cms.Core.DependencyInjection; namespace Umbraco.Cms.Api.Management.Controllers.Stylesheet.Tree; [ApiVersion("1.0")] public class RootStylesheetTreeController : StylesheetTreeControllerBase { + private readonly IStyleSheetTreeService _styleSheetTreeService; + + // TODO Remove the static service provider, and replace with base when the other constructors are obsoleted. + public RootStylesheetTreeController(IStyleSheetTreeService styleSheetTreeService) + : this(styleSheetTreeService, StaticServiceProvider.Instance.GetRequiredService()) + => _styleSheetTreeService = styleSheetTreeService; + + [ActivatorUtilitiesConstructor] + [Obsolete("Please use the other constructor. Scheduled to be removed in Umbraco 19")] + public RootStylesheetTreeController(IStyleSheetTreeService styleSheetTreeService, FileSystems fileSystems) + : base(styleSheetTreeService, fileSystems) => + _styleSheetTreeService = styleSheetTreeService; + + [Obsolete("Please use the other constructor. Scheduled to be removed in Umbraco 19")] public RootStylesheetTreeController(FileSystems fileSystems) - : base(fileSystems) + : this(StaticServiceProvider.Instance.GetRequiredService(), fileSystems) { } diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Stylesheet/Tree/SiblingsStylesheetTreeController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Stylesheet/Tree/SiblingsStylesheetTreeController.cs new file mode 100644 index 0000000000..0f2b03b704 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Controllers/Stylesheet/Tree/SiblingsStylesheetTreeController.cs @@ -0,0 +1,41 @@ +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.DependencyInjection; +using Umbraco.Cms.Api.Common.ViewModels.Pagination; +using Umbraco.Cms.Api.Management.Services.FileSystem; +using Umbraco.Cms.Api.Management.ViewModels.Tree; +using Umbraco.Cms.Core.DependencyInjection; +using Umbraco.Cms.Core.IO; + +namespace Umbraco.Cms.Api.Management.Controllers.Stylesheet.Tree; + +public class SiblingsStylesheetTreeController : StylesheetTreeControllerBase +{ + private readonly IStyleSheetTreeService _styleSheetTreeService; + + // TODO Remove the static service provider, and replace with base when the other constructors are obsoleted. + public SiblingsStylesheetTreeController(IStyleSheetTreeService styleSheetTreeService) + : this(styleSheetTreeService, StaticServiceProvider.Instance.GetRequiredService()) + => _styleSheetTreeService = styleSheetTreeService; + + [ActivatorUtilitiesConstructor] + [Obsolete("Please use the other constructor. Scheduled to be removed in Umbraco 19")] + public SiblingsStylesheetTreeController(IStyleSheetTreeService styleSheetTreeService, FileSystems fileSystems) + : base(styleSheetTreeService, fileSystems) => + _styleSheetTreeService = styleSheetTreeService; + + [Obsolete("Please use the other constructor. Scheduled to be removed in Umbraco 19")] + public SiblingsStylesheetTreeController(FileSystems fileSystems) + : this(StaticServiceProvider.Instance.GetRequiredService(), fileSystems) + { + } + + [HttpGet("siblings")] + [ProducesResponseType(typeof(SubsetViewModel), StatusCodes.Status200OK)] + public async Task>> Siblings( + CancellationToken cancellationToken, + string path, + int before, + int after) + => await GetSiblings(path, before, after); +} diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Stylesheet/Tree/StylesheetTreeControllerBase.cs b/src/Umbraco.Cms.Api.Management/Controllers/Stylesheet/Tree/StylesheetTreeControllerBase.cs index 428f892f6b..dd15a02d73 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Stylesheet/Tree/StylesheetTreeControllerBase.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Stylesheet/Tree/StylesheetTreeControllerBase.cs @@ -1,8 +1,11 @@ using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Api.Management.Controllers.Tree; using Umbraco.Cms.Api.Management.Routing; +using Umbraco.Cms.Api.Management.Services.FileSystem; using Umbraco.Cms.Core; +using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.IO; using Umbraco.Cms.Web.Common.Authorization; @@ -13,9 +16,30 @@ namespace Umbraco.Cms.Api.Management.Controllers.Stylesheet.Tree; [Authorize(Policy = AuthorizationPolicies.TreeAccessStylesheets)] public class StylesheetTreeControllerBase : FileSystemTreeControllerBase { - public StylesheetTreeControllerBase(FileSystems fileSystems) - => FileSystem = fileSystems.StylesheetsFileSystem ?? - throw new ArgumentException("Missing stylesheets file system", nameof(fileSystems)); + private readonly IStyleSheetTreeService _styleSheetTreeService; + // TODO Remove the static service provider, and replace with base when the other constructors are obsoleted. + public StylesheetTreeControllerBase(IStyleSheetTreeService styleSheetTreeService) + : this(styleSheetTreeService, StaticServiceProvider.Instance.GetRequiredService()) => + _styleSheetTreeService = styleSheetTreeService; + + // FileSystem is required therefore, we can't remove it without some wizadry. When obsoletion is due, remove this. + [ActivatorUtilitiesConstructor] + [Obsolete("Scheduled for removal in Umbraco 19")] + public StylesheetTreeControllerBase(IStyleSheetTreeService styleSheetTreeService, FileSystems fileSystems) + : base(styleSheetTreeService) + { + _styleSheetTreeService = styleSheetTreeService; + FileSystem = fileSystems.ScriptsFileSystem ?? + throw new ArgumentException("Missing scripts file system", nameof(fileSystems)); + } + + [Obsolete("Please use the other constructor. Scheduled to be removed in Umbraco 19")] + public StylesheetTreeControllerBase(FileSystems fileSystems) + : this(StaticServiceProvider.Instance.GetRequiredService()) + => FileSystem = fileSystems.ScriptsFileSystem ?? + throw new ArgumentException("Missing scripts file system", nameof(fileSystems)); + + [Obsolete("Included in the service class. Scheduled to be removed in Umbraco 19")] protected override IFileSystem FileSystem { get; } } diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Tree/FileSystemTreeControllerBase.cs b/src/Umbraco.Cms.Api.Management/Controllers/Tree/FileSystemTreeControllerBase.cs index 1388e4b798..933986e2f3 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Tree/FileSystemTreeControllerBase.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Tree/FileSystemTreeControllerBase.cs @@ -1,8 +1,11 @@ using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Api.Common.ViewModels.Pagination; using Umbraco.Cms.Api.Management.Extensions; +using Umbraco.Cms.Api.Management.Services.FileSystem; using Umbraco.Cms.Api.Management.ViewModels.FileSystem; using Umbraco.Cms.Api.Management.ViewModels.Tree; +using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.IO; using Umbraco.Extensions; @@ -10,11 +13,24 @@ namespace Umbraco.Cms.Api.Management.Controllers.Tree; public abstract class FileSystemTreeControllerBase : ManagementApiControllerBase { + private readonly IFileSystemTreeService _fileSystemTreeService; + + [Obsolete("Has been moved to the individual services. Scheduled to be removed in Umbraco 19")] protected abstract IFileSystem FileSystem { get; } + [ActivatorUtilitiesConstructor] + protected FileSystemTreeControllerBase(IFileSystemTreeService fileSystemTreeService) => _fileSystemTreeService = fileSystemTreeService; + + [Obsolete("Use the other constructor. Scheduled for removal in Umbraco 19")] + protected FileSystemTreeControllerBase() + : this(StaticServiceProvider.Instance.GetRequiredService()) + { + } + + protected Task>> GetRoot(int skip, int take) { - FileSystemTreeItemPresentationModel[] viewModels = GetPathViewModels(string.Empty, skip, take, out var totalItems); + FileSystemTreeItemPresentationModel[] viewModels = _fileSystemTreeService.GetPathViewModels(string.Empty, skip, take, out var totalItems); PagedViewModel result = PagedViewModel(viewModels, totalItems); return Task.FromResult>>(Ok(result)); @@ -22,20 +38,39 @@ public abstract class FileSystemTreeControllerBase : ManagementApiControllerBase protected Task>> GetChildren(string path, int skip, int take) { - FileSystemTreeItemPresentationModel[] viewModels = GetPathViewModels(path, skip, take, out var totalItems); + FileSystemTreeItemPresentationModel[] viewModels = _fileSystemTreeService.GetPathViewModels(path, skip, take, out var totalItems); PagedViewModel result = PagedViewModel(viewModels, totalItems); return Task.FromResult>>(Ok(result)); } + /// + /// Gets the sibling of the targeted item based on its path. + /// + /// The path to the item. + /// The amount of siblings you want to fetch from before the items position in the array. + /// The amount of siblings you want to fetch after the items position in the array. + /// A SubsetViewModel of the siblings of the item and the item itself. + protected Task>> GetSiblings(string path, int before, int after) + { + FileSystemTreeItemPresentationModel[] viewModels = _fileSystemTreeService.GetSiblingsViewModels(path, before, after, out var totalBefore, out var totalAfter); + + SubsetViewModel result = new() { TotalBefore = totalBefore, TotalAfter = totalAfter, Items = viewModels }; + return Task.FromResult>>(Ok(result)); + } + protected virtual Task>> GetAncestors(string path, bool includeSelf = true) { path = path.VirtualPathToSystemPath(); - FileSystemTreeItemPresentationModel[] models = GetAncestorModels(path, includeSelf); + FileSystemTreeItemPresentationModel[] models = _fileSystemTreeService.GetAncestorModels(path, includeSelf); return Task.FromResult>>(Ok(models)); } + private PagedViewModel PagedViewModel(IEnumerable viewModels, long totalItems) + => new() { Total = totalItems, Items = viewModels }; + + [Obsolete("Has been moved to FileSystemTreeServiceBase. Scheduled for removal in Umbraco 19")] protected virtual FileSystemTreeItemPresentationModel[] GetAncestorModels(string path, bool includeSelf) { var directories = path.Split(Path.DirectorySeparatorChar).Take(Range.EndAt(Index.FromEnd(1))).ToArray(); @@ -52,49 +87,28 @@ public abstract class FileSystemTreeControllerBase : ManagementApiControllerBase return result.ToArray(); } + [Obsolete("Has been moved to FileSystemTreeServiceBase. Scheduled for removal in Umbraco 19")] protected virtual string[] GetDirectories(string path) => FileSystem .GetDirectories(path) .OrderBy(directory => directory) .ToArray(); + [Obsolete("Has been moved to FileSystemTreeServiceBase. Scheduled for removal in Umbraco 19")] protected virtual string[] GetFiles(string path) => FileSystem .GetFiles(path) .OrderBy(file => file) .ToArray(); + [Obsolete("Has been moved to FileSystemTreeServiceBase. Scheduled for removal in Umbraco 19")] protected virtual bool DirectoryHasChildren(string path) => FileSystem.GetFiles(path).Any() || FileSystem.GetDirectories(path).Any(); - private FileSystemTreeItemPresentationModel[] GetPathViewModels(string path, int skip, int take, out long totalItems) - { - path = path.VirtualPathToSystemPath(); - 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; - - FileSystemTreeItemPresentationModel ViewModel(string itemPath, bool isFolder) - => MapViewModel( - itemPath, - GetFileSystemItemName(isFolder, itemPath), - isFolder); - - return allItems - .Skip(skip) - .Take(take) - .Select(item => ViewModel(item.Path, item.IsFolder)) - .ToArray(); - } - + [Obsolete("Has been moved to FileSystemTreeServiceBase. Scheduled for removal in Umbraco 19")] private string GetFileSystemItemName(bool isFolder, string itemPath) => isFolder ? Path.GetFileName(itemPath) : FileSystem.GetFileName(itemPath); - private PagedViewModel PagedViewModel(IEnumerable viewModels, long totalItems) - => new() { Total = totalItems, Items = viewModels }; - + [Obsolete("Has been moved to FileSystemTreeServiceBase. Scheduled for removal in Umbraco 19")] private FileSystemTreeItemPresentationModel MapViewModel(string path, string name, bool isFolder) { var parentPath = Path.GetDirectoryName(path); diff --git a/src/Umbraco.Cms.Api.Management/DependencyInjection/TreeBuilderExtensions.cs b/src/Umbraco.Cms.Api.Management/DependencyInjection/TreeBuilderExtensions.cs index ee95a14900..733223efa0 100644 --- a/src/Umbraco.Cms.Api.Management/DependencyInjection/TreeBuilderExtensions.cs +++ b/src/Umbraco.Cms.Api.Management/DependencyInjection/TreeBuilderExtensions.cs @@ -1,6 +1,8 @@ using Microsoft.Extensions.DependencyInjection; using Umbraco.Cms.Api.Management.Services.Entities; +using Umbraco.Cms.Api.Management.Services.FileSystem; using Umbraco.Cms.Core.DependencyInjection; +using Umbraco.Extensions; namespace Umbraco.Cms.Api.Management.DependencyInjection; @@ -9,6 +11,9 @@ internal static class TreeBuilderExtensions internal static IUmbracoBuilder AddTrees(this IUmbracoBuilder builder) { builder.Services.AddTransient(); + builder.Services.AddUnique(); + builder.Services.AddUnique(); + builder.Services.AddUnique(); return builder; } diff --git a/src/Umbraco.Cms.Api.Management/Services/FileSystem/FileSystemTreeServiceBase.cs b/src/Umbraco.Cms.Api.Management/Services/FileSystem/FileSystemTreeServiceBase.cs new file mode 100644 index 0000000000..43816e77b2 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Services/FileSystem/FileSystemTreeServiceBase.cs @@ -0,0 +1,102 @@ +using Umbraco.Cms.Api.Management.Extensions; +using Umbraco.Cms.Api.Management.ViewModels.FileSystem; +using Umbraco.Cms.Api.Management.ViewModels.Tree; +using Umbraco.Cms.Core.IO; +using Umbraco.Extensions; + +namespace Umbraco.Cms.Api.Management.Services.FileSystem; + +public abstract class FileSystemTreeServiceBase : IFileSystemTreeService +{ + protected abstract IFileSystem FileSystem { get; } + + public FileSystemTreeItemPresentationModel[] GetAncestorModels(string path, bool includeSelf) + { + var directories = path.Split(Path.DirectorySeparatorChar).Take(Range.EndAt(Index.FromEnd(1))).ToArray(); + var result = directories + .Select((directory, index) => MapViewModel(string.Join(Path.DirectorySeparatorChar, directories.Take(index + 1)), directory, true)) + .ToList(); + + if (includeSelf) + { + var selfIsFolder = FileSystem.FileExists(path) is false; + result.Add(MapViewModel(path, GetFileSystemItemName(selfIsFolder, path), selfIsFolder)); + } + + return result.ToArray(); + } + + public FileSystemTreeItemPresentationModel[] GetPathViewModels(string path, int skip, int take, out long totalItems) + { + path = path.VirtualPathToSystemPath(); + 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; + + FileSystemTreeItemPresentationModel ViewModel(string itemPath, bool isFolder) + => MapViewModel( + itemPath, + GetFileSystemItemName(isFolder, itemPath), + isFolder); + + return allItems + .Skip(skip) + .Take(take) + .Select(item => ViewModel(item.Path, item.IsFolder)) + .ToArray(); + } + + public FileSystemTreeItemPresentationModel[] GetSiblingsViewModels(string path, int before, int after, out long totalBefore, out long totalAfter) + { + var filePath = Path.GetDirectoryName(path); + var fileName = Path.GetFileName(path); + + FileSystemTreeItemPresentationModel[] viewModels = GetPathViewModels(filePath!, 0, int.MaxValue, out totalBefore); + FileSystemTreeItemPresentationModel? target = viewModels.FirstOrDefault(item => item.Name == fileName); + var position = Array.IndexOf(viewModels, target); + + totalBefore = position - before < 0 ? 0 : position - before; + totalAfter = (viewModels.Length - 1) - (position + after) < 0 ? 0 : (viewModels.Length - 1) - (position + after); + + return viewModels + .Select((item, index) => new { item, index }) + .Where(item => item.index >= position - before && item.index <= position + after) + .Select(item => item.item) + .ToArray(); + } + + public string[] GetDirectories(string path) => FileSystem + .GetDirectories(path) + .OrderBy(directory => directory) + .ToArray(); + + public string[] GetFiles(string path) => FileSystem + .GetFiles(path) + .OrderBy(file => file) + .ToArray(); + + public bool DirectoryHasChildren(string path) + => FileSystem.GetFiles(path).Any() || FileSystem.GetDirectories(path).Any(); + + public string GetFileSystemItemName(bool isFolder, string itemPath) => isFolder + ? Path.GetFileName(itemPath) + : FileSystem.GetFileName(itemPath); + + private FileSystemTreeItemPresentationModel MapViewModel(string path, string name, bool isFolder) + { + var parentPath = Path.GetDirectoryName(path); + return new FileSystemTreeItemPresentationModel + { + Path = path.SystemPathToVirtualPath(), + Name = name, + HasChildren = isFolder && DirectoryHasChildren(path), + IsFolder = isFolder, + Parent = parentPath.IsNullOrWhiteSpace() + ? null + : new FileSystemFolderModel { Path = parentPath.SystemPathToVirtualPath() } + }; + } +} diff --git a/src/Umbraco.Cms.Api.Management/Services/FileSystem/IFileSystemTreeService.cs b/src/Umbraco.Cms.Api.Management/Services/FileSystem/IFileSystemTreeService.cs new file mode 100644 index 0000000000..cdf96a4910 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Services/FileSystem/IFileSystemTreeService.cs @@ -0,0 +1,18 @@ +using Umbraco.Cms.Api.Management.ViewModels.Tree; +using Umbraco.Cms.Core.IO; + +namespace Umbraco.Cms.Api.Management.Services.FileSystem; + +public interface IFileSystemTreeService +{ + FileSystemTreeItemPresentationModel[] GetAncestorModels(string path, bool includeSelf); + + FileSystemTreeItemPresentationModel[] GetPathViewModels(string path, int skip, int take, out long totalItems); + + FileSystemTreeItemPresentationModel[] GetSiblingsViewModels(string path, int before, int after, out long totalBefore, + out long totalAfter); + + string[] GetDirectories(string path); + + string[] GetFiles(string path); +} diff --git a/src/Umbraco.Cms.Api.Management/Services/FileSystem/IPartialViewTreeService.cs b/src/Umbraco.Cms.Api.Management/Services/FileSystem/IPartialViewTreeService.cs new file mode 100644 index 0000000000..6f2e48e80d --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Services/FileSystem/IPartialViewTreeService.cs @@ -0,0 +1,5 @@ +namespace Umbraco.Cms.Api.Management.Services.FileSystem; + +public interface IPartialViewTreeService : IFileSystemTreeService +{ +} diff --git a/src/Umbraco.Cms.Api.Management/Services/FileSystem/IScriptTreeService.cs b/src/Umbraco.Cms.Api.Management/Services/FileSystem/IScriptTreeService.cs new file mode 100644 index 0000000000..1681b52cbb --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Services/FileSystem/IScriptTreeService.cs @@ -0,0 +1,5 @@ +namespace Umbraco.Cms.Api.Management.Services.FileSystem; + +public interface IScriptTreeService : IFileSystemTreeService +{ +} diff --git a/src/Umbraco.Cms.Api.Management/Services/FileSystem/IStyleSheetTreeService.cs b/src/Umbraco.Cms.Api.Management/Services/FileSystem/IStyleSheetTreeService.cs new file mode 100644 index 0000000000..4426be4a68 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Services/FileSystem/IStyleSheetTreeService.cs @@ -0,0 +1,7 @@ +using Umbraco.Cms.Core.IO; + +namespace Umbraco.Cms.Api.Management.Services.FileSystem; + +public interface IStyleSheetTreeService : IFileSystemTreeService +{ +} diff --git a/src/Umbraco.Cms.Api.Management/Services/FileSystem/PartialViewTreeService.cs b/src/Umbraco.Cms.Api.Management/Services/FileSystem/PartialViewTreeService.cs new file mode 100644 index 0000000000..3da299008c --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Services/FileSystem/PartialViewTreeService.cs @@ -0,0 +1,14 @@ +using Umbraco.Cms.Core.IO; + +namespace Umbraco.Cms.Api.Management.Services.FileSystem; + +public class PartialViewTreeService : FileSystemTreeServiceBase, IPartialViewTreeService +{ + private readonly IFileSystem _partialViewFileSystem; + + protected override IFileSystem FileSystem => _partialViewFileSystem; + + public PartialViewTreeService(FileSystems fileSystems) => + _partialViewFileSystem = fileSystems.PartialViewsFileSystem ?? + throw new ArgumentException("Missing partial views file system", nameof(fileSystems)); +} diff --git a/src/Umbraco.Cms.Api.Management/Services/FileSystem/ScriptTreeService.cs b/src/Umbraco.Cms.Api.Management/Services/FileSystem/ScriptTreeService.cs new file mode 100644 index 0000000000..ef870406f8 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Services/FileSystem/ScriptTreeService.cs @@ -0,0 +1,14 @@ +using Umbraco.Cms.Core.IO; + +namespace Umbraco.Cms.Api.Management.Services.FileSystem; + +public class ScriptTreeService : FileSystemTreeServiceBase, IScriptTreeService +{ + private readonly IFileSystem _scriptFileSystem; + + protected override IFileSystem FileSystem => _scriptFileSystem; + + public ScriptTreeService(FileSystems fileSystems) => + _scriptFileSystem = fileSystems.ScriptsFileSystem ?? + throw new ArgumentException("Missing partial views file system", nameof(fileSystems)); +} diff --git a/src/Umbraco.Cms.Api.Management/Services/FileSystem/StyleSheetTreeService.cs b/src/Umbraco.Cms.Api.Management/Services/FileSystem/StyleSheetTreeService.cs new file mode 100644 index 0000000000..ed14819231 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Services/FileSystem/StyleSheetTreeService.cs @@ -0,0 +1,14 @@ +using Umbraco.Cms.Core.IO; + +namespace Umbraco.Cms.Api.Management.Services.FileSystem; + +public class StyleSheetTreeService : FileSystemTreeServiceBase, IStyleSheetTreeService +{ + private readonly IFileSystem _scriptFileSystem; + + protected override IFileSystem FileSystem => _scriptFileSystem; + + public StyleSheetTreeService(FileSystems fileSystems) => + _scriptFileSystem = fileSystems.StylesheetsFileSystem ?? + throw new ArgumentException("Missing partial views file system", nameof(fileSystems)); +} diff --git a/src/Umbraco.Core/Persistence/Repositories/IEntityRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IEntityRepository.cs index 47cf15c3fc..9f8640c3c2 100644 --- a/src/Umbraco.Core/Persistence/Repositories/IEntityRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/IEntityRepository.cs @@ -46,6 +46,33 @@ public interface IEntityRepository : IRepository return []; } + /// + /// Gets trashed sibling entities of a specified target entity, within a given range before and after the target, ordered as specified. + /// + /// The object type keys of the entities. + /// The key of the target entity whose siblings are to be retrieved. + /// The number of siblings to retrieve before the target entity. + /// The number of siblings to retrieve after the target entity. + /// An optional filter to apply to the result set. + /// The ordering to apply to the siblings. + /// Outputs the total number of siblings before the target entity. + /// Outputs the total number of siblings after the target entity. + /// Enumerable of trashed sibling entities. + IEnumerable GetTrashedSiblings( + ISet objectTypes, + Guid targetKey, + int before, + int after, + IQuery? filter, + Ordering ordering, + out long totalBefore, + out long totalAfter) + { + totalBefore = 0; + totalAfter = 0; + return []; + } + /// /// Gets entities for a query /// diff --git a/src/Umbraco.Core/Services/EntityService.cs b/src/Umbraco.Core/Services/EntityService.cs index fe779507c4..c13111088e 100644 --- a/src/Umbraco.Core/Services/EntityService.cs +++ b/src/Umbraco.Core/Services/EntityService.cs @@ -359,6 +359,47 @@ public class EntityService : RepositoryService, IEntityService return siblings; } + /// + public IEnumerable GetTrashedSiblings( + Guid key, + IEnumerable objectTypes, + int before, + int after, + out long totalBefore, + out long totalAfter, + IQuery? filter = null, + Ordering? ordering = null) + { + if (before < 0) + { + throw new ArgumentOutOfRangeException(nameof(before), "The 'before' parameter must be greater than or equal to 0."); + } + + if (after < 0) + { + throw new ArgumentOutOfRangeException(nameof(after), "The 'after' parameter must be greater than or equal to 0."); + } + + ordering ??= new Ordering("sortOrder"); + + using ICoreScope scope = ScopeProvider.CreateCoreScope(); + + var objectTypeGuids = objectTypes.Select(x => x.GetGuid()).ToHashSet(); + + IEnumerable siblings = _entityRepository.GetTrashedSiblings( + objectTypeGuids, + key, + before, + after, + filter, + ordering, + out totalBefore, + out totalAfter); + + scope.Complete(); + return siblings; + } + /// public virtual IEnumerable GetDescendants(int id) { diff --git a/src/Umbraco.Core/Services/IEntityService.cs b/src/Umbraco.Core/Services/IEntityService.cs index 1099c4af74..cab5615103 100644 --- a/src/Umbraco.Core/Services/IEntityService.cs +++ b/src/Umbraco.Core/Services/IEntityService.cs @@ -171,7 +171,7 @@ public interface IEntityService IEntitySlim? GetParent(int id, UmbracoObjectTypes objectType); /// - /// Gets sibling entities of a specified target entity, within a given range before and after the target, ordered as specified. + /// Gets non-trashed sibling entities of a specified target entity, within a given range before and after the target, ordered as specified. /// /// The key of the target entity whose siblings are to be retrieved. /// The object types of the entities. @@ -181,7 +181,7 @@ public interface IEntityService /// The ordering to apply to the siblings. /// Outputs the total number of siblings before the target entity. /// Outputs the total number of siblings after the target entity. - /// Enumerable of sibling entities. + /// Enumerable of non-trashed sibling entities. IEnumerable GetSiblings( Guid key, IEnumerable objectTypes, @@ -197,6 +197,33 @@ public interface IEntityService return []; } + /// + /// Gets trashed sibling entities of a specified target entity, within a given range before and after the target, ordered as specified. + /// + /// The key of the target entity whose siblings are to be retrieved. + /// The object types of the entities. + /// The number of siblings to retrieve before the target entity. Needs to be greater or equal to 0. + /// The number of siblings to retrieve after the target entity. Needs to be greater or equal to 0. + /// An optional filter to apply to the result set. + /// The ordering to apply to the siblings. + /// Outputs the total number of siblings before the target entity. + /// Outputs the total number of siblings after the target entity. + /// Enumerable of trashed sibling entities. + IEnumerable GetTrashedSiblings( + Guid key, + IEnumerable objectTypes, + int before, + int after, + out long totalBefore, + out long totalAfter, + IQuery? filter = null, + Ordering? ordering = null) + { + totalBefore = 0; + totalAfter = 0; + return []; + } + /// /// Gets the children of an entity. /// diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/EntityRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/EntityRepository.cs index d98657578a..a5f76bbf47 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/EntityRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/EntityRepository.cs @@ -151,6 +151,72 @@ internal sealed class EntityRepository : RepositoryBase, IEntityRepositoryExtend Ordering ordering, out long totalBefore, out long totalAfter) + { + Sql? mainSql = SiblingsSql( + false, + objectTypes, + targetKey, + before, + after, + filter, + ordering, + out totalBefore, + out totalAfter); + + List? keys = Database.Fetch(mainSql); + + if (keys is null || keys.Count == 0) + { + return []; + } + + // To re-use this method we need to provide a single object type. By convention for folder based trees, we provide the primary object type last. + return PerformGetAll(objectTypes.ToArray(), ordering, sql => sql.WhereIn(x => x.UniqueId, keys)); + } + + /// + public IEnumerable GetTrashedSiblings( + ISet objectTypes, + Guid targetKey, + int before, + int after, + IQuery? filter, + Ordering ordering, + out long totalBefore, + out long totalAfter) + { + Sql? mainSql = SiblingsSql( + true, + objectTypes, + targetKey, + before, + after, + filter, + ordering, + out totalBefore, + out totalAfter); + + List? keys = Database.Fetch(mainSql); + + if (keys is null || keys.Count == 0) + { + return []; + } + + // To re-use this method we need to provide a single object type. By convention for folder based trees, we provide the primary object type last. + return PerformGetAll(objectTypes.ToArray(), ordering, sql => sql.WhereIn(x => x.UniqueId, keys)); + } + + private Sql? SiblingsSql( + bool isTrashed, + ISet objectTypes, + Guid targetKey, + int before, + int after, + IQuery? filter, + Ordering ordering, + out long totalBefore, + out long totalAfter) { // Ideally we don't want to have to do a second query for the parent ID, but the siblings query is already messy enough // without us also having to do a nested query for the parent ID too. @@ -170,7 +236,7 @@ internal sealed class EntityRepository : RepositoryBase, IEntityRepositoryExtend .Select($"ROW_NUMBER() OVER ({orderingSql.SQL}) AS rn") .AndSelect(n => n.UniqueId) .From() - .Where(x => x.ParentId == parentId && x.Trashed == false) + .Where(x => x.ParentId == parentId && x.Trashed == isTrashed) .WhereIn(x => x.NodeObjectType, objectTypes); // Apply the filter if provided. @@ -203,25 +269,16 @@ internal sealed class EntityRepository : RepositoryBase, IEntityRepositoryExtend var beforeAfterParameterIndex = BeforeAfterParameterIndex + beforeAfterParameterIndexOffset; var beforeArgumentsArray = beforeArguments.ToArray(); var afterArgumentsArray = afterArguments.ToArray(); - Sql? mainSql = Sql() + + totalBefore = GetNumberOfSiblingsOutsideSiblingRange(rowNumberSql, targetRowSql, beforeAfterParameterIndex, beforeArgumentsArray, true); + totalAfter = GetNumberOfSiblingsOutsideSiblingRange(rowNumberSql, targetRowSql, beforeAfterParameterIndex, afterArgumentsArray, false); + + return Sql() .Select("UniqueId") .From().AppendSubQuery(rowNumberSql, "NumberedNodes") .Where($"rn >= ({targetRowSql.SQL}) - @{beforeAfterParameterIndex}", beforeArgumentsArray) .Where($"rn <= ({targetRowSql.SQL}) + @{beforeAfterParameterIndex}", afterArgumentsArray) .OrderBy("rn"); - - List? keys = Database.Fetch(mainSql); - - totalBefore = GetNumberOfSiblingsOutsideSiblingRange(rowNumberSql, targetRowSql, beforeAfterParameterIndex, beforeArgumentsArray, true); - totalAfter = GetNumberOfSiblingsOutsideSiblingRange(rowNumberSql, targetRowSql, beforeAfterParameterIndex, afterArgumentsArray, false); - - if (keys is null || keys.Count == 0) - { - return []; - } - - // To re-use this method we need to provide a single object type. By convention for folder based trees, we provide the primary object type last. - return PerformGetAll(objectTypes.ToArray(), ordering, sql => sql.WhereIn(x => x.UniqueId, keys)); } private static int GetBeforeAfterParameterOffset(ISet objectTypes, IQuery? filter) diff --git a/tests/Umbraco.Tests.Integration/ManagementApi/Services/Trees/FileSystemTreeServiceTestsBase.cs b/tests/Umbraco.Tests.Integration/ManagementApi/Services/Trees/FileSystemTreeServiceTestsBase.cs new file mode 100644 index 0000000000..e328b90d41 --- /dev/null +++ b/tests/Umbraco.Tests.Integration/ManagementApi/Services/Trees/FileSystemTreeServiceTestsBase.cs @@ -0,0 +1,83 @@ +using System.Text; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; +using NUnit.Framework; +using Umbraco.Cms.Core.Configuration.Models; +using Umbraco.Cms.Core.Hosting; +using Umbraco.Cms.Core.IO; +using Umbraco.Cms.Tests.Common.TestHelpers; +using Umbraco.Cms.Tests.Integration.Testing; + +namespace Umbraco.Cms.Tests.Integration.ManagementApi.Services.Trees; + +public abstract class FileSystemTreeServiceTestsBase : UmbracoIntegrationTest +{ + protected FileSystems FileSystems { get; private set; } + + protected IFileSystem TestFileSystem { get; private set; } + + protected abstract string FileSystemPath { get; } + + protected IHostingEnvironment HostingEnvironment => GetRequiredService(); + + [SetUp] + public void SetUpFileSystem() + { + TestFileSystem = new PhysicalFileSystem(IOHelper, HostingEnvironment, LoggerFactory.CreateLogger(), HostingEnvironment.MapPathWebRoot(FileSystemPath), HostingEnvironment.ToAbsolute(FileSystemPath)); + + FileSystems = FileSystemsCreator.CreateTestFileSystems( + LoggerFactory, + IOHelper, + GetRequiredService>(), + HostingEnvironment, + GetPartialViewsFileSystem(), + GetStylesheetsFileSystem(), + GetScriptsFileSystem(), + null); + for (int i = 0; i < 10; i++) + { + using var stream = CreateStream(Path.Join("tests")); + TestFileSystem.AddFile($"file{i}", stream); + } + } + + private static Stream CreateStream(string contents = null) + { + if (string.IsNullOrEmpty(contents)) + { + contents = "/* test */"; + } + + var bytes = Encoding.UTF8.GetBytes(contents); + return new MemoryStream(bytes); + } + + protected virtual IFileSystem? GetPartialViewsFileSystem() => null; + + protected virtual IFileSystem? GetStylesheetsFileSystem() => null; + + protected virtual IFileSystem? GetScriptsFileSystem() => null; + + [TearDown] + public void TearDownFileSystem() + { + Purge(TestFileSystem, string.Empty); + FileSystems = null; + } + + private static void Purge(IFileSystem fs, string path) + { + var files = fs.GetFiles(path, "*"); + foreach (var file in files) + { + fs.DeleteFile(file); + } + + var dirs = fs.GetDirectories(path); + foreach (var dir in dirs) + { + Purge(fs, dir); + fs.DeleteDirectory(dir); + } + } +} diff --git a/tests/Umbraco.Tests.Integration/ManagementApi/Services/Trees/PartialViewTreeServiceTests.cs b/tests/Umbraco.Tests.Integration/ManagementApi/Services/Trees/PartialViewTreeServiceTests.cs new file mode 100644 index 0000000000..0dbe9f39a3 --- /dev/null +++ b/tests/Umbraco.Tests.Integration/ManagementApi/Services/Trees/PartialViewTreeServiceTests.cs @@ -0,0 +1,54 @@ +using NUnit.Framework; +using Umbraco.Cms.Api.Management.Services.FileSystem; +using Umbraco.Cms.Api.Management.ViewModels.Tree; +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.IO; + +namespace Umbraco.Cms.Tests.Integration.ManagementApi.Services.Trees; + +public class PartialViewTreeServiceTests : FileSystemTreeServiceTestsBase +{ + protected override string FileSystemPath => Constants.SystemDirectories.PartialViews; + + protected override IFileSystem? GetPartialViewsFileSystem() => TestFileSystem; + + [Test] + public void Can_Get_Siblings_From_PartialView_Tree_Service() + { + var service = new PartialViewTreeService(FileSystems); + + FileSystemTreeItemPresentationModel[] treeModel = service.GetSiblingsViewModels("file5", 1, 1, out long before, out var after); + int index = Array.FindIndex(treeModel, item => item.Name == "file5"); + + Assert.AreEqual(treeModel[index].Name, "file5"); + Assert.AreEqual(treeModel[index - 1].Name, "file4"); + Assert.AreEqual(treeModel[index + 1].Name, "file6"); + Assert.That(treeModel.Length == 3); + Assert.AreEqual(after, 3); + Assert.AreEqual(before, 4); + } + + [Test] + public void Can_Get_Ancestors_From_StyleSheet_Tree_Service() + { + var service = new PartialViewTreeService(FileSystems); + + var path = Path.Join("tests", "file5"); + FileSystemTreeItemPresentationModel[] treeModel = service.GetAncestorModels(path, true); + + Assert.IsNotEmpty(treeModel); + Assert.AreEqual(treeModel.Length, 2); + Assert.AreEqual(treeModel[0].Name, "tests"); + } + + [Test] + public void Can_Get_PathViewModels_From_StyleSheet_Tree_Service() + { + var service = new PartialViewTreeService(FileSystems); + + FileSystemTreeItemPresentationModel[] treeModels = service.GetPathViewModels(string.Empty, 0, Int32.MaxValue, out var totalItems); + + Assert.IsNotEmpty(treeModels); + Assert.AreEqual(treeModels.Length, totalItems); + } +} diff --git a/tests/Umbraco.Tests.Integration/ManagementApi/Services/Trees/ScriptTreeServiceTests.cs b/tests/Umbraco.Tests.Integration/ManagementApi/Services/Trees/ScriptTreeServiceTests.cs new file mode 100644 index 0000000000..481fbd609b --- /dev/null +++ b/tests/Umbraco.Tests.Integration/ManagementApi/Services/Trees/ScriptTreeServiceTests.cs @@ -0,0 +1,53 @@ +using NUnit.Framework; +using Umbraco.Cms.Api.Management.Services.FileSystem; +using Umbraco.Cms.Api.Management.ViewModels.Tree; +using Umbraco.Cms.Core.IO; + +namespace Umbraco.Cms.Tests.Integration.ManagementApi.Services.Trees; + +public class ScriptTreeServiceTests : FileSystemTreeServiceTestsBase +{ + protected override string FileSystemPath => GlobalSettings.UmbracoScriptsPath; + + protected override IFileSystem? GetScriptsFileSystem() => TestFileSystem; + + [Test] + public void Can_Get_Siblings_From_Script_Tree_Service() + { + var service = new ScriptTreeService(FileSystems); + + FileSystemTreeItemPresentationModel[] treeModel = service.GetSiblingsViewModels("file5", 1, 1, out long before, out var after); + int index = Array.FindIndex(treeModel, item => item.Name == "file5"); + + Assert.AreEqual(treeModel[index].Name, "file5"); + Assert.AreEqual(treeModel[index - 1].Name, "file4"); + Assert.AreEqual(treeModel[index + 1].Name, "file6"); + Assert.That(treeModel.Length == 3); + Assert.AreEqual(after, 3); + Assert.AreEqual(before, 4); + } + + [Test] + public void Can_Get_Ancestors_From_StyleSheet_Tree_Service() + { + var service = new ScriptTreeService(FileSystems); + + var path = Path.Join("tests", "file5"); + FileSystemTreeItemPresentationModel[] treeModel = service.GetAncestorModels(path, true); + + Assert.IsNotEmpty(treeModel); + Assert.AreEqual(treeModel.Length, 2); + Assert.AreEqual(treeModel[0].Name, "tests"); + } + + [Test] + public void Can_Get_PathViewModels_From_StyleSheet_Tree_Service() + { + var service = new ScriptTreeService(FileSystems); + + FileSystemTreeItemPresentationModel[] treeModels = service.GetPathViewModels(string.Empty, 0, Int32.MaxValue, out var totalItems); + + Assert.IsNotEmpty(treeModels); + Assert.AreEqual(treeModels.Length, totalItems); + } +} diff --git a/tests/Umbraco.Tests.Integration/ManagementApi/Services/Trees/StyleSheetTreeServiceTests.cs b/tests/Umbraco.Tests.Integration/ManagementApi/Services/Trees/StyleSheetTreeServiceTests.cs new file mode 100644 index 0000000000..2fe15f27ea --- /dev/null +++ b/tests/Umbraco.Tests.Integration/ManagementApi/Services/Trees/StyleSheetTreeServiceTests.cs @@ -0,0 +1,53 @@ +using NUnit.Framework; +using Umbraco.Cms.Api.Management.Services.FileSystem; +using Umbraco.Cms.Api.Management.ViewModels.Tree; +using Umbraco.Cms.Core.IO; + +namespace Umbraco.Cms.Tests.Integration.ManagementApi.Services.Trees; + +public class StyleSheetTreeServiceTests : FileSystemTreeServiceTestsBase +{ + protected override string FileSystemPath => GlobalSettings.UmbracoCssPath; + + protected override IFileSystem? GetStylesheetsFileSystem() => TestFileSystem; + + [Test] + public void Can_Get_Siblings_From_StyleSheet_Tree_Service() + { + var service = new StyleSheetTreeService(FileSystems); + + FileSystemTreeItemPresentationModel[] treeModel = service.GetSiblingsViewModels("file5", 1, 1, out long before, out var after); + int index = Array.FindIndex(treeModel, item => item.Name == "file5"); + + Assert.AreEqual(treeModel[index].Name, "file5"); + Assert.AreEqual(treeModel[index - 1].Name, "file4"); + Assert.AreEqual(treeModel[index + 1].Name, "file6"); + Assert.That(treeModel.Length == 3); + Assert.AreEqual(after, 3); + Assert.AreEqual(before, 4); + } + + [Test] + public void Can_Get_Ancestors_From_StyleSheet_Tree_Service() + { + var service = new StyleSheetTreeService(FileSystems); + + var path = Path.Join("tests", "file5"); + FileSystemTreeItemPresentationModel[] treeModel = service.GetAncestorModels(path, true); + + Assert.IsNotEmpty(treeModel); + Assert.AreEqual(treeModel.Length, 2); + Assert.AreEqual(treeModel[0].Name, "tests"); + } + + [Test] + public void Can_Get_PathViewModels_From_StyleSheet_Tree_Service() + { + var service = new StyleSheetTreeService(FileSystems); + + FileSystemTreeItemPresentationModel[] treeModels = service.GetPathViewModels(string.Empty, 0, Int32.MaxValue, out var totalItems); + + Assert.IsNotEmpty(treeModels); + Assert.AreEqual(treeModels.Length, totalItems); + } +} diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/EntityServiceTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/EntityServiceTests.cs index 090cc3c1f0..7cd63c18d1 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/EntityServiceTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/EntityServiceTests.cs @@ -972,6 +972,27 @@ internal sealed class EntityServiceTests : UmbracoIntegrationTest Assert.IsTrue(result[2].Key == children[3].Key); } + [Test] + public void EntityService_Siblings_Returns_Trashed_Siblings() + { + ContentService.EmptyRecycleBin(); + var children = CreateDocumentSiblingsTestData(); + + for (int i = 0; i <= 3; i++) + { + ContentService.MoveToRecycleBin(children[i]); + } + + var result = EntityService.GetTrashedSiblings(children[1].Key, [UmbracoObjectTypes.Document], 1, 1, out long totalBefore, out long totalAfter).ToArray(); + Assert.AreEqual(0, totalBefore); + Assert.AreEqual(1, totalAfter); + Assert.AreEqual(3, result.Length); + Assert.IsTrue(result[0].Key == children[0].Key); + Assert.IsTrue(result[1].Key == children[1].Key); + Assert.IsTrue(result[2].Key == children[2].Key); + Assert.IsFalse(result.Any(x => x.Key == children[3].Key)); + } + [Test] public void EntityService_Siblings_SkipsFilteredEntities_UsingFilterWithSet() {