diff --git a/src/Umbraco.Core/PropertyEditors/ValueConverters/LabelValueConverter.cs b/src/Umbraco.Core/PropertyEditors/ValueConverters/LabelValueConverter.cs index 84baf226cf..eb461b4920 100644 --- a/src/Umbraco.Core/PropertyEditors/ValueConverters/LabelValueConverter.cs +++ b/src/Umbraco.Core/PropertyEditors/ValueConverters/LabelValueConverter.cs @@ -65,6 +65,8 @@ namespace Umbraco.Core.PropertyEditors.ValueConverters if (source is decimal sourceDecimal) return sourceDecimal; if (source is string sourceDecimalString) return decimal.TryParse(sourceDecimalString, NumberStyles.Any, CultureInfo.InvariantCulture, out var d) ? d : 0; + if (source is double sourceDouble) + return Convert.ToDecimal(sourceDouble); return (decimal) 0; case ValueTypes.Integer: if (source is int sourceInt) return sourceInt; diff --git a/src/Umbraco.Tests/PublishedContent/NuCacheChildrenTests.cs b/src/Umbraco.Tests/PublishedContent/NuCacheChildrenTests.cs index 47572a63ee..fbcdf7731a 100644 --- a/src/Umbraco.Tests/PublishedContent/NuCacheChildrenTests.cs +++ b/src/Umbraco.Tests/PublishedContent/NuCacheChildrenTests.cs @@ -1,7 +1,9 @@ using System; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Data; using System.Linq; +using System.Reflection; using Moq; using NUnit.Framework; using Umbraco.Core; @@ -25,6 +27,7 @@ using Umbraco.Web.Cache; using Umbraco.Web.PublishedCache; using Umbraco.Web.PublishedCache.NuCache; using Umbraco.Web.PublishedCache.NuCache.DataSource; +using Umbraco.Web.PublishedCache.NuCache.Snap; namespace Umbraco.Tests.PublishedContent { @@ -163,7 +166,7 @@ namespace Umbraco.Tests.PublishedContent //1x variant (root) yield return CreateVariantKit(1, -1, 1, paths); - + //1x invariant under root yield return CreateInvariantKit(4, 1, 1, paths); @@ -952,6 +955,43 @@ namespace Umbraco.Tests.PublishedContent AssertDocuments(documents, "N1-en-US", "N2-en-US", "N3-en-US"); } + [Test] + public void Issue6353() + { + IEnumerable GetKits() + { + var paths = new Dictionary { { -1, "-1" } }; + + yield return CreateInvariantKit(1, -1, 1, paths); + yield return CreateInvariantKit(2, 1, 1, paths); + } + + Init(GetKits()); + + var snapshotService = (PublishedSnapshotService) _snapshotService; + var contentStoreField = typeof(PublishedSnapshotService).GetField("_contentStore", BindingFlags.Instance | BindingFlags.NonPublic); + var contentStore = (ContentStore) contentStoreField.GetValue(snapshotService); + var contentNodesField = typeof(ContentStore).GetField("_contentNodes", BindingFlags.Instance | BindingFlags.NonPublic); + var contentNodes = (ConcurrentDictionary>) contentNodesField.GetValue(contentStore); + + var parentNode = contentNodes[1].Value; + Assert.AreEqual(-1, parentNode.PreviousSiblingContentId); + Assert.AreEqual(-1, parentNode.NextSiblingContentId); + Assert.AreEqual(2, parentNode.FirstChildContentId); + Assert.AreEqual(2, parentNode.LastChildContentId); + + _snapshotService.Notify(new[] + { + new ContentCacheRefresher.JsonPayload(2, TreeChangeTypes.Remove) + }, out _, out _); + + parentNode = contentNodes[1].Value; + Assert.AreEqual(-1, parentNode.PreviousSiblingContentId); + Assert.AreEqual(-1, parentNode.NextSiblingContentId); + Assert.AreEqual(-1, parentNode.FirstChildContentId); + Assert.AreEqual(-1, parentNode.LastChildContentId); + } + private void AssertDocuments(IPublishedContent[] documents, params string[] names) { Assert.AreEqual(names.Length, documents.Length); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/buttons/umbbutton.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/buttons/umbbutton.directive.js index f026a05c45..56416e3544 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/buttons/umbbutton.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/buttons/umbbutton.directive.js @@ -67,6 +67,9 @@ Use this directive to render an umbraco button. The directive can be used to gen @param {boolean=} disabled Set to true to disable the button. @param {string=} addEllipsis Adds an ellipsis character (…) to the button label which means the button will open a dialog or prompt the user for more information. @param {string=} showCaret Shows a caret on the right side of the button label +@param {string=} autoFocus add autoFocus to the button +@param {string=} hasPopup Used to expose to the accessibility API whether the button will trigger a popup or not +@param {string=]} isExpanded Used to add an aria-expanded attribute and expose whether the button has expanded a popup or not **/ @@ -96,7 +99,9 @@ Use this directive to render an umbraco button. The directive can be used to gen alias: "@?", addEllipsis: "@?", showCaret: "@?", - autoFocus: "@?" + autoFocus: "@?", + hasPopup: "@?", + isExpanded: " + _.reduce( + _.map(v.$error, e => e.length), + (m, n) => m + n + ) + ), + (memo, num) => memo + num + ); + return val.length + innerErrorCount; }); //sum up all numbers in the resulting array var sum = _.reduce(validatorLengths, function (memo, num) { diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/validation/valpropertymsg.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/validation/valpropertymsg.directive.js index 39d903d85e..0369b4bd2e 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/validation/valpropertymsg.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/validation/valpropertymsg.directive.js @@ -9,7 +9,7 @@ * and when an error is detected for this property we'll show the error message. * In order for this directive to work, the valFormManager directive must be placed on the containing form. **/ -function valPropertyMsg(serverValidationManager) { +function valPropertyMsg(serverValidationManager, localizationService) { return { require: ['^^form', '^^valFormManager', '^^umbProperty', '?^^umbVariantContent'], @@ -40,6 +40,11 @@ function valPropertyMsg(serverValidationManager) { scope.currentProperty = currentProperty; var currentCulture = currentProperty.culture; + var labels = {}; + localizationService.localize("errors_propertyHasErrors").then(function (data) { + labels.propertyHasErrors = data; + }); + if (umbVariantCtrl) { //if we are inside of an umbVariantContent directive @@ -68,13 +73,11 @@ function valPropertyMsg(serverValidationManager) { return err.errorMsg; } else { - // TODO: localize - return scope.currentProperty.propertyErrorMessage ? scope.currentProperty.propertyErrorMessage : "Property has errors"; + return scope.currentProperty.propertyErrorMessage ? scope.currentProperty.propertyErrorMessage : labels.propertyHasErrors; } } - // TODO: localize - return "Property has errors"; + return labels.propertyHasErrors; } // We need to subscribe to any changes to our model (based on user input) diff --git a/src/Umbraco.Web.UI.Client/src/common/services/editor.service.js b/src/Umbraco.Web.UI.Client/src/common/services/editor.service.js index ad565412d9..ca4108e18c 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/editor.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/editor.service.js @@ -394,6 +394,52 @@ When building a custom infinite editor view you can use the same components as a editor.treeAlias = "documentTypes"; open(editor); } + + /** + * @ngdoc method + * @name umbraco.services.editorService#mediaTypePicker + * @methodOf umbraco.services.editorService + * + * @description + * Opens a media type picker in infinite editing, the submit callback returns an array of selected items + * + * @param {Object} editor rendering options + * @param {Boolean} editor.multiPicker Pick one or multiple items + * @param {Function} editor.submit Callback function when the submit button is clicked. Returns the editor model object + * @param {Function} editor.close Callback function when the close button is clicked. + * + * @returns {Object} editor object + */ + function mediaTypePicker(editor) { + editor.view = "views/common/infiniteeditors/treepicker/treepicker.html"; + editor.size = "small"; + editor.section = "settings"; + editor.treeAlias = "mediaTypes"; + open(editor); + } + + /** + * @ngdoc method + * @name umbraco.services.editorService#memberTypePicker + * @methodOf umbraco.services.editorService + * + * @description + * Opens a member type picker in infinite editing, the submit callback returns an array of selected items + * + * @param {Object} editor rendering options + * @param {Boolean} editor.multiPicker Pick one or multiple items + * @param {Function} editor.submit Callback function when the submit button is clicked. Returns the editor model object + * @param {Function} editor.close Callback function when the close button is clicked. + * + * @returns {Object} editor object + */ + function memberTypePicker(editor) { + editor.view = "views/common/infiniteeditors/treepicker/treepicker.html"; + editor.size = "small"; + editor.section = "settings"; + editor.treeAlias = "memberTypes"; + open(editor); + } /** * @ngdoc method * @name umbraco.services.editorService#copy @@ -908,6 +954,8 @@ When building a custom infinite editor view you can use the same components as a contentEditor: contentEditor, contentPicker: contentPicker, contentTypePicker: contentTypePicker, + mediaTypePicker: mediaTypePicker, + memberTypePicker: memberTypePicker, copy: copy, move: move, embed: embed, diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-nested-content.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-nested-content.less index 59c90972d2..d018e76c0d 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-nested-content.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-nested-content.less @@ -154,6 +154,11 @@ .umb-nested-content__icon--disabled { opacity: 0.3; + cursor: default !important; + + &:hover { + color: @ui-option-type; + } } diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-node-preview.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-node-preview.less index 1edaffe824..cdd9dfa958 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-node-preview.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-node-preview.less @@ -67,6 +67,9 @@ } .umb-node-preview__action { + background: transparent; + padding: 0; + border: 0 none; margin-left: 5px; margin-right: 5px; font-size: 13px; @@ -89,11 +92,13 @@ display: flex; align-items: center; justify-content: center; + background: transparent; border: 1px dashed @ui-action-discreet-border; color: @ui-action-discreet-type; font-weight: bold; padding: 5px 15px; box-sizing: border-box; + width: 100%; } .umb-node-preview-add:hover { diff --git a/src/Umbraco.Web.UI.Client/src/views/common/drawers/help/help.html b/src/Umbraco.Web.UI.Client/src/views/common/drawers/help/help.html index a0413ce1a6..4ae3121098 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/drawers/help/help.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/drawers/help/help.html @@ -9,7 +9,7 @@ -
+
Tours
diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/treepicker/treepicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/treepicker/treepicker.controller.js index 31430c81cb..0ff6403761 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/treepicker/treepicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/treepicker/treepicker.controller.js @@ -96,7 +96,7 @@ angular.module("umbraco").controller("Umbraco.Editors.TreePickerController", }); } } - if (vm.treeAlias === "documentTypes") { + else if (vm.treeAlias === "documentTypes") { vm.entityType = "DocumentType"; if (!$scope.model.title) { localizationService.localize("defaultdialogs_selectContentType").then(function(value){ @@ -107,9 +107,17 @@ angular.module("umbraco").controller("Umbraco.Editors.TreePickerController", else if (vm.treeAlias === "member" || vm.section === "member") { vm.entityType = "Member"; if (!$scope.model.title) { - localizationService.localize("defaultdialogs_selectMember").then(function(value){ + localizationService.localize("defaultdialogs_selectMember").then(function(value) { $scope.model.title = value; - }) + }); + } + } + else if (vm.treeAlias === "memberTypes") { + vm.entityType = "MemberType"; + if (!$scope.model.title) { + localizationService.localize("defaultdialogs_selectMemberType").then(function(value){ + $scope.model.title = value; + }); } } else if (vm.treeAlias === "media" || vm.section === "media") { @@ -120,6 +128,14 @@ angular.module("umbraco").controller("Umbraco.Editors.TreePickerController", }); } } + else if (vm.treeAlias === "mediaTypes") { + vm.entityType = "MediaType"; + if (!$scope.model.title) { + localizationService.localize("defaultdialogs_selectMediaType").then(function(value){ + $scope.model.title = value; + }); + } + } // TODO: Seems odd this logic is here, i don't think it needs to be and should just exist on the property editor using this if ($scope.model.minNumber) { diff --git a/src/Umbraco.Web.UI.Client/src/views/components/buttons/umb-button-group.html b/src/Umbraco.Web.UI.Client/src/views/components/buttons/umb-button-group.html index de082fb48e..49827910d0 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/buttons/umb-button-group.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/buttons/umb-button-group.html @@ -31,7 +31,13 @@ - + diff --git a/src/Umbraco.Web.UI.Client/src/views/components/buttons/umb-button.html b/src/Umbraco.Web.UI.Client/src/views/components/buttons/umb-button.html index 8163b2807b..0c4c58c38f 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/buttons/umb-button.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/buttons/umb-button.html @@ -9,25 +9,36 @@ - + {{vm.buttonLabel}} - + - diff --git a/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editor-menu.html b/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editor-menu.html index c621670462..fe90fef07a 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editor-menu.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editor-menu.html @@ -5,15 +5,18 @@ button-style="white" action="dropdown.isOpen = !dropdown.isOpen" label-key="general_actions" - show-caret="true"> + show-caret="true" + has-popup="true" + is-expanded="dropdown.isOpen" + > - + - - + diff --git a/src/Umbraco.Web.UI.Client/src/views/components/forms/umb-checkbox.html b/src/Umbraco.Web.UI.Client/src/views/components/forms/umb-checkbox.html index 2f90905c7d..fd3b451e90 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/forms/umb-checkbox.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/forms/umb-checkbox.html @@ -8,7 +8,7 @@ ng-model="vm.model" ng-disabled="vm.disabled" ng-required="vm.required" - ng-change="vm.onChange()"/> + ng-change="vm.change()"/>
diff --git a/src/Umbraco.Web.UI.Client/src/views/prevalueeditors/contenttypepicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/prevalueeditors/contenttypepicker.controller.js deleted file mode 100644 index dcb2c0e582..0000000000 --- a/src/Umbraco.Web.UI.Client/src/views/prevalueeditors/contenttypepicker.controller.js +++ /dev/null @@ -1,54 +0,0 @@ -function ContentTypePickerController($scope, contentTypeResource, editorService, angularHelper) { - var vm = this; - vm.loading = false; - vm.contentTypes = []; - vm.remove = remove; - vm.add = add; - - var allContentTypes = null; - - function init() { - vm.loading = true; - contentTypeResource.getAll().then(function (all) { - allContentTypes = all; - vm.loading = false; - // the model value is a comma separated list of content type aliases - var currentContentTypes = _.map(($scope.model.value || "").split(","), function (s) { return s.trim(); }); - vm.contentTypes = _.filter(allContentTypes, function (contentType) { - return currentContentTypes.indexOf(contentType.alias) >= 0; - }); - }); - } - - function add() { - editorService.contentTypePicker({ - multiPicker: true, - submit: function (model) { - var newContentTypes = _.map(model.selection, function (selected) { - return _.findWhere(allContentTypes, {udi: selected.udi}); - }); - vm.contentTypes = _.uniq(_.union(vm.contentTypes, newContentTypes)); - updateModel(); - editorService.close(); - }, - close: function () { - editorService.close(); - } - }); - } - - function remove(contentType) { - vm.contentTypes = _.without(vm.contentTypes, contentType); - updateModel(); - } - - function updateModel() { - // the model value is a comma separated list of content type aliases - $scope.model.value = _.pluck(vm.contentTypes, "alias").join(); - angularHelper.getCurrentForm($scope).$setDirty(); - } - - init(); -} - -angular.module('umbraco').controller("Umbraco.PrevalueEditors.ContentTypePickerController", ContentTypePickerController); diff --git a/src/Umbraco.Web.UI.Client/src/views/prevalueeditors/treepicker.html b/src/Umbraco.Web.UI.Client/src/views/prevalueeditors/treepicker.html index ae260b4502..ab1b84db97 100644 --- a/src/Umbraco.Web.UI.Client/src/views/prevalueeditors/treepicker.html +++ b/src/Umbraco.Web.UI.Client/src/views/prevalueeditors/treepicker.html @@ -11,7 +11,7 @@ allow-remove="allowRemove" allow-edit="allowEdit" on-remove="remove($index)" - on-edit="openContentPicker()"> + on-edit="openContentPicker()">
diff --git a/src/Umbraco.Web.UI.Client/src/views/prevalueeditors/treesource.controller.js b/src/Umbraco.Web.UI.Client/src/views/prevalueeditors/treesource.controller.js index 404abbab1f..828763bc1c 100644 --- a/src/Umbraco.Web.UI.Client/src/views/prevalueeditors/treesource.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/prevalueeditors/treesource.controller.js @@ -3,7 +3,7 @@ angular.module('umbraco') .controller("Umbraco.PrevalueEditors.TreeSourceController", - function($scope, entityResource, iconHelper, editorService){ + function($scope, $timeout, entityResource, iconHelper, editorService, eventsService){ if (!$scope.model) { $scope.model = {}; @@ -23,7 +23,11 @@ angular.module('umbraco') entityResource.getById($scope.model.value.id, entityType()).then(function(item){ populate(item); }); - } + } + + $timeout(function () { + treeSourceChanged(); + }, 100); function entityType() { var ent = "Document"; @@ -58,8 +62,13 @@ angular.module('umbraco') $scope.model.value.id = null; $scope.node = null; $scope.model.value.query = null; + + treeSourceChanged(); }; - + + function treeSourceChanged() { + eventsService.emit("treeSourceChanged", { value: $scope.model.value.type }); + } //we always need to ensure we dont submit anything broken var unsubscribe = $scope.$on("formSubmitting", function (ev, args) { diff --git a/src/Umbraco.Web.UI.Client/src/views/prevalueeditors/treesourcetypepicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/prevalueeditors/treesourcetypepicker.controller.js new file mode 100644 index 0000000000..0df1b3cafb --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/prevalueeditors/treesourcetypepicker.controller.js @@ -0,0 +1,95 @@ +function TreeSourceTypePickerController($scope, contentTypeResource, mediaTypeResource, memberTypeResource, editorService, eventsService, angularHelper) { + var vm = this; + vm.loading = false; + vm.itemTypes = []; + vm.remove = remove; + vm.add = add; + + var allItemTypes = null; + var currentItemType = null; + var initialLoad = true; + + function init() { + vm.loading = true; + + switch (currentItemType) { + case "content": + contentTypeResource.getAll().then(getAllItemTypesCallback); + break; + case "media": + mediaTypeResource.getAll().then(getAllItemTypesCallback); + break; + case "member": + memberTypeResource.getTypes().then(getAllItemTypesCallback); + break; + } + } + + function getAllItemTypesCallback(all) { + allItemTypes = all; + vm.loading = false; + // the model value is a comma separated list of content type aliases + var currentItemTypes = _.map(($scope.model.value || "").split(","), function (s) { return s.trim(); }); + vm.itemTypes = _.filter(allItemTypes, function (itemType) { + return currentItemTypes.indexOf(itemType.alias) >= 0; + }); + } + + function add() { + if (!currentItemType) { + return; + } + + var editor = { + multiPicker: true, + submit: function (model) { + var newItemTypes = _.map(model.selection, + function(selected) { + return _.findWhere(allItemTypes, { udi: selected.udi }); + }); + vm.itemTypes = _.uniq(_.union(vm.itemTypes, newItemTypes)); + updateModel(); + editorService.close(); + }, + close: function() { + editorService.close(); + } + }; + + switch (currentItemType) { + case "content": + editorService.contentTypePicker(editor); + break; + case "media": + editorService.mediaTypePicker(editor); + break; + case "member": + editorService.memberTypePicker(editor); + break; + } + } + + function remove(itemType) { + vm.itemTypes = _.without(vm.itemTypes, itemType); + updateModel(); + } + + function updateModel() { + // the model value is a comma separated list of content type aliases + $scope.model.value = _.pluck(vm.itemTypes, "alias").join(); + angularHelper.getCurrentForm($scope).$setDirty(); + } + + eventsService.on("treeSourceChanged", function (e, args) { + currentItemType = args.value; + // reset the model value if we changed node type (but not on the initial load) + if (!initialLoad) { + vm.itemTypes = []; + updateModel(); + } + initialLoad = false; + init(); + }); +} + +angular.module('umbraco').controller("Umbraco.PrevalueEditors.TreeSourceTypePickerController", TreeSourceTypePickerController); diff --git a/src/Umbraco.Web.UI.Client/src/views/prevalueeditors/contenttypepicker.html b/src/Umbraco.Web.UI.Client/src/views/prevalueeditors/treesourcetypepicker.html similarity index 56% rename from src/Umbraco.Web.UI.Client/src/views/prevalueeditors/contenttypepicker.html rename to src/Umbraco.Web.UI.Client/src/views/prevalueeditors/treesourcetypepicker.html index cd89fe4cb5..f679b53093 100644 --- a/src/Umbraco.Web.UI.Client/src/views/prevalueeditors/contenttypepicker.html +++ b/src/Umbraco.Web.UI.Client/src/views/prevalueeditors/treesourcetypepicker.html @@ -1,15 +1,15 @@ -
+
- + on-remove="vm.remove(itemType)">
  • - +
diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/contentpicker/contentpicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/contentpicker/contentpicker.controller.js index be99ffe8f9..0c8d77eabb 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/contentpicker/contentpicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/contentpicker/contentpicker.controller.js @@ -16,7 +16,8 @@ function contentPickerController($scope, entityResource, editorState, iconHelper var vm = { labels: { - general_recycleBin: "" + general_recycleBin: "", + general_add: "" } }; @@ -78,6 +79,8 @@ function contentPickerController($scope, entityResource, editorState, iconHelper $scope.renderModel = []; $scope.sortableModel = []; + $scope.labels = vm.labels; + $scope.dialogEditor = editorState && editorState.current && editorState.current.isDialogEditor === true; //the default pre-values @@ -479,9 +482,10 @@ function contentPickerController($scope, entityResource, editorState, iconHelper } function init() { - localizationService.localizeMany(["general_recycleBin"]) + localizationService.localizeMany(["general_recycleBin", "general_add"]) .then(function(data) { vm.labels.general_recycleBin = data[0]; + vm.labels.general_add = data[1]; syncRenderModel(false).then(function () { //everything is loaded, start the watch on the model diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/contentpicker/contentpicker.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/contentpicker/contentpicker.html index a589cf8947..ab9b078433 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/contentpicker/contentpicker.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/contentpicker/contentpicker.html @@ -19,13 +19,17 @@
- +
diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/decimal/decimal.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/decimal/decimal.html index 28cb909cc0..d5560c8433 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/decimal/decimal.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/decimal/decimal.html @@ -5,6 +5,8 @@ pattern="[\-0-9]+([,\.][0-9]+)?" class="umb-property-editor umb-number" ng-model="model.value" + ng-required="model.validation.mandatory" + aria-required="{{model.validation.mandatory}}" val-server="value" fix-number min="{{model.config.min}}" max="{{model.config.max}}" step="{{model.config.step}}" /> diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/integer/integer.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/integer/integer.html index 549a07319d..5c10790400 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/integer/integer.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/integer/integer.html @@ -5,6 +5,8 @@ pattern="[\-0-9]*" class="umb-property-editor umb-number" ng-model="model.value" + ng-required="model.validation.mandatory" + aria-required="{{model.validation.mandatory}}" id="{{model.alias}}" val-server="value" fix-number min="{{model.config.min}}" max="{{model.config.max}}" step="{{model.config.step}}" /> diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/multiurlpicker/multiurlpicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/multiurlpicker/multiurlpicker.controller.js index b2bb47b58c..8381a53644 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/multiurlpicker/multiurlpicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/multiurlpicker/multiurlpicker.controller.js @@ -134,6 +134,12 @@ function multiUrlPickerController($scope, angularHelper, localizationService, en .then(function (data) { vm.labels.general_recycleBin = data[0]; }); + + // if the property is mandatory, set the minCount config to 1 (unless of course it is set to something already), + // that way the minCount/maxCount validation handles the mandatory as well + if ($scope.model.validation && $scope.model.validation.mandatory && !$scope.model.config.minNumber) { + $scope.model.config.minNumber = 1; + } } init(); diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/multiurlpicker/multiurlpicker.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/multiurlpicker/multiurlpicker.html index 0d1c736438..ff1969aab8 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/multiurlpicker/multiurlpicker.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/multiurlpicker/multiurlpicker.html @@ -63,12 +63,12 @@ -
+
You need to add at least {{model.config.minNumber}} items
-
+
You can only have {{model.config.maxNumber}} items selected
diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.controller.js index fbaab3f94f..ffc9f13286 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.controller.js @@ -271,13 +271,15 @@ angular.module("umbraco").controller("Umbraco.PropertyEditors.NestedContent.Prop }; $scope.deleteNode = function (idx) { - if ($scope.nodes.length > $scope.model.config.minItems) { - $scope.nodes.splice(idx, 1); - $scope.setDirty(); - updateModel(); - } + $scope.nodes.splice(idx, 1); + $scope.setDirty(); + updateModel(); }; $scope.requestDeleteNode = function (idx) { + if ($scope.nodes.length <= $scope.model.config.minItems) { + return; + } + if ($scope.model.config.confirmDeletes === true) { localizationService.localizeMany(["content_nestedContentDeleteItem", "general_delete", "general_cancel", "contentTypeEditor_yesDelete"]).then(function (data) { const overlay = { diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/radiobuttons/radiobuttons.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/radiobuttons/radiobuttons.html index 8c5f2a22d3..22c4679d32 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/radiobuttons/radiobuttons.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/radiobuttons/radiobuttons.html @@ -1,7 +1,7 @@ 
  • - +
diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/textbox/textbox.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/textbox/textbox.html index ff62629b1c..f8f9b18c7f 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/textbox/textbox.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/textbox/textbox.html @@ -4,7 +4,7 @@ class="umb-property-editor umb-textstring textstring" val-server="value" ng-required="model.validation.mandatory" - aria-required="model.validation.mandatory" + aria-required="{{model.validation.mandatory}}" aria-invalid="False" ng-trim="false" ng-keyup="model.change()" /> diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/da.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/da.xml index c4bffedb28..35b26f4f18 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/da.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/da.xml @@ -432,6 +432,7 @@ Link til medie Vælg indhold startnode Vælg medie + Vælg medietype Vælg ikon Vælg item Vælg link @@ -441,6 +442,7 @@ Vælg medie startnode Vælg medlem Vælg medlemsgruppe + Vælg medlemstype Vælg node Vælg sektioner Vælg brugere @@ -565,6 +567,7 @@ Der er ingen aktive styles eller formatteringer på denne side Du skal stå til venstre for de 2 celler du ønsker at samle! Du kan ikke opdele en celle, som ikke allerede er delt. + Denne egenskab er ugyldig Om diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml index 8c8a3d9434..4a49a94823 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml @@ -450,6 +450,7 @@ Link to media Select content start node Select media + Select media type Select icon Select item Select link @@ -459,6 +460,7 @@ Select media start node Select member Select member group + Select member type Select node Select sections Select users @@ -593,6 +595,7 @@ No active styles available Please place cursor at the left of the two cells you wish to merge You cannot split a cell that hasn't been merged. + This property is invalid About 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 091d97f4cc..faf336d94d 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml @@ -453,6 +453,7 @@ Link to media Select content start node Select media + Select media type Select icon Select item Select link @@ -462,6 +463,7 @@ Select media start node Select member Select member group + Select member type Select node Select sections Select users @@ -596,6 +598,7 @@ No active styles available Please place cursor at the left of the two cells you wish to merge You cannot split a cell that hasn't been merged. + This property is invalid Options diff --git a/src/Umbraco.Web/PropertyEditors/MultiNodePickerConfiguration.cs b/src/Umbraco.Web/PropertyEditors/MultiNodePickerConfiguration.cs index b099573b9f..5789c34cb2 100644 --- a/src/Umbraco.Web/PropertyEditors/MultiNodePickerConfiguration.cs +++ b/src/Umbraco.Web/PropertyEditors/MultiNodePickerConfiguration.cs @@ -11,7 +11,7 @@ namespace Umbraco.Web.PropertyEditors [ConfigurationField("startNode", "Node type", "treesource")] public MultiNodePickerConfigurationTreeSource TreeSource { get; set; } - [ConfigurationField("filter", "Allow items of type", "contenttypepicker", Description = "Select the applicable content types")] + [ConfigurationField("filter", "Allow items of type", "treesourcetypepicker", Description = "Select the applicable types")] public string Filter { get; set; } [ConfigurationField("minNumber", "Minimum number of items", "number")] diff --git a/src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs b/src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs index b4d409f66f..884ccb0f74 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs @@ -612,43 +612,48 @@ namespace Umbraco.Web.PublishedCache.NuCache // NextSiblingContentId // PreviousSiblingContentId - ContentNode prev = null; - ContentNode currParent = null; + ContentNode previousNode = null; + ContentNode parent = null; foreach (var kit in kits) { - if (!BuildKit(kit, out var parentLink)) + if (!BuildKit(kit, out var parentLink)) { ok = false; continue; // skip that one } - if (currParent != null && currParent.Id != parentLink.Value.Id) + var thisNode = kit.Node; + + if (parent == null) { - //the parent is changing so that means the prev tracked one is the last child - currParent.LastChildContentId = prev.Id; - //changed parent, reset prev - prev = null; + // first parent + parent = parentLink.Value; + parent.FirstChildContentId = thisNode.Id; // this node is the first node } - - currParent = parentLink.Value; - - _logger.Debug($"Set {kit.Node.Id} with parent {kit.Node.ParentContentId}"); - SetValueLocked(_contentNodes, kit.Node.Id, kit.Node); - - //if the parent's FirstChildContentId isn't set, then it must be the current one - if (currParent.FirstChildContentId < 0) - currParent.FirstChildContentId = kit.Node.Id; - - //if there is a previous one on the same level then set it's next sibling id to the current oen - if (prev != null) + else if (parent.Id != parentLink.Value.Id) { - prev.NextSiblingContentId = kit.Node.Id; - kit.Node.PreviousSiblingContentId = prev.Id; - } + // new parent + parent = parentLink.Value; + parent.FirstChildContentId = thisNode.Id; // this node is the first node + previousNode = null; // there is no previous sibling + } - //store the prev - prev = kit.Node; + _logger.Debug($"Set {thisNode.Id} with parent {thisNode.ParentContentId}"); + SetValueLocked(_contentNodes, thisNode.Id, thisNode); + + // this node is always the last child + parent.LastChildContentId = thisNode.Id; + + // wire previous node as previous sibling + if (previousNode != null) + { + previousNode.NextSiblingContentId = thisNode.Id; + thisNode.PreviousSiblingContentId = previousNode.Id; + } + + // this node becomes the previous node + previousNode = thisNode; _xmap[kit.Node.Uid] = kit.Node.Id; } @@ -843,19 +848,16 @@ namespace Umbraco.Web.PublishedCache.NuCache if (parent.FirstChildContentId < 0) throw new PanicException("no children"); - if (parent.FirstChildContentId == content.Id) - { - // if first, clone parent + remove first child + // if first/last, clone parent, then remove + + if (parent.FirstChildContentId == content.Id || parent.LastChildContentId == content.Id) parent = GenCloneLocked(parentLink); + + if (parent.FirstChildContentId == content.Id) parent.FirstChildContentId = content.NextSiblingContentId; - } if (parent.LastChildContentId == content.Id) - { - // if last, clone parent + remove last child - parent = GenCloneLocked(parentLink); parent.LastChildContentId = content.PreviousSiblingContentId; - } // maintain linked list