diff --git a/src/Umbraco.Core/Exceptions/DataOperationException.cs b/src/Umbraco.Core/Exceptions/DataOperationException.cs new file mode 100644 index 0000000000..9a66e6a5be --- /dev/null +++ b/src/Umbraco.Core/Exceptions/DataOperationException.cs @@ -0,0 +1,21 @@ +using System; + +namespace Umbraco.Core.Exceptions +{ + internal class DataOperationException : Exception + { + public T Operation { get; private set; } + + public DataOperationException(T operation, string message) + :base(message) + { + Operation = operation; + } + + public DataOperationException(T operation) + : base("Data operation exception: " + operation) + { + Operation = operation; + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Repositories/ContentTypeBaseRepository.cs b/src/Umbraco.Core/Persistence/Repositories/ContentTypeBaseRepository.cs index 49ee1b4f43..2de5d0c641 100644 --- a/src/Umbraco.Core/Persistence/Repositories/ContentTypeBaseRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/ContentTypeBaseRepository.cs @@ -6,6 +6,8 @@ using System.Globalization; using System.Linq; using System.Text; using System.Threading.Tasks; +using Umbraco.Core.Events; +using Umbraco.Core.Exceptions; using Umbraco.Core.Logging; using Umbraco.Core.Models; using Umbraco.Core.Models.EntityBase; @@ -16,6 +18,7 @@ using Umbraco.Core.Persistence.Querying; using Umbraco.Core.Persistence.Relators; using Umbraco.Core.Persistence.SqlSyntax; using Umbraco.Core.Persistence.UnitOfWork; +using Umbraco.Core.Services; namespace Umbraco.Core.Persistence.Repositories { @@ -33,34 +36,76 @@ namespace Umbraco.Core.Persistence.Repositories : base(work, cache, logger, sqlSyntax) { _guidRepo = new GuidReadOnlyContentTypeBaseRepository(this, work, cache, logger, sqlSyntax); - _containerRepository = new EntityContainerRepository(work, cache, logger, sqlSyntax, containerType, NodeObjectTypeId); + ContainerRepository = new EntityContainerRepository(work, cache, logger, sqlSyntax, containerType, NodeObjectTypeId); } protected ContentTypeBaseRepository(IDatabaseUnitOfWork work, CacheHelper cache, ILogger logger, ISqlSyntaxProvider sqlSyntax) : base(work, cache, logger, sqlSyntax) { _guidRepo = new GuidReadOnlyContentTypeBaseRepository(this, work, cache, logger, sqlSyntax); - _containerRepository = null; + ContainerRepository = null; } - private readonly EntityContainerRepository _containerRepository; + protected EntityContainerRepository ContainerRepository { get; private set; } private readonly GuidReadOnlyContentTypeBaseRepository _guidRepo; + public IEnumerable> Move(TEntity toMove, int parentId) + { + if (parentId > 0) + { + var container = ContainerRepository.Get(parentId); + if (container == null) + throw new DataOperationException(MoveOperationStatusType.FailedParentNotFound); + + // Check on paths + if ((string.Format(",{0},", container.Path)).IndexOf(string.Format(",{0},", toMove.Id), StringComparison.Ordinal) > -1) + { + throw new DataOperationException(MoveOperationStatusType.FailedNotAllowedByPath); + } + } + + //used to track all the moved entities to be given to the event + var moveInfo = new List> + { + new MoveEventInfo(toMove, toMove.Path, parentId) + }; + + //do the move to a new parent + toMove.ParentId = parentId; + //schedule it for updating in the transaction + AddOrUpdate(toMove); + + //update all descendants + var descendants = this.GetByQuery( + new Query().Where(type => type.Path.StartsWith(toMove.Path + ","))); + foreach (var descendant in descendants) + { + moveInfo.Add(new MoveEventInfo(descendant, descendant.Path, descendant.ParentId)); + + //all we're doing here is setting the parent Id to be dirty so that it resets the path/level/etc... + descendant.ParentId = descendant.ParentId; + //schedule it for updating in the transaction + AddOrUpdate(descendant); + } + + return moveInfo; + } + /// /// Deletes a folder - this will move all contained entities into their parent /// /// public void DeleteContainer(int containerId) { - if (_containerRepository == null) throw new NotSupportedException("The repository type " + GetType() + " does not support containers"); + if (ContainerRepository == null) throw new NotSupportedException("The repository type " + GetType() + " does not support containers"); - var found = _containerRepository.Get(containerId); - _containerRepository.Delete(found); + var found = ContainerRepository.Get(containerId); + ContainerRepository.Delete(found); } public EntityContainer CreateContainer(int parentId, string name, int userId) { - if (_containerRepository == null) throw new NotSupportedException("The repository type " + GetType() + " does not support containers"); + if (ContainerRepository == null) throw new NotSupportedException("The repository type " + GetType() + " does not support containers"); var container = new EntityContainer { @@ -68,7 +113,7 @@ namespace Umbraco.Core.Persistence.Repositories Name = name, CreatorId = userId }; - _containerRepository.AddOrUpdate(container); + ContainerRepository.AddOrUpdate(container); return container; } diff --git a/src/Umbraco.Core/Persistence/Repositories/ContentTypeRepository.cs b/src/Umbraco.Core/Persistence/Repositories/ContentTypeRepository.cs index 4145cb89b4..04b3e46e3a 100644 --- a/src/Umbraco.Core/Persistence/Repositories/ContentTypeRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/ContentTypeRepository.cs @@ -1,6 +1,8 @@ using System; using System.Collections.Generic; using System.Linq; +using Umbraco.Core.Events; +using Umbraco.Core.Exceptions; using Umbraco.Core.Logging; using Umbraco.Core.Models; using Umbraco.Core.Models.EntityBase; @@ -11,6 +13,7 @@ using Umbraco.Core.Persistence.Querying; using Umbraco.Core.Persistence.Relators; using Umbraco.Core.Persistence.SqlSyntax; using Umbraco.Core.Persistence.UnitOfWork; +using Umbraco.Core.Services; namespace Umbraco.Core.Persistence.Repositories { @@ -25,10 +28,8 @@ namespace Umbraco.Core.Persistence.Repositories : base(work, cache, logger, sqlSyntax, new Guid(Constants.ObjectTypes.DocumentTypeContainer)) { _templateRepository = templateRepository; - } + } - #region Overrides of RepositoryBase - protected override IContentType PerformGet(int id) { var contentTypes = ContentTypeQueryMapper.GetContentTypes( @@ -46,7 +47,7 @@ namespace Umbraco.Core.Persistence.Repositories } else { - var sql = new Sql().Select("id").From().Where(dto => dto.NodeObjectType == NodeObjectTypeId); + var sql = new Sql().Select("id").From(SqlSyntax).Where(dto => dto.NodeObjectType == NodeObjectTypeId); var allIds = Database.Fetch(sql).ToArray(); return ContentTypeQueryMapper.GetContentTypes(allIds, Database, SqlSyntax, this, _templateRepository); } @@ -57,16 +58,14 @@ namespace Umbraco.Core.Persistence.Repositories var sqlClause = GetBaseQuery(false); var translator = new SqlTranslator(sqlClause, query); var sql = translator.Translate() - .OrderBy(x => x.Text); + .OrderBy(x => x.Text, SqlSyntax); var dtos = Database.Fetch(sql); return dtos.Any() ? GetAll(dtos.DistinctBy(x => x.ContentTypeDto.NodeId).Select(x => x.ContentTypeDto.NodeId).ToArray()) : Enumerable.Empty(); } - - #endregion - + /// /// Gets all entities of the specified query /// @@ -88,9 +87,7 @@ namespace Umbraco.Core.Persistence.Repositories { return Database.Fetch("SELECT DISTINCT Alias FROM cmsPropertyType ORDER BY Alias"); } - - #region Overrides of PetaPocoRepositoryBase - + protected override Sql GetBaseQuery(bool isCount) { var sql = new Sql(); @@ -135,11 +132,7 @@ namespace Umbraco.Core.Persistence.Repositories { get { return new Guid(Constants.ObjectTypes.DocumentType); } } - - #endregion - - #region Unit of Work Implementation - + /// /// Deletes a content type /// @@ -239,9 +232,6 @@ namespace Umbraco.Core.Persistence.Repositories entity.ResetDirtyProperties(); } - - #endregion - protected override IContentType PerformGet(Guid id) { diff --git a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IContentTypeRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IContentTypeRepository.cs index 5435e54c23..9a90af21d6 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IContentTypeRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IContentTypeRepository.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using Umbraco.Core.Events; using Umbraco.Core.Models; using Umbraco.Core.Persistence.Querying; @@ -34,5 +35,7 @@ namespace Umbraco.Core.Persistence.Repositories /// /// void DeleteContainer(int containerId); + + IEnumerable> Move(IContentType toMove, int parentId); } } \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IMediaTypeRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IMediaTypeRepository.cs index c39494d606..17073d18b3 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IMediaTypeRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IMediaTypeRepository.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using Umbraco.Core.Events; using Umbraco.Core.Models; using Umbraco.Core.Persistence.Querying; @@ -28,5 +29,7 @@ namespace Umbraco.Core.Persistence.Repositories /// /// void DeleteContainer(int folderId); + + IEnumerable> Move(IMediaType toMove, int parentId); } } \ No newline at end of file diff --git a/src/Umbraco.Core/Persistence/Repositories/MediaTypeRepository.cs b/src/Umbraco.Core/Persistence/Repositories/MediaTypeRepository.cs index 70423bb509..c4fa80a7d5 100644 --- a/src/Umbraco.Core/Persistence/Repositories/MediaTypeRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/MediaTypeRepository.cs @@ -1,6 +1,8 @@ using System; using System.Collections.Generic; using System.Linq; +using Umbraco.Core.Events; +using Umbraco.Core.Exceptions; using Umbraco.Core.Logging; using Umbraco.Core.Models; using Umbraco.Core.Models.EntityBase; @@ -10,6 +12,7 @@ using Umbraco.Core.Persistence.Factories; using Umbraco.Core.Persistence.Querying; using Umbraco.Core.Persistence.SqlSyntax; using Umbraco.Core.Persistence.UnitOfWork; +using Umbraco.Core.Services; namespace Umbraco.Core.Persistence.Repositories { @@ -23,9 +26,7 @@ namespace Umbraco.Core.Persistence.Repositories : base(work, cache, logger, sqlSyntax, new Guid(Constants.ObjectTypes.MediaTypeContainer)) { } - - #region Overrides of RepositoryBase - + protected override IMediaType PerformGet(int id) { var contentTypes = ContentTypeQueryMapper.GetMediaTypes( @@ -61,10 +62,7 @@ namespace Umbraco.Core.Persistence.Repositories ? GetAll(dtos.DistinctBy(x => x.NodeId).Select(x => x.NodeId).ToArray()) : Enumerable.Empty(); } - - #endregion - - + /// /// Gets all entities of the specified query /// @@ -76,10 +74,8 @@ namespace Umbraco.Core.Persistence.Repositories return ints.Any() ? GetAll(ints) : Enumerable.Empty(); - } - - #region Overrides of PetaPocoRepositoryBase - + } + protected override Sql GetBaseQuery(bool isCount) { var sql = new Sql(); @@ -119,11 +115,7 @@ namespace Umbraco.Core.Persistence.Repositories { get { return new Guid(Constants.ObjectTypes.MediaType); } } - - #endregion - - #region Unit of Work Implementation - + protected override void PersistNewItem(IMediaType entity) { ((MediaType)entity).AddingEntity(); @@ -163,9 +155,6 @@ namespace Umbraco.Core.Persistence.Repositories entity.ResetDirtyProperties(); } - - #endregion - protected override IMediaType PerformGet(Guid id) { diff --git a/src/Umbraco.Core/Services/ContentTypeService.cs b/src/Umbraco.Core/Services/ContentTypeService.cs index 89b4fc8324..eccdae1ab8 100644 --- a/src/Umbraco.Core/Services/ContentTypeService.cs +++ b/src/Umbraco.Core/Services/ContentTypeService.cs @@ -712,6 +712,76 @@ namespace Umbraco.Core.Services } } + public Attempt> MoveMediaType(IMediaType toMove, int parentId) + { + var evtMsgs = EventMessagesFactory.Get(); + + if (MovingMediaType.IsRaisedEventCancelled( + new MoveEventArgs(evtMsgs, new MoveEventInfo(toMove, toMove.Path, parentId)), + this)) + { + return Attempt.Fail( + new OperationStatus( + MoveOperationStatusType.FailedCancelledByEvent, evtMsgs)); + } + + var moveInfo = new List>(); + var uow = UowProvider.GetUnitOfWork(); + using (var repository = RepositoryFactory.CreateMediaTypeRepository(uow)) + { + try + { + moveInfo.AddRange(repository.Move(toMove, parentId)); + } + catch (DataOperationException ex) + { + return Attempt.Fail( + new OperationStatus(ex.Operation, evtMsgs)); + } + uow.Commit(); + } + + MovedMediaType.RaiseEvent(new MoveEventArgs(false, evtMsgs, moveInfo.ToArray()), this); + + return Attempt.Succeed( + new OperationStatus(MoveOperationStatusType.Success, evtMsgs)); + } + + public Attempt> MoveContentType(IContentType toMove, int parentId) + { + var evtMsgs = EventMessagesFactory.Get(); + + if (MovingContentType.IsRaisedEventCancelled( + new MoveEventArgs(evtMsgs, new MoveEventInfo(toMove, toMove.Path, parentId)), + this)) + { + return Attempt.Fail( + new OperationStatus( + MoveOperationStatusType.FailedCancelledByEvent, evtMsgs)); + } + + var moveInfo = new List>(); + var uow = UowProvider.GetUnitOfWork(); + using (var repository = RepositoryFactory.CreateContentTypeRepository(uow)) + { + try + { + moveInfo.AddRange(repository.Move(toMove, parentId)); + } + catch (DataOperationException ex) + { + return Attempt.Fail( + new OperationStatus(ex.Operation, evtMsgs)); + } + uow.Commit(); + } + + MovedContentType.RaiseEvent(new MoveEventArgs(false, evtMsgs, moveInfo.ToArray()), this); + + return Attempt.Succeed( + new OperationStatus(MoveOperationStatusType.Success, evtMsgs)); + } + /// /// Saves a single object /// @@ -950,6 +1020,26 @@ namespace Umbraco.Core.Services /// public static event TypedEventHandler> SavedMediaType; + /// + /// Occurs before Move + /// + public static event TypedEventHandler> MovingMediaType; + + /// + /// Occurs after Move + /// + public static event TypedEventHandler> MovedMediaType; + + /// + /// Occurs before Move + /// + public static event TypedEventHandler> MovingContentType; + + /// + /// Occurs after Move + /// + public static event TypedEventHandler> MovedContentType; + #endregion } } \ No newline at end of file diff --git a/src/Umbraco.Core/Services/IContentTypeService.cs b/src/Umbraco.Core/Services/IContentTypeService.cs index 4807765562..6be2174fdf 100644 --- a/src/Umbraco.Core/Services/IContentTypeService.cs +++ b/src/Umbraco.Core/Services/IContentTypeService.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.Xml.Linq; using Umbraco.Core.Models; +using Umbraco.Core.Models.EntityBase; namespace Umbraco.Core.Services { @@ -261,5 +262,8 @@ namespace Umbraco.Core.Services /// Id of the /// True if the media type has any children otherwise False bool MediaTypeHasChildren(Guid id); + + Attempt> MoveMediaType(IMediaType toMove, int parentId); + Attempt> MoveContentType(IContentType toMove, int parentId); } } \ No newline at end of file diff --git a/src/Umbraco.Core/Services/MoveOperationStatusType.cs b/src/Umbraco.Core/Services/MoveOperationStatusType.cs new file mode 100644 index 0000000000..d1a3439cab --- /dev/null +++ b/src/Umbraco.Core/Services/MoveOperationStatusType.cs @@ -0,0 +1,32 @@ +namespace Umbraco.Core.Services +{ + + /// + /// A status type of the result of moving an item + /// + /// + /// Anything less than 10 = Success! + /// + public enum MoveOperationStatusType + { + /// + /// The move was successful. + /// + Success = 0, + + /// + /// The parent being moved to doesn't exist + /// + FailedParentNotFound = 13, + + /// + /// The move action has been cancelled by an event handler + /// + FailedCancelledByEvent = 14, + + /// + /// Trying to move an item to an invalid path (i.e. a child of itself) + /// + FailedNotAllowedByPath = 15, + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index a1682e1449..2340d4d0ee 100644 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -336,6 +336,7 @@ + @@ -498,6 +499,7 @@ + diff --git a/src/Umbraco.Tests/Persistence/Repositories/ContentTypeRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/ContentTypeRepositoryTest.cs index e20e2867da..d5870810f8 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/ContentTypeRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/ContentTypeRepositoryTest.cs @@ -95,6 +95,49 @@ namespace Umbraco.Tests.Persistence.Repositories } + [Test] + public void Can_Move() + { + var provider = new PetaPocoUnitOfWorkProvider(Logger); + var unitOfWork = provider.GetUnitOfWork(); + using (var repository = CreateRepository(unitOfWork)) + { + var container = repository.CreateContainer(-1, "blah", 0); + unitOfWork.Commit(); + + var container2 = repository.CreateContainer(container.Id, "blah2", 0); + unitOfWork.Commit(); + + var contentType = (IContentType) MockedContentTypes.CreateBasicContentType("asdfasdf"); + contentType.ParentId = container2.Id; + repository.AddOrUpdate(contentType); + unitOfWork.Commit(); + + //create a + var contentType2 = (IContentType)new ContentType(contentType, "hello") + { + Name = "Blahasdfsadf" + }; + contentType.ParentId = contentType.Id; + repository.AddOrUpdate(contentType2); + unitOfWork.Commit(); + + var result = repository.Move(contentType, container.Id).ToArray(); + unitOfWork.Commit(); + + Assert.AreEqual(2, result.Count()); + + //re-get + contentType = repository.Get(contentType.Id); + contentType2 = repository.Get(contentType2.Id); + + Assert.AreEqual(container.Id, contentType.ParentId); + Assert.AreNotEqual(result.Single(x => x.Entity.Id == contentType.Id).OriginalPath, contentType.Path); + Assert.AreNotEqual(result.Single(x => x.Entity.Id == contentType2.Id).OriginalPath, contentType2.Path); + } + + } + [Test] public void Can_Create_Container() { diff --git a/src/Umbraco.Tests/Persistence/Repositories/MediaTypeRepositoryTest.cs b/src/Umbraco.Tests/Persistence/Repositories/MediaTypeRepositoryTest.cs index 97b3db96f8..82a6578ebb 100644 --- a/src/Umbraco.Tests/Persistence/Repositories/MediaTypeRepositoryTest.cs +++ b/src/Umbraco.Tests/Persistence/Repositories/MediaTypeRepositoryTest.cs @@ -31,6 +31,49 @@ namespace Umbraco.Tests.Persistence.Repositories return new MediaTypeRepository(unitOfWork, CacheHelper.CreateDisabledCacheHelper(), Mock.Of(), SqlSyntax); } + [Test] + public void Can_Move() + { + var provider = new PetaPocoUnitOfWorkProvider(Logger); + var unitOfWork = provider.GetUnitOfWork(); + using (var repository = CreateRepository(unitOfWork)) + { + var container = repository.CreateContainer(-1, "blah", 0); + unitOfWork.Commit(); + + var container2 = repository.CreateContainer(container.Id, "blah2", 0); + unitOfWork.Commit(); + + var contentType = (IMediaType)MockedContentTypes.CreateVideoMediaType(); + contentType.ParentId = container2.Id; + repository.AddOrUpdate(contentType); + unitOfWork.Commit(); + + //create a + var contentType2 = (IMediaType)new MediaType(contentType, "hello") + { + Name = "Blahasdfsadf" + }; + contentType.ParentId = contentType.Id; + repository.AddOrUpdate(contentType2); + unitOfWork.Commit(); + + var result = repository.Move(contentType, container.Id).ToArray(); + unitOfWork.Commit(); + + Assert.AreEqual(2, result.Count()); + + //re-get + contentType = repository.Get(contentType.Id); + contentType2 = repository.Get(contentType2.Id); + + Assert.AreEqual(container.Id, contentType.ParentId); + Assert.AreNotEqual(result.Single(x => x.Entity.Id == contentType.Id).OriginalPath, contentType.Path); + Assert.AreNotEqual(result.Single(x => x.Entity.Id == contentType2.Id).OriginalPath, contentType2.Path); + } + + } + [Test] public void Can_Create_Container() { diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/contenttype.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/contenttype.resource.js index 0ba60042bf..da4a736cd2 100644 --- a/src/Umbraco.Web.UI.Client/src/common/resources/contenttype.resource.js +++ b/src/Umbraco.Web.UI.Client/src/common/resources/contenttype.resource.js @@ -166,6 +166,49 @@ function contentTypeResource($q, $http, umbRequestHelper, umbDataFormatter) { 'Failed to save data for content type id ' + contentType.id); }, + /** + * @ngdoc method + * @name umbraco.resources.contentTypeResource#move + * @methodOf umbraco.resources.contentTypeResource + * + * @description + * Moves a node underneath a new parentId + * + * ##usage + *
+         * contentTypeResource.move({ parentId: 1244, id: 123 })
+         *    .then(function() {
+         *        alert("node was moved");
+         *    }, function(err){
+         *      alert("node didnt move:" + err.data.Message); 
+         *    });
+         * 
+ * @param {Object} args arguments object + * @param {Int} args.idd the ID of the node to move + * @param {Int} args.parentId the ID of the parent node to move to + * @returns {Promise} resourcePromise object. + * + */ + move: function (args) { + if (!args) { + throw "args cannot be null"; + } + if (!args.parentId) { + throw "args.parentId cannot be null"; + } + if (!args.id) { + throw "args.id cannot be null"; + } + + return umbRequestHelper.resourcePromise( + $http.post(umbRequestHelper.getApiUrl("contentTypeApiBaseUrl", "PostMove"), + { + parentId: args.parentId, + id: args.id + }), + 'Failed to move content'); + }, + createContainer: function(parentId, name) { return umbRequestHelper.resourcePromise( diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/mediatype.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/mediatype.resource.js index d483777250..dcdcb7de88 100644 --- a/src/Umbraco.Web.UI.Client/src/common/resources/mediatype.resource.js +++ b/src/Umbraco.Web.UI.Client/src/common/resources/mediatype.resource.js @@ -99,6 +99,49 @@ function mediaTypeResource($q, $http, umbRequestHelper, umbDataFormatter) { 'Failed to save data for content type id ' + contentType.id); }, + /** + * @ngdoc method + * @name umbraco.resources.mediaTypeResource#move + * @methodOf umbraco.resources.mediaTypeResource + * + * @description + * Moves a node underneath a new parentId + * + * ##usage + *
+         * mediaTypeResource.move({ parentId: 1244, id: 123 })
+         *    .then(function() {
+         *        alert("node was moved");
+         *    }, function(err){
+         *      alert("node didnt move:" + err.data.Message); 
+         *    });
+         * 
+ * @param {Object} args arguments object + * @param {Int} args.idd the ID of the node to move + * @param {Int} args.parentId the ID of the parent node to move to + * @returns {Promise} resourcePromise object. + * + */ + move: function (args) { + if (!args) { + throw "args cannot be null"; + } + if (!args.parentId) { + throw "args.parentId cannot be null"; + } + if (!args.id) { + throw "args.id cannot be null"; + } + + return umbRequestHelper.resourcePromise( + $http.post(umbRequestHelper.getApiUrl("mediaTypeApiBaseUrl", "PostMove"), + { + parentId: args.parentId, + id: args.id + }), + 'Failed to move content'); + }, + createContainer: function(parentId, name) { return umbRequestHelper.resourcePromise( diff --git a/src/Umbraco.Web.UI.Client/src/views/content/move.html b/src/Umbraco.Web.UI.Client/src/views/content/move.html index 96e76d4dc4..b64511ec22 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/move.html +++ b/src/Umbraco.Web.UI.Client/src/views/content/move.html @@ -3,7 +3,7 @@

- Choose where to move {{currentNode.name}} to in the tree structure below + Choose where to move {{currentNode.name}} to in the tree structure below

@@ -16,7 +16,7 @@
-
{{currentNode.name}} was moved underneath{{target.name}}
+
{{currentNode.name}} was moved underneath {{target.name}}
diff --git a/src/Umbraco.Web.UI.Client/src/views/documenttypes/move.controller.js b/src/Umbraco.Web.UI.Client/src/views/documenttypes/move.controller.js index 99af24e051..3cf3634371 100644 --- a/src/Umbraco.Web.UI.Client/src/views/documenttypes/move.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/documenttypes/move.controller.js @@ -1,5 +1,66 @@ angular.module("umbraco") .controller("Umbraco.Editors.DocumentTypes.MoveController", - function($scope){ + function ($scope, contentTypeResource, treeService, navigationService, notificationsService, appState) { + var dialogOptions = $scope.dialogOptions; + $scope.dialogTreeEventHandler = $({}); + function nodeSelectHandler(ev, args) { + args.event.preventDefault(); + args.event.stopPropagation(); + + if ($scope.target) { + //un-select if there's a current one selected + $scope.target.selected = false; + } + + $scope.target = args.node; + $scope.target.selected = true; + } + + $scope.move = function () { + + $scope.busy = true; + $scope.error = false; + + contentTypeResource.move({ parentId: $scope.target.id, id: dialogOptions.currentNode.id }) + .then(function (path) { + $scope.error = false; + $scope.success = true; + $scope.busy = false; + + //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 content - but don't activate the node, + //then sync to the currenlty edited content (note: this might not be the content that was moved!!) + + navigationService.syncTree({ tree: "documentTypes", 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: "documentTypes", path: activeNodePath, forceReload: false, activate: true }); + } + }); + + }, function (err) { + $scope.success = false; + $scope.error = err; + $scope.busy = false; + //show any notifications + if (angular.isArray(err.data.notifications)) { + for (var i = 0; i < err.data.notifications.length; i++) { + notificationsService.showNotification(err.data.notifications[i]); + } + } + }); + }; + + $scope.dialogTreeEventHandler.bind("treeNodeSelect", nodeSelectHandler); + + $scope.$on('$destroy', function () { + $scope.dialogTreeEventHandler.unbind("treeNodeSelect", nodeSelectHandler); + }); }); diff --git a/src/Umbraco.Web.UI.Client/src/views/documenttypes/move.html b/src/Umbraco.Web.UI.Client/src/views/documenttypes/move.html index 7ddde2f2fd..506db13d06 100644 --- a/src/Umbraco.Web.UI.Client/src/views/documenttypes/move.html +++ b/src/Umbraco.Web.UI.Client/src/views/documenttypes/move.html @@ -1,11 +1,50 @@
+
-

- Select the folder to move {{currentNode.name}} to. -

+

+ Select the folder to move {{currentNode.name}} to in the tree structure below +

+
+
+
+
+
{{error.errorMsg}}
+

{{error.data.message}}

+
+ +
+
{{currentNode.name}} was moved underneath {{target.name}}
+ +
+ +
+ +
+ + +
+ +
+
+
+ +
diff --git a/src/Umbraco.Web.UI.Client/src/views/mediatypes/move.controller.js b/src/Umbraco.Web.UI.Client/src/views/mediatypes/move.controller.js index f18a5c1b33..a8ce1e461a 100644 --- a/src/Umbraco.Web.UI.Client/src/views/mediatypes/move.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/mediatypes/move.controller.js @@ -1,5 +1,67 @@ angular.module("umbraco") .controller("Umbraco.Editors.MediaTypes.MoveController", - function($scope){ + function ($scope, mediaTypeResource, treeService, navigationService, notificationsService, appState) { + var dialogOptions = $scope.dialogOptions; + $scope.dialogTreeEventHandler = $({}); + + function nodeSelectHandler(ev, args) { + args.event.preventDefault(); + args.event.stopPropagation(); + + if ($scope.target) { + //un-select if there's a current one selected + $scope.target.selected = false; + } + + $scope.target = args.node; + $scope.target.selected = true; + } + + $scope.move = function () { + + $scope.busy = true; + $scope.error = false; + + mediaTypeResource.move({ parentId: $scope.target.id, id: dialogOptions.currentNode.id }) + .then(function (path) { + $scope.error = false; + $scope.success = true; + $scope.busy = false; + + //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 content - but don't activate the node, + //then sync to the currenlty edited content (note: this might not be the content that was moved!!) + + navigationService.syncTree({ tree: "mediaTypes", 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: "mediaTypes", path: activeNodePath, forceReload: false, activate: true }); + } + }); + + }, function (err) { + $scope.success = false; + $scope.error = err; + $scope.busy = false; + //show any notifications + if (angular.isArray(err.data.notifications)) { + for (var i = 0; i < err.data.notifications.length; i++) { + notificationsService.showNotification(err.data.notifications[i]); + } + } + }); + }; + + $scope.dialogTreeEventHandler.bind("treeNodeSelect", nodeSelectHandler); + + $scope.$on('$destroy', function () { + $scope.dialogTreeEventHandler.unbind("treeNodeSelect", nodeSelectHandler); + }); }); diff --git a/src/Umbraco.Web.UI.Client/src/views/mediatypes/move.html b/src/Umbraco.Web.UI.Client/src/views/mediatypes/move.html index d6a4dc49b4..22b6d14d62 100644 --- a/src/Umbraco.Web.UI.Client/src/views/mediatypes/move.html +++ b/src/Umbraco.Web.UI.Client/src/views/mediatypes/move.html @@ -1,11 +1,51 @@
- +
+
-

- Select the folder to move {{currentNode.name}} to. -

+

+ Select the folder to move {{currentNode.name}} to in the tree structure below +

+
+
+
+
+
{{error.errorMsg}}
+

{{error.data.message}}

+
+ +
+
{{currentNode.name}} was moved underneath {{target.name}}
+ +
+ +
+ +
+ + +
+ +
+
+ + +
diff --git a/src/Umbraco.Web/Editors/ContentTypeController.cs b/src/Umbraco.Web/Editors/ContentTypeController.cs index 8b84dbc450..8179edce2d 100644 --- a/src/Umbraco.Web/Editors/ContentTypeController.cs +++ b/src/Umbraco.Web/Editors/ContentTypeController.cs @@ -243,6 +243,19 @@ namespace Umbraco.Web.Editors return basics; } + /// + /// Move the media type + /// + /// + /// + public HttpResponseMessage PostMove(MoveOrCopy move) + { + return PerformMove( + move, + getContentType: i => Services.ContentTypeService.GetContentType(i), + doMove: (type, i) => Services.ContentTypeService.MoveContentType(type, i)); + } + } } \ No newline at end of file diff --git a/src/Umbraco.Web/Editors/ContentTypeControllerBase.cs b/src/Umbraco.Web/Editors/ContentTypeControllerBase.cs index 6f3f96d365..fd229e6c47 100644 --- a/src/Umbraco.Web/Editors/ContentTypeControllerBase.cs +++ b/src/Umbraco.Web/Editors/ContentTypeControllerBase.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Linq; using System.Net; using System.Net.Http; +using System.Text; using System.Text.RegularExpressions; using System.Web.Http; using AutoMapper; @@ -215,6 +216,50 @@ namespace Umbraco.Web.Editors } return newCt; } + } + + /// + /// Change the sort order for media + /// + /// + /// + /// + /// + protected HttpResponseMessage PerformMove( + MoveOrCopy move, + Func getContentType, + Func>> doMove) + where TContentType : IContentTypeComposition + { + var toMove = getContentType(move.Id); + if (toMove == null) + { + return Request.CreateResponse(HttpStatusCode.NotFound); + } + + var result = doMove(toMove, move.ParentId); + if (result.Success) + { + var response = Request.CreateResponse(HttpStatusCode.OK); + response.Content = new StringContent(toMove.Path, Encoding.UTF8, "application/json"); + return response; + } + + switch (result.Result.StatusType) + { + case MoveOperationStatusType.FailedParentNotFound: + return Request.CreateResponse(HttpStatusCode.NotFound); + case MoveOperationStatusType.FailedCancelledByEvent: + //returning an object of INotificationModel will ensure that any pending + // notification messages are added to the response. + return Request.CreateValidationErrorResponse(new SimpleNotificationModel()); + case MoveOperationStatusType.FailedNotAllowedByPath: + var notificationModel = new SimpleNotificationModel(); + notificationModel.AddErrorNotification(Services.TextService.Localize("moveOrCopy/notAllowedByPath"), ""); + return Request.CreateValidationErrorResponse(notificationModel); + default: + throw new ArgumentOutOfRangeException(); + } } private ICultureDictionary CultureDictionary diff --git a/src/Umbraco.Web/Editors/MediaTypeController.cs b/src/Umbraco.Web/Editors/MediaTypeController.cs index 05e861cce2..b212555ebe 100644 --- a/src/Umbraco.Web/Editors/MediaTypeController.cs +++ b/src/Umbraco.Web/Editors/MediaTypeController.cs @@ -14,6 +14,7 @@ using System.Net; using Umbraco.Core.PropertyEditors; using System; using System.Net.Http; +using System.Text; using Umbraco.Web.WebApi; using ContentType = System.Net.Mime.ContentType; using Umbraco.Core.Services; @@ -186,5 +187,19 @@ namespace Umbraco.Web.Editors return basics; } + + /// + /// Move the media type + /// + /// + /// + public HttpResponseMessage PostMove(MoveOrCopy move) + { + return PerformMove( + move, + getContentType: i => Services.ContentTypeService.GetMediaType(i), + doMove: (type, i) => Services.ContentTypeService.MoveMediaType(type, i)); + } + } } \ No newline at end of file diff --git a/src/Umbraco.Web/Trees/ContentTypeTreeController.cs b/src/Umbraco.Web/Trees/ContentTypeTreeController.cs index 3bb6c1dfd2..43cf60a178 100644 --- a/src/Umbraco.Web/Trees/ContentTypeTreeController.cs +++ b/src/Umbraco.Web/Trees/ContentTypeTreeController.cs @@ -42,6 +42,9 @@ namespace Umbraco.Web.Trees return node; })); + //if the request is for folders only then just return + if (queryStrings["foldersonly"].IsNullOrWhiteSpace() == false && queryStrings["foldersonly"] == "1") return nodes; + nodes.AddRange( Services.EntityService.GetChildren(intId.Result, UmbracoObjectTypes.DocumentType) .OrderBy(entity => entity.Name) @@ -80,25 +83,19 @@ namespace Umbraco.Web.Trees //set the default to create menu.DefaultMenuAlias = ActionNew.Instance.Alias; - // root actions menu.Items.Add(Services.TextService.Localize(string.Format("actions/{0}", ActionNew.Instance.Alias))); - + if (container.HasChildren() == false) { //can delete doc type menu.Items.Add(Services.TextService.Localize(string.Format("actions/{0}", ActionDelete.Instance.Alias))); } - - menu.Items.Add(Services.TextService.Localize(string.Format("actions/{0}", ActionMove.Instance.Alias)), hasSeparator: true); - menu.Items.Add(Services.TextService.Localize(string.Format("actions/{0}", ActionRefresh.Instance.Alias))); - - + menu.Items.Add(Services.TextService.Localize(string.Format("actions/{0}", ActionRefresh.Instance.Alias)), hasSeparator: true); } else { - //delete doc type - menu.Items.Add(Services.TextService.Localize(string.Format("actions/{0}", ActionMove.Instance.Alias))); menu.Items.Add(Services.TextService.Localize(string.Format("actions/{0}", ActionDelete.Instance.Alias))); + menu.Items.Add(Services.TextService.Localize(string.Format("actions/{0}", ActionMove.Instance.Alias)), hasSeparator: true); } return menu; diff --git a/src/Umbraco.Web/Trees/DataTypeTreeController.cs b/src/Umbraco.Web/Trees/DataTypeTreeController.cs index ee12562fc5..00640f8410 100644 --- a/src/Umbraco.Web/Trees/DataTypeTreeController.cs +++ b/src/Umbraco.Web/Trees/DataTypeTreeController.cs @@ -97,15 +97,14 @@ namespace Umbraco.Web.Trees //set the default to create menu.DefaultMenuAlias = ActionNew.Instance.Alias; - // root actions menu.Items.Add(Services.TextService.Localize(string.Format("actions/{0}", ActionNew.Instance.Alias))); - menu.Items.Add(Services.TextService.Localize(string.Format("actions/{0}", ActionRefresh.Instance.Alias))); if (container.HasChildren() == false) { //can delete data type menu.Items.Add(Services.TextService.Localize(string.Format("actions/{0}", ActionDelete.Instance.Alias))); - } + } + menu.Items.Add(Services.TextService.Localize(string.Format("actions/{0}", ActionRefresh.Instance.Alias)), hasSeparator: true); } else { @@ -113,8 +112,8 @@ namespace Umbraco.Web.Trees if (sysIds.Contains(int.Parse(id)) == false) { - //only have delete for each node - menu.Items.Add(ui.Text("actions", ActionDelete.Instance.Alias)); + menu.Items.Add(Services.TextService.Localize(string.Format("actions/{0}", ActionDelete.Instance.Alias))); + menu.Items.Add(Services.TextService.Localize(string.Format("actions/{0}", ActionMove.Instance.Alias)), hasSeparator: true); } } diff --git a/src/Umbraco.Web/Trees/MediaTypeTreeController.cs b/src/Umbraco.Web/Trees/MediaTypeTreeController.cs index 77414a1b36..a7da33898d 100644 --- a/src/Umbraco.Web/Trees/MediaTypeTreeController.cs +++ b/src/Umbraco.Web/Trees/MediaTypeTreeController.cs @@ -42,6 +42,9 @@ namespace Umbraco.Web.Trees return node; })); + //if the request is for folders only then just return + if (queryStrings["foldersonly"].IsNullOrWhiteSpace() == false && queryStrings["foldersonly"] == "1") return nodes; + nodes.AddRange( Services.EntityService.GetChildren(intId.Result, UmbracoObjectTypes.MediaType) .OrderBy(entity => entity.Name) @@ -55,8 +58,6 @@ namespace Umbraco.Web.Trees return nodes; } - - protected override MenuItemCollection GetMenuForNode(string id, FormDataCollection queryStrings) { var menu = new MenuItemCollection(); @@ -78,20 +79,19 @@ namespace Umbraco.Web.Trees //set the default to create menu.DefaultMenuAlias = ActionNew.Instance.Alias; - // root actions menu.Items.Add(Services.TextService.Localize(string.Format("actions/{0}", ActionNew.Instance.Alias))); - menu.Items.Add(Services.TextService.Localize(string.Format("actions/{0}", ActionRefresh.Instance.Alias))); - + if (container.HasChildren() == false) { //can delete doc type menu.Items.Add(Services.TextService.Localize(string.Format("actions/{0}", ActionDelete.Instance.Alias))); - } + } + menu.Items.Add(Services.TextService.Localize(string.Format("actions/{0}", ActionRefresh.Instance.Alias)), hasSeparator: true); } else - { - //delete doc type + { menu.Items.Add(Services.TextService.Localize(string.Format("actions/{0}", ActionDelete.Instance.Alias))); + menu.Items.Add(Services.TextService.Localize(string.Format("actions/{0}", ActionMove.Instance.Alias)), hasSeparator: true); } return menu;