From 1817900976bb91cd62698726ffbc2c5c3c8ba243 Mon Sep 17 00:00:00 2001 From: Andy Butland Date: Tue, 1 Apr 2025 11:44:27 +0200 Subject: [PATCH 01/10] Updated management API endpoint and model for data type references to align with that used for documents, media etc. --- .../ReferencedByDataTypeController.cs | 47 +++++ .../DataType/ReferencesDataTypeController.cs | 3 +- .../RelationTypePresentationFactory.cs | 9 +- ...TrackedReferenceViewModelsMapDefinition.cs | 45 +++++ src/Umbraco.Cms.Api.Management/OpenApi.json | 191 ++++++++++++++++++ .../ContentTypeReferenceResponseModel.cs | 6 + ...umentTypePropertyReferenceResponseModel.cs | 6 + ...MediaTypePropertyReferenceResponseModel.cs | 6 + ...emberTypePropertyReferenceResponseModel.cs | 6 + src/Umbraco.Core/Constants-ReferenceTypes.cs | 25 +++ src/Umbraco.Core/Models/RelationItemModel.cs | 4 +- .../Repositories/IDataTypeRepository.cs | 1 - src/Umbraco.Core/Services/DataTypeService.cs | 132 ++++++++++++ src/Umbraco.Core/Services/IDataTypeService.cs | 18 ++ .../Builders/MediaTypeBuilder.cs | 1 + .../Services/DataTypeServiceTests.cs | 39 ++++ 16 files changed, 533 insertions(+), 6 deletions(-) create mode 100644 src/Umbraco.Cms.Api.Management/Controllers/DataType/References/ReferencedByDataTypeController.cs create mode 100644 src/Umbraco.Cms.Api.Management/ViewModels/TrackedReferences/ContentTypeReferenceResponseModel.cs create mode 100644 src/Umbraco.Cms.Api.Management/ViewModels/TrackedReferences/DocumentTypePropertyReferenceResponseModel.cs create mode 100644 src/Umbraco.Cms.Api.Management/ViewModels/TrackedReferences/MediaTypePropertyReferenceResponseModel.cs create mode 100644 src/Umbraco.Cms.Api.Management/ViewModels/TrackedReferences/MemberTypePropertyReferenceResponseModel.cs create mode 100644 src/Umbraco.Core/Constants-ReferenceTypes.cs diff --git a/src/Umbraco.Cms.Api.Management/Controllers/DataType/References/ReferencedByDataTypeController.cs b/src/Umbraco.Cms.Api.Management/Controllers/DataType/References/ReferencedByDataTypeController.cs new file mode 100644 index 000000000000..809a45a724a5 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Controllers/DataType/References/ReferencedByDataTypeController.cs @@ -0,0 +1,47 @@ +using Asp.Versioning; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Api.Common.ViewModels.Pagination; +using Umbraco.Cms.Api.Management.Controllers.DataType; +using Umbraco.Cms.Api.Management.Factories; +using Umbraco.Cms.Api.Management.ViewModels.TrackedReferences; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Services; + +namespace Umbraco.Cms.Api.Management.Controllers.DateType.References; + +[ApiVersion("1.0")] +public class ReferencedByDataTypeController : DataTypeControllerBase +{ + private readonly IDataTypeService _dataTypeService; + private readonly IRelationTypePresentationFactory _relationTypePresentationFactory; + + public ReferencedByDataTypeController(IDataTypeService dataTypeService, IRelationTypePresentationFactory relationTypePresentationFactory) + { + _dataTypeService = dataTypeService; + _relationTypePresentationFactory = relationTypePresentationFactory; + } + + /// + /// Gets a paged list of references for the current data type, so you can see where is is being used. + /// + [HttpGet("{id:guid}/referenced-by")] + [MapToApiVersion("1.0")] + [ProducesResponseType(typeof(PagedViewModel), StatusCodes.Status200OK)] + public async Task>> ReferencedBy( + CancellationToken cancellationToken, + Guid id, + int skip = 0, + int take = 20) + { + PagedModel relationItems = await _dataTypeService.GetPagedRelationsAsync(id, skip, take); + + var pagedViewModel = new PagedViewModel + { + Total = relationItems.Total, + Items = await _relationTypePresentationFactory.CreateReferenceResponseModelsAsync(relationItems.Items), + }; + + return pagedViewModel; + } +} diff --git a/src/Umbraco.Cms.Api.Management/Controllers/DataType/ReferencesDataTypeController.cs b/src/Umbraco.Cms.Api.Management/Controllers/DataType/ReferencesDataTypeController.cs index c25586e93cad..0eee28e49b80 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/DataType/ReferencesDataTypeController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/DataType/ReferencesDataTypeController.cs @@ -1,4 +1,4 @@ -using Asp.Versioning; +using Asp.Versioning; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Umbraco.Cms.Api.Management.Factories; @@ -10,6 +10,7 @@ namespace Umbraco.Cms.Api.Management.Controllers.DataType; [ApiVersion("1.0")] +[Obsolete("Please use ReferencedByDataTypeController and the referenced-by endpoint. Scheduled for removal in Umbraco 17.")] public class ReferencesDataTypeController : DataTypeControllerBase { private readonly IDataTypeService _dataTypeService; diff --git a/src/Umbraco.Cms.Api.Management/Factories/RelationTypePresentationFactory.cs b/src/Umbraco.Cms.Api.Management/Factories/RelationTypePresentationFactory.cs index a6fb1cbae85f..8fc2309b0f21 100644 --- a/src/Umbraco.Cms.Api.Management/Factories/RelationTypePresentationFactory.cs +++ b/src/Umbraco.Cms.Api.Management/Factories/RelationTypePresentationFactory.cs @@ -56,9 +56,12 @@ public async Task> CreateReferenceResponseM IReferenceResponseModel[] result = relationItemModelsCollection.Select(relationItemModel => relationItemModel.NodeType switch { - Constants.UdiEntityType.Document => MapDocumentReference(relationItemModel, slimEntities), - Constants.UdiEntityType.Media => _umbracoMapper.Map(relationItemModel), - Constants.UdiEntityType.Member => _umbracoMapper.Map(relationItemModel), + Constants.ReferenceType.Document => MapDocumentReference(relationItemModel, slimEntities), + Constants.ReferenceType.Media => _umbracoMapper.Map(relationItemModel), + Constants.ReferenceType.Member => _umbracoMapper.Map(relationItemModel), + Constants.ReferenceType.DocumentTypeProperty => _umbracoMapper.Map(relationItemModel), + Constants.ReferenceType.MediaTypeProperty => _umbracoMapper.Map(relationItemModel), + Constants.ReferenceType.MemberTypeProperty => _umbracoMapper.Map(relationItemModel), _ => _umbracoMapper.Map(relationItemModel), }).WhereNotNull().ToArray(); diff --git a/src/Umbraco.Cms.Api.Management/Mapping/TrackedReferences/TrackedReferenceViewModelsMapDefinition.cs b/src/Umbraco.Cms.Api.Management/Mapping/TrackedReferences/TrackedReferenceViewModelsMapDefinition.cs index e9f2700f5ad2..9143ba810ddc 100644 --- a/src/Umbraco.Cms.Api.Management/Mapping/TrackedReferences/TrackedReferenceViewModelsMapDefinition.cs +++ b/src/Umbraco.Cms.Api.Management/Mapping/TrackedReferences/TrackedReferenceViewModelsMapDefinition.cs @@ -12,6 +12,9 @@ public void DefineMaps(IUmbracoMapper mapper) mapper.Define((source, context) => new DocumentReferenceResponseModel(), Map); mapper.Define((source, context) => new MediaReferenceResponseModel(), Map); mapper.Define((source, context) => new MemberReferenceResponseModel(), Map); + mapper.Define((source, context) => new DocumentTypePropertyReferenceResponseModel(), Map); + mapper.Define((source, context) => new MediaTypePropertyReferenceResponseModel(), Map); + mapper.Define((source, context) => new MemberTypePropertyReferenceResponseModel(), Map); mapper.Define((source, context) => new DefaultReferenceResponseModel(), Map); mapper.Define((source, context) => new ReferenceByIdModel(), Map); mapper.Define((source, context) => new ReferenceByIdModel(), Map); @@ -57,6 +60,48 @@ private void Map(RelationItemModel source, MemberReferenceResponseModel target, }; } + // Umbraco.Code.MapAll + private void Map(RelationItemModel source, DocumentTypePropertyReferenceResponseModel target, MapperContext context) + { + target.Id = source.NodeKey; + target.Name = source.NodeName; + target.Alias = source.NodeAlias; + target.DocumentType = new TrackedReferenceDocumentType + { + Alias = source.ContentTypeAlias, + Icon = source.ContentTypeIcon, + Name = source.ContentTypeName, + }; + } + + // Umbraco.Code.MapAll + private void Map(RelationItemModel source, MediaTypePropertyReferenceResponseModel target, MapperContext context) + { + target.Id = source.NodeKey; + target.Name = source.NodeName; + target.Alias = source.NodeAlias; + target.MediaType = new TrackedReferenceMediaType + { + Alias = source.ContentTypeAlias, + Icon = source.ContentTypeIcon, + Name = source.ContentTypeName, + }; + } + + // Umbraco.Code.MapAll + private void Map(RelationItemModel source, MemberTypePropertyReferenceResponseModel target, MapperContext context) + { + target.Id = source.NodeKey; + target.Name = source.NodeName; + target.Alias = source.NodeAlias; + target.MemberType = new TrackedReferenceMemberType + { + Alias = source.ContentTypeAlias, + Icon = source.ContentTypeIcon, + Name = source.ContentTypeName, + }; + } + // Umbraco.Code.MapAll private void Map(RelationItemModel source, DefaultReferenceResponseModel target, MapperContext context) { diff --git a/src/Umbraco.Cms.Api.Management/OpenApi.json b/src/Umbraco.Cms.Api.Management/OpenApi.json index 44a69ccc957e..b49d19f18468 100644 --- a/src/Umbraco.Cms.Api.Management/OpenApi.json +++ b/src/Umbraco.Cms.Api.Management/OpenApi.json @@ -816,6 +816,70 @@ ] } }, + "/umbraco/management/api/v1/data-type/{id}/referenced-by": { + "get": { + "tags": [ + "Data Type" + ], + "operationId": "GetDataTypeByIdReferencedBy", + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "schema": { + "type": "string", + "format": "uuid" + } + }, + { + "name": "skip", + "in": "query", + "schema": { + "type": "integer", + "format": "int32", + "default": 0 + } + }, + { + "name": "take", + "in": "query", + "schema": { + "type": "integer", + "format": "int32", + "default": 20 + } + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/PagedIReferenceResponseModel" + } + ] + } + } + } + }, + "401": { + "description": "The resource is protected and requires an authentication token" + }, + "403": { + "description": "The authenticated user does not have access to this resource" + } + }, + "security": [ + { + "Backoffice User": [ ] + } + ] + } + }, "/umbraco/management/api/v1/data-type/{id}/references": { "get": { "tags": [ @@ -872,6 +936,7 @@ "description": "The authenticated user does not have access to this resource" } }, + "deprecated": true, "security": [ { "Backoffice User": [ ] @@ -37705,6 +37770,45 @@ }, "additionalProperties": false }, + "DocumentTypePropertyReferenceResponseModel": { + "required": [ + "$type", + "documentType", + "id" + ], + "type": "object", + "properties": { + "$type": { + "type": "string" + }, + "id": { + "type": "string", + "format": "uuid" + }, + "name": { + "type": "string", + "nullable": true + }, + "alias": { + "type": "string", + "nullable": true + }, + "documentType": { + "oneOf": [ + { + "$ref": "#/components/schemas/TrackedReferenceDocumentTypeModel" + } + ] + } + }, + "additionalProperties": false, + "discriminator": { + "propertyName": "$type", + "mapping": { + "DocumentTypePropertyReferenceResponseModel": "#/components/schemas/DocumentTypePropertyReferenceResponseModel" + } + } + }, "DocumentTypePropertyTypeContainerResponseModel": { "required": [ "id", @@ -39769,6 +39873,45 @@ }, "additionalProperties": false }, + "MediaTypePropertyReferenceResponseModel": { + "required": [ + "$type", + "id", + "mediaType" + ], + "type": "object", + "properties": { + "$type": { + "type": "string" + }, + "id": { + "type": "string", + "format": "uuid" + }, + "name": { + "type": "string", + "nullable": true + }, + "alias": { + "type": "string", + "nullable": true + }, + "mediaType": { + "oneOf": [ + { + "$ref": "#/components/schemas/TrackedReferenceMediaTypeModel" + } + ] + } + }, + "additionalProperties": false, + "discriminator": { + "propertyName": "$type", + "mapping": { + "MediaTypePropertyReferenceResponseModel": "#/components/schemas/MediaTypePropertyReferenceResponseModel" + } + } + }, "MediaTypePropertyTypeContainerResponseModel": { "required": [ "id", @@ -40547,6 +40690,45 @@ }, "additionalProperties": false }, + "MemberTypePropertyReferenceResponseModel": { + "required": [ + "$type", + "id", + "memberType" + ], + "type": "object", + "properties": { + "$type": { + "type": "string" + }, + "id": { + "type": "string", + "format": "uuid" + }, + "name": { + "type": "string", + "nullable": true + }, + "alias": { + "type": "string", + "nullable": true + }, + "memberType": { + "oneOf": [ + { + "$ref": "#/components/schemas/TrackedReferenceMemberTypeModel" + } + ] + } + }, + "additionalProperties": false, + "discriminator": { + "propertyName": "$type", + "mapping": { + "MemberTypePropertyReferenceResponseModel": "#/components/schemas/MemberTypePropertyReferenceResponseModel" + } + } + }, "MemberTypePropertyTypeContainerResponseModel": { "required": [ "id", @@ -41754,11 +41936,20 @@ { "$ref": "#/components/schemas/DocumentReferenceResponseModel" }, + { + "$ref": "#/components/schemas/DocumentTypePropertyReferenceResponseModel" + }, { "$ref": "#/components/schemas/MediaReferenceResponseModel" }, + { + "$ref": "#/components/schemas/MediaTypePropertyReferenceResponseModel" + }, { "$ref": "#/components/schemas/MemberReferenceResponseModel" + }, + { + "$ref": "#/components/schemas/MemberTypePropertyReferenceResponseModel" } ] } diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/TrackedReferences/ContentTypeReferenceResponseModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/TrackedReferences/ContentTypeReferenceResponseModel.cs new file mode 100644 index 000000000000..f13cf28ccdbb --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/ViewModels/TrackedReferences/ContentTypeReferenceResponseModel.cs @@ -0,0 +1,6 @@ +namespace Umbraco.Cms.Api.Management.ViewModels.TrackedReferences; + +public abstract class ContentTypePropertyReferenceResponseModel : ReferenceResponseModel +{ + public string? Alias { get; set; } +} diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/TrackedReferences/DocumentTypePropertyReferenceResponseModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/TrackedReferences/DocumentTypePropertyReferenceResponseModel.cs new file mode 100644 index 000000000000..00cca40bdef3 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/ViewModels/TrackedReferences/DocumentTypePropertyReferenceResponseModel.cs @@ -0,0 +1,6 @@ +namespace Umbraco.Cms.Api.Management.ViewModels.TrackedReferences; + +public class DocumentTypePropertyReferenceResponseModel : ContentTypePropertyReferenceResponseModel +{ + public TrackedReferenceDocumentType DocumentType { get; set; } = new(); +} diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/TrackedReferences/MediaTypePropertyReferenceResponseModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/TrackedReferences/MediaTypePropertyReferenceResponseModel.cs new file mode 100644 index 000000000000..7c14cec50d1f --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/ViewModels/TrackedReferences/MediaTypePropertyReferenceResponseModel.cs @@ -0,0 +1,6 @@ +namespace Umbraco.Cms.Api.Management.ViewModels.TrackedReferences; + +public class MediaTypePropertyReferenceResponseModel : ContentTypePropertyReferenceResponseModel +{ + public TrackedReferenceMediaType MediaType { get; set; } = new(); +} diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/TrackedReferences/MemberTypePropertyReferenceResponseModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/TrackedReferences/MemberTypePropertyReferenceResponseModel.cs new file mode 100644 index 000000000000..423309bf00dd --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/ViewModels/TrackedReferences/MemberTypePropertyReferenceResponseModel.cs @@ -0,0 +1,6 @@ +namespace Umbraco.Cms.Api.Management.ViewModels.TrackedReferences; + +public class MemberTypePropertyReferenceResponseModel : ContentTypePropertyReferenceResponseModel +{ + public TrackedReferenceMemberType MemberType { get; set; } = new(); +} diff --git a/src/Umbraco.Core/Constants-ReferenceTypes.cs b/src/Umbraco.Core/Constants-ReferenceTypes.cs new file mode 100644 index 000000000000..638bde29c02b --- /dev/null +++ b/src/Umbraco.Core/Constants-ReferenceTypes.cs @@ -0,0 +1,25 @@ +namespace Umbraco.Cms.Core; + +public static partial class Constants +{ + /// + /// Defines reference tyoes. + /// + /// + /// Reference types are used to identify the type of entity that is being referenced when exposing references + /// between Umbraco entities. + /// These are used in the management API and backoffice to indicate and warn editors when working with an entity, + /// as to what other entities depend on it. + /// These consist of references managed by Umbraco relations (e.g. document, media and member). + /// But also references that come from schema (e.g. data type usage on content types). + /// + public static class ReferenceType + { + public const string Document = UdiEntityType.Document; + public const string Media = UdiEntityType.Media; + public const string Member = UdiEntityType.Member; + public const string DocumentTypeProperty = "document-type-property"; + public const string MediaTypeProperty = "media-type-property"; + public const string MemberTypeProperty = "member-type-property"; + } +} diff --git a/src/Umbraco.Core/Models/RelationItemModel.cs b/src/Umbraco.Core/Models/RelationItemModel.cs index a05c8f65918f..e4e0e28d86f4 100644 --- a/src/Umbraco.Core/Models/RelationItemModel.cs +++ b/src/Umbraco.Core/Models/RelationItemModel.cs @@ -1,9 +1,11 @@ -namespace Umbraco.Cms.Core.Models; +namespace Umbraco.Cms.Core.Models; public class RelationItemModel { public Guid NodeKey { get; set; } + public string? NodeAlias { get; set; } + public string? NodeName { get; set; } public string? NodeType { get; set; } diff --git a/src/Umbraco.Core/Persistence/Repositories/IDataTypeRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IDataTypeRepository.cs index ad113533d8d9..f0babf61f378 100644 --- a/src/Umbraco.Core/Persistence/Repositories/IDataTypeRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/IDataTypeRepository.cs @@ -24,6 +24,5 @@ public interface IDataTypeRepository : IReadWriteQueryRepository /// /// /// - IReadOnlyDictionary> FindListViewUsages(int id) => throw new NotImplementedException(); } diff --git a/src/Umbraco.Core/Services/DataTypeService.cs b/src/Umbraco.Core/Services/DataTypeService.cs index f22948968a56..73aa18f04431 100644 --- a/src/Umbraco.Core/Services/DataTypeService.cs +++ b/src/Umbraco.Core/Services/DataTypeService.cs @@ -25,12 +25,15 @@ public class DataTypeService : RepositoryService, IDataTypeService private readonly IDataTypeRepository _dataTypeRepository; private readonly IDataTypeContainerRepository _dataTypeContainerRepository; private readonly IContentTypeRepository _contentTypeRepository; + private readonly IMediaTypeRepository _mediaTypeRepository; + private readonly IMemberTypeRepository _memberTypeRepository; private readonly IAuditRepository _auditRepository; private readonly IIOHelper _ioHelper; private readonly IDataTypeContainerService _dataTypeContainerService; private readonly IUserIdKeyResolver _userIdKeyResolver; private readonly Lazy _idKeyMap; + [Obsolete("Please use the constructor taking all parameters. Scheduled for removal in Umbraco 17.")] public DataTypeService( ICoreScopeProvider provider, ILoggerFactory loggerFactory, @@ -41,12 +44,41 @@ public DataTypeService( IContentTypeRepository contentTypeRepository, IIOHelper ioHelper, Lazy idKeyMap) + : this( + provider, + loggerFactory, + eventMessagesFactory, + dataTypeRepository, + dataValueEditorFactory, + auditRepository, + contentTypeRepository, + StaticServiceProvider.Instance.GetRequiredService(), + StaticServiceProvider.Instance.GetRequiredService(), + ioHelper, + idKeyMap) + { + } + + public DataTypeService( + ICoreScopeProvider provider, + ILoggerFactory loggerFactory, + IEventMessagesFactory eventMessagesFactory, + IDataTypeRepository dataTypeRepository, + IDataValueEditorFactory dataValueEditorFactory, + IAuditRepository auditRepository, + IContentTypeRepository contentTypeRepository, + IMediaTypeRepository mediaTypeRepository, + IMemberTypeRepository memberTypeRepositor, + IIOHelper ioHelper, + Lazy idKeyMap) : base(provider, loggerFactory, eventMessagesFactory) { _dataValueEditorFactory = dataValueEditorFactory; _dataTypeRepository = dataTypeRepository; _auditRepository = auditRepository; _contentTypeRepository = contentTypeRepository; + _mediaTypeRepository = mediaTypeRepository; + _memberTypeRepository = memberTypeRepositor; _ioHelper = ioHelper; _idKeyMap = idKeyMap; @@ -703,12 +735,112 @@ public async Task>, DataTyp return await Task.FromResult(Attempt.SucceedWithStatus(DataTypeOperationStatus.Success, usages)); } + /// public IReadOnlyDictionary> GetListViewReferences(int id) { using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true); return _dataTypeRepository.FindListViewUsages(id); } + /// + public Task> GetPagedRelationsAsync(Guid key, int skip, int take) + { + using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true); + + IDataType? dataType = GetDataTypeFromRepository(key); + if (dataType == null) + { + // Is an unexpected response, but returning an empty collection aligns with how we handle retrieval of concrete Umbraco + // relations based on documents, media and members. + return Task.FromResult(new PagedModel()); + } + + // We don't really need true paging here, as the number of data type relations will be small compared to what there could + // potentially by for concrete Umbraco relations based on documents, media and members. + // So we'll retrieve all usages for the data type and construct a paged response. + // This allows us to re-use the existing repository methods used for GetReferencesAsync and GetListViewReferences. + IReadOnlyDictionary> usages = _dataTypeRepository.FindUsages(dataType.Id); + IReadOnlyDictionary> listViewUsages = _dataTypeRepository.FindListViewUsages(dataType.Id); + + // Combine the property and list view usages into a single collection of property aliases and content type UDIs. + IList<(string PropertyAlias, Udi Udi)> combinedUsages = usages + .SelectMany(kvp => kvp.Value.Select(value => (value, kvp.Key))) + .Concat(listViewUsages.SelectMany(kvp => kvp.Value.Select(value => (value, kvp.Key)))) + .ToList(); + + var totalItems = combinedUsages.Count; + + // Create the page of items. + IEnumerable<(string PropertyAlias, Udi Udi)> pagedUsages = combinedUsages + .OrderBy(x => x.Udi.EntityType) // Document types first, then media types, then member types. + .ThenBy(x => x.PropertyAlias) + .Skip(skip) + .Take(take); + + // Get the content types for the UDIs referenced in the page of items to construct the response from. + // They could be document, media or member types. + IEnumerable documentTypes = GetContentTypes( + pagedUsages, + Constants.UdiEntityType.DocumentType, + _contentTypeRepository); + IEnumerable mediaTypes = GetContentTypes( + pagedUsages, + Constants.UdiEntityType.MediaType, + _mediaTypeRepository); + IEnumerable memberTypes = GetContentTypes( + pagedUsages, + Constants.UdiEntityType.MemberType, + _memberTypeRepository); + var contentTypes = documentTypes.Concat(mediaTypes).Concat(memberTypes).ToList(); + + IEnumerable relations = pagedUsages + .Select(x => + { + // Get the matching content type so we can populate the content type and property details. + IContentTypeComposition contentType = contentTypes.Single(y => y.Key == ((GuidUdi)x.Udi).Guid); + + string nodeType = x.Udi.EntityType switch + { + Constants.UdiEntityType.DocumentType => Constants.ReferenceType.DocumentTypeProperty, + Constants.UdiEntityType.MediaType => Constants.ReferenceType.MediaTypeProperty, + Constants.UdiEntityType.MemberType => Constants.ReferenceType.MemberTypeProperty, + _ => throw new ArgumentOutOfRangeException(nameof(x.Udi.EntityType)), + }; + + // Look-up the property details from the property alias. This will be null for a list view reference. + IPropertyType? propertyType = contentType.PropertyTypes.SingleOrDefault(y => y.Alias == x.PropertyAlias); + return new RelationItemModel + { + ContentTypeAlias = contentType.Alias, + ContentTypeIcon = contentType.Icon, + ContentTypeName = contentType.Name, + NodeType = nodeType, + NodeName = propertyType?.Name ?? x.PropertyAlias, + NodeAlias = x.PropertyAlias, + NodeKey = propertyType?.Key ?? Guid.Empty, + }; + }); + + var pagedModel = new PagedModel(totalItems, relations); + return Task.FromResult(pagedModel); + } + + private static IEnumerable GetContentTypes( + IEnumerable<(string PropertyAlias, Udi Udi)> dataTypeUsages, + string entityType, + IContentTypeRepositoryBase repository) + where T : IContentTypeComposition + { + Guid[] contentTypeKeys = dataTypeUsages + .Where(x => x.Udi is GuidUdi && x.Udi.EntityType == entityType) + .Select(x => ((GuidUdi)x.Udi).Guid) + .Distinct() + .ToArray(); + return contentTypeKeys.Length > 0 + ? repository.GetMany(contentTypeKeys) + : []; + } + /// public IEnumerable ValidateConfigurationData(IDataType dataType) { diff --git a/src/Umbraco.Core/Services/IDataTypeService.cs b/src/Umbraco.Core/Services/IDataTypeService.cs index 3a4576552c2f..0f2f58ceb837 100644 --- a/src/Umbraco.Core/Services/IDataTypeService.cs +++ b/src/Umbraco.Core/Services/IDataTypeService.cs @@ -18,6 +18,7 @@ public interface IDataTypeService : IService [Obsolete("Please use GetReferencesAsync. Will be deleted in V15.")] IReadOnlyDictionary> GetReferences(int id); + [Obsolete("Please use GetPagedRelationsAsync. Scheduled for removal in Umbraco 17.")] IReadOnlyDictionary> GetListViewReferences(int id) => throw new NotImplementedException(); /// @@ -25,8 +26,25 @@ public interface IDataTypeService : IService /// /// The guid Id of the /// + [Obsolete("Please use GetPagedRelationsAsync. Scheduled for removal in Umbraco 17.")] Task>, DataTypeOperationStatus>> GetReferencesAsync(Guid id); + /// + /// Gets a paged result of items which are in relation with the current data type. + /// + /// The identifier of the data type to retrieve relations for. + /// The amount of items to skip + /// The amount of items to take. + /// A paged result of objects. + /// + /// Note that the model and method signature here aligns with with how we handle retrieval of concrete Umbraco + /// relations based on documents, media and members in . + /// The intention is that we align data type relations with these so they can be handled polymorphically at the management API + /// and backoffice UI level. + /// + Task> GetPagedRelationsAsync(Guid key, int skip, int take) + => Task.FromResult(new PagedModel()); + [Obsolete("Please use IDataTypeContainerService for all data type container operations. Will be removed in V15.")] Attempt?> CreateContainer(int parentId, Guid key, string name, int userId = Constants.Security.SuperUserId); diff --git a/tests/Umbraco.Tests.Common/Builders/MediaTypeBuilder.cs b/tests/Umbraco.Tests.Common/Builders/MediaTypeBuilder.cs index dcde82b47d4c..6439899955a5 100644 --- a/tests/Umbraco.Tests.Common/Builders/MediaTypeBuilder.cs +++ b/tests/Umbraco.Tests.Common/Builders/MediaTypeBuilder.cs @@ -139,6 +139,7 @@ public static MediaType CreateSimpleMediaType( var mediaType = builder .WithAlias(alias) .WithName(name) + .WithIcon("icon-picture") .WithParentContentType(parent) .AddPropertyGroup() .WithAlias(propertyGroupAlias) diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/DataTypeServiceTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/DataTypeServiceTests.cs index 1c2b46137b3c..659c75b048e7 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/DataTypeServiceTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/DataTypeServiceTests.cs @@ -30,6 +30,8 @@ public class DataTypeServiceTests : UmbracoIntegrationTest private IContentTypeService ContentTypeService => GetRequiredService(); + private IMediaTypeService MediaTypeService => GetRequiredService(); + private IFileService FileService => GetRequiredService(); private IConfigurationEditorJsonSerializer ConfigurationEditorJsonSerializer => @@ -446,4 +448,41 @@ public async Task Cannot_Delete_NonDeletable_DataType(string dataTypeKey) Assert.IsFalse(result.Success); Assert.AreEqual(DataTypeOperationStatus.NonDeletable, result.Status); } + + [Test] + public async Task DataTypeService_Can_Get_References() + { + IEnumerable dataTypeDefinitions = await DataTypeService.GetByEditorAliasAsync(Constants.PropertyEditors.Aliases.RichText); + + IContentType documentType = ContentTypeBuilder.CreateSimpleContentType("umbTextpage", "Text Page"); + ContentTypeService.Save(documentType); + + IMediaType mediaType = MediaTypeBuilder.CreateSimpleMediaType("umbMediaItem", "Media Item"); + MediaTypeService.Save(mediaType); + + documentType = ContentTypeService.Get(documentType.Id); + Assert.IsNotNull(documentType.PropertyTypes.SingleOrDefault(pt => pt.PropertyEditorAlias is Constants.PropertyEditors.Aliases.RichText)); + + mediaType = MediaTypeService.Get(mediaType.Id); + Assert.IsNotNull(mediaType.PropertyTypes.SingleOrDefault(pt => pt.PropertyEditorAlias is Constants.PropertyEditors.Aliases.RichText)); + + var definition = dataTypeDefinitions.First(); + var definitionKey = definition.Key; + PagedModel result = await DataTypeService.GetPagedRelationsAsync(definitionKey, 0, 10); + Assert.AreEqual(2, result.Total); + + RelationItemModel firstResult = result.Items.First(); + Assert.AreEqual("umbTextpage", firstResult.ContentTypeAlias); + Assert.AreEqual("Text Page", firstResult.ContentTypeName); + Assert.AreEqual("icon-document", firstResult.ContentTypeIcon); + Assert.AreEqual("bodyText", firstResult.NodeAlias); + Assert.AreEqual("Body text", firstResult.NodeName); + + RelationItemModel secondResult = result.Items.Skip(1).First(); + Assert.AreEqual("umbMediaItem", secondResult.ContentTypeAlias); + Assert.AreEqual("Media Item", secondResult.ContentTypeName); + Assert.AreEqual("icon-picture", secondResult.ContentTypeIcon); + Assert.AreEqual("bodyText", secondResult.NodeAlias); + Assert.AreEqual("Body text", secondResult.NodeName); + } } From d53b446972737663c738653087243b6ad2d3f8d0 Mon Sep 17 00:00:00 2001 From: Andy Butland Date: Tue, 1 Apr 2025 14:37:16 +0200 Subject: [PATCH 02/10] Refactoring. --- src/Umbraco.Core/Services/DataTypeService.cs | 33 +++++++++++--------- 1 file changed, 19 insertions(+), 14 deletions(-) diff --git a/src/Umbraco.Core/Services/DataTypeService.cs b/src/Umbraco.Core/Services/DataTypeService.cs index 73aa18f04431..789772daebee 100644 --- a/src/Umbraco.Core/Services/DataTypeService.cs +++ b/src/Umbraco.Core/Services/DataTypeService.cs @@ -758,7 +758,7 @@ public Task> GetPagedRelationsAsync(Guid key, int // We don't really need true paging here, as the number of data type relations will be small compared to what there could // potentially by for concrete Umbraco relations based on documents, media and members. // So we'll retrieve all usages for the data type and construct a paged response. - // This allows us to re-use the existing repository methods used for GetReferencesAsync and GetListViewReferences. + // This allows us to re-use the existing repository methods used for FindUsages and FindListViewUsages. IReadOnlyDictionary> usages = _dataTypeRepository.FindUsages(dataType.Id); IReadOnlyDictionary> listViewUsages = _dataTypeRepository.FindListViewUsages(dataType.Id); @@ -779,19 +779,7 @@ public Task> GetPagedRelationsAsync(Guid key, int // Get the content types for the UDIs referenced in the page of items to construct the response from. // They could be document, media or member types. - IEnumerable documentTypes = GetContentTypes( - pagedUsages, - Constants.UdiEntityType.DocumentType, - _contentTypeRepository); - IEnumerable mediaTypes = GetContentTypes( - pagedUsages, - Constants.UdiEntityType.MediaType, - _mediaTypeRepository); - IEnumerable memberTypes = GetContentTypes( - pagedUsages, - Constants.UdiEntityType.MemberType, - _memberTypeRepository); - var contentTypes = documentTypes.Concat(mediaTypes).Concat(memberTypes).ToList(); + IList contentTypes = GetReferencedContentTypes(pagedUsages); IEnumerable relations = pagedUsages .Select(x => @@ -825,6 +813,23 @@ public Task> GetPagedRelationsAsync(Guid key, int return Task.FromResult(pagedModel); } + private IList GetReferencedContentTypes(IEnumerable<(string PropertyAlias, Udi Udi)> pagedUsages) + { + IEnumerable documentTypes = GetContentTypes( + pagedUsages, + Constants.UdiEntityType.DocumentType, + _contentTypeRepository); + IEnumerable mediaTypes = GetContentTypes( + pagedUsages, + Constants.UdiEntityType.MediaType, + _mediaTypeRepository); + IEnumerable memberTypes = GetContentTypes( + pagedUsages, + Constants.UdiEntityType.MemberType, + _memberTypeRepository); + return documentTypes.Concat(mediaTypes).Concat(memberTypes).ToList(); + } + private static IEnumerable GetContentTypes( IEnumerable<(string PropertyAlias, Udi Udi)> dataTypeUsages, string entityType, From 4ac16318a719d7c3b444a1a4238d794fe73d79bc Mon Sep 17 00:00:00 2001 From: Andy Butland Date: Tue, 1 Apr 2025 14:58:19 +0200 Subject: [PATCH 03/10] Update src/Umbraco.Core/Constants-ReferenceTypes.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- src/Umbraco.Core/Constants-ReferenceTypes.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Core/Constants-ReferenceTypes.cs b/src/Umbraco.Core/Constants-ReferenceTypes.cs index 638bde29c02b..c76de94733bc 100644 --- a/src/Umbraco.Core/Constants-ReferenceTypes.cs +++ b/src/Umbraco.Core/Constants-ReferenceTypes.cs @@ -3,7 +3,7 @@ namespace Umbraco.Cms.Core; public static partial class Constants { /// - /// Defines reference tyoes. + /// Defines reference types. /// /// /// Reference types are used to identify the type of entity that is being referenced when exposing references From a07feb4fabb12ba61cd0105d1c18ebf1128fee57 Mon Sep 17 00:00:00 2001 From: Andy Butland Date: Tue, 1 Apr 2025 15:01:36 +0200 Subject: [PATCH 04/10] Fixed typos. --- .../DataType/References/ReferencedByDataTypeController.cs | 2 +- src/Umbraco.Core/Services/DataTypeService.cs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Umbraco.Cms.Api.Management/Controllers/DataType/References/ReferencedByDataTypeController.cs b/src/Umbraco.Cms.Api.Management/Controllers/DataType/References/ReferencedByDataTypeController.cs index 809a45a724a5..6f8c14fb7adb 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/DataType/References/ReferencedByDataTypeController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/DataType/References/ReferencedByDataTypeController.cs @@ -8,7 +8,7 @@ using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Services; -namespace Umbraco.Cms.Api.Management.Controllers.DateType.References; +namespace Umbraco.Cms.Api.Management.Controllers.DataType.References; [ApiVersion("1.0")] public class ReferencedByDataTypeController : DataTypeControllerBase diff --git a/src/Umbraco.Core/Services/DataTypeService.cs b/src/Umbraco.Core/Services/DataTypeService.cs index 789772daebee..f41b7dbcab14 100644 --- a/src/Umbraco.Core/Services/DataTypeService.cs +++ b/src/Umbraco.Core/Services/DataTypeService.cs @@ -68,7 +68,7 @@ public DataTypeService( IAuditRepository auditRepository, IContentTypeRepository contentTypeRepository, IMediaTypeRepository mediaTypeRepository, - IMemberTypeRepository memberTypeRepositor, + IMemberTypeRepository memberTypeRepository, IIOHelper ioHelper, Lazy idKeyMap) : base(provider, loggerFactory, eventMessagesFactory) @@ -78,7 +78,7 @@ public DataTypeService( _auditRepository = auditRepository; _contentTypeRepository = contentTypeRepository; _mediaTypeRepository = mediaTypeRepository; - _memberTypeRepository = memberTypeRepositor; + _memberTypeRepository = memberTypeRepository; _ioHelper = ioHelper; _idKeyMap = idKeyMap; From 56e0e64e1ae712d262b137d14a419856abd1d4df Mon Sep 17 00:00:00 2001 From: Andy Butland Date: Wed, 2 Apr 2025 09:34:36 +0200 Subject: [PATCH 05/10] Added id to tracked reference content type response. --- .../TrackedReferenceViewModelsMapDefinition.cs | 6 ++++++ .../TrackedReferences/TrackedReferenceContentType.cs | 4 +++- .../Mapping/RelationModelMapDefinition.cs | 3 ++- .../Repositories/Implement/RelationRepository.cs | 3 +++ .../Implement/TrackedReferencesRepository.cs | 10 ++++++++++ .../Services/DataTypeServiceTests.cs | 2 ++ 6 files changed, 26 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Cms.Api.Management/Mapping/TrackedReferences/TrackedReferenceViewModelsMapDefinition.cs b/src/Umbraco.Cms.Api.Management/Mapping/TrackedReferences/TrackedReferenceViewModelsMapDefinition.cs index 9143ba810ddc..da9b08844976 100644 --- a/src/Umbraco.Cms.Api.Management/Mapping/TrackedReferences/TrackedReferenceViewModelsMapDefinition.cs +++ b/src/Umbraco.Cms.Api.Management/Mapping/TrackedReferences/TrackedReferenceViewModelsMapDefinition.cs @@ -28,6 +28,7 @@ private void Map(RelationItemModel source, DocumentReferenceResponseModel target target.Published = source.NodePublished; target.DocumentType = new TrackedReferenceDocumentType { + Id = source.ContentTypeKey, Alias = source.ContentTypeAlias, Icon = source.ContentTypeIcon, Name = source.ContentTypeName, @@ -41,6 +42,7 @@ private void Map(RelationItemModel source, MediaReferenceResponseModel target, M target.Name = source.NodeName; target.MediaType = new TrackedReferenceMediaType { + Id = source.ContentTypeKey, Alias = source.ContentTypeAlias, Icon = source.ContentTypeIcon, Name = source.ContentTypeName, @@ -54,6 +56,7 @@ private void Map(RelationItemModel source, MemberReferenceResponseModel target, target.Name = source.NodeName; target.MemberType = new TrackedReferenceMemberType { + Id = source.ContentTypeKey, Alias = source.ContentTypeAlias, Icon = source.ContentTypeIcon, Name = source.ContentTypeName, @@ -68,6 +71,7 @@ private void Map(RelationItemModel source, DocumentTypePropertyReferenceResponse target.Alias = source.NodeAlias; target.DocumentType = new TrackedReferenceDocumentType { + Id = source.ContentTypeKey, Alias = source.ContentTypeAlias, Icon = source.ContentTypeIcon, Name = source.ContentTypeName, @@ -82,6 +86,7 @@ private void Map(RelationItemModel source, MediaTypePropertyReferenceResponseMod target.Alias = source.NodeAlias; target.MediaType = new TrackedReferenceMediaType { + Id = source.ContentTypeKey, Alias = source.ContentTypeAlias, Icon = source.ContentTypeIcon, Name = source.ContentTypeName, @@ -96,6 +101,7 @@ private void Map(RelationItemModel source, MemberTypePropertyReferenceResponseMo target.Alias = source.NodeAlias; target.MemberType = new TrackedReferenceMemberType { + Id = source.ContentTypeKey, Alias = source.ContentTypeAlias, Icon = source.ContentTypeIcon, Name = source.ContentTypeName, diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/TrackedReferences/TrackedReferenceContentType.cs b/src/Umbraco.Cms.Api.Management/ViewModels/TrackedReferences/TrackedReferenceContentType.cs index 31456abada1a..15ac365e41e2 100644 --- a/src/Umbraco.Cms.Api.Management/ViewModels/TrackedReferences/TrackedReferenceContentType.cs +++ b/src/Umbraco.Cms.Api.Management/ViewModels/TrackedReferences/TrackedReferenceContentType.cs @@ -1,7 +1,9 @@ -namespace Umbraco.Cms.Api.Management.ViewModels.TrackedReferences; +namespace Umbraco.Cms.Api.Management.ViewModels.TrackedReferences; public abstract class TrackedReferenceContentType { + public Guid Id { get; set; } + public string? Icon { get; set; } public string? Alias { get; set; } diff --git a/src/Umbraco.Infrastructure/Mapping/RelationModelMapDefinition.cs b/src/Umbraco.Infrastructure/Mapping/RelationModelMapDefinition.cs index 8ace25c07f57..1f0c986e0d7c 100644 --- a/src/Umbraco.Infrastructure/Mapping/RelationModelMapDefinition.cs +++ b/src/Umbraco.Infrastructure/Mapping/RelationModelMapDefinition.cs @@ -1,4 +1,4 @@ -using Umbraco.Cms.Core.Mapping; +using Umbraco.Cms.Core.Mapping; using Umbraco.Cms.Core.Models; using Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; @@ -19,6 +19,7 @@ private void Map(RelationItemDto source, RelationItemModel target, MapperContext target.RelationTypeName = source.RelationTypeName; target.RelationTypeIsBidirectional = source.RelationTypeIsBidirectional; target.RelationTypeIsDependency = source.RelationTypeIsDependency; + target.ContentTypeKey = source.ChildContentTypeKey; target.ContentTypeAlias = source.ChildContentTypeAlias; target.ContentTypeIcon = source.ChildContentTypeIcon; target.ContentTypeName = source.ChildContentTypeName; diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/RelationRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/RelationRepository.cs index a38bf4547ff6..2786bbfd1e45 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/RelationRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/RelationRepository.cs @@ -475,6 +475,9 @@ internal class RelationItemDto [Column(Name = "nodeObjectType")] public Guid ChildNodeObjectType { get; set; } + [Column(Name = "contentTypeKey")] + public Guid ChildContentTypeKey { get; set; } + [Column(Name = "contentTypeIcon")] public string? ChildContentTypeIcon { get; set; } diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/TrackedReferencesRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/TrackedReferencesRepository.cs index 2b7a43589c01..ff3b671a5cc3 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/TrackedReferencesRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/TrackedReferencesRepository.cs @@ -34,6 +34,7 @@ public IEnumerable GetPagedItemsWithRelations(int[] ids, long page "[n].[text] as nodeName", "[n].[nodeObjectType] as nodeObjectType", "[d].[published] as nodePublished", + "[ctn].[uniqueId] as contentTypeKey", "[ct].[icon] as contentTypeIcon", "[ct].[alias] as contentTypeAlias", "[ctn].[text] as contentTypeName", @@ -163,6 +164,7 @@ public IEnumerable GetPagedDescendantsInReferences( "[n].[text] as nodeName", "[n].[nodeObjectType] as nodeObjectType", "[d].[published] as nodePublished", + "[ctn].[uniqueId] as contentTypeKey", "[ct].[icon] as contentTypeIcon", "[ct].[alias] as contentTypeAlias", "[ctn].[text] as contentTypeName", @@ -225,6 +227,7 @@ public IEnumerable GetPagedRelationsForItem(int id, long pageIndex "[n].[text] as nodeName", "[n].[nodeObjectType] as nodeObjectType", "[d].[published] as nodePublished", + "[ctn].[uniqueId] as contentTypeKey", "[ct].[icon] as contentTypeIcon", "[ct].[alias] as contentTypeAlias", "[ctn].[text] as contentTypeName", @@ -285,6 +288,7 @@ public IEnumerable GetPagedRelationsForItem( "[n].[text] as nodeName", "[n].[nodeObjectType] as nodeObjectType", "[d].[published] as nodePublished", + "[ctn].[uniqueId] as contentTypeKey", "[ct].[icon] as contentTypeIcon", "[ct].[alias] as contentTypeAlias", "[ctn].[text] as contentTypeName", @@ -360,6 +364,7 @@ public IEnumerable GetPagedRelationsForItem( "[n].[uniqueId] as nodeKey", "[n].[text] as nodeName", "[n].[nodeObjectType] as nodeObjectType", + "[ctn].[uniqueId] as contentTypeKey", "[ct].[icon] as contentTypeIcon", "[ct].[alias] as contentTypeAlias", "[ctn].[text] as contentTypeName", @@ -426,6 +431,7 @@ public IEnumerable GetPagedItemsWithRelations( "[n].[uniqueId] as nodeKey", "[n].[text] as nodeName", "[n].[nodeObjectType] as nodeObjectType", + "[ctn].[uniqueId] as contentTypeKey", "[ct].[icon] as contentTypeIcon", "[ct].[alias] as contentTypeAlias", "[ctn].[text] as contentTypeName", @@ -544,6 +550,7 @@ public IEnumerable GetPagedDescendantsInReferences(Guid paren "[n].[text] as nodeName", "[n].[nodeObjectType] as nodeObjectType", "[d].[published] as nodePublished", + "[ctn].[uniqueId] as contentTypeKey", "[ct].[icon] as contentTypeIcon", "[ct].[alias] as contentTypeAlias", "[ctn].[text] as contentTypeName", @@ -620,6 +627,7 @@ public IEnumerable GetPagedItemsWithRelations( "[n].[uniqueId] as nodeKey", "[n].[text] as nodeName", "[n].[nodeObjectType] as nodeObjectType", + "[ctn].[uniqueId] as contentTypeKey", "[ct].[icon] as contentTypeIcon", "[ct].[alias] as contentTypeAlias", "[ctn].[text] as contentTypeName", @@ -698,6 +706,7 @@ public IEnumerable GetPagedDescendantsInReferences( "[n].[uniqueId] as nodeKey", "[n].[text] as nodeName", "[n].[nodeObjectType] as nodeObjectType", + "[ctn].[uniqueId] as contentTypeKey", "[ct].[icon] as contentTypeIcon", "[ct].[alias] as contentTypeAlias", "[ctn].[text] as contentTypeName", @@ -786,6 +795,7 @@ private RelationItem MapDtoToEntity(RelationItemDto dto) RelationTypeName = dto.RelationTypeName, RelationTypeIsBidirectional = dto.RelationTypeIsBidirectional, RelationTypeIsDependency = dto.RelationTypeIsDependency, + ContentTypeKey = dto.ChildContentTypeKey, ContentTypeAlias = dto.ChildContentTypeAlias, ContentTypeIcon = dto.ChildContentTypeIcon, ContentTypeName = dto.ChildContentTypeName, diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/DataTypeServiceTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/DataTypeServiceTests.cs index 659c75b048e7..92a1567a4f33 100644 --- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/DataTypeServiceTests.cs +++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/DataTypeServiceTests.cs @@ -475,6 +475,7 @@ public async Task DataTypeService_Can_Get_References() Assert.AreEqual("umbTextpage", firstResult.ContentTypeAlias); Assert.AreEqual("Text Page", firstResult.ContentTypeName); Assert.AreEqual("icon-document", firstResult.ContentTypeIcon); + Assert.AreEqual(documentType.Key, firstResult.ContentTypeKey); Assert.AreEqual("bodyText", firstResult.NodeAlias); Assert.AreEqual("Body text", firstResult.NodeName); @@ -482,6 +483,7 @@ public async Task DataTypeService_Can_Get_References() Assert.AreEqual("umbMediaItem", secondResult.ContentTypeAlias); Assert.AreEqual("Media Item", secondResult.ContentTypeName); Assert.AreEqual("icon-picture", secondResult.ContentTypeIcon); + Assert.AreEqual(mediaType.Key, secondResult.ContentTypeKey); Assert.AreEqual("bodyText", secondResult.NodeAlias); Assert.AreEqual("Body text", secondResult.NodeName); } From 8963db3217dca1c9b4566ca4f06d07872fc3e787 Mon Sep 17 00:00:00 2001 From: Andy Butland Date: Wed, 2 Apr 2025 09:36:19 +0200 Subject: [PATCH 06/10] Updated OpenApi.json. --- src/Umbraco.Cms.Api.Management/OpenApi.json | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/src/Umbraco.Cms.Api.Management/OpenApi.json b/src/Umbraco.Cms.Api.Management/OpenApi.json index b49d19f18468..06ab75e1b5f9 100644 --- a/src/Umbraco.Cms.Api.Management/OpenApi.json +++ b/src/Umbraco.Cms.Api.Management/OpenApi.json @@ -44587,8 +44587,15 @@ "additionalProperties": false }, "TrackedReferenceDocumentTypeModel": { + "required": [ + "id" + ], "type": "object", "properties": { + "id": { + "type": "string", + "format": "uuid" + }, "icon": { "type": "string", "nullable": true @@ -44605,8 +44612,15 @@ "additionalProperties": false }, "TrackedReferenceMediaTypeModel": { + "required": [ + "id" + ], "type": "object", "properties": { + "id": { + "type": "string", + "format": "uuid" + }, "icon": { "type": "string", "nullable": true @@ -44623,8 +44637,15 @@ "additionalProperties": false }, "TrackedReferenceMemberTypeModel": { + "required": [ + "id" + ], "type": "object", "properties": { + "id": { + "type": "string", + "format": "uuid" + }, "icon": { "type": "string", "nullable": true From ad20cac99a976db6e2fa9cc373f59c8151adba27 Mon Sep 17 00:00:00 2001 From: Andy Butland Date: Wed, 2 Apr 2025 09:43:20 +0200 Subject: [PATCH 07/10] Added missing updates. --- src/Umbraco.Core/Models/RelationItem.cs | 3 +++ src/Umbraco.Core/Models/RelationItemModel.cs | 2 ++ src/Umbraco.Core/Services/DataTypeService.cs | 1 + 3 files changed, 6 insertions(+) diff --git a/src/Umbraco.Core/Models/RelationItem.cs b/src/Umbraco.Core/Models/RelationItem.cs index a865e7cc2fbf..41fde0e867be 100644 --- a/src/Umbraco.Core/Models/RelationItem.cs +++ b/src/Umbraco.Core/Models/RelationItem.cs @@ -23,6 +23,9 @@ public class RelationItem [DataMember(Name = "published")] public bool? NodePublished { get; set; } + [DataMember(Name = "contentTypeKey")] + public Guid ContentTypeKey { get; set; } + [DataMember(Name = "icon")] public string? ContentTypeIcon { get; set; } diff --git a/src/Umbraco.Core/Models/RelationItemModel.cs b/src/Umbraco.Core/Models/RelationItemModel.cs index e4e0e28d86f4..1ca3bb9e111e 100644 --- a/src/Umbraco.Core/Models/RelationItemModel.cs +++ b/src/Umbraco.Core/Models/RelationItemModel.cs @@ -12,6 +12,8 @@ public class RelationItemModel public bool? NodePublished { get; set; } + public Guid ContentTypeKey { get; set; } + public string? ContentTypeIcon { get; set; } public string? ContentTypeAlias { get; set; } diff --git a/src/Umbraco.Core/Services/DataTypeService.cs b/src/Umbraco.Core/Services/DataTypeService.cs index f41b7dbcab14..89b24732d788 100644 --- a/src/Umbraco.Core/Services/DataTypeService.cs +++ b/src/Umbraco.Core/Services/DataTypeService.cs @@ -799,6 +799,7 @@ public Task> GetPagedRelationsAsync(Guid key, int IPropertyType? propertyType = contentType.PropertyTypes.SingleOrDefault(y => y.Alias == x.PropertyAlias); return new RelationItemModel { + ContentTypeKey = contentType.Key, ContentTypeAlias = contentType.Alias, ContentTypeIcon = contentType.Icon, ContentTypeName = contentType.Name, From 2007ee5893bdffab2cfc2b70c99e44649bd6718a Mon Sep 17 00:00:00 2001 From: Andy Butland Date: Wed, 2 Apr 2025 12:20:12 +0200 Subject: [PATCH 08/10] Renamed model and constants from code review feedback. --- .../Factories/RelationTypePresentationFactory.cs | 6 +++--- .../TrackedReferenceViewModelsMapDefinition.cs | 12 ++++++------ ...ContentTypePropertyTypeReferenceResponseModel.cs} | 2 +- ...ocumentTypePropertyTypeReferenceResponseModel.cs} | 2 +- ...> MediaTypePropertyTypeReferenceResponseModel.cs} | 2 +- ... MemberTypePropertyTypeReferenceResponseModel.cs} | 2 +- src/Umbraco.Core/Constants-ReferenceTypes.cs | 6 +++--- src/Umbraco.Core/Services/DataTypeService.cs | 6 +++--- 8 files changed, 19 insertions(+), 19 deletions(-) rename src/Umbraco.Cms.Api.Management/ViewModels/TrackedReferences/{ContentTypeReferenceResponseModel.cs => ContentTypePropertyTypeReferenceResponseModel.cs} (54%) rename src/Umbraco.Cms.Api.Management/ViewModels/TrackedReferences/{DocumentTypePropertyReferenceResponseModel.cs => DocumentTypePropertyTypeReferenceResponseModel.cs} (57%) rename src/Umbraco.Cms.Api.Management/ViewModels/TrackedReferences/{MediaTypePropertyReferenceResponseModel.cs => MediaTypePropertyTypeReferenceResponseModel.cs} (57%) rename src/Umbraco.Cms.Api.Management/ViewModels/TrackedReferences/{MemberTypePropertyReferenceResponseModel.cs => MemberTypePropertyTypeReferenceResponseModel.cs} (57%) diff --git a/src/Umbraco.Cms.Api.Management/Factories/RelationTypePresentationFactory.cs b/src/Umbraco.Cms.Api.Management/Factories/RelationTypePresentationFactory.cs index 8fc2309b0f21..40bcf9f04c16 100644 --- a/src/Umbraco.Cms.Api.Management/Factories/RelationTypePresentationFactory.cs +++ b/src/Umbraco.Cms.Api.Management/Factories/RelationTypePresentationFactory.cs @@ -59,9 +59,9 @@ public async Task> CreateReferenceResponseM Constants.ReferenceType.Document => MapDocumentReference(relationItemModel, slimEntities), Constants.ReferenceType.Media => _umbracoMapper.Map(relationItemModel), Constants.ReferenceType.Member => _umbracoMapper.Map(relationItemModel), - Constants.ReferenceType.DocumentTypeProperty => _umbracoMapper.Map(relationItemModel), - Constants.ReferenceType.MediaTypeProperty => _umbracoMapper.Map(relationItemModel), - Constants.ReferenceType.MemberTypeProperty => _umbracoMapper.Map(relationItemModel), + Constants.ReferenceType.DocumentTypePropertyType => _umbracoMapper.Map(relationItemModel), + Constants.ReferenceType.MediaTypePropertyType => _umbracoMapper.Map(relationItemModel), + Constants.ReferenceType.MemberTypePropertyType => _umbracoMapper.Map(relationItemModel), _ => _umbracoMapper.Map(relationItemModel), }).WhereNotNull().ToArray(); diff --git a/src/Umbraco.Cms.Api.Management/Mapping/TrackedReferences/TrackedReferenceViewModelsMapDefinition.cs b/src/Umbraco.Cms.Api.Management/Mapping/TrackedReferences/TrackedReferenceViewModelsMapDefinition.cs index da9b08844976..c4efca308829 100644 --- a/src/Umbraco.Cms.Api.Management/Mapping/TrackedReferences/TrackedReferenceViewModelsMapDefinition.cs +++ b/src/Umbraco.Cms.Api.Management/Mapping/TrackedReferences/TrackedReferenceViewModelsMapDefinition.cs @@ -12,9 +12,9 @@ public void DefineMaps(IUmbracoMapper mapper) mapper.Define((source, context) => new DocumentReferenceResponseModel(), Map); mapper.Define((source, context) => new MediaReferenceResponseModel(), Map); mapper.Define((source, context) => new MemberReferenceResponseModel(), Map); - mapper.Define((source, context) => new DocumentTypePropertyReferenceResponseModel(), Map); - mapper.Define((source, context) => new MediaTypePropertyReferenceResponseModel(), Map); - mapper.Define((source, context) => new MemberTypePropertyReferenceResponseModel(), Map); + mapper.Define((source, context) => new DocumentTypePropertyTypeReferenceResponseModel(), Map); + mapper.Define((source, context) => new MediaTypePropertyTypeReferenceResponseModel(), Map); + mapper.Define((source, context) => new MemberTypePropertyTypeReferenceResponseModel(), Map); mapper.Define((source, context) => new DefaultReferenceResponseModel(), Map); mapper.Define((source, context) => new ReferenceByIdModel(), Map); mapper.Define((source, context) => new ReferenceByIdModel(), Map); @@ -64,7 +64,7 @@ private void Map(RelationItemModel source, MemberReferenceResponseModel target, } // Umbraco.Code.MapAll - private void Map(RelationItemModel source, DocumentTypePropertyReferenceResponseModel target, MapperContext context) + private void Map(RelationItemModel source, DocumentTypePropertyTypeReferenceResponseModel target, MapperContext context) { target.Id = source.NodeKey; target.Name = source.NodeName; @@ -79,7 +79,7 @@ private void Map(RelationItemModel source, DocumentTypePropertyReferenceResponse } // Umbraco.Code.MapAll - private void Map(RelationItemModel source, MediaTypePropertyReferenceResponseModel target, MapperContext context) + private void Map(RelationItemModel source, MediaTypePropertyTypeReferenceResponseModel target, MapperContext context) { target.Id = source.NodeKey; target.Name = source.NodeName; @@ -94,7 +94,7 @@ private void Map(RelationItemModel source, MediaTypePropertyReferenceResponseMod } // Umbraco.Code.MapAll - private void Map(RelationItemModel source, MemberTypePropertyReferenceResponseModel target, MapperContext context) + private void Map(RelationItemModel source, MemberTypePropertyTypeReferenceResponseModel target, MapperContext context) { target.Id = source.NodeKey; target.Name = source.NodeName; diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/TrackedReferences/ContentTypeReferenceResponseModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/TrackedReferences/ContentTypePropertyTypeReferenceResponseModel.cs similarity index 54% rename from src/Umbraco.Cms.Api.Management/ViewModels/TrackedReferences/ContentTypeReferenceResponseModel.cs rename to src/Umbraco.Cms.Api.Management/ViewModels/TrackedReferences/ContentTypePropertyTypeReferenceResponseModel.cs index f13cf28ccdbb..83dc6a1a7a5f 100644 --- a/src/Umbraco.Cms.Api.Management/ViewModels/TrackedReferences/ContentTypeReferenceResponseModel.cs +++ b/src/Umbraco.Cms.Api.Management/ViewModels/TrackedReferences/ContentTypePropertyTypeReferenceResponseModel.cs @@ -1,6 +1,6 @@ namespace Umbraco.Cms.Api.Management.ViewModels.TrackedReferences; -public abstract class ContentTypePropertyReferenceResponseModel : ReferenceResponseModel +public abstract class ContentTypePropertyTypeReferenceResponseModel : ReferenceResponseModel { public string? Alias { get; set; } } diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/TrackedReferences/DocumentTypePropertyReferenceResponseModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/TrackedReferences/DocumentTypePropertyTypeReferenceResponseModel.cs similarity index 57% rename from src/Umbraco.Cms.Api.Management/ViewModels/TrackedReferences/DocumentTypePropertyReferenceResponseModel.cs rename to src/Umbraco.Cms.Api.Management/ViewModels/TrackedReferences/DocumentTypePropertyTypeReferenceResponseModel.cs index 00cca40bdef3..b2ef3e4a0ad2 100644 --- a/src/Umbraco.Cms.Api.Management/ViewModels/TrackedReferences/DocumentTypePropertyReferenceResponseModel.cs +++ b/src/Umbraco.Cms.Api.Management/ViewModels/TrackedReferences/DocumentTypePropertyTypeReferenceResponseModel.cs @@ -1,6 +1,6 @@ namespace Umbraco.Cms.Api.Management.ViewModels.TrackedReferences; -public class DocumentTypePropertyReferenceResponseModel : ContentTypePropertyReferenceResponseModel +public class DocumentTypePropertyTypeReferenceResponseModel : ContentTypePropertyTypeReferenceResponseModel { public TrackedReferenceDocumentType DocumentType { get; set; } = new(); } diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/TrackedReferences/MediaTypePropertyReferenceResponseModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/TrackedReferences/MediaTypePropertyTypeReferenceResponseModel.cs similarity index 57% rename from src/Umbraco.Cms.Api.Management/ViewModels/TrackedReferences/MediaTypePropertyReferenceResponseModel.cs rename to src/Umbraco.Cms.Api.Management/ViewModels/TrackedReferences/MediaTypePropertyTypeReferenceResponseModel.cs index 7c14cec50d1f..1baf6476545f 100644 --- a/src/Umbraco.Cms.Api.Management/ViewModels/TrackedReferences/MediaTypePropertyReferenceResponseModel.cs +++ b/src/Umbraco.Cms.Api.Management/ViewModels/TrackedReferences/MediaTypePropertyTypeReferenceResponseModel.cs @@ -1,6 +1,6 @@ namespace Umbraco.Cms.Api.Management.ViewModels.TrackedReferences; -public class MediaTypePropertyReferenceResponseModel : ContentTypePropertyReferenceResponseModel +public class MediaTypePropertyTypeReferenceResponseModel : ContentTypePropertyTypeReferenceResponseModel { public TrackedReferenceMediaType MediaType { get; set; } = new(); } diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/TrackedReferences/MemberTypePropertyReferenceResponseModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/TrackedReferences/MemberTypePropertyTypeReferenceResponseModel.cs similarity index 57% rename from src/Umbraco.Cms.Api.Management/ViewModels/TrackedReferences/MemberTypePropertyReferenceResponseModel.cs rename to src/Umbraco.Cms.Api.Management/ViewModels/TrackedReferences/MemberTypePropertyTypeReferenceResponseModel.cs index 423309bf00dd..199a4b0ba18d 100644 --- a/src/Umbraco.Cms.Api.Management/ViewModels/TrackedReferences/MemberTypePropertyReferenceResponseModel.cs +++ b/src/Umbraco.Cms.Api.Management/ViewModels/TrackedReferences/MemberTypePropertyTypeReferenceResponseModel.cs @@ -1,6 +1,6 @@ namespace Umbraco.Cms.Api.Management.ViewModels.TrackedReferences; -public class MemberTypePropertyReferenceResponseModel : ContentTypePropertyReferenceResponseModel +public class MemberTypePropertyTypeReferenceResponseModel : ContentTypePropertyTypeReferenceResponseModel { public TrackedReferenceMemberType MemberType { get; set; } = new(); } diff --git a/src/Umbraco.Core/Constants-ReferenceTypes.cs b/src/Umbraco.Core/Constants-ReferenceTypes.cs index c76de94733bc..b006a0d59007 100644 --- a/src/Umbraco.Core/Constants-ReferenceTypes.cs +++ b/src/Umbraco.Core/Constants-ReferenceTypes.cs @@ -18,8 +18,8 @@ public static class ReferenceType public const string Document = UdiEntityType.Document; public const string Media = UdiEntityType.Media; public const string Member = UdiEntityType.Member; - public const string DocumentTypeProperty = "document-type-property"; - public const string MediaTypeProperty = "media-type-property"; - public const string MemberTypeProperty = "member-type-property"; + public const string DocumentTypePropertyType = "document-type-property-type"; + public const string MediaTypePropertyType = "media-type-property-type"; + public const string MemberTypePropertyType = "member-type-property-type"; } } diff --git a/src/Umbraco.Core/Services/DataTypeService.cs b/src/Umbraco.Core/Services/DataTypeService.cs index 89b24732d788..4420fa8d9a96 100644 --- a/src/Umbraco.Core/Services/DataTypeService.cs +++ b/src/Umbraco.Core/Services/DataTypeService.cs @@ -789,9 +789,9 @@ public Task> GetPagedRelationsAsync(Guid key, int string nodeType = x.Udi.EntityType switch { - Constants.UdiEntityType.DocumentType => Constants.ReferenceType.DocumentTypeProperty, - Constants.UdiEntityType.MediaType => Constants.ReferenceType.MediaTypeProperty, - Constants.UdiEntityType.MemberType => Constants.ReferenceType.MemberTypeProperty, + Constants.UdiEntityType.DocumentType => Constants.ReferenceType.DocumentTypePropertyType, + Constants.UdiEntityType.MediaType => Constants.ReferenceType.MediaTypePropertyType, + Constants.UdiEntityType.MemberType => Constants.ReferenceType.MemberTypePropertyType, _ => throw new ArgumentOutOfRangeException(nameof(x.Udi.EntityType)), }; From e77f77a73ca6d11533c064dd90cc5a54c664edf6 Mon Sep 17 00:00:00 2001 From: mole Date: Fri, 4 Apr 2025 13:18:23 +0200 Subject: [PATCH 09/10] Fix typo --- .../DataType/References/ReferencedByDataTypeController.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Umbraco.Cms.Api.Management/Controllers/DataType/References/ReferencedByDataTypeController.cs b/src/Umbraco.Cms.Api.Management/Controllers/DataType/References/ReferencedByDataTypeController.cs index 6f8c14fb7adb..b36815d408ee 100644 --- a/src/Umbraco.Cms.Api.Management/Controllers/DataType/References/ReferencedByDataTypeController.cs +++ b/src/Umbraco.Cms.Api.Management/Controllers/DataType/References/ReferencedByDataTypeController.cs @@ -2,7 +2,6 @@ using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using Umbraco.Cms.Api.Common.ViewModels.Pagination; -using Umbraco.Cms.Api.Management.Controllers.DataType; using Umbraco.Cms.Api.Management.Factories; using Umbraco.Cms.Api.Management.ViewModels.TrackedReferences; using Umbraco.Cms.Core.Models; @@ -23,7 +22,7 @@ public ReferencedByDataTypeController(IDataTypeService dataTypeService, IRelatio } /// - /// Gets a paged list of references for the current data type, so you can see where is is being used. + /// Gets a paged list of references for the current data type, so you can see where it is being used. /// [HttpGet("{id:guid}/referenced-by")] [MapToApiVersion("1.0")] From 35519af84e9c563831179e8a1582a717d580b5f4 Mon Sep 17 00:00:00 2001 From: mole Date: Fri, 4 Apr 2025 13:18:32 +0200 Subject: [PATCH 10/10] Fix multiple enumeration --- src/Umbraco.Core/Services/DataTypeService.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/Umbraco.Core/Services/DataTypeService.cs b/src/Umbraco.Core/Services/DataTypeService.cs index 4420fa8d9a96..7bc0b4d95f67 100644 --- a/src/Umbraco.Core/Services/DataTypeService.cs +++ b/src/Umbraco.Core/Services/DataTypeService.cs @@ -771,11 +771,12 @@ public Task> GetPagedRelationsAsync(Guid key, int var totalItems = combinedUsages.Count; // Create the page of items. - IEnumerable<(string PropertyAlias, Udi Udi)> pagedUsages = combinedUsages + IList<(string PropertyAlias, Udi Udi)> pagedUsages = combinedUsages .OrderBy(x => x.Udi.EntityType) // Document types first, then media types, then member types. .ThenBy(x => x.PropertyAlias) .Skip(skip) - .Take(take); + .Take(take) + .ToList(); // Get the content types for the UDIs referenced in the page of items to construct the response from. // They could be document, media or member types. @@ -814,7 +815,7 @@ public Task> GetPagedRelationsAsync(Guid key, int return Task.FromResult(pagedModel); } - private IList GetReferencedContentTypes(IEnumerable<(string PropertyAlias, Udi Udi)> pagedUsages) + private IList GetReferencedContentTypes(IList<(string PropertyAlias, Udi Udi)> pagedUsages) { IEnumerable documentTypes = GetContentTypes( pagedUsages,