diff --git a/src/Umbraco.Core/Exceptions/InvalidCompositionException.cs b/src/Umbraco.Core/Exceptions/InvalidCompositionException.cs index fcf7a44677..bb9becf058 100644 --- a/src/Umbraco.Core/Exceptions/InvalidCompositionException.cs +++ b/src/Umbraco.Core/Exceptions/InvalidCompositionException.cs @@ -3,22 +3,41 @@ namespace Umbraco.Core.Exceptions { public class InvalidCompositionException : Exception - { - public string ContentTypeAlias { get; set; } + { + public InvalidCompositionException(string contentTypeAlias, string addedCompositionAlias, string[] propertyTypeAliass) + { + ContentTypeAlias = contentTypeAlias; + AddedCompositionAlias = addedCompositionAlias; + PropertyTypeAliases = propertyTypeAliass; + } - public string AddedCompositionAlias { get; set; } + public InvalidCompositionException(string contentTypeAlias, string[] propertyTypeAliass) + { + ContentTypeAlias = contentTypeAlias; + PropertyTypeAliases = propertyTypeAliass; + } - public string PropertyTypeAlias { get; set; } + public string ContentTypeAlias { get; private set; } + + public string AddedCompositionAlias { get; private set; } + + public string[] PropertyTypeAliases { get; private set; } public override string Message { get { - return string.Format( - "InvalidCompositionException - ContentType with alias '{0}' was added as a Compsition to ContentType with alias '{1}', " + - "but there was a conflict on the PropertyType alias '{2}'. " + + return AddedCompositionAlias.IsNullOrWhiteSpace() + ? string.Format( + "ContentType with alias '{0}' has an invalid composition " + + "and there was a conflict on the following PropertyTypes: '{1}'. " + "PropertyTypes must have a unique alias across all Compositions in order to compose a valid ContentType Composition.", - AddedCompositionAlias, ContentTypeAlias, PropertyTypeAlias); + ContentTypeAlias, string.Join(", ", PropertyTypeAliases)) + : string.Format( + "ContentType with alias '{0}' was added as a Composition to ContentType with alias '{1}', " + + "but there was a conflict on the following PropertyTypes: '{2}'. " + + "PropertyTypes must have a unique alias across all Compositions in order to compose a valid ContentType Composition.", + AddedCompositionAlias, ContentTypeAlias, string.Join(", ", PropertyTypeAliases)); } } } diff --git a/src/Umbraco.Core/Models/ContentTypeCompositionBase.cs b/src/Umbraco.Core/Models/ContentTypeCompositionBase.cs index d102f06483..ea5a8fbcbf 100644 --- a/src/Umbraco.Core/Models/ContentTypeCompositionBase.cs +++ b/src/Umbraco.Core/Models/ContentTypeCompositionBase.cs @@ -94,13 +94,7 @@ namespace Umbraco.Core.Models .Select(p => p.Alias)).ToList(); if (conflictingPropertyTypeAliases.Any()) - throw new InvalidCompositionException - { - AddedCompositionAlias = contentType.Alias, - ContentTypeAlias = Alias, - PropertyTypeAlias = - string.Join(", ", conflictingPropertyTypeAliases) - }; + throw new InvalidCompositionException(Alias, contentType.Alias, conflictingPropertyTypeAliases.ToArray()); _contentTypeComposition.Add(contentType); OnPropertyChanged(ContentTypeCompositionSelector); diff --git a/src/Umbraco.Core/Models/Mapping/MappingExpressionExtensions.cs b/src/Umbraco.Core/Models/Mapping/MappingExpressionExtensions.cs new file mode 100644 index 0000000000..570e51dbc3 --- /dev/null +++ b/src/Umbraco.Core/Models/Mapping/MappingExpressionExtensions.cs @@ -0,0 +1,20 @@ +using AutoMapper; + +namespace Umbraco.Core.Models.Mapping +{ + internal static class MappingExpressionExtensions + { + /// + /// Ignores all unmapped members by default - Use with caution! + /// + /// + /// + /// + /// + public static IMappingExpression IgnoreAllUnmapped(this IMappingExpression expression) + { + expression.ForAllMembers(opt => opt.Ignore()); + return expression; + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Core/Models/PropertyType.cs b/src/Umbraco.Core/Models/PropertyType.cs index 69c714c85a..7f22b65c8c 100644 --- a/src/Umbraco.Core/Models/PropertyType.cs +++ b/src/Umbraco.Core/Models/PropertyType.cs @@ -33,6 +33,8 @@ namespace Umbraco.Core.Models public PropertyType(IDataTypeDefinition dataTypeDefinition) { + if (dataTypeDefinition == null) throw new ArgumentNullException("dataTypeDefinition"); + if(dataTypeDefinition.HasIdentity) _dataTypeDefinitionId = dataTypeDefinition.Id; diff --git a/src/Umbraco.Core/Persistence/Repositories/DataTypeDefinitionRepository.cs b/src/Umbraco.Core/Persistence/Repositories/DataTypeDefinitionRepository.cs index d00d6dd2ee..c1ad88af3d 100644 --- a/src/Umbraco.Core/Persistence/Repositories/DataTypeDefinitionRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/DataTypeDefinitionRepository.cs @@ -118,9 +118,9 @@ namespace Umbraco.Core.Persistence.Repositories { var sql = new Sql(); sql.Select(isCount ? "COUNT(*)" : "*") - .From() - .InnerJoin() - .On(left => left.DataTypeId, right => right.NodeId) + .From(SqlSyntax) + .InnerJoin(SqlSyntax) + .On(SqlSyntax, left => left.DataTypeId, right => right.NodeId) .Where(x => x.NodeObjectType == NodeObjectTypeId); return sql; } @@ -338,9 +338,9 @@ AND umbracoNode.id <> @id", { //first just get all pre-values for this data type so we can compare them to see if we need to insert or update or replace var sql = new Sql().Select("*") - .From() + .From(SqlSyntax) .Where(dto => dto.DataTypeNodeId == dataType.Id) - .OrderBy(dto => dto.SortOrder); + .OrderBy(dto => dto.SortOrder, SqlSyntax); currentVals = Database.Fetch(sql).ToArray(); } @@ -431,7 +431,7 @@ AND umbracoNode.id <> @id", var sql = new Sql(); sql.Select("*") - .From() + .From(SqlSyntax) .Where(x => x.NodeObjectType == NodeObjectTypeId && x.Text.StartsWith(nodeName)); int uniqueNumber = 1; diff --git a/src/Umbraco.Core/Security/BackOfficeUserManager.cs b/src/Umbraco.Core/Security/BackOfficeUserManager.cs index f86c06c39c..65170341c0 100644 --- a/src/Umbraco.Core/Security/BackOfficeUserManager.cs +++ b/src/Umbraco.Core/Security/BackOfficeUserManager.cs @@ -21,6 +21,18 @@ namespace Umbraco.Core.Security { } + public BackOfficeUserManager( + IUserStore store, + IdentityFactoryOptions options, + MembershipProviderBase membershipProvider) + : base(store) + { + if (options == null) throw new ArgumentNullException("options"); + var manager = new BackOfficeUserManager(store); + InitUserManager(manager, membershipProvider, options); + } + + #region Static Create methods /// /// Creates a BackOfficeUserManager instance with all default options and the default BackOfficeUserManager /// @@ -56,13 +68,10 @@ namespace Umbraco.Core.Security BackOfficeUserStore customUserStore, MembershipProviderBase membershipProvider) { - if (options == null) throw new ArgumentNullException("options"); - if (customUserStore == null) throw new ArgumentNullException("customUserStore"); - - var manager = new BackOfficeUserManager(customUserStore); - - return InitUserManager(manager, membershipProvider, options); - } + var manager = new BackOfficeUserManager(customUserStore, options, membershipProvider); + return manager; + } + #endregion /// /// Initializes the user manager with the correct options diff --git a/src/Umbraco.Core/Services/ContentTypeService.cs b/src/Umbraco.Core/Services/ContentTypeService.cs index f144fa51e4..fde2b06d8d 100644 --- a/src/Umbraco.Core/Services/ContentTypeService.cs +++ b/src/Umbraco.Core/Services/ContentTypeService.cs @@ -308,15 +308,28 @@ namespace Umbraco.Core.Services } - public void Validate(IContentTypeComposition compo) + /// + /// Validates the composition, if its invalid a list of property type aliases that were duplicated is returned + /// + /// + /// + public Attempt ValidateComposition(IContentTypeComposition compo) { using (new WriteLock(Locker)) { - ValidateLocked(compo); + try + { + ValidateLocked(compo); + return Attempt.Succeed(); + } + catch (InvalidCompositionException ex) + { + return Attempt.Fail(ex.PropertyTypeAliases, ex); + } } } - private void ValidateLocked(IContentTypeComposition compositionContentType) + protected void ValidateLocked(IContentTypeComposition compositionContentType) { // performs business-level validation of the composition // should ensure that it is absolutely safe to save the composition @@ -369,10 +382,8 @@ namespace Umbraco.Core.Services if (contentTypeDependency == null) continue; var intersect = contentTypeDependency.PropertyTypes.Select(x => x.Alias.ToLowerInvariant()).Intersect(propertyTypeAliases).ToArray(); if (intersect.Length == 0) continue; - - var message = string.Format("The following PropertyType aliases from the current ContentType conflict with existing PropertyType aliases: {0}.", - string.Join(", ", intersect)); - throw new Exception(message); + + throw new InvalidCompositionException(compositionContentType.Alias, intersect.ToArray()); } } diff --git a/src/Umbraco.Core/Services/IContentTypeService.cs b/src/Umbraco.Core/Services/IContentTypeService.cs index 1eb0b8695c..a68a5f7a6f 100644 --- a/src/Umbraco.Core/Services/IContentTypeService.cs +++ b/src/Umbraco.Core/Services/IContentTypeService.cs @@ -10,6 +10,13 @@ namespace Umbraco.Core.Services /// public interface IContentTypeService : IService { + /// + /// Validates the composition, if its invalid a list of property type aliases that were duplicated is returned + /// + /// + /// + Attempt ValidateComposition(IContentTypeComposition compo); + Attempt CreateFolder(int parentId, string name, int userId = 0); /// diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index a0999741ee..e6514693f3 100644 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -367,6 +367,7 @@ + diff --git a/src/Umbraco.Tests/Models/Mapping/ContentTypeModelMappingTests.cs b/src/Umbraco.Tests/Models/Mapping/ContentTypeModelMappingTests.cs index 4048ad5fde..88dc4e8e89 100644 --- a/src/Umbraco.Tests/Models/Mapping/ContentTypeModelMappingTests.cs +++ b/src/Umbraco.Tests/Models/Mapping/ContentTypeModelMappingTests.cs @@ -26,12 +26,12 @@ namespace Umbraco.Tests.Models.Mapping public class ContentTypeModelMappingTests : BaseUmbracoConfigurationTest { //Mocks of services that can be setup on a test by test basis to return whatever we want - private Mock _contentTypeService = new Mock(); - private Mock _contentService = new Mock(); - private Mock _dataTypeService = new Mock(); + private readonly Mock _contentTypeService = new Mock(); + private readonly Mock _contentService = new Mock(); + private readonly Mock _dataTypeService = new Mock(); private Mock _propertyEditorResolver; - - private Mock _entityService = new Mock(); + private readonly Mock _entityService = new Mock(); + private readonly Mock _fileService = new Mock(); [SetUp] public void Setup() @@ -47,7 +47,9 @@ namespace Umbraco.Tests.Models.Mapping contentService: _contentService.Object, contentTypeService:_contentTypeService.Object, - dataTypeService:_dataTypeService.Object), + dataTypeService:_dataTypeService.Object, + entityService:_entityService.Object, + fileService: _fileService.Object), nullCacheHelper, new ProfilingLogger(Mock.Of(), Mock.Of())); @@ -67,84 +69,9 @@ namespace Umbraco.Tests.Models.Mapping entityMapper.ConfigureMappings(configuration, appContext); }); } - + [Test] - public void PropertyTypeDisplay_To_PropertyType() - { - // setup the mocks to return the data we want to test against... - - _dataTypeService.Setup(x => x.GetDataTypeDefinitionById(It.IsAny())) - .Returns(Mock.Of( - definition => - definition.Id == 555 - && definition.PropertyEditorAlias == "myPropertyType" - && definition.DatabaseType == DataTypeDatabaseType.Nvarchar)); - - var display = new PropertyTypeDisplay() - { - Id = 1, - Alias = "test", - ContentTypeId = 4, - Description = "testing", - DataTypeId = 555, - - Value = "testsdfasdf", - Inherited = false, - Editor = "blah", - SortOrder = 6, - ContentTypeName = "Hello", - Label = "asdfasdf", - GroupId = 8, - Validation = new PropertyTypeValidation() - { - Mandatory = true, - Pattern = "asdfasdfa" - } - }; - - var result = Mapper.Map(display); - - Assert.AreEqual(1, result.Id); - Assert.AreEqual("test", result.Alias); - Assert.AreEqual("testing", result.Description); - Assert.AreEqual("blah", result.PropertyEditorAlias); - Assert.AreEqual(6, result.SortOrder); - Assert.AreEqual("asdfasdf", result.Name); - } - - [Test] - public void ContentGroupDisplay_To_PropertyGroup() - { - var display = new PropertyGroupDisplay() - { - ContentTypeId = 2, - Id = 1, - Inherited = false, - Name = "test", - ParentGroupId = 4, - ParentTabContentTypeNames = new[] - { - "hello", "world" - }, - SortOrder = 5, - ParentTabContentTypes = new[] - { - 10, 11 - } - }; - - - var result = Mapper.Map(display); - - Assert.AreEqual(1, result.Id); - Assert.AreEqual("test", result.Name); - Assert.AreEqual(4, result.ParentId); - Assert.AreEqual(5, result.SortOrder); - - } - - [Test] - public void ContentTypeDisplay_To_IContentType() + public void ContentTypeSave_To_IContentType() { //Arrange @@ -157,8 +84,14 @@ namespace Umbraco.Tests.Models.Mapping && definition.PropertyEditorAlias == "myPropertyType" && definition.DatabaseType == DataTypeDatabaseType.Nvarchar)); - - var display = CreateSimpleContentTypeDisplay(); + + _fileService.Setup(x => x.GetTemplate(It.IsAny())) + .Returns((string alias) => Mock.Of( + definition => + definition.Id == alias.GetHashCode() && definition.Alias == alias)); + + + var display = CreateContentTypeSave(); //Act @@ -194,10 +127,14 @@ namespace Umbraco.Tests.Models.Mapping } } - Assert.AreEqual(display.AllowedTemplates.Count(), result.AllowedTemplates.Count()); + var allowedTemplateAliases = display.AllowedTemplates + .Concat(new[] {display.DefaultTemplate}) + .Distinct(); + + Assert.AreEqual(allowedTemplateAliases.Count(), result.AllowedTemplates.Count()); for (var i = 0; i < display.AllowedTemplates.Count(); i++) { - Assert.AreEqual(display.AllowedTemplates.ElementAt(i).Id, result.AllowedTemplates.ElementAt(i).Id); + Assert.AreEqual(display.AllowedTemplates.ElementAt(i), result.AllowedTemplates.ElementAt(i).Alias); } Assert.AreEqual(display.AllowedContentTypes.Count(), result.AllowedContentTypes.Count()); @@ -208,7 +145,7 @@ namespace Umbraco.Tests.Models.Mapping } [Test] - public void ContentTypeDisplay_With_Composition_To_IContentType() + public void ContentTypeSave_With_Composition_To_IContentType() { //Arrange @@ -222,7 +159,7 @@ namespace Umbraco.Tests.Models.Mapping && definition.DatabaseType == DataTypeDatabaseType.Nvarchar)); - var display = CreateCompositionContentTypeDisplay(); + var display = CreateCompositionContentTypeSave(); //Act @@ -306,6 +243,91 @@ namespace Umbraco.Tests.Models.Mapping } + [Test] + public void PropertyGroupBasic_To_PropertyGroup() + { + var basic = new PropertyGroupBasic() + { + Id = 222, + Name = "Group 1", + SortOrder = 1, + Properties = new[] + { + new PropertyTypeBasic() + { + Id = 33, + SortOrder = 1, + Alias = "prop1", + Description = "property 1", + DataTypeId = 99, + GroupId = 222, + Label = "Prop 1", + Validation = new PropertyTypeValidation() + { + Mandatory = true, + Pattern = null + } + }, + new PropertyTypeBasic() + { + Id = 34, + SortOrder = 2, + Alias = "prop2", + Description = "property 2", + DataTypeId = 99, + GroupId = 222, + Label = "Prop 2", + Validation = new PropertyTypeValidation() + { + Mandatory = false, + Pattern = null + } + }, + } + }; + + var result = Mapper.Map(basic); + + Assert.AreEqual(basic.Name, result.Name); + Assert.AreEqual(basic.Id, result.Id); + Assert.AreEqual(basic.SortOrder, result.SortOrder); + Assert.AreEqual(basic.Properties.Count(), result.PropertyTypes.Count()); + } + + [Test] + public void PropertyTypeBasic_To_PropertyType() + { + _dataTypeService.Setup(x => x.GetDataTypeDefinitionById(It.IsAny())) + .Returns(new DataTypeDefinition("test")); + + var basic = new PropertyTypeBasic() + { + Id = 33, + SortOrder = 1, + Alias = "prop1", + Description = "property 1", + DataTypeId = 99, + GroupId = 222, + Label = "Prop 1", + Validation = new PropertyTypeValidation() + { + Mandatory = true, + Pattern = "xyz" + } + }; + + var result = Mapper.Map(basic); + + Assert.AreEqual(basic.Id, result.Id); + Assert.AreEqual(basic.SortOrder, result.SortOrder); + Assert.AreEqual(basic.Alias, result.Alias); + Assert.AreEqual(basic.Description, result.Description); + Assert.AreEqual(basic.DataTypeId, result.DataTypeDefinitionId); + Assert.AreEqual(basic.Label, result.Name); + Assert.AreEqual(basic.Validation.Mandatory, result.Mandatory); + Assert.AreEqual(basic.Validation.Pattern, result.ValidationRegExp); + } + [Test] public void IContentTypeComposition_To_ContentTypeDisplay() { @@ -318,6 +340,9 @@ namespace Umbraco.Tests.Models.Mapping _dataTypeService.Setup(x => x.GetPreValuesCollectionByDataTypeId(It.IsAny())) .Returns(new PreValueCollection(new Dictionary())); + _entityService.Setup(x => x.GetObjectType(It.IsAny())) + .Returns(UmbracoObjectTypes.DocumentType); + //return a textbox property editor for any requested editor by alias _propertyEditorResolver.Setup(resolver => resolver.GetByAlias(It.IsAny())) .Returns(new TextboxPropertyEditor()); @@ -403,31 +428,19 @@ namespace Umbraco.Tests.Models.Mapping } - private ContentTypeDisplay CreateSimpleContentTypeDisplay() + private ContentTypeSave CreateContentTypeSave() { - return new ContentTypeDisplay + return new ContentTypeSave { Alias = "test", AllowAsRoot = true, - AllowedTemplates = new List + AllowedTemplates = new [] { - new EntityBasic - { - Id = 555, - Alias = "template1", - Name = "Template1" - }, - new EntityBasic - { - Id = 556, - Alias = "template2", - Name = "Template2" - } + "template1", + "template2" }, - AllowedContentTypes = new [] {666, 667}, - AvailableCompositeContentTypes = new List(), - DefaultTemplate = new EntityBasic(){ Alias = "test" }, + DefaultTemplate = "test", Description = "hello world", Icon = "tree-icon", Id = 1234, @@ -437,18 +450,17 @@ namespace Umbraco.Tests.Models.Mapping ParentId = -1, Thumbnail = "tree-thumb", IsContainer = true, - Groups = new List() + Groups = new [] { - new PropertyGroupDisplay + new PropertyGroupBasic() { Id = 987, Name = "Tab 1", - ParentGroupId = -1, SortOrder = 0, Inherited = false, - Properties = new List + Properties = new [] { - new PropertyTypeDisplay + new PropertyTypeBasic { Alias = "property1", Description = "this is property 1", @@ -459,17 +471,8 @@ namespace Umbraco.Tests.Models.Mapping Mandatory = false, Pattern = "" }, - Editor = "myPropertyType", - Value = "value 1", - //View = ??? - isn't this the same as editor? - Config = new Dictionary - { - {"item1", "value1"}, - {"item2", "value2"} - }, SortOrder = 0, - DataTypeId = 555, - View = "blah" + DataTypeId = 555 } } } @@ -477,30 +480,19 @@ namespace Umbraco.Tests.Models.Mapping }; } - private ContentTypeDisplay CreateCompositionContentTypeDisplay() + private ContentTypeSave CreateCompositionContentTypeSave() { - return new ContentTypeDisplay + return new ContentTypeSave { Alias = "test", AllowAsRoot = true, - AllowedTemplates = new List + AllowedTemplates = new[] { - new EntityBasic - { - Id = 555, - Alias = "template1", - Name = "Template1" - }, - new EntityBasic - { - Id = 556, - Alias = "template2", - Name = "Template2" - } + "template1", + "template2" }, AllowedContentTypes = new[] { 666, 667 }, - AvailableCompositeContentTypes = new List(), - DefaultTemplate = new EntityBasic() { Alias = "test" }, + DefaultTemplate = "test", Description = "hello world", Icon = "tree-icon", Id = 1234, @@ -510,18 +502,17 @@ namespace Umbraco.Tests.Models.Mapping ParentId = -1, Thumbnail = "tree-thumb", IsContainer = true, - Groups = new List() + Groups = new[] { - new PropertyGroupDisplay + new PropertyGroupBasic() { Id = 987, Name = "Tab 1", - ParentGroupId = -1, SortOrder = 0, Inherited = false, - Properties = new List + Properties = new[] { - new PropertyTypeDisplay + new PropertyTypeBasic { Alias = "property1", Description = "this is property 1", @@ -532,30 +523,20 @@ namespace Umbraco.Tests.Models.Mapping Mandatory = false, Pattern = "" }, - Editor = "myPropertyType", - Value = "value 1", - //View = ??? - isn't this the same as editor? - Config = new Dictionary - { - {"item1", "value1"}, - {"item2", "value2"} - }, SortOrder = 0, - DataTypeId = 555, - View = "blah" + DataTypeId = 555 } } }, - new PropertyGroupDisplay + new PropertyGroupBasic() { Id = 894, Name = "Tab 2", - ParentGroupId = -1, SortOrder = 0, Inherited = true, - Properties = new List + Properties = new[] { - new PropertyTypeDisplay + new PropertyTypeBasic { Alias = "parentProperty", Description = "this is a property from the parent", @@ -565,17 +546,9 @@ namespace Umbraco.Tests.Models.Mapping { Mandatory = false, Pattern = "" - }, - Editor = "myPropertyType", - Value = "parent value", - //View = ??? - isn't this the same as editor? - Config = new Dictionary - { - {"item1", "value1"} - }, + }, SortOrder = 0, - DataTypeId = 555, - View = "blah" + DataTypeId = 555 } } diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbeditorheader.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbeditorheader.directive.js index b037ce3e9a..aa69c76670 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbeditorheader.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbeditorheader.directive.js @@ -46,8 +46,11 @@ nameLocked: "=", menu: "=", icon: "=", + hideIcon: "@", alias: "=", + hideAlias: "@", description: "=", + hideDescription: "@", navigation: "=" }, link: link diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/umbautoresize.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/umbautoresize.directive.js index e28d6ef29e..5ba8839d08 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/umbautoresize.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/umbautoresize.directive.js @@ -69,18 +69,14 @@ angular.module("umbraco.directives") function resizeInput() { if (domEl.scrollWidth !== domEl.clientWidth) { - - if (ngModelController.$modelValue === undefined || ngModelController.$modelValue === "" || ngModelController.$modelValue === null) { - - if (attr.placeholder) { - attr.$set('size', attr.placeholder.length); - element.width('auto'); - } - - } else { + if (ngModelController.$modelValue) { element.width(domEl.scrollWidth); } + } + if(!ngModelController.$modelValue && attr.placeholder) { + attr.$set('size', attr.placeholder.length); + element.width('auto'); } } diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbGenerateAlias.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbGenerateAlias.directive.js index 4a52407972..2fadc76173 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbGenerateAlias.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbGenerateAlias.directive.js @@ -1,5 +1,5 @@ angular.module("umbraco.directives") - .directive('umbGenerateAlias', function ($timeout, contentTypeResource) { + .directive('umbGenerateAlias', function ($timeout, entityResource) { return { restrict: 'E', templateUrl: 'views/components/umb-generate-alias.html', @@ -29,7 +29,7 @@ angular.module("umbraco.directives") scope.alias = "Generating Alias..."; generateAliasTimeout = $timeout(function () { - contentTypeResource.getSafeAlias(value, true).then(function(safeAlias){ + entityResource.getSafeAlias(value, true).then(function (safeAlias) { scope.alias = safeAlias.alias; }); }, 500); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbgroupsbuilder.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbgroupsbuilder.directive.js index 12487187e5..c93df61e13 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbgroupsbuilder.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbgroupsbuilder.directive.js @@ -298,7 +298,6 @@ if (addGroup) { groups.push({ - groups: [], properties: [], parentTabContentTypes: [], parentTabContentTypeNames: [], diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/umblockedfield.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/umblockedfield.directive.js index d5a9e34ac7..dca497caa6 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/umblockedfield.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/umblockedfield.directive.js @@ -11,7 +11,7 @@ function LockedFieldDirective($timeout, localizationService) { - function link(scope, el, attr, ngModel) { + function link(scope, el, attr, ngModel) { var input = el.children('.umb-locked-field__input'); @@ -22,6 +22,16 @@ scope.locked = true; } + // if regex validation is not defined as an attr set default state + // if this is set to an empty string then regex validation can be ignored. + if (scope.regexValidation === undefined || scope.regexValidation === null) { + scope.regexValidation = "^[a-zA-Z]\\w.*$"; + } + + if (scope.serverValidationField === undefined || scope.serverValidationField === null) { + scope.serverValidationField = ""; + } + // if locked state is not defined as an attr set default state if (scope.placeholderText === undefined || scope.placeholderText === null) { scope.placeholderText = "Enter value..."; @@ -71,7 +81,9 @@ scope: { model: '=ngModel', locked: "=?", - placeholderText: "=?" + placeholderText: "=?", + regexValidation: "=?", + serverValidationField: "@" }, link: link }; diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/validation/valCustom.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/validation/valCustom.directive.js index a402065708..dac010a97f 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/validation/valCustom.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/validation/valCustom.directive.js @@ -57,7 +57,7 @@ angular.module('umbraco.directives.validation') } }; validators[key] = validateFn; - ctrl.$formatters.push(validateFn); + ctrl.$parsers.push(validateFn); }); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/validation/valHighlight.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/validation/valHighlight.directive.js index fdcf768947..599cda766c 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/validation/valHighlight.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/validation/valHighlight.directive.js @@ -9,9 +9,7 @@ function valHighlight($timeout) { restrict: "A", link: function (scope, element, attrs, ctrl) { - scope.$watch(function() { - return scope.$eval(attrs.valHighlight); - }, function(newVal, oldVal) { + attrs.$observe("valHighlight", function (newVal) { if (newVal === true) { element.addClass("highlight-error"); $timeout(function () { @@ -23,7 +21,7 @@ function valHighlight($timeout) { element.removeClass("highlight-error"); } }); - + } }; } diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/validation/valemail.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/validation/valemail.directive.js index 88ffd6f0fa..1e81d8edec 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/validation/valemail.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/validation/valemail.directive.js @@ -29,7 +29,6 @@ function valEmail(valEmailExpression) { } }; - ctrl.$formatters.push(patternValidator); ctrl.$parsers.push(patternValidator); } }; diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/validation/valformmanager.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/validation/valformmanager.directive.js index 634f6eb4ec..37c0313c45 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/validation/valformmanager.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/validation/valformmanager.directive.js @@ -16,6 +16,27 @@ function valFormManager(serverValidationManager, $rootScope, $log, $timeout, not return { require: "form", restrict: "A", + controller: function($scope) { + //This exposes an API for direct use with this directive + + var unsubscribe = []; + var self = this; + + //This is basically the same as a directive subscribing to an event but maybe a little + // nicer since the other directive can use this directive's API instead of a magical event + this.onValidationStatusChanged = function (cb) { + unsubscribe.push($scope.$on("valStatusChanged", function(evt, args) { + cb.apply(self, [evt, args]); + })); + }; + + //Ensure to remove the event handlers when this instance is destroyted + $scope.$on('$destroy', function () { + for (var u in unsubscribe) { + unsubscribe[u](); + } + }); + }, link: function (scope, element, attr, formCtrl) { scope.$watch(function () { diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/validation/valpropertyvalidator.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/validation/valpropertyvalidator.directive.js index 77652d7f69..53a1ea67b2 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/validation/valpropertyvalidator.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/validation/valpropertyvalidator.directive.js @@ -59,9 +59,6 @@ function valPropertyValidator(serverValidationManager) { } }; - // Formatters are invoked when the model is modified in the code. - modelCtrl.$formatters.push(validate); - // Parsers are called as soon as the value in the form input is modified modelCtrl.$parsers.push(validate); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/validation/valregex.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/validation/valregex.directive.js index 0f3372eecb..bc69d1cd02 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/validation/valregex.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/validation/valregex.directive.js @@ -13,50 +13,51 @@ function valRegex() { link: function (scope, elm, attrs, ctrl) { var flags = ""; - if (attrs.valRegexFlags) { - try { - flags = scope.$eval(attrs.valRegexFlags); - if (!flags) { - flags = attrs.valRegexFlags; + var regex; + + attrs.$observe("valRegexFlags", function (newVal) { + if (newVal) { + flags = newVal; + } + }); + + attrs.$observe("valRegex", function (newVal) { + if (newVal) { + try { + var resolved = newVal; + if (resolved) { + regex = new RegExp(resolved, flags); + } + else { + regex = new RegExp(attrs.valRegex, flags); + } + } + catch (e) { + regex = new RegExp(attrs.valRegex, flags); } } - catch (e) { - flags = attrs.valRegexFlags; - } - } - var regex; - try { - var resolved = scope.$eval(attrs.valRegex); - if (resolved) { - regex = new RegExp(resolved, flags); - } - else { - regex = new RegExp(attrs.valRegex, flags); - } - } - catch(e) { - regex = new RegExp(attrs.valRegex, flags); - } + }); var patternValidator = function (viewValue) { - //NOTE: we don't validate on empty values, use required validator for that + if (regex) { + //NOTE: we don't validate on empty values, use required validator for that if (!viewValue || regex.test(viewValue.toString())) { - // it is valid - ctrl.$setValidity('valRegex', true); - //assign a message to the validator - ctrl.errorMsg = ""; - return viewValue; - } - else { - // it is invalid, return undefined (no model update) - ctrl.$setValidity('valRegex', false); - //assign a message to the validator - ctrl.errorMsg = "Value is invalid, it does not match the correct pattern"; - return undefined; + // it is valid + ctrl.$setValidity('valRegex', true); + //assign a message to the validator + ctrl.errorMsg = ""; + return viewValue; + } + else { + // it is invalid, return undefined (no model update) + ctrl.$setValidity('valRegex', false); + //assign a message to the validator + ctrl.errorMsg = "Value is invalid, it does not match the correct pattern"; + return undefined; + } } }; - ctrl.$formatters.push(patternValidator); ctrl.$parsers.push(patternValidator); } }; diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/validation/valserverfield.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/validation/valserverfield.directive.js index 9a077615df..6fe2dfdf08 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/validation/valserverfield.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/validation/valserverfield.directive.js @@ -11,43 +11,46 @@ function valServerField(serverValidationManager) { restrict: "A", link: function (scope, element, attr, ctrl) { - if (!attr.valServerField) { - throw "valServerField must have a field name for referencing server errors"; - } + var fieldName = null; + + attr.$observe("valServerField", function (newVal) { + if (newVal && fieldName === null) { + fieldName = newVal; + + //subscribe to the changed event of the view model. This is required because when we + // have a server error we actually invalidate the form which means it cannot be + // resubmitted. So once a field is changed that has a server error assigned to it + // we need to re-validate it for the server side validator so the user can resubmit + // the form. Of course normal client-side validators will continue to execute. + ctrl.$viewChangeListeners.push(function () { + if (ctrl.$invalid) { + ctrl.$setValidity('valServerField', true); + } + }); + + //subscribe to the server validation changes + serverValidationManager.subscribe(null, fieldName, function (isValid, fieldErrors, allErrors) { + if (!isValid) { + ctrl.$setValidity('valServerField', false); + //assign an error msg property to the current validator + ctrl.errorMsg = fieldErrors[0].errorMsg; + } + else { + ctrl.$setValidity('valServerField', true); + //reset the error message + ctrl.errorMsg = ""; + } + }); + + //when the element is disposed we need to unsubscribe! + // NOTE: this is very important otherwise when this controller re-binds the previous subscriptsion will remain + // but they are a different callback instance than the above. + element.bind('$destroy', function () { + serverValidationManager.unsubscribe(null, fieldName); + }); - var fieldName = attr.valServerField; - - //subscribe to the changed event of the view model. This is required because when we - // have a server error we actually invalidate the form which means it cannot be - // resubmitted. So once a field is changed that has a server error assigned to it - // we need to re-validate it for the server side validator so the user can resubmit - // the form. Of course normal client-side validators will continue to execute. - ctrl.$viewChangeListeners.push(function () { - if (ctrl.$invalid) { - ctrl.$setValidity('valServerField', true); } }); - - //subscribe to the server validation changes - serverValidationManager.subscribe(null, fieldName, function (isValid, fieldErrors, allErrors) { - if (!isValid) { - ctrl.$setValidity('valServerField', false); - //assign an error msg property to the current validator - ctrl.errorMsg = fieldErrors[0].errorMsg; - } - else { - ctrl.$setValidity('valServerField', true); - //reset the error message - ctrl.errorMsg = ""; - } - }); - - //when the element is disposed we need to unsubscribe! - // NOTE: this is very important otherwise when this controller re-binds the previous subscriptsion will remain - // but they are a different callback instance than the above. - element.bind('$destroy', function () { - serverValidationManager.unsubscribe(null, fieldName); - }); } }; } diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/validation/valtab.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/validation/valtab.directive.js index cd6dc51eca..fbca0cd233 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/validation/valtab.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/validation/valtab.directive.js @@ -8,16 +8,16 @@ **/ function valTab() { return { - require: "^form", + require: ['^form', '^valFormManager'], restrict: "A", - link: function (scope, element, attr, formCtrl) { - - var tabId = "tab" + scope.tab.id; - + link: function (scope, element, attr, ctrs) { + + var valFormManager = ctrs[1]; + var tabId = "tab" + scope.tab.id; scope.tabHasError = false; //listen for form validation changes - scope.$on("valStatusChanged", function(evt, args) { + valFormManager.onValidationStatusChanged(function (evt, args) { if (!args.form.$valid) { var tabContent = element.closest(".umb-panel").find("#" + tabId); //check if the validation messages are contained inside of this tabs diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbvalidatecomponent.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/validation/valrequirecomponent.directive.js similarity index 100% rename from src/Umbraco.Web.UI.Client/src/common/directives/components/umbvalidatecomponent.directive.js rename to src/Umbraco.Web.UI.Client/src/common/directives/validation/valrequirecomponent.directive.js diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/contenttype.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/contenttype.resource.js index 936f3a1d56..e8b2a2e623 100644 --- a/src/Umbraco.Web.UI.Client/src/common/resources/contenttype.resource.js +++ b/src/Umbraco.Web.UI.Client/src/common/resources/contenttype.resource.js @@ -3,7 +3,7 @@ * @name umbraco.resources.contentTypeResource * @description Loads in data for content types **/ -function contentTypeResource($q, $http, umbRequestHelper) { +function contentTypeResource($q, $http, umbRequestHelper, umbDataFormatter) { return { @@ -145,16 +145,6 @@ function contentTypeResource($q, $http, umbRequestHelper) { 'Failed to retrieve content type scaffold'); }, - getSafeAlias: function (value, camelCase) { - - return umbRequestHelper.resourcePromise( - $http.get( - umbRequestHelper.getApiUrl( - "contentTypeApiBaseUrl", - "GetSafeAlias", { value: value, camelCase: camelCase })), - 'Failed to retrieve content type scaffold'); - }, - /** * @ngdoc method * @name umbraco.resources.contentTypeResource#save @@ -169,8 +159,10 @@ function contentTypeResource($q, $http, umbRequestHelper) { */ save: function (contentType) { + var saveModel = umbDataFormatter.formatContentTypePostData(contentType); + return umbRequestHelper.resourcePromise( - $http.post(umbRequestHelper.getApiUrl("contentTypeApiBaseUrl", "PostSave"), contentType), + $http.post(umbRequestHelper.getApiUrl("contentTypeApiBaseUrl", "PostSave"), saveModel), 'Failed to save data for content type id ' + contentType.id); }, diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/entity.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/entity.resource.js index c136f88bc7..3bbe1d5679 100644 --- a/src/Umbraco.Web.UI.Client/src/common/resources/entity.resource.js +++ b/src/Umbraco.Web.UI.Client/src/common/resources/entity.resource.js @@ -36,6 +36,16 @@ function entityResource($q, $http, umbRequestHelper) { //the factory object returned return { + getSafeAlias: function (value, camelCase) { + + return umbRequestHelper.resourcePromise( + $http.get( + umbRequestHelper.getApiUrl( + "entityApiBaseUrl", + "GetSafeAlias", { value: value, camelCase: camelCase })), + 'Failed to retrieve content type scaffold'); + }, + /** * @ngdoc method * @name umbraco.resources.entityResource#getPath diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/mediatype.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/mediatype.resource.js index c151aeba13..93e7fefa2b 100644 --- a/src/Umbraco.Web.UI.Client/src/common/resources/mediatype.resource.js +++ b/src/Umbraco.Web.UI.Client/src/common/resources/mediatype.resource.js @@ -3,7 +3,7 @@ * @name umbraco.resources.mediaTypeResource * @description Loads in data for media types **/ -function mediaTypeResource($q, $http, umbRequestHelper) { +function mediaTypeResource($q, $http, umbRequestHelper, umbDataFormatter) { return { @@ -81,12 +81,10 @@ function mediaTypeResource($q, $http, umbRequestHelper) { save: function (contentType) { + var saveModel = umbDataFormatter.formatContentTypePostData(contentType); + return umbRequestHelper.resourcePromise( - $http.post( - umbRequestHelper.getApiUrl( - "mediaTypeApiBaseUrl", - "PostSave" - ), contentType), + $http.post(umbRequestHelper.getApiUrl("mediaTypeApiBaseUrl", "PostSave"), saveModel), 'Failed to save data for content type id ' + contentType.id); }, diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/membertype.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/membertype.resource.js index dae65d5912..b2d1e8d7ed 100644 --- a/src/Umbraco.Web.UI.Client/src/common/resources/membertype.resource.js +++ b/src/Umbraco.Web.UI.Client/src/common/resources/membertype.resource.js @@ -3,7 +3,7 @@ * @name umbraco.resources.memberTypeResource * @description Loads in data for member types **/ -function memberTypeResource($q, $http, umbRequestHelper) { +function memberTypeResource($q, $http, umbRequestHelper, umbDataFormatter) { return { @@ -74,8 +74,10 @@ function memberTypeResource($q, $http, umbRequestHelper) { */ save: function (contentType) { + var saveModel = umbDataFormatter.formatContentTypePostData(contentType); + return umbRequestHelper.resourcePromise( - $http.post(umbRequestHelper.getApiUrl("memberTypeApiBaseUrl", "PostSave"), contentType), + $http.post(umbRequestHelper.getApiUrl("memberTypeApiBaseUrl", "PostSave"), saveModel), 'Failed to save data for member type id ' + contentType.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 ae4ca3cde8..d21f297a91 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 @@ -48,6 +48,9 @@ function contentEditingHelper(fileManager, $q, $location, $routeParams, notifica var self = this; + //we will use the default one for content if not specified + var rebindCallback = args.rebindCallback === undefined ? self.reBindChangedProperties : args.rebindCallback; + var deferred = $q.defer(); if (!args.scope.busy && formHelper.submitForm({ scope: args.scope, statusMessage: args.statusMessage })) { @@ -62,7 +65,9 @@ function contentEditingHelper(fileManager, $q, $location, $routeParams, notifica self.handleSuccessfulSave({ scope: args.scope, savedContent: data, - rebindCallback: self.reBindChangedProperties(args.content, data) + rebindCallback: function() { + rebindCallback.apply(self, [args.content, data]); + } }); args.scope.busy = false; @@ -72,7 +77,9 @@ function contentEditingHelper(fileManager, $q, $location, $routeParams, notifica self.handleSaveError({ redirectOnFailure: true, err: err, - rebindCallback: self.reBindChangedProperties(args.content, err.data) + rebindCallback: function() { + rebindCallback.apply(self, [args.content, err.data]); + } }); //show any notifications if (angular.isArray(err.data.notifications)) { @@ -91,6 +98,7 @@ function contentEditingHelper(fileManager, $q, $location, $routeParams, notifica 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. */ diff --git a/src/Umbraco.Web.UI.Client/src/common/services/datatypehelper.service.js b/src/Umbraco.Web.UI.Client/src/common/services/datatypehelper.service.js index 4a2707fd80..3cde632d4b 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/datatypehelper.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/datatypehelper.service.js @@ -24,6 +24,28 @@ function dataTypeHelper() { return preValues; + }, + + rebindChangedProperties: function (origContent, savedContent) { + + //a method to ignore built-in prop changes + var shouldIgnore = function (propName) { + return _.some(["notifications", "ModelState"], function (i) { + return i === propName; + }); + }; + //check for changed built-in properties of the content + for (var o in origContent) { + + //ignore the ones listed in the array + if (shouldIgnore(o)) { + continue; + } + + if (!_.isEqual(origContent[o], savedContent[o])) { + origContent[o] = savedContent[o]; + } + } } }; diff --git a/src/Umbraco.Web.UI.Client/src/common/services/formhelper.service.js b/src/Umbraco.Web.UI.Client/src/common/services/formhelper.service.js index 057e0b8cff..4b5521b8db 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/formhelper.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/formhelper.service.js @@ -7,7 +7,7 @@ * A utility class used to streamline how forms are developed, to ensure that validation is check and displayed consistently and to ensure that the correct events * fire when they need to. */ -function formHelper(angularHelper, serverValidationManager, $timeout, notificationsService, dialogService) { +function formHelper(angularHelper, serverValidationManager, $timeout, notificationsService, dialogService, localizationService) { return { /** @@ -157,9 +157,19 @@ function formHelper(angularHelper, serverValidationManager, $timeout, notificati * * @param {object} err The error object returned from the http promise */ - handleServerValidation: function(modelState) { + handleServerValidation: function (modelState) { for (var e in modelState) { + //This is where things get interesting.... + // We need to support validation for all editor types such as both the content and content type editors. + // The Content editor ModelState is quite specific with the way that Properties are validated especially considering + // that each property is a User Developer property editor. + // The way that Content Type Editor ModelState is created is simply based on the ASP.Net validation data-annotations + // system. + // So, to do this (since we need to support backwards compat), we need to hack a little bit. For Content Properties, + // which are user defined, we know that they will exist with a prefixed ModelState of "_Properties.", so if we detect + // this, then we know it's a Property. + //the alias in model state can be in dot notation which indicates // * the first part is the content property alias // * the second part is the field to which the valiation msg is associated with @@ -167,7 +177,11 @@ function formHelper(angularHelper, serverValidationManager, $timeout, notificati //If it is not prefixed with "Properties" that means the error is for a field of the object directly. var parts = e.split("."); - if (parts.length > 1) { + + //Check if this is for content properties - specific to content/media/member editors because those are special + // user defined properties with custom controls. + if (parts.length > 1 && parts[0] === "_Properties") { + var propertyAlias = parts[1]; //if it contains 2 '.' then we will wire it up to a property's field @@ -182,12 +196,15 @@ function formHelper(angularHelper, serverValidationManager, $timeout, notificati } else { - //the parts are only 1, this means its not a property but a native content property - serverValidationManager.addFieldError(parts[0], modelState[e][0]); + + //Everthing else is just a 'Field'... the field name could contain any level of 'parts' though, for example: + // Groups[0].Properties[2].Alias + serverValidationManager.addFieldError(e, modelState[e][0]); } //add to notifications notificationsService.error("Validation", modelState[e][0]); + } } }; 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 008d0c2183..8d94a66793 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 @@ -487,6 +487,53 @@ angular.module('umbraco.services').factory('umbPropEditorHelper', umbPropEditorH function umbDataFormatter() { return { + formatContentTypePostData: function (displayModel, action) { + + //create the save model from the display model + var saveModel = _.pick(displayModel, + 'compositeContentTypes', 'isContainer', 'allowAsRoot', 'allowedTemplates', 'allowedContentTypes', + 'alias', 'description', 'thumbnail', 'name', 'id', 'icon', 'trashed', + 'key', 'parentId', 'alias', 'path'); + + //TODO: Map these + saveModel.allowedTemplates = _.map(displayModel.allowedTemplates, function (t) { return t.alias; }); + saveModel.defaultTemplate = displayModel.defaultTemplate ? displayModel.defaultTemplate.alias : null; + var realGroups = _.reject(displayModel.groups, function(g) { + //do not include these tabs + return g.tabState === "init"; + }); + saveModel.groups = _.map(realGroups, function (g) { + + var saveGroup = _.pick(g, 'inherited', 'id', 'sortOrder', 'name'); + + var realProperties = _.reject(g.properties, function (p) { + //do not include these properties + return p.propertyState === "init" || p.inherited === true; + }); + + var saveProperties = _.map(realProperties, function (p) { + var saveProperty = _.pick(p, 'id', 'alias', 'description', 'validation', 'label', 'sortOrder', 'dataTypeId', 'groupId'); + return saveProperty; + }); + + saveGroup.properties = saveProperties; + + //if this is an inherited group and there are not non-inherited properties on it, then don't send up the data + if (saveGroup.inherited === true && saveProperties.length === 0) { + return null; + } + + return saveGroup; + }); + + //we don't want any null groups + saveModel.groups = _.reject(saveModel.groups, function(g) { + return !g; + }); + + return saveModel; + }, + /** 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 = { diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-group-builder.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-group-builder.less index 40e3787360..ae5e1de146 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-group-builder.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-group-builder.less @@ -123,7 +123,7 @@ input.umb-group-builder__group-title-input { } .umb-group-builder__group-title-input.-placeholder { - border: 1px dashed #979797 !important; + border: 1px dashed #979797; } .umb-group-builder__group-inherited-label { @@ -412,11 +412,10 @@ input.umb-group-builder__group-sort-value { } textarea.editor-label { - border: none; + border-color:transparent; box-shadow: none; width: 100%; - box-sizing: border-box; - padding: 10px 0 0 0; + box-sizing: border-box; margin-bottom: 10px; font-size: 16px; font-weight: bold; diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-locked-field.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-locked-field.less index 61d1eae8f6..9fb534e388 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-locked-field.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-locked-field.less @@ -27,9 +27,8 @@ input.umb-locked-field__input { background: rgba(255, 255, 255, 0); // if using transparent it will hide the text in safari - border: none; - font-size: 13px; - padding: 0; + border-color:transparent; + font-size: 13px; margin-bottom: 0; color: #ccc; transition: color 0.25s; diff --git a/src/Umbraco.Web.UI.Client/src/less/forms.less b/src/Umbraco.Web.UI.Client/src/less/forms.less index 38b63ad100..449ae42fd2 100644 --- a/src/Umbraco.Web.UI.Client/src/less/forms.less +++ b/src/Umbraco.Web.UI.Client/src/less/forms.less @@ -442,37 +442,11 @@ input[type="checkbox"][readonly] { // FORM FIELD FEEDBACK STATES // -------------------------- -// Warning -.show-validation.ng-invalid .control-group.warning { - .formFieldState(@formWarningText, @formWarningText, @formWarningBackground); -} // Error -.show-validation.ng-invalid .control-group.error { +.show-validation.ng-invalid .control-group.error, +.show-validation.ng-invalid .umb-panel-header-content-wrapper { .formFieldState(@formErrorText, @formErrorText, @formErrorBackground); } -// Success -.show-validation.ng-invalid .control-group.success { - .formFieldState(@formSuccessText, @formSuccessText, @formSuccessBackground); -} -// Success -.show-validation.ng-invalid .control-group.info { - .formFieldState(@formInfoText, @formInfoText, @formInfoBackground); -} - -// HTML5 invalid states -// Shares styles with the .control-group.error above - -.show-validation input:focus:invalid, -.show-validation textarea:focus:invalid, -.show-validation select:focus:invalid { - color: @formErrorText; - border-color: #ee5f5b; - &:focus { - border-color: darken(#ee5f5b, 10%); - @shadow: 0 0 6px lighten(#ee5f5b, 20%); - .box-shadow(@shadow); - } -} //val-highlight directive styling .highlight-error { @@ -747,6 +721,11 @@ input.search-query { margin-bottom: @baseLineHeight / 2; } +//modifier for control group +.control-group.-no-margin { + margin-bottom:0; +} + // Legend collapses margin, so next element is responsible for spacing legend + .control-group { margin-top: @baseLineHeight; @@ -819,4 +798,4 @@ legend + .control-group { margin-left: 0; } -} +} diff --git a/src/Umbraco.Web.UI.Client/src/less/mixins.less b/src/Umbraco.Web.UI.Client/src/less/mixins.less index e1f9170424..38df48a457 100644 --- a/src/Umbraco.Web.UI.Client/src/less/mixins.less +++ b/src/Umbraco.Web.UI.Client/src/less/mixins.less @@ -182,12 +182,6 @@ select.ng-invalid, textarea.ng-invalid { border-color: @borderColor; - .box-shadow(inset 0 1px 1px rgba(0,0,0,.075)); // Redeclare so transitions work - &:focus { - border-color: darken(@borderColor, 10%); - @shadow: inset 0 1px 1px rgba(0,0,0,.075), 0 0 6px lighten(@borderColor, 20%); - .box-shadow(@shadow); - } } // Give a small background color for input-prepend/-append .input-prepend .add-on, @@ -196,6 +190,18 @@ background-color: @backgroundColor; border-color: @textColor; } + //SD: We could do this but need to get the colors right + /*input.ng-invalid { + &:-moz-placeholder { + color: lighten(@textColor, 50%) !important; + } + &:-ms-input-placeholder { + color: lighten(@textColor, 50%) !important; + } + &::-webkit-input-placeholder { + color: lighten(@textColor, 50%) !important; + } + }*/ } // CSS3 PROPERTIES diff --git a/src/Umbraco.Web.UI.Client/src/less/panel.less b/src/Umbraco.Web.UI.Client/src/less/panel.less index 7c04d953d1..b1184934a2 100644 --- a/src/Umbraco.Web.UI.Client/src/less/panel.less +++ b/src/Umbraco.Web.UI.Client/src/less/panel.less @@ -99,21 +99,13 @@ color: @red; } +.umb-headline-editor-wrapper input.ng-invalid::-moz-placeholder, +.umb-headline-editor-wrapper input.ng-invalid:-ms-input-placeholder, .umb-headline-editor-wrapper input.ng-invalid::-webkit-input-placeholder { color: @red; line-height: 22px; } -.umb-headline-editor-wrapper input.ng-invalid::-moz-placeholder { - color: @red; - line-height: 22px; -} - -.umb-headline-editor-wrapper input.ng-invalid:-ms-input-placeholder { - color: @red; - line-height: 22px; -} - /* .umb-panel-header i { font-size: 13px; diff --git a/src/Umbraco.Web.UI.Client/src/views/common/dashboard.html b/src/Umbraco.Web.UI.Client/src/views/common/dashboard.html index 97a109d6fe..452743d19f 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/dashboard.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/dashboard.html @@ -12,7 +12,10 @@ + tabs="dashboard.tabs" + hide-icon="true" + hide-description="true" + hide-alias="true"> diff --git a/src/Umbraco.Web.UI.Client/src/views/common/overlays/contenttypeeditor/editorsettings/editorsettings.html b/src/Umbraco.Web.UI.Client/src/views/common/overlays/contenttypeeditor/editorsettings/editorsettings.html index 1f61855e34..03f4030ae7 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/overlays/contenttypeeditor/editorsettings/editorsettings.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/overlays/contenttypeeditor/editorsettings/editorsettings.html @@ -2,7 +2,7 @@
- +
diff --git a/src/Umbraco.Web.UI.Client/src/views/common/overlays/contenttypeeditor/propertysettings/propertysettings.html b/src/Umbraco.Web.UI.Client/src/views/common/overlays/contenttypeeditor/propertysettings/propertysettings.html index a2dc83cc5c..64f895e291 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/overlays/contenttypeeditor/propertysettings/propertysettings.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/overlays/contenttypeeditor/propertysettings/propertysettings.html @@ -1,36 +1,33 @@
-
- -
- - -
- - - +
+
+ +
+
+ +
-
+
-
+
Add Editor diff --git a/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editor-header.html b/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editor-header.html index cd8fb83ac5..6806e52563 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editor-header.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editor-header.html @@ -6,19 +6,29 @@
-
+
Add icon
- -
{{ name }}
+ + + - +
{{ name }}
- + + +
diff --git a/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editor-sub-views.html b/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editor-sub-views.html index fefe12e233..c53d74176f 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editor-sub-views.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editor-sub-views.html @@ -1,5 +1,4 @@
-
+
-
- +
\ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/views/components/umb-groups-builder.html b/src/Umbraco.Web.UI.Client/src/views/components/umb-groups-builder.html index 1c3a3a93c1..8636cdf803 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/umb-groups-builder.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/umb-groups-builder.html @@ -29,10 +29,23 @@
-
- - -
+ +
+ + + + +
+
@@ -80,19 +93,32 @@ Inherited from {{property.contentTypeName}}
-
+ +
+ +
{{ property.alias }}
+ + -
{{ property.alias }}
+
+ + + +
-
- +
+ +
- -
- -
- -
+
diff --git a/src/Umbraco.Web.UI.Client/src/views/components/umb-locked-field.html b/src/Umbraco.Web.UI.Client/src/views/components/umb-locked-field.html index 91f08da5f8..7e6b556117 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/umb-locked-field.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/umb-locked-field.html @@ -1,22 +1,27 @@
+ + + + - - - + + + - - - - - + + + Invalid alias + +
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 cbbad8267d..0b1e3b2202 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/edit.html +++ b/src/Umbraco.Web.UI.Client/src/views/content/edit.html @@ -13,7 +13,10 @@ + tabs="content.tabs" + hide-icon="true" + hide-description="true" + hide-alias="true"> diff --git a/src/Umbraco.Web.UI.Client/src/views/content/recyclebin.html b/src/Umbraco.Web.UI.Client/src/views/content/recyclebin.html index 19976aa6f7..f4c22c0220 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/recyclebin.html +++ b/src/Umbraco.Web.UI.Client/src/views/content/recyclebin.html @@ -2,7 +2,10 @@ + name-locked="page.nameLocked" + hide-icon="true" + hide-description="true" + hide-alias="true" > diff --git a/src/Umbraco.Web.UI.Client/src/views/datatypes/datatype.edit.controller.js b/src/Umbraco.Web.UI.Client/src/views/datatypes/datatype.edit.controller.js index 3d85f47d29..7a6fdf0a9d 100644 --- a/src/Umbraco.Web.UI.Client/src/views/datatypes/datatype.edit.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/datatypes/datatype.edit.controller.js @@ -6,7 +6,7 @@ * @description * The controller for the content editor */ -function DataTypeEditController($scope, $routeParams, $location, appState, navigationService, treeService, dataTypeResource, notificationsService, angularHelper, serverValidationManager, contentEditingHelper, formHelper, editorState) { +function DataTypeEditController($scope, $routeParams, $location, appState, navigationService, treeService, dataTypeResource, notificationsService, angularHelper, serverValidationManager, contentEditingHelper, formHelper, editorState, dataTypeHelper) { //setup scope vars $scope.page = {}; @@ -158,6 +158,8 @@ function DataTypeEditController($scope, $routeParams, $location, appState, navig $scope.page.saveButtonState = "success"; + dataTypeHelper.rebindChangedProperties($scope.content, data); + }, function(err) { //NOTE: in the case of data type values we are setting the orig/new props @@ -171,6 +173,8 @@ function DataTypeEditController($scope, $routeParams, $location, appState, navig //share state editorState.set($scope.content); + + dataTypeHelper.rebindChangedProperties($scope.content, data); }); } diff --git a/src/Umbraco.Web.UI.Client/src/views/datatypes/edit.html b/src/Umbraco.Web.UI.Client/src/views/datatypes/edit.html index 8e32589c35..1c80fa44c1 100644 --- a/src/Umbraco.Web.UI.Client/src/views/datatypes/edit.html +++ b/src/Umbraco.Web.UI.Client/src/views/datatypes/edit.html @@ -13,7 +13,10 @@ + name-locked="page.nameLocked" + hide-icon="true" + hide-description="true" + hide-alias="true"> diff --git a/src/Umbraco.Web.UI.Client/src/views/documenttypes/edit.controller.js b/src/Umbraco.Web.UI.Client/src/views/documenttypes/edit.controller.js index 64157d3183..45ee09d65a 100644 --- a/src/Umbraco.Web.UI.Client/src/views/documenttypes/edit.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/documenttypes/edit.controller.js @@ -6,268 +6,268 @@ * @description * The controller for the content type editor */ -(function() { - "use strict"; +(function () { + "use strict"; + + function DocumentTypesEditController($scope, $routeParams, modelsResource, contentTypeResource, dataTypeResource, editorState, contentEditingHelper, formHelper, navigationService, iconHelper, contentTypeHelper, notificationsService, $filter, $q, localizationService) { - function DocumentTypesEditController($scope, $routeParams, contentTypeResource, dataTypeResource, editorState, contentEditingHelper, formHelper, navigationService, iconHelper, contentTypeHelper, notificationsService, $filter) { + var vm = this; - var vm = this; + vm.save = save; - vm.save = save; - - vm.currentNode = null; - vm.contentType = {}; - vm.page = {}; - vm.page.loading = false; - vm.page.saveButtonState = "init"; - vm.page.navigation = [ + vm.currentNode = null; + vm.contentType = {}; + vm.page = {}; + vm.page.loading = false; + vm.page.saveButtonState = "init"; + vm.page.navigation = [ { - "name": "Design", - "icon": "icon-document-dashed-line", - "view": "views/documenttypes/views/design/design.html", - "active": true + "name": "Design", + "icon": "icon-document-dashed-line", + "view": "views/documenttypes/views/design/design.html", + "active": true }, { - "name": "List view", - "icon": "icon-list", - "view": "views/documenttypes/views/listview/listview.html" + "name": "List view", + "icon": "icon-list", + "view": "views/documenttypes/views/listview/listview.html" }, { - "name": "Permissions", - "icon": "icon-keychain", - "view": "views/documenttypes/views/permissions/permissions.html" + "name": "Permissions", + "icon": "icon-keychain", + "view": "views/documenttypes/views/permissions/permissions.html" }, { - "name": "Templates", - "icon": "icon-layout", - "view": "views/documenttypes/views/templates/templates.html" + "name": "Templates", + "icon": "icon-layout", + "view": "views/documenttypes/views/templates/templates.html" } - ]; + ]; - vm.page.keyboardShortcutsOverview = [ + vm.page.keyboardShortcutsOverview = [ { - "name": "Sections", - "shortcuts": [ + "name": "Sections", + "shortcuts": [ { - "description": "Navigate sections", - "keys": [{"key": "1"}, {"key": "4"}], - "keyRange": true + "description": "Navigate sections", + "keys": [{ "key": "1" }, { "key": "4" }], + "keyRange": true } - ] + ] }, { - "name": "Design", - "shortcuts": [ + "name": "Design", + "shortcuts": [ { - "description": "Add tab", - "keys": [{"key": "alt"},{"key": "shift"},{"key": "t"}] + "description": "Add tab", + "keys": [{ "key": "alt" }, { "key": "shift" }, { "key": "t" }] }, { - "description": "Add property", - "keys": [{"key": "alt"},{"key": "shift"},{"key": "p"}] + "description": "Add property", + "keys": [{ "key": "alt" }, { "key": "shift" }, { "key": "p" }] }, { - "description": "Add editor", - "keys": [{"key": "alt"},{"key": "shift"},{"key": "e"}] + "description": "Add editor", + "keys": [{ "key": "alt" }, { "key": "shift" }, { "key": "e" }] }, { - "description": "Edit data type", - "keys": [{"key": "alt"},{"key": "shift"},{"key": "d"}] + "description": "Edit data type", + "keys": [{ "key": "alt" }, { "key": "shift" }, { "key": "d" }] } - ] + ] + }, + { + "name": "List view", + "shortcuts": [ + { + "description": "Toggle list view", + "keys": [{ "key": "alt" }, { "key": "shift" }, { "key": "l" }] + } + ] }, { - "name": "List view", - "shortcuts": [ + "name": "Permissions", + "shortcuts": [ { - "description": "Toggle list view", - "keys": [{"key": "alt"},{"key": "shift"},{"key": "l"}] - } - ] - }, - { - "name": "Permissions", - "shortcuts": [ - { - "description": "Toggle allow as root", - "keys": [{"key": "alt"},{"key": "shift"},{"key": "r"}] + "description": "Toggle allow as root", + "keys": [{ "key": "alt" }, { "key": "shift" }, { "key": "r" }] }, { - "description": "Add child node", - "keys": [{"key": "alt"},{"key": "shift"},{"key": "c"}] + "description": "Add child node", + "keys": [{ "key": "alt" }, { "key": "shift" }, { "key": "c" }] } - ] + ] }, { - "name": "Templates", - "shortcuts": [ + "name": "Templates", + "shortcuts": [ { - "description": "Add template", - "keys": [{"key": "alt"},{"key": "shift"},{"key": "t"}] + "description": "Add template", + "keys": [{ "key": "alt" }, { "key": "shift" }, { "key": "t" }] } - ] + ] } - ]; + ]; - if ($routeParams.create) { + if ($routeParams.create) { - vm.page.loading = true; + vm.page.loading = true; - //we are creating so get an empty data type item - contentTypeResource.getScaffold($routeParams.id) - .then(function(dt) { + //we are creating so get an empty data type item + contentTypeResource.getScaffold($routeParams.id) + .then(function (dt) { - init(dt); + init(dt); - vm.page.loading = false; + vm.page.loading = false; }); - } - else { + } + else { - vm.page.loading = true; + vm.page.loading = true; - contentTypeResource.getById($routeParams.id).then(function(dt){ - init(dt); + contentTypeResource.getById($routeParams.id).then(function (dt) { + init(dt); - syncTreeNode(vm.contentType, dt.path, true); + syncTreeNode(vm.contentType, dt.path, true); - vm.page.loading = false; + vm.page.loading = false; - }); - } + }); + } - /* ---------- SAVE ---------- */ + /* ---------- SAVE ---------- */ - function save() { + function save() { - // validate form - if (formHelper.submitForm({ scope: $scope })) { + var deferred = $q.defer(); - formHelper.resetForm({ scope: $scope }); + vm.page.saveButtonState = "busy"; - // if form validates - perform save - performSave(); + // reformat allowed content types to array if id's + vm.contentType.allowedContentTypes = contentTypeHelper.createIdArray(vm.contentType.allowedContentTypes); - } + // update placeholder template information on new doc types + if (!$routeParams.notemplate && vm.contentType.id === 0) { + vm.contentType = contentTypeHelper.updateTemplatePlaceholder(vm.contentType); + } - } + contentEditingHelper.contentEditorPerformSave({ + statusMessage: "Saving...", + saveMethod: contentTypeResource.save, + scope: $scope, + content: vm.contentType, + //no-op for rebind callback... we don't really need to rebind for content types + rebindCallback: angular.noop + }).then(function (data) { + //success + syncTreeNode(vm.contentType, data.path); - function performSave() { + vm.page.saveButtonState = "success"; - vm.page.saveButtonState = "busy"; + deferred.resolve(data); + }, function (err) { + //error + if (err) { + editorState.set($scope.content); + } + else { + localizationService.localize("speechBubbles_validationFailedHeader").then(function (headerValue) { + localizationService.localize("speechBubbles_validationFailedMessage").then(function(msgValue) { + notificationsService.error(headerValue, msgValue); + }); + }); + } - // reformat allowed content types to array if id's - vm.contentType.allowedContentTypes = contentTypeHelper.createIdArray(vm.contentType.allowedContentTypes); + vm.page.saveButtonState = "error"; - // update placeholder template information on new doc types - if (!$routeParams.notemplate && vm.contentType.id === 0) { - vm.contentType = contentTypeHelper.updateTemplatePlaceholder(vm.contentType); - } + deferred.reject(err); + }); - contentTypeResource.save(vm.contentType).then(function(dt){ + return deferred.promise; - formHelper.resetForm({ scope: $scope, notifications: dt.notifications }); - contentEditingHelper.handleSuccessfulSave({ - scope: $scope, - savedContent: dt, - rebindCallback: function() { + } - } - }); + function init(contentType) { - notificationsService.success("Document type save"); - //post save logic here -the saved doctype returns as a new object - init(dt); + // set all tab to inactive + if (contentType.groups.length !== 0) { + angular.forEach(contentType.groups, function (group) { - syncTreeNode(vm.contentType, dt.path); + angular.forEach(group.properties, function (property) { + // get data type details for each property + getDataTypeDetails(property); + }); - vm.page.saveButtonState = "success"; + }); + } - }); + // convert legacy icons + convertLegacyIcons(contentType); - } + // sort properties after sort order + angular.forEach(contentType.groups, function (group) { + group.properties = $filter('orderBy')(group.properties, 'sortOrder'); + }); + // insert template on new doc types + if (!$routeParams.notemplate && contentType.id === 0) { + contentType.defaultTemplate = contentTypeHelper.insertDefaultTemplatePlaceholder(contentType.defaultTemplate); + contentType.allowedTemplates = contentTypeHelper.insertTemplatePlaceholder(contentType.allowedTemplates); + } - function init(contentType){ + //set a shared state + editorState.set(contentType); - // set all tab to inactive - if( contentType.groups.length !== 0 ) { - angular.forEach(contentType.groups, function(group){ + vm.contentType = contentType; - angular.forEach(group.properties, function(property){ - // get data type details for each property - getDataTypeDetails(property); + } + + function convertLegacyIcons(contentType) { + + // convert icons for composite content types + iconHelper.formatContentTypeIcons(contentType.availableCompositeContentTypes); + + // make array to store contentType icon + var contentTypeArray = []; + + // push icon to array + contentTypeArray.push({ "icon": contentType.icon }); + + // run through icon method + iconHelper.formatContentTypeIcons(contentTypeArray); + + // set icon back on contentType + contentType.icon = contentTypeArray[0].icon; + + } + + function getDataTypeDetails(property) { + + if (property.propertyState !== "init") { + + dataTypeResource.getById(property.dataTypeId) + .then(function (dataType) { + property.dataTypeIcon = dataType.icon; + property.dataTypeName = dataType.name; }); - - }); - } - - // convert legacy icons - convertLegacyIcons(contentType); - - // sort properties after sort order - angular.forEach(contentType.groups, function(group){ - group.properties = $filter('orderBy')(group.properties, 'sortOrder'); - }); - - // insert template on new doc types - if (!$routeParams.notemplate && contentType.id === 0) { - contentType.defaultTemplate = contentTypeHelper.insertDefaultTemplatePlaceholder(contentType.defaultTemplate); - contentType.allowedTemplates = contentTypeHelper.insertTemplatePlaceholder(contentType.allowedTemplates); - } - - //set a shared state - editorState.set(contentType); - - vm.contentType = contentType; - - } - - function convertLegacyIcons(contentType) { - - // convert icons for composite content types - iconHelper.formatContentTypeIcons(contentType.availableCompositeContentTypes); - - // make array to store contentType icon - var contentTypeArray = []; - - // push icon to array - contentTypeArray.push({"icon":contentType.icon}); - - // run through icon method - iconHelper.formatContentTypeIcons(contentTypeArray); - - // set icon back on contentType - contentType.icon = contentTypeArray[0].icon; - - } - - function getDataTypeDetails(property) { - - if( property.propertyState !== "init" ) { - - dataTypeResource.getById(property.dataTypeId) - .then(function(dataType) { - property.dataTypeIcon = dataType.icon; - property.dataTypeName = dataType.name; - }); - } - } + } + } - /** Syncs the content type to it's tree node - this occurs on first load and after saving */ - function syncTreeNode(dt, path, initialLoad) { + /** Syncs the content type to it's tree node - this occurs on first load and after saving */ + function syncTreeNode(dt, path, initialLoad) { - navigationService.syncTree({ tree: "documenttypes", path: path.split(","), forceReload: initialLoad !== true }).then(function (syncArgs) { - vm.currentNode = syncArgs.node; - }); + navigationService.syncTree({ tree: "documenttypes", path: path.split(","), forceReload: initialLoad !== true }).then(function (syncArgs) { + vm.currentNode = syncArgs.node; + }); - } + } - } + } - angular.module("umbraco").controller("Umbraco.Editors.DocumentTypes.EditController", DocumentTypesEditController); + angular.module("umbraco").controller("Umbraco.Editors.DocumentTypes.EditController", DocumentTypesEditController); })(); 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 b5e8665227..9bebaa8524 100644 --- a/src/Umbraco.Web.UI.Client/src/views/media/edit.html +++ b/src/Umbraco.Web.UI.Client/src/views/media/edit.html @@ -12,7 +12,10 @@ + menu="page.menu" + hide-icon="true" + hide-description="true" + hide-alias="true"> diff --git a/src/Umbraco.Web.UI.Client/src/views/media/recyclebin.html b/src/Umbraco.Web.UI.Client/src/views/media/recyclebin.html index ef1b88bcf0..bfdfcbf8bf 100644 --- a/src/Umbraco.Web.UI.Client/src/views/media/recyclebin.html +++ b/src/Umbraco.Web.UI.Client/src/views/media/recyclebin.html @@ -2,7 +2,10 @@ + name-locked="page.nameLocked" + hide-icon="true" + hide-description="true" + hide-alias="true"> diff --git a/src/Umbraco.Web.UI.Client/src/views/mediatypes/edit.controller.js b/src/Umbraco.Web.UI.Client/src/views/mediatypes/edit.controller.js index c872c593d9..14bfbbc580 100644 --- a/src/Umbraco.Web.UI.Client/src/views/mediatypes/edit.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/mediatypes/edit.controller.js @@ -6,240 +6,240 @@ * @description * The controller for the media type editor */ -(function() { - "use strict"; +(function () { + "use strict"; - function MediaTypesEditController($scope, $routeParams, mediaTypeResource, dataTypeResource, editorState, contentEditingHelper, formHelper, navigationService, iconHelper, contentTypeHelper, notificationsService, $filter) { + function MediaTypesEditController($scope, $routeParams, mediaTypeResource, dataTypeResource, editorState, contentEditingHelper, formHelper, navigationService, iconHelper, contentTypeHelper, notificationsService, $filter, $q, localizationService) { - var vm = this; + var vm = this; - vm.save = save; + vm.save = save; - vm.currentNode = null; - vm.contentType = {}; - vm.page = {}; - vm.page.loading = false; - vm.page.saveButtonState = "init"; - vm.page.navigation = [ + vm.currentNode = null; + vm.contentType = {}; + vm.page = {}; + vm.page.loading = false; + vm.page.saveButtonState = "init"; + vm.page.navigation = [ { - "name": "Design", - "icon": "icon-document-dashed-line", - "view": "views/mediatypes/views/design/design.html", - "active": true + "name": "Design", + "icon": "icon-document-dashed-line", + "view": "views/mediatypes/views/design/design.html", + "active": true }, { - "name": "List view", - "icon": "icon-list", - "view": "views/mediatypes/views/listview/listview.html" + "name": "List view", + "icon": "icon-list", + "view": "views/mediatypes/views/listview/listview.html" }, { - "name": "Permissions", - "icon": "icon-keychain", - "view": "views/mediatypes/views/permissions/permissions.html" + "name": "Permissions", + "icon": "icon-keychain", + "view": "views/mediatypes/views/permissions/permissions.html" } - ]; + ]; - vm.page.keyboardShortcutsOverview = [ + vm.page.keyboardShortcutsOverview = [ { - "name": "Sections", - "shortcuts": [ + "name": "Sections", + "shortcuts": [ { - "description": "Navigate sections", - "keys": [{"key": "1"}, {"key": "3"}], - "keyRange": true + "description": "Navigate sections", + "keys": [{ "key": "1" }, { "key": "3" }], + "keyRange": true } - ] + ] }, { - "name": "Design", - "shortcuts": [ + "name": "Design", + "shortcuts": [ { - "description": "Add tab", - "keys": [{"key": "alt"},{"key": "shift"},{"key": "t"}] + "description": "Add tab", + "keys": [{ "key": "alt" }, { "key": "shift" }, { "key": "t" }] }, { - "description": "Add property", - "keys": [{"key": "alt"},{"key": "shift"},{"key": "p"}] + "description": "Add property", + "keys": [{ "key": "alt" }, { "key": "shift" }, { "key": "p" }] }, { - "description": "Add editor", - "keys": [{"key": "alt"},{"key": "shift"},{"key": "e"}] + "description": "Add editor", + "keys": [{ "key": "alt" }, { "key": "shift" }, { "key": "e" }] }, { - "description": "Edit data type", - "keys": [{"key": "alt"},{"key": "shift"},{"key": "d"}] + "description": "Edit data type", + "keys": [{ "key": "alt" }, { "key": "shift" }, { "key": "d" }] } - ] + ] + }, + { + "name": "List view", + "shortcuts": [ + { + "description": "Toggle list view", + "keys": [{ "key": "alt" }, { "key": "shift" }, { "key": "l" }] + } + ] }, { - "name": "List view", - "shortcuts": [ + "name": "Permissions", + "shortcuts": [ { - "description": "Toggle list view", - "keys": [{"key": "alt"},{"key": "shift"},{"key": "l"}] - } - ] - }, - { - "name": "Permissions", - "shortcuts": [ - { - "description": "Toggle allow as root", - "keys": [{"key": "alt"},{"key": "shift"},{"key": "r"}] + "description": "Toggle allow as root", + "keys": [{ "key": "alt" }, { "key": "shift" }, { "key": "r" }] }, { - "description": "Add child node", - "keys": [{"key": "alt"},{"key": "shift"},{"key": "c"}] + "description": "Add child node", + "keys": [{ "key": "alt" }, { "key": "shift" }, { "key": "c" }] } - ] + ] } - ]; + ]; - if ($routeParams.create) { + if ($routeParams.create) { - vm.page.loading = true; + vm.page.loading = true; - //we are creating so get an empty data type item - mediaTypeResource.getScaffold($routeParams.id) - .then(function(dt) { - init(dt); + //we are creating so get an empty data type item + mediaTypeResource.getScaffold($routeParams.id) + .then(function (dt) { + init(dt); - vm.page.loading = false; + vm.page.loading = false; }); - } - else { + } + else { - vm.page.loading = true; + vm.page.loading = true; - mediaTypeResource.getById($routeParams.id).then(function(dt){ - init(dt); + mediaTypeResource.getById($routeParams.id).then(function (dt) { + init(dt); - syncTreeNode(vm.contentType, dt.path, true); + syncTreeNode(vm.contentType, dt.path, true); - vm.page.loading = false; - }); - } + vm.page.loading = false; + }); + } - /* ---------- SAVE ---------- */ + /* ---------- SAVE ---------- */ - function save() { + function save() { - // validate form - if (formHelper.submitForm({ scope: $scope })) { + var deferred = $q.defer(); - formHelper.resetForm({ scope: $scope }); + vm.page.saveButtonState = "busy"; - // if form validates - perform save - performSave(); + // reformat allowed content types to array if id's + vm.contentType.allowedContentTypes = contentTypeHelper.createIdArray(vm.contentType.allowedContentTypes); - } + contentEditingHelper.contentEditorPerformSave({ + statusMessage: "Saving...", + saveMethod: mediaTypeResource.save, + scope: $scope, + content: vm.contentType, + //no-op for rebind callback... we don't really need to rebind for content types + rebindCallback: angular.noop + }).then(function (data) { + //success + syncTreeNode(vm.contentType, data.path); - } + vm.page.saveButtonState = "success"; - function performSave() { + deferred.resolve(data); + }, function (err) { + //error + if (err) { + editorState.set($scope.content); + } + else { + localizationService.localize("speechBubbles_validationFailedHeader").then(function (headerValue) { + localizationService.localize("speechBubbles_validationFailedMessage").then(function (msgValue) { + notificationsService.error(headerValue, msgValue); + }); + }); + } - vm.page.saveButtonState = "busy"; + vm.page.saveButtonState = "error"; - // reformat allowed content types to array if id's - vm.contentType.allowedContentTypes = contentTypeHelper.createIdArray(vm.contentType.allowedContentTypes); + deferred.reject(err); + }); - mediaTypeResource.save(vm.contentType).then(function(dt){ + return deferred.promise; - formHelper.resetForm({ scope: $scope, notifications: dt.notifications }); - contentEditingHelper.handleSuccessfulSave({ - scope: $scope, - savedContent: dt, - rebindCallback: function() { + } - } - }); + function init(contentType) { - notificationsService.success("Media type saved"); - //post save logic here -the saved doctype returns as a new object - init(dt); + // set all tab to inactive + if (contentType.groups.length !== 0) { + angular.forEach(contentType.groups, function (group) { - syncTreeNode(vm.contentType, dt.path); + angular.forEach(group.properties, function (property) { + // get data type details for each property + getDataTypeDetails(property); + }); - vm.page.saveButtonState = "success"; + }); + } - }); + // convert legacy icons + convertLegacyIcons(contentType); - } + // sort properties after sort order + angular.forEach(contentType.groups, function (group) { + group.properties = $filter('orderBy')(group.properties, 'sortOrder'); + }); + //set a shared state + editorState.set(contentType); - function init(contentType){ + vm.contentType = contentType; - // set all tab to inactive - if( contentType.groups.length !== 0 ) { - angular.forEach(contentType.groups, function(group){ + } - angular.forEach(group.properties, function(property){ - // get data type details for each property - getDataTypeDetails(property); + function convertLegacyIcons(contentType) { + + // convert icons for composite content types + iconHelper.formatContentTypeIcons(contentType.availableCompositeContentTypes); + + // make array to store contentType icon + var contentTypeArray = []; + + // push icon to array + contentTypeArray.push({ "icon": contentType.icon }); + + // run through icon method + iconHelper.formatContentTypeIcons(contentTypeArray); + + // set icon back on contentType + contentType.icon = contentTypeArray[0].icon; + + } + + function getDataTypeDetails(property) { + + if (property.propertyState !== "init") { + + dataTypeResource.getById(property.dataTypeId) + .then(function (dataType) { + property.dataTypeIcon = dataType.icon; + property.dataTypeName = dataType.name; }); - - }); - } - - // convert legacy icons - convertLegacyIcons(contentType); - - // sort properties after sort order - angular.forEach(contentType.groups, function(group){ - group.properties = $filter('orderBy')(group.properties, 'sortOrder'); - }); - - //set a shared state - editorState.set(contentType); - - vm.contentType = contentType; - - } - - function convertLegacyIcons(contentType) { - - // convert icons for composite content types - iconHelper.formatContentTypeIcons(contentType.availableCompositeContentTypes); - - // make array to store contentType icon - var contentTypeArray = []; - - // push icon to array - contentTypeArray.push({"icon":contentType.icon}); - - // run through icon method - iconHelper.formatContentTypeIcons(contentTypeArray); - - // set icon back on contentType - contentType.icon = contentTypeArray[0].icon; - - } - - function getDataTypeDetails(property) { - - if( property.propertyState !== "init" ) { - - dataTypeResource.getById(property.dataTypeId) - .then(function(dataType) { - property.dataTypeIcon = dataType.icon; - property.dataTypeName = dataType.name; - }); - } - } + } + } - /** Syncs the content type to it's tree node - this occurs on first load and after saving */ - function syncTreeNode(dt, path, initialLoad) { + /** Syncs the content type to it's tree node - this occurs on first load and after saving */ + function syncTreeNode(dt, path, initialLoad) { - navigationService.syncTree({ tree: "mediatypes", path: path.split(","), forceReload: initialLoad !== true }).then(function (syncArgs) { - vm.currentNode = syncArgs.node; - }); + navigationService.syncTree({ tree: "mediatypes", path: path.split(","), forceReload: initialLoad !== true }).then(function (syncArgs) { + vm.currentNode = syncArgs.node; + }); - } + } - } + } - angular.module("umbraco").controller("Umbraco.Editors.MediaTypes.EditController", MediaTypesEditController); + angular.module("umbraco").controller("Umbraco.Editors.MediaTypes.EditController", MediaTypesEditController); })(); diff --git a/src/Umbraco.Web.UI.Client/src/views/members/edit.html b/src/Umbraco.Web.UI.Client/src/views/members/edit.html index 08abe59050..3492275c08 100644 --- a/src/Umbraco.Web.UI.Client/src/views/members/edit.html +++ b/src/Umbraco.Web.UI.Client/src/views/members/edit.html @@ -13,7 +13,10 @@ name="content.name" name-locked="page.nameLocked" tabs="content.tabs" - menu="page.menu"> + menu="page.menu" + hide-icon="true" + hide-description="true" + hide-alias="true"> diff --git a/src/Umbraco.Web.UI.Client/src/views/members/list.html b/src/Umbraco.Web.UI.Client/src/views/members/list.html index 99fc997f16..7976677432 100644 --- a/src/Umbraco.Web.UI.Client/src/views/members/list.html +++ b/src/Umbraco.Web.UI.Client/src/views/members/list.html @@ -10,7 +10,10 @@ name="content.name" name-locked="page.lockedName" tabs="content.tabs" - menu="page.menu"> + menu="page.menu" + hide-icon="true" + hide-description="true" + hide-alias="true"> diff --git a/src/Umbraco.Web.UI.Client/src/views/membertypes/edit.controller.js b/src/Umbraco.Web.UI.Client/src/views/membertypes/edit.controller.js index 499650c9c5..bf830c8915 100644 --- a/src/Umbraco.Web.UI.Client/src/views/membertypes/edit.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/membertypes/edit.controller.js @@ -6,188 +6,188 @@ * @description * The controller for the member type editor */ - (function() { - "use strict"; +(function () { + "use strict"; - function MemberTypesEditController($scope, $rootScope, $routeParams, $log, $filter, memberTypeResource, dataTypeResource, editorState, iconHelper, formHelper, navigationService, contentEditingHelper, notificationsService) { + function MemberTypesEditController($scope, $rootScope, $routeParams, $log, $filter, memberTypeResource, dataTypeResource, editorState, iconHelper, formHelper, navigationService, contentEditingHelper, notificationsService, $q, localizationService) { - var vm = this; + var vm = this; - vm.save = save; + vm.save = save; - vm.currentNode = null; - vm.contentType = {}; - vm.page = {}; + vm.currentNode = null; + vm.contentType = {}; + vm.page = {}; vm.page.loading = false; vm.page.saveButtonState = "init"; - vm.page.navigation = [ + vm.page.navigation = [ { - "name": "Design", - "icon": "icon-document-dashed-line", - "view": "views/membertypes/views/design/design.html", - "active": true + "name": "Design", + "icon": "icon-document-dashed-line", + "view": "views/membertypes/views/design/design.html", + "active": true } - ]; + ]; - vm.page.keyboardShortcutsOverview = [ + vm.page.keyboardShortcutsOverview = [ { - "shortcuts": [ + "shortcuts": [ { - "description": "Add tab", - "keys": [{"key": "alt"},{"key": "shift"},{"key": "t"}] + "description": "Add tab", + "keys": [{ "key": "alt" }, { "key": "shift" }, { "key": "t" }] }, { - "description": "Add property", - "keys": [{"key": "alt"},{"key": "shift"},{"key": "p"}] + "description": "Add property", + "keys": [{ "key": "alt" }, { "key": "shift" }, { "key": "p" }] }, { - "description": "Add editor", - "keys": [{"key": "alt"},{"key": "shift"},{"key": "e"}] + "description": "Add editor", + "keys": [{ "key": "alt" }, { "key": "shift" }, { "key": "e" }] }, { - "description": "Edit data type", - "keys": [{"key": "alt"},{"key": "shift"},{"key": "d"}] + "description": "Edit data type", + "keys": [{ "key": "alt" }, { "key": "shift" }, { "key": "d" }] } - ] + ] } - ]; + ]; - if ($routeParams.create) { + if ($routeParams.create) { - vm.page.loading = true; + vm.page.loading = true; - //we are creating so get an empty data type item - memberTypeResource.getScaffold($routeParams.id) - .then(function(dt) { - init(dt); + //we are creating so get an empty data type item + memberTypeResource.getScaffold($routeParams.id) + .then(function (dt) { + init(dt); - vm.page.loading = false; + vm.page.loading = false; }); - } - else { + } + else { - vm.page.loading = true; + vm.page.loading = true; - memberTypeResource.getById($routeParams.id).then(function(dt){ - init(dt); + memberTypeResource.getById($routeParams.id).then(function (dt) { + init(dt); - syncTreeNode(vm.contentType, dt.path, true); + syncTreeNode(vm.contentType, dt.path, true); - vm.page.loading = false; - }); - } + vm.page.loading = false; + }); + } - function save() { + function save() { - // validate form - if (formHelper.submitForm({ scope: $scope })) { + var deferred = $q.defer(); - formHelper.resetForm({ scope: $scope }); + vm.page.saveButtonState = "busy"; + + contentEditingHelper.contentEditorPerformSave({ + statusMessage: "Saving...", + saveMethod: memberTypeResource.save, + scope: $scope, + content: vm.contentType, + //no-op for rebind callback... we don't really need to rebind for content types + rebindCallback: angular.noop + }).then(function (data) { + //success + syncTreeNode(vm.contentType, data.path); - // if form validates - perform save - performSave(); + vm.page.saveButtonState = "success"; - } + deferred.resolve(data); + }, function (err) { + //error + if (err) { + editorState.set($scope.content); + } + else { + localizationService.localize("speechBubbles_validationFailedHeader").then(function (headerValue) { + localizationService.localize("speechBubbles_validationFailedMessage").then(function (msgValue) { + notificationsService.error(headerValue, msgValue); + }); + }); + } - } + vm.page.saveButtonState = "error"; - function performSave() { + deferred.reject(err); + }); - vm.page.saveButtonState = "busy"; + return deferred.promise; - memberTypeResource.save(vm.contentType).then(function(dt){ + } - formHelper.resetForm({ scope: $scope, notifications: dt.notifications }); - contentEditingHelper.handleSuccessfulSave({ - scope: $scope, - savedContent: dt, - rebindCallback: function() { + function init(contentType) { - } - }); + // set all tab to inactive + if (contentType.groups.length !== 0) { + angular.forEach(contentType.groups, function (group) { - notificationsService.success("Member type saved"); - //post save logic here -the saved doctype returns as a new object - init(dt); + angular.forEach(group.properties, function (property) { + // get data type details for each property + getDataTypeDetails(property); + }); - syncTreeNode(vm.contentType, dt.path); + }); + } - vm.page.saveButtonState = "success"; + // convert legacy icons + convertLegacyIcons(contentType); + // sort properties after sort order + angular.forEach(contentType.groups, function (group) { + group.properties = $filter('orderBy')(group.properties, 'sortOrder'); + }); - }); + //set a shared state + editorState.set(contentType); - } + vm.contentType = contentType; - function init(contentType){ + } - // set all tab to inactive - if( contentType.groups.length !== 0 ) { - angular.forEach(contentType.groups, function(group){ + function convertLegacyIcons(contentType) { - angular.forEach(group.properties, function(property){ - // get data type details for each property - getDataTypeDetails(property); + // make array to store contentType icon + var contentTypeArray = []; + + // push icon to array + contentTypeArray.push({ "icon": contentType.icon }); + + // run through icon method + iconHelper.formatContentTypeIcons(contentTypeArray); + + // set icon back on contentType + contentType.icon = contentTypeArray[0].icon; + + } + + function getDataTypeDetails(property) { + + if (property.propertyState !== "init") { + + dataTypeResource.getById(property.dataTypeId) + .then(function (dataType) { + property.dataTypeIcon = dataType.icon; + property.dataTypeName = dataType.name; }); + } + } - }); - } + /** Syncs the content type to it's tree node - this occurs on first load and after saving */ + function syncTreeNode(dt, path, initialLoad) { - // convert legacy icons - convertLegacyIcons(contentType); + navigationService.syncTree({ tree: "membertypes", path: path.split(","), forceReload: initialLoad !== true }).then(function (syncArgs) { + vm.currentNode = syncArgs.node; + }); - // sort properties after sort order - angular.forEach(contentType.groups, function(group){ - group.properties = $filter('orderBy')(group.properties, 'sortOrder'); - }); - - //set a shared state - editorState.set(contentType); - - vm.contentType = contentType; - - } - - function convertLegacyIcons(contentType) { - - // make array to store contentType icon - var contentTypeArray = []; - - // push icon to array - contentTypeArray.push({"icon":contentType.icon}); - - // run through icon method - iconHelper.formatContentTypeIcons(contentTypeArray); - - // set icon back on contentType - contentType.icon = contentTypeArray[0].icon; - - } - - function getDataTypeDetails(property) { - - if( property.propertyState !== "init" ) { - - dataTypeResource.getById(property.dataTypeId) - .then(function(dataType) { - property.dataTypeIcon = dataType.icon; - property.dataTypeName = dataType.name; - }); - } - } - - /** Syncs the content type to it's tree node - this occurs on first load and after saving */ - function syncTreeNode(dt, path, initialLoad) { - - navigationService.syncTree({ tree: "membertypes", path: path.split(","), forceReload: initialLoad !== true }).then(function (syncArgs) { - vm.currentNode = syncArgs.node; - }); - - } + } - } + } - angular.module("umbraco").controller("Umbraco.Editors.MemberTypes.EditController", MemberTypesEditController); + angular.module("umbraco").controller("Umbraco.Editors.MemberTypes.EditController", MemberTypesEditController); })(); diff --git a/src/Umbraco.Web.UI.Client/test/unit/app/content/edit-content-controller.spec.js b/src/Umbraco.Web.UI.Client/test/unit/app/content/edit-content-controller.spec.js index 0fc5a4cedd..0d9b8a29e2 100644 --- a/src/Umbraco.Web.UI.Client/test/unit/app/content/edit-content-controller.spec.js +++ b/src/Umbraco.Web.UI.Client/test/unit/app/content/edit-content-controller.spec.js @@ -5,7 +5,7 @@ describe('edit content controller tests', function () { beforeEach(module('umbraco')); //inject the contentMocks service - beforeEach(inject(function ($rootScope, $controller, angularHelper, $httpBackend, contentMocks, entityMocks, mocksUtils) { + beforeEach(inject(function ($rootScope, $controller, angularHelper, $httpBackend, contentMocks, entityMocks, mocksUtils, localizationMocks) { //for these tests we don't want any authorization to occur mocksUtils.disableAuth(); @@ -17,6 +17,7 @@ describe('edit content controller tests', function () { //see /mocks/content.mocks.js for how its setup contentMocks.register(); entityMocks.register(); + localizationMocks.register(); //this controller requires an angular form controller applied to it scope.contentForm = angularHelper.getNullForm("contentForm"); diff --git a/src/Umbraco.Web.UI.Client/test/unit/app/media/edit-media-controller.spec.js b/src/Umbraco.Web.UI.Client/test/unit/app/media/edit-media-controller.spec.js index 0d2b1a7bd9..35179c5646 100644 --- a/src/Umbraco.Web.UI.Client/test/unit/app/media/edit-media-controller.spec.js +++ b/src/Umbraco.Web.UI.Client/test/unit/app/media/edit-media-controller.spec.js @@ -5,7 +5,7 @@ describe('edit media controller tests', function () { beforeEach(module('umbraco')); //inject the contentMocks service - beforeEach(inject(function ($rootScope, $controller, angularHelper, $httpBackend, mediaMocks, entityMocks, mocksUtils) { + beforeEach(inject(function ($rootScope, $controller, angularHelper, $httpBackend, mediaMocks, entityMocks, mocksUtils, localizationMocks) { //for these tests we don't want any authorization to occur mocksUtils.disableAuth(); @@ -16,6 +16,7 @@ describe('edit media controller tests', function () { //see /mocks/content.mocks.js for how its setup mediaMocks.register(); entityMocks.register(); + localizationMocks.register(); //this controller requires an angular form controller applied to it scope.contentForm = angularHelper.getNullForm("contentForm"); diff --git a/src/Umbraco.Web.UI.Client/test/unit/app/propertyeditors/content-picker-controller.spec.js b/src/Umbraco.Web.UI.Client/test/unit/app/propertyeditors/content-picker-controller.spec.js index 3444f39407..e6d1312109 100644 --- a/src/Umbraco.Web.UI.Client/test/unit/app/propertyeditors/content-picker-controller.spec.js +++ b/src/Umbraco.Web.UI.Client/test/unit/app/propertyeditors/content-picker-controller.spec.js @@ -5,7 +5,7 @@ describe('Content picker controller tests', function () { beforeEach(module('umbraco')); //inject the contentMocks service - beforeEach(inject(function ($rootScope, $controller, angularHelper, $httpBackend, entityMocks, mocksUtils) { + beforeEach(inject(function ($rootScope, $controller, angularHelper, $httpBackend, entityMocks, mocksUtils, localizationMocks) { //for these tests we don't want any authorization to occur mocksUtils.disableAuth(); @@ -28,6 +28,7 @@ describe('Content picker controller tests', function () { //have the contentMocks register its expect urls on the httpbackend //see /mocks/content.mocks.js for how its setup entityMocks.register(); + localizationMocks.register(); controller = $controller('Umbraco.PropertyEditors.ContentPickerController', { $scope: scope, 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 0fa55c2a11..ad336cf544 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 @@ -8,7 +8,9 @@ describe('contentEditingHelper tests', function () { //Only for 1.2: beforeEach(module('ngRoute')); - beforeEach(inject(function ($injector) { + beforeEach(inject(function ($injector, localizationMocks) { + localizationMocks.register(); + contentEditingHelper = $injector.get('contentEditingHelper'); $routeParams = $injector.get('$routeParams'); serverValidationManager = $injector.get('serverValidationManager'); @@ -108,7 +110,7 @@ describe('contentEditingHelper tests', function () { var allProps = contentEditingHelper.getAllProps(content); //act - formHelper.handleServerValidation({ "Property.bodyText": ["Required"] }); + formHelper.handleServerValidation({ "_Properties.bodyText": ["Required"] }); //assert expect(serverValidationManager.items.length).toBe(1); @@ -124,7 +126,7 @@ describe('contentEditingHelper tests', function () { var allProps = contentEditingHelper.getAllProps(content); //act - formHelper.handleServerValidation({ "Property.bodyText.value": ["Required"] }); + formHelper.handleServerValidation({ "_Properties.bodyText.value": ["Required"] }); //assert expect(serverValidationManager.items.length).toBe(1); @@ -144,8 +146,8 @@ describe('contentEditingHelper tests', function () { { "Name": ["Required"], "UpdateDate": ["Invalid date"], - "Property.bodyText.value": ["Required field"], - "Property.textarea": ["Invalid format"] + "_Properties.bodyText.value": ["Required field"], + "_Properties.textarea": ["Invalid format"] }); //assert diff --git a/src/Umbraco.Web.UI/umbraco/config/lang/en.xml b/src/Umbraco.Web.UI/umbraco/config/lang/en.xml index 5ffa141bc2..58eab32bed 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/en.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/en.xml @@ -785,6 +785,8 @@ To manage your website, simply open the Umbraco back office and start adding con
Do not close this window during sorting]]>
+ Validation + Validation errors must be fixed before the item can be saved Failed Insufficient user permissions, could not complete the operation Cancelled diff --git a/src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml b/src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml index c30e771c27..6e7babba44 100644 --- a/src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml +++ b/src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml @@ -785,6 +785,8 @@ To manage your website, simply open the Umbraco back office and start adding con
Do not close this window during sorting]]>
+ Validation + Validation errors must be fixed before the item can be saved Failed Insufficient user permissions, could not complete the operation Cancelled diff --git a/src/Umbraco.Web/Editors/ContentTypeController.cs b/src/Umbraco.Web/Editors/ContentTypeController.cs index 8e52819b2d..aac243f727 100644 --- a/src/Umbraco.Web/Editors/ContentTypeController.cs +++ b/src/Umbraco.Web/Editors/ContentTypeController.cs @@ -16,7 +16,7 @@ using Newtonsoft.Json; using Umbraco.Core.PropertyEditors; using System; using System.Net.Http; -using System.Text; +using Umbraco.Core.Services; namespace Umbraco.Web.Editors { @@ -81,6 +81,36 @@ namespace Umbraco.Web.Editors return Request.CreateResponse(HttpStatusCode.OK); } + /// + /// Gets all user defined properties. + /// + /// + public IEnumerable GetAllPropertyTypeAliases() + { + return ApplicationContext.Services.ContentTypeService.GetAllPropertyTypeAliases(); + } + + public ContentPropertyDisplay GetPropertyTypeScaffold(int id) + { + var dataTypeDiff = Services.DataTypeService.GetDataTypeDefinitionById(id); + + if (dataTypeDiff == null) + { + throw new HttpResponseException(HttpStatusCode.NotFound); + } + + var preVals = UmbracoContext.Current.Application.Services.DataTypeService.GetPreValuesCollectionByDataTypeId(id); + var editor = PropertyEditorResolver.Current.GetByAlias(dataTypeDiff.PropertyEditorAlias); + + return new ContentPropertyDisplay() + { + Editor = dataTypeDiff.PropertyEditorAlias, + Validation = new PropertyTypeValidation() { }, + View = editor.ValueEditor.View, + Config = editor.PreValueEditor.ConvertDbToEditor(editor.DefaultPreValues, preVals) + }; + } + /// /// Deletes a document type container wth a given ID /// @@ -90,19 +120,21 @@ namespace Umbraco.Web.Editors [HttpPost] public HttpResponseMessage DeleteContainerById(int id) { + //TODO: This needs to be implemented correctly + var foundType = Services.EntityService.Get(id); if (foundType == null) { throw new HttpResponseException(HttpStatusCode.NotFound); } - if(foundType.HasChildren()) + if (foundType.HasChildren()) { throw new HttpResponseException(HttpStatusCode.Forbidden); } //TODO: what service to use to delete? - + return Request.CreateResponse(HttpStatusCode.OK); } @@ -115,109 +147,39 @@ namespace Umbraco.Web.Editors : Request.CreateValidationErrorResponse(result.Exception.Message); } - /// - /// Move a content type to a container - /// - /// - /// - public HttpResponseMessage PostMove(MoveOrCopy move) + + + public ContentTypeDisplay PostSave(ContentTypeSave contentTypeSave) { - //TODO, validate move - - //TODO, service method for moving - - var response = Request.CreateResponse(HttpStatusCode.OK); - - //TODO, response - response.Content = new StringContent("", Encoding.UTF8, "application/json"); - return response; - } - - public ContentTypeDisplay PostSave(ContentTypeDisplay contentType) - { - var ctService = Services.ContentTypeService; - - //TODO: warn on content type alias conflicts - //TODO: warn on property alias conflicts - - //TODO: Validate the submitted model - - var ctId = Convert.ToInt32(contentType.Id); - - //filter out empty properties - contentType.Groups = contentType.Groups.Where(x => x.Name.IsNullOrWhiteSpace() == false).ToList(); - foreach (var group in contentType.Groups) - { - group.Properties = group.Properties.Where(x => x.Alias.IsNullOrWhiteSpace() == false).ToList(); - } - - if (ctId > 0) - { - //its an update to an existing - IContentType found = ctService.GetContentType(ctId); - if (found == null) - throw new HttpResponseException(HttpStatusCode.NotFound); - - Mapper.Map(contentType, found); - ctService.Save(found); - - //map the saved item back to the content type (it should now get id etc set) - Mapper.Map(found, contentType); - return contentType; - } - else - { - //ensure alias is set - if (string.IsNullOrEmpty(contentType.Alias)) - contentType.Alias = contentType.Name.ToSafeAlias(); - - //set id to null to ensure its handled as a new type - contentType.Id = null; - contentType.CreateDate = DateTime.Now; - contentType.UpdateDate = DateTime.Now; - - - //create a default template if it doesnt exist -but only if default template is == to the content type - if (contentType.DefaultTemplate != null && contentType.DefaultTemplate.Alias == contentType.Alias) + var savedCt = PerformPostSave( + contentTypeSave: contentTypeSave, + getContentType: i => Services.ContentTypeService.GetContentType(i), + saveContentType: type => Services.ContentTypeService.Save(type), + beforeCreateNew: ctSave => { - var template = Services.FileService.GetTemplate(contentType.Alias); - if (template == null) + //create a default template if it doesnt exist -but only if default template is == to the content type + //TODO: Is this really what we want? What if we don't want any template assigned at all ? + if (ctSave.DefaultTemplate.IsNullOrWhiteSpace() == false && ctSave.DefaultTemplate == ctSave.Alias) { - template = new Template(contentType.Name, contentType.Alias); - Services.FileService.SaveTemplate(template); + var template = Services.FileService.GetTemplate(ctSave.Alias); + if (template == null) + { + template = new Template(ctSave.Name, ctSave.Alias); + Services.FileService.SaveTemplate(template); + } + + //make sure the template alias is set on the default and allowed template so we can map it back + ctSave.DefaultTemplate = template.Alias; } + }); - //make sure the template id is set on the default and allowed template - contentType.DefaultTemplate.Id = template.Id; - var found = contentType.AllowedTemplates.FirstOrDefault(x => x.Alias == contentType.Alias); - if (found != null) - found.Id = template.Id; - } + var display = Mapper.Map(savedCt); - //check if the type is trying to allow type 0 below itself - id zero refers to the currently unsaved type - //always filter these 0 types out - var allowItselfAsChild = false; - if (contentType.AllowedContentTypes != null) - { - allowItselfAsChild = contentType.AllowedContentTypes.Any(x => x == 0); - contentType.AllowedContentTypes = contentType.AllowedContentTypes.Where(x => x > 0).ToList(); - } + display.AddSuccessNotification( + Services.TextService.Localize("speechBubbles/contentTypeSavedHeader"), + string.Empty); - //save as new - var newCt = Mapper.Map(contentType); - ctService.Save(newCt); - - //we need to save it twice to allow itself under itself. - if (allowItselfAsChild) - { - newCt.AddContentType(newCt); - ctService.Save(newCt); - } - - //map the saved item back to the content type (it should now get id etc set) - Mapper.Map(newCt, contentType); - return contentType; - } + return display; } /// @@ -296,6 +258,6 @@ namespace Umbraco.Web.Editors return basics; } - + } } \ No newline at end of file diff --git a/src/Umbraco.Web/Editors/ContentTypeControllerBase.cs b/src/Umbraco.Web/Editors/ContentTypeControllerBase.cs index 2ece1f21d8..6f3f96d365 100644 --- a/src/Umbraco.Web/Editors/ContentTypeControllerBase.cs +++ b/src/Umbraco.Web/Editors/ContentTypeControllerBase.cs @@ -14,6 +14,7 @@ using Umbraco.Core.PropertyEditors; using Umbraco.Core.Services; using Umbraco.Web.Models.ContentEditing; using Umbraco.Web.Mvc; +using Umbraco.Web.WebApi; using Constants = Umbraco.Core.Constants; namespace Umbraco.Web.Editors @@ -22,6 +23,7 @@ namespace Umbraco.Web.Editors /// Am abstract API controller providing functionality used for dealing with content and media types /// [PluginController("UmbracoApi")] + [PrefixlessBodyModelValidator] public abstract class ContentTypeControllerBase : UmbracoAuthorizedJsonController { private ICultureDictionary _cultureDictionary; @@ -43,7 +45,7 @@ namespace Umbraco.Web.Editors { } - public DataTypeBasic GetAssignedListViewDataType(int contentTypeId) + protected internal DataTypeBasic GetAssignedListViewDataType(int contentTypeId) { var objectType = Services.EntityService.GetObjectType(contentTypeId); @@ -76,49 +78,41 @@ namespace Umbraco.Web.Editors } /// - /// Gets all user defined properties. + /// Validates the composition and adds errors to the model state if any are found then throws an error response if there are errors /// + /// + /// /// - public IEnumerable GetAllPropertyTypeAliases() + protected void ValidateComposition(ContentTypeSave contentTypeSave, IContentTypeComposition composition) { - return ApplicationContext.Services.ContentTypeService.GetAllPropertyTypeAliases(); - } - - public ContentPropertyDisplay GetPropertyTypeScaffold(int id) - { - var dataTypeDiff = Services.DataTypeService.GetDataTypeDefinitionById(id); - - if (dataTypeDiff == null) + var validateAttempt = Services.ContentTypeService.ValidateComposition(composition); + if (validateAttempt == false) { - throw new HttpResponseException(HttpStatusCode.NotFound); + //if it's not successful then we need to return some model state for the property aliases that + // are duplicated + var propertyAliases = validateAttempt.Result.Distinct(); + foreach (var propertyAlias in propertyAliases) + { + //find the property relating to these + var prop = contentTypeSave.Groups.SelectMany(x => x.Properties).Single(x => x.Alias == propertyAlias); + var group = contentTypeSave.Groups.Single(x => x.Properties.Contains(prop)); + var propIndex = group.Properties.IndexOf(prop); + var groupIndex = contentTypeSave.Groups.IndexOf(group); + + var key = string.Format("Groups[{0}].Properties[{1}].Alias", groupIndex, propIndex); + ModelState.AddModelError(key, "Duplicate property aliases not allowed between compositions"); + } + + var display = Mapper.Map(composition); + //map the 'save' data on top + display = Mapper.Map(contentTypeSave, display); + display.Errors = ModelState.ToErrorDictionary(); + throw new HttpResponseException(Request.CreateValidationErrorResponse(display)); } - var preVals = UmbracoContext.Current.Application.Services.DataTypeService.GetPreValuesCollectionByDataTypeId(id); - var editor = PropertyEditorResolver.Current.GetByAlias(dataTypeDiff.PropertyEditorAlias); - - return new ContentPropertyDisplay() - { - Editor = dataTypeDiff.PropertyEditorAlias, - Validation = new PropertyTypeValidation() { }, - View = editor.ValueEditor.View, - Config = editor.PreValueEditor.ConvertDbToEditor(editor.DefaultPreValues, preVals) - }; } - public dynamic GetSafeAlias(string value, bool camelCase = true) - { - var returnValue = (string.IsNullOrWhiteSpace(value)) ? string.Empty : value.ToSafeAlias(camelCase); - dynamic returnObj = new System.Dynamic.ExpandoObject(); - returnObj.alias = returnValue; - returnObj.original = value; - returnObj.camelCase = camelCase; - - return returnObj; - } - - - - public string TranslateItem(string text) + protected string TranslateItem(string text) { if (text == null) { @@ -132,6 +126,97 @@ namespace Umbraco.Web.Editors return CultureDictionary[text].IfNullOrWhiteSpace(text); } + protected TContentType PerformPostSave( + ContentTypeSave contentTypeSave, + Func getContentType, + Action saveContentType, + bool validateComposition = true, + Action beforeCreateNew = null) + where TContentType : IContentTypeComposition + where TContentTypeDisplay : ContentTypeCompositionDisplay + { + var ctId = Convert.ToInt32(contentTypeSave.Id); + + if (ModelState.IsValid == false) + { + var ct = getContentType(ctId); + //Required data is invalid so we cannot continue + var forDisplay = Mapper.Map(ct); + //map the 'save' data on top + forDisplay = Mapper.Map(contentTypeSave, forDisplay); + forDisplay.Errors = ModelState.ToErrorDictionary(); + throw new HttpResponseException(Request.CreateValidationErrorResponse(forDisplay)); + } + + //filter out empty properties + contentTypeSave.Groups = contentTypeSave.Groups.Where(x => x.Name.IsNullOrWhiteSpace() == false).ToList(); + foreach (var group in contentTypeSave.Groups) + { + group.Properties = group.Properties.Where(x => x.Alias.IsNullOrWhiteSpace() == false).ToList(); + } + + if (ctId > 0) + { + //its an update to an existing + var found = getContentType(ctId); + if (found == null) + throw new HttpResponseException(HttpStatusCode.NotFound); + + Mapper.Map(contentTypeSave, found); + + if (validateComposition) + { + //NOTE: this throws an error response if it is not valid + ValidateComposition(contentTypeSave, found); + } + + saveContentType(found); + + return found; + } + else + { + if (beforeCreateNew != null) + { + beforeCreateNew(contentTypeSave); + } + + //set id to null to ensure its handled as a new type + contentTypeSave.Id = null; + contentTypeSave.CreateDate = DateTime.Now; + contentTypeSave.UpdateDate = DateTime.Now; + + //check if the type is trying to allow type 0 below itself - id zero refers to the currently unsaved type + //always filter these 0 types out + var allowItselfAsChild = false; + if (contentTypeSave.AllowedContentTypes != null) + { + allowItselfAsChild = contentTypeSave.AllowedContentTypes.Any(x => x == 0); + contentTypeSave.AllowedContentTypes = contentTypeSave.AllowedContentTypes.Where(x => x > 0).ToList(); + } + + //save as new + var newCt = Mapper.Map(contentTypeSave); + + if (validateComposition) + { + //NOTE: this throws an error response if it is not valid + ValidateComposition(contentTypeSave, newCt); + } + + saveContentType(newCt); + + //we need to save it twice to allow itself under itself. + if (allowItselfAsChild) + { + //NOTE: This will throw if the composition isn't right... but it shouldn't be at this stage + newCt.AddContentType(newCt); + saveContentType(newCt); + } + return newCt; + } + } + private ICultureDictionary CultureDictionary { get diff --git a/src/Umbraco.Web/Editors/EntityController.cs b/src/Umbraco.Web/Editors/EntityController.cs index 4ff740df7e..596e27e3a5 100644 --- a/src/Umbraco.Web/Editors/EntityController.cs +++ b/src/Umbraco.Web/Editors/EntityController.cs @@ -42,6 +42,23 @@ namespace Umbraco.Web.Editors [PluginController("UmbracoApi")] public class EntityController : UmbracoAuthorizedJsonController { + /// + /// Returns an Umbraco alias given a string + /// + /// + /// + /// + public dynamic GetSafeAlias(string value, bool camelCase = true) + { + var returnValue = (string.IsNullOrWhiteSpace(value)) ? string.Empty : value.ToSafeAlias(camelCase); + dynamic returnObj = new System.Dynamic.ExpandoObject(); + returnObj.alias = returnValue; + returnObj.original = value; + returnObj.camelCase = camelCase; + + return returnObj; + } + /// /// Searches for results based on the entity type /// diff --git a/src/Umbraco.Web/Editors/MediaTypeController.cs b/src/Umbraco.Web/Editors/MediaTypeController.cs index e613cd28b5..fbdaa77e5f 100644 --- a/src/Umbraco.Web/Editors/MediaTypeController.cs +++ b/src/Umbraco.Web/Editors/MediaTypeController.cs @@ -14,7 +14,9 @@ using System.Net; using Umbraco.Core.PropertyEditors; using System; using System.Net.Http; +using Umbraco.Web.WebApi; using ContentType = System.Net.Mime.ContentType; +using Umbraco.Core.Services; namespace Umbraco.Web.Editors { @@ -100,58 +102,20 @@ namespace Umbraco.Web.Editors .Select(Mapper.Map); } - public ContentTypeCompositionDisplay PostSave(ContentTypeCompositionDisplay contentType) + public ContentTypeCompositionDisplay PostSave(ContentTypeSave contentTypeSave) { + var savedCt = PerformPostSave( + contentTypeSave: contentTypeSave, + getContentType: i => Services.ContentTypeService.GetMediaType(i), + saveContentType: type => Services.ContentTypeService.Save(type)); - var ctService = ApplicationContext.Services.ContentTypeService; + var display = Mapper.Map(savedCt); - //TODO: warn on content type alias conflicts - //TODO: warn on property alias conflicts - - //TODO: Validate the submitted model - - //filter out empty properties - contentType.Groups = contentType.Groups.Where(x => x.Name.IsNullOrWhiteSpace() == false).ToList(); - foreach (var group in contentType.Groups) - { - group.Properties = group.Properties.Where(x => x.Alias.IsNullOrWhiteSpace() == false).ToList(); - } - - var ctId = Convert.ToInt32(contentType.Id); - - if (ctId > 0) - { - //its an update to an existing - IMediaType found = ctService.GetMediaType(ctId); - if (found == null) - throw new HttpResponseException(HttpStatusCode.NotFound); - - Mapper.Map(contentType, found); - ctService.Save(found); - - //map the saved item back to the content type (it should now get id etc set) - Mapper.Map(found, contentType); - return contentType; - } - else - { - //ensure alias is set - if (string.IsNullOrEmpty(contentType.Alias)) - contentType.Alias = contentType.Name.ToSafeAlias(); - - contentType.Id = null; - - //save as new - IMediaType newCt = new MediaType(-1); - Mapper.Map(contentType, newCt); - - ctService.Save(newCt); - - //map the saved item back to the content type (it should now get id etc set) - Mapper.Map(newCt, contentType); - return contentType; - } + display.AddSuccessNotification( + Services.TextService.Localize("speechBubbles/contentTypeSavedHeader"), + string.Empty); + return display; } diff --git a/src/Umbraco.Web/Editors/MemberTypeController.cs b/src/Umbraco.Web/Editors/MemberTypeController.cs index 39bfcb77f0..19a89cd963 100644 --- a/src/Umbraco.Web/Editors/MemberTypeController.cs +++ b/src/Umbraco.Web/Editors/MemberTypeController.cs @@ -4,6 +4,7 @@ using System.Web.Security; using AutoMapper; using Umbraco.Core; using Umbraco.Core.Models; +using Umbraco.Core.Services; using Umbraco.Core.Security; using Umbraco.Web.Models.ContentEditing; using Umbraco.Web.Mvc; @@ -18,16 +19,12 @@ using ContentType = System.Net.Mime.ContentType; namespace Umbraco.Web.Editors { - //TODO: We'll need to be careful about the security on this controller, when we start implementing - // methods to modify content types we'll need to enforce security on the individual methods, we - // cannot put security on the whole controller because things like GetAllowedChildren are required for content editing. - + /// /// An API controller used for dealing with content types /// [PluginController("UmbracoApi")] - [UmbracoTreeAuthorize(Constants.Trees.MemberTypes)] - [EnableOverrideAuthorization] + [UmbracoTreeAuthorize(Constants.Trees.MemberTypes)] public class MemberTypeController : ContentTypeControllerBase { /// @@ -105,58 +102,21 @@ namespace Umbraco.Web.Editors return Enumerable.Empty(); } - public ContentTypeCompositionDisplay PostSave(ContentTypeCompositionDisplay contentType) + public ContentTypeCompositionDisplay PostSave(ContentTypeSave contentTypeSave) { + var savedCt = PerformPostSave( + contentTypeSave: contentTypeSave, + getContentType: i => Services.MemberTypeService.Get(i), + saveContentType: type => Services.MemberTypeService.Save(type), + validateComposition: false); - var ctService = ApplicationContext.Services.MemberTypeService; + var display = Mapper.Map(savedCt); - //TODO: warn on content type alias conflicts - //TODO: warn on property alias conflicts - - //TODO: Validate the submitted model - - //filter out empty properties - contentType.Groups = contentType.Groups.Where(x => x.Name.IsNullOrWhiteSpace() == false).ToList(); - foreach (var group in contentType.Groups) - { - group.Properties = group.Properties.Where(x => x.Alias.IsNullOrWhiteSpace() == false).ToList(); - } - - var ctId = Convert.ToInt32(contentType.Id); - - if (ctId > 0) - { - //its an update to an existing - IMemberType found = ctService.Get(ctId); - if (found == null) - throw new HttpResponseException(HttpStatusCode.NotFound); - - Mapper.Map(contentType, found); - ctService.Save(found); - - //map the saved item back to the content type (it should now get id etc set) - Mapper.Map(found, contentType); - return contentType; - } - else - { - //ensure alias is set - if (string.IsNullOrEmpty(contentType.Alias)) - contentType.Alias = contentType.Name.ToSafeAlias(); - - contentType.Id = null; - - //save as new - IMemberType newCt = new MemberType(-1); - Mapper.Map(contentType, newCt); - - ctService.Save(newCt); - - //map the saved item back to the content type (it should now get id etc set) - Mapper.Map(newCt, contentType); - return contentType; - } + display.AddSuccessNotification( + Services.TextService.Localize("speechBubbles/contentTypeSavedHeader"), + string.Empty); + return display; } } } \ No newline at end of file diff --git a/src/Umbraco.Web/ModelStateExtensions.cs b/src/Umbraco.Web/ModelStateExtensions.cs index 08868e4e5f..5df479ce76 100644 --- a/src/Umbraco.Web/ModelStateExtensions.cs +++ b/src/Umbraco.Web/ModelStateExtensions.cs @@ -64,7 +64,7 @@ namespace Umbraco.Web if (!result.MemberNames.Any()) { //add a model state error for the entire property - modelState.AddModelError(string.Format("{0}.{1}", "Properties", propertyAlias), result.ErrorMessage); + modelState.AddModelError(string.Format("{0}.{1}", "_Properties", propertyAlias), result.ErrorMessage); } else { @@ -72,7 +72,7 @@ namespace Umbraco.Web // so that we can try to match it up to a real sub field of this editor foreach (var field in result.MemberNames) { - modelState.AddModelError(string.Format("{0}.{1}.{2}", "Properties", propertyAlias, field), result.ErrorMessage); + modelState.AddModelError(string.Format("{0}.{1}.{2}", "_Properties", propertyAlias, field), result.ErrorMessage); } } } diff --git a/src/Umbraco.Web/Models/ContentEditing/ContentItemDisplayBase.cs b/src/Umbraco.Web/Models/ContentEditing/ContentItemDisplayBase.cs index 3a13a85f78..87de7b435c 100644 --- a/src/Umbraco.Web/Models/ContentEditing/ContentItemDisplayBase.cs +++ b/src/Umbraco.Web/Models/ContentEditing/ContentItemDisplayBase.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.ComponentModel; using System.Runtime.Serialization; using Umbraco.Core.Models; @@ -30,6 +31,7 @@ namespace Umbraco.Web.Models.ContentEditing /// This is used to add custom localized messages/strings to the response for the app to use for localized UI purposes. /// [DataMember(Name = "notifications")] + [ReadOnly(true)] public List Notifications { get; private set; } /// @@ -43,6 +45,7 @@ namespace Umbraco.Web.Models.ContentEditing /// NOTE: The ProperCase is important because when we return ModeState normally it will always be proper case. /// [DataMember(Name = "ModelState")] + [ReadOnly(true)] public IDictionary Errors { get; set; } } } diff --git a/src/Umbraco.Web/Models/ContentEditing/ContentTypeBasic.cs b/src/Umbraco.Web/Models/ContentEditing/ContentTypeBasic.cs index b12233b1b3..f138083143 100644 --- a/src/Umbraco.Web/Models/ContentEditing/ContentTypeBasic.cs +++ b/src/Umbraco.Web/Models/ContentEditing/ContentTypeBasic.cs @@ -1,8 +1,10 @@ using System; +using System.ComponentModel; using System.ComponentModel.DataAnnotations; using System.Runtime.Serialization; using Umbraco.Core; using Umbraco.Core.IO; +using Umbraco.Core.Models.Validation; namespace Umbraco.Web.Models.ContentEditing { @@ -15,10 +17,20 @@ namespace Umbraco.Web.Models.ContentEditing [DataContract(Name = "contentType", Namespace = "")] public class ContentTypeBasic : EntityBasic { + /// + /// Overridden to apply our own validation attributes since this is not always required for other classes + /// + [Required] + [RegularExpression(@"^([a-zA-Z]\w.*)$", ErrorMessage = "Invalid alias")] + [DataMember(Name = "alias")] + public override string Alias { get; set; } + [DataMember(Name = "updateDate")] + [ReadOnly(true)] public DateTime UpdateDate { get; set; } [DataMember(Name = "createDate")] + [ReadOnly(true)] public DateTime CreateDate { get; set; } [DataMember(Name = "description")] @@ -31,6 +43,7 @@ namespace Umbraco.Web.Models.ContentEditing /// Returns true if the icon represents a CSS class instead of a file path /// [DataMember(Name = "iconIsClass")] + [ReadOnly(true)] public bool IconIsClass { get @@ -48,6 +61,7 @@ namespace Umbraco.Web.Models.ContentEditing /// Returns the icon file path if the icon is not a class, otherwise returns an empty string ///
[DataMember(Name = "iconFilePath")] + [ReadOnly(true)] public string IconFilePath { get @@ -62,6 +76,7 @@ namespace Umbraco.Web.Models.ContentEditing /// Returns true if the icon represents a CSS class instead of a file path /// [DataMember(Name = "thumbnailIsClass")] + [ReadOnly(true)] public bool ThumbnailIsClass { get @@ -79,6 +94,7 @@ namespace Umbraco.Web.Models.ContentEditing /// Returns the icon file path if the icon is not a class, otherwise returns an empty string /// [DataMember(Name = "thumbnailFilePath")] + [ReadOnly(true)] public string ThumbnailFilePath { get diff --git a/src/Umbraco.Web/Models/ContentEditing/ContentTypeCompositionDisplay.cs b/src/Umbraco.Web/Models/ContentEditing/ContentTypeCompositionDisplay.cs index 85be0670ca..7a3869d9b1 100644 --- a/src/Umbraco.Web/Models/ContentEditing/ContentTypeCompositionDisplay.cs +++ b/src/Umbraco.Web/Models/ContentEditing/ContentTypeCompositionDisplay.cs @@ -1,14 +1,17 @@ using System; using System.Collections.Generic; +using System.ComponentModel; +using System.ComponentModel.DataAnnotations; using System.Linq; using System.Runtime.Serialization; using System.Text; using System.Threading.Tasks; +using Umbraco.Core.Models.Validation; namespace Umbraco.Web.Models.ContentEditing { [DataContract(Name = "contentType", Namespace = "")] - public class ContentTypeCompositionDisplay : ContentTypeBasic + public class ContentTypeCompositionDisplay : ContentTypeBasic, INotificationModel { public ContentTypeCompositionDisplay() { @@ -17,15 +20,17 @@ namespace Umbraco.Web.Models.ContentEditing AllowedContentTypes = new List(); CompositeContentTypes = new List(); AvailableCompositeContentTypes = new List(); + Notifications = new List(); } - //name, alias, icon, thumb, desc, inherited from basic - + //name, alias, icon, thumb, desc, inherited from basic + //List view [DataMember(Name = "isContainer")] public bool IsContainer { get; set; } [DataMember(Name = "listViewEditorName")] + [ReadOnly(true)] public string ListViewEditorName { get; set; } //Tabs @@ -41,9 +46,31 @@ namespace Umbraco.Web.Models.ContentEditing public IEnumerable CompositeContentTypes { get; set; } [DataMember(Name = "availableCompositeContentTypes")] + [ReadOnly(true)] public IEnumerable AvailableCompositeContentTypes { get; set; } [DataMember(Name = "allowAsRoot")] public bool AllowAsRoot { 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")] + [ReadOnly(true)] + public List Notifications { get; private set; } + + /// + /// This is used for validation of a content item. + /// + /// + /// 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")] + [ReadOnly(true)] + public IDictionary Errors { get; set; } } } diff --git a/src/Umbraco.Web/Models/ContentEditing/ContentTypeDisplay.cs b/src/Umbraco.Web/Models/ContentEditing/ContentTypeDisplay.cs index 7fb77edc46..e0165450ed 100644 --- a/src/Umbraco.Web/Models/ContentEditing/ContentTypeDisplay.cs +++ b/src/Umbraco.Web/Models/ContentEditing/ContentTypeDisplay.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.ComponentModel; using System.Linq; using System.Runtime.Serialization; using System.Text; @@ -7,6 +8,7 @@ using System.Threading.Tasks; namespace Umbraco.Web.Models.ContentEditing { + [DataContract(Name = "contentType", Namespace = "")] public class ContentTypeDisplay : ContentTypeCompositionDisplay { diff --git a/src/Umbraco.Web/Models/ContentEditing/ContentTypeSave.cs b/src/Umbraco.Web/Models/ContentEditing/ContentTypeSave.cs new file mode 100644 index 0000000000..17eb6bb35e --- /dev/null +++ b/src/Umbraco.Web/Models/ContentEditing/ContentTypeSave.cs @@ -0,0 +1,91 @@ +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Runtime.Serialization; +using Umbraco.Core; + +namespace Umbraco.Web.Models.ContentEditing +{ + [DataContract(Name = "contentType", Namespace = "")] + public class ContentTypeSave : ContentTypeBasic, IValidatableObject + { + public ContentTypeSave() + { + //initialize collections so at least their never null + Groups = new List>(); + AllowedContentTypes = new List(); + CompositeContentTypes = new List(); + } + + //Compositions + [DataMember(Name = "compositeContentTypes")] + public IEnumerable CompositeContentTypes { get; set; } + + [DataMember(Name = "isContainer")] + public bool IsContainer { get; set; } + + [DataMember(Name = "allowAsRoot")] + public bool AllowAsRoot { get; set; } + + /// + /// The list of allowed templates to assign (template alias) + /// + [DataMember(Name = "allowedTemplates")] + public IEnumerable AllowedTemplates { get; set; } + + //Allowed child types + [DataMember(Name = "allowedContentTypes")] + public IEnumerable AllowedContentTypes { get; set; } + + /// + /// The default template to assign (template alias) + /// + [DataMember(Name = "defaultTemplate")] + public string DefaultTemplate { get; set; } + + //Tabs + [DataMember(Name = "groups")] + public IEnumerable> Groups { get; set; } + + /// + /// Custom validation + /// + /// + /// + public IEnumerable Validate(ValidationContext validationContext) + { + if (AllowedTemplates.Any(x => x.IsNullOrWhiteSpace())) + yield return new ValidationResult("Template value cannot be null", new[] {"AllowedTemplates"}); + + if (CompositeContentTypes.Any(x => x.IsNullOrWhiteSpace())) + yield return new ValidationResult("Composite Content Type value cannot be null", new[] { "CompositeContentTypes" }); + + var duplicateGroups = Groups.GroupBy(x => x.Name).Where(x => x.Count() > 1).ToArray(); + if (duplicateGroups.Any()) + { + //we need to return the field name with an index so it's wired up correctly + var firstIndex = Groups.IndexOf(duplicateGroups.First().First()); + yield return new ValidationResult("Duplicate group names not allowed", new[] + { + string.Format("Groups[{0}].Name", firstIndex) + }); + } + + var duplicateProperties = Groups.SelectMany(x => x.Properties).Where(x => x.Inherited == false).GroupBy(x => x.Alias).Where(x => x.Count() > 1).ToArray(); + if (duplicateProperties.Any()) + { + //we need to return the field name with an index so it's wired up correctly + var firstProperty = duplicateProperties.First().First(); + var propertyGroup = Groups.Single(x => x.Properties.Contains(firstProperty)); + var groupIndex = Groups.IndexOf(propertyGroup); + var propertyIndex = propertyGroup.Properties.IndexOf(firstProperty); + + yield return new ValidationResult("Duplicate property aliases not allowed", new[] + { + string.Format("Groups[{0}].Properties[{1}].Alias", groupIndex, propertyIndex) + }); + } + + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/Models/ContentEditing/EntityBasic.cs b/src/Umbraco.Web/Models/ContentEditing/EntityBasic.cs index 520ff677b0..40d884d653 100644 --- a/src/Umbraco.Web/Models/ContentEditing/EntityBasic.cs +++ b/src/Umbraco.Web/Models/ContentEditing/EntityBasic.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.ComponentModel; using System.ComponentModel.DataAnnotations; using System.Linq; using System.Runtime.Serialization; @@ -29,6 +30,7 @@ namespace Umbraco.Web.Models.ContentEditing public string Icon { get; set; } [DataMember(Name = "trashed")] + [ReadOnly(true)] public bool Trashed { get; set; } /// @@ -44,8 +46,11 @@ namespace Umbraco.Web.Models.ContentEditing /// /// This will only be populated for some entities like macros /// + /// + /// This is overrideable to specify different validation attributes if required + /// [DataMember(Name = "alias")] - public string Alias { get; set; } + public virtual string Alias { get; set; } /// /// The path of the entity @@ -57,6 +62,7 @@ namespace Umbraco.Web.Models.ContentEditing /// A collection of extra data that is available for this specific entity/entity type /// [DataMember(Name = "metaData")] + [ReadOnly(true)] public IDictionary AdditionalData { get; private set; } } } diff --git a/src/Umbraco.Web/Models/ContentEditing/PropertyGroupBasic.cs b/src/Umbraco.Web/Models/ContentEditing/PropertyGroupBasic.cs new file mode 100644 index 0000000000..d942c433ce --- /dev/null +++ b/src/Umbraco.Web/Models/ContentEditing/PropertyGroupBasic.cs @@ -0,0 +1,34 @@ +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Runtime.Serialization; + +namespace Umbraco.Web.Models.ContentEditing +{ + [DataContract(Name = "propertyGroup", Namespace = "")] + public class PropertyGroupBasic + where TPropertyType: PropertyTypeBasic + { + public PropertyGroupBasic() + { + Properties = new List(); + } + + //Indicate if this tab was inherited + [DataMember(Name = "inherited")] + public bool Inherited { get; set; } + + //TODO: Required ? + [DataMember(Name = "id")] + public int Id { get; set; } + + [DataMember(Name = "properties")] + public IEnumerable Properties { get; set; } + + [DataMember(Name = "sortOrder")] + public int SortOrder { get; set; } + + [Required] + [DataMember(Name = "name")] + public string Name { get; set; } + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/Models/ContentEditing/PropertyGroupDisplay.cs b/src/Umbraco.Web/Models/ContentEditing/PropertyGroupDisplay.cs index 091ebc8484..49a3b84261 100644 --- a/src/Umbraco.Web/Models/ContentEditing/PropertyGroupDisplay.cs +++ b/src/Umbraco.Web/Models/ContentEditing/PropertyGroupDisplay.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.ComponentModel; using System.Linq; using System.Runtime.Serialization; using System.Text; @@ -8,7 +9,7 @@ using System.Threading.Tasks; namespace Umbraco.Web.Models.ContentEditing { [DataContract(Name = "propertyGroup", Namespace = "")] - public class PropertyGroupDisplay + public class PropertyGroupDisplay : PropertyGroupBasic { public PropertyGroupDisplay() { @@ -16,33 +17,22 @@ namespace Umbraco.Web.Models.ContentEditing ParentTabContentTypeNames = new List(); ParentTabContentTypes = new List(); } - - [DataMember(Name = "id")] - public int Id { get; set; } - + [DataMember(Name = "parentGroupId")] + [ReadOnly(true)] public int ParentGroupId { get; set; } - - [DataMember(Name = "sortOrder")] - public int SortOrder { get; set; } - - [DataMember(Name = "name")] - public string Name { get; set; } - - [DataMember(Name = "properties")] - public IEnumerable Properties { get; set; } - - //Indicate if this tab was inherited - [DataMember(Name = "inherited")] - public bool Inherited { get; set; } - + + //SD: Seems strange that this is required [DataMember(Name = "contentTypeId")] + [ReadOnly(true)] public int ContentTypeId { get; set; } [DataMember(Name = "parentTabContentTypes")] + [ReadOnly(true)] public IEnumerable ParentTabContentTypes { get; set; } [DataMember(Name = "parentTabContentTypeNames")] + [ReadOnly(true)] public IEnumerable ParentTabContentTypeNames { get; set; } } } diff --git a/src/Umbraco.Web/Models/ContentEditing/PropertyTypeBasic.cs b/src/Umbraco.Web/Models/ContentEditing/PropertyTypeBasic.cs new file mode 100644 index 0000000000..ce7be6dd84 --- /dev/null +++ b/src/Umbraco.Web/Models/ContentEditing/PropertyTypeBasic.cs @@ -0,0 +1,43 @@ +using System.ComponentModel.DataAnnotations; +using System.Runtime.Serialization; + +namespace Umbraco.Web.Models.ContentEditing +{ + [DataContract(Name = "propertyType")] + public class PropertyTypeBasic + { + //indicates if this property was inherited + [DataMember(Name = "inherited")] + public bool Inherited { get; set; } + + //TODO: Required ? + [DataMember(Name = "id")] + public int Id { get; set; } + + [Required] + [RegularExpression(@"^([a-zA-Z]\w.*)$", ErrorMessage = "Invalid alias")] + [DataMember(Name = "alias")] + public string Alias { get; set; } + + [DataMember(Name = "description")] + public string Description { get; set; } + + [DataMember(Name = "validation")] + public PropertyTypeValidation Validation { get; set; } + + [DataMember(Name = "label")] + [Required] + public string Label { get; set; } + + [DataMember(Name = "sortOrder")] + public int SortOrder { get; set; } + + [DataMember(Name = "dataTypeId")] + [Required] + public int DataTypeId { get; set; } + + //SD: Is this really needed ? + [DataMember(Name = "groupId")] + public int GroupId { get; set; } + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/Models/ContentEditing/PropertyTypeDisplay.cs b/src/Umbraco.Web/Models/ContentEditing/PropertyTypeDisplay.cs index bcf939c76a..a05924ad5c 100644 --- a/src/Umbraco.Web/Models/ContentEditing/PropertyTypeDisplay.cs +++ b/src/Umbraco.Web/Models/ContentEditing/PropertyTypeDisplay.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.ComponentModel; using System.Linq; using System.Runtime.Serialization; using System.Text; @@ -8,52 +9,28 @@ using System.Threading.Tasks; namespace Umbraco.Web.Models.ContentEditing { [DataContract(Name = "propertyType")] - public class PropertyTypeDisplay - { - [DataMember(Name = "id")] - public int Id { get; set; } - - [DataMember(Name = "alias")] - public string Alias { get; set; } - - [DataMember(Name = "description")] - public string Description { get; set; } - + public class PropertyTypeDisplay : PropertyTypeBasic + { [DataMember(Name = "editor")] + [ReadOnly(true)] public string Editor { get; set; } - - [DataMember(Name = "validation")] - public PropertyTypeValidation Validation { get; set; } - - [DataMember(Name = "label")] - public string Label { get; set; } - + [DataMember(Name = "view")] + [ReadOnly(true)] public string View { get; set; } [DataMember(Name = "config")] + [ReadOnly(true)] public IDictionary Config { get; set; } - - [DataMember(Name = "value")] - public string Value { get; set; } - - [DataMember(Name = "sortOrder")] - public int SortOrder { get; set; } - - //indicates if this property was inherited - [DataMember(Name = "inherited")] - public bool Inherited { get; set; } - - [DataMember(Name = "dataTypeId")] - public int DataTypeId { get; set; } - - [DataMember(Name = "groupId")] - public int GroupId { get; set; } - + + //SD: Seems strange that this is needed [DataMember(Name = "contentTypeId")] + [ReadOnly(true)] public int ContentTypeId { get; set; } + //SD: Seems strange that this is needed [DataMember(Name = "contentTypeName")] + [ReadOnly(true)] public string ContentTypeName { get; set; } } } diff --git a/src/Umbraco.Web/Models/Mapping/AvailableCompositeContentTypesResolver.cs b/src/Umbraco.Web/Models/Mapping/AvailableCompositeContentTypesResolver.cs index dacd2ab0a7..9cc415d27a 100644 --- a/src/Umbraco.Web/Models/Mapping/AvailableCompositeContentTypesResolver.cs +++ b/src/Umbraco.Web/Models/Mapping/AvailableCompositeContentTypesResolver.cs @@ -12,12 +12,11 @@ namespace Umbraco.Web.Models.Mapping { internal class AvailableCompositeContentTypesResolver : ValueResolver> { - private ApplicationContext _context; - private bool _mediaType; - internal AvailableCompositeContentTypesResolver(ApplicationContext context, bool mediaType = false) + private readonly ApplicationContext _context; + + internal AvailableCompositeContentTypesResolver(ApplicationContext context) { _context = context; - _mediaType = mediaType; } protected override IEnumerable ResolveCore(IContentTypeComposition source) @@ -27,19 +26,19 @@ namespace Umbraco.Web.Models.Mapping var s = source; var type = _context.Services.EntityService.GetObjectType(source.Id); - IContentTypeComposition[] allContentTypes = new IContentTypeComposition[0]; + var allContentTypes = new IContentTypeComposition[0]; switch (type) { - case UmbracoObjectTypes.DocumentType: + case UmbracoObjectTypes.DocumentType: allContentTypes = _context.Services.ContentTypeService.GetAllContentTypes().Cast().ToArray(); break; - case UmbracoObjectTypes.MediaType: + case UmbracoObjectTypes.MediaType: allContentTypes = _context.Services.ContentTypeService.GetAllMediaTypes().Cast().ToArray(); break; - case UmbracoObjectTypes.MemberType: + case UmbracoObjectTypes.MemberType: allContentTypes = _context.Services.MemberTypeService.GetAll().Cast().ToArray(); break; } @@ -56,42 +55,40 @@ namespace Umbraco.Web.Models.Mapping //if already in use a composition, do not allow any composited types return new List(); } - else - { - // if it is not used then composition is possible - // hashset guarantees unicity on Id - var list = new HashSet(new DelegateEqualityComparer( - (x, y) => x.Id == y.Id, - x => x.Id)); - // usable types are those that are top-level - var usableContentTypes = allContentTypes - .Where(x => x.ContentTypeComposition.Any() == false).ToArray(); - foreach (var x in usableContentTypes) - list.Add(x); + // if it is not used then composition is possible + // hashset guarantees unicity on Id + var list = new HashSet(new DelegateEqualityComparer( + (x, y) => x.Id == y.Id, + x => x.Id)); - // indirect types are those that we use, directly or indirectly - var indirectContentTypes = GetIndirect(source).ToArray(); - foreach (var x in indirectContentTypes) - list.Add(x); + // usable types are those that are top-level + var usableContentTypes = allContentTypes + .Where(x => x.ContentTypeComposition.Any() == false).ToArray(); + foreach (var x in usableContentTypes) + list.Add(x); - // directContentTypes are those we use directly - // they are already in indirectContentTypes, no need to add to the list - var directContentTypes = source.ContentTypeComposition.ToArray(); + // indirect types are those that we use, directly or indirectly + var indirectContentTypes = GetIndirect(source).ToArray(); + foreach (var x in indirectContentTypes) + list.Add(x); - var enabled = usableContentTypes.Select(x => x.Id) // those we can use - .Except(indirectContentTypes.Select(x => x.Id)) // except those that are indirectly used - .Union(directContentTypes.Select(x => x.Id)) // but those that are directly used - .Where(x => x != source.ParentId) // but not the parent - .Distinct() - .ToArray(); + //// directContentTypes are those we use directly + //// they are already in indirectContentTypes, no need to add to the list + //var directContentTypes = source.ContentTypeComposition.ToArray(); - var wtf = new List(); - foreach (var contentType in list.OrderBy(x => x.Name).Where(x => x.Id != source.Id)) - wtf.Add(Mapper.Map(contentType)); + //var enabled = usableContentTypes.Select(x => x.Id) // those we can use + // .Except(indirectContentTypes.Select(x => x.Id)) // except those that are indirectly used + // .Union(directContentTypes.Select(x => x.Id)) // but those that are directly used + // .Where(x => x != source.ParentId) // but not the parent + // .Distinct() + // .ToArray(); - return wtf; - } + return list + .Where(x => x.Id != source.Id) + .OrderBy(x => x.Name) + .Select(Mapper.Map) + .ToList(); } diff --git a/src/Umbraco.Web/Models/Mapping/ContentTypeModelMapper.cs b/src/Umbraco.Web/Models/Mapping/ContentTypeModelMapper.cs index 1d7fbd4cb3..626da45caa 100644 --- a/src/Umbraco.Web/Models/Mapping/ContentTypeModelMapper.cs +++ b/src/Umbraco.Web/Models/Mapping/ContentTypeModelMapper.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using System.Linq; using System.Threading; using AutoMapper; @@ -20,7 +19,7 @@ namespace Umbraco.Web.Models.Mapping internal class ContentTypeModelMapper : MapperConfiguration { private readonly Lazy _propertyEditorResolver; - + //default ctor public ContentTypeModelMapper() { @@ -35,207 +34,69 @@ namespace Umbraco.Web.Models.Mapping public override void ConfigureMappings(IConfiguration config, ApplicationContext applicationContext) { - config.CreateMap() - .Include() - .Include() - .Include() + + config.CreateMap() + .ConstructUsing(basic => new PropertyType(applicationContext.Services.DataTypeService.GetDataTypeDefinitionById(basic.DataTypeId))) + .ForMember(type => type.ValidationRegExp, expression => expression.ResolveUsing(basic => basic.Validation.Pattern)) + .ForMember(type => type.Mandatory, expression => expression.ResolveUsing(basic => basic.Validation.Mandatory)) + .ForMember(type => type.Name, expression => expression.ResolveUsing(basic => basic.Label)) + .ForMember(type => type.DataTypeDefinitionId, expression => expression.ResolveUsing(basic => basic.DataTypeId)) + .ForMember(type => type.DataTypeId, expression => expression.Ignore()) + .ForMember(type => type.PropertyEditorAlias, expression => expression.Ignore()) + .ForMember(type => type.HelpText, expression => expression.Ignore()) + .ForMember(type => type.Key, expression => expression.Ignore()) + .ForMember(type => type.CreateDate, expression => expression.Ignore()) + .ForMember(type => type.UpdateDate, expression => expression.Ignore()) + .ForMember(type => type.HasIdentity, expression => expression.Ignore()); - //only map id if set to something higher then zero - .ForMember(dto => dto.Id, expression => expression.Condition(display => (Convert.ToInt32(display.Id) > 0))) - .ForMember(dto => dto.Id, expression => expression.MapFrom(display => Convert.ToInt32(display.Id))) - - .ForMember(dto => dto.AllowedAsRoot, expression => expression.MapFrom(display => display.AllowAsRoot)) - .ForMember(dto => dto.CreatorId, expression => expression.Ignore()) - .ForMember(dto => dto.Level, expression => expression.Ignore()) - .ForMember(dto => dto.SortOrder, expression => expression.Ignore()) - - .ForMember( - dto => dto.AllowedContentTypes, - expression => expression.MapFrom(dto => dto.AllowedContentTypes.Select( (t, i) => new ContentTypeSort(t, i) ))) - - //ignore, we'll do this in after map - .ForMember(dto => dto.PropertyGroups, expression => expression.Ignore()) - - .AfterMap((source, dest) => - { - - var addedProperties = new List(); - - //get all properties from groups that are not generic properties or inhertied (-666 id) - var selfNonGenericGroups = source.Groups.Where(x => x.Inherited == false && x.Id != -666).ToArray(); - - foreach (var groupDisplay in selfNonGenericGroups) - { - //use underlying logic to add the property group which should wire most things up for us - dest.AddPropertyGroup(groupDisplay.Name); - - //now update that group with the values from the display object - Mapper.Map(groupDisplay, dest.PropertyGroups[groupDisplay.Name]); - - foreach (var propertyTypeDisplay in groupDisplay.Properties.Where(x => x.Inherited == false)) - { - //update existing - if(propertyTypeDisplay.Id > 0) - { - var currentPropertyType = dest.PropertyTypes.FirstOrDefault(x => x.Id == propertyTypeDisplay.Id); - Mapper.Map(propertyTypeDisplay, currentPropertyType); - }else - {//add new - var mapped = Mapper.Map(propertyTypeDisplay); - dest.AddPropertyType(mapped, groupDisplay.Name); - } - - addedProperties.Add(propertyTypeDisplay.Alias); - } - } - - //Groups to remove - var groupsToRemove = dest.PropertyGroups.Select(x => x.Name).Except(selfNonGenericGroups.Select(x => x.Name)).ToArray(); - foreach (var toRemove in groupsToRemove) - { - dest.RemovePropertyGroup(toRemove); - } - - //add generic properties - var genericProperties = source.Groups.FirstOrDefault(x => x.Id == -666); - if(genericProperties != null) - { - foreach (var propertyTypeDisplay in genericProperties.Properties.Where(x => x.Inherited == false)) - { - dest.AddPropertyType(Mapper.Map(propertyTypeDisplay)); - addedProperties.Add(propertyTypeDisplay.Alias); - } - } - - //remove deleted types - foreach(var removedType in dest.PropertyTypes - .Where(x => addedProperties.Contains(x.Alias) == false).ToList()) - { - dest.RemovePropertyType(removedType.Alias); - } - - - }); - - config.CreateMap() + config.CreateMap() + //do the base mapping + .MapBaseContentTypeSaveToEntity(applicationContext) .ConstructUsing((source) => new ContentType(source.ParentId)) - .ForMember(dto => dto.Id, expression => expression.Ignore()) - .ForMember(dto => dto.AllowedTemplates, expression => expression.Ignore()) + .ForMember(source => source.AllowedTemplates, expression => expression.Ignore()) .ForMember(dto => dto.DefaultTemplate, expression => expression.Ignore()) - .AfterMap((source, dest) => { - //sync templates - dest.AllowedTemplates = source.AllowedTemplates.Where(x => x != null).Select(x => Mapper.Map(x)); + dest.AllowedTemplates = source.AllowedTemplates + .Where(x => x != null) + .Select(s => applicationContext.Services.FileService.GetTemplate(s)) + .ToArray(); if (source.DefaultTemplate != null) - dest.SetDefaultTemplate(Mapper.Map(source.DefaultTemplate)); + dest.SetDefaultTemplate(applicationContext.Services.FileService.GetTemplate(source.DefaultTemplate)); - - //sync compositions - var current = dest.CompositionAliases().ToArray(); - var proposed = source.CompositeContentTypes; - - var remove = current.Where(x => proposed.Contains(x) == false); - var add = proposed.Where(x => current.Contains(x) == false); - - foreach (var rem in remove) - { - dest.RemoveContentType(rem); - } - - foreach (var a in add) - { - - //TODO: Remove N+1 lookup - var addCt = applicationContext.Services.ContentTypeService.GetContentType(a); - if (addCt != null) - dest.AddContentType(addCt); - } + ContentTypeModelMapperExtensions.AfterMapContentTypeSaveToEntity(source, dest, applicationContext); }); - config.CreateMap() + config.CreateMap() + //do the base mapping + .MapBaseContentTypeSaveToEntity(applicationContext) + .ConstructUsing((source) => new MediaType(source.ParentId)) .AfterMap((source, dest) => - { - - //sync compositions - var current = dest.CompositionAliases().ToArray(); - var proposed = source.CompositeContentTypes; + { + ContentTypeModelMapperExtensions.AfterMapContentTypeSaveToEntity(source, dest, applicationContext); + }); - var remove = current.Where(x => proposed.Contains(x) == false); - var add = proposed.Where(x => current.Contains(x) == false); - - foreach (var rem in remove) - dest.RemoveContentType(rem); - - foreach (var a in add) - { - //TODO: Remove N+1 lookup - var addCt = applicationContext.Services.MemberTypeService.Get(a); - if (addCt != null) - dest.AddContentType(addCt); - } - }); - - - config.CreateMap() + config.CreateMap() + //do the base mapping + .MapBaseContentTypeSaveToEntity(applicationContext) + .ConstructUsing((source) => new MemberType(source.ParentId)) .AfterMap((source, dest) => - { - //sync compositions - var current = dest.CompositionAliases().ToArray(); - var proposed = source.CompositeContentTypes; + { + ContentTypeModelMapperExtensions.AfterMapContentTypeSaveToEntity(source, dest, applicationContext); + }); - var remove = current.Where(x => proposed.Contains(x) == false); - var add = proposed.Where(x => current.Contains(x) == false); - - foreach (var rem in remove) - dest.RemoveContentType(rem); - - foreach (var a in add) - { - //TODO: Remove N+1 lookup - var addCt = applicationContext.Services.ContentTypeService.GetMediaType(a); - if (addCt != null) - dest.AddContentType(addCt); - } - }); - - config.CreateMap().ConvertUsing(x => x.Alias); + config.CreateMap() + //map base logic + .MapBaseContentTypeEntityToDisplay(applicationContext, _propertyEditorResolver); - config.CreateMap() - .Include() - .Include() - .Include() - - .ForMember(display => display.AllowAsRoot, expression => expression.MapFrom(type => type.AllowedAsRoot)) - .ForMember(display => display.ListViewEditorName, expression => expression.Ignore()) - //Ignore because this is not actually used for content types - .ForMember(display => display.Trashed, expression => expression.Ignore()) - - .ForMember( - dto => dto.AllowedContentTypes, - expression => expression.MapFrom(dto => dto.AllowedContentTypes.Select(x => x.Id.Value))) - - .ForMember( - dto => dto.AvailableCompositeContentTypes, - expression => expression.ResolveUsing(new AvailableCompositeContentTypesResolver(applicationContext))) - - .ForMember( - dto => dto.CompositeContentTypes, - expression => expression.MapFrom(dto => dto.ContentTypeComposition)) - - .ForMember( - dto => dto.Groups, - expression => expression.ResolveUsing(new PropertyTypeGroupResolver(applicationContext, _propertyEditorResolver))); - - - config.CreateMap(); config.CreateMap() + //map base logic + .MapBaseContentTypeEntityToDisplay(applicationContext, _propertyEditorResolver) .AfterMap((source, dest) => { - //default listview dest.ListViewEditorName = Constants.Conventions.DataTypes.ListViewPrefix + "Media"; @@ -248,20 +109,22 @@ namespace Umbraco.Web.Models.Mapping }); config.CreateMap() + //map base logic + .MapBaseContentTypeEntityToDisplay(applicationContext, _propertyEditorResolver) .ForMember(dto => dto.AllowedTemplates, expression => expression.Ignore()) .ForMember(dto => dto.DefaultTemplate, expression => expression.Ignore()) - + .ForMember(display => display.Notifications, expression => expression.Ignore()) .AfterMap((source, dest) => { //sync templates - dest.AllowedTemplates = source.AllowedTemplates.Select(Mapper.Map); + dest.AllowedTemplates = source.AllowedTemplates.Select(Mapper.Map).ToArray(); if (source.DefaultTemplate != null) dest.DefaultTemplate = Mapper.Map(source.DefaultTemplate); //default listview dest.ListViewEditorName = Constants.Conventions.DataTypes.ListViewPrefix + "Content"; - + if (string.IsNullOrEmpty(source.Name) == false) { var name = Constants.Conventions.DataTypes.ListViewPrefix + source.Name; @@ -275,26 +138,13 @@ namespace Umbraco.Web.Models.Mapping config.CreateMap(); config.CreateMap(); + config.CreateMap() - config.CreateMap() - .ForMember(dest => dest.Id, expression => expression.Condition(source => source.Id > 0)) - .ForMember(g => g.Key, expression => expression.Ignore()) - .ForMember(g => g.HasIdentity, expression => expression.Ignore()) - .ForMember(dto => dto.CreateDate, expression => expression.Ignore()) - .ForMember(dto => dto.UpdateDate, expression => expression.Ignore()) - //only map if a parent is actually set - .ForMember(g => g.ParentId, expression => expression.Condition(display => display.ParentGroupId > 0)) - .ForMember(g => g.ParentId, expression => expression.MapFrom(display => display.ParentGroupId)) - //ignore these, this is handled with IContentType.AddPropertyType - .ForMember(g => g.PropertyTypes, expression => expression.Ignore()); - - config.CreateMap() - - .ConstructUsing((PropertyTypeDisplay propertyTypeDisplay) => + .ConstructUsing((PropertyTypeBasic propertyTypeBasic) => { - var dataType = applicationContext.Services.DataTypeService.GetDataTypeDefinitionById(propertyTypeDisplay.DataTypeId); - if (dataType == null) throw new NullReferenceException("No data type found with id " + propertyTypeDisplay.DataTypeId); - return new PropertyType(dataType, propertyTypeDisplay.Alias); + var dataType = applicationContext.Services.DataTypeService.GetDataTypeDefinitionById(propertyTypeBasic.DataTypeId); + if (dataType == null) throw new NullReferenceException("No data type found with id " + propertyTypeBasic.DataTypeId); + return new PropertyType(dataType, propertyTypeBasic.Alias); }) //only map if it is actually set @@ -314,11 +164,75 @@ namespace Umbraco.Web.Models.Mapping .ForMember(type => type.DataTypeId, expression => expression.Ignore()) .ForMember(type => type.Mandatory, expression => expression.MapFrom(display => display.Validation.Mandatory)) .ForMember(type => type.ValidationRegExp, expression => expression.MapFrom(display => display.Validation.Pattern)) - .ForMember(type => type.PropertyEditorAlias, expression => expression.MapFrom(display => display.Editor)) .ForMember(type => type.DataTypeDefinitionId, expression => expression.MapFrom(display => display.DataTypeId)) .ForMember(type => type.Name, expression => expression.MapFrom(display => display.Label)); + + #region *** Used for mapping on top of an existing display object from a save object *** + + config.CreateMap() + .MapBaseContentTypeSaveToDisplay(); + + config.CreateMap() + .MapBaseContentTypeSaveToDisplay() + .ForMember(dto => dto.AllowedTemplates, expression => expression.Ignore()) + .ForMember(dto => dto.DefaultTemplate, expression => expression.Ignore()) + .AfterMap((source, dest) => + { + //sync templates + var destAllowedTemplateAliases = dest.AllowedTemplates.Select(x => x.Alias); + //if the dest is set and it's the same as the source, then don't change + if (destAllowedTemplateAliases.SequenceEqual(source.AllowedTemplates) == false) + { + var templates = applicationContext.Services.FileService.GetTemplates(source.AllowedTemplates.ToArray()); + dest.AllowedTemplates = source.AllowedTemplates.Select(x => Mapper.Map(templates.Single(t => t.Alias == x))).ToArray(); + } + + if (source.DefaultTemplate.IsNullOrWhiteSpace() == false) + { + //if the dest is set and it's the same as the source, then don't change + if (dest.DefaultTemplate == null || source.DefaultTemplate != dest.DefaultTemplate.Alias) + { + var template = applicationContext.Services.FileService.GetTemplate(source.DefaultTemplate); + dest.DefaultTemplate = template == null ? null : Mapper.Map(template); + } + } + else + { + dest.DefaultTemplate = null; + } + }); + + config.CreateMap, PropertyGroup>() + .ForMember(dest => dest.Id, expression => expression.Condition(source => source.Id > 0)) + .ForMember(g => g.Key, expression => expression.Ignore()) + .ForMember(g => g.HasIdentity, expression => expression.Ignore()) + .ForMember(dto => dto.CreateDate, expression => expression.Ignore()) + .ForMember(dto => dto.UpdateDate, expression => expression.Ignore()) + .ForMember(g => g.ParentId, expression => expression.Ignore()) + .ForMember(g => g.PropertyTypes, expression => expression.MapFrom(basic => basic.Properties.Select(Mapper.Map))); + + config.CreateMap, PropertyGroupDisplay>() + .ForMember(dest => dest.Id, expression => expression.Condition(source => source.Id > 0)) + .ForMember(g => g.ParentGroupId, expression => expression.Ignore()) + .ForMember(g => g.ContentTypeId, expression => expression.Ignore()) + .ForMember(g => g.ParentTabContentTypes, expression => expression.Ignore()) + .ForMember(g => g.ParentTabContentTypeNames, expression => expression.Ignore()) + .ForMember(g => g.Properties, expression => expression.MapFrom(display => display.Properties.Select(Mapper.Map))); + + config.CreateMap() + .ForMember(g => g.Editor, expression => expression.Ignore()) + .ForMember(g => g.View, expression => expression.Ignore()) + .ForMember(g => g.Config, expression => expression.Ignore()) + .ForMember(g => g.ContentTypeId, expression => expression.Ignore()) + .ForMember(g => g.ContentTypeName, expression => expression.Ignore()); + + #endregion + + + + } - + } } \ No newline at end of file diff --git a/src/Umbraco.Web/Models/Mapping/ContentTypeModelMapperExtensions.cs b/src/Umbraco.Web/Models/Mapping/ContentTypeModelMapperExtensions.cs new file mode 100644 index 0000000000..cfa12e214f --- /dev/null +++ b/src/Umbraco.Web/Models/Mapping/ContentTypeModelMapperExtensions.cs @@ -0,0 +1,192 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using AutoMapper; +using Umbraco.Core; +using Umbraco.Core.Models; +using Umbraco.Core.PropertyEditors; +using Umbraco.Web.Models.ContentEditing; + +namespace Umbraco.Web.Models.Mapping +{ + /// + /// Used as a shared way to do the underlying mapping for content types base classes + /// + /// + /// We used to use 'Include' Automapper inheritance functionality and although this works, the unit test + /// to assert mappings fails which is an Automapper bug. So instead we will use an extension method for the mappings + /// to re-use mappings. + /// + internal static class ContentTypeModelMapperExtensions + { + + public static void AfterMapContentTypeSaveToEntity( + TSource source, TDestination dest, + ApplicationContext applicationContext) + where TSource : ContentTypeSave + where TDestination : IContentTypeComposition + { + //sync compositions + var current = dest.CompositionAliases().ToArray(); + var proposed = source.CompositeContentTypes; + + var remove = current.Where(x => proposed.Contains(x) == false); + var add = proposed.Where(x => current.Contains(x) == false); + + foreach (var rem in remove) + { + dest.RemoveContentType(rem); + } + + foreach (var a in add) + { + //TODO: Remove N+1 lookup + var addCt = applicationContext.Services.ContentTypeService.GetContentType(a); + if (addCt != null) + dest.AddContentType(addCt); + } + } + + public static IMappingExpression MapBaseContentTypeSaveToDisplay( + this IMappingExpression mapping) + where TSource : ContentTypeSave + where TDestination : ContentTypeCompositionDisplay + { + return mapping + .ForMember(dto => dto.CreateDate, expression => expression.Ignore()) + .ForMember(dto => dto.UpdateDate, expression => expression.Ignore()) + .ForMember(dto => dto.ListViewEditorName, expression => expression.Ignore()) + .ForMember(dto => dto.AvailableCompositeContentTypes, expression => expression.Ignore()) + .ForMember(dto => dto.Notifications, expression => expression.Ignore()) + .ForMember(dto => dto.Errors, expression => expression.Ignore()); + } + + public static IMappingExpression MapBaseContentTypeEntityToDisplay( + this IMappingExpression mapping, ApplicationContext applicationContext, Lazy propertyEditorResolver) + where TSource : IContentTypeComposition + where TDestination : ContentTypeCompositionDisplay + { + return mapping + .ForMember(display => display.Notifications, expression => expression.Ignore()) + .ForMember(display => display.Errors, expression => expression.Ignore()) + .ForMember(display => display.AllowAsRoot, expression => expression.MapFrom(type => type.AllowedAsRoot)) + .ForMember(display => display.ListViewEditorName, expression => expression.Ignore()) + //Ignore because this is not actually used for content types + .ForMember(display => display.Trashed, expression => expression.Ignore()) + + .ForMember( + dto => dto.AllowedContentTypes, + expression => expression.MapFrom(dto => dto.AllowedContentTypes.Select(x => x.Id.Value))) + + .ForMember( + dto => dto.AvailableCompositeContentTypes, + expression => expression.ResolveUsing(new AvailableCompositeContentTypesResolver(applicationContext))) + + .ForMember( + dto => dto.CompositeContentTypes, + expression => expression.MapFrom(dto => dto.ContentTypeComposition)) + + .ForMember( + dto => dto.Groups, + expression => expression.ResolveUsing(new PropertyTypeGroupResolver(applicationContext, propertyEditorResolver))); + } + + /// + /// Display -> Entity class base mapping logic + /// + /// + /// + /// + /// + /// + public static IMappingExpression MapBaseContentTypeSaveToEntity( + this IMappingExpression mapping, ApplicationContext applicationContext) + //where TSource : ContentTypeCompositionDisplay + where TSource : ContentTypeSave + where TDestination : IContentTypeComposition + { + return mapping + //only map id if set to something higher then zero + .ForMember(dto => dto.Id, expression => expression.Condition(display => (Convert.ToInt32(display.Id) > 0))) + .ForMember(dto => dto.Id, expression => expression.MapFrom(display => Convert.ToInt32(display.Id))) + + //These get persisted as part of the saving procedure, nothing to do with the display model + .ForMember(dto => dto.CreateDate, expression => expression.Ignore()) + .ForMember(dto => dto.UpdateDate, expression => expression.Ignore()) + + .ForMember(dto => dto.AllowedAsRoot, expression => expression.MapFrom(display => display.AllowAsRoot)) + .ForMember(dto => dto.CreatorId, expression => expression.Ignore()) + .ForMember(dto => dto.Level, expression => expression.Ignore()) + .ForMember(dto => dto.SortOrder, expression => expression.Ignore()) + //ignore, we'll do this in after map + .ForMember(dto => dto.PropertyGroups, expression => expression.Ignore()) + + .ForMember( + dto => dto.AllowedContentTypes, + expression => expression.MapFrom(dto => dto.AllowedContentTypes.Select((t, i) => new ContentTypeSort(t, i)))) + + .AfterMap((source, dest) => + { + + var addedProperties = new List(); + + //get all properties from groups that are not generic properties or inhertied (-666 id) + var selfNonGenericGroups = source.Groups.Where(x => x.Inherited == false && x.Id != -666).ToArray(); + + foreach (var group in selfNonGenericGroups) + { + //use underlying logic to add the property group which should wire most things up for us + dest.AddPropertyGroup(group.Name); + + //now update that group with the values from the display object + Mapper.Map(group, dest.PropertyGroups[group.Name]); + + foreach (var propType in group.Properties.Where(x => x.Inherited == false)) + { + //update existing + if (propType.Id > 0) + { + var currentPropertyType = dest.PropertyTypes.FirstOrDefault(x => x.Id == propType.Id); + Mapper.Map(propType, currentPropertyType); + } + else + { + //add new + var mapped = Mapper.Map(propType); + dest.AddPropertyType(mapped, group.Name); + } + + addedProperties.Add(propType.Alias); + } + } + + //Groups to remove + var groupsToRemove = dest.PropertyGroups.Select(x => x.Name).Except(selfNonGenericGroups.Select(x => x.Name)).ToArray(); + foreach (var toRemove in groupsToRemove) + { + dest.RemovePropertyGroup(toRemove); + } + + //add generic properties + var genericProperties = source.Groups.FirstOrDefault(x => x.Id == -666); + if (genericProperties != null) + { + foreach (var propertyTypeBasic in genericProperties.Properties.Where(x => x.Inherited == false)) + { + dest.AddPropertyType(Mapper.Map(propertyTypeBasic)); + addedProperties.Add(propertyTypeBasic.Alias); + } + } + + //remove deleted types + foreach (var removedType in dest.PropertyTypes + .Where(x => addedProperties.Contains(x.Alias) == false).ToList()) + { + dest.RemovePropertyType(removedType.Alias); + } + + + }); + } + } +} \ 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 0b3327a553..c451e1a9bd 100644 --- a/src/Umbraco.Web/Models/Mapping/DataTypeModelMapper.cs +++ b/src/Umbraco.Web/Models/Mapping/DataTypeModelMapper.cs @@ -35,6 +35,7 @@ namespace Umbraco.Web.Models.Mapping }; config.CreateMap() + .ForMember(x => x.HasPrevalues, expression => expression.Ignore()) .ForMember(x => x.IsSystemDataType, expression => expression.Ignore()) .ForMember(x => x.Id, expression => expression.Ignore()) .ForMember(x => x.Trashed, expression => expression.Ignore()) @@ -44,6 +45,7 @@ namespace Umbraco.Web.Models.Mapping .ForMember(x => x.AdditionalData, expression => expression.Ignore()); config.CreateMap() + .ForMember(x => x.HasPrevalues, expression => expression.Ignore()) .ForMember(x => x.Icon, expression => expression.Ignore()) .ForMember(x => x.Alias, expression => expression.Ignore()) .ForMember(x => x.Group, expression => expression.Ignore()) @@ -65,6 +67,7 @@ namespace Umbraco.Web.Models.Mapping new PreValueDisplayResolver(lazyDataTypeService))) .ForMember(display => display.SelectedEditor, expression => expression.MapFrom( definition => definition.PropertyEditorAlias.IsNullOrWhiteSpace() ? null : definition.PropertyEditorAlias)) + .ForMember(x => x.HasPrevalues, expression => expression.Ignore()) .ForMember(x => x.Notifications, expression => expression.Ignore()) .ForMember(x => x.Icon, expression => expression.Ignore()) .ForMember(x => x.Alias, expression => expression.Ignore()) diff --git a/src/Umbraco.Web/Models/Mapping/EntityModelMapper.cs b/src/Umbraco.Web/Models/Mapping/EntityModelMapper.cs index 44925b5a7f..5610a70008 100644 --- a/src/Umbraco.Web/Models/Mapping/EntityModelMapper.cs +++ b/src/Umbraco.Web/Models/Mapping/EntityModelMapper.cs @@ -52,18 +52,18 @@ namespace Umbraco.Web.Models.Mapping .ForMember(dto => dto.Trashed, expression => expression.Ignore()) .ForMember(x => x.AdditionalData, expression => expression.Ignore()); - config.CreateMap() - .ConstructUsing(basic => new Template(basic.Name, basic.Alias) - { - Id = Convert.ToInt32(basic.Id), - Key = basic.Key - }) - .ForMember(t => t.Path, expression => expression.Ignore()) - .ForMember(t => t.Id, expression => expression.MapFrom(template => Convert.ToInt32(template.Id))) - .ForMember(x => x.VirtualPath, expression => expression.Ignore()) - .ForMember(x => x.CreateDate, expression => expression.Ignore()) - .ForMember(x => x.UpdateDate, expression => expression.Ignore()) - .ForMember(x => x.Content, expression => expression.Ignore()); + //config.CreateMap() + // .ConstructUsing(basic => new Template(basic.Name, basic.Alias) + // { + // Id = Convert.ToInt32(basic.Id), + // Key = basic.Key + // }) + // .ForMember(t => t.Path, expression => expression.Ignore()) + // .ForMember(t => t.Id, expression => expression.MapFrom(template => Convert.ToInt32(template.Id))) + // .ForMember(x => x.VirtualPath, expression => expression.Ignore()) + // .ForMember(x => x.CreateDate, expression => expression.Ignore()) + // .ForMember(x => x.UpdateDate, expression => expression.Ignore()) + // .ForMember(x => x.Content, expression => expression.Ignore()); config.CreateMap() .ForMember(x => x.Id, expression => expression.MapFrom(entity => new Lazy(() => Convert.ToInt32(entity.Id)))) diff --git a/src/Umbraco.Web/Models/Mapping/PropertyTypeGroupResolver.cs b/src/Umbraco.Web/Models/Mapping/PropertyTypeGroupResolver.cs index ae300a824e..2802fe2965 100644 --- a/src/Umbraco.Web/Models/Mapping/PropertyTypeGroupResolver.cs +++ b/src/Umbraco.Web/Models/Mapping/PropertyTypeGroupResolver.cs @@ -176,7 +176,7 @@ namespace Umbraco.Web.Models.Mapping Label = p.Name, View = editor.ValueEditor.View, Config = editor.PreValueEditor.ConvertDbToEditor(editor.DefaultPreValues, preVals) , - Value = "", + //Value = "", ContentTypeId = contentType.Id, ContentTypeName = contentType.Name, GroupId = groupId, diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index 07c378714b..1110ef956b 100644 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -304,7 +304,11 @@ + + + + @@ -609,7 +613,9 @@ ASPXCodeBehind - + + ASPXCodeBehind + ASPXCodeBehind @@ -623,8 +629,12 @@ ASPXCodeBehind - - + + ASPXCodeBehind + + + ASPXCodeBehind + @@ -830,14 +840,30 @@ ASPXCodeBehind - - - - - - - - + + ASPXCodeBehind + + + ASPXCodeBehind + + + ASPXCodeBehind + + + ASPXCodeBehind + + + ASPXCodeBehind + + + ASPXCodeBehind + + + ASPXCodeBehind + + + ASPXCodeBehind + AssignDomain2.aspx ASPXCodeBehind @@ -845,10 +871,16 @@ AssignDomain2.aspx - + + ASPXCodeBehind + - - + + ASPXCodeBehind + + + ASPXCodeBehind + @@ -873,6 +905,8 @@ + + @@ -984,37 +1018,57 @@ ASPXCodeBehind - + + ASPXCodeBehind + ASPXCodeBehind - + + ASPXCodeBehind + ASPXCodeBehind - + + ASPXCodeBehind + ASPXCodeBehind - + + ASPXCodeBehind + ASPXCodeBehind - + + ASPXCodeBehind + ASPXCodeBehind - - - + + ASPXCodeBehind + + + ASPXCodeBehind + + + ASPXCodeBehind + ASPXCodeBehind - - + + ASPXCodeBehind + + + ASPXCodeBehind + ASPXCodeBehind @@ -1025,9 +1079,15 @@ ASPXCodeBehind - - - + + ASPXCodeBehind + + + ASPXCodeBehind + + + ASPXCodeBehind + @@ -1056,7 +1116,9 @@ - + + ASPXCodeBehind + @@ -1145,24 +1207,28 @@ delete.aspx + ASPXCodeBehind delete.aspx editContent.aspx + ASPXCodeBehind editContent.aspx preview.aspx + ASPXCodeBehind preview.aspx publish.aspx + ASPXCodeBehind publish.aspx @@ -1205,7 +1271,9 @@ ProgressBar.ascx - + + ASPXCodeBehind + Component @@ -1236,12 +1304,14 @@ FeedProxy.aspx + ASPXCodeBehind FeedProxy.aspx EditRelationType.aspx + ASPXCodeBehind EditRelationType.aspx @@ -1262,6 +1332,7 @@ Preview.aspx + ASPXCodeBehind Preview.aspx @@ -1278,42 +1349,49 @@ xsltVisualize.aspx + ASPXCodeBehind xsltVisualize.aspx insertMasterpageContent.aspx + ASPXCodeBehind insertMasterpageContent.aspx insertMasterpagePlaceholder.aspx + ASPXCodeBehind insertMasterpagePlaceholder.aspx mediaPicker.aspx + ASPXCodeBehind mediaPicker.aspx republish.aspx + ASPXCodeBehind republish.aspx search.aspx + ASPXCodeBehind search.aspx SendPublish.aspx + ASPXCodeBehind SendPublish.aspx @@ -1330,6 +1408,7 @@ media.ascx + ASPXCodeBehind media.ascx @@ -1360,30 +1439,35 @@ assemblyBrowser.aspx + ASPXCodeBehind assemblyBrowser.aspx autoDoc.aspx + ASPXCodeBehind autoDoc.aspx BrowseRepository.aspx + ASPXCodeBehind BrowseRepository.aspx editPackage.aspx + ASPXCodeBehind editPackage.aspx installedPackage.aspx + ASPXCodeBehind installedPackage.aspx @@ -1415,90 +1499,106 @@ xsltInsertValueOf.aspx + ASPXCodeBehind xsltInsertValueOf.aspx about.aspx + ASPXCodeBehind about.aspx exportDocumenttype.aspx + ASPXCodeBehind imageViewer.aspx + ASPXCodeBehind imageViewer.aspx importDocumenttype.aspx + ASPXCodeBehind insertMacro.aspx + ASPXCodeBehind insertMacro.aspx insertTable.aspx + ASPXCodeBehind insertTable.aspx notifications.aspx + ASPXCodeBehind notifications.aspx RegexWs.aspx + ASPXCodeBehind RegexWs.aspx rollBack.aspx + ASPXCodeBehind rollBack.aspx sendToTranslation.aspx + ASPXCodeBehind sendToTranslation.aspx uploadImage.aspx + ASPXCodeBehind uploadImage.aspx viewAuditTrail.aspx + ASPXCodeBehind viewAuditTrail.aspx language.aspx + ASPXCodeBehind language.aspx EditMemberGroup.aspx + ASPXCodeBehind EditMemberGroup.aspx EditMemberType.aspx + ASPXCodeBehind EditMemberType.aspx @@ -1512,6 +1612,7 @@ ViewMembers.aspx + ASPXCodeBehind ViewMembers.aspx @@ -1525,30 +1626,35 @@ InsertAnchor.aspx + ASPXCodeBehind InsertAnchor.aspx insertChar.aspx + ASPXCodeBehind insertChar.aspx insertImage.aspx + ASPXCodeBehind insertImage.aspx insertLink.aspx + ASPXCodeBehind insertLink.aspx insertMacro.aspx + ASPXCodeBehind insertMacro.aspx @@ -1568,6 +1674,7 @@ DictionaryItemList.aspx + ASPXCodeBehind DictionaryItemList.aspx @@ -1581,18 +1688,21 @@ editLanguage.aspx + ASPXCodeBehind editLanguage.aspx EditMediaType.aspx + ASPXCodeBehind EditMediaType.aspx editScript.aspx + ASPXCodeBehind @@ -1647,23 +1757,28 @@ details.aspx + ASPXCodeBehind details.aspx preview.aspx + ASPXCodeBehind preview.aspx xml.aspx + ASPXCodeBehind xml.aspx - + + ASPXCodeBehind + @@ -1706,6 +1821,7 @@ EditUserType.aspx + ASPXCodeBehind EditUserType.aspx @@ -1719,6 +1835,7 @@ PermissionEditor.aspx + ASPXCodeBehind PermissionEditor.aspx diff --git a/src/Umbraco.Web/WebApi/PrefixlessBodyModelValidator.cs b/src/Umbraco.Web/WebApi/PrefixlessBodyModelValidator.cs new file mode 100644 index 0000000000..c5563e6509 --- /dev/null +++ b/src/Umbraco.Web/WebApi/PrefixlessBodyModelValidator.cs @@ -0,0 +1,37 @@ +using System; +using System.Web.Http.Controllers; +using System.Web.Http.Metadata; +using System.Web.Http.Validation; + +namespace Umbraco.Web.WebApi +{ + /// + /// By default WebApi always appends a prefix to any ModelState error but we don't want this, + /// so this is a custom validator that ensures there is no prefix set. + /// + /// + /// We were already doing this with the content/media/members validation since we had to manually validate because we + /// were posting multi-part values. We were always passing in an empty prefix so it worked. However for other editors we + /// are validating with normal data annotations (for the most part) and we don't want the prefix there either. + /// + internal class PrefixlessBodyModelValidator : IBodyModelValidator + { + private readonly IBodyModelValidator _innerValidator; + + public PrefixlessBodyModelValidator(IBodyModelValidator innerValidator) + { + if (innerValidator == null) + { + throw new ArgumentNullException("innerValidator"); + } + + _innerValidator = innerValidator; + } + + public bool Validate(object model, Type type, ModelMetadataProvider metadataProvider, HttpActionContext actionContext, string keyPrefix) + { + // Remove the keyPrefix but otherwise let innerValidator do what it normally does. + return _innerValidator.Validate(model, type, metadataProvider, actionContext, string.Empty); + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/WebApi/PrefixlessBodyModelValidatorAttribute.cs b/src/Umbraco.Web/WebApi/PrefixlessBodyModelValidatorAttribute.cs new file mode 100644 index 0000000000..e018881f8a --- /dev/null +++ b/src/Umbraco.Web/WebApi/PrefixlessBodyModelValidatorAttribute.cs @@ -0,0 +1,20 @@ +using System; +using System.Web.Http; +using System.Web.Http.Controllers; +using System.Web.Http.Validation; + +namespace Umbraco.Web.WebApi +{ + /// + /// Applying this attribute to any webapi controller will ensure that it only contains one json formatter compatible with the angular json vulnerability prevention. + /// + internal class PrefixlessBodyModelValidatorAttribute : Attribute, IControllerConfiguration + { + public virtual void Initialize(HttpControllerSettings controllerSettings, HttpControllerDescriptor controllerDescriptor) + { + //replace the normal validator with our custom one for this controller + controllerSettings.Services.Replace(typeof(IBodyModelValidator), + new PrefixlessBodyModelValidator(controllerSettings.Services.GetBodyModelValidator())); + } + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/WebBootManager.cs b/src/Umbraco.Web/WebBootManager.cs index baac1109e4..48486fd676 100644 --- a/src/Umbraco.Web/WebBootManager.cs +++ b/src/Umbraco.Web/WebBootManager.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Collections.Specialized; using System.Configuration; using System.Linq; +using System.Reflection; using System.Web; using System.Web.Configuration; using System.Web.Http; @@ -103,10 +104,10 @@ namespace Umbraco.Web public override IBootManager Initialize() { //This is basically a hack for this item: http://issues.umbraco.org/issue/U4-5976 - // when Examine initializes it will try to rebuild if the indexes are empty, however in many cases not all of Examine's - // event handlers will be assigned during bootup when the rebuilding starts which is a problem. So with the examine 0.1.58.2941 build - // it has an event we can subscribe to in order to cancel this rebuilding process, but what we'll do is cancel it and postpone the rebuilding until the - // boot process has completed. It's a hack but it works. + // when Examine initializes it will try to rebuild if the indexes are empty, however in many cases not all of Examine's + // event handlers will be assigned during bootup when the rebuilding starts which is a problem. So with the examine 0.1.58.2941 build + // it has an event we can subscribe to in order to cancel this rebuilding process, but what we'll do is cancel it and postpone the rebuilding until the + // boot process has completed. It's a hack but it works. ExamineManager.Instance.BuildingEmptyIndexOnStartup += OnInstanceOnBuildingEmptyIndexOnStartup; base.Initialize();