Added management API endpoint, service and repository for retrieval of references from the recycle bin (#18882)
* Added management API endpoint, service and repository for retrieval of references from the recycle bin. * Update src/Umbraco.Cms.Api.Management/Controllers/Document/RecycleBin/ReferencedByDocumentRecycleBinController.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Removed unused code. --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,50 @@
|
||||
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.Document.RecycleBin;
|
||||
|
||||
[ApiVersion("1.0")]
|
||||
public class ReferencedByDocumentRecycleBinController : DocumentRecycleBinControllerBase
|
||||
{
|
||||
private readonly ITrackedReferencesService _trackedReferencesService;
|
||||
private readonly IRelationTypePresentationFactory _relationTypePresentationFactory;
|
||||
|
||||
public ReferencedByDocumentRecycleBinController(
|
||||
IEntityService entityService,
|
||||
IDocumentPresentationFactory documentPresentationFactory,
|
||||
ITrackedReferencesService trackedReferencesService,
|
||||
IRelationTypePresentationFactory relationTypePresentationFactory)
|
||||
: base(entityService, documentPresentationFactory)
|
||||
{
|
||||
_trackedReferencesService = trackedReferencesService;
|
||||
_relationTypePresentationFactory = relationTypePresentationFactory;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a paged list of tracked references for all items in the document recycle bin, so you can see where an item is being used.
|
||||
/// </summary>
|
||||
[HttpGet("referenced-by")]
|
||||
[MapToApiVersion("1.0")]
|
||||
[ProducesResponseType(typeof(PagedViewModel<IReferenceResponseModel>), StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<PagedViewModel<IReferenceResponseModel>>> ReferencedBy(
|
||||
CancellationToken cancellationToken,
|
||||
int skip = 0,
|
||||
int take = 20)
|
||||
{
|
||||
PagedModel<RelationItemModel> relationItems = await _trackedReferencesService.GetPagedRelationsForRecycleBinAsync(UmbracoObjectTypes.Document, skip, take, true);
|
||||
|
||||
var pagedViewModel = new PagedViewModel<IReferenceResponseModel>
|
||||
{
|
||||
Total = relationItems.Total,
|
||||
Items = await _relationTypePresentationFactory.CreateReferenceResponseModelsAsync(relationItems.Items),
|
||||
};
|
||||
|
||||
return pagedViewModel;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
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.Media.RecycleBin;
|
||||
|
||||
[ApiVersion("1.0")]
|
||||
public class ReferencedByMediaRecycleBinController : MediaRecycleBinControllerBase
|
||||
{
|
||||
private readonly ITrackedReferencesService _trackedReferencesService;
|
||||
private readonly IRelationTypePresentationFactory _relationTypePresentationFactory;
|
||||
|
||||
public ReferencedByMediaRecycleBinController(
|
||||
IEntityService entityService,
|
||||
IMediaPresentationFactory mediaPresentationFactory,
|
||||
ITrackedReferencesService trackedReferencesService,
|
||||
IRelationTypePresentationFactory relationTypePresentationFactory)
|
||||
: base(entityService, mediaPresentationFactory)
|
||||
{
|
||||
_trackedReferencesService = trackedReferencesService;
|
||||
_relationTypePresentationFactory = relationTypePresentationFactory;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a paged list of tracked references for all items in the media recycle bin, so you can see where an item is being used.
|
||||
/// </summary>
|
||||
[HttpGet("referenced-by")]
|
||||
[MapToApiVersion("1.0")]
|
||||
[ProducesResponseType(typeof(PagedViewModel<IReferenceResponseModel>), StatusCodes.Status200OK)]
|
||||
public async Task<ActionResult<PagedViewModel<IReferenceResponseModel>>> ReferencedBy(
|
||||
CancellationToken cancellationToken,
|
||||
int skip = 0,
|
||||
int take = 20)
|
||||
{
|
||||
PagedModel<RelationItemModel> relationItems = await _trackedReferencesService.GetPagedRelationsForRecycleBinAsync(UmbracoObjectTypes.Media, skip, take, true);
|
||||
|
||||
var pagedViewModel = new PagedViewModel<IReferenceResponseModel>
|
||||
{
|
||||
Total = relationItems.Total,
|
||||
Items = await _relationTypePresentationFactory.CreateReferenceResponseModelsAsync(relationItems.Items),
|
||||
};
|
||||
|
||||
return pagedViewModel;
|
||||
}
|
||||
}
|
||||
@@ -10705,6 +10705,61 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"/umbraco/management/api/v1/recycle-bin/document/referenced-by": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"Document"
|
||||
],
|
||||
"operationId": "GetRecycleBinDocumentReferencedBy",
|
||||
"parameters": [
|
||||
{
|
||||
"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/recycle-bin/document/root": {
|
||||
"get": {
|
||||
"tags": [
|
||||
@@ -17856,6 +17911,61 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"/umbraco/management/api/v1/recycle-bin/media/referenced-by": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"Media"
|
||||
],
|
||||
"operationId": "GetRecycleBinMediaReferencedBy",
|
||||
"parameters": [
|
||||
{
|
||||
"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/recycle-bin/media/root": {
|
||||
"get": {
|
||||
"tags": [
|
||||
|
||||
@@ -70,6 +70,29 @@ public interface ITrackedReferencesRepository
|
||||
bool filterMustBeIsDependency,
|
||||
out long totalRecords);
|
||||
|
||||
/// <summary>
|
||||
/// Gets a paged result of items which are in relation with an item in the recycle bin.
|
||||
/// </summary>
|
||||
/// <param name="objectTypeKey">The Umbraco object type that has recycle bin support (currently Document or Media).</param>
|
||||
/// <param name="skip">The amount of items to skip.</param>
|
||||
/// <param name="take">The amount of items to take.</param>
|
||||
/// <param name="filterMustBeIsDependency">
|
||||
/// A boolean indicating whether to filter only the RelationTypes which are
|
||||
/// dependencies (isDependency field is set to true).
|
||||
/// </param>
|
||||
/// <param name="totalRecords">The total count of the items with reference to the current item.</param>
|
||||
/// <returns>An enumerable list of <see cref="RelationItem" /> objects.</returns>
|
||||
IEnumerable<RelationItemModel> GetPagedRelationsForRecycleBin(
|
||||
Guid objectTypeKey,
|
||||
long skip,
|
||||
long take,
|
||||
bool filterMustBeIsDependency,
|
||||
out long totalRecords)
|
||||
{
|
||||
totalRecords = 0;
|
||||
return [];
|
||||
}
|
||||
|
||||
[Obsolete("Use overload that takes key instead of id. This will be removed in Umbraco 15.")]
|
||||
IEnumerable<RelationItemModel> GetPagedRelationsForItem(
|
||||
int id,
|
||||
|
||||
@@ -64,6 +64,20 @@ public interface ITrackedReferencesService
|
||||
/// <returns>A paged result of <see cref="RelationItemModel" /> objects.</returns>
|
||||
Task<PagedModel<RelationItemModel>> GetPagedRelationsForItemAsync(Guid key, long skip, long take, bool filterMustBeIsDependency);
|
||||
|
||||
/// <summary>
|
||||
/// Gets a paged result of items which are in relation with an item in the recycle bin.
|
||||
/// </summary>
|
||||
/// <param name="objectType">The Umbraco object type that has recycle bin support (currently Document or Media).</param>
|
||||
/// <param name="skip">The amount of items to skip</param>
|
||||
/// <param name="take">The amount of items to take.</param>
|
||||
/// <param name="filterMustBeIsDependency">
|
||||
/// A boolean indicating whether to filter only the RelationTypes which are
|
||||
/// dependencies (isDependency field is set to true).
|
||||
/// </param>
|
||||
/// <returns>A paged result of <see cref="RelationItemModel" /> objects.</returns>
|
||||
Task<PagedModel<RelationItemModel>> GetPagedRelationsForRecycleBinAsync(UmbracoObjectTypes objectType, long skip, long take, bool filterMustBeIsDependency)
|
||||
=> Task.FromResult(new PagedModel<RelationItemModel>(0, []));
|
||||
|
||||
[Obsolete("Use method that takes key (Guid) instead of id (int). This will be removed in Umbraco 15.")]
|
||||
PagedModel<RelationItemModel> GetPagedDescendantsInReferences(int parentId, long skip, long take, bool filterMustBeIsDependency);
|
||||
|
||||
|
||||
@@ -92,6 +92,21 @@ public class TrackedReferencesService : ITrackedReferencesService
|
||||
return await Task.FromResult(pagedModel);
|
||||
}
|
||||
|
||||
public async Task<PagedModel<RelationItemModel>> GetPagedRelationsForRecycleBinAsync(UmbracoObjectTypes objectType, long skip, long take, bool filterMustBeIsDependency)
|
||||
{
|
||||
Guid objectTypeKey = objectType switch
|
||||
{
|
||||
UmbracoObjectTypes.Document => Constants.ObjectTypes.Document,
|
||||
UmbracoObjectTypes.Media => Constants.ObjectTypes.Media,
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(objectType), "Only documents and media have recycle bin support."),
|
||||
};
|
||||
|
||||
using ICoreScope scope = _scopeProvider.CreateCoreScope(autoComplete: true);
|
||||
IEnumerable<RelationItemModel> items = _trackedReferencesRepository.GetPagedRelationsForRecycleBin(objectTypeKey, skip, take, filterMustBeIsDependency, out var totalItems);
|
||||
var pagedModel = new PagedModel<RelationItemModel>(totalItems, items);
|
||||
return await Task.FromResult(pagedModel);
|
||||
}
|
||||
|
||||
[Obsolete("Use overload that takes key instead of id. This will be removed in Umbraco 15.")]
|
||||
public PagedModel<RelationItemModel> GetPagedDescendantsInReferences(int parentId, long skip, long take, bool filterMustBeIsDependency)
|
||||
{
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using System.Linq.Expressions;
|
||||
using NPoco;
|
||||
using Umbraco.Cms.Core.Mapping;
|
||||
using Umbraco.Cms.Core.Models;
|
||||
@@ -92,8 +93,16 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement
|
||||
}
|
||||
|
||||
Sql<ISqlContext> innerUnionSqlChild = _scopeAccessor.AmbientScope.Database.SqlContext.Sql().Select(
|
||||
"[cn].uniqueId as [key]", "[pn].uniqueId as otherKey, [cr].childId as id", "[cr].parentId as otherId", "[rt].[alias]", "[rt].[name]",
|
||||
"[rt].[isDependency]", "[rt].[dual]")
|
||||
"[cn].uniqueId as [key]",
|
||||
"[cn].trashed as [trashed]",
|
||||
"[cn].nodeObjectType as [nodeObjectType]",
|
||||
"[pn].uniqueId as otherKey," +
|
||||
"[cr].childId as id",
|
||||
"[cr].parentId as otherId",
|
||||
"[rt].[alias]",
|
||||
"[rt].[name]",
|
||||
"[rt].[isDependency]",
|
||||
"[rt].[dual]")
|
||||
.From<RelationDto>("cr")
|
||||
.InnerJoin<RelationTypeDto>("rt")
|
||||
.On<RelationDto, RelationTypeDto>((cr, rt) => rt.Dual == false && rt.Id == cr.RelationType, "cr", "rt")
|
||||
@@ -103,8 +112,16 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement
|
||||
.On<RelationDto, NodeDto>((cr, pn) => cr.ParentId == pn.NodeId, "cr", "pn");
|
||||
|
||||
Sql<ISqlContext> innerUnionSqlDualParent = _scopeAccessor.AmbientScope.Database.SqlContext.Sql().Select(
|
||||
"[pn].uniqueId as [key]", "[cn].uniqueId as otherKey, [dpr].parentId as id", "[dpr].childId as otherId", "[dprt].[alias]", "[dprt].[name]",
|
||||
"[dprt].[isDependency]", "[dprt].[dual]")
|
||||
"[pn].uniqueId as [key]",
|
||||
"[pn].trashed as [trashed]",
|
||||
"[pn].nodeObjectType as [nodeObjectType]",
|
||||
"[cn].uniqueId as otherKey," +
|
||||
"[dpr].parentId as id",
|
||||
"[dpr].childId as otherId",
|
||||
"[dprt].[alias]",
|
||||
"[dprt].[name]",
|
||||
"[dprt].[isDependency]",
|
||||
"[dprt].[dual]")
|
||||
.From<RelationDto>("dpr")
|
||||
.InnerJoin<RelationTypeDto>("dprt")
|
||||
.On<RelationDto, RelationTypeDto>(
|
||||
@@ -115,8 +132,16 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement
|
||||
.On<RelationDto, NodeDto>((dpr, pn) => dpr.ParentId == pn.NodeId, "dpr", "pn");
|
||||
|
||||
Sql<ISqlContext> innerUnionSql3 = _scopeAccessor.AmbientScope.Database.SqlContext.Sql().Select(
|
||||
"[cn].uniqueId as [key]", "[pn].uniqueId as otherKey, [dcr].childId as id", "[dcr].parentId as otherId", "[dcrt].[alias]", "[dcrt].[name]",
|
||||
"[dcrt].[isDependency]", "[dcrt].[dual]")
|
||||
"[cn].uniqueId as [key]",
|
||||
"[cn].trashed as [trashed]",
|
||||
"[cn].nodeObjectType as [nodeObjectType]",
|
||||
"[pn].uniqueId as otherKey," +
|
||||
"[dcr].childId as id",
|
||||
"[dcr].parentId as otherId",
|
||||
"[dcrt].[alias]",
|
||||
"[dcrt].[name]",
|
||||
"[dcrt].[isDependency]",
|
||||
"[dcrt].[dual]")
|
||||
.From<RelationDto>("dcr")
|
||||
.InnerJoin<RelationTypeDto>("dcrt")
|
||||
.On<RelationDto, RelationTypeDto>(
|
||||
@@ -277,6 +302,32 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement
|
||||
long take,
|
||||
bool filterMustBeIsDependency,
|
||||
out long totalRecords)
|
||||
=> GetPagedRelations(
|
||||
x => x.Key == key,
|
||||
skip,
|
||||
take,
|
||||
filterMustBeIsDependency,
|
||||
out totalRecords);
|
||||
|
||||
public IEnumerable<RelationItemModel> GetPagedRelationsForRecycleBin(
|
||||
Guid objectTypeKey,
|
||||
long skip,
|
||||
long take,
|
||||
bool filterMustBeIsDependency,
|
||||
out long totalRecords)
|
||||
=> GetPagedRelations(
|
||||
x => x.NodeObjectType == objectTypeKey && x.Trashed == true,
|
||||
skip,
|
||||
take,
|
||||
filterMustBeIsDependency,
|
||||
out totalRecords);
|
||||
|
||||
private IEnumerable<RelationItemModel> GetPagedRelations(
|
||||
Expression<Func<UnionHelperDto, bool>> itemsFilter,
|
||||
long skip,
|
||||
long take,
|
||||
bool filterMustBeIsDependency,
|
||||
out long totalRecords)
|
||||
{
|
||||
Sql<ISqlContext> innerUnionSql = GetInnerUnionSql();
|
||||
Sql<ISqlContext>? sql = _scopeAccessor.AmbientScope?.Database.SqlContext.Sql().SelectDistinct(
|
||||
@@ -315,7 +366,7 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement
|
||||
(left, right) => left.NodeId == right.NodeId,
|
||||
aliasLeft: "n",
|
||||
aliasRight: "d")
|
||||
.Where<UnionHelperDto>(x => x.Key == key, "x");
|
||||
.Where(itemsFilter, "x");
|
||||
|
||||
|
||||
if (filterMustBeIsDependency)
|
||||
@@ -763,6 +814,10 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement
|
||||
|
||||
[Column("key")] public Guid Key { get; set; }
|
||||
|
||||
[Column("trashed")] public bool Trashed { get; set; }
|
||||
|
||||
[Column("nodeObjectType")] public Guid NodeObjectType { get; set; }
|
||||
|
||||
[Column("otherKey")] public Guid OtherKey { get; set; }
|
||||
|
||||
[Column("alias")] public string? Alias { get; set; }
|
||||
|
||||
@@ -134,4 +134,11 @@
|
||||
<Right>lib/net9.0/Umbraco.Tests.Integration.dll</Right>
|
||||
<IsBaselineSuppression>true</IsBaselineSuppression>
|
||||
</Suppression>
|
||||
<Suppression>
|
||||
<DiagnosticId>CP0002</DiagnosticId>
|
||||
<Target>M:Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services.TrackedReferencesServiceTests.Does_not_return_references_if_item_is_not_referenced</Target>
|
||||
<Left>lib/net9.0/Umbraco.Tests.Integration.dll</Left>
|
||||
<Right>lib/net9.0/Umbraco.Tests.Integration.dll</Right>
|
||||
<IsBaselineSuppression>true</IsBaselineSuppression>
|
||||
</Suppression>
|
||||
</Suppressions>
|
||||
@@ -80,7 +80,7 @@ public class TrackedReferencesServiceTests : UmbracoIntegrationTest
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Does_not_return_references_if_item_is_not_referenced()
|
||||
public async Task Does_Not_Return_References_If_Item_Is_Not_Referenced()
|
||||
{
|
||||
var sut = GetRequiredService<ITrackedReferencesService>();
|
||||
|
||||
@@ -88,4 +88,22 @@ public class TrackedReferencesServiceTests : UmbracoIntegrationTest
|
||||
|
||||
Assert.AreEqual(0, actual.Total);
|
||||
}
|
||||
|
||||
[Test]
|
||||
public async Task Get_Pages_That_Reference_Recycle_Bin_Contents()
|
||||
{
|
||||
ContentService.MoveToRecycleBin(Root1);
|
||||
|
||||
var sut = GetRequiredService<ITrackedReferencesService>();
|
||||
|
||||
var actual = await sut.GetPagedRelationsForRecycleBinAsync(UmbracoObjectTypes.Document, 0, 10, true);
|
||||
|
||||
Assert.Multiple(() =>
|
||||
{
|
||||
Assert.AreEqual(1, actual.Total);
|
||||
var item = actual.Items.FirstOrDefault();
|
||||
Assert.AreEqual(Root2.ContentType.Alias, item?.ContentTypeAlias);
|
||||
Assert.AreEqual(Root2.Key, item?.NodeKey);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user