diff --git a/src/Umbraco.Core/Persistence/Repositories/ContentTypeBaseRepository.cs b/src/Umbraco.Core/Persistence/Repositories/ContentTypeBaseRepository.cs index 907f66435e..d3e7182783 100644 --- a/src/Umbraco.Core/Persistence/Repositories/ContentTypeBaseRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/ContentTypeBaseRepository.cs @@ -1212,5 +1212,19 @@ AND umbracoNode.id <> @id", { return PerformExists(id); } + + public string GetUniqueAlias(string alias) + { + var aliasColumn = SqlSyntax.GetQuotedColumnName("alias"); + var aliases = Database.Fetch(@"SELECT cmsContentType." + aliasColumn + @" FROM cmsContentType +INNER JOIN umbracoNode ON cmsContentType.nodeId = umbracoNode.id +WHERE cmsContentType." + aliasColumn + @" LIKE @pattern +AND umbracoNode.nodeObjectType = @objectType", + new { pattern = alias + "%", objectType = NodeObjectTypeId }); + var i = 1; + string test; + while (aliases.Contains(test = alias + i)) i++; + return test; + } } } diff --git a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IContentTypeRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IContentTypeRepository.cs index 49520387cd..acb1d50a61 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Interfaces/IContentTypeRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Interfaces/IContentTypeRepository.cs @@ -32,5 +32,12 @@ namespace Umbraco.Core.Persistence.Repositories /// /// IEnumerable GetAllContentTypeAliases(params Guid[] objectTypes); + + /// + /// Derives a unique alias from an existing alias. + /// + /// The original alias. + /// The original alias with a number appended to it, so that it is unique. + string GetUniqueAlias(string alias); } } \ No newline at end of file diff --git a/src/Umbraco.Core/Services/ContentTypeService.cs b/src/Umbraco.Core/Services/ContentTypeService.cs index 496e855b60..141f4a5a97 100644 --- a/src/Umbraco.Core/Services/ContentTypeService.cs +++ b/src/Umbraco.Core/Services/ContentTypeService.cs @@ -794,7 +794,7 @@ namespace Umbraco.Core.Services //TODO: This needs to change, if we are deleting a content type, we should just delete the data, // this method will recursively go lookup every content item, check if any of it's descendants are - // of a different type, move them to the recycle bin, then permanently delete the content items. + // of a different type, move them to the recycle bin, then permanently delete the content items. // The main problem with this is that for every content item being deleted, events are raised... // which we need for many things like keeping caches in sync, but we can surely do this MUCH better. @@ -1067,6 +1067,48 @@ namespace Umbraco.Core.Services new OperationStatus(MoveOperationStatusType.Success, evtMsgs)); } + public Attempt> CopyContentType(IContentType toCopy, int containerId) + { + var evtMsgs = EventMessagesFactory.Get(); + + var uow = UowProvider.GetUnitOfWork(); + using (var containerRepository = RepositoryFactory.CreateEntityContainerRepository(uow, Constants.ObjectTypes.DocumentTypeContainerGuid)) + using (var repository = RepositoryFactory.CreateContentTypeRepository(uow)) + { + try + { + if (containerId > 0) + { + var container = containerRepository.Get(containerId); + if (container == null) + throw new DataOperationException(MoveOperationStatusType.FailedParentNotFound); + } + var alias = repository.GetUniqueAlias(toCopy.Alias); + var copy = toCopy.DeepCloneWithResetIdentities(alias); + copy.Name = copy.Name + " (copy)"; // might not be unique + + // if it has a parent, and the parent is a content type, unplug composition + // all other compositions remain in place in the copied content type + if (copy.ParentId > 0) + { + var parent = repository.Get(copy.ParentId); + if (parent != null) + copy.RemoveContentType(parent.Alias); + } + + copy.ParentId = containerId; + repository.AddOrUpdate(copy); + } + catch (DataOperationException ex) + { + return Attempt.Fail(new OperationStatus(ex.Operation, evtMsgs)); + } + uow.Commit(); + } + + return Attempt.Succeed(new OperationStatus(MoveOperationStatusType.Success, evtMsgs)); + } + /// /// Saves a single object /// diff --git a/src/Umbraco.Core/Services/IContentTypeService.cs b/src/Umbraco.Core/Services/IContentTypeService.cs index cd905a5ccc..e55430330c 100644 --- a/src/Umbraco.Core/Services/IContentTypeService.cs +++ b/src/Umbraco.Core/Services/IContentTypeService.cs @@ -291,5 +291,6 @@ namespace Umbraco.Core.Services Attempt> MoveMediaType(IMediaType toMove, int containerId); Attempt> MoveContentType(IContentType toMove, int containerId); + Attempt> CopyContentType(IContentType toCopy, int containerId); } } \ No newline at end of file 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 55cc2974b7..4c7b6f916e 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 @@ -29,7 +29,7 @@ function contentTypeResource($q, $http, umbRequestHelper, umbDataFormatter) { filterContentTypes: filterContentTypes, filterPropertyTypes: filterPropertyTypes }; - + return umbRequestHelper.resourcePromise( $http.post( umbRequestHelper.getApiUrl( @@ -201,9 +201,9 @@ function contentTypeResource($q, $http, umbRequestHelper, umbDataFormatter) { * .then(function() { * alert("node was moved"); * }, function(err){ - * alert("node didnt move:" + err.data.Message); + * 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 @@ -230,6 +230,26 @@ function contentTypeResource($q, $http, umbRequestHelper, umbDataFormatter) { 'Failed to move content'); }, + copy: 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", "PostCopy"), + { + parentId: args.parentId, + id: args.id + }), + 'Failed to copy content'); + }, + createContainer: function(parentId, name) { return umbRequestHelper.resourcePromise( @@ -237,7 +257,7 @@ function contentTypeResource($q, $http, umbRequestHelper, umbDataFormatter) { 'Failed to create a folder under parent id ' + parentId); } - + }; } angular.module('umbraco.resources').factory('contentTypeResource', contentTypeResource); diff --git a/src/Umbraco.Web.UI.Client/src/views/documenttypes/copy.controller.js b/src/Umbraco.Web.UI.Client/src/views/documenttypes/copy.controller.js new file mode 100644 index 0000000000..c666a2159c --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/documenttypes/copy.controller.js @@ -0,0 +1,63 @@ +angular.module("umbraco") +.controller("Umbraco.Editors.DocumentTypes.CopyController", + function ($scope, contentTypeResource, treeService, navigationService, notificationsService, appState, eventsService) { + 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.copy = function () { + + $scope.busy = true; + $scope.error = false; + + contentTypeResource.copy({ parentId: $scope.target.id, id: dialogOptions.currentNode.id }) + .then(function (path) { + $scope.error = false; + $scope.success = true; + $scope.busy = false; + + //get the currently edited node (if any) + var activeNode = appState.getTreeState("selectedNode"); + + //we need to do a double sync here: first sync to the copied content - but don't activate the node, + //then sync to the currenlty edited content (note: this might not be the content that was copied!!) + + 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/copy.html b/src/Umbraco.Web.UI.Client/src/views/documenttypes/copy.html new file mode 100644 index 0000000000..db1a0db640 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/documenttypes/copy.html @@ -0,0 +1,51 @@ +
+ +
+
+ +

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

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

{{error.data.message}}

+
+ +
+
+ {{currentNode.name}} was copied 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 4f6b65dc64..0842755da3 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/en.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/en.xml @@ -1055,7 +1055,9 @@ To manage your website, simply open the Umbraco back office and start adding con Yes, delete was moved underneath + was copied underneath Select the folder to move + Select the folder to copy to in the tree structure below All Document types 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 1cc40abd1d..ce25e990c8 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml @@ -1051,7 +1051,9 @@ To manage your website, simply open the Umbraco back office and start adding con Yes, delete was moved underneath + was copied underneath Select the folder to move + Select the folder to copy to in the tree structure below All Document types diff --git a/src/Umbraco.Web/Editors/ContentTypeController.cs b/src/Umbraco.Web/Editors/ContentTypeController.cs index eaa587fc6c..5099db8cac 100644 --- a/src/Umbraco.Web/Editors/ContentTypeController.cs +++ b/src/Umbraco.Web/Editors/ContentTypeController.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Configuration; using System.Linq; using System.Net; @@ -22,9 +23,9 @@ using Umbraco.Web.Models; namespace Umbraco.Web.Editors { - //TODO: We'll need to be careful about the security on this controller, when we start implementing + //TODO: We'll need to be careful about the security on this controller, when we start implementing // methods to modify content types we'll need to enforce security on the individual methods, we - // cannot put security on the whole controller because things like + // cannot put security on the whole controller because things like // GetAllowedChildren, GetPropertyTypeScaffold, GetAllPropertyTypeAliases are required for content editing. /// @@ -129,7 +130,7 @@ namespace Umbraco.Web.Editors } [UmbracoTreeAuthorize( - Constants.Trees.DocumentTypes, Constants.Trees.Content, + Constants.Trees.DocumentTypes, Constants.Trees.Content, Constants.Trees.MediaTypes, Constants.Trees.Media, Constants.Trees.MemberTypes, Constants.Trees.Members)] public ContentPropertyDisplay GetPropertyTypeScaffold(int id) @@ -166,13 +167,13 @@ namespace Umbraco.Web.Editors return Request.CreateResponse(HttpStatusCode.OK); } - + public HttpResponseMessage PostCreateContainer(int parentId, string name) { var result = Services.ContentTypeService.CreateContentTypeContainer(parentId, name, Security.CurrentUser.Id); return result - ? Request.CreateResponse(HttpStatusCode.OK, result.Result) //return the id + ? Request.CreateResponse(HttpStatusCode.OK, result.Result) //return the id : Request.CreateNotificationValidationErrorResponse(result.Exception.Message); } @@ -203,7 +204,7 @@ namespace Umbraco.Web.Editors //make sure the template alias is set on the default and allowed template so we can map it back ctSave.DefaultTemplate = template.Alias; - + } }); @@ -231,7 +232,7 @@ namespace Umbraco.Web.Editors } else ct = new ContentType(parentId); - + ct.Icon = "icon-document"; var dto = Mapper.Map(ct); @@ -302,18 +303,29 @@ namespace Umbraco.Web.Editors } /// - /// Move the media type + /// Move the content type /// /// /// public HttpResponseMessage PostMove(MoveOrCopy move) { - return PerformMove( + return PerformMoveOrCopy( move, getContentType: i => Services.ContentTypeService.GetContentType(i), - doMove: (type, i) => Services.ContentTypeService.MoveContentType(type, i)); + doMoveOrCopy: (type, i) => Services.ContentTypeService.MoveContentType(type, i)); } - + /// + /// Copy the content type + /// + /// + /// + public HttpResponseMessage PostCopy(MoveOrCopy copy) + { + return PerformMoveOrCopy( + copy, + getContentType: i => Services.ContentTypeService.GetContentType(i), + doMoveOrCopy: (type, i) => Services.ContentTypeService.CopyContentType(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 bc49e10f02..361c1133fd 100644 --- a/src/Umbraco.Web/Editors/ContentTypeControllerBase.cs +++ b/src/Umbraco.Web/Editors/ContentTypeControllerBase.cs @@ -25,7 +25,7 @@ namespace Umbraco.Web.Editors /// /// Am abstract API controller providing functionality used for dealing with content and media types /// - [PluginController("UmbracoApi")] + [PluginController("UmbracoApi")] [PrefixlessBodyModelValidator] public abstract class ContentTypeControllerBase : UmbracoAuthorizedJsonController { @@ -36,7 +36,7 @@ namespace Umbraco.Web.Editors /// protected ContentTypeControllerBase() : this(UmbracoContext.Current) - { + { } /// @@ -61,17 +61,17 @@ namespace Umbraco.Web.Editors /// This is required because in the case of creating/modifying a content type because new property types being added to it are not yet persisted so cannot /// be looked up via the db, they need to be passed in. /// - /// + /// /// - protected IEnumerable> PerformGetAvailableCompositeContentTypes(int contentTypeId, - UmbracoObjectTypes type, + protected IEnumerable> PerformGetAvailableCompositeContentTypes(int contentTypeId, + UmbracoObjectTypes type, string[] filterContentTypes, string[] filterPropertyTypes) { IContentTypeComposition source = null; //below is all ported from the old doc type editor and comes with the same weaknesses /insanity / magic - + IContentTypeComposition[] allContentTypes; switch (type) @@ -132,7 +132,7 @@ namespace Umbraco.Web.Editors }) .ToList(); } - + protected string TranslateItem(string text) { @@ -155,7 +155,7 @@ namespace Umbraco.Web.Editors Action beforeCreateNew = null) where TContentType : class, IContentTypeComposition where TContentTypeDisplay : ContentTypeCompositionDisplay - where TContentTypeSave : ContentTypeSave + where TContentTypeSave : ContentTypeSave where TPropertyType : PropertyTypeBasic { var ctId = Convert.ToInt32(contentTypeSave.Id); @@ -187,10 +187,10 @@ namespace Umbraco.Web.Editors { group.Properties = group.Properties.Where(x => x.Alias.IsNullOrWhiteSpace() == false).ToList(); } - + if (ctId > 0) { - //its an update to an existing content type + //its an update to an existing content type //This mapping will cause a lot of content type validation to occur which we need to deal with try @@ -216,7 +216,7 @@ namespace Umbraco.Web.Editors { beforeCreateNew(contentTypeSave); } - + //check if the type is trying to allow type 0 below itself - id zero refers to the currently unsaved type //always filter these 0 types out var allowItselfAsChild = false; @@ -227,12 +227,12 @@ namespace Umbraco.Web.Editors } //save as new - + TContentType newCt = null; try { //This mapping will cause a lot of content type validation to occur which we need to deal with - newCt = Mapper.Map(contentTypeSave); + newCt = Mapper.Map(contentTypeSave); } catch (Exception ex) { @@ -260,18 +260,18 @@ namespace Umbraco.Web.Editors return newCt; } } - + /// - /// Change the sort order for media + /// Move /// /// /// - /// + /// /// - protected HttpResponseMessage PerformMove( + protected HttpResponseMessage PerformMoveOrCopy( MoveOrCopy move, Func getContentType, - Func>> doMove) + Func>> doMoveOrCopy) where TContentType : IContentTypeComposition { var toMove = getContentType(move.Id); @@ -280,7 +280,7 @@ namespace Umbraco.Web.Editors return Request.CreateResponse(HttpStatusCode.NotFound); } - var result = doMove(toMove, move.ParentId); + var result = doMoveOrCopy(toMove, move.ParentId); if (result.Success) { var response = Request.CreateResponse(HttpStatusCode.OK); @@ -293,7 +293,7 @@ namespace Umbraco.Web.Editors case MoveOperationStatusType.FailedParentNotFound: return Request.CreateResponse(HttpStatusCode.NotFound); case MoveOperationStatusType.FailedCancelledByEvent: - //returning an object of INotificationModel will ensure that any pending + //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: @@ -312,14 +312,14 @@ namespace Umbraco.Web.Editors /// /// private HttpResponseException CreateCompositionValidationExceptionIfInvalid(TContentTypeSave contentTypeSave, IContentTypeComposition composition) - where TContentTypeSave : ContentTypeSave + where TContentTypeSave : ContentTypeSave where TPropertyType : PropertyTypeBasic where TContentTypeDisplay : ContentTypeCompositionDisplay { var validateAttempt = Services.ContentTypeService.ValidateComposition(composition); if (validateAttempt == false) { - //if it's not successful then we need to return some model state for the property aliases that + //if it's not successful then we need to return some model state for the property aliases that // are duplicated var invalidPropertyAliases = validateAttempt.Result.Distinct(); AddCompositionValidationErrors(contentTypeSave, invalidPropertyAliases); @@ -340,7 +340,7 @@ namespace Umbraco.Web.Editors /// /// private void AddCompositionValidationErrors(TContentTypeSave contentTypeSave, IEnumerable invalidPropertyAliases) - where TContentTypeSave : ContentTypeSave + where TContentTypeSave : ContentTypeSave where TPropertyType : PropertyTypeBasic { foreach (var propertyAlias in invalidPropertyAliases) @@ -369,8 +369,8 @@ namespace Umbraco.Web.Editors private HttpResponseException CreateInvalidCompositionResponseException( Exception ex, TContentTypeSave contentTypeSave, TContentType ct, int ctId) where TContentType : class, IContentTypeComposition - where TContentTypeDisplay : ContentTypeCompositionDisplay - where TContentTypeSave : ContentTypeSave + where TContentTypeDisplay : ContentTypeCompositionDisplay + where TContentTypeSave : ContentTypeSave where TPropertyType : PropertyTypeBasic { InvalidCompositionException invalidCompositionException = null; @@ -431,7 +431,7 @@ namespace Umbraco.Web.Editors (_cultureDictionary = CultureDictionaryFactoryResolver.Current.Factory.CreateDictionary()); } } - + } } \ No newline at end of file diff --git a/src/Umbraco.Web/Editors/MediaTypeController.cs b/src/Umbraco.Web/Editors/MediaTypeController.cs index 43071125de..81b92f1ee4 100644 --- a/src/Umbraco.Web/Editors/MediaTypeController.cs +++ b/src/Umbraco.Web/Editors/MediaTypeController.cs @@ -22,7 +22,7 @@ using Umbraco.Web.Models; namespace Umbraco.Web.Editors { - //TODO: We'll need to be careful about the security on this controller, when we start implementing + //TODO: We'll need to be careful about the security on this controller, when we start implementing // methods to modify content types we'll need to enforce security on the individual methods, we // cannot put security on the whole controller because things like GetAllowedChildren are required for content editing. @@ -49,7 +49,7 @@ namespace Umbraco.Web.Editors public MediaTypeController(UmbracoContext umbracoContext) : base(umbracoContext) { - + } public int GetCount() @@ -112,7 +112,7 @@ namespace Umbraco.Web.Editors contentType = x.Item1, allowed = x.Item2 }); - return Request.CreateResponse(result); + return Request.CreateResponse(result); } public MediaTypeDisplay GetEmpty(int parentId) @@ -129,8 +129,8 @@ namespace Umbraco.Web.Editors /// Returns all member types /// public IEnumerable GetAll() - { - + { + return Services.ContentTypeService.GetAllMediaTypes() .Select(Mapper.Map); } @@ -154,7 +154,7 @@ namespace Umbraco.Web.Editors var result = Services.ContentTypeService.CreateMediaTypeContainer(parentId, name, Security.CurrentUser.Id); return result - ? Request.CreateResponse(HttpStatusCode.OK, result.Result) //return the id + ? Request.CreateResponse(HttpStatusCode.OK, result.Result) //return the id : Request.CreateNotificationValidationErrorResponse(result.Exception.Message); } @@ -227,11 +227,10 @@ namespace Umbraco.Web.Editors /// public HttpResponseMessage PostMove(MoveOrCopy move) { - return PerformMove( - move, - getContentType: i => Services.ContentTypeService.GetMediaType(i), - doMove: (type, i) => Services.ContentTypeService.MoveMediaType(type, i)); + return PerformMoveOrCopy( + move, + getContentType: i => Services.ContentTypeService.GetMediaType(i), + doMoveOrCopy: (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 c45d5d0c1b..1cda4995fa 100644 --- a/src/Umbraco.Web/Trees/ContentTypeTreeController.cs +++ b/src/Umbraco.Web/Trees/ContentTypeTreeController.cs @@ -123,6 +123,7 @@ namespace Umbraco.Web.Trees menu.Items.Add(Services.TextService.Localize(string.Format("actions/{0}", ActionMove.Instance.Alias)), true); } } + menu.Items.Add(Services.TextService.Localize(string.Format("actions/{0}", ActionCopy.Instance.Alias))); menu.Items.Add(Services.TextService.Localize(string.Format("actions/{0}", ActionExport.Instance.Alias)), true).ConvertLegacyMenuItem(new UmbracoEntity { Id = int.Parse(id),