Working on U4-5687 Fix issues with mini content editor that is launched from new Edit button in MNTP

This commit is contained in:
Shannon
2014-10-27 13:01:51 +10:00
parent 5d9c7d9068
commit 7d3a66de07
8 changed files with 363 additions and 260 deletions

View File

@@ -5,10 +5,177 @@
* @description A helper service for most editors, some methods are specific to content/media/member model types but most are used by
* all editors to share logic and reduce the amount of replicated code among editors.
**/
function contentEditingHelper($location, $routeParams, notificationsService, serverValidationManager, dialogService, formHelper, appState) {
function contentEditingHelper($q, $location, $routeParams, notificationsService, serverValidationManager, dialogService, formHelper, appState, keyboardService) {
return {
/** Used by the content editor and mini content editor to perform saving operations */
contentEditorPerformSave: function (args) {
if (!angular.isObject(args)) {
throw "args must be an object";
}
if (!args.fileManager) {
throw "args.fileManager is not defined";
}
if (!args.scope) {
throw "args.scope is not defined";
}
if (!args.content) {
throw "args.content is not defined";
}
if (!args.statusMessage) {
throw "args.statusMessage is not defined";
}
if (!args.saveMethod) {
throw "args.saveMethod is not defined";
}
var self = this;
var deferred = $q.defer();
if (formHelper.submitForm({ scope: args.scope, statusMessage: args.statusMessage })) {
args.saveMethod(args.content, $routeParams.create, args.fileManager.getFiles())
.then(function (data) {
formHelper.resetForm({ scope: args.scope, notifications: data.notifications });
self.handleSuccessfulSave({
scope: args.scope,
savedContent: data,
rebindCallback: self.reBindChangedProperties(args.content, data)
});
deferred.resolve(data);
}, function (err) {
self.handleSaveError({
redirectOnFailure: true,
err: err,
rebindCallback: self.reBindChangedProperties(args.content, err.data)
});
deferred.reject(err);
});
}
else {
deferred.reject();
}
return deferred.promise;
},
/** Returns the action button definitions based on what permissions the user has.
The content.allowedActions parameter contains a list of chars, each represents a button by permission so
here we'll build the buttons according to the chars of the user. */
configureContentEditorButtons: function (args) {
if (!angular.isObject(args)) {
throw "args must be an object";
}
if (!args.content) {
throw "args.content is not defined";
}
if (!args.methods) {
throw "args.methods is not defined";
}
if (!args.methods.saveAndPublish || !args.methods.sendToPublish || !args.methods.save || !args.methods.unPublish) {
throw "args.methods does not contain all required defined methods";
}
var buttons = {
defaultButton: null,
subButtons: []
};
function createButtonDefinition(ch) {
switch (ch) {
case "U":
//publish action
keyboardService.bind("ctrl+p", args.methods.saveAndPublish);
return {
letter: ch,
labelKey: "buttons_saveAndPublish",
handler: args.methods.saveAndPublish,
hotKey: "ctrl+p"
};
case "H":
//send to publish
keyboardService.bind("ctrl+p", args.methods.sendToPublish);
return {
letter: ch,
labelKey: "buttons_saveToPublish",
handler: args.methods.sendToPublish,
hotKey: "ctrl+p"
};
case "A":
//save
keyboardService.bind("ctrl+s", args.methods.save);
return {
letter: ch,
labelKey: "buttons_save",
handler: args.methods.save,
hotKey: "ctrl+s"
};
case "Z":
//unpublish
keyboardService.bind("ctrl+u", args.methods.unPublish);
return {
letter: ch,
labelKey: "content_unPublish",
handler: args.methods.unPublish
};
default:
return null;
}
}
//reset
buttons.subButtons = [];
//This is the ideal button order but depends on circumstance, we'll use this array to create the button list
// Publish, SendToPublish, Save
var buttonOrder = ["U", "H", "A"];
//Create the first button (primary button)
//We cannot have the Save or SaveAndPublish buttons if they don't have create permissions when we are creating a new item.
if (!args.create || _.contains(args.content.allowedActions, "C")) {
for (var b in buttonOrder) {
if (_.contains(args.content.allowedActions, buttonOrder[b])) {
buttons.defaultButton = createButtonDefinition(buttonOrder[b]);
break;
}
}
}
//Now we need to make the drop down button list, this is also slightly tricky because:
//We cannot have any buttons if there's no default button above.
//We cannot have the unpublish button (Z) when there's no publish permission.
//We cannot have the unpublish button (Z) when the item is not published.
if (buttons.defaultButton) {
//get the last index of the button order
var lastIndex = _.indexOf(buttonOrder, buttons.defaultButton.letter);
//add the remaining
for (var i = lastIndex + 1; i < buttonOrder.length; i++) {
if (_.contains(args.content.allowedActions, buttonOrder[i])) {
buttons.subButtons.push(createButtonDefinition(buttonOrder[i]));
}
}
//if we are not creating, then we should add unpublish too,
// so long as it's already published and if the user has access to publish
if (!args.create) {
if (args.content.publishDate && _.contains(args.content.allowedActions, "U")) {
buttons.subButtons.push(createButtonDefinition("Z"));
}
}
}
return buttons;
},
/**
* @ngdoc method

View File

@@ -1,21 +1,18 @@
/**
* @ngdoc service
* @name umbraco.services.fileManager
* @function
*
* @description
* Used by editors to manage any files that require uploading with the posted data, normally called by property editors
* that need to attach files.
* When a route changes successfully, we ensure that the collection is cleared.
*/
function fileManager() {
//This is the file manager class, for the normal angular service 'fileManager' an instance of this class is returned,
// however in some cases we also want to create sub-instances (non-singleton) of this class. An example for this is the mini
// content editor that is launched from inside of a content editor. If the global singleton instance is used for both editors, then
// they will be sharing the same file collection state which will cause unwanted side affects. So in the mini editor, we create
// a standalone instance of the file manager and dipose of it when the scope disposes.
function FileManager() {
var fileCollection = [];
return {
/**
* @ngdoc function
* @name umbraco.services.fileManager#addFiles
* @name umbraco.services.fileManager#createStandaloneInstance
* @methodOf umbraco.services.fileManager
* @function
*
@@ -23,7 +20,23 @@ function fileManager() {
* Attaches files to the current manager for the current editor for a particular property, if an empty array is set
* for the files collection that effectively clears the files for the specified editor.
*/
setFiles: function(propertyAlias, files) {
createStandaloneInstance: function() {
return new FileManager();
},
/**
* @ngdoc function
* @name umbraco.services.fileManager#addFiles
* @methodOf umbraco.services.fileManager
* @function
*
* @description
* in some cases we want to create stand alone instances (non-singleton) of the fileManager. An example for this is the mini
* content editor that is launched from inside of a content editor. If the default global singleton instance is used for both editors, then
* they will be sharing the same file collection state which will cause unwanted side affects. So in the mini editor, we create
* a standalone instance of the file manager and dipose of it when the scope disposes.
*/
setFiles: function (propertyAlias, files) {
//this will clear the files for the current property and then add the new ones for the current property
fileCollection = _.reject(fileCollection, function (item) {
return item.alias === propertyAlias;
@@ -33,7 +46,7 @@ function fileManager() {
fileCollection.push({ alias: propertyAlias, file: files[i] });
}
},
/**
* @ngdoc function
* @name umbraco.services.fileManager#getFiles
@@ -43,10 +56,10 @@ function fileManager() {
* @description
* Returns all of the files attached to the file manager
*/
getFiles: function() {
getFiles: function () {
return fileCollection;
},
/**
* @ngdoc function
* @name umbraco.services.fileManager#clearFiles
@@ -59,7 +72,23 @@ function fileManager() {
clearFiles: function () {
fileCollection = [];
}
};
};
}
angular.module('umbraco.services').factory('fileManager', fileManager);
angular.module('umbraco.services').factory('fileManager',
/**
* @ngdoc service
* @name umbraco.services.fileManager
* @function
*
* @description
* Used by editors to manage any files that require uploading with the posted data, normally called by property editors
* that need to attach files.
* When a route changes successfully, we ensure that the collection is cleared.
*/
function () {
//return a new instance of a file Manager - this will be a singleton per app
return new FileManager();
});

View File

@@ -40,6 +40,10 @@
padding: 10px;
}
.umb-contentpicker small a {
color: @gray;
}
/* CODEMIRROR DATATYPE */
div.umb-codeeditor {
border: 1px solid #e8e8e8;

View File

@@ -1,50 +1,82 @@
function ContentEditDialogController($scope, $routeParams, $q, $timeout, $window, appState, contentResource, entityResource, navigationService, notificationsService, angularHelper, serverValidationManager, contentEditingHelper, treeService, fileManager, formHelper, umbRequestHelper, umbModelMapper, $http) {
//setup scope vars
$scope.model = {};
$scope.model.defaultButton = null;
$scope.model.subButtons = [];
$scope.model.nodeId = 0;
$scope.defaultButton = null;
$scope.subButtons = [];
var dialogOptions = $scope.$parent.dialogOptions;
var fileManagerInstance = fileManager.createStandaloneInstance();
if(angular.isObject(dialogOptions.entity)){
$scope.model.entity = $scope.filterTabs(dialogOptions.entity, dialogOptions.tabFilter);
$scope.loaded = true;
}else{
// This is a helper method to reduce the amount of code repitition for actions: Save, Publish, SendToPublish
function performSave(args) {
contentEditingHelper.contentEditorPerformSave({
fileManager: fileManagerInstance,
statusMessage: args.statusMessage,
saveMethod: args.saveMethod,
scope: $scope,
content: $scope.content
}).then(function (content) {
//success
$scope.busy = false;
if (dialogOptions.closeOnSave) {
$scope.submit(content);
}
if (dialogOptions.create) {
//we are creating so get an empty content item
contentResource.getScaffold(dialogOptions.id, dialogOptions.contentType)
.then(function(data) {
$scope.loaded = true;
$scope.model.entity = $scope.filterTabs(data, dialogOptions.tabFilter);
});
}
else {
//we are editing so get the content item from the server
contentResource.getById(dialogOptions.id)
.then(function(data) {
$scope.loaded = true;
$scope.model.entity = $scope.filterTabs(data, dialogOptions.tabFilter);
//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();
});
}
}, function(err) {
//error
$scope.busy = false;
});
}
function performSave(args) {
var deferred = $q.defer();
function filterTabs(entity, blackList) {
if (blackList) {
_.each(entity.tabs, function (tab) {
tab.hide = _.contains(blackList, tab.alias);
});
}
$scope.busy = true;
return entity;
};
function init(content) {
var buttons = contentEditingHelper.configureContentEditorButtons({
create: $routeParams.create,
content: content,
methods: {
saveAndPublish: $scope.saveAndPublish,
sendToPublish: $scope.sendToPublish,
save: $scope.save,
unPublish: $scope.unPublish
}
});
$scope.defaultButton = buttons.defaultButton;
$scope.subButtons = buttons.subButtons;
}
if (formHelper.submitForm({ scope: $scope, statusMessage: args.statusMessage })) {
//check if the entity is being passed in, otherwise load it from the server
if (angular.isObject(dialogOptions.entity)) {
$scope.loaded = true;
$scope.content = filterTabs(dialogOptions.entity, dialogOptions.tabFilter);
init($scope.content);
}
else {
contentResource.getById(dialogOptions.id)
.then(function(data) {
$scope.loaded = true;
$scope.content = filterTabs(data, dialogOptions.tabFilter);
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();
});
}
args.saveMethod($scope.model.entity, $routeParams.create, fileManager.getFiles())
$scope.unPublish = function () {
if (formHelper.submitForm({ scope: $scope, statusMessage: "Unpublishing...", skipValidation: true })) {
contentResource.unPublish($scope.content.id)
.then(function (data) {
formHelper.resetForm({ scope: $scope, notifications: data.notifications });
@@ -52,63 +84,43 @@ function ContentEditDialogController($scope, $routeParams, $q, $timeout, $window
contentEditingHelper.handleSuccessfulSave({
scope: $scope,
savedContent: data,
rebindCallback: contentEditingHelper.reBindChangedProperties($scope.model.entity, data)
rebindCallback: contentEditingHelper.reBindChangedProperties($scope.content, data)
});
$scope.busy = false;
deferred.resolve(data);
}, function (err) {
contentEditingHelper.handleSaveError({
redirectOnFailure: true,
err: err,
rebindCallback: contentEditingHelper.reBindChangedProperties($scope.model.entity, err.data)
});
$scope.busy = false;
deferred.reject(err);
init($scope.content);
});
}
else {
$scope.busy = false;
deferred.reject();
}
return deferred.promise;
}
$scope.filterTabs = function(entity, blackList){
if(blackList){
_.each(entity.tabs, function(tab){
tab.hide = _.contains(blackList, tab.alias);
});
}
return entity;
};
$scope.saveAndPublish = function() {
$scope.submit($scope.model.entity);
$scope.sendToPublish = function () {
performSave({ saveMethod: contentResource.sendToPublish, statusMessage: "Sending..." });
};
$scope.saveAndPublish = function() {
performSave({ saveMethod: contentResource.publish, statusMessage: "Publishing..." })
.then(function(content){
if(dialogOptions.closeOnSave){
$scope.submit(content);
}
});
$scope.saveAndPublish = function () {
performSave({ saveMethod: contentResource.publish, statusMessage: "Publishing..." });
};
$scope.save = function () {
performSave({ saveMethod: contentResource.save, statusMessage: "Saving..." })
.then(function(content){
if(dialogOptions.closeOnSave){
$scope.submit(content);
}
});
performSave({ saveMethod: contentResource.save, statusMessage: "Saving..." });
};
// this method is called for all action buttons and then we proxy based on the btn definition
$scope.performAction = function (btn) {
if (!btn || !angular.isFunction(btn.handler)) {
throw "btn.handler must be a function reference";
}
if (!$scope.busy) {
btn.handler.apply(this);
}
};
//dispose the file manager instance
$scope.$on('$destroy', function () {
fileManagerInstance.clearFiles();
});
}

View File

@@ -8,20 +8,19 @@
<div class="umb-panel-header">
<umb-content-name
placeholder="@placeholders_entername"
ng-model="model.entity.name"/>
ng-model="content.name"/>
</div>
<div class="umb-panel-body with-footer">
<div id="tab{{tab.id}}" ng-repeat="tab in model.entity.tabs">
<div class="umb-pane" ng-if="!tab.hide">
<h5>{{tab.label}} - {{tab.alias}}</h5>
<umb-property
property="property"
ng-repeat="property in tab.properties">
<umb-editor model="property"></umb-editor>
</umb-property>
</div>
</div>
<div id="tab{{tab.id}}" ng-repeat="tab in content.tabs">
<div class="umb-pane" ng-if="!tab.hide">
<h5>{{tab.label}} - {{tab.alias}}</h5>
<umb-property property="property"
ng-repeat="property in tab.properties">
<umb-editor model="property"></umb-editor>
</umb-property>
</div>
</div>
</div>

View File

@@ -14,53 +14,23 @@ function ContentEditController($scope, $rootScope, $routeParams, $q, $timeout, $
$scope.currentSection = appState.getSectionState("currentSection");
$scope.currentNode = null; //the editors affiliated node
$scope.isNew = $routeParams.create;
function init(content) {
//This sets up the action buttons based on what permissions the user has.
//The allowedActions parameter contains a list of chars, each represents a button by permission so
//here we'll build the buttons according to the chars of the user.
function configureButtons(content) {
//reset
$scope.subButtons = [];
//This is the ideal button order but depends on circumstance, we'll use this array to create the button list
// Publish, SendToPublish, Save
var buttonOrder = ["U", "H", "A"];
//Create the first button (primary button)
//We cannot have the Save or SaveAndPublish buttons if they don't have create permissions when we are creating a new item.
if (!$routeParams.create || _.contains(content.allowedActions, "C")) {
for (var b in buttonOrder) {
if (_.contains(content.allowedActions, buttonOrder[b])) {
$scope.defaultButton = createButtonDefinition(buttonOrder[b]);
break;
}
var buttons = contentEditingHelper.configureContentEditorButtons({
create: $routeParams.create,
content: content,
methods: {
saveAndPublish: $scope.saveAndPublish,
sendToPublish: $scope.sendToPublish,
save: $scope.save,
unPublish: $scope.unPublish
}
}
});
$scope.defaultButton = buttons.defaultButton;
$scope.subButtons = buttons.subButtons;
//Now we need to make the drop down button list, this is also slightly tricky because:
//We cannot have any buttons if there's no default button above.
//We cannot have the unpublish button (Z) when there's no publish permission.
//We cannot have the unpublish button (Z) when the item is not published.
if ($scope.defaultButton) {
//get the last index of the button order
var lastIndex = _.indexOf(buttonOrder, $scope.defaultButton.letter);
//add the remaining
for (var i = lastIndex + 1; i < buttonOrder.length; i++) {
if (_.contains(content.allowedActions, buttonOrder[i])) {
$scope.subButtons.push(createButtonDefinition(buttonOrder[i]));
}
}
//if we are not creating, then we should add unpublish too,
// so long as it's already published and if the user has access to publish
if (!$routeParams.create) {
if (content.publishDate && _.contains(content.allowedActions, "U")) {
$scope.subButtons.push(createButtonDefinition("Z"));
}
}
}
editorState.set($scope.content);
//We fetch all ancestors of the node to generate the footer breadcrump navigation
if (!$routeParams.create) {
@@ -72,51 +42,6 @@ function ContentEditController($scope, $rootScope, $routeParams, $q, $timeout, $
}
}
function createButtonDefinition(ch) {
switch (ch) {
case "U":
//publish action
keyboardService.bind("ctrl+p", $scope.saveAndPublish);
return {
letter: ch,
labelKey: "buttons_saveAndPublish",
handler: $scope.saveAndPublish,
hotKey: "ctrl+p"
};
case "H":
//send to publish
keyboardService.bind("ctrl+p", $scope.sendToPublish);
return {
letter: ch,
labelKey: "buttons_saveToPublish",
handler: $scope.sendToPublish,
hotKey: "ctrl+p"
};
case "A":
//save
keyboardService.bind("ctrl+s", $scope.save);
return {
letter: ch,
labelKey: "buttons_save",
handler: $scope.save,
hotKey: "ctrl+s"
};
case "Z":
//unpublish
keyboardService.bind("ctrl+u", $scope.unPublish);
return {
letter: ch,
labelKey: "content_unPublish",
handler: $scope.unPublish
};
default:
return null;
}
}
/** Syncs the content item to it's tree node - this occurs on first load and after saving */
function syncTreeNode(content, path, initialLoad) {
@@ -140,54 +65,26 @@ function ContentEditController($scope, $rootScope, $routeParams, $q, $timeout, $
}
}
/** This is a helper method to reduce the amount of code repitition for actions: Save, Publish, SendToPublish */
// 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.busy = true;
if (formHelper.submitForm({ scope: $scope, statusMessage: args.statusMessage })) {
args.saveMethod($scope.content, $routeParams.create, fileManager.getFiles())
.then(function (data) {
formHelper.resetForm({ scope: $scope, notifications: data.notifications });
contentEditingHelper.handleSuccessfulSave({
scope: $scope,
savedContent: data,
rebindCallback: contentEditingHelper.reBindChangedProperties($scope.content, data)
});
editorState.set($scope.content);
$scope.busy = false;
configureButtons(data);
syncTreeNode($scope.content, data.path);
deferred.resolve(data);
}, function (err) {
contentEditingHelper.handleSaveError({
redirectOnFailure: true,
err: err,
rebindCallback: contentEditingHelper.reBindChangedProperties($scope.content, err.data)
});
editorState.set($scope.content);
$scope.busy = false;
deferred.reject(err);
});
}
else {
contentEditingHelper.contentEditorPerformSave({
fileManager: fileManager,
statusMessage: args.statusMessage,
saveMethod: args.saveMethod,
scope: $scope,
content: $scope.content
}).then(function (data) {
//success
$scope.busy = false;
deferred.reject();
}
return deferred.promise;
init($scope.content);
syncTreeNode($scope.content, data.path);
}, function (err) {
//error
$scope.busy = false;
if (err) {
editorState.set($scope.content);
}
});
}
function resetLastListPageNumber(content) {
@@ -206,9 +103,7 @@ function ContentEditController($scope, $rootScope, $routeParams, $q, $timeout, $
$scope.loaded = true;
$scope.content = data;
editorState.set($scope.content);
configureButtons($scope.content);
init($scope.content);
resetLastListPageNumber($scope.content);
});
@@ -226,9 +121,7 @@ function ContentEditController($scope, $rootScope, $routeParams, $q, $timeout, $
: "/content/content/edit/" + data.parentId;
}
editorState.set($scope.content);
configureButtons($scope.content);
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
@@ -258,9 +151,7 @@ function ContentEditController($scope, $rootScope, $routeParams, $q, $timeout, $
rebindCallback: contentEditingHelper.reBindChangedProperties($scope.content, data)
});
editorState.set($scope.content);
configureButtons(data);
init($scope.content);
syncTreeNode($scope.content, data.path);
@@ -301,7 +192,7 @@ function ContentEditController($scope, $rootScope, $routeParams, $q, $timeout, $
};
/** this method is called for all action buttons and then we proxy based on the btn definition */
// this method is called for all action buttons and then we proxy based on the btn definition
$scope.performAction = function (btn) {
if (!btn || !angular.isFunction(btn.handler)) {

View File

@@ -68,8 +68,7 @@ angular.module('umbraco')
};
$scope.edit = function (node) {
dialogService.open(
{
dialogService.open({
template: "views/common/dialogs/content/edit.html",
id: node.id,
closeOnSave:true,

View File

@@ -15,7 +15,9 @@
<i class="{{node.icon}} hover-hide"></i>
{{node.name}}
</a>
<br/><small><a href ng-click="edit(node)">Edit</a></small>
<div>
<small><a href ng-click="edit(node)">Edit</a></small>
</div>
</li>
</ul>