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 <nikolajlauridsen@protonmail.ch>
This commit is contained in:
Andy Butland
2025-04-04 15:10:06 +02:00
committed by GitHub
parent fd77074d57
commit 1f4c19d484
21 changed files with 591 additions and 8 deletions

View File

@@ -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;
}
/// <summary>
/// Gets a paged list of references for the current data type, so you can see where it is being used.
/// </summary>
[HttpGet("{id:guid}/referenced-by")]
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(PagedViewModel<IReferenceResponseModel>), StatusCodes.Status200OK)]
public async Task<ActionResult<PagedViewModel<IReferenceResponseModel>>> ReferencedBy(
CancellationToken cancellationToken,
Guid id,
int skip = 0,
int take = 20)
{
PagedModel<RelationItemModel> relationItems = await _dataTypeService.GetPagedRelationsAsync(id, skip, take);
var pagedViewModel = new PagedViewModel<IReferenceResponseModel>
{
Total = relationItems.Total,
Items = await _relationTypePresentationFactory.CreateReferenceResponseModelsAsync(relationItems.Items),
};
return pagedViewModel;
}
}

View File

@@ -1,4 +1,4 @@
using Asp.Versioning; using Asp.Versioning;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Management.Factories; using Umbraco.Cms.Api.Management.Factories;
@@ -10,6 +10,7 @@ using Umbraco.Cms.Core.Services.OperationStatus;
namespace Umbraco.Cms.Api.Management.Controllers.DataType; namespace Umbraco.Cms.Api.Management.Controllers.DataType;
[ApiVersion("1.0")] [ApiVersion("1.0")]
[Obsolete("Please use ReferencedByDataTypeController and the referenced-by endpoint. Scheduled for removal in Umbraco 17.")]
public class ReferencesDataTypeController : DataTypeControllerBase public class ReferencesDataTypeController : DataTypeControllerBase
{ {
private readonly IDataTypeService _dataTypeService; private readonly IDataTypeService _dataTypeService;

View File

@@ -56,9 +56,12 @@ public class RelationTypePresentationFactory : IRelationTypePresentationFactory
IReferenceResponseModel[] result = relationItemModelsCollection.Select(relationItemModel => IReferenceResponseModel[] result = relationItemModelsCollection.Select(relationItemModel =>
relationItemModel.NodeType switch relationItemModel.NodeType switch
{ {
Constants.UdiEntityType.Document => MapDocumentReference(relationItemModel, slimEntities), Constants.ReferenceType.Document => MapDocumentReference(relationItemModel, slimEntities),
Constants.UdiEntityType.Media => _umbracoMapper.Map<MediaReferenceResponseModel>(relationItemModel), Constants.ReferenceType.Media => _umbracoMapper.Map<MediaReferenceResponseModel>(relationItemModel),
Constants.UdiEntityType.Member => _umbracoMapper.Map<MemberReferenceResponseModel>(relationItemModel), Constants.ReferenceType.Member => _umbracoMapper.Map<MemberReferenceResponseModel>(relationItemModel),
Constants.ReferenceType.DocumentTypePropertyType => _umbracoMapper.Map<DocumentTypePropertyTypeReferenceResponseModel>(relationItemModel),
Constants.ReferenceType.MediaTypePropertyType => _umbracoMapper.Map<MediaTypePropertyTypeReferenceResponseModel>(relationItemModel),
Constants.ReferenceType.MemberTypePropertyType => _umbracoMapper.Map<MemberTypePropertyTypeReferenceResponseModel>(relationItemModel),
_ => _umbracoMapper.Map<DefaultReferenceResponseModel>(relationItemModel), _ => _umbracoMapper.Map<DefaultReferenceResponseModel>(relationItemModel),
}).WhereNotNull().ToArray(); }).WhereNotNull().ToArray();

View File

@@ -12,6 +12,9 @@ public class TrackedReferenceViewModelsMapDefinition : IMapDefinition
mapper.Define<RelationItemModel, DocumentReferenceResponseModel>((source, context) => new DocumentReferenceResponseModel(), Map); mapper.Define<RelationItemModel, DocumentReferenceResponseModel>((source, context) => new DocumentReferenceResponseModel(), Map);
mapper.Define<RelationItemModel, MediaReferenceResponseModel>((source, context) => new MediaReferenceResponseModel(), Map); mapper.Define<RelationItemModel, MediaReferenceResponseModel>((source, context) => new MediaReferenceResponseModel(), Map);
mapper.Define<RelationItemModel, MemberReferenceResponseModel>((source, context) => new MemberReferenceResponseModel(), Map); mapper.Define<RelationItemModel, MemberReferenceResponseModel>((source, context) => new MemberReferenceResponseModel(), Map);
mapper.Define<RelationItemModel, DocumentTypePropertyTypeReferenceResponseModel>((source, context) => new DocumentTypePropertyTypeReferenceResponseModel(), Map);
mapper.Define<RelationItemModel, MediaTypePropertyTypeReferenceResponseModel>((source, context) => new MediaTypePropertyTypeReferenceResponseModel(), Map);
mapper.Define<RelationItemModel, MemberTypePropertyTypeReferenceResponseModel>((source, context) => new MemberTypePropertyTypeReferenceResponseModel(), Map);
mapper.Define<RelationItemModel, DefaultReferenceResponseModel>((source, context) => new DefaultReferenceResponseModel(), Map); mapper.Define<RelationItemModel, DefaultReferenceResponseModel>((source, context) => new DefaultReferenceResponseModel(), Map);
mapper.Define<RelationItemModel, ReferenceByIdModel>((source, context) => new ReferenceByIdModel(), Map); mapper.Define<RelationItemModel, ReferenceByIdModel>((source, context) => new ReferenceByIdModel(), Map);
mapper.Define<Guid, ReferenceByIdModel>((source, context) => new ReferenceByIdModel(), Map); mapper.Define<Guid, ReferenceByIdModel>((source, context) => new ReferenceByIdModel(), Map);
@@ -25,6 +28,7 @@ public class TrackedReferenceViewModelsMapDefinition : IMapDefinition
target.Published = source.NodePublished; target.Published = source.NodePublished;
target.DocumentType = new TrackedReferenceDocumentType target.DocumentType = new TrackedReferenceDocumentType
{ {
Id = source.ContentTypeKey,
Alias = source.ContentTypeAlias, Alias = source.ContentTypeAlias,
Icon = source.ContentTypeIcon, Icon = source.ContentTypeIcon,
Name = source.ContentTypeName, Name = source.ContentTypeName,
@@ -38,6 +42,7 @@ public class TrackedReferenceViewModelsMapDefinition : IMapDefinition
target.Name = source.NodeName; target.Name = source.NodeName;
target.MediaType = new TrackedReferenceMediaType target.MediaType = new TrackedReferenceMediaType
{ {
Id = source.ContentTypeKey,
Alias = source.ContentTypeAlias, Alias = source.ContentTypeAlias,
Icon = source.ContentTypeIcon, Icon = source.ContentTypeIcon,
Name = source.ContentTypeName, Name = source.ContentTypeName,
@@ -51,6 +56,52 @@ public class TrackedReferenceViewModelsMapDefinition : IMapDefinition
target.Name = source.NodeName; target.Name = source.NodeName;
target.MemberType = new TrackedReferenceMemberType 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, Alias = source.ContentTypeAlias,
Icon = source.ContentTypeIcon, Icon = source.ContentTypeIcon,
Name = source.ContentTypeName, Name = source.ContentTypeName,

View File

@@ -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": { "/umbraco/management/api/v1/data-type/{id}/references": {
"get": { "get": {
"tags": [ "tags": [
@@ -872,6 +936,7 @@
"description": "The authenticated user does not have access to this resource" "description": "The authenticated user does not have access to this resource"
} }
}, },
"deprecated": true,
"security": [ "security": [
{ {
"Backoffice User": [ ] "Backoffice User": [ ]
@@ -37931,6 +37996,45 @@
}, },
"additionalProperties": false "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": { "DocumentTypePropertyTypeContainerResponseModel": {
"required": [ "required": [
"id", "id",
@@ -39995,6 +40099,45 @@
}, },
"additionalProperties": false "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": { "MediaTypePropertyTypeContainerResponseModel": {
"required": [ "required": [
"id", "id",
@@ -40773,6 +40916,45 @@
}, },
"additionalProperties": false "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": { "MemberTypePropertyTypeContainerResponseModel": {
"required": [ "required": [
"id", "id",
@@ -41980,11 +42162,20 @@
{ {
"$ref": "#/components/schemas/DocumentReferenceResponseModel" "$ref": "#/components/schemas/DocumentReferenceResponseModel"
}, },
{
"$ref": "#/components/schemas/DocumentTypePropertyReferenceResponseModel"
},
{ {
"$ref": "#/components/schemas/MediaReferenceResponseModel" "$ref": "#/components/schemas/MediaReferenceResponseModel"
}, },
{
"$ref": "#/components/schemas/MediaTypePropertyReferenceResponseModel"
},
{ {
"$ref": "#/components/schemas/MemberReferenceResponseModel" "$ref": "#/components/schemas/MemberReferenceResponseModel"
},
{
"$ref": "#/components/schemas/MemberTypePropertyReferenceResponseModel"
} }
] ]
} }
@@ -44639,8 +44830,15 @@
"additionalProperties": false "additionalProperties": false
}, },
"TrackedReferenceDocumentTypeModel": { "TrackedReferenceDocumentTypeModel": {
"required": [
"id"
],
"type": "object", "type": "object",
"properties": { "properties": {
"id": {
"type": "string",
"format": "uuid"
},
"icon": { "icon": {
"type": "string", "type": "string",
"nullable": true "nullable": true
@@ -44657,8 +44855,15 @@
"additionalProperties": false "additionalProperties": false
}, },
"TrackedReferenceMediaTypeModel": { "TrackedReferenceMediaTypeModel": {
"required": [
"id"
],
"type": "object", "type": "object",
"properties": { "properties": {
"id": {
"type": "string",
"format": "uuid"
},
"icon": { "icon": {
"type": "string", "type": "string",
"nullable": true "nullable": true
@@ -44675,8 +44880,15 @@
"additionalProperties": false "additionalProperties": false
}, },
"TrackedReferenceMemberTypeModel": { "TrackedReferenceMemberTypeModel": {
"required": [
"id"
],
"type": "object", "type": "object",
"properties": { "properties": {
"id": {
"type": "string",
"format": "uuid"
},
"icon": { "icon": {
"type": "string", "type": "string",
"nullable": true "nullable": true

View File

@@ -0,0 +1,6 @@
namespace Umbraco.Cms.Api.Management.ViewModels.TrackedReferences;
public abstract class ContentTypePropertyTypeReferenceResponseModel : ReferenceResponseModel
{
public string? Alias { get; set; }
}

View File

@@ -0,0 +1,6 @@
namespace Umbraco.Cms.Api.Management.ViewModels.TrackedReferences;
public class DocumentTypePropertyTypeReferenceResponseModel : ContentTypePropertyTypeReferenceResponseModel
{
public TrackedReferenceDocumentType DocumentType { get; set; } = new();
}

View File

@@ -0,0 +1,6 @@
namespace Umbraco.Cms.Api.Management.ViewModels.TrackedReferences;
public class MediaTypePropertyTypeReferenceResponseModel : ContentTypePropertyTypeReferenceResponseModel
{
public TrackedReferenceMediaType MediaType { get; set; } = new();
}

View File

@@ -0,0 +1,6 @@
namespace Umbraco.Cms.Api.Management.ViewModels.TrackedReferences;
public class MemberTypePropertyTypeReferenceResponseModel : ContentTypePropertyTypeReferenceResponseModel
{
public TrackedReferenceMemberType MemberType { get; set; } = new();
}

View File

@@ -1,7 +1,9 @@
namespace Umbraco.Cms.Api.Management.ViewModels.TrackedReferences; namespace Umbraco.Cms.Api.Management.ViewModels.TrackedReferences;
public abstract class TrackedReferenceContentType public abstract class TrackedReferenceContentType
{ {
public Guid Id { get; set; }
public string? Icon { get; set; } public string? Icon { get; set; }
public string? Alias { get; set; } public string? Alias { get; set; }

View File

@@ -0,0 +1,25 @@
namespace Umbraco.Cms.Core;
public static partial class Constants
{
/// <summary>
/// Defines reference types.
/// </summary>
/// <remarks>
/// 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).
/// </remarks>
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";
}
}

View File

@@ -23,6 +23,9 @@ public class RelationItem
[DataMember(Name = "published")] [DataMember(Name = "published")]
public bool? NodePublished { get; set; } public bool? NodePublished { get; set; }
[DataMember(Name = "contentTypeKey")]
public Guid ContentTypeKey { get; set; }
[DataMember(Name = "icon")] [DataMember(Name = "icon")]
public string? ContentTypeIcon { get; set; } public string? ContentTypeIcon { get; set; }

View File

@@ -1,15 +1,19 @@
namespace Umbraco.Cms.Core.Models; namespace Umbraco.Cms.Core.Models;
public class RelationItemModel public class RelationItemModel
{ {
public Guid NodeKey { get; set; } public Guid NodeKey { get; set; }
public string? NodeAlias { get; set; }
public string? NodeName { get; set; } public string? NodeName { get; set; }
public string? NodeType { get; set; } public string? NodeType { get; set; }
public bool? NodePublished { get; set; } public bool? NodePublished { get; set; }
public Guid ContentTypeKey { get; set; }
public string? ContentTypeIcon { get; set; } public string? ContentTypeIcon { get; set; }
public string? ContentTypeAlias { get; set; } public string? ContentTypeAlias { get; set; }

View File

@@ -24,6 +24,5 @@ public interface IDataTypeRepository : IReadWriteQueryRepository<int, IDataType>
/// </summary> /// </summary>
/// <param name="id"></param> /// <param name="id"></param>
/// <returns></returns> /// <returns></returns>
IReadOnlyDictionary<Udi, IEnumerable<string>> FindListViewUsages(int id) => throw new NotImplementedException(); IReadOnlyDictionary<Udi, IEnumerable<string>> FindListViewUsages(int id) => throw new NotImplementedException();
} }

View File

@@ -25,12 +25,15 @@ namespace Umbraco.Cms.Core.Services.Implement
private readonly IDataTypeRepository _dataTypeRepository; private readonly IDataTypeRepository _dataTypeRepository;
private readonly IDataTypeContainerRepository _dataTypeContainerRepository; private readonly IDataTypeContainerRepository _dataTypeContainerRepository;
private readonly IContentTypeRepository _contentTypeRepository; private readonly IContentTypeRepository _contentTypeRepository;
private readonly IMediaTypeRepository _mediaTypeRepository;
private readonly IMemberTypeRepository _memberTypeRepository;
private readonly IAuditRepository _auditRepository; private readonly IAuditRepository _auditRepository;
private readonly IIOHelper _ioHelper; private readonly IIOHelper _ioHelper;
private readonly IDataTypeContainerService _dataTypeContainerService; private readonly IDataTypeContainerService _dataTypeContainerService;
private readonly IUserIdKeyResolver _userIdKeyResolver; private readonly IUserIdKeyResolver _userIdKeyResolver;
private readonly Lazy<IIdKeyMap> _idKeyMap; private readonly Lazy<IIdKeyMap> _idKeyMap;
[Obsolete("Please use the constructor taking all parameters. Scheduled for removal in Umbraco 17.")]
public DataTypeService( public DataTypeService(
ICoreScopeProvider provider, ICoreScopeProvider provider,
ILoggerFactory loggerFactory, ILoggerFactory loggerFactory,
@@ -41,12 +44,41 @@ namespace Umbraco.Cms.Core.Services.Implement
IContentTypeRepository contentTypeRepository, IContentTypeRepository contentTypeRepository,
IIOHelper ioHelper, IIOHelper ioHelper,
Lazy<IIdKeyMap> idKeyMap) Lazy<IIdKeyMap> idKeyMap)
: this(
provider,
loggerFactory,
eventMessagesFactory,
dataTypeRepository,
dataValueEditorFactory,
auditRepository,
contentTypeRepository,
StaticServiceProvider.Instance.GetRequiredService<IMediaTypeRepository>(),
StaticServiceProvider.Instance.GetRequiredService<IMemberTypeRepository>(),
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<IIdKeyMap> idKeyMap)
: base(provider, loggerFactory, eventMessagesFactory) : base(provider, loggerFactory, eventMessagesFactory)
{ {
_dataValueEditorFactory = dataValueEditorFactory; _dataValueEditorFactory = dataValueEditorFactory;
_dataTypeRepository = dataTypeRepository; _dataTypeRepository = dataTypeRepository;
_auditRepository = auditRepository; _auditRepository = auditRepository;
_contentTypeRepository = contentTypeRepository; _contentTypeRepository = contentTypeRepository;
_mediaTypeRepository = mediaTypeRepository;
_memberTypeRepository = memberTypeRepository;
_ioHelper = ioHelper; _ioHelper = ioHelper;
_idKeyMap = idKeyMap; _idKeyMap = idKeyMap;
@@ -703,12 +735,119 @@ namespace Umbraco.Cms.Core.Services.Implement
return await Task.FromResult(Attempt.SucceedWithStatus(DataTypeOperationStatus.Success, usages)); return await Task.FromResult(Attempt.SucceedWithStatus(DataTypeOperationStatus.Success, usages));
} }
/// <inheritdoc />
public IReadOnlyDictionary<Udi, IEnumerable<string>> GetListViewReferences(int id) public IReadOnlyDictionary<Udi, IEnumerable<string>> GetListViewReferences(int id)
{ {
using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true); using ICoreScope scope = ScopeProvider.CreateCoreScope(autoComplete: true);
return _dataTypeRepository.FindListViewUsages(id); return _dataTypeRepository.FindListViewUsages(id);
} }
/// <inheritdoc />
public Task<PagedModel<RelationItemModel>> 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<RelationItemModel>());
}
// 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<Udi, IEnumerable<string>> usages = _dataTypeRepository.FindUsages(dataType.Id);
IReadOnlyDictionary<Udi, IEnumerable<string>> 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<IContentTypeComposition> contentTypes = GetReferencedContentTypes(pagedUsages);
IEnumerable<RelationItemModel> 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<RelationItemModel>(totalItems, relations);
return Task.FromResult(pagedModel);
}
private IList<IContentTypeComposition> GetReferencedContentTypes(IList<(string PropertyAlias, Udi Udi)> pagedUsages)
{
IEnumerable<IContentTypeComposition> documentTypes = GetContentTypes(
pagedUsages,
Constants.UdiEntityType.DocumentType,
_contentTypeRepository);
IEnumerable<IContentTypeComposition> mediaTypes = GetContentTypes(
pagedUsages,
Constants.UdiEntityType.MediaType,
_mediaTypeRepository);
IEnumerable<IContentTypeComposition> memberTypes = GetContentTypes(
pagedUsages,
Constants.UdiEntityType.MemberType,
_memberTypeRepository);
return documentTypes.Concat(mediaTypes).Concat(memberTypes).ToList();
}
private static IEnumerable<T> GetContentTypes<T>(
IEnumerable<(string PropertyAlias, Udi Udi)> dataTypeUsages,
string entityType,
IContentTypeRepositoryBase<T> 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)
: [];
}
/// <inheritdoc /> /// <inheritdoc />
public IEnumerable<ValidationResult> ValidateConfigurationData(IDataType dataType) public IEnumerable<ValidationResult> ValidateConfigurationData(IDataType dataType)
{ {

View File

@@ -18,6 +18,7 @@ public interface IDataTypeService : IService
[Obsolete("Please use GetReferencesAsync. Will be deleted in V15.")] [Obsolete("Please use GetReferencesAsync. Will be deleted in V15.")]
IReadOnlyDictionary<Udi, IEnumerable<string>> GetReferences(int id); IReadOnlyDictionary<Udi, IEnumerable<string>> GetReferences(int id);
[Obsolete("Please use GetPagedRelationsAsync. Scheduled for removal in Umbraco 17.")]
IReadOnlyDictionary<Udi, IEnumerable<string>> GetListViewReferences(int id) => throw new NotImplementedException(); IReadOnlyDictionary<Udi, IEnumerable<string>> GetListViewReferences(int id) => throw new NotImplementedException();
/// <summary> /// <summary>
@@ -25,8 +26,25 @@ public interface IDataTypeService : IService
/// </summary> /// </summary>
/// <param name="id">The guid Id of the <see cref="IDataType" /></param> /// <param name="id">The guid Id of the <see cref="IDataType" /></param>
/// <returns></returns> /// <returns></returns>
[Obsolete("Please use GetPagedRelationsAsync. Scheduled for removal in Umbraco 17.")]
Task<Attempt<IReadOnlyDictionary<Udi, IEnumerable<string>>, DataTypeOperationStatus>> GetReferencesAsync(Guid id); Task<Attempt<IReadOnlyDictionary<Udi, IEnumerable<string>>, DataTypeOperationStatus>> GetReferencesAsync(Guid id);
/// <summary>
/// Gets a paged result of items which are in relation with the current data type.
/// </summary>
/// <param name="key">The identifier of the data type to retrieve relations for.</param>
/// <param name="skip">The amount of items to skip</param>
/// <param name="take">The amount of items to take.</param>
/// <returns>A paged result of <see cref="RelationItemModel" /> objects.</returns>
/// <remarks>
/// 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 <see cref="ITrackedReferencesService"/>.
/// 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.
/// </remarks>
Task<PagedModel<RelationItemModel>> GetPagedRelationsAsync(Guid key, int skip, int take)
=> Task.FromResult(new PagedModel<RelationItemModel>());
[Obsolete("Please use IDataTypeContainerService for all data type container operations. Will be removed in V15.")] [Obsolete("Please use IDataTypeContainerService for all data type container operations. Will be removed in V15.")]
Attempt<OperationResult<OperationResultType, EntityContainer>?> CreateContainer(int parentId, Guid key, string name, Attempt<OperationResult<OperationResultType, EntityContainer>?> CreateContainer(int parentId, Guid key, string name,
int userId = Constants.Security.SuperUserId); int userId = Constants.Security.SuperUserId);

View File

@@ -1,4 +1,4 @@
using Umbraco.Cms.Core.Mapping; using Umbraco.Cms.Core.Mapping;
using Umbraco.Cms.Core.Models; using Umbraco.Cms.Core.Models;
using Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement; using Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement;
@@ -19,6 +19,7 @@ public class RelationModelMapDefinition : IMapDefinition
target.RelationTypeName = source.RelationTypeName; target.RelationTypeName = source.RelationTypeName;
target.RelationTypeIsBidirectional = source.RelationTypeIsBidirectional; target.RelationTypeIsBidirectional = source.RelationTypeIsBidirectional;
target.RelationTypeIsDependency = source.RelationTypeIsDependency; target.RelationTypeIsDependency = source.RelationTypeIsDependency;
target.ContentTypeKey = source.ChildContentTypeKey;
target.ContentTypeAlias = source.ChildContentTypeAlias; target.ContentTypeAlias = source.ChildContentTypeAlias;
target.ContentTypeIcon = source.ChildContentTypeIcon; target.ContentTypeIcon = source.ChildContentTypeIcon;
target.ContentTypeName = source.ChildContentTypeName; target.ContentTypeName = source.ChildContentTypeName;

View File

@@ -475,6 +475,9 @@ internal class RelationItemDto
[Column(Name = "nodeObjectType")] [Column(Name = "nodeObjectType")]
public Guid ChildNodeObjectType { get; set; } public Guid ChildNodeObjectType { get; set; }
[Column(Name = "contentTypeKey")]
public Guid ChildContentTypeKey { get; set; }
[Column(Name = "contentTypeIcon")] [Column(Name = "contentTypeIcon")]
public string? ChildContentTypeIcon { get; set; } public string? ChildContentTypeIcon { get; set; }

View File

@@ -35,6 +35,7 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement
"[n].[text] as nodeName", "[n].[text] as nodeName",
"[n].[nodeObjectType] as nodeObjectType", "[n].[nodeObjectType] as nodeObjectType",
"[d].[published] as nodePublished", "[d].[published] as nodePublished",
"[ctn].[uniqueId] as contentTypeKey",
"[ct].[icon] as contentTypeIcon", "[ct].[icon] as contentTypeIcon",
"[ct].[alias] as contentTypeAlias", "[ct].[alias] as contentTypeAlias",
"[ctn].[text] as contentTypeName", "[ctn].[text] as contentTypeName",
@@ -188,6 +189,7 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement
"[n].[text] as nodeName", "[n].[text] as nodeName",
"[n].[nodeObjectType] as nodeObjectType", "[n].[nodeObjectType] as nodeObjectType",
"[d].[published] as nodePublished", "[d].[published] as nodePublished",
"[ctn].[uniqueId] as contentTypeKey",
"[ct].[icon] as contentTypeIcon", "[ct].[icon] as contentTypeIcon",
"[ct].[alias] as contentTypeAlias", "[ct].[alias] as contentTypeAlias",
"[ctn].[text] as contentTypeName", "[ctn].[text] as contentTypeName",
@@ -250,6 +252,7 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement
"[n].[text] as nodeName", "[n].[text] as nodeName",
"[n].[nodeObjectType] as nodeObjectType", "[n].[nodeObjectType] as nodeObjectType",
"[d].[published] as nodePublished", "[d].[published] as nodePublished",
"[ctn].[uniqueId] as contentTypeKey",
"[ct].[icon] as contentTypeIcon", "[ct].[icon] as contentTypeIcon",
"[ct].[alias] as contentTypeAlias", "[ct].[alias] as contentTypeAlias",
"[ctn].[text] as contentTypeName", "[ctn].[text] as contentTypeName",
@@ -336,6 +339,7 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement
"[n].[text] as nodeName", "[n].[text] as nodeName",
"[n].[nodeObjectType] as nodeObjectType", "[n].[nodeObjectType] as nodeObjectType",
"[d].[published] as nodePublished", "[d].[published] as nodePublished",
"[ctn].[uniqueId] as contentTypeKey",
"[ct].[icon] as contentTypeIcon", "[ct].[icon] as contentTypeIcon",
"[ct].[alias] as contentTypeAlias", "[ct].[alias] as contentTypeAlias",
"[ctn].[text] as contentTypeName", "[ctn].[text] as contentTypeName",
@@ -411,6 +415,7 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement
"[n].[uniqueId] as nodeKey", "[n].[uniqueId] as nodeKey",
"[n].[text] as nodeName", "[n].[text] as nodeName",
"[n].[nodeObjectType] as nodeObjectType", "[n].[nodeObjectType] as nodeObjectType",
"[ctn].[uniqueId] as contentTypeKey",
"[ct].[icon] as contentTypeIcon", "[ct].[icon] as contentTypeIcon",
"[ct].[alias] as contentTypeAlias", "[ct].[alias] as contentTypeAlias",
"[ctn].[text] as contentTypeName", "[ctn].[text] as contentTypeName",
@@ -477,6 +482,7 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement
"[n].[uniqueId] as nodeKey", "[n].[uniqueId] as nodeKey",
"[n].[text] as nodeName", "[n].[text] as nodeName",
"[n].[nodeObjectType] as nodeObjectType", "[n].[nodeObjectType] as nodeObjectType",
"[ctn].[uniqueId] as contentTypeKey",
"[ct].[icon] as contentTypeIcon", "[ct].[icon] as contentTypeIcon",
"[ct].[alias] as contentTypeAlias", "[ct].[alias] as contentTypeAlias",
"[ctn].[text] as contentTypeName", "[ctn].[text] as contentTypeName",
@@ -595,6 +601,7 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement
"[n].[text] as nodeName", "[n].[text] as nodeName",
"[n].[nodeObjectType] as nodeObjectType", "[n].[nodeObjectType] as nodeObjectType",
"[d].[published] as nodePublished", "[d].[published] as nodePublished",
"[ctn].[uniqueId] as contentTypeKey",
"[ct].[icon] as contentTypeIcon", "[ct].[icon] as contentTypeIcon",
"[ct].[alias] as contentTypeAlias", "[ct].[alias] as contentTypeAlias",
"[ctn].[text] as contentTypeName", "[ctn].[text] as contentTypeName",
@@ -671,6 +678,7 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement
"[n].[uniqueId] as nodeKey", "[n].[uniqueId] as nodeKey",
"[n].[text] as nodeName", "[n].[text] as nodeName",
"[n].[nodeObjectType] as nodeObjectType", "[n].[nodeObjectType] as nodeObjectType",
"[ctn].[uniqueId] as contentTypeKey",
"[ct].[icon] as contentTypeIcon", "[ct].[icon] as contentTypeIcon",
"[ct].[alias] as contentTypeAlias", "[ct].[alias] as contentTypeAlias",
"[ctn].[text] as contentTypeName", "[ctn].[text] as contentTypeName",
@@ -749,6 +757,7 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement
"[n].[uniqueId] as nodeKey", "[n].[uniqueId] as nodeKey",
"[n].[text] as nodeName", "[n].[text] as nodeName",
"[n].[nodeObjectType] as nodeObjectType", "[n].[nodeObjectType] as nodeObjectType",
"[ctn].[uniqueId] as contentTypeKey",
"[ct].[icon] as contentTypeIcon", "[ct].[icon] as contentTypeIcon",
"[ct].[alias] as contentTypeAlias", "[ct].[alias] as contentTypeAlias",
"[ctn].[text] as contentTypeName", "[ctn].[text] as contentTypeName",
@@ -841,6 +850,7 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement
RelationTypeName = dto.RelationTypeName, RelationTypeName = dto.RelationTypeName,
RelationTypeIsBidirectional = dto.RelationTypeIsBidirectional, RelationTypeIsBidirectional = dto.RelationTypeIsBidirectional,
RelationTypeIsDependency = dto.RelationTypeIsDependency, RelationTypeIsDependency = dto.RelationTypeIsDependency,
ContentTypeKey = dto.ChildContentTypeKey,
ContentTypeAlias = dto.ChildContentTypeAlias, ContentTypeAlias = dto.ChildContentTypeAlias,
ContentTypeIcon = dto.ChildContentTypeIcon, ContentTypeIcon = dto.ChildContentTypeIcon,
ContentTypeName = dto.ChildContentTypeName, ContentTypeName = dto.ChildContentTypeName,

View File

@@ -139,6 +139,7 @@ public class MediaTypeBuilder
var mediaType = builder var mediaType = builder
.WithAlias(alias) .WithAlias(alias)
.WithName(name) .WithName(name)
.WithIcon("icon-picture")
.WithParentContentType(parent) .WithParentContentType(parent)
.AddPropertyGroup() .AddPropertyGroup()
.WithAlias(propertyGroupAlias) .WithAlias(propertyGroupAlias)

View File

@@ -30,6 +30,8 @@ public class DataTypeServiceTests : UmbracoIntegrationTest
private IContentTypeService ContentTypeService => GetRequiredService<IContentTypeService>(); private IContentTypeService ContentTypeService => GetRequiredService<IContentTypeService>();
private IMediaTypeService MediaTypeService => GetRequiredService<IMediaTypeService>();
private IFileService FileService => GetRequiredService<IFileService>(); private IFileService FileService => GetRequiredService<IFileService>();
private IConfigurationEditorJsonSerializer ConfigurationEditorJsonSerializer => private IConfigurationEditorJsonSerializer ConfigurationEditorJsonSerializer =>
@@ -446,4 +448,43 @@ public class DataTypeServiceTests : UmbracoIntegrationTest
Assert.IsFalse(result.Success); Assert.IsFalse(result.Success);
Assert.AreEqual(DataTypeOperationStatus.NonDeletable, result.Status); Assert.AreEqual(DataTypeOperationStatus.NonDeletable, result.Status);
} }
[Test]
public async Task DataTypeService_Can_Get_References()
{
IEnumerable<IDataType> 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<RelationItemModel> 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);
}
} }