From 5209a54f2ae297a7182d9108e73b54947d59b8ff Mon Sep 17 00:00:00 2001 From: Shannon Date: Thu, 12 Nov 2015 18:13:41 +0100 Subject: [PATCH] Added Move methods for both content types and media types ... these move methods are done as part of the UOW commit/transaction process, something that should be done with the Move/Copy operations of the content/media services. I added unit tests for the move operations for both content, media and data types. Events are all in places for content type and media type moves (something which Courier will need to use). Updated the trees/menu items to support moving which includes rendering the tree with only folders, updated the move dialog views to work. --- .../Exceptions/DataOperationException.cs | 21 +++++ .../Repositories/ContentTypeBaseRepository.cs | 61 +++++++++++-- .../Repositories/ContentTypeRepository.cs | 28 ++---- .../Interfaces/IContentTypeRepository.cs | 3 + .../Interfaces/IMediaTypeRepository.cs | 3 + .../Repositories/MediaTypeRepository.cs | 27 ++---- .../Services/ContentTypeService.cs | 90 +++++++++++++++++++ .../Services/IContentTypeService.cs | 4 + .../Services/MoveOperationStatusType.cs | 32 +++++++ src/Umbraco.Core/Umbraco.Core.csproj | 2 + .../Repositories/ContentTypeRepositoryTest.cs | 43 +++++++++ .../Repositories/MediaTypeRepositoryTest.cs | 43 +++++++++ .../common/resources/contenttype.resource.js | 43 +++++++++ .../common/resources/mediatype.resource.js | 43 +++++++++ .../src/views/content/move.html | 4 +- .../views/documenttypes/move.controller.js | 63 ++++++++++++- .../src/views/documenttypes/move.html | 45 +++++++++- .../src/views/mediatypes/move.controller.js | 64 ++++++++++++- .../src/views/mediatypes/move.html | 48 +++++++++- .../Editors/ContentTypeController.cs | 13 +++ .../Editors/ContentTypeControllerBase.cs | 45 ++++++++++ .../Editors/MediaTypeController.cs | 15 ++++ .../Trees/ContentTypeTreeController.cs | 15 ++-- .../Trees/DataTypeTreeController.cs | 9 +- .../Trees/MediaTypeTreeController.cs | 16 ++-- 25 files changed, 701 insertions(+), 79 deletions(-) create mode 100644 src/Umbraco.Core/Exceptions/DataOperationException.cs create mode 100644 src/Umbraco.Core/Services/MoveOperationStatusType.cs 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;