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 33e451bdc5bf..cacf862b57bb 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.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 @@ protected DocumentTreeControllerBase( AppCaches appCaches, IBackOfficeSecurityAccessor backofficeSecurityAccessor, IDocumentPresentationFactory documentPresentationFactory) - : base(entityService, userStartNodeEntitiesService, dataTypeService) + : base(entityService, userStartNodeEntitiesService, dataTypeService) { _publicAccessService = publicAccessService; _appCaches = appCaches; @@ -52,6 +53,8 @@ protected override DocumentTreeItemResponseModel MapTreeItemViewModel(Guid? pare 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 228952b46960..b63704cbb260 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 @@ protected override Task SetUnmappedProperties(ListViewPagedModel conte } 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 dbc51a6f4197..15eb6bafd8ec 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 @@ private void Map(IContent source, PublishedDocumentResponseModel target, MapperC target.IsTrashed = source.Trashed; } - // Umbraco.Code.MapAll -IsProtected + // Umbraco.Code.MapAll -IsProtected -Ancestors private void Map(IContent source, DocumentCollectionResponseModel target, MapperContext context) { target.Id = source.Key; diff --git a/src/Umbraco.Cms.Api.Management/OpenApi.json b/src/Umbraco.Cms.Api.Management/OpenApi.json index 44a69ccc957e..665c1f05d507 100644 --- a/src/Umbraco.Cms.Api.Management/OpenApi.json +++ b/src/Umbraco.Cms.Api.Management/OpenApi.json @@ -37088,6 +37088,7 @@ }, "DocumentCollectionResponseModel": { "required": [ + "ancestors", "documentType", "id", "isProtected", @@ -37143,6 +37144,16 @@ "isProtected": { "type": "boolean" }, + "ancestors": { + "type": "array", + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/ReferenceByIdModel" + } + ] + } + }, "updater": { "type": "string", "nullable": true @@ -37456,6 +37467,7 @@ }, "DocumentTreeItemResponseModel": { "required": [ + "ancestors", "createDate", "documentType", "hasChildren", @@ -37495,6 +37507,16 @@ "isProtected": { "type": "boolean" }, + "ancestors": { + "type": "array", + "items": { + "oneOf": [ + { + "$ref": "#/components/schemas/ReferenceByIdModel" + } + ] + } + }, "documentType": { "oneOf": [ { diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/Document/Collection/DocumentCollectionResponseModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/Document/Collection/DocumentCollectionResponseModel.cs index a7c209ad2a20..391714346a9f 100644 --- a/src/Umbraco.Cms.Api.Management/ViewModels/Document/Collection/DocumentCollectionResponseModel.cs +++ b/src/Umbraco.Cms.Api.Management/ViewModels/Document/Collection/DocumentCollectionResponseModel.cs @@ -11,5 +11,7 @@ public class DocumentCollectionResponseModel : ContentCollectionResponseModelBas public bool IsProtected { get; set; } + public IEnumerable 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 094522b91ab2..1bde7631025b 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 7f2ba473b7a1..f2fe2eefd871 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 IEnumerable GetPagedChildren( 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 08ff2feb8c46..964ec9f502b3 100644 --- a/src/Umbraco.Core/Services/IEntityService.cs +++ b/src/Umbraco.Core/Services/IEntityService.cs @@ -382,4 +382,12 @@ IEnumerable GetPagedDescendants( /// 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 d18caf52b419..0da6ba13d75f 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/EntityServiceTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/EntityServiceTests.cs @@ -870,6 +870,27 @@ public void ReserveId() 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;