Merge remote-tracking branch 'lars/temp-content-blueprints' into temp-content-blueprints

This commit is contained in:
Shannon
2017-06-05 14:02:23 +02:00
6 changed files with 304 additions and 233 deletions

View File

@@ -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);
})();

View File

@@ -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);
});

View File

@@ -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',

View File

@@ -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");
});
});
}());

View File

@@ -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);

View File

@@ -115,20 +115,25 @@ namespace Umbraco.Web.Editors
var content = Mapper.Map<IContent, ContentItemDisplay>(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);
}
/// <summary>
/// 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<OperationStatus>.Succeed(new OperationStatus(OperationStatusType.Success, new EventMessages()));
});
SetupBlueprint(contentItemDisplay, contentItemDisplay.PersistedContent);
return contentItemDisplay;
}
/// <summary>
@@ -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;
}