Fixed up creating content so that after creation it re-routes back in to edit mode so we don't just keep the 'create' flag. This caused some issues with validation because after creation the edit page needs to re-bind the entity from the server and previous to now that means that the server validation collection would have been cleared. We now persist the collection and it's up to the controller to re-bind if necessary.

This commit is contained in:
Shannon
2013-07-18 11:45:45 +10:00
parent 6f4a7f06a5
commit 4cd0489540
7 changed files with 297 additions and 169 deletions

View File

@@ -25,10 +25,10 @@ function valPropertyMsg(serverValidationService) {
link: function (scope, element, attrs, formCtrl) {
//assign the form control to our isolated scope so we can watch it's values
scope.formCtrl = formCtrl;
scope.formCtrl = formCtrl;
//flags for use in the below closures
var showValidation = false;
//if there's any remaining errors in the server validation service then we should show them.
var showValidation = serverValidationService.items.length > 0;
var hasError = false;
//create properties on our custom scope so we can use it in our template
@@ -53,7 +53,7 @@ function valPropertyMsg(serverValidationService) {
hasError = true;
//update the validation message if we don't already have one assigned.
if (showValidation && scope.errorMsg === "") {
scope.errorMsg = serverValidationService.getPropertyError(scope.currentProperty, "");
scope.errorMsg = serverValidationService.getPropertyError(scope.currentProperty, "").errorMsg;
}
}
else {
@@ -71,7 +71,7 @@ function valPropertyMsg(serverValidationService) {
scope.$on("saving", function (ev, args) {
showValidation = true;
if (hasError && scope.errorMsg === "") {
scope.errorMsg = serverValidationService.getPropertyError(scope.currentProperty, "");
scope.errorMsg = serverValidationService.getPropertyError(scope.currentProperty, "").errorMsg;
}
else if (!hasError) {
scope.errorMsg = "";
@@ -94,7 +94,7 @@ function valPropertyMsg(serverValidationService) {
scope.$watch("currentProperty.value", function(newValue) {
if (formCtrl.$invalid) {
scope.errorMsg = "";
formCtrl.$setValidity('valPropertyMsg', true);
formCtrl.$setValidity('valPropertyMsg', true);
}
});
@@ -102,16 +102,14 @@ function valPropertyMsg(serverValidationService) {
// NOTE: we pass in "" in order to listen for all validation changes to the content property, not for
// validation changes to fields in the property this is because some server side validators may not
// return the field name for which the error belongs too, just the property for which it belongs.
// It's important to note that we need to subscribe to server validation changes here because we always must
// indicate that a content property is invalid at the property level since developers may not actually implement
// the correct field validation in their property editors.
serverValidationService.subscribe(scope.currentProperty, "", function (isValid, propertyErrors, allErrors) {
hasError = !isValid;
if (hasError) {
//set the error message to the server message
scope.errorMsg = propertyErrors[0].errorMsg;
//now that we've used the server validation message, we need to remove it from the
//error collection... it is a 'one-time' usage so that when the field is invalidated
//again, the message we display is the client side message.
//NOTE: 'this' in the subscribe callback context is the validation manager object.
this.removePropertyError(scope.currentProperty);
scope.errorMsg = propertyErrors[0].errorMsg;
//flag that the current validator is invalid
formCtrl.$setValidity('valPropertyMsg', false);
}

View File

@@ -7,19 +7,27 @@
* This is used because we don't want to show validation messages until after the form is submitted and then reset
* the process when the form is successful. We do this by listening to the current controller's saving and saved events.
**/
function valShowValidation() {
function valShowValidation(serverValidationService) {
return {
require: "ngController",
restrict: "A",
link: function (scope, element, attr, ctrl) {
//we should show validation if there are any msgs in the server validation collection
if (serverValidationService.items.length > 0) {
element.addClass("show-validation");
}
//listen for the forms saving event
scope.$on("saving", function (ev, args) {
element.addClass("show-validation");
});
//listen for the forms saved event
scope.$on("saved", function (ev, args) {
element.removeClass("show-validation");
});
}
};
}

View File

@@ -1,4 +1,4 @@
function valToggleMsg() {
function valToggleMsg(serverValidationService) {
return {
require: "^form",
restrict: "A",
@@ -15,8 +15,8 @@ function valToggleMsg() {
throw "valToggleMsg requires that the attribute valMsgFor exists on the element";
}
//create a flag for us to be able to reference in the below closures for watching.
var showValidation = false;
//if there's any remaining errors in the server validation service then we should show them.
var showValidation = serverValidationService.items.length > 0;
var hasError = false;
//add a watch to the validator for the value (i.e. myForm.value.$error.required )
@@ -29,7 +29,7 @@ function valToggleMsg() {
element.hide();
}
});
scope.$on("saving", function(ev, args) {
showValidation = true;
if (hasError) {

View File

@@ -1,127 +1,127 @@
angular.module('umbraco.mocks').
factory('mocksUtils', ['$cookieStore', function($cookieStore) {
'use strict';
//by default we will perform authorization
var doAuth = true;
return {
/** Creats a mock content object */
getMockContent: function(id) {
var node = {
name: "My content with id: " + id,
updateDate: new Date(),
publishDate: new Date(),
id: id,
parentId: 1234,
icon: "icon-file-alt",
owner: { name: "Administrator", id: 0 },
updater: { name: "Per Ploug Krogslund", id: 1 },
tabs: [
{
label: "Child documents",
alias: "tab00",
id: 0,
active: true,
properties: [
{ alias: "list", label: "List", view: "listview", value: "", hideLabel: true }
]
},
{
label: "Content",
alias: "tab01",
id: 1,
properties: [
{ alias: "bodyText", label: "Body Text", description: "Here you enter the primary article contents", view: "rte", value: "<p>askjdkasj lasjd</p>" },
{ alias: "textarea", label: "textarea", view: "textarea", value: "ajsdka sdjkds", config: { rows: 4 } },
{ alias: "map", label: "Map", view: "googlemaps", value: "37.4419,-122.1419", config: { mapType: "ROADMAP", zoom: 4 } },
{ alias: "media", label: "Media picker", view: "mediapicker", value: "" },
{ alias: "content", label: "Content picker", view: "contentpicker", value: "" }
]
},
{
label: "Sample Editor",
alias: "tab02",
id: 2,
properties: [
{ alias: "datepicker", label: "Datepicker", view: "datepicker", config: { rows: 7 } },
{ alias: "tags", label: "Tags", view: "tags", value: "" }
]
},
{
label: "Grid",
alias: "tab03",
id: 3,
properties: [
{ alias: "grid", label: "Grid", view: "grid", controller: "umbraco.grid", value: "test", hideLabel: true }
]
}, {
label: "WIP",
alias: "tab04",
id: 4,
properties: [
{
alias: "tes", label: "Stuff", view: "test", controller: "umbraco.embeddedcontent", value: "",
config: {
fields: [
{ alias: "embedded", label: "Embbeded", view: "textstring", value: "" },
{ alias: "embedded2", label: "Embbeded 2", view: "contentpicker", value: "" },
{ alias: "embedded3", label: "Embbeded 3", view: "textarea", value: "" },
{ alias: "embedded4", label: "Embbeded 4", view: "datepicker", value: "" }
]
}
}
]
}
]
};
return node;
},
/** generally used for unit tests, calling this will disable the auth check and always return true */
disableAuth: function() {
doAuth = false;
},
/** generally used for unit tests, calling this will enabled the auth check */
enabledAuth: function() {
doAuth = true;
},
/** Checks for our mock auth cookie, if it's not there, returns false */
checkAuth: function () {
if (doAuth) {
var mockAuthCookie = $cookieStore.get("mockAuthCookie");
if (!mockAuthCookie) {
return false;
}
return true;
}
else {
return true;
}
},
/** Creates/sets the auth cookie with a value indicating the user is now authenticated */
setAuth: function() {
//set the cookie for loging
$cookieStore.put("mockAuthCookie", "Logged in!");
},
urlRegex: function(url) {
url = url.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&");
return new RegExp("^" + url);
},
getParameterByName: function(url, name) {
name = name.replace(/[\[]/, "\\[").replace(/[\]]/, "\\]");
var regex = new RegExp("[\\?&]" + name + "=([^&#]*)"),
results = regex.exec(url);
return results == null ? "" : decodeURIComponent(results[1].replace(/\+/g, " "));
}
};
angular.module('umbraco.mocks').
factory('mocksUtils', ['$cookieStore', function($cookieStore) {
'use strict';
//by default we will perform authorization
var doAuth = true;
return {
/** Creats a mock content object */
getMockContent: function(id) {
var node = {
name: "My content with id: " + id,
updateDate: new Date(),
publishDate: new Date(),
id: id,
parentId: 1234,
icon: "icon-file-alt",
owner: { name: "Administrator", id: 0 },
updater: { name: "Per Ploug Krogslund", id: 1 },
tabs: [
{
label: "Child documents",
alias: "tab00",
id: 0,
active: true,
properties: [
{ alias: "list", label: "List", view: "listview", value: "", hideLabel: true }
]
},
{
label: "Content",
alias: "tab01",
id: 1,
properties: [
{ alias: "bodyText", label: "Body Text", description: "Here you enter the primary article contents", view: "rte", value: "<p>askjdkasj lasjd</p>" },
{ alias: "textarea", label: "textarea", view: "textarea", value: "ajsdka sdjkds", config: { rows: 4 } },
{ alias: "map", label: "Map", view: "googlemaps", value: "37.4419,-122.1419", config: { mapType: "ROADMAP", zoom: 4 } },
{ alias: "media", label: "Media picker", view: "mediapicker", value: "" },
{ alias: "content", label: "Content picker", view: "contentpicker", value: "" }
]
},
{
label: "Sample Editor",
alias: "tab02",
id: 2,
properties: [
{ alias: "datepicker", label: "Datepicker", view: "datepicker", config: { rows: 7 } },
{ alias: "tags", label: "Tags", view: "tags", value: "" }
]
},
{
label: "Grid",
alias: "tab03",
id: 3,
properties: [
{ alias: "grid", label: "Grid", view: "grid", controller: "umbraco.grid", value: "test", hideLabel: true }
]
}, {
label: "WIP",
alias: "tab04",
id: 4,
properties: [
{
alias: "tes", label: "Stuff", view: "test", controller: "umbraco.embeddedcontent", value: "",
config: {
fields: [
{ alias: "embedded", label: "Embbeded", view: "textstring", value: "" },
{ alias: "embedded2", label: "Embbeded 2", view: "contentpicker", value: "" },
{ alias: "embedded3", label: "Embbeded 3", view: "textarea", value: "" },
{ alias: "embedded4", label: "Embbeded 4", view: "datepicker", value: "" }
]
}
}
]
}
]
};
return node;
},
/** generally used for unit tests, calling this will disable the auth check and always return true */
disableAuth: function() {
doAuth = false;
},
/** generally used for unit tests, calling this will enabled the auth check */
enabledAuth: function() {
doAuth = true;
},
/** Checks for our mock auth cookie, if it's not there, returns false */
checkAuth: function () {
if (doAuth) {
var mockAuthCookie = $cookieStore.get("mockAuthCookie");
if (!mockAuthCookie) {
return false;
}
return true;
}
else {
return true;
}
},
/** Creates/sets the auth cookie with a value indicating the user is now authenticated */
setAuth: function() {
//set the cookie for loging
$cookieStore.put("mockAuthCookie", "Logged in!");
},
urlRegex: function(url) {
url = url.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&");
return new RegExp("^" + url);
},
getParameterByName: function(url, name) {
name = name.replace(/[\[]/, "\\[").replace(/[\]]/, "\\]");
var regex = new RegExp("[\\?&]" + name + "=([^&#]*)"),
results = regex.exec(url);
return results == null ? "" : decodeURIComponent(results[1].replace(/\+/g, " "));
}
};
}]);

View File

@@ -8,11 +8,84 @@
* is for user defined properties (called Properties) and the other is for field properties which are attached to the native
* model objects (not user defined). The methods below are named according to these rules: Properties vs Fields.
*/
function serverValidationService() {
function serverValidationService($timeout) {
var callbacks = [];
/** calls the callback specified with the errors specified, used internally */
function executeCallback(self, errorsForCallback, callback) {
callback.apply(self, [
false, //pass in a value indicating it is invalid
errorsForCallback, //pass in the errors for this item
self.items]); //pass in all errors in total
}
function getFieldErrors(self, fieldName) {
//find errors for this field name
return _.filter(self.items, function (item) {
return (item.propertyAlias === null && item.fieldName === fieldName);
});
}
function getPropertyErrors(self, contentProperty, fieldName) {
//find all errors for this property
return _.filter(self.items, function (item) {
return (item.propertyAlias === contentProperty.alias && item.fieldName === fieldName);
});
}
return {
executeAndClearAllSubscriptions: function() {
var self = this;
$timeout(function () {
for (var cb in callbacks) {
if (callbacks[cb].propertyAlias === null) {
//its a field error callback
var fieldErrors = getFieldErrors(self, callbacks[cb].fieldName);
if (fieldErrors.length > 0) {
executeCallback(self, fieldErrors, callbacks[cb].callback);
}
}
else {
//its a property error
var propErrors = getPropertyErrors(self, { alias: callbacks[cb].propertyAlias }, callbacks[cb].fieldName);
if (propErrors.length > 0) {
executeCallback(self, propErrors, callbacks[cb].callback);
}
}
}
////iterate all items, detect if the error is a field vs property error and then
//// execute any callbacks registered for that particular error.
//for (var i in self.items) {
// if (self.items[i].propertyAlias === null) {
// //its a field error
// var cbs1 = self.getFieldCallbacks(self.items[i].fieldName);
// for (var cb1 in cbs1) {
// executeCallback(self, self.items[i], cbs1[cb1].callback);
// }
// }
// else {
// //its a property error
// var cbs2 = self.getPropertyCallbacks({ alias: self.items[i].propertyAlias }, self.items[i].fieldName);
// for (var cb2 in cbs2) {
// executeCallback(self, self.items[i], cbs2[cb2].callback);
// }
// }
//}
//now that they are all executed, we're gonna clear all of the errors we have
self.clear();
});
},
/**
* @ngdoc function
* @name subscribe
@@ -25,13 +98,17 @@ function serverValidationService() {
* a particular field, otherwise we can only pinpoint that there is an error for a content property, not the
* property's specific field. This is used with the val-server directive in which the directive specifies the
* field alias to listen for.
* If contentProperty is null, then this subscription is for a field property (not a user defined property)
* If contentProperty is null, then this subscription is for a field property (not a user defined property).
* During the call to subscribe we will check if there are any current validation errors for the subscription and
* execute the specified callback.
*/
subscribe: function (contentProperty, fieldName, callback) {
if (!callback) {
return;
}
var self = this;
if (contentProperty === null) {
//don't add it if it already exists
var exists1 = _.find(callbacks, function (item) {
@@ -39,6 +116,17 @@ function serverValidationService() {
});
if (!exists1) {
callbacks.push({ propertyAlias: null, fieldName: fieldName, callback: callback });
////TODO: Figure out how the heck to clear the validation collection!!!!!!!!!!!!!!!!!!
////find errors for this callback and execute the callback after this current digest
//$timeout(function() {
// var fieldErrors = getFieldErrors(self, fieldName);
// if (fieldErrors.length > 0) {
// executeCallback(self, fieldErrors, callback);
// }
//});
}
}
else if (contentProperty !== undefined) {
@@ -48,6 +136,14 @@ function serverValidationService() {
});
if (!exists2) {
callbacks.push({ propertyAlias: contentProperty.alias, fieldName: fieldName, callback: callback });
////find errors for this callback and execute the callback after this current digest
//$timeout(function() {
// var propErrors = getPropertyErrors(self, contentProperty, fieldName);
// if (propErrors.length > 0) {
// executeCallback(self, propErrors, callback);
// }
//});
}
}
},
@@ -136,17 +232,12 @@ function serverValidationService() {
}
//find all errors for this item
var errorsForCallback = _.filter(this.items, function (item) {
return (item.propertyAlias === null && item.fieldName === fieldName);
});
var errorsForCallback = getFieldErrors(this, fieldName);
//we should now call all of the call backs registered for this error
var cbs = this.getFieldCallbacks(fieldName);
//call each callback for this error
for (var cb in cbs) {
cbs[cb].callback.apply(this, [
false, //pass in a value indicating it is invalid
errorsForCallback, //pass in the errors for this item
this.items]); //pass in all errors in total
executeCallback(this, errorsForCallback, cbs[cb].callback);
}
},
@@ -174,17 +265,12 @@ function serverValidationService() {
}
//find all errors for this item
var errorsForCallback = _.filter(this.items, function (item) {
return (item.propertyAlias === contentProperty.alias && item.fieldName === fieldName);
});
var errorsForCallback = getPropertyErrors(this, contentProperty, fieldName);
//we should now call all of the call backs registered for this error
var cbs = this.getPropertyCallbacks(contentProperty, fieldName);
//call each callback for this error
for (var cb in cbs) {
cbs[cb].callback.apply(this, [
false, //pass in a value indicating it is invalid
errorsForCallback, //pass in the errors for this item
this.items]); //pass in all errors in total
executeCallback(this, errorsForCallback, cbs[cb].callback);
}
},
@@ -218,7 +304,7 @@ function serverValidationService() {
* Clears all errors and notifies all callbacks that all server errros are now valid - used when submitting a form
*/
reset: function () {
this.items = [];
this.clear();
for (var cb in callbacks) {
callbacks[cb].callback.apply(this, [
true, //pass in a value indicating it is VALID
@@ -227,6 +313,19 @@ function serverValidationService() {
}
},
/**
* @ngdoc function
* @name clear
* @methodOf umbraco.services.serverValidationService
* @function
*
* @description
* Clears all errors
*/
clear: function() {
this.items = [];
},
/**
* @ngdoc function
* @name getPropertyError
@@ -242,7 +341,7 @@ function serverValidationService() {
return (item.propertyAlias === contentProperty.alias && (item.fieldName === fieldName || (fieldName === undefined || fieldName === "")));
});
//return generic property error message if the error doesn't exist
return err ? err : "Property has errors";
return err ? err : { errorMsg: "Property has errors" };
},
/**
@@ -260,7 +359,7 @@ function serverValidationService() {
return (item.propertyAlias === null && item.fieldName === fieldName);
});
//return generic property error message if the error doesn't exist
return err ? err : "Field has errors";
return err ? err : { errorMsg: "Field has errors" };
},
/**

View File

@@ -729,7 +729,6 @@ function contentEditingHelper($location, $routeParams, notificationsService, ser
//add to notifications
notificationsService.error("Validation", modelState[e][0]);
}
},
@@ -749,9 +748,15 @@ function contentEditingHelper($location, $routeParams, notificationsService, ser
if (err.status === 403) {
//now we need to look through all the validation errors
if (err.data && (err.data.ModelState)) {
this.handleValidationErrors(err.data, err.data.ModelState);
this.redirectToCreatedContent(err.data.id, err.data.ModelState);
if (!this.redirectToCreatedContent(err.data.id, err.data.ModelState)) {
//we are not redirecting because this is not new content, it is existing content. In this case
// we need to clear the server validation items. When we are creating new content we cannot clear
// the server validation items because we redirect and they need to persist until the validation is re-bound.
serverValidationService.clear();
}
//indicates we've handled the server result
return true;
@@ -795,6 +800,8 @@ function contentEditingHelper($location, $routeParams, notificationsService, ser
$location.search(null);
//change to new path
$location.path("/" + $routeParams.section + "/" + $routeParams.method + "/" + id);
//don't add a browser history for this
$location.replace();
return true;
}
return false;

View File

@@ -8,8 +8,13 @@
*/
function ContentEditController($scope, $routeParams, $location, contentResource, notificationsService, angularHelper, serverValidationService, contentEditingHelper) {
//get the data to show, scaffold for new or get existing
//This is important with our routing:
// * When we create something the id will be 'create' and a 'parentId' query string will be supplied so we know where to create it
// * After it is create there might be validation errors but the item has still be saved to the server, we cannot just change the route
// to the normal editing route for the new ID because our server validation errors will no long exists
if ($routeParams.create) {
//we are creating so get an empty content item
contentResource.getScaffold($routeParams.id, $routeParams.doctype)
.then(function(data) {
$scope.contentLoaded = true;
@@ -17,10 +22,18 @@ function ContentEditController($scope, $routeParams, $location, contentResource,
});
}
else {
//we are editing so get the content item from the server
contentResource.getById($routeParams.id)
.then(function(data) {
$scope.contentLoaded = true;
$scope.content = data;
//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.
serverValidationService.executeAndClearAllSubscriptions();
});
}
@@ -58,11 +71,10 @@ function ContentEditController($scope, $routeParams, $location, contentResource,
notificationsService.success("Published", "Content has been saved and published");
$scope.$broadcast("saved", { scope: $scope });
contentEditingHelper.redirectToCreatedContent(data.id);
contentEditingHelper.redirectToCreatedContent($scope.content.id);
}, function (err) {
$location.search(null);
//TODO: only update the content that has changed!
$scope.content = err.data;
//$scope.content = err.data;
contentEditingHelper.handleSaveError(err);
});
};
@@ -78,12 +90,16 @@ function ContentEditController($scope, $routeParams, $location, contentResource,
contentResource.saveContent(cnt, $routeParams.create, $scope.files)
.then(function (data) {
//TODO: only update the content that has changed!
$scope.content = data;
notificationsService.success("Saved", "Content has been saved");
$scope.$broadcast("saved", { scope: $scope });
contentEditingHelper.redirectToCreatedContent($scope.content.id);
}, function (err) {
//TODO: only update the content that has changed!
$scope.content = err.data;
//$scope.content = err.data;
contentEditingHelper.handleSaveError(err);
});