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); + }); + } }