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.
This commit is contained in:
Andy Butland
2025-04-04 06:51:29 +02:00
committed by GitHub
parent 3e6b9313e5
commit 6247f54976
9 changed files with 101 additions and 5 deletions

View File

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

View File

@@ -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<IContent, DocumentCollectionResponseModel, DocumentValueResponseModel, DocumentVariantResponseModel>, 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<IEntityService>())
{
}
[ActivatorUtilitiesConstructor]
public DocumentCollectionPresentationFactory(IUmbracoMapper mapper, IPublicAccessService publicAccessService, IEntityService entityService)
: base(mapper)
{
_publicAccessService = publicAccessService;
_entityService = entityService;
}
protected override Task SetUnmappedProperties(ListViewPagedModel<IContent> contentCollection, List<DocumentCollectionResponseModel> 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;

View File

@@ -67,7 +67,7 @@ public class DocumentMapDefinition : ContentMapDefinition<IContent, DocumentValu
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;

View File

@@ -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": [
{

View File

@@ -11,5 +11,7 @@ public class DocumentCollectionResponseModel : ContentCollectionResponseModelBas
public bool IsProtected { get; set; }
public IEnumerable<ReferenceByIdModel> Ancestors { get; set; } = [];
public string? Updater { get; set; }
}

View File

@@ -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<ReferenceByIdModel> Ancestors { get; set; } = [];
public DocumentTypeReferenceResponseModel DocumentType { get; set; } = new();
public IEnumerable<DocumentVariantItemResponseModel> Variants { get; set; } = Enumerable.Empty<DocumentVariantItemResponseModel>();

View File

@@ -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);
}
}
}
/// <inheritdoc/>>
public Guid[] GetPathKeys(ITreeEntity entity, bool omitSelf = false)
{
IEnumerable<int> 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;
}
}

View File

@@ -382,4 +382,12 @@ public interface IEntityService
/// <returns>The identifier.</returns>
/// <remarks>When a new content or a media is saved with the key, it will have the reserved identifier.</remarks>
int ReserveId(Guid key);
/// <summary>
/// Gets the GUID keys for an entity's path (provided as a comma separated list of integer Ids).
/// </summary>
/// <param name="entity">The entity.</param>
/// <param name="omitSelf">A value indicating whether to omit the entity's own key from the result.</param>
/// <returns>The path with each ID converted to a GUID.</returns>
Guid[] GetPathKeys(ITreeEntity entity, bool omitSelf = false) => [];
}

View File

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