diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/content/edit.controller.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/content/edit.controller.js index 1b88ea4843..db5def83d8 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/content/edit.controller.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/content/edit.controller.js @@ -1,233 +1,236 @@ (function () { 'use strict'; - function ContentEditController($rootScope, $routeParams, $q, $timeout, $window, appState, contentResource, entityResource, navigationService, notificationsService, angularHelper, serverValidationManager, contentEditingHelper, treeService, fileManager, formHelper, umbRequestHelper, keyboardService, umbModelMapper, editorState, $http) { + function ContentEditController($rootScope, $scope, $routeParams, $q, $timeout, $window, appState, contentResource, entityResource, navigationService, notificationsService, angularHelper, serverValidationManager, contentEditingHelper, treeService, fileManager, formHelper, umbRequestHelper, keyboardService, umbModelMapper, editorState, $http) { - function link(scope, element, attrs, ctrl) { - //setup scope vars - scope.defaultButton = null; - scope.subButtons = []; + //setup scope vars + $scope.defaultButton = null; + $scope.subButtons = []; - scope.page = {}; - scope.page.loading = false; - scope.page.menu = {}; - scope.page.menu.currentNode = null; - scope.page.menu.currentSection = appState.getSectionState("currentSection"); - scope.page.listViewPath = null; - scope.page.isNew = scope.isNew ? true : false; - scope.page.buttonGroupState = "init"; + $scope.page = {}; + $scope.page.loading = false; + $scope.page.menu = {}; + $scope.page.menu.currentNode = null; + $scope.page.menu.currentSection = appState.getSectionState("currentSection"); + $scope.page.listViewPath = null; + $scope.page.isNew = $scope.isNew ? true : false; + $scope.page.buttonGroupState = "init"; - function init(content) { + function init(content) { - var buttons = contentEditingHelper.configureContentEditorButtons({ - create: scope.page.isNew, - content: content, - methods: { - saveAndPublish: scope.saveAndPublish, - sendToPublish: scope.sendToPublish, - save: scope.save, - unPublish: scope.unPublish - } - }); - scope.defaultButton = buttons.defaultButton; - scope.subButtons = buttons.subButtons; - - editorState.set(scope.content); - - //We fetch all ancestors of the node to generate the footer breadcrumb navigation - if (!scope.page.isNew) { - if (content.parentId && content.parentId !== -1) { - entityResource.getAncestors(content.id, "document") - .then(function (anc) { - scope.ancestors = anc; - }); - } + var buttons = contentEditingHelper.configureContentEditorButtons({ + create: $scope.page.isNew, + content: content, + methods: { + saveAndPublish: $scope.saveAndPublish, + sendToPublish: $scope.sendToPublish, + save: $scope.save, + unPublish: $scope.unPublish } - } + }); + $scope.defaultButton = buttons.defaultButton; + $scope.subButtons = buttons.subButtons; - /** Syncs the content item to it's tree node - this occurs on first load and after saving */ - function syncTreeNode(content, path, initialLoad) { - - if (!scope.content.isChildOfListView) { - navigationService.syncTree({ tree: scope.treeAlias, path: path.split(","), forceReload: initialLoad !== true }).then(function (syncArgs) { - scope.page.menu.currentNode = syncArgs.node; - }); - } - else if (initialLoad === true) { - - //it's a child item, just sync the ui node to the parent - navigationService.syncTree({ tree: scope.treeAlias, path: path.substring(0, path.lastIndexOf(",")).split(","), forceReload: initialLoad !== true }); - - //if this is a child of a list view and it's the initial load of the editor, we need to get the tree node - // from the server so that we can load in the actions menu. - umbRequestHelper.resourcePromise( - $http.get(content.treeNodeUrl), - 'Failed to retrieve data for child node ' + content.id).then(function (node) { - scope.page.menu.currentNode = node; - }); - } - } - - // This is a helper method to reduce the amount of code repitition for actions: Save, Publish, SendToPublish - function performSave(args) { - var deferred = $q.defer(); - - scope.page.buttonGroupState = "busy"; - - contentEditingHelper.contentEditorPerformSave({ - statusMessage: args.statusMessage, - saveMethod: args.saveMethod, - scope: scope, - content: scope.content, - action: args.action - }).then(function (data) { - //success - init(scope.content); - syncTreeNode(scope.content, data.path); - - scope.page.buttonGroupState = "success"; - - deferred.resolve(data); - }, function (err) { - //error - if (err) { - editorState.set(scope.content); - } - - scope.page.buttonGroupState = "error"; - - deferred.reject(err); - }); - - return deferred.promise; - } - - function resetLastListPageNumber(content) { - // We're using rootScope to store the page number for list views, so if returning to the list - // we can restore the page. If we've moved on to edit a piece of content that's not the list or it's children - // we should remove this so as not to confuse if navigating to a different list - if (!content.isChildOfListView && !content.isContainer) { - $rootScope.lastListViewPageViewed = null; - } - } - - if (scope.page.isNew) { - - scope.page.loading = true; - - //we are creating so get an empty content item - scope.getScaffoldMethod()() - .then(function (data) { - - scope.content = data; - - init(scope.content); - - resetLastListPageNumber(scope.content); - - scope.page.loading = false; - - }); - } - else { - - scope.page.loading = true; - - //we are editing so get the content item from the server - scope.getMethod()(scope.contentId) - .then(function (data) { - - scope.content = data; - - if (data.isChildOfListView && data.trashed === false) { - scope.page.listViewPath = ($routeParams.page) ? - "/content/content/edit/" + data.parentId + "?page=" + $routeParams.page : - "/content/content/edit/" + data.parentId; - } - - init(scope.content); - - //in one particular special case, after we've created a new item we redirect back to the edit - // route but there might be server validation errors in the collection which we need to display - // after the redirect, so we will bind all subscriptions which will show the server validation errors - // if there are any and then clear them so the collection no longer persists them. - serverValidationManager.executeAndClearAllSubscriptions(); - - syncTreeNode(scope.content, data.path, true); - - resetLastListPageNumber(scope.content); - - scope.page.loading = false; - - }); - } - - - scope.unPublish = function () { - - if (formHelper.submitForm({ scope: scope, statusMessage: "Unpublishing...", skipValidation: true })) { - - scope.page.buttonGroupState = "busy"; - - contentResource.unPublish(scope.content.id) - .then(function (data) { - - formHelper.resetForm({ scope: scope, notifications: data.notifications }); - - contentEditingHelper.handleSuccessfulSave({ - scope: scope, - savedContent: data, - rebindCallback: contentEditingHelper.reBindChangedProperties(scope.content, data) - }); - - init(scope.content); - - syncTreeNode(scope.content, data.path); - - scope.page.buttonGroupState = "success"; + editorState.set($scope.content); + //We fetch all ancestors of the node to generate the footer breadcrumb navigation + if (!$scope.page.isNew) { + if (content.parentId && content.parentId !== -1) { + entityResource.getAncestors(content.id, "document") + .then(function (anc) { + $scope.ancestors = anc; }); } + } + } - }; + /** Syncs the content item to it's tree node - this occurs on first load and after saving */ + function syncTreeNode(content, path, initialLoad) { - scope.sendToPublish = function () { - return performSave({ saveMethod: contentResource.sendToPublish, statusMessage: "Sending...", action: "sendToPublish" }); - }; + if (!$scope.content.isChildOfListView) { + navigationService.syncTree({ tree: $scope.treeAlias, path: path.split(","), forceReload: initialLoad !== true }).then(function (syncArgs) { + $scope.page.menu.currentNode = syncArgs.node; + }); + } + else if (initialLoad === true) { - scope.saveAndPublish = function () { - return performSave({ saveMethod: contentResource.publish, statusMessage: "Publishing...", action: "publish" }); - }; + //it's a child item, just sync the ui node to the parent + navigationService.syncTree({ tree: $scope.treeAlias, path: path.substring(0, path.lastIndexOf(",")).split(","), forceReload: initialLoad !== true }); - scope.save = function () { - return performSave({ saveMethod: scope.saveMethod(), statusMessage: "Saving...", action: "save" }); - }; + //if this is a child of a list view and it's the initial load of the editor, we need to get the tree node + // from the server so that we can load in the actions menu. + umbRequestHelper.resourcePromise( + $http.get(content.treeNodeUrl), + 'Failed to retrieve data for child node ' + content.id).then(function (node) { + $scope.page.menu.currentNode = node; + }); + } + } - scope.preview = function (content) { + // This is a helper method to reduce the amount of code repitition for actions: Save, Publish, SendToPublish + function performSave(args) { + var deferred = $q.defer(); + $scope.page.buttonGroupState = "busy"; - if (!scope.busy) { - - // Chromes popup blocker will kick in if a window is opened - // outwith the initial scoped request. This trick will fix that. - // - var previewWindow = $window.open('preview/?id=' + content.id, 'umbpreview'); - scope.save().then(function (data) { - // Build the correct path so both /#/ and #/ work. - var redirect = Umbraco.Sys.ServerVariables.umbracoSettings.umbracoPath + '/preview/?id=' + data.id; - previewWindow.location.href = redirect; - }); + contentEditingHelper.contentEditorPerformSave({ + statusMessage: args.statusMessage, + saveMethod: args.saveMethod, + scope: $scope, + content: $scope.content, + action: args.action + }).then(function (data) { + //success + init($scope.content); + syncTreeNode($scope.content, data.path); + $scope.page.buttonGroupState = "success"; + deferred.resolve(data); + }, function (err) { + //error + if (err) { + editorState.set($scope.content); } - }; + $scope.page.buttonGroupState = "error"; + + deferred.reject(err); + }); + + return deferred.promise; } + function resetLastListPageNumber(content) { + // We're using rootScope to store the page number for list views, so if returning to the list + // we can restore the page. If we've moved on to edit a piece of content that's not the list or it's children + // we should remove this so as not to confuse if navigating to a different list + if (!content.isChildOfListView && !content.isContainer) { + $rootScope.lastListViewPageViewed = null; + } + } + + if ($scope.page.isNew) { + + $scope.page.loading = true; + + //we are creating so get an empty content item + $scope.getScaffoldMethod()() + .then(function (data) { + + $scope.content = data; + + init($scope.content); + + resetLastListPageNumber($scope.content); + + $scope.page.loading = false; + + }); + } + else { + + $scope.page.loading = true; + + //we are editing so get the content item from the server + $scope.getMethod()($scope.contentId) + .then(function (data) { + + $scope.content = data; + + if (data.isChildOfListView && data.trashed === false) { + $scope.page.listViewPath = ($routeParams.page) ? + "/content/content/edit/" + data.parentId + "?page=" + $routeParams.page : + "/content/content/edit/" + data.parentId; + } + + init($scope.content); + + //in one particular special case, after we've created a new item we redirect back to the edit + // route but there might be server validation errors in the collection which we need to display + // after the redirect, so we will bind all subscriptions which will show the server validation errors + // if there are any and then clear them so the collection no longer persists them. + serverValidationManager.executeAndClearAllSubscriptions(); + + syncTreeNode($scope.content, data.path, true); + + resetLastListPageNumber($scope.content); + + $scope.page.loading = false; + + }); + } + + + $scope.unPublish = function () { + + if (formHelper.submitForm({ scope: $scope, statusMessage: "Unpublishing...", skipValidation: true })) { + + $scope.page.buttonGroupState = "busy"; + + contentResource.unPublish($scope.content.id) + .then(function (data) { + + formHelper.resetForm({ scope: $scope, notifications: data.notifications }); + + contentEditingHelper.handleSuccessfulSave({ + scope: $scope, + savedContent: data, + rebindCallback: contentEditingHelper.reBindChangedProperties($scope.content, data) + }); + + init($scope.content); + + syncTreeNode($scope.content, data.path); + + $scope.page.buttonGroupState = "success"; + + }); + } + + }; + + $scope.sendToPublish = function () { + return performSave({ saveMethod: contentResource.sendToPublish, statusMessage: "Sending...", action: "sendToPublish" }); + }; + + $scope.saveAndPublish = function () { + return performSave({ saveMethod: contentResource.publish, statusMessage: "Publishing...", action: "publish" }); + }; + + $scope.save = function () { + return performSave({ saveMethod: $scope.saveMethod(), statusMessage: "Saving...", action: "save" }); + }; + + $scope.preview = function (content) { + + + if (!$scope.busy) { + + // Chromes popup blocker will kick in if a window is opened + // outwith the initial scoped request. This trick will fix that. + // + var previewWindow = $window.open('preview/?id=' + content.id, 'umbpreview'); + $scope.save().then(function (data) { + // Build the correct path so both /#/ and #/ work. + var redirect = Umbraco.Sys.ServerVariables.umbracoSettings.umbracoPath + '/preview/?id=' + data.id; + previewWindow.location.href = redirect; + }); + + + } + + }; + + } + + function createDirective() { + var directive = { restrict: 'E', replace: true, templateUrl: 'views/components/content/edit.html', + controller: 'Umbraco.Editors.Content.EditorDirectiveController', scope: { contentId: "=", isNew: "=?", @@ -236,14 +239,14 @@ saveMethod: "&", getMethod: "&", getScaffoldMethod: "&?" - }, - link: link + } }; return directive; } - angular.module('umbraco.directives').directive('contentEditor', ContentEditController); + angular.module('umbraco.directives').controller('Umbraco.Editors.Content.EditorDirectiveController', ContentEditController); + angular.module('umbraco.directives').directive('contentEditor', createDirective); })(); diff --git a/src/Umbraco.Web.UI.Client/src/views/content/content.create.controller.js b/src/Umbraco.Web.UI.Client/src/views/content/content.create.controller.js index aca3332181..2cdbffc8e7 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/content.create.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/content/content.create.controller.js @@ -12,7 +12,8 @@ function contentCreateController($scope, iconHelper, $location, navigationService, - blueprintConfig) { + blueprintConfig) { + contentTypeResource.getAllowedTypes($scope.currentNode.id).then(function(data) { $scope.allowedTypes = iconHelper.formatContentTypeIcons(data); }); diff --git a/src/Umbraco.Web.UI.Client/test/config/karma.conf.js b/src/Umbraco.Web.UI.Client/test/config/karma.conf.js index ede7c20538..0e06085ff4 100644 --- a/src/Umbraco.Web.UI.Client/test/config/karma.conf.js +++ b/src/Umbraco.Web.UI.Client/test/config/karma.conf.js @@ -32,7 +32,7 @@ module.exports = function(karma) { 'test/config/app.unit.js', 'src/common/mocks/umbraco.servervariables.js', - 'src/common/directives/*.js', + 'src/common/directives/**/*.js', 'src/common/filters/*.js', 'src/common/services/*.js', 'src/common/security/*.js', diff --git a/src/Umbraco.Web.UI.Client/test/unit/app/content/create-content-controller.spec.js b/src/Umbraco.Web.UI.Client/test/unit/app/content/create-content-controller.spec.js new file mode 100644 index 0000000000..f84fe3a01d --- /dev/null +++ b/src/Umbraco.Web.UI.Client/test/unit/app/content/create-content-controller.spec.js @@ -0,0 +1,54 @@ +(function() { + + describe("create content dialog", + function() { + + var scope, + allowedTypes = [{ id: 1, alias: "x" }, { id: 2, alias: "y" }], + location; + + beforeEach(module("umbraco")); + + beforeEach(inject(function ($controller, $rootScope, $q, $location) { + var contentTypeResource = { + getAllowedTypes: function() { + var def = $q.defer(); + def.resolve(allowedTypes); + return def.promise; + } + } + + location = $location; + + scope = $rootScope.$new(); + scope.currentNode = { id: 1234 }; + + $controller("Umbraco.Editors.Content.CreateController", { + $scope: scope, + contentTypeResource: contentTypeResource + }); + + scope.$digest(); + + })); + + it("shows available child document types for the given node", function() { + expect(scope.allowedTypes).toBe(allowedTypes); + }); + + it("creates content directly when there are no blueprints", + function() { + var searcher = {search:function(){}}; + spyOn(location, "path").andReturn(searcher); + spyOn(searcher, "search"); + + scope.createOrSelectBlueprintIfAny(allowedTypes[0]); + + expect(location.path).toHaveBeenCalledWith("/content/content/edit/1234"); + expect(searcher.search).toHaveBeenCalledWith("doctype=x&create=true"); + }); + + }); + +}()); + diff --git a/src/Umbraco.Web.UI.Client/test/unit/app/content/edit-content-controller.spec.js b/src/Umbraco.Web.UI.Client/test/unit/app/content/edit-content-controller.spec.js index dad6bf78ca..ae3c33a19a 100644 --- a/src/Umbraco.Web.UI.Client/test/unit/app/content/edit-content-controller.spec.js +++ b/src/Umbraco.Web.UI.Client/test/unit/app/content/edit-content-controller.spec.js @@ -2,15 +2,15 @@ // The current setup will have problems with loading the HTML etc. // These tests are therefore ignored for now. -xdescribe('edit content controller tests', function () { - var scope, controller, routeParams, httpBackend; +describe('edit content controller tests', function () { + var scope, controller, routeParams, httpBackend, wasSaved, q; routeParams = {id: 1234, create: false}; beforeEach(module('umbraco')); //inject the contentMocks service - beforeEach(inject(function ($rootScope, $controller, $compile, angularHelper, $httpBackend, contentMocks, entityMocks, mocksUtils, localizationMocks) { - + beforeEach(inject(function ($rootScope, $q, $controller, $compile, angularHelper, $httpBackend, contentMocks, entityMocks, mocksUtils, localizationMocks) { + q = $q; //for these tests we don't want any authorization to occur mocksUtils.disableAuth(); @@ -24,28 +24,32 @@ xdescribe('edit content controller tests', function () { localizationMocks.register(); //this controller requires an angular form controller applied to it - scope.contentForm = angularHelper.getNullForm("contentForm"); - - controller = $controller('Umbraco.Editors.Content.EditController', { + scope.contentForm = angularHelper.getNullForm("contentForm"); + + var deferred = $q.defer(); + wasSaved = false; + scope.saveMethod = function() { wasSaved = true; }; + scope.getMethod = function() { return function() { return deferred.promise; } }; + scope.treeAlias = "content"; + + controller = $controller('Umbraco.Editors.Content.EditorDirectiveController', { $scope: scope, $routeParams: routeParams }); //For controller tests its easiest to have the digest and flush happen here //since its intially always the same $http calls made - - //scope.$digest resolves the promise against the httpbackend - scope.$digest(); - //httpbackend.flush() resolves all request against the httpbackend - //to fake a async response, (which is what happens on a real setup) - //httpBackend.flush(); + // Resolve the get method + deferred.resolve(mocksUtils.getMockContent(1234)); + + //scope.$digest resolves the promise + scope.$digest(); })); describe('content edit controller save and publish', function () { - it('it should have an content object', function() { - + it('it should have an content object', function() { //controller should have a content object expect(scope.content).toNotBe(undefined); diff --git a/src/Umbraco.Web/Editors/ContentController.cs b/src/Umbraco.Web/Editors/ContentController.cs index 9643f061f4..c47ee30846 100644 --- a/src/Umbraco.Web/Editors/ContentController.cs +++ b/src/Umbraco.Web/Editors/ContentController.cs @@ -115,20 +115,25 @@ namespace Umbraco.Web.Editors var content = Mapper.Map(foundContent); - content.AllowPreview = false; - + SetupBlueprint(content, foundContent); + + return content; + } + + private static void SetupBlueprint(ContentItemDisplay content, IContent persistedContent) + { + content.AllowPreview = false; + //set a custom path since the tree that renders this has the content type id as the parent - content.Path = string.Format("-1,{0},{1}", foundContent.ContentTypeId, content.Id); + content.Path = string.Format("-1,{0},{1}", persistedContent.ContentTypeId, content.Id); content.AllowedActions = new[] {'A'}; var excludeProps = new[] {"_umb_urls", "_umb_releasedate", "_umb_expiredate", "_umb_template"}; var propsTab = content.Tabs.Last(); propsTab.Properties = propsTab.Properties - .Where(p => excludeProps.Contains(p.Alias) == false); - - return content; - } + .Where(p => excludeProps.Contains(p.Alias) == false); + } /// /// Gets the content json for the content id @@ -365,13 +370,16 @@ namespace Umbraco.Web.Editors public ContentItemDisplay PostSaveBlueprint( [ModelBinder(typeof(ContentItemBinder))] ContentItemSave contentItem) { - return PostSaveInternal(contentItem, + var contentItemDisplay = PostSaveInternal(contentItem, content => - { - Services.ContentService.SaveBlueprint(contentItem.PersistedContent, Security.CurrentUser.Id); - //we need to reuse the underlying logic so return the result that it wants + { + Services.ContentService.SaveBlueprint(contentItem.PersistedContent, Security.CurrentUser.Id); + //we need to reuse the underlying logic so return the result that it wants return Attempt.Succeed(new OperationStatus(OperationStatusType.Success, new EventMessages())); }); + SetupBlueprint(contentItemDisplay, contentItemDisplay.PersistedContent); + + return contentItemDisplay; } /// @@ -396,7 +404,6 @@ namespace Umbraco.Web.Editors // * any file attachments have been saved to their temporary location for us to use // * we have a reference to the DTO object and the persisted object // * Permissions are valid - MapPropertyValues(contentItem); //We need to manually check the validation results here because: @@ -503,6 +510,8 @@ namespace Umbraco.Web.Editors throw new HttpResponseException(Request.CreateValidationErrorResponse(display)); } + display.PersistedContent = contentItem.PersistedContent; + return display; }