diff --git a/build/Build.bat b/build/Build.bat index d1335ea93e..ff11f0e7ab 100644 --- a/build/Build.bat +++ b/build/Build.bat @@ -1,5 +1,5 @@ @ECHO OFF -SET release=6.1.4 +SET release=7.0.0 SET comment= SET version=%release% @@ -20,7 +20,6 @@ echo This file is only here so that the containing folder will be included in th echo This file is only here so that the containing folder will be included in the NuGet package, it is safe to delete. > .\_BuildOutput\WebApp\usercontrols\dummy.txt echo This file is only here so that the containing folder will be included in the NuGet package, it is safe to delete. > .\_BuildOutput\WebApp\Views\Partials\dummy.txt echo This file is only here so that the containing folder will be included in the NuGet package, it is safe to delete. > .\_BuildOutput\WebApp\Views\MacroPartials\dummy.txt -echo This file is only here so that the containing folder will be included in the NuGet package, it is safe to delete. > .\_BuildOutput\WebApp\xslt\dummy.txt ..\src\.nuget\NuGet.exe pack NuSpecs\UmbracoCms.Core.nuspec -Version %version% ..\src\.nuget\NuGet.exe pack NuSpecs\UmbracoCms.nuspec -Version %version% diff --git a/build/Build.proj b/build/Build.proj index 5ceeadb141..6e9b2a453d 100644 --- a/build/Build.proj +++ b/build/Build.proj @@ -162,10 +162,21 @@ - + + + + + + + + + diff --git a/src/Umbraco.Core/Configuration/UmbracoVersion.cs b/src/Umbraco.Core/Configuration/UmbracoVersion.cs index 9863edfee3..e15e365ba4 100644 --- a/src/Umbraco.Core/Configuration/UmbracoVersion.cs +++ b/src/Umbraco.Core/Configuration/UmbracoVersion.cs @@ -5,7 +5,7 @@ namespace Umbraco.Core.Configuration { public class UmbracoVersion { - private static readonly Version Version = new Version("6.1.4"); + private static readonly Version Version = new Version("7.0.0"); /// /// Gets the current version of Umbraco. diff --git a/src/Umbraco.Core/Models/DataTypeDefinition.cs b/src/Umbraco.Core/Models/DataTypeDefinition.cs index b4b3f42422..f879510c00 100644 --- a/src/Umbraco.Core/Models/DataTypeDefinition.cs +++ b/src/Umbraco.Core/Models/DataTypeDefinition.cs @@ -146,6 +146,8 @@ namespace Umbraco.Core.Models } } + //NOTE: SD: Why do we have this ?? + /// /// Boolean indicating whether this entity is Trashed or not. /// diff --git a/src/Umbraco.Core/Persistence/UnitOfWork/PetaPocoUnitOfWork.cs b/src/Umbraco.Core/Persistence/UnitOfWork/PetaPocoUnitOfWork.cs index 4cdf4de85e..eb070ae4e6 100644 --- a/src/Umbraco.Core/Persistence/UnitOfWork/PetaPocoUnitOfWork.cs +++ b/src/Umbraco.Core/Persistence/UnitOfWork/PetaPocoUnitOfWork.cs @@ -93,31 +93,50 @@ namespace Umbraco.Core.Persistence.UnitOfWork /// public void Commit() { - using (var transaction = Database.GetTransaction()) - { - foreach (var operation in _operations.OrderBy(o => o.ProcessDate)) - { - switch (operation.Type) - { - case TransactionType.Insert: - operation.Repository.PersistNewItem(operation.Entity); - break; - case TransactionType.Delete: - operation.Repository.PersistDeletedItem(operation.Entity); - break; - case TransactionType.Update: - operation.Repository.PersistUpdatedItem(operation.Entity); - break; - } - } - transaction.Complete(); - } - - // Clear everything - _operations.Clear(); - _key = Guid.NewGuid(); + Commit(null); } + /// + /// Commits all batched changes within the scope of a PetaPoco transaction + /// + /// + /// Allows you to set a callback which is executed before the transaction is committed, allow you to add additional SQL + /// operations to the overall commit process after the queue has been processed. + /// + internal void Commit(Action transactionCompleting) + { + using (var transaction = Database.GetTransaction()) + { + foreach (var operation in _operations.OrderBy(o => o.ProcessDate)) + { + switch (operation.Type) + { + case TransactionType.Insert: + operation.Repository.PersistNewItem(operation.Entity); + break; + case TransactionType.Delete: + operation.Repository.PersistDeletedItem(operation.Entity); + break; + case TransactionType.Update: + operation.Repository.PersistUpdatedItem(operation.Entity); + break; + } + } + + //Execute the callback if there is one + if (transactionCompleting != null) + { + transactionCompleting(Database); + } + + transaction.Complete(); + } + + // Clear everything + _operations.Clear(); + _key = Guid.NewGuid(); + } + public object Key { get { return _key; } diff --git a/src/Umbraco.Core/PropertyEditors/PreValueEditor.cs b/src/Umbraco.Core/PropertyEditors/PreValueEditor.cs index c3917e2227..be006a71d7 100644 --- a/src/Umbraco.Core/PropertyEditors/PreValueEditor.cs +++ b/src/Umbraco.Core/PropertyEditors/PreValueEditor.cs @@ -17,7 +17,7 @@ namespace Umbraco.Core.PropertyEditors { public PreValueEditor() { - Fields = Enumerable.Empty(); + Fields = Enumerable.Empty(); } /// @@ -27,7 +27,7 @@ namespace Umbraco.Core.PropertyEditors /// If fields are specified then the master View and Validators will be ignored /// [JsonProperty("fields")] - public IEnumerable Fields { get; set; } - + public IEnumerable Fields { get; set; } + } } \ No newline at end of file diff --git a/src/Umbraco.Core/PropertyEditors/PropertyEditor.cs b/src/Umbraco.Core/PropertyEditors/PropertyEditor.cs index e86915bb49..ae71b5c7a6 100644 --- a/src/Umbraco.Core/PropertyEditors/PropertyEditor.cs +++ b/src/Umbraco.Core/PropertyEditors/PropertyEditor.cs @@ -74,12 +74,6 @@ namespace Umbraco.Core.PropertyEditors [JsonProperty("defaultConfig")] public virtual IDictionary DefaultPreValues { get; set; } - //TODO: Now we need to implement a couple of methods for saving the data for editors and pre-value editors - // generally we can handle that automatically in this base class but people should be allowed to override - // it so they can perform custom operations on saving the data. - // This also means that we should have methods for formatting the data upon retreival from the repository - // essentially we need serialization and deserialization methods to override. - /// /// Creates a value editor instance /// diff --git a/src/Umbraco.Core/PropertyEditors/RequiredValueValidator.cs b/src/Umbraco.Core/PropertyEditors/RequiredValueValidator.cs index 7990998d9e..e40ae002d6 100644 --- a/src/Umbraco.Core/PropertyEditors/RequiredValueValidator.cs +++ b/src/Umbraco.Core/PropertyEditors/RequiredValueValidator.cs @@ -15,11 +15,11 @@ namespace Umbraco.Core.PropertyEditors if (value == null) { - yield return new ValidationResult("Value cannot be null"); + yield return new ValidationResult("Value cannot be null", new[] { "value" }); } if (value.IsNullOrWhiteSpace()) { - yield return new ValidationResult("Value cannot be empty"); + yield return new ValidationResult("Value cannot be empty", new[] { "value" }); } } } diff --git a/src/Umbraco.Core/PropertyEditors/ValidatorBase.cs b/src/Umbraco.Core/PropertyEditors/ValidatorBase.cs index c75d1eb2d6..16bb4b8c6e 100644 --- a/src/Umbraco.Core/PropertyEditors/ValidatorBase.cs +++ b/src/Umbraco.Core/PropertyEditors/ValidatorBase.cs @@ -14,9 +14,12 @@ namespace Umbraco.Core.PropertyEditors /// /// Depending on what is being validated, this value can be a json structure representing an editor's model, it could be a single /// string representing an editor's model, this class structure is also used to validate pre-values and in that case this value - /// could be a json structure representing the entire pre-value model or it could ba a single value representing a pre-value field. + /// could be a json structure or a single value representing a pre-value field. + /// + /// + /// When validating a property editor value (not a pre-value), this is the current pre-values stored for the data type. + /// When validating a pre-value field, this is the name of that field. /// - /// The current pre-values stored for the data type /// The property editor instance that we are validating for /// public abstract IEnumerable Validate(string value, string preValues, PropertyEditor editor); diff --git a/src/Umbraco.Core/Services/DataTypeService.cs b/src/Umbraco.Core/Services/DataTypeService.cs index 3a1e2e8f46..e5a7002037 100644 --- a/src/Umbraco.Core/Services/DataTypeService.cs +++ b/src/Umbraco.Core/Services/DataTypeService.cs @@ -224,6 +224,8 @@ namespace Umbraco.Core.Services /// List of string values to save public void SavePreValues(int id, IEnumerable values) { + //TODO: Should we raise an event here since we are really saving values for the data type? + using (new WriteLock(Locker)) { using (var uow = _uowProvider.GetUnitOfWork()) @@ -252,6 +254,93 @@ namespace Umbraco.Core.Services } } + /// + /// Saves/updates the pre-values + /// + /// + /// + /// + /// We will actually just remove all pre-values and re-insert them in one transaction + /// + internal void SavePreValues(int id, PreValueCollection values) + { + //TODO: Should we raise an event here since we are really saving values for the data type? + + using (new WriteLock(Locker)) + { + using (var uow = _uowProvider.GetUnitOfWork()) + { + using (var transaction = uow.Database.GetTransaction()) + { + uow.Database.Execute("DELETE FROM cmsDataTypePreValues WHERE datatypeNodeId = @DataTypeId", new { DataTypeId = id }); + + var sortOrder = 1; + foreach (var value in PreValueCollection.AsDictionary(values)) + { + var dto = new DataTypePreValueDto + { + DataTypeNodeId = id, + Value = (string)value.Value, + SortOrder = sortOrder, + Alias = value.Key + }; + uow.Database.Insert(dto); + sortOrder++; + } + + transaction.Complete(); + } + } + } + } + + /// + /// This will save a data type and it's pre-values in one transaction + /// + /// + /// + /// + internal void SaveDataTypeAndPreValues(IDataTypeDefinition dataTypeDefinition, PreValueCollection values, int userId = 0) + { + if (Saving.IsRaisedEventCancelled(new SaveEventArgs(dataTypeDefinition), this)) + return; + + using (new WriteLock(Locker)) + { + var uow = (PetaPocoUnitOfWork)_uowProvider.GetUnitOfWork(); + using (var repository = _repositoryFactory.CreateDataTypeDefinitionRepository(uow)) + { + dataTypeDefinition.CreatorId = userId; + repository.AddOrUpdate(dataTypeDefinition); + + //complete the transaction, but run the delegate before the db transaction is finalized + uow.Commit(database => + { + //Execute this before the transaction is completed! + database.Execute("DELETE FROM cmsDataTypePreValues WHERE datatypeNodeId = @DataTypeId", new { DataTypeId = dataTypeDefinition.Id }); + + var sortOrder = 1; + foreach (var value in PreValueCollection.AsDictionary(values)) + { + var dto = new DataTypePreValueDto + { + DataTypeNodeId = dataTypeDefinition.Id, + Value = (string)value.Value, + SortOrder = sortOrder, + Alias = value.Key + }; + database.Insert(dto); + sortOrder++; + } + }); + + Saved.RaiseEvent(new SaveEventArgs(dataTypeDefinition, false), this); + } + } + + Audit.Add(AuditTypes.Save, string.Format("Save DataTypeDefinition performed by user"), userId, dataTypeDefinition.Id); + } + /// /// Deletes an /// diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/valshowvalidation.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/valshowvalidation.directive.js index 3cd94116b1..85a4748a85 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/valshowvalidation.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/valshowvalidation.directive.js @@ -8,7 +8,6 @@ **/ function valShowValidation(serverValidationManager) { return { - require: "ngController", restrict: "A", link: function (scope, element, attr, ctrl) { diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/datatype.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/datatype.resource.js index 291710f53a..89e23b5e31 100644 --- a/src/Umbraco.Web.UI.Client/src/common/resources/datatype.resource.js +++ b/src/Umbraco.Web.UI.Client/src/common/resources/datatype.resource.js @@ -43,13 +43,12 @@ function dataTypeResource($q, $http, umbDataFormatter, umbRequestHelper) { }, /** saves or updates a data type object */ - save: function (dataType, isNew) { + save: function (dataType, preValues, isNew) { + + var saveModel = umbDataFormatter.formatDataTypePostData(dataType, preValues, "save" + (isNew ? "New" : "")); + return umbRequestHelper.resourcePromise( - $http.post(umbRequestHelper.getApiUrl("dataTypeApiBaseUrl", "PostSave"), - { - //TODO: SD: I need to finish this on Monday! - action: "save" + (isNew ? "New" : "") - }), + $http.post(umbRequestHelper.getApiUrl("dataTypeApiBaseUrl", "PostSave"), saveModel), 'Failed to save data for data type id ' + dataType.id); } }; diff --git a/src/Umbraco.Web.UI.Client/src/common/services/contenteditinghelper.service.js b/src/Umbraco.Web.UI.Client/src/common/services/contenteditinghelper.service.js index bf480be7a3..10bb81b4d4 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/contenteditinghelper.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/contenteditinghelper.service.js @@ -38,14 +38,10 @@ function contentEditingHelper($location, $routeParams, notificationsService, ser * @description * re-binds all changed property values to the origContent object from the newContent object and returns an array of changed properties. */ - reBindChangedProperties: function (origContent, newContent) { + reBindChangedProperties: function (allOrigProps, allNewProps) { var changed = []; - //get a list of properties since they are contained in tabs - var allOrigProps = this.getAllProps(origContent); - var allNewProps = this.getAllProps(newContent); - function getNewProp(alias) { if (alias.startsWith("_umb_")) { return null; @@ -78,10 +74,8 @@ function contentEditingHelper($location, $routeParams, notificationsService, ser * It's worth noting that when a 403 occurs, the data is still saved just never published, though this depends on if the entity is a new * entity and whether or not the data fulfils the absolute basic requirements like having a mandatory Name. */ - handleValidationErrors: function (content, modelState) { - //get a list of properties since they are contained in tabs - var allProps = this.getAllProps(content); - + handleValidationErrors: function (allProps, modelState) { + //find the content property for the current error, for use in the loop below function findContentProp(props, propAlias) { return _.find(props, function (item) { @@ -132,21 +126,36 @@ function contentEditingHelper($location, $routeParams, notificationsService, ser * @description * A function to handle what happens when we have validation issues from the server side */ - handleSaveError: function (err, scope) { + handleSaveError: function (args) { + + if (!args.err) { + throw "args.err cannot be null"; + } + if (!args.allNewProps && !angular.isArray(args.allNewProps)) { + throw "args.allNewProps must be a valid array"; + } + if (!args.allOrigProps && !angular.isArray(args.allOrigProps)) { + throw "args.allOrigProps must be a valid array"; + } + //When the status is a 403 status, we have validation errors. //Otherwise the error is probably due to invalid data (i.e. someone mucking around with the ids or something). //Or, some strange server error - if (err.status === 403) { + if (args.err.status === 403) { //now we need to look through all the validation errors - if (err.data && (err.data.modelState)) { + if (args.err.data && (args.err.data.ModelState)) { + + this.handleValidationErrors(args.allNewProps, args.err.data.ModelState); - this.handleValidationErrors(err.data, err.data.modelState); - - if (!this.redirectToCreatedContent(err.data.id, err.data.modelState)) { + if (!args.redirectOnFailure || !this.redirectToCreatedContent(args.err.data.id, args.err.data.ModelState)) { //we are not redirecting because this is not new content, it is existing content. In this case // we need to detect what properties have changed and re-bind them with the server data. Then we need // to re-bind any server validation errors after the digest takes place. - this.reBindChangedProperties(scope.content, err.data); + + if (args.rebindCallback && angular.isFunction(args.rebindCallback)) { + args.rebindCallback(); + } + serverValidationManager.executeAndClearAllSubscriptions(); } @@ -154,11 +163,11 @@ function contentEditingHelper($location, $routeParams, notificationsService, ser return true; } else { - dialogService.ysodDialog(err); + dialogService.ysodDialog(args.err); } } else { - dialogService.ysodDialog(err); + dialogService.ysodDialog(args.err); } return false; @@ -166,7 +175,7 @@ function contentEditingHelper($location, $routeParams, notificationsService, ser /** * @ngdoc function - * @name umbraco.services.contentEditingHelper#handleSaveError + * @name umbraco.services.contentEditingHelper#handleSuccessfulSave * @methodOf umbraco.services.contentEditingHelper * @function * @@ -195,7 +204,7 @@ function contentEditingHelper($location, $routeParams, notificationsService, ser } args.scope.$broadcast("saved", { scope: args.scope }); - if (!this.redirectToCreatedContent(args.scope.content.id)) { + if (!this.redirectToCreatedContent(args.newContent.id)) { //we are not redirecting because this is not new content, it is existing content. In this case // we need to detect what properties have changed and re-bind them with the server data. diff --git a/src/Umbraco.Web.UI.Client/src/common/services/menuactions.service.js b/src/Umbraco.Web.UI.Client/src/common/services/menuactions.service.js index 302834194c..28bc971096 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/menuactions.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/menuactions.service.js @@ -8,13 +8,13 @@ * @description * Defines the methods that are called when menu items declare only an action to execute */ -function umbracoMenuActions($q, treeService, $location) { +function umbracoMenuActions($q, treeService, $location, navigationService) { return { /** * @ngdoc method - * @name umbraco.services.umbracoMenuActions#RefreshNodeMenuItem + * @name umbraco.services.umbracoMenuActions#RefreshNode * @methodOf umbraco.services.umbracoMenuActions * @function * @@ -24,7 +24,7 @@ function umbracoMenuActions($q, treeService, $location) { * @param {object} args.treeNode The tree node * @param {object} args.section The current section */ - "RefreshNodeMenuItem": function (args) { + "RefreshNode": function (args) { treeService.loadNodeChildren({ node: args.treeNode, section: args.section }); }, @@ -41,9 +41,13 @@ function umbracoMenuActions($q, treeService, $location) { * @param {object} args.section The current section */ "CreateChildEntity": function (args) { + + navigationService.hideNavigation(); + var route = "/" + args.section + "/" + treeService.getTreeAlias(args.treeNode) + "/edit/" + args.treeNode.id; //change to new path $location.path(route).search({ create: true }); + } }; } diff --git a/src/Umbraco.Web.UI.Client/src/common/services/notifications.service.js b/src/Umbraco.Web.UI.Client/src/common/services/notifications.service.js index 643b276a87..d0ae580921 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/notifications.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/notifications.service.js @@ -74,9 +74,7 @@ angular.module('umbraco.services') if (!args.header) { throw "args.header cannot be null"; } - if (!args.message) { - throw "args.message cannot be null"; - } + switch(args.type) { case 0: //save diff --git a/src/Umbraco.Web.UI.Client/src/common/services/umbrequesthelper.service.js b/src/Umbraco.Web.UI.Client/src/common/services/umbrequesthelper.service.js index a260ce904e..9b52da35ad 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/umbrequesthelper.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/umbrequesthelper.service.js @@ -85,7 +85,7 @@ function umbRequestHelper($http, $q, umbDataFormatter, angularHelper, dialogServ * In both of the above, the successCallback must accept these parameters: data, status, headers, config * If using the errorCallback it must accept these parameters: data, status, headers, config * The success callback must return the data which will be resolved by the deferred object. - * The error callback must return an object containing: {errorMsg: errorMessage, data: originalData } + * The error callback must return an object containing: {errorMsg: errorMessage, data: originalData, status: status } */ resourcePromise: function (httpPromise, opts) { var deferred = $q.defer(); @@ -101,7 +101,8 @@ function umbRequestHelper($http, $q, umbDataFormatter, angularHelper, dialogServ return { //NOTE: the default error message here should never be used based on the above docs! errorMsg: (angular.isString(opts) ? opts : 'An error occurred!'), - data: data + data: data, + status: status }; } @@ -137,7 +138,8 @@ function umbRequestHelper($http, $q, umbDataFormatter, angularHelper, dialogServ //return an error object including the error message for UI deferred.reject({ errorMsg: result.errorMsg, - data: result.data + data: result.data, + status: result.status }); } diff --git a/src/Umbraco.Web.UI.Client/src/common/services/util.service.js b/src/Umbraco.Web.UI.Client/src/common/services/util.service.js index 96c1f22497..5556e3403f 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/util.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/util.service.js @@ -284,6 +284,25 @@ angular.module('umbraco.services').factory('umbImageHelper', umbImageHelper); function umbDataFormatter() { return { + /** formats the display model used to display the data type to the model used to save the data type */ + formatDataTypePostData: function(displayModel, preValues, action) { + var saveModel = { + id: displayModel.id, + name: displayModel.name, + selectedEditor: displayModel.selectedEditor, + //set the action on the save model + action: action, + preValues: [] + }; + for (var i = 0; i < preValues.length; i++) { + saveModel.preValues.push({ + key: preValues[i].alias, + value: preValues[i].value + }); + } + return saveModel; + }, + /** formats the display model used to display the content to the model used to save the content */ formatContentPostData: function (displayModel, action) { //NOTE: the display model inherits from the save model so we can in theory just post up the display model but diff --git a/src/Umbraco.Web.UI.Client/src/views/content/content.edit.controller.js b/src/Umbraco.Web.UI.Client/src/views/content/content.edit.controller.js index f87f02896f..e193e1f3f5 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/content.edit.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/content/content.edit.controller.js @@ -1,96 +1,114 @@ -/** - * @ngdoc controller - * @name Umbraco.Editors.Content.EditController - * @function - * - * @description - * The controller for the content editor - */ -function ContentEditController($scope, $routeParams, $location, contentResource, notificationsService, angularHelper, serverValidationManager, contentEditingHelper) { - - if ($routeParams.create) { - //we are creating so get an empty content item - contentResource.getScaffold($routeParams.id, $routeParams.doctype) - .then(function(data) { - $scope.loaded = true; - $scope.content = data; - }); - } - else { - //we are editing so get the content item from the server - contentResource.getById($routeParams.id) - .then(function(data) { - $scope.loaded = 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. - serverValidationManager.executeAndClearAllSubscriptions(); - }); - } - - $scope.files = []; - $scope.addFiles = function (propertyId, files) { - //this will clear the files for the current property and then add the new ones for the current property - $scope.files = _.reject($scope.files, function (item) { - return item.id == propertyId; - }); - for (var i = 0; i < files.length; i++) { - //save the file object to the scope's files collection - $scope.files.push({ id: propertyId, file: files[i] }); - } - }; - - //ensure there is a form object assigned. - var currentForm = angularHelper.getRequiredCurrentForm($scope); - - //TODO: Need to figure out a way to share the saving and event broadcasting with all editors! - - $scope.saveAndPublish = function (cnt) { - $scope.$broadcast("saving", { scope: $scope }); - - //don't continue if the form is invalid - if (currentForm.$invalid) return; - - serverValidationManager.reset(); - - contentResource.publish(cnt, $routeParams.create, $scope.files) - .then(function (data) { - contentEditingHelper.handleSuccessfulSave({ - scope: $scope, - newContent: data, - rebindCallback: contentEditingHelper.reBindChangedProperties($scope.content, data) - }); - - }, function (err) { - contentEditingHelper.handleSaveError(err, $scope); - }); - }; - - $scope.save = function (cnt) { - $scope.$broadcast("saving", { scope: $scope }); - - //don't continue if the form is invalid - if (currentForm.$invalid) return; - - serverValidationManager.reset(); - - contentResource.save(cnt, $routeParams.create, $scope.files) - .then(function (data) { - - contentEditingHelper.handleSuccessfulSave({ - scope: $scope, - newContent: data, - rebindCallback: contentEditingHelper.reBindChangedProperties($scope.content, data) - }); - - }, function (err) { - contentEditingHelper.handleSaveError(err, $scope); - }); - }; - -} - -angular.module("umbraco").controller("Umbraco.Editors.Content.EditController", ContentEditController); +/** + * @ngdoc controller + * @name Umbraco.Editors.Content.EditController + * @function + * + * @description + * The controller for the content editor + */ +function ContentEditController($scope, $routeParams, $location, contentResource, notificationsService, angularHelper, serverValidationManager, contentEditingHelper) { + + if ($routeParams.create) { + //we are creating so get an empty content item + contentResource.getScaffold($routeParams.id, $routeParams.doctype) + .then(function(data) { + $scope.loaded = true; + $scope.content = data; + }); + } + else { + //we are editing so get the content item from the server + contentResource.getById($routeParams.id) + .then(function(data) { + $scope.loaded = 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. + serverValidationManager.executeAndClearAllSubscriptions(); + }); + } + + $scope.files = []; + $scope.addFiles = function (propertyId, files) { + //this will clear the files for the current property and then add the new ones for the current property + $scope.files = _.reject($scope.files, function (item) { + return item.id == propertyId; + }); + for (var i = 0; i < files.length; i++) { + //save the file object to the scope's files collection + $scope.files.push({ id: propertyId, file: files[i] }); + } + }; + + //TODO: Need to figure out a way to share the saving and event broadcasting with all editors! + + $scope.saveAndPublish = function () { + $scope.$broadcast("saving", { scope: $scope }); + + var currentForm = angularHelper.getRequiredCurrentForm($scope); + + //don't continue if the form is invalid + if (currentForm.$invalid) return; + + serverValidationManager.reset(); + + contentResource.publish($scope.content, $routeParams.create, $scope.files) + .then(function (data) { + + contentEditingHelper.handleSuccessfulSave({ + scope: $scope, + newContent: data, + rebindCallback: contentEditingHelper.reBindChangedProperties( + contentEditingHelper.getAllProps($scope.content), + contentEditingHelper.getAllProps(data)) + }); + + }, function (err) { + + var allNewProps = contentEditingHelper.getAllProps(err.data); + var allOrigProps = contentEditingHelper.getAllProps($scope.content); + + contentEditingHelper.handleSaveError({ + err: err, + redirectOnFailure: true, + allNewProps: allNewProps, + allOrigProps: contentEditingHelper.getAllProps($scope.content), + rebindCallback: contentEditingHelper.reBindChangedProperties(allOrigProps, allNewProps) + }); + }); + }; + + $scope.save = function () { + $scope.$broadcast("saving", { scope: $scope }); + + var currentForm = angularHelper.getRequiredCurrentForm($scope); + + //don't continue if the form is invalid + if (currentForm.$invalid) return; + + serverValidationManager.reset(); + + contentResource.save($scope.content, $routeParams.create, $scope.files) + .then(function (data) { + + contentEditingHelper.handleSuccessfulSave({ + scope: $scope, + newContent: data, + rebindCallback: contentEditingHelper.reBindChangedProperties(scope.content, data) + }); + + }, function (err) { + contentEditingHelper.handleSaveError({ + err: err, + allNewProps: contentEditingHelper.getAllProps(err.data), + allOrigProps: contentEditingHelper.getAllProps($scope.content) + }); + }); + }; + +} + +angular.module("umbraco").controller("Umbraco.Editors.Content.EditController", ContentEditController); diff --git a/src/Umbraco.Web.UI.Client/src/views/content/edit.html b/src/Umbraco.Web.UI.Client/src/views/content/edit.html index fc32829941..64835d40c3 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/edit.html +++ b/src/Umbraco.Web.UI.Client/src/views/content/edit.html @@ -1,5 +1,8 @@ -
- + + @@ -16,7 +19,7 @@ diff --git a/src/Umbraco.Web.UI.Client/src/views/datatype/datatype.edit.controller.js b/src/Umbraco.Web.UI.Client/src/views/datatype/datatype.edit.controller.js index caaa332b10..206f4c4a50 100644 --- a/src/Umbraco.Web.UI.Client/src/views/datatype/datatype.edit.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/datatype/datatype.edit.controller.js @@ -8,22 +8,7 @@ */ function DataTypeEditController($scope, $routeParams, $location, dataTypeResource, notificationsService, angularHelper, serverValidationManager, contentEditingHelper) { - //set up the standard data type props - function createDisplayProps() { - $scope.properties = { - selectedEditor: { - alias: "selectedEditor", - description: "Select a property editor", - label: "Property editor" - }, - selectedEditorId: { - alias: "selectedEditorId", - label: "Property editor GUID" - } - }; - } - - //setup the pre-values as props + //method used to configure the pre-values when we retreive them from the server function createPreValueProps(preVals) { $scope.preValues = []; for (var i = 0; i < preVals.length; i++) { @@ -33,10 +18,27 @@ function DataTypeEditController($scope, $routeParams, $location, dataTypeResourc description: preVals[i].description, label: preVals[i].label, view: preVals[i].view, + value: preVals[i].value }); } } + //set up the standard data type props + $scope.properties = { + selectedEditor: { + alias: "selectedEditor", + description: "Select a property editor", + label: "Property editor" + }, + selectedEditorId: { + alias: "selectedEditorId", + label: "Property editor GUID" + } + }; + + //setup the pre-values as props + $scope.preValues = []; + if ($routeParams.create) { //we are creating so get an empty content item dataTypeResource.getScaffold($routeParams.id, $routeParams.doctype) @@ -44,7 +46,6 @@ function DataTypeEditController($scope, $routeParams, $location, dataTypeResourc $scope.loaded = true; $scope.preValuesLoaded = true; $scope.content = data; - createDisplayProps(); }); } else { @@ -54,7 +55,6 @@ function DataTypeEditController($scope, $routeParams, $location, dataTypeResourc $scope.loaded = true; $scope.preValuesLoaded = true; $scope.content = data; - createDisplayProps(); createPreValueProps($scope.content.preValues); //in one particular special case, after we've created a new item we redirect back to the edit @@ -65,9 +65,6 @@ function DataTypeEditController($scope, $routeParams, $location, dataTypeResourc }); } - //ensure there is a form object assigned. - var currentForm = angularHelper.getRequiredCurrentForm($scope); - $scope.$watch("content.selectedEditor", function (newVal, oldVal) { //when the value changes, we need to dynamically load in the new editor if (newVal && oldVal && newVal != oldVal) { @@ -81,15 +78,18 @@ function DataTypeEditController($scope, $routeParams, $location, dataTypeResourc } }); - $scope.save = function (cnt) { + $scope.save = function () { $scope.$broadcast("saving", { scope: $scope }); - + + //ensure there is a form object assigned. + var currentForm = angularHelper.getRequiredCurrentForm($scope); + //don't continue if the form is invalid if (currentForm.$invalid) return; serverValidationManager.reset(); - - dataTypeResource.save(cnt, $routeParams.create) + + dataTypeResource.save($scope.content, $scope.preValues, $routeParams.create) .then(function (data) { contentEditingHelper.handleSuccessfulSave({ @@ -101,7 +101,14 @@ function DataTypeEditController($scope, $routeParams, $location, dataTypeResourc }); }, function (err) { - contentEditingHelper.handleSaveError(err, $scope); + + //NOTE: in the case of data type values we are setting the orig/new props + // to be the same thing since that only really matters for content/media. + contentEditingHelper.handleSaveError({ + err: err, + allNewProps: $scope.preValues, + allOrigProps: $scope.preValues + }); }); }; diff --git a/src/Umbraco.Web.UI.Client/src/views/datatype/edit.html b/src/Umbraco.Web.UI.Client/src/views/datatype/edit.html index 87987bc464..3a4f3d2dda 100644 --- a/src/Umbraco.Web.UI.Client/src/views/datatype/edit.html +++ b/src/Umbraco.Web.UI.Client/src/views/datatype/edit.html @@ -1,5 +1,8 @@ - - + + @@ -13,7 +16,7 @@
- +
diff --git a/src/Umbraco.Web.UI.Client/src/views/media/edit.html b/src/Umbraco.Web.UI.Client/src/views/media/edit.html index 8f031bb0b0..a1121e7f32 100644 --- a/src/Umbraco.Web.UI.Client/src/views/media/edit.html +++ b/src/Umbraco.Web.UI.Client/src/views/media/edit.html @@ -1,5 +1,8 @@ - - + + @@ -9,7 +12,7 @@
- +
diff --git a/src/Umbraco.Web.UI.Client/src/views/media/media.edit.controller.js b/src/Umbraco.Web.UI.Client/src/views/media/media.edit.controller.js index 8559f2a988..18ccc0aaf9 100644 --- a/src/Umbraco.Web.UI.Client/src/views/media/media.edit.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/media/media.edit.controller.js @@ -42,30 +42,40 @@ function mediaEditController($scope, $routeParams, mediaResource, notificationsS $scope.files.push({ id: propertyId, file: files[i] }); } }; - - //ensure there is a form object assigned. - var currentForm = angularHelper.getRequiredCurrentForm($scope); - - $scope.save = function (cnt) { + + $scope.save = function () { $scope.$broadcast("saving", { scope: $scope }); + var currentForm = angularHelper.getRequiredCurrentForm($scope); //don't continue if the form is invalid if (currentForm.$invalid) return; serverValidationManager.reset(); - mediaResource.save(cnt, $routeParams.create, $scope.files) + mediaResource.save($scope.content, $routeParams.create, $scope.files) .then(function (data) { contentEditingHelper.handleSuccessfulSave({ scope: $scope, newContent: data, - rebindCallback: contentEditingHelper.reBindChangedProperties(scope.content, data) + rebindCallback: contentEditingHelper.reBindChangedProperties( + contentEditingHelper.getAllProps($scope.content), + contentEditingHelper.getAllProps(data)) }); }, function (err) { - contentEditingHelper.handleSaveError(err, $scope); + + var allNewProps = contentEditingHelper.getAllProps(err.data); + var allOrigProps = contentEditingHelper.getAllProps($scope.content); + + contentEditingHelper.handleSaveError({ + err: err, + redirectOnFailure: true, + allNewProps: allNewProps, + allOrigProps: contentEditingHelper.getAllProps($scope.content), + rebindCallback: contentEditingHelper.reBindChangedProperties(allOrigProps, allNewProps) + }); }); }; } diff --git a/src/Umbraco.Web.UI.Client/src/views/prevalueeditors/requiredfield.html b/src/Umbraco.Web.UI.Client/src/views/prevalueeditors/requiredfield.html index 621a0cb9d7..14c06d29d6 100644 --- a/src/Umbraco.Web.UI.Client/src/views/prevalueeditors/requiredfield.html +++ b/src/Umbraco.Web.UI.Client/src/views/prevalueeditors/requiredfield.html @@ -1,7 +1,9 @@ 
+ required + val-server="value" /> Required + {{propertyForm.requiredField.errorMsg}}
diff --git a/src/Umbraco.Web.UI.Client/test/unit/common/services/content-editing-helper.spec.js b/src/Umbraco.Web.UI.Client/test/unit/common/services/content-editing-helper.spec.js index 1778cd34c2..3aa47a7eb5 100644 --- a/src/Umbraco.Web.UI.Client/test/unit/common/services/content-editing-helper.spec.js +++ b/src/Umbraco.Web.UI.Client/test/unit/common/services/content-editing-helper.spec.js @@ -22,10 +22,14 @@ describe('contentEditingHelper tests', function () { data: content, status: 403 }; - err.data.modelState = {}; + err.data.ModelState = {}; //act - var handled = contentEditingHelper.handleSaveError(err, {content: content}); + var handled = contentEditingHelper.handleSaveError({ + err: err, + allNewProps: contentEditingHelper.getAllProps(content), + allOrigProps: contentEditingHelper.getAllProps(content) + }); //assert expect(handled).toBe(true); @@ -39,7 +43,11 @@ describe('contentEditingHelper tests', function () { }; //act - var handled = contentEditingHelper.handleSaveError(err, null); + var handled = contentEditingHelper.handleSaveError({ + err: err, + allNewProps: [], + allOrigProps: [] + }); //assert expect(handled).toBe(false); @@ -55,7 +63,11 @@ describe('contentEditingHelper tests', function () { }; //act - var handled = contentEditingHelper.handleSaveError(err, { content: content }); + var handled = contentEditingHelper.handleSaveError({ + err: err, + allNewProps: contentEditingHelper.getAllProps(content), + allOrigProps: contentEditingHelper.getAllProps(content) + }); //assert expect(handled).toBe(false); @@ -69,9 +81,10 @@ describe('contentEditingHelper tests', function () { //arrange var content = mocksUtils.getMockContent(1234); + var allProps = contentEditingHelper.getAllProps(content); //act - contentEditingHelper.handleValidationErrors(content, { Name: ["Required"] }); + contentEditingHelper.handleValidationErrors(allProps, { Name: ["Required"] }); //assert expect(serverValidationManager.items.length).toBe(1); @@ -84,9 +97,10 @@ describe('contentEditingHelper tests', function () { //arrange var content = mocksUtils.getMockContent(1234); + var allProps = contentEditingHelper.getAllProps(content); //act - contentEditingHelper.handleValidationErrors(content, { "Property.bodyText": ["Required"] }); + contentEditingHelper.handleValidationErrors(allProps, { "Property.bodyText": ["Required"] }); //assert expect(serverValidationManager.items.length).toBe(1); @@ -99,9 +113,10 @@ describe('contentEditingHelper tests', function () { //arrange var content = mocksUtils.getMockContent(1234); + var allProps = contentEditingHelper.getAllProps(content); //act - contentEditingHelper.handleValidationErrors(content, { "Property.bodyText.value": ["Required"] }); + contentEditingHelper.handleValidationErrors(allProps, { "Property.bodyText.value": ["Required"] }); //assert expect(serverValidationManager.items.length).toBe(1); @@ -114,10 +129,11 @@ describe('contentEditingHelper tests', function () { //arrange var content = mocksUtils.getMockContent(1234); + var allProps = contentEditingHelper.getAllProps(content); //act contentEditingHelper.handleValidationErrors( - content, + allProps, { "Name": ["Required"], "UpdateDate": ["Invalid date"], @@ -202,7 +218,9 @@ describe('contentEditingHelper tests', function () { newContent.tabs[1].properties[2].value = "origValue4"; //act - var changed = contentEditingHelper.reBindChangedProperties(origContent, newContent); + var changed = contentEditingHelper.reBindChangedProperties( + contentEditingHelper.getAllProps(origContent), + contentEditingHelper.getAllProps(newContent)); //assert expect(changed.length).toBe(2); diff --git a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj index 1ae6479358..463e421a13 100644 --- a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj +++ b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj @@ -2627,9 +2627,9 @@ xcopy "$(ProjectDir)"..\packages\SqlServerCE.4.0.0.0\x86\*.* "$(TargetDir)x86\" True True - 6140 + 7000 / - http://localhost:6140 + http://localhost:7000 False False diff --git a/src/Umbraco.Web/Editors/DataTypeController.cs b/src/Umbraco.Web/Editors/DataTypeController.cs index 41dbed0c26..f84fd9b48a 100644 --- a/src/Umbraco.Web/Editors/DataTypeController.cs +++ b/src/Umbraco.Web/Editors/DataTypeController.cs @@ -3,12 +3,16 @@ using System.Collections.Generic; using System.Linq; using System.Net; using System.Web.Http; +using System.Web.Http.ModelBinding; using AutoMapper; using Umbraco.Core.Models; using Umbraco.Core.PropertyEditors; +using Umbraco.Core.Services; using Umbraco.Web.Models.ContentEditing; using Umbraco.Web.Mvc; +using Umbraco.Web.WebApi.Binders; using Umbraco.Web.WebApi.Filters; +using umbraco; using Constants = Umbraco.Core.Constants; namespace Umbraco.Web.Editors @@ -40,6 +44,12 @@ namespace Umbraco.Web.Editors return Mapper.Map(dataType); } + public DataTypeDisplay GetEmpty() + { + var dt = new DataTypeDefinition(-1, Guid.Empty); + return Mapper.Map(dt); + } + /// /// Returns the pre-values for the specified property editor /// @@ -55,5 +65,29 @@ namespace Umbraco.Web.Editors return propEd.PreValueEditor.Fields.Select(Mapper.Map); } + + //TODO: Generally there probably won't be file uploads for pre-values but we should allow them just like we do for the content editor + + /// + /// Saves the data type + /// + /// + /// + [DataTypeValidate] + public DataTypeDisplay PostSave(DataTypeSave dataType) + { + //If we've made it here, then everything has been wired up and validated by the attribute + + //finally we need to save the data type and it's pre-vals + var dtService = (DataTypeService) ApplicationContext.Services.DataTypeService; + var preVals = Mapper.Map(dataType.PreValues); + dtService.SaveDataTypeAndPreValues(dataType.PersistedDataType, preVals, (int)Security.CurrentUser.Id); + + var display = Mapper.Map(dataType.PersistedDataType); + display.AddSuccessNotification(ui.Text("speechBubbles", "dataTypeSaved"), ""); + + //now return the updated model + return display; + } } } \ No newline at end of file diff --git a/src/Umbraco.Web/Editors/DataTypeValidateAttribute.cs b/src/Umbraco.Web/Editors/DataTypeValidateAttribute.cs new file mode 100644 index 0000000000..1d6d2f5b12 --- /dev/null +++ b/src/Umbraco.Web/Editors/DataTypeValidateAttribute.cs @@ -0,0 +1,122 @@ +using System; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Web.Http; +using System.Web.Http.Controllers; +using System.Web.Http.Filters; +using AutoMapper; +using Umbraco.Core; +using Umbraco.Core.Models; +using Umbraco.Core.Models.EntityBase; +using Umbraco.Core.PropertyEditors; +using Umbraco.Core.Services; +using Umbraco.Web.Models.ContentEditing; + +namespace Umbraco.Web.Editors +{ + /// + /// An action filter that wires up the persisted entity of the DataTypeSave model and validates the whole request + /// + internal sealed class DataTypeValidateAttribute : ActionFilterAttribute + { + private readonly IDataTypeService _dataTypeService; + + public DataTypeValidateAttribute() + { + } + + public DataTypeValidateAttribute(IDataTypeService dataTypeService) + { + if (dataTypeService == null) throw new ArgumentNullException("dataTypeService"); + _dataTypeService = dataTypeService; + } + + private IDataTypeService DataTypeService + { + get { return _dataTypeService ?? ApplicationContext.Current.Services.DataTypeService; } + } + + public override void OnActionExecuting(HttpActionContext actionContext) + { + var dataType = (DataTypeSave)actionContext.ActionArguments["dataType"]; + + //Validate that the property editor exists + var propertyEditor = PropertyEditorResolver.Current.GetById(dataType.SelectedEditor); + if (propertyEditor == null) + { + var message = string.Format("Property editor with id: {0} was not found", dataType.SelectedEditor); + actionContext.Response = actionContext.Request.CreateErrorResponse(HttpStatusCode.NotFound, message); + return; + } + + //assign the prop editor to the model + dataType.PropertyEditor = propertyEditor; + + //Validate the data type exists or create one if required + IDataTypeDefinition persisted; + switch (dataType.Action) + { + case ContentSaveAction.Save: + persisted = DataTypeService.GetDataTypeDefinitionById(dataType.Id); + if (persisted == null) + { + var message = string.Format("Data type with id: {0} was not found", dataType.Id); + actionContext.Response = actionContext.Request.CreateErrorResponse(HttpStatusCode.NotFound, message); + return; + } + //map the model to the persisted instance + Mapper.Map(dataType, persisted); + break; + case ContentSaveAction.SaveNew: + //create the persisted model from mapping the saved model + persisted = Mapper.Map(dataType); + ((DataTypeDefinition)persisted).ResetIdentity(); + break; + default: + actionContext.Response = actionContext.Request.CreateErrorResponse(HttpStatusCode.NotFound, new ArgumentOutOfRangeException()); + return; + } + + //now assign the persisted entity to the model so we can use it in the action + dataType.PersistedDataType = persisted; + + //Validate each field + foreach (var preVal in dataType.PreValues) + { + var postedValue = preVal.Value; + + foreach (var v in propertyEditor.PreValueEditor.Fields.SelectMany(x => x.Validators)) + { + foreach (var result in v.Validate(postedValue, preVal.Key, propertyEditor)) + { + //if there are no member names supplied then we assume that the validation message is for the overall property + // not a sub field on the property editor + if (!result.MemberNames.Any()) + { + //add a model state error for the entire property + actionContext.ModelState.AddModelError(string.Format("{0}.{1}", "Properties", preVal.Key), result.ErrorMessage); + } + else + { + //there's assigned field names so we'll combine the field name with the property name + // so that we can try to match it up to a real sub field of this editor + foreach (var field in result.MemberNames) + { + actionContext.ModelState.AddModelError(string.Format("{0}.{1}.{2}", "Properties", preVal.Key, field), result.ErrorMessage); + } + } + } + } + } + + if (actionContext.ModelState.IsValid == false) + { + //if it is not valid, do not continue and return the model state + actionContext.Response = actionContext.Request.CreateErrorResponse(HttpStatusCode.Forbidden, actionContext.ModelState); + return; + } + + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/Models/ContentEditing/ContentItemDisplayBase.cs b/src/Umbraco.Web/Models/ContentEditing/ContentItemDisplayBase.cs index 676c694793..dfdd08757b 100644 --- a/src/Umbraco.Web/Models/ContentEditing/ContentItemDisplayBase.cs +++ b/src/Umbraco.Web/Models/ContentEditing/ContentItemDisplayBase.cs @@ -32,8 +32,10 @@ namespace Umbraco.Web.Models.ContentEditing /// A content item can be invalid but still be saved. This occurs when there's property validation errors, we will /// still save the item but it cannot be published. So we need a way of returning validation errors as well as the /// updated model. + /// + /// NOTE: The ProperCase is important because when we return ModeState normally it will always be proper case. /// - [DataMember(Name = "modelState")] + [DataMember(Name = "ModelState")] public IDictionary Errors { get; set; } } } diff --git a/src/Umbraco.Web/Models/ContentEditing/DataTypeDisplay.cs b/src/Umbraco.Web/Models/ContentEditing/DataTypeDisplay.cs index f52bd623d2..4c9e159595 100644 --- a/src/Umbraco.Web/Models/ContentEditing/DataTypeDisplay.cs +++ b/src/Umbraco.Web/Models/ContentEditing/DataTypeDisplay.cs @@ -9,16 +9,30 @@ namespace Umbraco.Web.Models.ContentEditing /// Represents a data type that is being edited ///
[DataContract(Name = "dataType", Namespace = "")] - public class DataTypeDisplay : EntityBasic + public class DataTypeDisplay : EntityBasic, INotificationModel { + public DataTypeDisplay() + { + Notifications = new List(); + } + + /// + /// This is nullable because when we are creating a new one, it is nothing + /// [DataMember(Name = "selectedEditor", IsRequired = true)] [Required] - public Guid SelectedEditor { get; set; } + public Guid? SelectedEditor { get; set; } [DataMember(Name = "availableEditors")] public IEnumerable AvailableEditors { get; set; } [DataMember(Name = "preValues")] public IEnumerable PreValues { get; set; } + + /// + /// This is used to add custom localized messages/strings to the response for the app to use for localized UI purposes. + /// + [DataMember(Name = "notifications")] + public List Notifications { get; private set; } } } \ No newline at end of file diff --git a/src/Umbraco.Web/Models/ContentEditing/DataTypeSave.cs b/src/Umbraco.Web/Models/ContentEditing/DataTypeSave.cs new file mode 100644 index 0000000000..4215932716 --- /dev/null +++ b/src/Umbraco.Web/Models/ContentEditing/DataTypeSave.cs @@ -0,0 +1,44 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Runtime.Serialization; +using Newtonsoft.Json; +using Umbraco.Core.Models; +using Umbraco.Core.PropertyEditors; + +namespace Umbraco.Web.Models.ContentEditing +{ + [DataContract(Name = "dataType", Namespace = "")] + public class DataTypeSave : EntityBasic + { + /// + /// The action to perform when saving this data type + /// + /// + /// If either of the Publish actions are specified an exception will be thrown. + /// + [DataMember(Name = "action", IsRequired = true)] + [Required] + public ContentSaveAction Action { get; set; } + + [DataMember(Name = "selectedEditor", IsRequired = true)] + [Required] + public Guid SelectedEditor { get; set; } + + [DataMember(Name = "preValues")] + public IEnumerable PreValues { get; set; } + + /// + /// The real persisted data type + /// + [JsonIgnore] + internal IDataTypeDefinition PersistedDataType { get; set; } + + /// + /// The PropertyEditor assigned + /// + [JsonIgnore] + internal PropertyEditor PropertyEditor { get; set; } + + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/Models/ContentEditing/PreValueFieldDisplay.cs b/src/Umbraco.Web/Models/ContentEditing/PreValueFieldDisplay.cs index 9f38f6c331..8d779ca23b 100644 --- a/src/Umbraco.Web/Models/ContentEditing/PreValueFieldDisplay.cs +++ b/src/Umbraco.Web/Models/ContentEditing/PreValueFieldDisplay.cs @@ -5,8 +5,8 @@ namespace Umbraco.Web.Models.ContentEditing /// /// Defines a pre value editable field for a data type /// - [DataContract(Name = "propertyEditor", Namespace = "")] - public class PreValueFieldDisplay + [DataContract(Name = "preValue", Namespace = "")] + public class PreValueFieldDisplay : PreValueFieldSave { /// /// The name to display for this pre-value field @@ -26,16 +26,11 @@ namespace Umbraco.Web.Models.ContentEditing [DataMember(Name = "hideLabel")] public bool HideLabel { get; set; } - /// - /// The key to store the pre-value against - /// - [DataMember(Name = "key", IsRequired = true)] - public string Key { get; set; } - /// /// The view to render for the field /// [DataMember(Name = "view", IsRequired = true)] public string View { get; set; } + } } \ No newline at end of file diff --git a/src/Umbraco.Web/Models/ContentEditing/PreValueFieldSave.cs b/src/Umbraco.Web/Models/ContentEditing/PreValueFieldSave.cs new file mode 100644 index 0000000000..e13a5dcf24 --- /dev/null +++ b/src/Umbraco.Web/Models/ContentEditing/PreValueFieldSave.cs @@ -0,0 +1,23 @@ +using System.Runtime.Serialization; + +namespace Umbraco.Web.Models.ContentEditing +{ + /// + /// Defines a pre value editable field for a data type + /// + [DataContract(Name = "preValue", Namespace = "")] + public class PreValueFieldSave + { + /// + /// The key to store the pre-value against + /// + [DataMember(Name = "key", IsRequired = true)] + public string Key { get; set; } + + /// + /// The value stored for the pre-value field + /// + [DataMember(Name = "value", IsRequired = true)] + public string Value { get; set; } + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/Models/Mapping/ContentPropertyDisplayConverter.cs b/src/Umbraco.Web/Models/Mapping/ContentPropertyDisplayConverter.cs index 8214d08262..1eb883e1eb 100644 --- a/src/Umbraco.Web/Models/Mapping/ContentPropertyDisplayConverter.cs +++ b/src/Umbraco.Web/Models/Mapping/ContentPropertyDisplayConverter.cs @@ -1,4 +1,5 @@ -using System.Linq; +using System; +using System.Linq; using Umbraco.Core; using Umbraco.Core.Configuration; using Umbraco.Core.Models; @@ -12,11 +13,11 @@ namespace Umbraco.Web.Models.Mapping /// internal class ContentPropertyDisplayConverter : ContentPropertyBasicConverter { - private readonly ApplicationContext _applicationContext; + private readonly Lazy _dataTypeService; - public ContentPropertyDisplayConverter(ApplicationContext applicationContext) + public ContentPropertyDisplayConverter(Lazy dataTypeService) { - _applicationContext = applicationContext; + _dataTypeService = dataTypeService; } protected override ContentPropertyDisplay ConvertCore(Property originalProp) @@ -27,12 +28,11 @@ namespace Umbraco.Web.Models.Mapping display.Alias = originalProp.Alias; display.Description = originalProp.PropertyType.Description; display.Label = originalProp.PropertyType.Name; - var dataTypeService = (DataTypeService) _applicationContext.Services.DataTypeService; + + var dataTypeService = (DataTypeService)_dataTypeService.Value; var preVals = dataTypeService.GetPreValuesCollectionByDataTypeId(originalProp.PropertyType.DataTypeDefinitionId); - - if (display.PropertyEditor == null) { display.Config = PreValueCollection.AsDictionary(preVals); diff --git a/src/Umbraco.Web/Models/Mapping/ContentPropertyDtoConverter.cs b/src/Umbraco.Web/Models/Mapping/ContentPropertyDtoConverter.cs index 297406e10d..7c2310375b 100644 --- a/src/Umbraco.Web/Models/Mapping/ContentPropertyDtoConverter.cs +++ b/src/Umbraco.Web/Models/Mapping/ContentPropertyDtoConverter.cs @@ -1,6 +1,8 @@ -using Umbraco.Core; +using System; +using Umbraco.Core; using Umbraco.Core.Models; using Umbraco.Core.PropertyEditors; +using Umbraco.Core.Services; using Umbraco.Web.Models.ContentEditing; namespace Umbraco.Web.Models.Mapping @@ -10,22 +12,24 @@ namespace Umbraco.Web.Models.Mapping ///
internal class ContentPropertyDtoConverter : ContentPropertyBasicConverter { - private readonly ApplicationContext _applicationContext; + private readonly Lazy _dataTypeService; - public ContentPropertyDtoConverter(ApplicationContext applicationContext) + public ContentPropertyDtoConverter(Lazy dataTypeService) { - _applicationContext = applicationContext; + _dataTypeService = dataTypeService; } protected override ContentPropertyDto ConvertCore(Property originalProperty) { var propertyDto = base.ConvertCore(originalProperty); + var dataTypeService = (DataTypeService)_dataTypeService.Value; + propertyDto.IsRequired = originalProperty.PropertyType.Mandatory; propertyDto.ValidationRegExp = originalProperty.PropertyType.ValidationRegExp; propertyDto.Description = originalProperty.PropertyType.Description; propertyDto.Label = originalProperty.PropertyType.Name; - propertyDto.DataType = _applicationContext.Services.DataTypeService.GetDataTypeDefinitionById(originalProperty.PropertyType.DataTypeDefinitionId); + propertyDto.DataType = dataTypeService.GetDataTypeDefinitionById(originalProperty.PropertyType.DataTypeDefinitionId); return propertyDto; } diff --git a/src/Umbraco.Web/Models/Mapping/ContentPropertyModelMapper.cs b/src/Umbraco.Web/Models/Mapping/ContentPropertyModelMapper.cs index f567010f73..9ebb27098c 100644 --- a/src/Umbraco.Web/Models/Mapping/ContentPropertyModelMapper.cs +++ b/src/Umbraco.Web/Models/Mapping/ContentPropertyModelMapper.cs @@ -1,7 +1,9 @@ -using AutoMapper; +using System; +using AutoMapper; using Umbraco.Core; using Umbraco.Core.Models; using Umbraco.Core.Models.Mapping; +using Umbraco.Core.Services; using Umbraco.Web.Models.ContentEditing; namespace Umbraco.Web.Models.Mapping @@ -14,6 +16,8 @@ namespace Umbraco.Web.Models.Mapping { public override void ConfigureMappings(IConfiguration config, ApplicationContext applicationContext) { + var lazyDataTypeService = new Lazy(() => applicationContext.Services.DataTypeService); + //FROM Property TO ContentPropertyBasic config.CreateMap>() .ForMember(tab => tab.Label, expression => expression.MapFrom(@group => @group.Name)) @@ -26,11 +30,11 @@ namespace Umbraco.Web.Models.Mapping //FROM Property TO ContentPropertyDto config.CreateMap() - .ConvertUsing(new ContentPropertyDtoConverter(applicationContext)); + .ConvertUsing(new ContentPropertyDtoConverter(lazyDataTypeService)); //FROM Property TO ContentPropertyDisplay config.CreateMap() - .ConvertUsing(new ContentPropertyDisplayConverter(applicationContext)); + .ConvertUsing(new ContentPropertyDisplayConverter(lazyDataTypeService)); } } } \ No newline at end of file diff --git a/src/Umbraco.Web/Models/Mapping/DataTypeModelMapper.cs b/src/Umbraco.Web/Models/Mapping/DataTypeModelMapper.cs index e3eb42897b..0f4a53f9e2 100644 --- a/src/Umbraco.Web/Models/Mapping/DataTypeModelMapper.cs +++ b/src/Umbraco.Web/Models/Mapping/DataTypeModelMapper.cs @@ -1,16 +1,26 @@ -using AutoMapper; +using System; +using System.Collections.Generic; +using AutoMapper; +using System.Collections.Generic; +using System.Linq; using Umbraco.Core; using Umbraco.Core.Models; using Umbraco.Core.Models.Mapping; using Umbraco.Core.PropertyEditors; +using Umbraco.Core.Services; using Umbraco.Web.Models.ContentEditing; namespace Umbraco.Web.Models.Mapping { + /// + /// Configure's model mappings for Data types + /// internal class DataTypeModelMapper : MapperConfiguration { public override void ConfigureMappings(IConfiguration config, ApplicationContext applicationContext) { + var lazyDataTypeService = new Lazy(() => applicationContext.Services.DataTypeService); + config.CreateMap() .ForMember(basic => basic.EditorId, expression => expression.MapFrom(editor => editor.Id)); @@ -18,8 +28,20 @@ namespace Umbraco.Web.Models.Mapping config.CreateMap() .ForMember(display => display.AvailableEditors, expression => expression.ResolveUsing()) - .ForMember(display => display.PreValues, expression => expression.ResolveUsing()) - .ForMember(display => display.SelectedEditor, expression => expression.MapFrom(definition => definition.ControlId)); + .ForMember(display => display.PreValues, expression => expression.ResolveUsing( + new PreValueDisplayResolver(lazyDataTypeService))) + .ForMember(display => display.SelectedEditor, expression => expression.MapFrom( + definition => definition.ControlId == Guid.Empty ? null : (Guid?) definition.ControlId)); + + config.CreateMap() + .ConstructUsing(save => new DataTypeDefinition(-1, save.SelectedEditor) {CreateDate = DateTime.Now}) + .ForMember(definition => definition.ControlId, expression => expression.MapFrom(save => save.SelectedEditor)) + .ForMember(definition => definition.ParentId, expression => expression.MapFrom(save => -1)) + .ForMember(definition => definition.DatabaseType, expression => expression.ResolveUsing()); + + config.CreateMap, PreValueCollection>() + .ConstructUsing( + saves => new PreValueCollection(saves.ToDictionary(x => x.Key, x => (object) x.Value))); } } } \ No newline at end of file diff --git a/src/Umbraco.Web/Models/Mapping/DatabaseTypeResolver.cs b/src/Umbraco.Web/Models/Mapping/DatabaseTypeResolver.cs new file mode 100644 index 0000000000..3142689b4a --- /dev/null +++ b/src/Umbraco.Web/Models/Mapping/DatabaseTypeResolver.cs @@ -0,0 +1,24 @@ +using System; +using AutoMapper; +using Umbraco.Core.Models; +using Umbraco.Core.PropertyEditors; +using Umbraco.Web.Models.ContentEditing; + +namespace Umbraco.Web.Models.Mapping +{ + /// + /// Gets the DataTypeDatabaseType from the selected property editor for the data type + /// + internal class DatabaseTypeResolver : ValueResolver + { + protected override DataTypeDatabaseType ResolveCore(DataTypeSave source) + { + var propertyEditor = PropertyEditorResolver.Current.GetById(source.SelectedEditor); + if (propertyEditor == null) + { + throw new InvalidOperationException("Could not find property editor with id " + source.SelectedEditor); + } + return propertyEditor.ValueEditor.GetDatabaseType(); + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/Models/Mapping/PreValueDisplayResolver.cs b/src/Umbraco.Web/Models/Mapping/PreValueDisplayResolver.cs index 5bef6c75bd..f89047a83d 100644 --- a/src/Umbraco.Web/Models/Mapping/PreValueDisplayResolver.cs +++ b/src/Umbraco.Web/Models/Mapping/PreValueDisplayResolver.cs @@ -2,23 +2,76 @@ using System.Collections.Generic; using System.Linq; using AutoMapper; +using Umbraco.Core; +using Umbraco.Core.Logging; using Umbraco.Core.Models; using Umbraco.Core.PropertyEditors; +using Umbraco.Core.Services; using Umbraco.Web.Models.ContentEditing; namespace Umbraco.Web.Models.Mapping { internal class PreValueDisplayResolver : ValueResolver> { + private readonly Lazy _dataTypeService; + + public PreValueDisplayResolver(Lazy dataTypeService) + { + _dataTypeService = dataTypeService; + } + protected override IEnumerable ResolveCore(IDataTypeDefinition source) { - var propEd = PropertyEditorResolver.Current.GetById(source.ControlId); - if (propEd == null) + PropertyEditor propEd = null; + if (source.ControlId != Guid.Empty) { - throw new InvalidOperationException("Could not find property editor with id " + source.ControlId); + propEd = PropertyEditorResolver.Current.GetById(source.ControlId); + if (propEd == null) + { + throw new InvalidOperationException("Could not find property editor with id " + source.ControlId); + } + } + + + var dataTypeService = (DataTypeService) _dataTypeService.Value; + var preVals = dataTypeService.GetPreValuesCollectionByDataTypeId(source.Id); + var dictionaryVals = PreValueCollection.AsDictionary(preVals); + + var result = Enumerable.Empty(); + if (propEd != null) + { + result = propEd.PreValueEditor.Fields.Select(Mapper.Map).ToArray(); + } + var currentIndex = 0; //used if the collection is non-dictionary based. + foreach (var field in result) + { + if (preVals.IsDictionaryBased == false) + { + //we'll just need to wire up the values based on the order that the pre-values are stored + var found = dictionaryVals.Any(x => x.Key.InvariantEquals(currentIndex.ToInvariantString())); + if (found == false) + { + LogHelper.Warn("Could not find persisted pre-value for index " + currentIndex); + continue; + } + field.Value = (string) dictionaryVals.Single(x => x.Key.InvariantEquals(currentIndex.ToInvariantString())).Value; + currentIndex++; + } + else + { + var found = dictionaryVals.Any(x => x.Key.InvariantEquals(field.Key)); + if (found == false) + { + LogHelper.Warn("Could not find persisted pre-value for field " + field.Key); + continue; + } + field.Value = (string)dictionaryVals.Single(x => x.Key.InvariantEquals(field.Key)).Value; + } + + } - return propEd.PreValueEditor.Fields.Select(Mapper.Map); + return result; } } } \ No newline at end of file diff --git a/src/Umbraco.Web/Trees/ContentTreeController.cs b/src/Umbraco.Web/Trees/ContentTreeController.cs index 27e787575e..b5a450638f 100644 --- a/src/Umbraco.Web/Trees/ContentTreeController.cs +++ b/src/Umbraco.Web/Trees/ContentTreeController.cs @@ -111,7 +111,7 @@ namespace Umbraco.Web.Trees // add default actions for *all* users allowedMenu.AddMenuItem(); - allowedMenu.AddMenuItem(true); + allowedMenu.AddMenuItem(true); return allowedMenu; } @@ -155,7 +155,7 @@ namespace Umbraco.Web.Trees menu.AddMenuItem(true); menu.AddMenuItem(); - menu.AddMenuItem(true); + menu.AddMenuItem(true); return menu; } diff --git a/src/Umbraco.Web/Trees/DataTypeTreeController.cs b/src/Umbraco.Web/Trees/DataTypeTreeController.cs index 73477e0f71..5907f89fe9 100644 --- a/src/Umbraco.Web/Trees/DataTypeTreeController.cs +++ b/src/Umbraco.Web/Trees/DataTypeTreeController.cs @@ -45,8 +45,8 @@ namespace Umbraco.Web.Trees if (id == Constants.System.Root.ToInvariantString()) { // root actions - menu.AddMenuItem(); - menu.AddMenuItem(true); + menu.AddMenuItem(); + menu.AddMenuItem(true); return menu; } diff --git a/src/Umbraco.Web/Trees/MediaTreeController.cs b/src/Umbraco.Web/Trees/MediaTreeController.cs index 7078e7d2ac..95a39fd3c6 100644 --- a/src/Umbraco.Web/Trees/MediaTreeController.cs +++ b/src/Umbraco.Web/Trees/MediaTreeController.cs @@ -47,7 +47,7 @@ namespace Umbraco.Web.Trees // root actions menu.AddMenuItem(); menu.AddMenuItem(true); - menu.AddMenuItem(true); + menu.AddMenuItem(true); return menu; } diff --git a/src/Umbraco.Web/Trees/Menu/CreateChildEntity.cs b/src/Umbraco.Web/Trees/Menu/CreateChildEntity.cs new file mode 100644 index 0000000000..8a26e12aed --- /dev/null +++ b/src/Umbraco.Web/Trees/Menu/CreateChildEntity.cs @@ -0,0 +1,10 @@ +namespace Umbraco.Web.Trees.Menu +{ + /// + /// Represents the refresh node menu item + /// + [ActionMenuItem("umbracoMenuActions")] + public sealed class CreateChildEntity : ActionMenuItem + { + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/Trees/Menu/RefreshNodeMenuItem.cs b/src/Umbraco.Web/Trees/Menu/RefreshNode.cs similarity index 72% rename from src/Umbraco.Web/Trees/Menu/RefreshNodeMenuItem.cs rename to src/Umbraco.Web/Trees/Menu/RefreshNode.cs index 0eb78a0eb5..5f1d06bf4e 100644 --- a/src/Umbraco.Web/Trees/Menu/RefreshNodeMenuItem.cs +++ b/src/Umbraco.Web/Trees/Menu/RefreshNode.cs @@ -1,10 +1,10 @@ -namespace Umbraco.Web.Trees.Menu -{ - /// - /// Represents the refresh node menu item - /// - [ActionMenuItem("umbracoMenuActions")] - public sealed class RefreshNodeMenuItem : ActionMenuItem - { - } +namespace Umbraco.Web.Trees.Menu +{ + /// + /// Represents the refresh node menu item + /// + [ActionMenuItem("umbracoMenuActions")] + public sealed class RefreshNode : ActionMenuItem + { + } } \ No newline at end of file diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index 903e4df729..651a651cdc 100644 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -299,13 +299,17 @@ + + + + @@ -317,6 +321,7 @@ + @@ -376,7 +381,7 @@ - + diff --git a/src/Umbraco.Web/WebApi/Binders/ContentItemBaseBinder.cs b/src/Umbraco.Web/WebApi/Binders/ContentItemBaseBinder.cs index f0b805ae22..e7805adaaa 100644 --- a/src/Umbraco.Web/WebApi/Binders/ContentItemBaseBinder.cs +++ b/src/Umbraco.Web/WebApi/Binders/ContentItemBaseBinder.cs @@ -33,7 +33,7 @@ namespace Umbraco.Web.WebApi.Binders /// Binds the content model to the controller action for the posted multi-part Post /// internal abstract class ContentItemBaseBinder : IModelBinder - where TPersisted : IContentBase + where TPersisted : class, IContentBase { protected ApplicationContext ApplicationContext { get; private set; } @@ -168,10 +168,15 @@ namespace Umbraco.Web.WebApi.Binders } //create the dto from the persisted model - model.ContentDto = MapFromPersisted(model); - - //now map all of the saved values to the dto - MapPropertyValuesFromSaved(model, model.ContentDto); + if (model.PersistedContent != null) + { + model.ContentDto = MapFromPersisted(model); + } + if (model.ContentDto != null) + { + //now map all of the saved values to the dto + MapPropertyValuesFromSaved(model, model.ContentDto); + } return model; } diff --git a/src/Umbraco.Web/WebApi/Filters/ContentItemValidationHelper.cs b/src/Umbraco.Web/WebApi/Filters/ContentItemValidationHelper.cs index f8a2e4e958..f77d9a8910 100644 --- a/src/Umbraco.Web/WebApi/Filters/ContentItemValidationHelper.cs +++ b/src/Umbraco.Web/WebApi/Filters/ContentItemValidationHelper.cs @@ -19,7 +19,7 @@ namespace Umbraco.Web.WebApi.Filters /// errors will just be added to the ModelState. /// internal class ContentItemValidationHelper - where TPersisted : IContentBase + where TPersisted : class, IContentBase { private readonly ApplicationContext _applicationContext; diff --git a/src/umbraco.cms/Actions/ActionRefresh.cs b/src/umbraco.cms/Actions/ActionRefresh.cs index 9aa0a4fe78..e94d4d2fed 100644 --- a/src/umbraco.cms/Actions/ActionRefresh.cs +++ b/src/umbraco.cms/Actions/ActionRefresh.cs @@ -9,7 +9,7 @@ namespace umbraco.BusinessLogic.Actions /// Concerns only the tree itself and thus you should not handle /// this action from without umbraco. /// - [LegacyActionMenuItem("umbracoMenuActions", "RefreshNodeMenuItem")] + [LegacyActionMenuItem("umbracoMenuActions", "RefreshNode")] public class ActionRefresh : IAction { //create singleton