From 60910efc5397c0259580ff06cdcb09df1b92a1f8 Mon Sep 17 00:00:00 2001 From: Richard Thompson Date: Fri, 27 Jul 2018 14:56:18 +0100 Subject: [PATCH] U4-10681 Restore option in media recycle bin (#2342) --- src/Umbraco.Core/Constants-Conventions.cs | 10 +++ .../Models/Rdbms/RelationTypeDto.cs | 2 +- .../Migrations/Initial/BaseDataCreation.cs | 3 + .../AddRelationTypeForMediaFolderOnDelete.cs | 38 +++++++++ .../Strategies/RelateOnTrashHandler.cs | 68 ++++++++++++++- src/Umbraco.Core/Umbraco.Core.csproj | 11 +-- .../RelationTypeRepositoryTest.cs | 6 +- .../content/content.restore.controller.js | 14 +-- .../src/views/content/restore.html | 16 ++-- .../views/media/media.restore.controller.js | 85 +++++++++++++++++++ .../src/views/media/restore.html | 26 ++++++ src/Umbraco.Web.UI/umbraco/config/lang/en.xml | 8 ++ .../umbraco/config/lang/en_us.xml | 8 ++ src/Umbraco.Web/Trees/MediaTreeController.cs | 8 +- src/umbraco.cms/Actions/ActionRestore.cs | 2 +- 15 files changed, 278 insertions(+), 27 deletions(-) create mode 100644 src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenTwelveZero/AddRelationTypeForMediaFolderOnDelete.cs create mode 100644 src/Umbraco.Web.UI.Client/src/views/media/media.restore.controller.js create mode 100644 src/Umbraco.Web.UI.Client/src/views/media/restore.html diff --git a/src/Umbraco.Core/Constants-Conventions.cs b/src/Umbraco.Core/Constants-Conventions.cs index b59b7e487e..a19e59f4ef 100644 --- a/src/Umbraco.Core/Constants-Conventions.cs +++ b/src/Umbraco.Core/Constants-Conventions.cs @@ -341,6 +341,16 @@ namespace Umbraco.Core /// ContentType alias for default relation type "Relate Parent Document On Delete". /// public const string RelateParentDocumentOnDeleteAlias = "relateParentDocumentOnDelete"; + + /// + /// ContentType name for default relation type "Relate Parent Media Folder On Delete". + /// + public const string RelateParentMediaFolderOnDeleteName = "Relate Parent Media Folder On Delete"; + + /// + /// ContentType alias for default relation type "Relate Parent Media Folder On Delete". + /// + public const string RelateParentMediaFolderOnDeleteAlias = "relateParentMediaFolderOnDelete"; } } } diff --git a/src/Umbraco.Core/Models/Rdbms/RelationTypeDto.cs b/src/Umbraco.Core/Models/Rdbms/RelationTypeDto.cs index d13ce33520..0c04a1c0be 100644 --- a/src/Umbraco.Core/Models/Rdbms/RelationTypeDto.cs +++ b/src/Umbraco.Core/Models/Rdbms/RelationTypeDto.cs @@ -9,7 +9,7 @@ namespace Umbraco.Core.Models.Rdbms [ExplicitColumns] internal class RelationTypeDto { - public const int NodeIdSeed = 3; + public const int NodeIdSeed = 4; [Column("id")] [PrimaryKeyColumn(IdentitySeed = NodeIdSeed)] diff --git a/src/Umbraco.Core/Persistence/Migrations/Initial/BaseDataCreation.cs b/src/Umbraco.Core/Persistence/Migrations/Initial/BaseDataCreation.cs index da63de3d33..d19e0e0b60 100644 --- a/src/Umbraco.Core/Persistence/Migrations/Initial/BaseDataCreation.cs +++ b/src/Umbraco.Core/Persistence/Migrations/Initial/BaseDataCreation.cs @@ -310,6 +310,9 @@ namespace Umbraco.Core.Persistence.Migrations.Initial relationType = new RelationTypeDto { Id = 2, Alias = Constants.Conventions.RelationTypes.RelateParentDocumentOnDeleteAlias, ChildObjectType = new Guid(Constants.ObjectTypes.Document), ParentObjectType = new Guid(Constants.ObjectTypes.Document), Dual = false, Name = Constants.Conventions.RelationTypes.RelateParentDocumentOnDeleteName }; relationType.UniqueId = (relationType.Alias + "____" + relationType.Name).ToGuid(); _database.Insert("umbracoRelationType", "id", false, relationType); + relationType = new RelationTypeDto { Id = 3, Alias = Constants.Conventions.RelationTypes.RelateParentMediaFolderOnDeleteAlias, ChildObjectType = new Guid(Constants.ObjectTypes.Media), ParentObjectType = new Guid(Constants.ObjectTypes.Media), Dual = false, Name = Constants.Conventions.RelationTypes.RelateParentMediaFolderOnDeleteName }; + relationType.UniqueId = (relationType.Alias + "____" + relationType.Name).ToGuid(); + _database.Insert("umbracoRelationType", "id", false, relationType); } private void CreateCmsTaskTypeData() diff --git a/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenTwelveZero/AddRelationTypeForMediaFolderOnDelete.cs b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenTwelveZero/AddRelationTypeForMediaFolderOnDelete.cs new file mode 100644 index 0000000000..9de051e725 --- /dev/null +++ b/src/Umbraco.Core/Persistence/Migrations/Upgrades/TargetVersionSevenTwelveZero/AddRelationTypeForMediaFolderOnDelete.cs @@ -0,0 +1,38 @@ +using System; +using Umbraco.Core.Logging; +using Umbraco.Core.Models.Rdbms; +using Umbraco.Core.Persistence.DatabaseAnnotations; +using Umbraco.Core.Persistence.SqlSyntax; + +namespace Umbraco.Core.Persistence.Migrations.Upgrades.TargetVersionSevenTwelveZero +{ + [Migration("7.12.0", 0, Constants.System.UmbracoMigrationName)] + public class AddRelationTypeForMediaFolderOnDelete : MigrationBase + { + public AddRelationTypeForMediaFolderOnDelete(ISqlSyntaxProvider sqlSyntax, ILogger logger) + : base(sqlSyntax, logger) + { + } + + public override void Up() + { + var exists = Context.Database.FirstOrDefault("WHERE alias=@alias", new { alias = Constants.Conventions.RelationTypes.RelateParentMediaFolderOnDeleteAlias }); + if (exists == null) + { + var relationTypeDto = new RelationTypeDto + { + Alias = Constants.Conventions.RelationTypes.RelateParentMediaFolderOnDeleteAlias, + Name = Constants.Conventions.RelationTypes.RelateParentMediaFolderOnDeleteName, + ChildObjectType = Guid.Parse(Constants.ObjectTypes.MediaType), + ParentObjectType = Guid.Parse(Constants.ObjectTypes.MediaType), + Dual = false + }; + + Context.Database.Insert(relationTypeDto); + } + } + + public override void Down() + { } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Strategies/RelateOnTrashHandler.cs b/src/Umbraco.Core/Strategies/RelateOnTrashHandler.cs index 38477aca47..45fe1d76c2 100644 --- a/src/Umbraco.Core/Strategies/RelateOnTrashHandler.cs +++ b/src/Umbraco.Core/Strategies/RelateOnTrashHandler.cs @@ -1,6 +1,5 @@ using System; using System.Linq; -using Umbraco.Core.Auditing; using Umbraco.Core.Events; using Umbraco.Core.Models; using Umbraco.Core.Services; @@ -13,6 +12,9 @@ namespace Umbraco.Core.Strategies { ContentService.Moved += ContentService_Moved; ContentService.Trashed += ContentService_Trashed; + + MediaService.Moved += MediaService_Moved; + MediaService.Trashed += MediaService_Trashed; } private void ContentService_Moved(IContentService sender, MoveEventArgs e) @@ -30,10 +32,26 @@ namespace Umbraco.Core.Strategies } } + private void MediaService_Moved(IMediaService sender, MoveEventArgs e) + { + foreach (var item in e.MoveInfoCollection.Where(x => x.OriginalPath.Contains(Constants.System.RecycleBinMedia.ToInvariantString()))) + { + var relationService = ApplicationContext.Current.Services.RelationService; + var relationTypeAlias = Constants.Conventions.RelationTypes.RelateParentMediaFolderOnDeleteAlias; + var relations = relationService.GetByChildId(item.Entity.Id); + + foreach (var relation in relations.Where(x => x.RelationType.Alias.InvariantEquals(relationTypeAlias))) + { + relationService.Delete(relation); + } + } + } + private void ContentService_Trashed(IContentService sender, MoveEventArgs e) { var relationService = ApplicationContext.Current.Services.RelationService; var entityService = ApplicationContext.Current.Services.EntityService; + var textService = ApplicationContext.Current.Services.TextService; var relationTypeAlias = Constants.Conventions.RelationTypes.RelateParentDocumentOnDeleteAlias; var relationType = relationService.GetRelationTypeByAlias(relationTypeAlias); @@ -64,7 +82,9 @@ namespace Umbraco.Core.Strategies relationService.Save(relation); ApplicationContext.Current.Services.AuditService.Add(AuditType.Delete, - string.Format("Trashed content with Id: '{0}' related to original parent content with Id: '{1}'", item.Entity.Id, originalParentId), + string.Format(textService.Localize( + "recycleBin/contentTrashed"), + item.Entity.Id, originalParentId), item.Entity.WriterId, item.Entity.Id); } @@ -72,5 +92,49 @@ namespace Umbraco.Core.Strategies } } + + private void MediaService_Trashed(IMediaService sender, MoveEventArgs e) + { + var relationService = ApplicationContext.Current.Services.RelationService; + var entityService = ApplicationContext.Current.Services.EntityService; + var textService = ApplicationContext.Current.Services.TextService; + var relationTypeAlias = Constants.Conventions.RelationTypes.RelateParentMediaFolderOnDeleteAlias; + var relationType = relationService.GetRelationTypeByAlias(relationTypeAlias); + + // check that the relation-type exists, if not, then recreate it + if (relationType == null) + { + var documentObjectType = new Guid(Constants.ObjectTypes.Document); + var relationTypeName = Constants.Conventions.RelationTypes.RelateParentMediaFolderOnDeleteName; + + relationType = new RelationType(documentObjectType, documentObjectType, relationTypeAlias, relationTypeName); + relationService.Save(relationType); + } + + foreach (var item in e.MoveInfoCollection) + { + var originalPath = item.OriginalPath.ToDelimitedList(); + var originalParentId = originalPath.Count > 2 + ? int.Parse(originalPath[originalPath.Count - 2]) + : Constants.System.Root; + + //before we can create this relation, we need to ensure that the original parent still exists which + //may not be the case if the encompassing transaction also deleted it when this item was moved to the bin + + if (entityService.Exists(originalParentId)) + { + // Add a relation for the item being deleted, so that we can know the original parent for if we need to restore later + var relation = new Relation(originalParentId, item.Entity.Id, relationType); + relationService.Save(relation); + + ApplicationContext.Current.Services.AuditService.Add(AuditType.Delete, + string.Format(textService.Localize( + "recycleBin/mediaTrashed"), + item.Entity.Id, originalParentId), + item.Entity.CreatorId, + item.Entity.Id); + } + } + } } } \ No newline at end of file diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index 7b8c0fb5b7..7625af7629 100644 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -1,4 +1,4 @@ - + Debug @@ -568,12 +568,13 @@ - - + + + @@ -638,7 +639,7 @@ - + @@ -1688,4 +1689,4 @@ --> - \ No newline at end of file + diff --git a/src/Umbraco.Tests/Persistence/Repositories/RelationTypeRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/RelationTypeRepositoryTest.cs index 9355b90546..bc6c58264e 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/RelationTypeRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/RelationTypeRepositoryTest.cs @@ -139,7 +139,7 @@ namespace Umbraco.Tests.Persistence.Repositories Assert.That(relationTypes, Is.Not.Null); Assert.That(relationTypes.Any(), Is.True); Assert.That(relationTypes.Any(x => x == null), Is.False); - Assert.That(relationTypes.Count(), Is.EqualTo(4)); + Assert.That(relationTypes.Count(), Is.EqualTo(5)); } } @@ -174,7 +174,7 @@ namespace Umbraco.Tests.Persistence.Repositories // Act var exists = repository.Exists(3); - var doesntExist = repository.Exists(5); + var doesntExist = repository.Exists(6); // Assert Assert.That(exists, Is.True); @@ -196,7 +196,7 @@ namespace Umbraco.Tests.Persistence.Repositories int count = repository.Count(query); // Assert - Assert.That(count, Is.EqualTo(4)); + Assert.That(count, Is.EqualTo(5)); } } diff --git a/src/Umbraco.Web.UI.Client/src/views/content/content.restore.controller.js b/src/Umbraco.Web.UI.Client/src/views/content/content.restore.controller.js index c9124bf956..62eaa6fca6 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/content.restore.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/content/content.restore.controller.js @@ -1,5 +1,5 @@ angular.module("umbraco").controller("Umbraco.Editors.Content.RestoreController", - function ($scope, relationResource, contentResource, navigationService, appState, treeService) { + function ($scope, relationResource, contentResource, navigationService, appState, treeService, localizationService) { var dialogOptions = $scope.dialogOptions; var node = dialogOptions.currentNode; @@ -12,9 +12,9 @@ angular.module("umbraco").controller("Umbraco.Editors.Content.RestoreController" if (data.length == 0) { $scope.success = false; $scope.error = { - errorMsg: "Cannot automatically restore this item", + errorMsg: localizationService.localize('recycleBin_itemCannotBeRestored'), data: { - Message: "There is no 'restore' relation found for this node. Use the Move menu item to move it manually." + Message: localizationService.localize('recycleBin_noRestoreRelation') } } return; @@ -32,9 +32,11 @@ angular.module("umbraco").controller("Umbraco.Editors.Content.RestoreController" // make sure the target item isn't in the recycle bin if($scope.target.path.indexOf("-20") !== -1) { $scope.error = { - errorMsg: "Cannot automatically restore this item", + errorMsg: localizationService.localize('recycleBin_itemCannotBeRestored'), data: { - Message: "The item you want to restore it under (" + $scope.target.name + ") is in the recycle bin. Use the Move menu item to move the item manually." + Message: localizationService.localize('recycleBin_restoreUnderRecycled').then(function (value) { + value.replace('%0%', $scope.target.name); + }) } }; $scope.success = false; @@ -80,4 +82,4 @@ angular.module("umbraco").controller("Umbraco.Editors.Content.RestoreController" $scope.error = err; }); }; - }); \ No newline at end of file + }); diff --git a/src/Umbraco.Web.UI.Client/src/views/content/restore.html b/src/Umbraco.Web.UI.Client/src/views/content/restore.html index 8564a80bf7..e99e2eb251 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/restore.html +++ b/src/Umbraco.Web.UI.Client/src/views/content/restore.html @@ -2,9 +2,9 @@
-

- Restore {{currentNode.name}} under {{target.name}}? -

+

+ Restore {{currentNode.name}} under {{target.name}}? +

{{error.errorMsg}}
@@ -12,15 +12,15 @@
-

{{currentNode.name}} was moved underneath {{target.name}}

- +

{{currentNode.name}} was moved underneath {{target.name}}

+
- \ No newline at end of file + diff --git a/src/Umbraco.Web.UI.Client/src/views/media/media.restore.controller.js b/src/Umbraco.Web.UI.Client/src/views/media/media.restore.controller.js new file mode 100644 index 0000000000..6ef9232c37 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/media/media.restore.controller.js @@ -0,0 +1,85 @@ +angular.module("umbraco").controller("Umbraco.Editors.Media.RestoreController", + function ($scope, relationResource, mediaResource, navigationService, appState, treeService, localizationService) { + var dialogOptions = $scope.dialogOptions; + + var node = dialogOptions.currentNode; + + $scope.error = null; + $scope.success = false; + + relationResource.getByChildId(node.id, "relateParentDocumentOnDelete").then(function (data) { + + if (data.length == 0) { + $scope.success = false; + $scope.error = { + errorMsg: localizationService.localize('recycleBin_itemCannotBeRestored'), + data: { + Message: localizationService.localize('recycleBin_noRestoreRelation') + } + } + return; + } + + $scope.relation = data[0]; + + if ($scope.relation.parentId == -1) { + $scope.target = { id: -1, name: "Root" }; + + } else { + mediaResource.getById($scope.relation.parentId).then(function (data) { + $scope.target = data; + + // make sure the target item isn't in the recycle bin + if ($scope.target.path.indexOf("-20") !== -1) { + $scope.error = { + errorMsg: localizationService.localize('recycleBin_itemCannotBeRestored'), + data: { + Message: localizationService.localize('recycleBin_restoreUnderRecycled').then(function (value) { + value.replace('%0%', $scope.target.name); + }) + } + }; + $scope.success = false; + } + + }, function (err) { + $scope.success = false; + $scope.error = err; + }); + } + + }, function (err) { + $scope.success = false; + $scope.error = err; + }); + + $scope.restore = function () { + // this code was copied from `content.move.controller.js` + mediaResource.move({ parentId: $scope.target.id, id: node.id }) + .then(function (path) { + + $scope.success = true; + + //first we need to remove the node that launched the dialog + treeService.removeNode($scope.currentNode); + + //get the currently edited node (if any) + var activeNode = appState.getTreeState("selectedNode"); + + //we need to do a double sync here: first sync to the moved media item - but don't activate the node, + //then sync to the currenlty edited media item (note: this might not be the media item that was moved!!) + + navigationService.syncTree({ tree: "media", path: path, forceReload: true, activate: false }).then(function (args) { + if (activeNode) { + var activeNodePath = treeService.getPath(activeNode).join(); + //sync to this node now - depending on what was copied this might already be synced but might not be + navigationService.syncTree({ tree: "media", path: activeNodePath, forceReload: false, activate: true }); + } + }); + + }, function (err) { + $scope.success = false; + $scope.error = err; + }); + }; + }); diff --git a/src/Umbraco.Web.UI.Client/src/views/media/restore.html b/src/Umbraco.Web.UI.Client/src/views/media/restore.html new file mode 100644 index 0000000000..17fff154d1 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/media/restore.html @@ -0,0 +1,26 @@ +
+
+ + +

+ Restore {{currentNode.name}} under {{target.name}}? +

+ +
+
{{error.errorMsg}}
+
{{error.data.Message}}
+
+ +
+

{{currentNode.name}} was moved underneath {{target.name}}

+ +
+ +
+
+ + +
diff --git a/src/Umbraco.Web.UI/umbraco/config/lang/en.xml b/src/Umbraco.Web.UI/umbraco/config/lang/en.xml index eb514a62f7..e9bbbca1aa 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/en.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/en.xml @@ -656,6 +656,7 @@ Submit Type Type to search... + under Up Update Upgrade @@ -2201,4 +2202,11 @@ To manage your website, simply open the Umbraco back office and start adding con characters left + + Trashed content with Id: {0} related to original parent content with Id: {1} + Trashed media with Id: {0} related to original parent media item with Id: {1} + Cannot automatically restore this item + There is no 'restore' relation found for this node. Use the Move menu item to move it manually. + The item you want to restore it under ('%0%') is in the recycle bin. Use the Move menu item to move the item manually. + diff --git a/src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml b/src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml index 77dd604bd0..e8e537895a 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml @@ -656,6 +656,7 @@ Submit Type Type to search... + under Up Update Upgrade @@ -2193,4 +2194,11 @@ To manage your website, simply open the Umbraco back office and start adding con characters left + + Trashed content with Id: {0} related to original parent content with Id: {1} + Trashed media with Id: {0} related to original parent media item with Id: {1} + Cannot automatically restore this item + There is no 'restore' relation found for this node. Use the Move menu item to move it manually. + The item you want to restore it under ('%0%') is in the recycle bin. Use the Move menu item to move the item manually. + diff --git a/src/Umbraco.Web/Trees/MediaTreeController.cs b/src/Umbraco.Web/Trees/MediaTreeController.cs index 99117b3b04..932ced9616 100644 --- a/src/Umbraco.Web/Trees/MediaTreeController.cs +++ b/src/Umbraco.Web/Trees/MediaTreeController.cs @@ -140,7 +140,13 @@ namespace Umbraco.Web.Trees //if the media item is in the recycle bin, don't have a default menu, just show the regular menu if (item.Path.Split(new[] { ',' }, StringSplitOptions.RemoveEmptyEntries).Contains(RecycleBinId.ToInvariantString())) { - menu.DefaultMenuAlias = null; + menu.DefaultMenuAlias = null; + menu.Items.Insert(2, new MenuItem(ActionRestore.Instance, ui.Text("actions", ActionRestore.Instance.Alias))); + } + else + { + //set the default to create + menu.DefaultMenuAlias = ActionNew.Instance.Alias; } return menu; diff --git a/src/umbraco.cms/Actions/ActionRestore.cs b/src/umbraco.cms/Actions/ActionRestore.cs index 9b38918fd4..89fb858d6a 100644 --- a/src/umbraco.cms/Actions/ActionRestore.cs +++ b/src/umbraco.cms/Actions/ActionRestore.cs @@ -5,7 +5,7 @@ using umbraco.BasePages; namespace umbraco.BusinessLogic.Actions { /// - /// This action is invoked when the content item is to be restored from the recycle bin + /// This action is invoked when the content/media item is to be restored from the recycle bin /// public class ActionRestore : IAction {