From 1f4c19d4845349850f98b76d16ecb375d51b8b7d Mon Sep 17 00:00:00 2001 From: Andy Butland Date: Fri, 4 Apr 2025 15:10:06 +0200 Subject: [PATCH] Updated management API endpoint and model for data type references to align with that used for documents, media etc. (#18905) * Updated management API endpoint and model for data type references to align with that used for documents, media etc. * Refactoring. * Update src/Umbraco.Core/Constants-ReferenceTypes.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Fixed typos. * Added id to tracked reference content type response. * Updated OpenApi.json. * Added missing updates. * Renamed model and constants from code review feedback. * Fix typo * Fix multiple enumeration --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: mole --- .../ReferencedByDataTypeController.cs | 46 ++++ .../DataType/ReferencesDataTypeController.cs | 3 +- .../RelationTypePresentationFactory.cs | 9 +- ...TrackedReferenceViewModelsMapDefinition.cs | 51 +++++ src/Umbraco.Cms.Api.Management/OpenApi.json | 212 ++++++++++++++++++ ...tTypePropertyTypeReferenceResponseModel.cs | 6 + ...tTypePropertyTypeReferenceResponseModel.cs | 6 + ...aTypePropertyTypeReferenceResponseModel.cs | 6 + ...rTypePropertyTypeReferenceResponseModel.cs | 6 + .../TrackedReferenceContentType.cs | 4 +- src/Umbraco.Core/Constants-ReferenceTypes.cs | 25 +++ src/Umbraco.Core/Models/RelationItem.cs | 3 + src/Umbraco.Core/Models/RelationItemModel.cs | 6 +- .../Repositories/IDataTypeRepository.cs | 1 - src/Umbraco.Core/Services/DataTypeService.cs | 139 ++++++++++++ src/Umbraco.Core/Services/IDataTypeService.cs | 18 ++ .../Mapping/RelationModelMapDefinition.cs | 3 +- .../Implement/RelationRepository.cs | 3 + .../Implement/TrackedReferencesRepository.cs | 10 + .../Builders/MediaTypeBuilder.cs | 1 + .../Services/DataTypeServiceTests.cs | 41 ++++ 21 files changed, 591 insertions(+), 8 deletions(-) create mode 100644 src/Umbraco.Cms.Api.Management/Controllers/DataType/References/ReferencedByDataTypeController.cs create mode 100644 src/Umbraco.Cms.Api.Management/ViewModels/TrackedReferences/ContentTypePropertyTypeReferenceResponseModel.cs create mode 100644 src/Umbraco.Cms.Api.Management/ViewModels/TrackedReferences/DocumentTypePropertyTypeReferenceResponseModel.cs create mode 100644 src/Umbraco.Cms.Api.Management/ViewModels/TrackedReferences/MediaTypePropertyTypeReferenceResponseModel.cs create mode 100644 src/Umbraco.Cms.Api.Management/ViewModels/TrackedReferences/MemberTypePropertyTypeReferenceResponseModel.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 0000000000..b36815d408 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/Controllers/DataType/References/ReferencedByDataTypeController.cs @@ -0,0 +1,46 @@ +using Asp.Versioning; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Umbraco.Cms.Api.Common.ViewModels.Pagination; +using Umbraco.Cms.Api.Management.Factories; +using Umbraco.Cms.Api.Management.ViewModels.TrackedReferences; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Services; + +namespace Umbraco.Cms.Api.Management.Controllers.DataType.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 it 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 c25586e93c..0eee28e49b 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 @@ using Umbraco.Cms.Core.Services.OperationStatus; 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 a6fb1cbae8..40bcf9f04c 100644 --- a/src/Umbraco.Cms.Api.Management/Factories/RelationTypePresentationFactory.cs +++ b/src/Umbraco.Cms.Api.Management/Factories/RelationTypePresentationFactory.cs @@ -56,9 +56,12 @@ public class RelationTypePresentationFactory : IRelationTypePresentationFactory 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.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 e9f2700f5a..c4efca3088 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 class TrackedReferenceViewModelsMapDefinition : IMapDefinition 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 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); @@ -25,6 +28,7 @@ public class TrackedReferenceViewModelsMapDefinition : IMapDefinition target.Published = source.NodePublished; target.DocumentType = new TrackedReferenceDocumentType { + Id = source.ContentTypeKey, Alias = source.ContentTypeAlias, Icon = source.ContentTypeIcon, Name = source.ContentTypeName, @@ -38,6 +42,7 @@ public class TrackedReferenceViewModelsMapDefinition : IMapDefinition target.Name = source.NodeName; target.MediaType = new TrackedReferenceMediaType { + Id = source.ContentTypeKey, Alias = source.ContentTypeAlias, Icon = source.ContentTypeIcon, Name = source.ContentTypeName, @@ -51,6 +56,52 @@ public class TrackedReferenceViewModelsMapDefinition : IMapDefinition target.Name = source.NodeName; target.MemberType = new TrackedReferenceMemberType { + Id = source.ContentTypeKey, + Alias = source.ContentTypeAlias, + Icon = source.ContentTypeIcon, + Name = source.ContentTypeName, + }; + } + + // Umbraco.Code.MapAll + private void Map(RelationItemModel source, DocumentTypePropertyTypeReferenceResponseModel target, MapperContext context) + { + target.Id = source.NodeKey; + target.Name = source.NodeName; + target.Alias = source.NodeAlias; + target.DocumentType = new TrackedReferenceDocumentType + { + Id = source.ContentTypeKey, + Alias = source.ContentTypeAlias, + Icon = source.ContentTypeIcon, + Name = source.ContentTypeName, + }; + } + + // Umbraco.Code.MapAll + private void Map(RelationItemModel source, MediaTypePropertyTypeReferenceResponseModel target, MapperContext context) + { + target.Id = source.NodeKey; + target.Name = source.NodeName; + target.Alias = source.NodeAlias; + target.MediaType = new TrackedReferenceMediaType + { + Id = source.ContentTypeKey, + Alias = source.ContentTypeAlias, + Icon = source.ContentTypeIcon, + Name = source.ContentTypeName, + }; + } + + // Umbraco.Code.MapAll + private void Map(RelationItemModel source, MemberTypePropertyTypeReferenceResponseModel target, MapperContext context) + { + target.Id = source.NodeKey; + target.Name = source.NodeName; + 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/OpenApi.json b/src/Umbraco.Cms.Api.Management/OpenApi.json index e87920c9ba..304e344e62 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": [ ] @@ -37931,6 +37996,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", @@ -39995,6 +40099,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", @@ -40773,6 +40916,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", @@ -41980,11 +42162,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" } ] } @@ -44639,8 +44830,15 @@ "additionalProperties": false }, "TrackedReferenceDocumentTypeModel": { + "required": [ + "id" + ], "type": "object", "properties": { + "id": { + "type": "string", + "format": "uuid" + }, "icon": { "type": "string", "nullable": true @@ -44657,8 +44855,15 @@ "additionalProperties": false }, "TrackedReferenceMediaTypeModel": { + "required": [ + "id" + ], "type": "object", "properties": { + "id": { + "type": "string", + "format": "uuid" + }, "icon": { "type": "string", "nullable": true @@ -44675,8 +44880,15 @@ "additionalProperties": false }, "TrackedReferenceMemberTypeModel": { + "required": [ + "id" + ], "type": "object", "properties": { + "id": { + "type": "string", + "format": "uuid" + }, "icon": { "type": "string", "nullable": true diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/TrackedReferences/ContentTypePropertyTypeReferenceResponseModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/TrackedReferences/ContentTypePropertyTypeReferenceResponseModel.cs new file mode 100644 index 0000000000..83dc6a1a7a --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/ViewModels/TrackedReferences/ContentTypePropertyTypeReferenceResponseModel.cs @@ -0,0 +1,6 @@ +namespace Umbraco.Cms.Api.Management.ViewModels.TrackedReferences; + +public abstract class ContentTypePropertyTypeReferenceResponseModel : ReferenceResponseModel +{ + public string? Alias { get; set; } +} diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/TrackedReferences/DocumentTypePropertyTypeReferenceResponseModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/TrackedReferences/DocumentTypePropertyTypeReferenceResponseModel.cs new file mode 100644 index 0000000000..b2ef3e4a0a --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/ViewModels/TrackedReferences/DocumentTypePropertyTypeReferenceResponseModel.cs @@ -0,0 +1,6 @@ +namespace Umbraco.Cms.Api.Management.ViewModels.TrackedReferences; + +public class DocumentTypePropertyTypeReferenceResponseModel : ContentTypePropertyTypeReferenceResponseModel +{ + public TrackedReferenceDocumentType DocumentType { get; set; } = new(); +} diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/TrackedReferences/MediaTypePropertyTypeReferenceResponseModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/TrackedReferences/MediaTypePropertyTypeReferenceResponseModel.cs new file mode 100644 index 0000000000..1baf647654 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/ViewModels/TrackedReferences/MediaTypePropertyTypeReferenceResponseModel.cs @@ -0,0 +1,6 @@ +namespace Umbraco.Cms.Api.Management.ViewModels.TrackedReferences; + +public class MediaTypePropertyTypeReferenceResponseModel : ContentTypePropertyTypeReferenceResponseModel +{ + public TrackedReferenceMediaType MediaType { get; set; } = new(); +} diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/TrackedReferences/MemberTypePropertyTypeReferenceResponseModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/TrackedReferences/MemberTypePropertyTypeReferenceResponseModel.cs new file mode 100644 index 0000000000..199a4b0ba1 --- /dev/null +++ b/src/Umbraco.Cms.Api.Management/ViewModels/TrackedReferences/MemberTypePropertyTypeReferenceResponseModel.cs @@ -0,0 +1,6 @@ +namespace Umbraco.Cms.Api.Management.ViewModels.TrackedReferences; + +public class MemberTypePropertyTypeReferenceResponseModel : ContentTypePropertyTypeReferenceResponseModel +{ + public TrackedReferenceMemberType MemberType { get; set; } = new(); +} diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/TrackedReferences/TrackedReferenceContentType.cs b/src/Umbraco.Cms.Api.Management/ViewModels/TrackedReferences/TrackedReferenceContentType.cs index 31456abada..15ac365e41 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.Core/Constants-ReferenceTypes.cs b/src/Umbraco.Core/Constants-ReferenceTypes.cs new file mode 100644 index 0000000000..b006a0d590 --- /dev/null +++ b/src/Umbraco.Core/Constants-ReferenceTypes.cs @@ -0,0 +1,25 @@ +namespace Umbraco.Cms.Core; + +public static partial class Constants +{ + /// + /// Defines reference types. + /// + /// + /// 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 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/Models/RelationItem.cs b/src/Umbraco.Core/Models/RelationItem.cs index a865e7cc2f..41fde0e867 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 a05c8f6591..1ca3bb9e11 100644 --- a/src/Umbraco.Core/Models/RelationItemModel.cs +++ b/src/Umbraco.Core/Models/RelationItemModel.cs @@ -1,15 +1,19 @@ -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; } 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/Persistence/Repositories/IDataTypeRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IDataTypeRepository.cs index ad113533d8..f0babf61f3 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 f22948968a..7bc0b4d95f 100644 --- a/src/Umbraco.Core/Services/DataTypeService.cs +++ b/src/Umbraco.Core/Services/DataTypeService.cs @@ -25,12 +25,15 @@ namespace Umbraco.Cms.Core.Services.Implement 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 @@ namespace Umbraco.Cms.Core.Services.Implement 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 memberTypeRepository, + IIOHelper ioHelper, + Lazy idKeyMap) : base(provider, loggerFactory, eventMessagesFactory) { _dataValueEditorFactory = dataValueEditorFactory; _dataTypeRepository = dataTypeRepository; _auditRepository = auditRepository; _contentTypeRepository = contentTypeRepository; + _mediaTypeRepository = mediaTypeRepository; + _memberTypeRepository = memberTypeRepository; _ioHelper = ioHelper; _idKeyMap = idKeyMap; @@ -703,12 +735,119 @@ namespace Umbraco.Cms.Core.Services.Implement 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 FindUsages and FindListViewUsages. + 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. + 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) + .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. + IList contentTypes = GetReferencedContentTypes(pagedUsages); + + 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.DocumentTypePropertyType, + Constants.UdiEntityType.MediaType => Constants.ReferenceType.MediaTypePropertyType, + Constants.UdiEntityType.MemberType => Constants.ReferenceType.MemberTypePropertyType, + _ => 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 + { + ContentTypeKey = contentType.Key, + 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 IList GetReferencedContentTypes(IList<(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, + 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 3a4576552c..0f2f58ceb8 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/src/Umbraco.Infrastructure/Mapping/RelationModelMapDefinition.cs b/src/Umbraco.Infrastructure/Mapping/RelationModelMapDefinition.cs index 8ace25c07f..1f0c986e0d 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 @@ public class RelationModelMapDefinition : IMapDefinition 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 a38bf4547f..2786bbfd1e 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 ced64f4996..d9bda52916 100644 --- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/TrackedReferencesRepository.cs +++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/TrackedReferencesRepository.cs @@ -35,6 +35,7 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement "[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", @@ -188,6 +189,7 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement "[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", @@ -250,6 +252,7 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement "[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", @@ -336,6 +339,7 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement "[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", @@ -411,6 +415,7 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement "[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", @@ -477,6 +482,7 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement "[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", @@ -595,6 +601,7 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement "[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", @@ -671,6 +678,7 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement "[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", @@ -749,6 +757,7 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement "[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", @@ -841,6 +850,7 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement 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.Common/Builders/MediaTypeBuilder.cs b/tests/Umbraco.Tests.Common/Builders/MediaTypeBuilder.cs index dcde82b47d..6439899955 100644 --- a/tests/Umbraco.Tests.Common/Builders/MediaTypeBuilder.cs +++ b/tests/Umbraco.Tests.Common/Builders/MediaTypeBuilder.cs @@ -139,6 +139,7 @@ public class MediaTypeBuilder 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 1c2b46137b..92a1567a4f 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,43 @@ public class DataTypeServiceTests : UmbracoIntegrationTest 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(documentType.Key, firstResult.ContentTypeKey); + 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(mediaType.Key, secondResult.ContentTypeKey); + Assert.AreEqual("bodyText", secondResult.NodeAlias); + Assert.AreEqual("Body text", secondResult.NodeName); + } }