From 6180784646a9e2fe3645af60c78d97b8f5033b94 Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 23 Sep 2015 15:44:38 +0200 Subject: [PATCH 01/22] Fixes client side validation on editorsettings --- .../contenttypeeditor/editorsettings/editorsettings.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 @@
- +
From a7a262de8b9cb601537d8f5f2aaf34b72ad17b5f Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 23 Sep 2015 16:04:23 +0200 Subject: [PATCH 02/22] Ensures validation messages are shown for the alias and that the aliases is validated with regex too --- .../src/views/components/umb-locked-field.html | 5 +++++ 1 file changed, 5 insertions(+) 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..8a70b25c80 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 @@ -17,6 +17,11 @@ ng-class="{'-unlocked': !locked}" placeholder="{{placeholderText}}" umb-auto-resize + required + val-regex="^[a-zA-Z]\w.*$" /> + + + Invalid alias From 94bc4404376b7fb9ded0c6014c09799aab4865d2 Mon Sep 17 00:00:00 2001 From: Shannon Date: Thu, 24 Sep 2015 12:14:08 +0200 Subject: [PATCH 03/22] adds note --- src/Umbraco.Web/Editors/ContentTypeController.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Umbraco.Web/Editors/ContentTypeController.cs b/src/Umbraco.Web/Editors/ContentTypeController.cs index 1a0f7be2fa..4418de580f 100644 --- a/src/Umbraco.Web/Editors/ContentTypeController.cs +++ b/src/Umbraco.Web/Editors/ContentTypeController.cs @@ -130,6 +130,8 @@ namespace Umbraco.Web.Editors //set id to null to ensure its handled as a new type contentType.Id = null; + //TODO: This all needs to be done in a transaction!! + // Which means that all of this logic needs to take place inside the service //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) From 5fff1e4ac51080b47bcb5dda6979e949e2c09756 Mon Sep 17 00:00:00 2001 From: Shannon Date: Thu, 24 Sep 2015 15:53:24 +0200 Subject: [PATCH 04/22] Fixes umb-editor-header to toggle visibility of certain fields based on boolean toggles since we cannot rely on model values being undefined when there are validators present because validators change the model value to undefined. --- .../editor/umbeditorheader.directive.js | 3 +++ .../src/views/common/dashboard.html | 5 ++++- .../components/editor/umb-editor-header.html | 16 +++++++++++----- .../src/views/content/edit.html | 5 ++++- .../src/views/content/recyclebin.html | 5 ++++- .../src/views/datatype/edit.html | 5 ++++- .../src/views/media/edit.html | 5 ++++- .../src/views/media/recyclebin.html | 5 ++++- .../src/views/member/edit.html | 5 ++++- .../src/views/member/list.html | 5 ++++- 10 files changed, 46 insertions(+), 13 deletions(-) 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/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/components/editor/umb-editor-header.html b/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editor-header.html index cd8fb83ac5..f99eab3b1a 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,25 @@
-
+
Add icon
- -
{{ name }}
+ +
{{ name }}
- + - +
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/datatype/edit.html b/src/Umbraco.Web.UI.Client/src/views/datatype/edit.html index 8e32589c35..1c80fa44c1 100644 --- a/src/Umbraco.Web.UI.Client/src/views/datatype/edit.html +++ b/src/Umbraco.Web.UI.Client/src/views/datatype/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/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/member/edit.html b/src/Umbraco.Web.UI.Client/src/views/member/edit.html index 08abe59050..3492275c08 100644 --- a/src/Umbraco.Web.UI.Client/src/views/member/edit.html +++ b/src/Umbraco.Web.UI.Client/src/views/member/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/member/list.html b/src/Umbraco.Web.UI.Client/src/views/member/list.html index 99fc997f16..7976677432 100644 --- a/src/Umbraco.Web.UI.Client/src/views/member/list.html +++ b/src/Umbraco.Web.UI.Client/src/views/member/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"> From f41f565eb8d0a49c8ab349adb048719a0462d52c Mon Sep 17 00:00:00 2001 From: Shannon Date: Thu, 24 Sep 2015 16:17:14 +0200 Subject: [PATCH 05/22] Fixes: U4-7136 Client side validation no longer works for header text --- src/Umbraco.Web.UI.Client/src/less/forms.less | 3 ++- .../components/editor/umb-editor-header.html | 18 +++++++++++------- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/less/forms.less b/src/Umbraco.Web.UI.Client/src/less/forms.less index 1c529f6e01..03b8bb4bb0 100644 --- a/src/Umbraco.Web.UI.Client/src/less/forms.less +++ b/src/Umbraco.Web.UI.Client/src/less/forms.less @@ -441,7 +441,8 @@ input[type="checkbox"][readonly] { .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 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 f99eab3b1a..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 @@ -13,13 +13,17 @@
- + + + +
{{ name }}
From b27200ec36f288fdf0c1802cbd3a1b9d4a39f340 Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 25 Sep 2015 14:06:07 +0200 Subject: [PATCH 06/22] streamlines some validation styling: no more focuses border highlighting since that doesn't work consistently with all fields, ensures that the new fields have a border but it's set to transparent since we want to show a border for validation, removes the zero padding from new fields since when a validation border is added it doesn't look good and now padding is just using the base consistent padding for fields. Moves the valrequirecomponent.directive to the validation folder. --- .../valrequirecomponent.directive.js} | 0 .../less/components/umb-group-builder.less | 5 ++- .../src/less/components/umb-locked-field.less | 5 ++- src/Umbraco.Web.UI.Client/src/less/forms.less | 27 ---------------- .../src/less/mixins.less | 6 ---- src/Umbraco.Web.UI.Client/src/less/panel.less | 12 ++----- .../propertysettings/propertysettings.html | 31 +++++++++---------- 7 files changed, 20 insertions(+), 66 deletions(-) rename src/Umbraco.Web.UI.Client/src/common/directives/{components/umbvalidatecomponent.directive.js => validation/valrequirecomponent.directive.js} (100%) 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/less/components/umb-group-builder.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-group-builder.less index 0f2ae52962..5986f6c91d 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 @@ -410,11 +410,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 03b8bb4bb0..3b2e949b2d 100644 --- a/src/Umbraco.Web.UI.Client/src/less/forms.less +++ b/src/Umbraco.Web.UI.Client/src/less/forms.less @@ -436,38 +436,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 .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 { diff --git a/src/Umbraco.Web.UI.Client/src/less/mixins.less b/src/Umbraco.Web.UI.Client/src/less/mixins.less index e1f9170424..bf2d75e888 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, diff --git a/src/Umbraco.Web.UI.Client/src/less/panel.less b/src/Umbraco.Web.UI.Client/src/less/panel.less index 062954f9c2..f35cd2c207 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/overlays/contenttypeeditor/propertysettings/propertysettings.html b/src/Umbraco.Web.UI.Client/src/views/common/overlays/contenttypeeditor/propertysettings/propertysettings.html index 99d4b30ea3..7e861b9024 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,9 +1,7 @@
-
- -
- - -
- - - +
-
+
+ +
+ +
-
+
Add Editor From 97c01924755db6e2d283103bf96af777bd807aaf Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 25 Sep 2015 14:26:10 +0200 Subject: [PATCH 07/22] adds no-margin class modifier for control-group, adjust minor styles to the previous fields i updated and add required to the group name with control-group so it shows validation errors --- src/Umbraco.Web.UI.Client/src/less/forms.less | 5 ++++ .../propertysettings/propertysettings.html | 30 +++++++++---------- .../views/components/umb-groups-builder.html | 18 ++++++++--- 3 files changed, 34 insertions(+), 19 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/less/forms.less b/src/Umbraco.Web.UI.Client/src/less/forms.less index 3b2e949b2d..1138ab109a 100644 --- a/src/Umbraco.Web.UI.Client/src/less/forms.less +++ b/src/Umbraco.Web.UI.Client/src/less/forms.less @@ -715,6 +715,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; 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 7e861b9024..29b2acbd19 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,22 +1,22 @@
-
- +
+
+ +
+
+ +
-
- -
-
+
-
- +
+ +
- -
- -
- -
+
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 3de552eccc..050476090a 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 @@ -180,11 +180,8 @@ saveMethod: contentTypeResource.save, scope: $scope, content: vm.contentType, - rebindCallback: function (origContent, savedContent) { - //This is called when the data returns and we need to merge the property values from the saved content to the original content. - //re-binds all changed property values to the origContent object from the savedContent object and returns an array of changed properties. - - } + //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); 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..73772ae2a9 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 @@ -108,7 +108,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 +124,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 +144,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/Editors/ContentTypeController.cs b/src/Umbraco.Web/Editors/ContentTypeController.cs index 9bd0ba50fd..dc47d15234 100644 --- a/src/Umbraco.Web/Editors/ContentTypeController.cs +++ b/src/Umbraco.Web/Editors/ContentTypeController.cs @@ -16,6 +16,7 @@ using Newtonsoft.Json; using Umbraco.Core.PropertyEditors; using System; using System.Net.Http; +using Umbraco.Core.Services; namespace Umbraco.Web.Editors { @@ -118,6 +119,8 @@ namespace Umbraco.Web.Editors //TODO: This all needs to be done in a transaction!! // Which means that all of this logic needs to take place inside the service + ContentTypeDisplay display; + if (ctId > 0) { //its an update to an existing @@ -127,8 +130,8 @@ namespace Umbraco.Web.Editors Mapper.Map(contentTypeSave, found); ctService.Save(found); - - return Mapper.Map(found); + + display = Mapper.Map(found); } else { @@ -171,9 +174,15 @@ namespace Umbraco.Web.Editors newCt.AddContentType(newCt); ctService.Save(newCt); } - - return Mapper.Map(newCt); + + display = Mapper.Map(newCt); } + + display.AddSuccessNotification( + Services.TextService.Localize("speechBubbles/contentTypeSavedHeader"), + string.Empty); + + return display; } /// diff --git a/src/Umbraco.Web/Editors/ContentTypeControllerBase.cs b/src/Umbraco.Web/Editors/ContentTypeControllerBase.cs index 2ece1f21d8..ea652dbe50 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; 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/ContentTypeBasic.cs b/src/Umbraco.Web/Models/ContentEditing/ContentTypeBasic.cs index 13f8235f69..f138083143 100644 --- a/src/Umbraco.Web/Models/ContentEditing/ContentTypeBasic.cs +++ b/src/Umbraco.Web/Models/ContentEditing/ContentTypeBasic.cs @@ -26,9 +26,11 @@ namespace Umbraco.Web.Models.ContentEditing 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")] diff --git a/src/Umbraco.Web/Models/ContentEditing/ContentTypeCompositionDisplay.cs b/src/Umbraco.Web/Models/ContentEditing/ContentTypeCompositionDisplay.cs index 16ff968cee..7a3869d9b1 100644 --- a/src/Umbraco.Web/Models/ContentEditing/ContentTypeCompositionDisplay.cs +++ b/src/Umbraco.Web/Models/ContentEditing/ContentTypeCompositionDisplay.cs @@ -11,7 +11,7 @@ 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() { diff --git a/src/Umbraco.Web/Models/ContentEditing/PropertyTypeBasic.cs b/src/Umbraco.Web/Models/ContentEditing/PropertyTypeBasic.cs index 4332a93436..ce7be6dd84 100644 --- a/src/Umbraco.Web/Models/ContentEditing/PropertyTypeBasic.cs +++ b/src/Umbraco.Web/Models/ContentEditing/PropertyTypeBasic.cs @@ -26,15 +26,17 @@ namespace Umbraco.Web.Models.ContentEditing 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 required ? + //SD: Is this really needed ? [DataMember(Name = "groupId")] public int GroupId { get; set; } } diff --git a/src/Umbraco.Web/Models/Mapping/ContentTypeModelMapper.cs b/src/Umbraco.Web/Models/Mapping/ContentTypeModelMapper.cs index 3c7f0d7022..75bd6bbaae 100644 --- a/src/Umbraco.Web/Models/Mapping/ContentTypeModelMapper.cs +++ b/src/Umbraco.Web/Models/Mapping/ContentTypeModelMapper.cs @@ -225,6 +225,8 @@ namespace Umbraco.Web.Models.Mapping #region *** Used for mapping on top of an existing display object from a save object *** config.CreateMap() + .ForMember(dto => dto.CreateDate, expression => expression.Ignore()) + .ForMember(dto => dto.UpdateDate, expression => expression.Ignore()) .ForMember(dto => dto.AllowedTemplates, expression => expression.Ignore()) .ForMember(dto => dto.DefaultTemplate, expression => expression.Ignore()) .ForMember(dto => dto.ListViewEditorName, expression => expression.Ignore()) diff --git a/src/Umbraco.Web/Models/Mapping/ContentTypeModelMapperExtensions.cs b/src/Umbraco.Web/Models/Mapping/ContentTypeModelMapperExtensions.cs index 96b286b8fe..9d9457eb50 100644 --- a/src/Umbraco.Web/Models/Mapping/ContentTypeModelMapperExtensions.cs +++ b/src/Umbraco.Web/Models/Mapping/ContentTypeModelMapperExtensions.cs @@ -68,6 +68,10 @@ namespace Umbraco.Web.Models.Mapping .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()) diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index 1f1f5585c7..c7864b2204 100644 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -904,6 +904,8 @@ + + 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 From 6017e9ddf9ed44e9bcceeaa69ef24e347982ac38 Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 9 Oct 2015 17:28:22 +0200 Subject: [PATCH 11/22] Gets group and property level validation wired up to the fields, ensures things like duplicate field warnings are created with the correct indexed field names. Fixes a few old directives to correctly watch for attribute interpolation changes which is need for some of this validation to work since it's dynamically populating the field names. --- .../components/umblockedfield.directive.js | 16 +++- .../validation/valCustom.directive.js | 2 +- .../validation/valHighlight.directive.js | 6 +- .../validation/valemail.directive.js | 1 - .../valpropertyvalidator.directive.js | 3 - .../validation/valregex.directive.js | 75 ++++++++++--------- .../validation/valserverfield.directive.js | 73 +++++++++--------- .../views/components/umb-groups-builder.html | 12 ++- .../views/components/umb-locked-field.html | 29 +++---- .../Models/ContentEditing/ContentTypeSave.cs | 29 ++++++- 10 files changed, 142 insertions(+), 104 deletions(-) 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 bd4279a161..63a4dece6e 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..."; @@ -70,7 +80,9 @@ scope: { model: '=ngModel', locked: "=?", - placeholderText: "=?" + placeholderText: "=?", + regexValidation: "=?", + serverValidationField: "@" }, link: link }; diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/validation/valCustom.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/validation/valCustom.directive.js index a402065708..dac010a97f 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/validation/valCustom.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/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/validation/valHighlight.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/validation/valHighlight.directive.js index fdcf768947..599cda766c 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/validation/valHighlight.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/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/validation/valemail.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/validation/valemail.directive.js index 88ffd6f0fa..1e81d8edec 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/validation/valemail.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/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/validation/valpropertyvalidator.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/validation/valpropertyvalidator.directive.js index 77652d7f69..53a1ea67b2 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/validation/valpropertyvalidator.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/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/validation/valregex.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/validation/valregex.directive.js index 651c0a54c7..45a35c5257 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/validation/valregex.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/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 (!viewValue || regex.test(viewValue)) { - // 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; + if (regex) { + //NOTE: we don't validate on empty values, use required validator for that + if (!viewValue || regex.test(viewValue)) { + // 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/validation/valserverfield.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/validation/valserverfield.directive.js index c07ee26dec..6fe2dfdf08 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/validation/valserverfield.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/validation/valserverfield.directive.js @@ -11,47 +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; - var fieldName = attr.valServerField; - var evalfieldName = scope.$eval(attr.valServerField); - if (evalfieldName) { - fieldName = evalfieldName; - } + 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); + }); - //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/views/components/umb-groups-builder.html b/src/Umbraco.Web.UI.Client/src/views/components/umb-groups-builder.html index 3d02980e97..c7fbcb9308 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 @@ -42,6 +42,8 @@ umb-auto-resize required val-server-field="{{'Groups[' + $index + '].Name'}}" /> + +
@@ -93,13 +95,21 @@
-
{{ 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 8a70b25c80..a57f7cc1ef 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 @@ -8,20 +8,21 @@ - - + + - Invalid alias + Invalid alias +
diff --git a/src/Umbraco.Web/Models/ContentEditing/ContentTypeSave.cs b/src/Umbraco.Web/Models/ContentEditing/ContentTypeSave.cs index 15cc1d6785..17eb6bb35e 100644 --- a/src/Umbraco.Web/Models/ContentEditing/ContentTypeSave.cs +++ b/src/Umbraco.Web/Models/ContentEditing/ContentTypeSave.cs @@ -60,11 +60,32 @@ namespace Umbraco.Web.Models.ContentEditing if (CompositeContentTypes.Any(x => x.IsNullOrWhiteSpace())) yield return new ValidationResult("Composite Content Type value cannot be null", new[] { "CompositeContentTypes" }); - if (Groups.GroupBy(x => x.Name).Any(x => x.Count() > 1)) - yield return new ValidationResult("Duplicate group names not allowed", new[] { "Groups" }); + 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); - if (Groups.SelectMany(x => x.Properties).Where(x => x.Inherited == false).GroupBy(x => x.Alias).Any(x => x.Count() > 1)) - yield return new ValidationResult("Duplicate property aliases not allowed", new[] { "Groups" }); + 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 From 2922e962e048bdb68895e93361aedec34ffc2c3d Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 9 Oct 2015 19:03:11 +0200 Subject: [PATCH 12/22] Gets validation working with composition alias conflicts --- .../Exceptions/InvalidCompositionException.cs | 35 ++++++++++--- .../Models/ContentTypeCompositionBase.cs | 8 +-- .../Services/ContentTypeService.cs | 25 +++++++--- .../Services/IContentTypeService.cs | 7 +++ .../src/common/services/util.service.js | 12 ++++- .../views/components/umb-groups-builder.html | 8 +-- .../Editors/ContentTypeController.cs | 49 +++++++++++++++++-- 7 files changed, 114 insertions(+), 30 deletions(-) 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/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.Web.UI.Client/src/common/services/util.service.js b/src/Umbraco.Web.UI.Client/src/common/services/util.service.js index 805ffd33ea..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 @@ -508,7 +508,7 @@ function umbDataFormatter() { var realProperties = _.reject(g.properties, function (p) { //do not include these properties - return p.propertyState === "init"; + return p.propertyState === "init" || p.inherited === true; }); var saveProperties = _.map(realProperties, function (p) { @@ -518,9 +518,19 @@ function umbDataFormatter() { 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; }, 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 c7fbcb9308..f628367cb3 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 @@ -96,10 +96,12 @@
- {{ property.alias }}
+ + server-validation-field="{{'Groups[' + $parent.$parent.$parent.$parent.$index + '].Properties[' + $index + '].Alias'}}">
@@ -107,7 +109,7 @@ name="groupName" umb-auto-resize required - val-server-field="{{'Groups[' + $parent.$index + '].Properties[' + $index + '].Label'}}"> + val-server-field="{{'Groups[' + $parent.$parent.$parent.$parent.$index + '].Properties[' + $index + '].Label'}}">
diff --git a/src/Umbraco.Web/Editors/ContentTypeController.cs b/src/Umbraco.Web/Editors/ContentTypeController.cs index dc47d15234..561f929706 100644 --- a/src/Umbraco.Web/Editors/ContentTypeController.cs +++ b/src/Umbraco.Web/Editors/ContentTypeController.cs @@ -89,7 +89,42 @@ namespace Umbraco.Web.Editors ? Request.CreateResponse(HttpStatusCode.OK, result.Result) //return the id : Request.CreateValidationErrorResponse(result.Exception.Message); } - + + /// + /// Validates the composition and adds errors to the model state if any are found then throws an error response if there are errors + /// + /// + /// + /// + private void ValidateComposition(ContentTypeSave contentTypeSave, IContentTypeComposition composition) + { + var validateAttempt = Services.ContentTypeService.ValidateComposition(composition); + if (validateAttempt == false) + { + //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)); + } + + } + public ContentTypeDisplay PostSave(ContentTypeSave contentTypeSave) { var ctId = Convert.ToInt32(contentTypeSave.Id); @@ -106,8 +141,6 @@ namespace Umbraco.Web.Editors forDisplay.Errors = ModelState.ToErrorDictionary(); throw new HttpResponseException(Request.CreateValidationErrorResponse(forDisplay)); } - - //TODO: Deal with validation for composition with property and group names/aliases //filter out empty properties contentTypeSave.Groups = contentTypeSave.Groups.Where(x => x.Name.IsNullOrWhiteSpace() == false).ToList(); @@ -129,8 +162,11 @@ namespace Umbraco.Web.Editors throw new HttpResponseException(HttpStatusCode.NotFound); Mapper.Map(contentTypeSave, found); - ctService.Save(found); + //NOTE: this throws an error response if it is not valid + ValidateComposition(contentTypeSave, found); + + ctService.Save(found); display = Mapper.Map(found); } else @@ -166,11 +202,16 @@ namespace Umbraco.Web.Editors //save as new var newCt = Mapper.Map(contentTypeSave); + + //NOTE: this throws an error response if it is not valid + ValidateComposition(contentTypeSave, newCt); + ctService.Save(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); ctService.Save(newCt); } From 222d126508033022f2254338f061ac7fcb7bc811 Mon Sep 17 00:00:00 2001 From: Shannon Date: Tue, 13 Oct 2015 17:43:40 +0200 Subject: [PATCH 13/22] adds a ctor to the BackOfficeUserManager to allow better inheritance so that all settings are initialized by default. --- .../Security/BackOfficeUserManager.cs | 23 +++++++++++++------ 1 file changed, 16 insertions(+), 7 deletions(-) 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 From 85e46cbbe5701ffaed1272f743a538fddaaa4c8e Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 14 Oct 2015 16:54:58 +0200 Subject: [PATCH 14/22] Fixes: U4-7251 When changing a data type name to be the same as another we suffix the duplication with a number - this needs to be reflected in the UI --- .../DataTypeDefinitionRepository.cs | 12 +++++----- .../common/services/datatypehelper.service.js | 22 +++++++++++++++++++ .../datatypes/datatype.edit.controller.js | 6 ++++- 3 files changed, 33 insertions(+), 7 deletions(-) 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.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/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); }); } From 815d34f8a69b03238068db9ade259f310b4848f5 Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 14 Oct 2015 17:51:00 +0200 Subject: [PATCH 15/22] exposes api on valFormManager which can be used instead of subscribing to events, changes valTab to use this api instead of events, ensures locked field has a ng-form wrapper so the validation messages are displayed. --- .../validation/valformmanager.directive.js | 21 +++++++++ .../directives/validation/valtab.directive.js | 12 ++--- .../views/components/umb-locked-field.html | 45 +++++++++---------- 3 files changed, 49 insertions(+), 29 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/validation/valformmanager.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/validation/valformmanager.directive.js index 634f6eb4ec..37c0313c45 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/validation/valformmanager.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/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/validation/valtab.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/validation/valtab.directive.js index cd6dc51eca..fbca0cd233 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/validation/valtab.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/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/views/components/umb-locked-field.html b/src/Umbraco.Web.UI.Client/src/views/components/umb-locked-field.html index a57f7cc1ef..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,28 +1,27 @@
+ + + + - - - + + + - - - - - - - - Invalid alias - + + + Invalid alias + +
From 33c206d820eed3e9a5707b7bfef8edc2e638685e Mon Sep 17 00:00:00 2001 From: Shannon Date: Thu, 15 Oct 2015 14:48:50 +0200 Subject: [PATCH 16/22] Wires up the correct saving logic for media types + validation, refactors the logic so it can be reused everywhere. --- .../common/resources/mediatype.resource.js | 10 +- .../views/documenttypes/edit.controller.js | 23 +-- .../src/views/mediatypes/edit.controller.js | 74 +++++---- .../Editors/ContentTypeController.cs | 141 +++--------------- .../Editors/ContentTypeControllerBase.cs | 126 +++++++++++++++- .../Editors/MediaTypeController.cs | 60 ++------ .../Editors/MemberTypeController.cs | 59 ++------ .../Models/Mapping/ContentTypeModelMapper.cs | 101 +++---------- .../ContentTypeModelMapperExtensions.cs | 42 ++++++ 9 files changed, 268 insertions(+), 368 deletions(-) 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/views/documenttypes/edit.controller.js b/src/Umbraco.Web.UI.Client/src/views/documenttypes/edit.controller.js index 75b5c41527..21d41cffb3 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 @@ -154,33 +154,12 @@ vm.contentType = contentTypeHelper.updateTemplatePlaceholder(vm.contentType); } - //contentTypeResource.save(vm.contentType).then(function(dt){ - - // formHelper.resetForm({ scope: $scope, notifications: dt.notifications }); - // contentEditingHelper.handleSuccessfulSave({ - // scope: $scope, - // savedContent: dt, - // rebindCallback: function() { - - // } - // }); - - // notificationsService.success("Document type save"); - // //post save logic here -the saved doctype returns as a new object - // init(dt); - - // syncTreeNode(vm.contentType, dt.path); - - // vm.page.saveButtonState = "success"; - - //}); - 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 + //no-op for rebind callback... we don't really need to rebind for content types rebindCallback: angular.noop }).then(function (data) { //success 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..d7f3f4ee83 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 @@ -9,7 +9,7 @@ (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) { var vm = this; @@ -125,49 +125,47 @@ 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: 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"; + + deferred.resolve(data); + }, function (err) { + //error + if (err) { + editorState.set($scope.content); + } + + vm.page.saveButtonState = "error"; + + deferred.reject(err); + }); + + return deferred.promise; } - function performSave() { - - vm.page.saveButtonState = "busy"; - - // reformat allowed content types to array if id's - vm.contentType.allowedContentTypes = contentTypeHelper.createIdArray(vm.contentType.allowedContentTypes); - - mediaTypeResource.save(vm.contentType).then(function(dt){ - - formHelper.resetForm({ scope: $scope, notifications: dt.notifications }); - contentEditingHelper.handleSuccessfulSave({ - scope: $scope, - savedContent: dt, - rebindCallback: function() { - - } - }); - - notificationsService.success("Media type saved"); - //post save logic here -the saved doctype returns as a new object - init(dt); - - syncTreeNode(vm.contentType, dt.path); - - vm.page.saveButtonState = "success"; - - }); - - } - - function init(contentType){ // set all tab to inactive diff --git a/src/Umbraco.Web/Editors/ContentTypeController.cs b/src/Umbraco.Web/Editors/ContentTypeController.cs index 561f929706..038b2b19ba 100644 --- a/src/Umbraco.Web/Editors/ContentTypeController.cs +++ b/src/Umbraco.Web/Editors/ContentTypeController.cs @@ -90,134 +90,33 @@ namespace Umbraco.Web.Editors : Request.CreateValidationErrorResponse(result.Exception.Message); } - /// - /// Validates the composition and adds errors to the model state if any are found then throws an error response if there are errors - /// - /// - /// - /// - private void ValidateComposition(ContentTypeSave contentTypeSave, IContentTypeComposition composition) - { - var validateAttempt = Services.ContentTypeService.ValidateComposition(composition); - if (validateAttempt == false) - { - //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)); - } - - } + public ContentTypeDisplay PostSave(ContentTypeSave contentTypeSave) { - var ctId = Convert.ToInt32(contentTypeSave.Id); - - var ctService = Services.ContentTypeService; - - if (ModelState.IsValid == false) - { - var ct = ctService.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(); - } - - //TODO: This all needs to be done in a transaction!! - // Which means that all of this logic needs to take place inside the service - - ContentTypeDisplay display; - - if (ctId > 0) - { - //its an update to an existing - var found = ctService.GetContentType(ctId); - if (found == null) - throw new HttpResponseException(HttpStatusCode.NotFound); - - Mapper.Map(contentTypeSave, found); - - //NOTE: this throws an error response if it is not valid - ValidateComposition(contentTypeSave, found); - - ctService.Save(found); - display = Mapper.Map(found); - } - else - { - //set id to null to ensure its handled as a new type - contentTypeSave.Id = null; - contentTypeSave.CreateDate = DateTime.Now; - contentTypeSave.UpdateDate = DateTime.Now; - - //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 (contentTypeSave.DefaultTemplate.IsNullOrWhiteSpace() == false && contentTypeSave.DefaultTemplate == contentTypeSave.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(contentTypeSave.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(contentTypeSave.Name, contentTypeSave.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 alias is set on the default and allowed template so we can map it back - contentTypeSave.DefaultTemplate = template.Alias; - } - - //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); - - //NOTE: this throws an error response if it is not valid - ValidateComposition(contentTypeSave, newCt); - - ctService.Save(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); - ctService.Save(newCt); - } - - display = Mapper.Map(newCt); - } + var display = Mapper.Map(savedCt); display.AddSuccessNotification( Services.TextService.Localize("speechBubbles/contentTypeSavedHeader"), diff --git a/src/Umbraco.Web/Editors/ContentTypeControllerBase.cs b/src/Umbraco.Web/Editors/ContentTypeControllerBase.cs index ea652dbe50..2926bd5983 100644 --- a/src/Umbraco.Web/Editors/ContentTypeControllerBase.cs +++ b/src/Umbraco.Web/Editors/ContentTypeControllerBase.cs @@ -45,7 +45,7 @@ namespace Umbraco.Web.Editors { } - public DataTypeBasic GetAssignedListViewDataType(int contentTypeId) + protected internal DataTypeBasic GetAssignedListViewDataType(int contentTypeId) { var objectType = Services.EntityService.GetObjectType(contentTypeId); @@ -81,12 +81,12 @@ namespace Umbraco.Web.Editors /// Gets all user defined properties. ///
/// - public IEnumerable GetAllPropertyTypeAliases() + protected IEnumerable GetAllPropertyTypeAliases() { return ApplicationContext.Services.ContentTypeService.GetAllPropertyTypeAliases(); } - public ContentPropertyDisplay GetPropertyTypeScaffold(int id) + protected ContentPropertyDisplay GetPropertyTypeScaffold(int id) { var dataTypeDiff = Services.DataTypeService.GetDataTypeDefinitionById(id); @@ -107,7 +107,7 @@ namespace Umbraco.Web.Editors }; } - public dynamic GetSafeAlias(string value, bool camelCase = true) + protected dynamic GetSafeAlias(string value, bool camelCase = true) { var returnValue = (string.IsNullOrWhiteSpace(value)) ? string.Empty : value.ToSafeAlias(camelCase); dynamic returnObj = new System.Dynamic.ExpandoObject(); @@ -118,9 +118,42 @@ namespace Umbraco.Web.Editors return returnObj; } + /// + /// Validates the composition and adds errors to the model state if any are found then throws an error response if there are errors + /// + /// + /// + /// + protected void ValidateComposition(ContentTypeSave contentTypeSave, IContentTypeComposition composition) + { + var validateAttempt = Services.ContentTypeService.ValidateComposition(composition); + if (validateAttempt == false) + { + //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"); + } - public string TranslateItem(string text) + 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)); + } + + } + + protected string TranslateItem(string text) { if (text == null) { @@ -134,6 +167,89 @@ namespace Umbraco.Web.Editors return CultureDictionary[text].IfNullOrWhiteSpace(text); } + protected TContentType PerformPostSave( + ContentTypeSave contentTypeSave, + Func getContentType, + Action saveContentType, + Action beforeCreateNew = null) + where TContentType : IContentTypeComposition + { + 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); + + //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); + + //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/MediaTypeController.cs b/src/Umbraco.Web/Editors/MediaTypeController.cs index 91607f0df7..9cdc3e6b95 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..2846c9a1ea 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; @@ -105,58 +106,20 @@ 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)); - 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/Models/Mapping/ContentTypeModelMapper.cs b/src/Umbraco.Web/Models/Mapping/ContentTypeModelMapper.cs index 75bd6bbaae..27aaf17599 100644 --- a/src/Umbraco.Web/Models/Mapping/ContentTypeModelMapper.cs +++ b/src/Umbraco.Web/Models/Mapping/ContentTypeModelMapper.cs @@ -49,7 +49,6 @@ namespace Umbraco.Web.Models.Mapping .ForMember(type => type.UpdateDate, expression => expression.Ignore()) .ForMember(type => type.HasIdentity, expression => expression.Ignore()); - config.CreateMap() //do the base mapping .MapBaseContentTypeSaveToEntity(applicationContext) @@ -64,84 +63,29 @@ namespace Umbraco.Web.Models.Mapping if (source.DefaultTemplate != null) 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() - // //do the base mapping - // .MapBaseContentTypeSaveToEntity(applicationContext) - // .AfterMap((source, dest) => - // { - - // //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.MemberTypeService.Get(a); - // if (addCt != null) - // dest.AddContentType(addCt); - // } - // }); - - - //config.CreateMap() - // //do the base mapping - // .MapBaseContentTypeSaveToEntity(applicationContext) - // .AfterMap((source, dest) => - // { - // //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.GetMediaType(a); - // if (addCt != null) - // dest.AddContentType(addCt); - // } - // }); + config.CreateMap() + //do the base mapping + .MapBaseContentTypeSaveToEntity(applicationContext) + .ConstructUsing((source) => new MediaType(source.ParentId)) + .AfterMap((source, dest) => + { + ContentTypeModelMapperExtensions.AfterMapContentTypeSaveToEntity(source, dest, applicationContext); + }); + config.CreateMap() + //do the base mapping + .MapBaseContentTypeSaveToEntity(applicationContext) + .ConstructUsing((source) => new MemberType(source.ParentId)) + .AfterMap((source, dest) => + { + ContentTypeModelMapperExtensions.AfterMapContentTypeSaveToEntity(source, dest, applicationContext); + }); config.CreateMap().ConvertUsing(x => x.Alias); - config.CreateMap() //map base logic .MapBaseContentTypeEntityToDisplay(applicationContext, _propertyEditorResolver); @@ -151,7 +95,6 @@ namespace Umbraco.Web.Models.Mapping .MapBaseContentTypeEntityToDisplay(applicationContext, _propertyEditorResolver) .AfterMap((source, dest) => { - //default listview dest.ListViewEditorName = Constants.Conventions.DataTypes.ListViewPrefix + "Media"; @@ -224,15 +167,13 @@ namespace Umbraco.Web.Models.Mapping #region *** Used for mapping on top of an existing display object from a save object *** + config.CreateMap() + .MapBaseContentTypeSaveToDisplay(); + config.CreateMap() - .ForMember(dto => dto.CreateDate, expression => expression.Ignore()) - .ForMember(dto => dto.UpdateDate, expression => expression.Ignore()) + .MapBaseContentTypeSaveToDisplay() .ForMember(dto => dto.AllowedTemplates, expression => expression.Ignore()) .ForMember(dto => dto.DefaultTemplate, 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()) .AfterMap((source, dest) => { //sync templates diff --git a/src/Umbraco.Web/Models/Mapping/ContentTypeModelMapperExtensions.cs b/src/Umbraco.Web/Models/Mapping/ContentTypeModelMapperExtensions.cs index 9d9457eb50..cfa12e214f 100644 --- a/src/Umbraco.Web/Models/Mapping/ContentTypeModelMapperExtensions.cs +++ b/src/Umbraco.Web/Models/Mapping/ContentTypeModelMapperExtensions.cs @@ -19,6 +19,48 @@ namespace Umbraco.Web.Models.Mapping /// 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 From e1629932dceba46e9a04ed8373aeda890e3e938c Mon Sep 17 00:00:00 2001 From: Shannon Date: Thu, 15 Oct 2015 15:29:14 +0200 Subject: [PATCH 17/22] Wires up the correct saving logic + validation for the member type editor, moves the GetSafeAlias method to the EntityController --- .../editors/umbGenerateAlias.directive.js | 4 +- .../common/resources/contenttype.resource.js | 10 - .../src/common/resources/entity.resource.js | 10 + .../common/resources/membertype.resource.js | 6 +- .../views/documenttypes/edit.controller.js | 368 +++++++++--------- .../src/views/mediatypes/edit.controller.js | 333 ++++++++-------- .../src/views/membertypes/edit.controller.js | 249 ++++++------ .../Editors/ContentTypeController.cs | 38 +- .../Editors/ContentTypeControllerBase.cs | 64 +-- src/Umbraco.Web/Editors/EntityController.cs | 17 + .../Editors/MediaTypeController.cs | 6 +- .../Editors/MemberTypeController.cs | 15 +- 12 files changed, 560 insertions(+), 560 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/editors/umbGenerateAlias.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/editors/umbGenerateAlias.directive.js index 4a52407972..2fadc76173 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/editors/umbGenerateAlias.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/editors/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/resources/contenttype.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/contenttype.resource.js index 631a3f6630..ecf1edba5b 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 @@ -133,16 +133,6 @@ function contentTypeResource($q, $http, umbRequestHelper, umbDataFormatter) { '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 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/membertype.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/membertype.resource.js index 5d562ae381..06782df40c 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 { @@ -63,8 +63,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/views/documenttypes/edit.controller.js b/src/Umbraco.Web.UI.Client/src/views/documenttypes/edit.controller.js index 21d41cffb3..0119773b1f 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,261 +6,261 @@ * @description * The controller for the content type editor */ -(function() { - "use strict"; +(function () { + "use strict"; - function DocumentTypesEditController($scope, $routeParams, contentTypeResource, dataTypeResource, editorState, contentEditingHelper, formHelper, navigationService, iconHelper, contentTypeHelper, notificationsService, $filter, $q) { + function DocumentTypesEditController($scope, $routeParams, contentTypeResource, dataTypeResource, editorState, contentEditingHelper, formHelper, navigationService, iconHelper, contentTypeHelper, notificationsService, $filter, $q) { - 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() { - var deferred = $q.defer(); + var deferred = $q.defer(); - vm.page.saveButtonState = "busy"; + vm.page.saveButtonState = "busy"; - // reformat allowed content types to array if id's - vm.contentType.allowedContentTypes = contentTypeHelper.createIdArray(vm.contentType.allowedContentTypes); + // 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); - } + // 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); + 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); - vm.page.saveButtonState = "success"; + vm.page.saveButtonState = "success"; - deferred.resolve(data); - }, function (err) { - //error - if (err) { - editorState.set($scope.content); - } + deferred.resolve(data); + }, function (err) { + //error + if (err) { + editorState.set($scope.content); + } - vm.page.saveButtonState = "error"; + vm.page.saveButtonState = "error"; - deferred.reject(err); - }); + deferred.reject(err); + }); - return deferred.promise; + return deferred.promise; - } - - function init(contentType){ + } - // set all tab to inactive - if( contentType.groups.length !== 0 ) { - angular.forEach(contentType.groups, function(group){ + function init(contentType) { - angular.forEach(group.properties, function(property){ - // get data type details for each property - getDataTypeDetails(property); + // 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); + }); + + }); + } + + // 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; }); - - }); - } - - // 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/mediatypes/edit.controller.js b/src/Umbraco.Web.UI.Client/src/views/mediatypes/edit.controller.js index d7f3f4ee83..56d3110965 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,238 +6,233 @@ * @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, $q) { + function MediaTypesEditController($scope, $routeParams, mediaTypeResource, dataTypeResource, editorState, contentEditingHelper, formHelper, navigationService, iconHelper, contentTypeHelper, notificationsService, $filter, $q) { - 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() { - var deferred = $q.defer(); + var deferred = $q.defer(); - vm.page.saveButtonState = "busy"; + vm.page.saveButtonState = "busy"; - // reformat allowed content types to array if id's - vm.contentType.allowedContentTypes = contentTypeHelper.createIdArray(vm.contentType.allowedContentTypes); + // 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: 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); - 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"; - vm.page.saveButtonState = "success"; + deferred.resolve(data); + }, function (err) { + //error + if (err) { + editorState.set($scope.content); + } - deferred.resolve(data); - }, function (err) { - //error - if (err) { - editorState.set($scope.content); - } + vm.page.saveButtonState = "error"; - vm.page.saveButtonState = "error"; + deferred.reject(err); + }); - deferred.reject(err); - }); + return deferred.promise; - return deferred.promise; + } - } + function init(contentType) { - function init(contentType){ + // set all tab to inactive + if (contentType.groups.length !== 0) { + angular.forEach(contentType.groups, function (group) { - // 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); + }); - angular.forEach(group.properties, function(property){ - // get data type details for each property - getDataTypeDetails(property); + }); + } + + // 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; }); - - }); - } - - // 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/membertypes/edit.controller.js b/src/Umbraco.Web.UI.Client/src/views/membertypes/edit.controller.js index 499650c9c5..5b02063b54 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,181 @@ * @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) { - 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); + } - } + 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/Editors/ContentTypeController.cs b/src/Umbraco.Web/Editors/ContentTypeController.cs index 038b2b19ba..9568a39678 100644 --- a/src/Umbraco.Web/Editors/ContentTypeController.cs +++ b/src/Umbraco.Web/Editors/ContentTypeController.cs @@ -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) + }; + } + public HttpResponseMessage PostCreateFolder(int parentId, string name) { var result = Services.ContentTypeService.CreateFolder(parentId, name, Security.CurrentUser.Id); @@ -95,10 +125,10 @@ namespace Umbraco.Web.Editors public ContentTypeDisplay PostSave(ContentTypeSave contentTypeSave) { var savedCt = PerformPostSave( - contentTypeSave: contentTypeSave, - getContentType: i => Services.ContentTypeService.GetContentType(i), - saveContentType: type => Services.ContentTypeService.Save(type), - beforeCreateNew: ctSave => + contentTypeSave: contentTypeSave, + getContentType: i => Services.ContentTypeService.GetContentType(i), + saveContentType: type => Services.ContentTypeService.Save(type), + beforeCreateNew: ctSave => { //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 ? diff --git a/src/Umbraco.Web/Editors/ContentTypeControllerBase.cs b/src/Umbraco.Web/Editors/ContentTypeControllerBase.cs index 2926bd5983..7ad770a045 100644 --- a/src/Umbraco.Web/Editors/ContentTypeControllerBase.cs +++ b/src/Umbraco.Web/Editors/ContentTypeControllerBase.cs @@ -77,47 +77,6 @@ namespace Umbraco.Web.Editors } } - /// - /// Gets all user defined properties. - /// - /// - protected IEnumerable GetAllPropertyTypeAliases() - { - return ApplicationContext.Services.ContentTypeService.GetAllPropertyTypeAliases(); - } - - protected 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) - }; - } - - protected 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; - } - /// /// Validates the composition and adds errors to the model state if any are found then throws an error response if there are errors /// @@ -171,6 +130,7 @@ namespace Umbraco.Web.Editors ContentTypeSave contentTypeSave, Func getContentType, Action saveContentType, + bool validateComposition = true, Action beforeCreateNew = null) where TContentType : IContentTypeComposition { @@ -201,10 +161,13 @@ namespace Umbraco.Web.Editors if (found == null) throw new HttpResponseException(HttpStatusCode.NotFound); - Mapper.Map(contentTypeSave, found); - - //NOTE: this throws an error response if it is not valid - ValidateComposition(contentTypeSave, found); + Mapper.Map(contentTypeSave, found); + + if (validateComposition) + { + //NOTE: this throws an error response if it is not valid + ValidateComposition(contentTypeSave, found); + } saveContentType(found); @@ -232,10 +195,13 @@ namespace Umbraco.Web.Editors } //save as new - var newCt = Mapper.Map(contentTypeSave); - - //NOTE: this throws an error response if it is not valid - ValidateComposition(contentTypeSave, newCt); + var newCt = Mapper.Map(contentTypeSave); + + if (validateComposition) + { + //NOTE: this throws an error response if it is not valid + ValidateComposition(contentTypeSave, newCt); + } saveContentType(newCt); 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 9cdc3e6b95..06bbecf0cb 100644 --- a/src/Umbraco.Web/Editors/MediaTypeController.cs +++ b/src/Umbraco.Web/Editors/MediaTypeController.cs @@ -105,9 +105,9 @@ namespace Umbraco.Web.Editors public ContentTypeCompositionDisplay PostSave(ContentTypeSave contentTypeSave) { var savedCt = PerformPostSave( - contentTypeSave: contentTypeSave, - getContentType: i => Services.ContentTypeService.GetMediaType(i), - saveContentType: type => Services.ContentTypeService.Save(type)); + contentTypeSave: contentTypeSave, + getContentType: i => Services.ContentTypeService.GetMediaType(i), + saveContentType: type => Services.ContentTypeService.Save(type)); var display = Mapper.Map(savedCt); diff --git a/src/Umbraco.Web/Editors/MemberTypeController.cs b/src/Umbraco.Web/Editors/MemberTypeController.cs index 2846c9a1ea..fa9b678340 100644 --- a/src/Umbraco.Web/Editors/MemberTypeController.cs +++ b/src/Umbraco.Web/Editors/MemberTypeController.cs @@ -19,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 { /// @@ -109,9 +105,10 @@ namespace Umbraco.Web.Editors public ContentTypeCompositionDisplay PostSave(ContentTypeSave contentTypeSave) { var savedCt = PerformPostSave( - contentTypeSave: contentTypeSave, - getContentType: i => Services.MemberTypeService.Get(i), - saveContentType: type => Services.MemberTypeService.Save(type)); + contentTypeSave: contentTypeSave, + getContentType: i => Services.MemberTypeService.Get(i), + saveContentType: type => Services.MemberTypeService.Save(type), + validateComposition: false); var display = Mapper.Map(savedCt); From 7268c6786d191c3ffa9f98e0dede3500a9797db7 Mon Sep 17 00:00:00 2001 From: Shannon Date: Thu, 15 Oct 2015 15:42:46 +0200 Subject: [PATCH 18/22] manual merge back in the temporary DeleteContainerById method --- .../Editors/ContentTypeController.cs | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/src/Umbraco.Web/Editors/ContentTypeController.cs b/src/Umbraco.Web/Editors/ContentTypeController.cs index 9568a39678..15b80eaa75 100644 --- a/src/Umbraco.Web/Editors/ContentTypeController.cs +++ b/src/Umbraco.Web/Editors/ContentTypeController.cs @@ -111,6 +111,33 @@ namespace Umbraco.Web.Editors }; } + /// + /// Deletes a document type container wth a given ID + /// + /// + /// + [HttpDelete] + [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()) + { + throw new HttpResponseException(HttpStatusCode.Forbidden); + } + + //TODO: what service to use to delete? + + return Request.CreateResponse(HttpStatusCode.OK); + } + public HttpResponseMessage PostCreateFolder(int parentId, string name) { var result = Services.ContentTypeService.CreateFolder(parentId, name, Security.CurrentUser.Id); From 65b080b03f41ea26197a5aedec2c877dc2e51fa2 Mon Sep 17 00:00:00 2001 From: Shannon Date: Tue, 20 Oct 2015 15:37:11 +0200 Subject: [PATCH 19/22] fixes ysod with validation errors on doc type editor. --- src/Umbraco.Web/Editors/ContentTypeController.cs | 2 +- src/Umbraco.Web/Editors/ContentTypeControllerBase.cs | 5 +++-- src/Umbraco.Web/Editors/MediaTypeController.cs | 2 +- src/Umbraco.Web/Editors/MemberTypeController.cs | 2 +- 4 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/Umbraco.Web/Editors/ContentTypeController.cs b/src/Umbraco.Web/Editors/ContentTypeController.cs index 15b80eaa75..aac243f727 100644 --- a/src/Umbraco.Web/Editors/ContentTypeController.cs +++ b/src/Umbraco.Web/Editors/ContentTypeController.cs @@ -151,7 +151,7 @@ namespace Umbraco.Web.Editors public ContentTypeDisplay PostSave(ContentTypeSave contentTypeSave) { - var savedCt = PerformPostSave( + var savedCt = PerformPostSave( contentTypeSave: contentTypeSave, getContentType: i => Services.ContentTypeService.GetContentType(i), saveContentType: type => Services.ContentTypeService.Save(type), diff --git a/src/Umbraco.Web/Editors/ContentTypeControllerBase.cs b/src/Umbraco.Web/Editors/ContentTypeControllerBase.cs index 7ad770a045..6f3f96d365 100644 --- a/src/Umbraco.Web/Editors/ContentTypeControllerBase.cs +++ b/src/Umbraco.Web/Editors/ContentTypeControllerBase.cs @@ -126,13 +126,14 @@ namespace Umbraco.Web.Editors return CultureDictionary[text].IfNullOrWhiteSpace(text); } - protected TContentType PerformPostSave( + 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); @@ -140,7 +141,7 @@ namespace Umbraco.Web.Editors { var ct = getContentType(ctId); //Required data is invalid so we cannot continue - var forDisplay = Mapper.Map(ct); + var forDisplay = Mapper.Map(ct); //map the 'save' data on top forDisplay = Mapper.Map(contentTypeSave, forDisplay); forDisplay.Errors = ModelState.ToErrorDictionary(); diff --git a/src/Umbraco.Web/Editors/MediaTypeController.cs b/src/Umbraco.Web/Editors/MediaTypeController.cs index 06bbecf0cb..4b1da89713 100644 --- a/src/Umbraco.Web/Editors/MediaTypeController.cs +++ b/src/Umbraco.Web/Editors/MediaTypeController.cs @@ -104,7 +104,7 @@ namespace Umbraco.Web.Editors public ContentTypeCompositionDisplay PostSave(ContentTypeSave contentTypeSave) { - var savedCt = PerformPostSave( + var savedCt = PerformPostSave( contentTypeSave: contentTypeSave, getContentType: i => Services.ContentTypeService.GetMediaType(i), saveContentType: type => Services.ContentTypeService.Save(type)); diff --git a/src/Umbraco.Web/Editors/MemberTypeController.cs b/src/Umbraco.Web/Editors/MemberTypeController.cs index fa9b678340..19a89cd963 100644 --- a/src/Umbraco.Web/Editors/MemberTypeController.cs +++ b/src/Umbraco.Web/Editors/MemberTypeController.cs @@ -104,7 +104,7 @@ namespace Umbraco.Web.Editors public ContentTypeCompositionDisplay PostSave(ContentTypeSave contentTypeSave) { - var savedCt = PerformPostSave( + var savedCt = PerformPostSave( contentTypeSave: contentTypeSave, getContentType: i => Services.MemberTypeService.Get(i), saveContentType: type => Services.MemberTypeService.Save(type), From 73fb761c4d0995bcab487d7a35989839837dddf3 Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 21 Oct 2015 17:52:58 +0200 Subject: [PATCH 20/22] Fixes issue with umb-editor-sub-views so that each view is actually rendered out and not replaced otherwise when you switch sections all of the wired up validation gets removed. This will also enable us to highlight the menu header when validation errors exist on that section. We also now show a single validation error message even when there is client side validation errors. --- .../src/common/services/formhelper.service.js | 5 +++-- .../views/components/editor/umb-editor-sub-views.html | 5 ++--- .../src/views/documenttypes/edit.controller.js | 9 ++++++++- .../src/views/mediatypes/edit.controller.js | 9 ++++++++- .../src/views/membertypes/edit.controller.js | 9 ++++++++- .../unit/app/content/edit-content-controller.spec.js | 3 ++- .../test/unit/app/media/edit-media-controller.spec.js | 3 ++- .../propertyeditors/content-picker-controller.spec.js | 3 ++- .../unit/common/services/content-editing-helper.spec.js | 4 +++- src/Umbraco.Web.UI/umbraco/config/lang/en.xml | 2 ++ src/Umbraco.Web.UI/umbraco/config/lang/en_us.xml | 2 ++ 11 files changed, 42 insertions(+), 12 deletions(-) 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 e3261b7372..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,7 +157,7 @@ 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.... @@ -204,6 +204,7 @@ function formHelper(angularHelper, serverValidationManager, $timeout, notificati //add to notifications notificationsService.error("Validation", modelState[e][0]); + } } }; 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/documenttypes/edit.controller.js b/src/Umbraco.Web.UI.Client/src/views/documenttypes/edit.controller.js index 37d2b1b3d7..59981bd3b3 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 @@ -9,7 +9,7 @@ (function () { "use strict"; - function DocumentTypesEditController($scope, $routeParams, modelsResource, contentTypeResource, dataTypeResource, editorState, contentEditingHelper, formHelper, navigationService, iconHelper, contentTypeHelper, notificationsService, $filter, $q) { + function DocumentTypesEditController($scope, $routeParams, modelsResource, contentTypeResource, dataTypeResource, editorState, contentEditingHelper, formHelper, navigationService, iconHelper, contentTypeHelper, notificationsService, $filter, $q, localizationService) { var vm = this; @@ -219,6 +219,13 @@ 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"; 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 56d3110965..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 @@ -9,7 +9,7 @@ (function () { "use strict"; - function MediaTypesEditController($scope, $routeParams, mediaTypeResource, dataTypeResource, editorState, contentEditingHelper, formHelper, navigationService, iconHelper, contentTypeHelper, notificationsService, $filter, $q) { + function MediaTypesEditController($scope, $routeParams, mediaTypeResource, dataTypeResource, editorState, contentEditingHelper, formHelper, navigationService, iconHelper, contentTypeHelper, notificationsService, $filter, $q, localizationService) { var vm = this; @@ -151,6 +151,13 @@ 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"; 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 5b02063b54..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 @@ -9,7 +9,7 @@ (function () { "use strict"; - function MemberTypesEditController($scope, $rootScope, $routeParams, $log, $filter, memberTypeResource, dataTypeResource, editorState, iconHelper, formHelper, navigationService, contentEditingHelper, notificationsService, $q) { + function MemberTypesEditController($scope, $rootScope, $routeParams, $log, $filter, memberTypeResource, dataTypeResource, editorState, iconHelper, formHelper, navigationService, contentEditingHelper, notificationsService, $q, localizationService) { var vm = this; @@ -102,6 +102,13 @@ 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"; 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 73772ae2a9..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'); diff --git a/src/Umbraco.Web.UI/umbraco/config/lang/en.xml b/src/Umbraco.Web.UI/umbraco/config/lang/en.xml index b1a2821ca5..2a96b3e173 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 f8c956e14d..3fcb54e939 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 From 9372b1df47f6a80cb9b9b4e86052d5050f450b43 Mon Sep 17 00:00:00 2001 From: Mads Rasmussen Date: Thu, 22 Oct 2015 14:02:18 +0200 Subject: [PATCH 21/22] fix placeholder width on input auto resize in firefox --- .../directives/editors/umbAutoResize.directive.js | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/editors/umbAutoResize.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/editors/umbAutoResize.directive.js index e28d6ef29e..5ba8839d08 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/editors/umbAutoResize.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/editors/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'); } } From fd26f9835646b3310b7f2e2a2243c03407109c2e Mon Sep 17 00:00:00 2001 From: Shannon Date: Thu, 22 Oct 2015 15:10:44 +0200 Subject: [PATCH 22/22] fixes template mapping issue when saving content types --- .../Models/Mapping/ContentTypeModelMapper.cs | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/Umbraco.Web/Models/Mapping/ContentTypeModelMapper.cs b/src/Umbraco.Web/Models/Mapping/ContentTypeModelMapper.cs index 27aaf17599..626da45caa 100644 --- a/src/Umbraco.Web/Models/Mapping/ContentTypeModelMapper.cs +++ b/src/Umbraco.Web/Models/Mapping/ContentTypeModelMapper.cs @@ -53,13 +53,15 @@ namespace Umbraco.Web.Models.Mapping //do the base mapping .MapBaseContentTypeSaveToEntity(applicationContext) .ConstructUsing((source) => new ContentType(source.ParentId)) - .ForMember( - dto => dto.AllowedTemplates, - expression => expression.ResolveUsing(basic => basic.AllowedTemplates.Where(x => x != null) - .Select(s => applicationContext.Services.FileService.GetTemplate(s)))) + .ForMember(source => source.AllowedTemplates, expression => expression.Ignore()) .ForMember(dto => dto.DefaultTemplate, expression => expression.Ignore()) .AfterMap((source, dest) => { + dest.AllowedTemplates = source.AllowedTemplates + .Where(x => x != null) + .Select(s => applicationContext.Services.FileService.GetTemplate(s)) + .ToArray(); + if (source.DefaultTemplate != null) dest.SetDefaultTemplate(applicationContext.Services.FileService.GetTemplate(source.DefaultTemplate)); @@ -115,7 +117,7 @@ namespace Umbraco.Web.Models.Mapping .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); @@ -182,7 +184,7 @@ namespace Umbraco.Web.Models.Mapping 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))); + dest.AllowedTemplates = source.AllowedTemplates.Select(x => Mapper.Map(templates.Single(t => t.Alias == x))).ToArray(); } if (source.DefaultTemplate.IsNullOrWhiteSpace() == false)