From 6247f549768547411bee428020f68d82af413d47 Mon Sep 17 00:00:00 2001 From: Andy Butland Date: Fri, 4 Apr 2025 06:51:29 +0200 Subject: [PATCH] Adds ancestor ID details on document tree and collection responses (#18909) * Populate ancestor keys on document tree response items. * Populate ancestor keys on document collection response items. * Update OpenApi.json * Use array of objects rather than Ids for the ancestor collection. * Update OpenApi.json. --- .../Tree/DocumentTreeControllerBase.cs | 5 +++- .../DocumentCollectionPresentationFactory.cs | 19 ++++++++++++++- .../Mapping/Document/DocumentMapDefinition.cs | 2 +- src/Umbraco.Cms.Api.Management/OpenApi.json | 22 +++++++++++++++++ .../DocumentCollectionResponseModel.cs | 2 ++ .../Tree/DocumentTreeItemResponseModel.cs | 3 ++- src/Umbraco.Core/Services/EntityService.cs | 24 ++++++++++++++++++- src/Umbraco.Core/Services/IEntityService.cs | 8 +++++++ .../Services/EntityServiceTests.cs | 21 ++++++++++++++++ 9 files changed, 101 insertions(+), 5 deletions(-) diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Document/Tree/DocumentTreeControllerBase.cs b/src/Umbraco.Cms.Api.Management/Controllers/Document/Tree/DocumentTreeControllerBase.cs index 33e451bdc5..cacf862b57 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/Document/Tree/DocumentTreeControllerBase.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/Document/Tree/DocumentTreeControllerBase.cs @@ -4,6 +4,7 @@ using Umbraco.Cms.Api.Management.Controllers.Tree; using Umbraco.Cms.Api.Management.Factories; using Umbraco.Cms.Api.Management.Routing; using Umbraco.Cms.Api.Management.Services.Entities; +using Umbraco.Cms.Api.Management.ViewModels; using Umbraco.Cms.Api.Management.ViewModels.Tree; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Cache; @@ -33,7 +34,7 @@ public abstract class DocumentTreeControllerBase : UserStartNodeTreeControllerBa AppCaches appCaches, IBackOfficeSecurityAccessor backofficeSecurityAccessor, IDocumentPresentationFactory documentPresentationFactory) - : base(entityService, userStartNodeEntitiesService, dataTypeService) + : base(entityService, userStartNodeEntitiesService, dataTypeService) { _publicAccessService = publicAccessService; _appCaches = appCaches; @@ -52,6 +53,8 @@ public abstract class DocumentTreeControllerBase : UserStartNodeTreeControllerBa if (entity is IDocumentEntitySlim documentEntitySlim) { responseModel.IsProtected = _publicAccessService.IsProtected(entity.Path); + responseModel.Ancestors = EntityService.GetPathKeys(entity, omitSelf: true) + .Select(x => new ReferenceByIdModel(x)); responseModel.IsTrashed = entity.Trashed; responseModel.Id = entity.Key; responseModel.CreateDate = entity.CreateDate; diff --git a/src/Umbraco.Cms.Api.Management/Factories/DocumentCollectionPresentationFactory.cs b/src/Umbraco.Cms.Api.Management/Factories/DocumentCollectionPresentationFactory.cs index 228952b469..b63704cbb2 100644 --- a/src/Umbraco.Cms.Api.Management/Factories/DocumentCollectionPresentationFactory.cs +++ b/src/Umbraco.Cms.Api.Management/Factories/DocumentCollectionPresentationFactory.cs @@ -1,5 +1,8 @@ +using Microsoft.Extensions.DependencyInjection; +using Umbraco.Cms.Api.Management.ViewModels; using Umbraco.Cms.Api.Management.ViewModels.Document; using Umbraco.Cms.Api.Management.ViewModels.Document.Collection; +using Umbraco.Cms.Core.DependencyInjection; using Umbraco.Cms.Core.Mapping; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Services; @@ -9,11 +12,23 @@ namespace Umbraco.Cms.Api.Management.Factories; public class DocumentCollectionPresentationFactory : ContentCollectionPresentationFactory, IDocumentCollectionPresentationFactory { private readonly IPublicAccessService _publicAccessService; + private readonly IEntityService _entityService; + [Obsolete("Please use the non-obsolete constructor. Scheduled for removal in V17.")] public DocumentCollectionPresentationFactory(IUmbracoMapper mapper, IPublicAccessService publicAccessService) - : base(mapper) + : this( + mapper, + publicAccessService, + StaticServiceProvider.Instance.GetRequiredService()) + { + } + + [ActivatorUtilitiesConstructor] + public DocumentCollectionPresentationFactory(IUmbracoMapper mapper, IPublicAccessService publicAccessService, IEntityService entityService) + : base(mapper) { _publicAccessService = publicAccessService; + _entityService = entityService; } protected override Task SetUnmappedProperties(ListViewPagedModel contentCollection, List collectionResponseModels) @@ -27,6 +42,8 @@ public class DocumentCollectionPresentationFactory : ContentCollectionPresentati } item.IsProtected = _publicAccessService.IsProtected(matchingContentItem).Success; + item.Ancestors = _entityService.GetPathKeys(matchingContentItem, omitSelf: true) + .Select(x => new ReferenceByIdModel(x)); } return Task.CompletedTask; diff --git a/src/Umbraco.Cms.Api.Management/Mapping/Document/DocumentMapDefinition.cs b/src/Umbraco.Cms.Api.Management/Mapping/Document/DocumentMapDefinition.cs index dbc51a6f41..15eb6bafd8 100644 --- a/src/Umbraco.Cms.Api.Management/Mapping/Document/DocumentMapDefinition.cs +++ b/src/Umbraco.Cms.Api.Management/Mapping/Document/DocumentMapDefinition.cs @@ -67,7 +67,7 @@ public class DocumentMapDefinition : ContentMapDefinition Ancestors { get; set; } = []; + public string? Updater { get; set; } } diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/Tree/DocumentTreeItemResponseModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/Tree/DocumentTreeItemResponseModel.cs index 094522b91a..1bde763102 100644 --- a/src/Umbraco.Cms.Api.Management/ViewModels/Tree/DocumentTreeItemResponseModel.cs +++ b/src/Umbraco.Cms.Api.Management/ViewModels/Tree/DocumentTreeItemResponseModel.cs @@ -1,4 +1,3 @@ -using Umbraco.Cms.Api.Management.ViewModels.Content; using Umbraco.Cms.Api.Management.ViewModels.Document; using Umbraco.Cms.Api.Management.ViewModels.DocumentType; @@ -8,6 +7,8 @@ public class DocumentTreeItemResponseModel : ContentTreeItemResponseModel { public bool IsProtected { get; set; } + public IEnumerable Ancestors { get; set; } = []; + public DocumentTypeReferenceResponseModel DocumentType { get; set; } = new(); public IEnumerable Variants { get; set; } = Enumerable.Empty(); diff --git a/src/Umbraco.Core/Services/EntityService.cs b/src/Umbraco.Core/Services/EntityService.cs index 7f2ba473b7..f2fe2eefd8 100644 --- a/src/Umbraco.Core/Services/EntityService.cs +++ b/src/Umbraco.Core/Services/EntityService.cs @@ -1,3 +1,4 @@ +using System.Globalization; using System.Linq.Expressions; using Microsoft.Extensions.Logging; using Umbraco.Cms.Core.Events; @@ -758,5 +759,26 @@ public class EntityService : RepositoryService, IEntityService return _entityRepository.GetPagedResultsByQuery(query, objectTypeGuids, pageNumber, pageSize, out totalRecords, filter, ordering); } } -} + /// > + public Guid[] GetPathKeys(ITreeEntity entity, bool omitSelf = false) + { + IEnumerable ids = entity.Path.Split(Constants.CharArrays.Comma, StringSplitOptions.RemoveEmptyEntries) + .Select(x => int.TryParse(x, NumberStyles.Integer, CultureInfo.InvariantCulture, out var val) ? val : -1) + .Where(x => x != -1); + + Guid[] keys = ids + .Select(x => _idKeyMap.GetKeyForId(x, UmbracoObjectTypes.Document)) + .Where(x => x.Success) + .Select(x => x.Result) + .ToArray(); + + if (omitSelf) + { + // Omit the last path key as that will be for the item itself. + return keys.Take(keys.Length - 1).ToArray(); + } + + return keys; + } +} diff --git a/src/Umbraco.Core/Services/IEntityService.cs b/src/Umbraco.Core/Services/IEntityService.cs index 08ff2feb8c..964ec9f502 100644 --- a/src/Umbraco.Core/Services/IEntityService.cs +++ b/src/Umbraco.Core/Services/IEntityService.cs @@ -382,4 +382,12 @@ public interface IEntityService /// The identifier. /// When a new content or a media is saved with the key, it will have the reserved identifier. int ReserveId(Guid key); + + /// + /// Gets the GUID keys for an entity's path (provided as a comma separated list of integer Ids). + /// + /// The entity. + /// A value indicating whether to omit the entity's own key from the result. + /// The path with each ID converted to a GUID. + Guid[] GetPathKeys(ITreeEntity entity, bool omitSelf = false) => []; } diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/EntityServiceTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/EntityServiceTests.cs index 20df8e690a..b6b7f2cb64 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/EntityServiceTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/EntityServiceTests.cs @@ -910,6 +910,27 @@ public class EntityServiceTests : UmbracoIntegrationTest Assert.IsFalse(EntityService.GetId(Guid.NewGuid(), UmbracoObjectTypes.DocumentType).Success); } + [Test] + public void EntityService_GetPathKeys_ReturnsExpectedKeys() + { + var contentType = ContentTypeService.Get("umbTextpage"); + + var root = ContentBuilder.CreateSimpleContent(contentType); + ContentService.Save(root); + + var child = ContentBuilder.CreateSimpleContent(contentType, Guid.NewGuid().ToString(), root); + ContentService.Save(child); + var grandChild = ContentBuilder.CreateSimpleContent(contentType, Guid.NewGuid().ToString(), child); + ContentService.Save(grandChild); + + var result = EntityService.GetPathKeys(grandChild); + Assert.AreEqual($"{root.Key},{child.Key},{grandChild.Key}", string.Join(",", result)); + + var result2 = EntityService.GetPathKeys(grandChild, omitSelf: true); + Assert.AreEqual($"{root.Key},{child.Key}", string.Join(",", result2)); + + } + private static bool _isSetup; private int _folderId;