diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Document/RecycleBin/ReferencedByDocumentRecycleBinController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Document/RecycleBin/ReferencedByDocumentRecycleBinController.cs
new file mode 100644
index 0000000000..2b94fb443f
--- /dev/null
+++ b/src/Umbraco.Cms.Api.Management/Controllers/Document/RecycleBin/ReferencedByDocumentRecycleBinController.cs
@@ -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;
+ }
+
+ ///
+ /// 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.
+ ///
+ [HttpGet("referenced-by")]
+ [MapToApiVersion("1.0")]
+ [ProducesResponseType(typeof(PagedViewModel), StatusCodes.Status200OK)]
+ public async Task>> ReferencedBy(
+ CancellationToken cancellationToken,
+ int skip = 0,
+ int take = 20)
+ {
+ PagedModel relationItems = await _trackedReferencesService.GetPagedRelationsForRecycleBinAsync(UmbracoObjectTypes.Document, skip, take, true);
+
+ var pagedViewModel = new PagedViewModel
+ {
+ Total = relationItems.Total,
+ Items = await _relationTypePresentationFactory.CreateReferenceResponseModelsAsync(relationItems.Items),
+ };
+
+ return pagedViewModel;
+ }
+}
diff --git a/src/Umbraco.Cms.Api.Management/Controllers/Media/RecycleBin/ReferencedByMediaRecycleBinController.cs b/src/Umbraco.Cms.Api.Management/Controllers/Media/RecycleBin/ReferencedByMediaRecycleBinController.cs
new file mode 100644
index 0000000000..a3c72184d7
--- /dev/null
+++ b/src/Umbraco.Cms.Api.Management/Controllers/Media/RecycleBin/ReferencedByMediaRecycleBinController.cs
@@ -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;
+ }
+
+ ///
+ /// 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.
+ ///
+ [HttpGet("referenced-by")]
+ [MapToApiVersion("1.0")]
+ [ProducesResponseType(typeof(PagedViewModel), StatusCodes.Status200OK)]
+ public async Task>> ReferencedBy(
+ CancellationToken cancellationToken,
+ int skip = 0,
+ int take = 20)
+ {
+ PagedModel relationItems = await _trackedReferencesService.GetPagedRelationsForRecycleBinAsync(UmbracoObjectTypes.Media, skip, take, true);
+
+ var pagedViewModel = new PagedViewModel
+ {
+ Total = relationItems.Total,
+ Items = await _relationTypePresentationFactory.CreateReferenceResponseModelsAsync(relationItems.Items),
+ };
+
+ return pagedViewModel;
+ }
+}
diff --git a/src/Umbraco.Cms.Api.Management/OpenApi.json b/src/Umbraco.Cms.Api.Management/OpenApi.json
index 239aebc54a..e87920c9ba 100644
--- a/src/Umbraco.Cms.Api.Management/OpenApi.json
+++ b/src/Umbraco.Cms.Api.Management/OpenApi.json
@@ -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": [
diff --git a/src/Umbraco.Core/Persistence/Repositories/ITrackedReferencesRepository.cs b/src/Umbraco.Core/Persistence/Repositories/ITrackedReferencesRepository.cs
index 01bff9f356..270ed24250 100644
--- a/src/Umbraco.Core/Persistence/Repositories/ITrackedReferencesRepository.cs
+++ b/src/Umbraco.Core/Persistence/Repositories/ITrackedReferencesRepository.cs
@@ -70,6 +70,29 @@ public interface ITrackedReferencesRepository
bool filterMustBeIsDependency,
out long totalRecords);
+ ///
+ /// Gets a paged result of items which are in relation with an item in the recycle bin.
+ ///
+ /// The Umbraco object type that has recycle bin support (currently Document or Media).
+ /// The amount of items to skip.
+ /// The amount of items to take.
+ ///
+ /// A boolean indicating whether to filter only the RelationTypes which are
+ /// dependencies (isDependency field is set to true).
+ ///
+ /// The total count of the items with reference to the current item.
+ /// An enumerable list of objects.
+ IEnumerable 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 GetPagedRelationsForItem(
int id,
diff --git a/src/Umbraco.Core/Services/ITrackedReferencesService.cs b/src/Umbraco.Core/Services/ITrackedReferencesService.cs
index c60156d31b..74578cc23b 100644
--- a/src/Umbraco.Core/Services/ITrackedReferencesService.cs
+++ b/src/Umbraco.Core/Services/ITrackedReferencesService.cs
@@ -64,6 +64,20 @@ public interface ITrackedReferencesService
/// A paged result of objects.
Task> GetPagedRelationsForItemAsync(Guid key, long skip, long take, bool filterMustBeIsDependency);
+ ///
+ /// Gets a paged result of items which are in relation with an item in the recycle bin.
+ ///
+ /// The Umbraco object type that has recycle bin support (currently Document or Media).
+ /// The amount of items to skip
+ /// The amount of items to take.
+ ///
+ /// A boolean indicating whether to filter only the RelationTypes which are
+ /// dependencies (isDependency field is set to true).
+ ///
+ /// A paged result of objects.
+ Task> GetPagedRelationsForRecycleBinAsync(UmbracoObjectTypes objectType, long skip, long take, bool filterMustBeIsDependency)
+ => Task.FromResult(new PagedModel(0, []));
+
[Obsolete("Use method that takes key (Guid) instead of id (int). This will be removed in Umbraco 15.")]
PagedModel GetPagedDescendantsInReferences(int parentId, long skip, long take, bool filterMustBeIsDependency);
diff --git a/src/Umbraco.Core/Services/TrackedReferencesService.cs b/src/Umbraco.Core/Services/TrackedReferencesService.cs
index 5d5d76f7f0..0d6ed003b8 100644
--- a/src/Umbraco.Core/Services/TrackedReferencesService.cs
+++ b/src/Umbraco.Core/Services/TrackedReferencesService.cs
@@ -92,6 +92,21 @@ public class TrackedReferencesService : ITrackedReferencesService
return await Task.FromResult(pagedModel);
}
+ public async Task> 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 items = _trackedReferencesRepository.GetPagedRelationsForRecycleBin(objectTypeKey, skip, take, filterMustBeIsDependency, out var totalItems);
+ var pagedModel = new PagedModel(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 GetPagedDescendantsInReferences(int parentId, long skip, long take, bool filterMustBeIsDependency)
{
diff --git a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/TrackedReferencesRepository.cs b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/TrackedReferencesRepository.cs
index 2b7a43589c..ced64f4996 100644
--- a/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/TrackedReferencesRepository.cs
+++ b/src/Umbraco.Infrastructure/Persistence/Repositories/Implement/TrackedReferencesRepository.cs
@@ -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 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("cr")
.InnerJoin("rt")
.On((cr, rt) => rt.Dual == false && rt.Id == cr.RelationType, "cr", "rt")
@@ -103,8 +112,16 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement
.On((cr, pn) => cr.ParentId == pn.NodeId, "cr", "pn");
Sql 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("dpr")
.InnerJoin("dprt")
.On(
@@ -115,8 +132,16 @@ namespace Umbraco.Cms.Infrastructure.Persistence.Repositories.Implement
.On((dpr, pn) => dpr.ParentId == pn.NodeId, "dpr", "pn");
Sql 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("dcr")
.InnerJoin("dcrt")
.On(
@@ -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 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 GetPagedRelations(
+ Expression> itemsFilter,
+ long skip,
+ long take,
+ bool filterMustBeIsDependency,
+ out long totalRecords)
{
Sql innerUnionSql = GetInnerUnionSql();
Sql? 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(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; }
diff --git a/tests/Umbraco.Tests.Integration/CompatibilitySuppressions.xml b/tests/Umbraco.Tests.Integration/CompatibilitySuppressions.xml
index 3b12394aaa..c3bf60b3cb 100644
--- a/tests/Umbraco.Tests.Integration/CompatibilitySuppressions.xml
+++ b/tests/Umbraco.Tests.Integration/CompatibilitySuppressions.xml
@@ -134,4 +134,11 @@
lib/net9.0/Umbraco.Tests.Integration.dll
true
+
+ CP0002
+ M:Umbraco.Cms.Tests.Integration.Umbraco.Infrastructure.Services.TrackedReferencesServiceTests.Does_not_return_references_if_item_is_not_referenced
+ lib/net9.0/Umbraco.Tests.Integration.dll
+ lib/net9.0/Umbraco.Tests.Integration.dll
+ true
+
\ No newline at end of file
diff --git a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/TrackedReferencesServiceTests.cs b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/TrackedReferencesServiceTests.cs
index 27517e23dd..018da86cd0 100644
--- a/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/TrackedReferencesServiceTests.cs
+++ b/tests/Umbraco.Tests.Integration/Umbraco.Infrastructure/Services/TrackedReferencesServiceTests.cs
@@ -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();
@@ -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();
+
+ 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);
+ });
+ }
}