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..b36815d408ee --- /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 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..40bcf9f04c16 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.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 e9f2700f5ad2..c4efca308829 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 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 @@ 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, @@ -38,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, @@ -51,6 +56,52 @@ 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, + }; + } + + // 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 44a69ccc957e..06ab75e1b5f9 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" } ] } @@ -44396,8 +44587,15 @@ "additionalProperties": false }, "TrackedReferenceDocumentTypeModel": { + "required": [ + "id" + ], "type": "object", "properties": { + "id": { + "type": "string", + "format": "uuid" + }, "icon": { "type": "string", "nullable": true @@ -44414,8 +44612,15 @@ "additionalProperties": false }, "TrackedReferenceMediaTypeModel": { + "required": [ + "id" + ], "type": "object", "properties": { + "id": { + "type": "string", + "format": "uuid" + }, "icon": { "type": "string", "nullable": true @@ -44432,8 +44637,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 000000000000..83dc6a1a7a5f --- /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 000000000000..b2ef3e4a0ad2 --- /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 000000000000..1baf6476545f --- /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 000000000000..199a4b0ba18d --- /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 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.Core/Constants-ReferenceTypes.cs b/src/Umbraco.Core/Constants-ReferenceTypes.cs new file mode 100644 index 000000000000..b006a0d59007 --- /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 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 a05c8f65918f..1ca3bb9e111e 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 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..7bc0b4d95f67 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 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 @@ 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 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 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/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.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..92a1567a4f33 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 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(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); + } }