From f860b4b0befd189266e0df7b30cdd6434093f812 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Wed, 26 Aug 2020 16:30:15 +0200 Subject: [PATCH 01/31] ability to registrer paste resolvers --- .../src/common/services/clipboard.service.js | 52 ++++++++++++++++--- 1 file changed, 45 insertions(+), 7 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/services/clipboard.service.js b/src/Umbraco.Web.UI.Client/src/common/services/clipboard.service.js index 0d2ca6623b..2ad4bec1ea 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/clipboard.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/clipboard.service.js @@ -14,6 +14,7 @@ function clipboardService(notificationsService, eventsService, localStorageServi var clearPropertyResolvers = []; + var pastePropertyResolvers = []; var STORAGE_KEY = "umbClipboardService"; @@ -95,8 +96,46 @@ function clipboardService(notificationsService, eventsService, localStorageServi } + + + function resolvePropertyForPaste(prop) { + for (var i=0; i Date: Wed, 26 Aug 2020 16:30:35 +0200 Subject: [PATCH 02/31] replace UDIs of Block Editors with new ones on paste --- .../common/services/blockeditor.service.js | 96 +++++++++++++++++-- 1 file changed, 88 insertions(+), 8 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/services/blockeditor.service.js b/src/Umbraco.Web.UI.Client/src/common/services/blockeditor.service.js index ffb1971169..0a4e3b1af4 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/blockeditor.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/blockeditor.service.js @@ -4,39 +4,119 @@ * * @description * Added in Umbraco 8.7. Service for dealing with Block Editors. - * + * * Block Editor Service provides the basic features for a block editor. * The main feature is the ability to create a Model Object which takes care of your data for your Block Editor. - * - * + * + * * ##Samples * * ####Instantiate a Model Object for your property editor: - * + * *
  *     modelObject = blockEditorService.createModelObject(vm.model.value, vm.model.editor, vm.model.config.blocks, $scope);
  *     modelObject.load().then(onLoaded);
  * 
* - * + * * See {@link umbraco.services.blockEditorModelObject BlockEditorModelObject} for more samples. - * + * */ (function () { 'use strict'; + + /** + * When performing a runtime copy of Block Editors entries, we copy the ElementType Data Model and inner IDs are kept identical, to ensure new IDs are changed on paste we need to provide a resolver for the ClipboardService. + */ + angular.module('umbraco').run(['clipboardService', 'udiService', function (clipboardService, udiService) { + + function replaceUdi(obj, key, dataObject) { + var udi = obj[key]; + var newUdi = udiService.create("element"); + obj[key] = newUdi; + dataObject.forEach((data) => { + if (data.udi === udi) { + data.udi = newUdi; + } + }); + } + function replaceUdisOfObject(obj, propValue) { + for (var k in obj) { + if(k === "contentUdi") { + replaceUdi(obj, k, propValue.contentData); + } else if(k === "settingsUdi") { + replaceUdi(obj, k, propValue.settingsData); + } else { + var propType = typeof obj[k]; + if(propType === "object" || propType === "array") { + replaceUdisOfObject(obj[k], propValue) + } + } + } + } + + function replaceBlockListUDIsResolver(obj, propClearingMethod) { + + if (typeof obj === "object") { + + // 'obj' can both be a property object or the raw value of a inner property. + var value = obj; + + // if we got a property object from a ContentTypeModel we need to look at the value. We check for value and editor to, sort of, ensure this is the case. + if(obj.value !== undefined && obj.editor !== undefined) { + value = obj.value; + // If value isnt a object, lets break out. + if(typeof obj.value !== "object") { + return; + } + } + + // we got an object, and it has these three props then we are most likely dealing with a Block Editor. + if ((value.layout !== undefined && value.contentData !== undefined && value.settingsData !== undefined)) { + + replaceUdisOfObject(value.layout, value); + + // replace UDIs for inner properties of this Block Editors content data. + if(value.contentData.length > 0) { + value.contentData.forEach((item) => { + for (var k in item) { + propClearingMethod(item[k]); + } + }); + } + // replace UDIs for inner properties of this Block Editors settings data. + if(value.settingsData.length > 0) { + value.settingsData.forEach((item) => { + for (var k in item) { + propClearingMethod(item[k]); + } + }); + } + + } + } + } + + clipboardService.registerPastePropertyResolver(replaceBlockListUDIsResolver) + + }]); + + + + function blockEditorService(blockEditorModelObject) { /** * @ngdocs function * @name createModelObject * @methodOf umbraco.services.blockEditorService - * + * * @description * Create a new Block Editor Model Object. * See {@link umbraco.services.blockEditorModelObject blockEditorModelObject} - * + * * @see umbraco.services.blockEditorModelObject * @param {object} propertyModelValue data object of the property editor, usually model.value. * @param {string} propertyEditorAlias alias of the property. From fa07cf0ac9b810b68eed1fa76cd77e90d19abc8b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Wed, 26 Aug 2020 16:30:48 +0200 Subject: [PATCH 03/31] use ClipboardService on Paste --- .../src/common/services/blockeditormodelobject.service.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/services/blockeditormodelobject.service.js b/src/Umbraco.Web.UI.Client/src/common/services/blockeditormodelobject.service.js index 86b5bdd0d0..6dc0fdd06f 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/blockeditormodelobject.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/blockeditormodelobject.service.js @@ -13,8 +13,7 @@ (function () { 'use strict'; - - function blockEditorModelObjectFactory($interpolate, $q, udiService, contentResource, localizationService, umbRequestHelper) { + function blockEditorModelObjectFactory($interpolate, $q, udiService, contentResource, localizationService, umbRequestHelper, clipboardService) { /** * Simple mapping from property model content entry to editing model, @@ -748,7 +747,9 @@ */ createFromElementType: function (elementTypeDataModel) { - elementTypeDataModel = Utilities.copy(elementTypeDataModel); + + elementTypeDataModel = clipboardService.parseContentForPaste(elementTypeDataModel); + var contentElementTypeKey = elementTypeDataModel.contentTypeKey; From 096f2b73d20844f821fe2c5f3e2cbe9f35b6cbd3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Wed, 26 Aug 2020 16:33:03 +0200 Subject: [PATCH 04/31] remove space --- .../src/common/services/blockeditormodelobject.service.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/services/blockeditormodelobject.service.js b/src/Umbraco.Web.UI.Client/src/common/services/blockeditormodelobject.service.js index 6dc0fdd06f..df4c1b581e 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/blockeditormodelobject.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/blockeditormodelobject.service.js @@ -747,10 +747,8 @@ */ createFromElementType: function (elementTypeDataModel) { - elementTypeDataModel = clipboardService.parseContentForPaste(elementTypeDataModel); - var contentElementTypeKey = elementTypeDataModel.contentTypeKey; var layoutEntry = this.create(contentElementTypeKey); From 24ec954a7dca471f6e6fc130b653f528034dc920 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Wed, 26 Aug 2020 16:33:42 +0200 Subject: [PATCH 05/31] update Nested Content to run paste resolvers and update deprecated method name --- .../nestedcontent/nestedcontent.controller.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) 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 ee406caa8a..75d75298db 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 @@ -28,7 +28,7 @@ } } - clipboardService.registrerClearPropertyResolver(clearNestedContentPropertiesForStorage) + clipboardService.registerClearPropertyResolver(clearNestedContentPropertiesForStorage) function clearInnerNestedContentPropertiesForStorage(prop, propClearingMethod) { @@ -50,7 +50,7 @@ } } - clipboardService.registrerClearPropertyResolver(clearInnerNestedContentPropertiesForStorage) + clipboardService.registerClearPropertyResolver(clearInnerNestedContentPropertiesForStorage) }]); angular @@ -474,6 +474,8 @@ return; } + newNode = clipboardService.parseContentForPaste(newNode); + // generate a new key. newNode.key = String.CreateGuid(); From 459403eb9a9c0fca6c30e1eb22603a009db2ddd5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Thu, 27 Aug 2020 22:23:51 +0200 Subject: [PATCH 06/31] moved validation indication one element up. --- .../umb-block-list-property-editor.less | 77 ++++++++++--------- .../blocklist/umb-block-list-row.html | 4 +- 2 files changed, 41 insertions(+), 40 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/umb-block-list-property-editor.less b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/umb-block-list-property-editor.less index 0c7d58d245..4379167c00 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/umb-block-list-property-editor.less +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/umb-block-list-property-editor.less @@ -38,6 +38,45 @@ opacity: 1; } } + + &.--show-validation { + ng-form.ng-invalid-val-server-match-content > & { + border: 2px solid @formErrorText; + border-radius: @baseBorderRadius; + &::after { + content: "!"; + position: absolute; + top: -12px; + right: -12px; + display: inline-flex; + align-items: center; + justify-content: center; + width: 18px; + height: 18px; + border-radius: 50%; + font-size: 13px; + text-align: center; + font-weight: bold; + background-color: @errorBackground; + color: @errorText; + border: 2px solid @white; + font-weight: 900; + + animation-duration: 1.4s; + animation-iteration-count: infinite; + animation-name: umb-block-list__block--content--badge-bounce; + animation-timing-function: ease; + @keyframes umb-block-list__block--content--badge-bounce { + 0% { transform: translateY(0); } + 20% { transform: translateY(-6px); } + 40% { transform: translateY(0); } + 55% { transform: translateY(-3px); } + 70% { transform: translateY(0); } + 100% { transform: translateY(0); } + } + } + } + } } ng-form.ng-invalid-val-server-match-settings > .umb-block-list__block > .umb-block-list__block--actions { opacity: 1; @@ -109,44 +148,6 @@ ng-form.ng-invalid-val-server-match-settings > .umb-block-list__block > .umb-blo box-sizing: border-box; } - &.--show-validation { - ng-form.ng-invalid-val-server-match-content > .umb-block-list__block > & > div { - border: 2px solid @formErrorText; - border-radius: @baseBorderRadius; - &::after { - content: "!"; - position: absolute; - top: -12px; - right: -12px; - display: inline-flex; - align-items: center; - justify-content: center; - width: 18px; - height: 18px; - border-radius: 50%; - font-size: 13px; - text-align: center; - font-weight: bold; - background-color: @errorBackground; - color: @errorText; - border: 2px solid @white; - font-weight: 900; - - animation-duration: 1.4s; - animation-iteration-count: infinite; - animation-name: umb-block-list__block--content--badge-bounce; - animation-timing-function: ease; - @keyframes umb-block-list__block--content--badge-bounce { - 0% { transform: translateY(0); } - 20% { transform: translateY(-6px); } - 40% { transform: translateY(0); } - 55% { transform: translateY(-3px); } - 70% { transform: translateY(0); } - 100% { transform: translateY(0); } - } - } - } - } } .blockelement__draggable-element { diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/umb-block-list-row.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/umb-block-list-row.html index c2657985cf..c692c81eaf 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/umb-block-list-row.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/umb-block-list-row.html @@ -1,9 +1,9 @@ -
+
Date: Tue, 1 Sep 2020 11:33:57 +0200 Subject: [PATCH 07/31] no need to hideDialog as thats already begin done by the other methods --- .../src/views/content/content.create.controller.js | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/content/content.create.controller.js b/src/Umbraco.Web.UI.Client/src/views/content/content.create.controller.js index 487a73f948..089fc1a6ce 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/content.create.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/content/content.create.controller.js @@ -2,7 +2,7 @@ * @ngdoc controller * @name Umbraco.Editors.Content.CreateController * @function - * + * * @description * The controller for the content creation dialog */ @@ -22,7 +22,7 @@ function contentCreateController($scope, function initialize() { $scope.loading = true; $scope.allowedTypes = null; - + var getAllowedTypes = contentTypeResource.getAllowedTypes($scope.currentNode.id).then(function (data) { $scope.allowedTypes = iconHelper.formatContentTypeIcons(data); if ($scope.allowedTypes.length === 0) { @@ -60,13 +60,13 @@ function contentCreateController($scope, .path("/content/content/edit/" + $scope.currentNode.id) .search("doctype", docType.alias) .search("create", "true") - /* when we create a new node we want to make sure it uses the same + /* when we create a new node we want to make sure it uses the same language as what is selected in the tree */ .search("cculture", mainCulture) - /* when we create a new node we must make sure that any previously + /* when we create a new node we must make sure that any previously opened segments is reset */ .search("csegment", null) - /* when we create a new node we must make sure that any previously + /* when we create a new node we must make sure that any previously used blueprint is reset */ .search("blueprintId", null); close(); @@ -92,8 +92,6 @@ function contentCreateController($scope, } else { createBlank(docType); } - - navigationService.hideDialog(); } function createFromBlueprint(blueprintId) { @@ -127,7 +125,7 @@ function contentCreateController($scope, $scope.createOrSelectBlueprintIfAny = createOrSelectBlueprintIfAny; $scope.createFromBlueprint = createFromBlueprint; - // the current node changes behind the scenes when the context menu is clicked without closing + // the current node changes behind the scenes when the context menu is clicked without closing // the default menu first, so we must watch the current node and re-initialize accordingly var unbindModelWatcher = $scope.$watch("currentNode", initialize); $scope.$on('$destroy', function () { From 31e69ad1e2b13a78b05219af5b3d173df7c1c6c0 Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Wed, 2 Sep 2020 09:13:43 +0200 Subject: [PATCH 08/31] Guard compareArrays filter against invalid arguments (cherry picked from commit a542bd4bd2614a138ac2d6c525a209da1f507dbc) --- .../src/common/filters/compareArrays.filter.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Umbraco.Web.UI.Client/src/common/filters/compareArrays.filter.js b/src/Umbraco.Web.UI.Client/src/common/filters/compareArrays.filter.js index 13f603260d..d99c21a38c 100644 --- a/src/Umbraco.Web.UI.Client/src/common/filters/compareArrays.filter.js +++ b/src/Umbraco.Web.UI.Client/src/common/filters/compareArrays.filter.js @@ -2,6 +2,10 @@ angular.module("umbraco.filters") .filter('compareArrays', function() { return function inArray(array, compareArray, compareProperty) { + if (!compareArray || !compareArray.length) { + return [...array]; + } + var result = []; angular.forEach(array, function(arrayItem){ From 0aa1f9ea7bd1a7303cf31b46a62a5d6bfc9dc9b9 Mon Sep 17 00:00:00 2001 From: Bjarne Fyrstenborg Date: Tue, 1 Sep 2020 23:06:22 +0200 Subject: [PATCH 09/31] Create more unique id for radiobutton list when using splitview and where two properties on each culture has same alias (cherry picked from commit 124415e413a7f716b220a126513d1b967da73cff) --- .../propertyeditors/radiobuttons/radiobuttons.controller.js | 2 ++ .../src/views/propertyeditors/radiobuttons/radiobuttons.html | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/radiobuttons/radiobuttons.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/radiobuttons/radiobuttons.controller.js index 6bfde10e9c..b30c04fdb9 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/radiobuttons/radiobuttons.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/radiobuttons/radiobuttons.controller.js @@ -7,6 +7,8 @@ angular.module("umbraco").controller("Umbraco.PropertyEditors.RadioButtonsContro function init() { + vm.uniqueId = String.CreateGuid(); + //we can't really do anything if the config isn't an object if (Utilities.isObject($scope.model.config.items)) { 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 82952193e7..c9d05a303c 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 @@ -3,7 +3,7 @@
  • -
-
+

{{mandatoryMessage}}

From 823a81aacc074f38763d49992059aec328e371ae Mon Sep 17 00:00:00 2001 From: Nathan Woulfe Date: Thu, 3 Sep 2020 10:27:26 +1000 Subject: [PATCH 10/31] Merge pull request #8713 from umbraco/nul800sebastiaan-patch-1 Adding `display: none;` prevents `ng-show` from working (cherry picked from commit 7b1f7ced3ab41979eabd785431909823ed82434f) --- .../src/views/components/content/umb-content-node-info.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/components/content/umb-content-node-info.html b/src/Umbraco.Web.UI.Client/src/views/components/content/umb-content-node-info.html index 9299a3b991..0fcec4c559 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/content/umb-content-node-info.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/content/umb-content-node-info.html @@ -20,7 +20,7 @@ - +
From 055d1179a140afbeff159dbf9844509cef9c6bd6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Wed, 26 Aug 2020 12:18:06 +0200 Subject: [PATCH 11/31] To prevent issues of combining Nested Content with Block Editors, plus nudging Devs to use Block List instead, we now prevent the usage of NC in BL. --- .../src/common/services/blockeditormodelobject.service.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/services/blockeditormodelobject.service.js b/src/Umbraco.Web.UI.Client/src/common/services/blockeditormodelobject.service.js index 920ba1c58d..cfb3184d09 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/blockeditormodelobject.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/blockeditormodelobject.service.js @@ -231,7 +231,8 @@ var notSupportedProperties = [ "Umbraco.Tags", "Umbraco.UploadField", - "Umbraco.ImageCropper" + "Umbraco.ImageCropper", + "Umbraco.NestedContent" ]; From 1a35017d8f4ef4fed3e1df7f11cf056086f905b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Thu, 3 Sep 2020 11:09:17 +0200 Subject: [PATCH 12/31] AC7976 dont place runtime vars in model (#8807) --- .../textarea/textarea.controller.js | 34 ++++++++----------- .../propertyeditors/textarea/textarea.html | 22 +++++++++--- .../textbox/textbox.controller.js | 32 +++++++++-------- .../propertyeditors/textbox/textbox.html | 21 +++++++----- .../PropertyEditors/TextboxConfiguration.cs | 2 +- 5 files changed, 63 insertions(+), 48 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/textarea/textarea.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/textarea/textarea.controller.js index 4a7fff99f8..4f3cb6770b 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/textarea/textarea.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/textarea/textarea.controller.js @@ -1,42 +1,38 @@ function textAreaController($scope, validationMessageService) { // macro parameter editor doesn't contains a config object, - // so we create a new one to hold any properties + // so we create a new one to hold any properties if (!$scope.model.config) { $scope.model.config = {}; } - $scope.model.count = 0; - if (!$scope.model.config.maxChars) { - $scope.model.config.maxChars = false; - } - - $scope.model.maxlength = false; - if ($scope.model.config && $scope.model.config.maxChars) { - $scope.model.maxlength = true; - } + $scope.maxChars = $scope.model.config.maxChars || 0; + $scope.maxCharsLimit = ($scope.model.config && $scope.model.config.maxChars > 0); + $scope.charsCount = 0; + $scope.nearMaxLimit = false; + $scope.validLength = true; $scope.$on("formSubmitting", function() { - if ($scope.isLengthValid()) { + if ($scope.validLength) { $scope.textareaFieldForm.textarea.$setValidity("maxChars", true); } else { $scope.textareaFieldForm.textarea.$setValidity("maxChars", false); } }); - $scope.isLengthValid = function() { - if (!$scope.model.maxlength) { - return true; - } - return $scope.model.config.maxChars >= $scope.model.count; + function checkLengthVadility() { + $scope.validLength = !($scope.maxCharsLimit === true && $scope.charsCount > $scope.maxChars); } - $scope.model.change = function () { + $scope.change = function () { if ($scope.model.value) { - $scope.model.count = $scope.model.value.length; + $scope.charsCount = $scope.model.value.length; + checkLengthVadility(); + $scope.nearMaxLimit = $scope.maxCharsLimit === true && $scope.validLength === true && $scope.charsCount > Math.max($scope.maxChars*.8, $scope.maxChars-50); } } - $scope.model.change(); + $scope.model.onValueChanged = $scope.change; + $scope.change(); // Set the message to use for when a mandatory field isn't completed. // Will either use the one provided on the property type or a localised default. diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/textarea/textarea.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/textarea/textarea.html index 87f6ffeac9..2f183c29f0 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/textarea/textarea.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/textarea/textarea.html @@ -1,17 +1,29 @@
- + {{mandatoryMessage}} {{textareaFieldForm.textarea.errorMsg}} -
- %0% characters left. +
+

{{model.label}} %0% characters left.

+
-
- Maximum %0% characters, %1% too many. +
+

{{model.label}} Maximum %0% characters, %1% too many.

+
diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/textbox/textbox.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/textbox/textbox.controller.js index b47c3584b3..b7c740e749 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/textbox/textbox.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/textbox/textbox.controller.js @@ -4,37 +4,41 @@ function textboxController($scope, validationMessageService) { if (!$scope.model.config) { $scope.model.config = {}; } - $scope.model.count = 0; - if (!$scope.model.config.maxChars) { - // 500 is the maximum number that can be stored - // in the database, so set it to the max, even - // if no max is specified in the config - $scope.model.config.maxChars = 500; - } + + // 512 is the maximum number that can be stored + // in the database, so set it to the max, even + // if no max is specified in the config + $scope.maxChars = Math.min($scope.model.config.maxChars || 512, 512); + $scope.charsCount = 0; + $scope.nearMaxLimit = false; + $scope.validLength = true; $scope.$on("formSubmitting", function() { - if ($scope.isLengthValid()) { + if ($scope.validLength === true) { $scope.textboxFieldForm.textbox.$setValidity("maxChars", true); } else { $scope.textboxFieldForm.textbox.$setValidity("maxChars", false); } }); - $scope.isLengthValid = function() { - return $scope.model.config.maxChars >= $scope.model.count; + function checkLengthVadility() { + $scope.validLength = $scope.charsCount <= $scope.maxChars; } - $scope.model.change = function () { + $scope.change = function () { if ($scope.model.value) { - $scope.model.count = $scope.model.value.length; + $scope.charsCount = $scope.model.value.length; + checkLengthVadility(); + $scope.nearMaxLimit = $scope.validLength && $scope.charsCount > Math.max($scope.maxChars*.8, $scope.maxChars-25); } } - $scope.model.change(); + $scope.model.onValueChanged = $scope.change; + $scope.change(); // Set the message to use for when a mandatory field isn't completed. // Will either use the one provided on the property type or a localised default. validationMessageService.getMandatoryMessage($scope.model.validation).then(function(value) { $scope.mandatoryMessage = value; - }); + }); } angular.module('umbraco').controller("Umbraco.PropertyEditors.textboxController", textboxController); 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 5d86259e93..5e135ea7d9 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 @@ -1,27 +1,30 @@
- - + ng-keyup="change()" /> +

{{model.label}} {{textboxFieldForm.textbox.errorMsg}}

{{mandatoryMessage}}

-
-

{{model.label}} %0% characters left.

-

%0% characters left.

+
+

{{model.label}} %0% characters left.

+
-
-

{{model.label}} Maximum %0% characters, %1% too many.

- +
+

{{model.label}} Maximum %0% characters, %1% too many.

+
diff --git a/src/Umbraco.Web/PropertyEditors/TextboxConfiguration.cs b/src/Umbraco.Web/PropertyEditors/TextboxConfiguration.cs index c9f15e2e2f..641cc8e42a 100644 --- a/src/Umbraco.Web/PropertyEditors/TextboxConfiguration.cs +++ b/src/Umbraco.Web/PropertyEditors/TextboxConfiguration.cs @@ -7,7 +7,7 @@ namespace Umbraco.Web.PropertyEditors /// public class TextboxConfiguration { - [ConfigurationField("maxChars", "Maximum allowed characters", "textstringlimited", Description = "If empty, 500 character limit")] + [ConfigurationField("maxChars", "Maximum allowed characters", "textstringlimited", Description = "If empty, 512 character limit")] public int? MaxChars { get; set; } } } From 46488836f4caa12c7fce1286e5b2092418b21d38 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Thu, 3 Sep 2020 14:37:51 +0200 Subject: [PATCH 13/31] Changed clipboardService property resolvers to be specific for clipboard-entry-types --- .../common/services/blockeditor.service.js | 26 ++-- .../blockeditormodelobject.service.js | 2 +- .../src/common/services/clipboard.service.js | 139 +++++++++++++----- .../umb-block-list-property-editor.less | 24 +-- .../umbBlockListPropertyEditor.component.js | 54 +++---- .../nestedcontent/nestedcontent.controller.js | 39 ++--- 6 files changed, 158 insertions(+), 126 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/services/blockeditor.service.js b/src/Umbraco.Web.UI.Client/src/common/services/blockeditor.service.js index 0a4e3b1af4..dfa0eae297 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/blockeditor.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/blockeditor.service.js @@ -49,6 +49,7 @@ } else if(k === "settingsUdi") { replaceUdi(obj, k, propValue.settingsData); } else { + // lets crawl through all properties of layout to make sure get captured all `contentUdi` and `settingsUdi` properties. var propType = typeof obj[k]; if(propType === "object" || propType === "array") { replaceUdisOfObject(obj[k], propValue) @@ -56,22 +57,15 @@ } } } + function replaceElementTypeBlockListUDIsResolver(obj, propClearingMethod) { + replaceRawBlockListUDIsResolver(obj.value, propClearingMethod); + } - function replaceBlockListUDIsResolver(obj, propClearingMethod) { + clipboardService.registerPastePropertyResolver(replaceElementTypeBlockListUDIsResolver, clipboardService.TYPES.ELEMENT_TYPE); - if (typeof obj === "object") { - // 'obj' can both be a property object or the raw value of a inner property. - var value = obj; - - // if we got a property object from a ContentTypeModel we need to look at the value. We check for value and editor to, sort of, ensure this is the case. - if(obj.value !== undefined && obj.editor !== undefined) { - value = obj.value; - // If value isnt a object, lets break out. - if(typeof obj.value !== "object") { - return; - } - } + function replaceRawBlockListUDIsResolver(value, propClearingMethod) { + if (typeof value === "object") { // we got an object, and it has these three props then we are most likely dealing with a Block Editor. if ((value.layout !== undefined && value.contentData !== undefined && value.settingsData !== undefined)) { @@ -82,7 +76,7 @@ if(value.contentData.length > 0) { value.contentData.forEach((item) => { for (var k in item) { - propClearingMethod(item[k]); + propClearingMethod(item[k], clipboardService.TYPES.RAW); } }); } @@ -90,7 +84,7 @@ if(value.settingsData.length > 0) { value.settingsData.forEach((item) => { for (var k in item) { - propClearingMethod(item[k]); + propClearingMethod(item[k], clipboardService.TYPES.RAW); } }); } @@ -99,7 +93,7 @@ } } - clipboardService.registerPastePropertyResolver(replaceBlockListUDIsResolver) + clipboardService.registerPastePropertyResolver(replaceRawBlockListUDIsResolver, clipboardService.TYPES.RAW); }]); diff --git a/src/Umbraco.Web.UI.Client/src/common/services/blockeditormodelobject.service.js b/src/Umbraco.Web.UI.Client/src/common/services/blockeditormodelobject.service.js index 988e8f9fa9..a03c64ff9d 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/blockeditormodelobject.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/blockeditormodelobject.service.js @@ -745,7 +745,7 @@ */ createFromElementType: function (elementTypeDataModel) { - elementTypeDataModel = clipboardService.parseContentForPaste(elementTypeDataModel); + elementTypeDataModel = clipboardService.parseContentForPaste(clipboardService.TYPES.ELEMENT_TYPE, elementTypeDataModel); var contentElementTypeKey = elementTypeDataModel.contentTypeKey; diff --git a/src/Umbraco.Web.UI.Client/src/common/services/clipboard.service.js b/src/Umbraco.Web.UI.Client/src/common/services/clipboard.service.js index 2ad4bec1ea..f2e8a86361 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/clipboard.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/clipboard.service.js @@ -13,8 +13,28 @@ function clipboardService(notificationsService, eventsService, localStorageService, iconHelper) { - var clearPropertyResolvers = []; - var pastePropertyResolvers = []; + const TYPES = {}; + TYPES.ELEMENT_TYPE = "elementType"; + TYPES.RAW = "raw"; + + var clearPropertyResolvers = {}; + var pastePropertyResolvers = {}; + var pasteTypeResolvers = {}; + + pasteTypeResolvers[TYPES.ELEMENT_TYPE] = function(data, propMethod) { + for (var t = 0; t < data.variants[0].tabs.length; t++) { + var tab = data.variants[0].tabs[t]; + for (var p = 0; p < tab.properties.length; p++) { + var prop = tab.properties[p]; + propMethod(prop, TYPES.ELEMENT_TYPE); + } + } + } + pasteTypeResolvers[TYPES.RAW] = function(data, propMethod) { + for (var p = 0; p < data.length; p++) { + propMethod(data[p], TYPES.RAW); + } + } var STORAGE_KEY = "umbClipboardService"; @@ -58,28 +78,29 @@ function clipboardService(notificationsService, eventsService, localStorageServi } - function clearPropertyForStorage(prop) { + function resolvePropertyForStorage(prop, type) { - for (var i=0; i prepareEntryForStorage(data, firstLevelClearupMethod)); + var copiedDatas = datas.map(data => prepareEntryForStorage(type, data, firstLevelClearupMethod)); // remove previous copies of this entry: storage.entries = storage.entries.filter( diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/umb-block-list-property-editor.less b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/umb-block-list-property-editor.less index a2c124a6ea..b8ac443047 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/umb-block-list-property-editor.less +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/umb-block-list-property-editor.less @@ -187,25 +187,29 @@ ng-form.ng-invalid-val-server-match-settings > .umb-block-list__block > .umb-blo > .__plus { position: absolute; - pointer-events: none; // lets stop avoiding the mouse values in JS move event. - width: 24px; - height: 24px; - padding: 0; - border-radius: 3em; - border: 2px solid @blueMid; display: flex; justify-content: center; align-items: center; + pointer-events: none; // lets stop avoiding the mouse values in JS move event. + box-sizing: border-box; + width: 28px; + height: 28px; + margin-left: -16px - 8px; + margin-top: -16px; + padding: 0; + border-radius: 3em; + border: 2px solid @blueMid; color: @blueMid; + line-height: 22px; font-size: 20px; font-weight: 800; background-color: rgba(255, 255, 255, .96); box-shadow: 0 0 0 2px rgba(255, 255, 255, .96); - transform: scale(0) translate(-80%, -50%); + transform: scale(0); transition: transform 240ms ease-in; - animation: umb-block-list__block--create-button_after 800ms ease-in-out infinite; + animation: umb-block-list__block--create-button__plus 400ms ease-in-out infinite; - @keyframes umb-block-list__block--create-button_after { + @keyframes umb-block-list__block--create-button__plus { 0% { color: rgba(@blueMid, 0.8); } 50% { color: rgba(@blueMid, 1); } 100% { color: rgba(@blueMid, 0.8); } @@ -223,7 +227,7 @@ ng-form.ng-invalid-val-server-match-settings > .umb-block-list__block > .umb-blo transition-duration: 120ms; > .__plus { - transform: scale(1) translate(-80%, -50%); + transform: scale(1); transition-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1.275); } } diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/umbBlockListPropertyEditor.component.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/umbBlockListPropertyEditor.component.js index bfe5b43521..9cb5f95f79 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/umbBlockListPropertyEditor.component.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/umbBlockListPropertyEditor.component.js @@ -406,7 +406,7 @@ size: (amountOfAvailableTypes > 8 ? "medium" : "small"), filter: (amountOfAvailableTypes > 8), clickPasteItem: function(item, mouseEvent) { - if (item.type === "elementTypeArray") { + if (Array.isArray(item.pasteData)) { var indexIncrementor = 0; item.pasteData.forEach(function (entry) { if (requestPasteFromClipboard(createIndex + indexIncrementor, entry)) { @@ -448,42 +448,28 @@ }; blockPickerModel.clickClearClipboard = function ($event) { - clipboardService.clearEntriesOfType("elementType", vm.availableContentTypesAliases); - clipboardService.clearEntriesOfType("elementTypeArray", vm.availableContentTypesAliases); + clipboardService.clearEntriesOfType(clipboardService.TYPES.ELEMENT_TYPE, vm.availableContentTypesAliases); }; blockPickerModel.clipboardItems = []; - var singleEntriesForPaste = clipboardService.retriveEntriesOfType("elementType", vm.availableContentTypesAliases); - singleEntriesForPaste.forEach(function (entry) { - blockPickerModel.clipboardItems.push( - { - type: "elementType", - date: entry.date, - pasteData: entry.data, - blockConfigModel: modelObject.getScaffoldFromAlias(entry.alias), - elementTypeModel: { - name: entry.label, - icon: entry.icon - } + var entriesForPaste = clipboardService.retriveEntriesOfType(clipboardService.TYPES.ELEMENT_TYPE, vm.availableContentTypesAliases); + entriesForPaste.forEach(function (entry) { + var pasteEntry = { + type: clipboardService.TYPES.ELEMENT_TYPE, + date: entry.date, + pasteData: entry.data, + elementTypeModel: { + name: entry.label, + icon: entry.icon } - ); - }); - - var arrayEntriesForPaste = clipboardService.retriveEntriesOfType("elementTypeArray", vm.availableContentTypesAliases); - arrayEntriesForPaste.forEach(function (entry) { - blockPickerModel.clipboardItems.push( - { - type: "elementTypeArray", - date: entry.date, - pasteData: entry.data, - blockConfigModel: {}, // no block configuration for paste items of elementTypeArray. - elementTypeModel: { - name: entry.label, - icon: entry.icon - } - } - ); + } + if(Array.isArray(pasteEntry.data) === false) { + pasteEntry.blockConfigModel = modelObject.getScaffoldFromAlias(entry.alias); + } else { + pasteEntry.blockConfigModel = {}; + } + blockPickerModel.clipboardItems.push(pasteEntry); }); blockPickerModel.clipboardItems.sort( (a, b) => { @@ -513,11 +499,11 @@ } localizationService.localize("clipboard_labelForArrayOfItemsFrom", [vm.model.label, contentNodeName]).then(function(localizedLabel) { - clipboardService.copyArray("elementTypeArray", aliases, elementTypesToCopy, localizedLabel, "icon-thumbnail-list", vm.model.id); + clipboardService.copyArray(clipboardService.TYPES.ELEMENT_TYPE, aliases, elementTypesToCopy, localizedLabel, "icon-thumbnail-list", vm.model.id); }); } function copyBlock(block) { - clipboardService.copy("elementType", block.content.contentTypeAlias, block.content, block.label); + clipboardService.copy(clipboardService.TYPES.ELEMENT_TYPE, block.content.contentTypeAlias, block.content, block.label); } function requestPasteFromClipboard(index, pasteEntry) { 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 1742496f75..2d39e3a4c0 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 @@ -22,18 +22,18 @@ // Loop through all inner properties: for (var k in obj) { - propClearingMethod(obj[k]); + propClearingMethod(obj[k], clipboardService.TYPES.RAW); } } } } - clipboardService.registerClearPropertyResolver(clearNestedContentPropertiesForStorage) + clipboardService.registerClearPropertyResolver(clearNestedContentPropertiesForStorage, clipboardService.TYPES.ELEMENT_TYPE) function clearInnerNestedContentPropertiesForStorage(prop, propClearingMethod) { - // if we got an array, and it has a entry with ncContentTypeAlias this meants that we are dealing with a NestedContent property inside a NestedContent property. + // if we got an array, and it has a entry with ncContentTypeAlias this meants that we are dealing with a NestedContent property data. if ((Array.isArray(prop) && prop.length > 0 && prop[0].ncContentTypeAlias !== undefined)) { for (var i = 0; i < prop.length; i++) { @@ -44,13 +44,13 @@ // Loop through all inner properties: for (var k in obj) { - propClearingMethod(obj[k]); + propClearingMethod(obj[k], clipboardService.TYPES.RAW); } } } } - clipboardService.registerClearPropertyResolver(clearInnerNestedContentPropertiesForStorage) + clipboardService.registerClearPropertyResolver(clearInnerNestedContentPropertiesForStorage, clipboardService.TYPES.RAW) }]); angular @@ -128,7 +128,7 @@ } localizationService.localize("clipboard_labelForArrayOfItemsFrom", [model.label, nodeName]).then(function (data) { - clipboardService.copyArray("elementTypeArray", aliases, vm.nodes, data, "icon-thumbnail-list", model.id, clearNodeForCopy); + clipboardService.copyArray(clipboardService.TYPES.ELEMENT_TYPE, aliases, vm.nodes, data, "icon-thumbnail-list", model.id, clearNodeForCopy); }); } @@ -197,7 +197,7 @@ view: "itempicker", event: $event, clickPasteItem: function (item) { - if (item.type === "elementTypeArray") { + if (Array.isArray(item.data)) { _.each(item.data, function (entry) { pasteFromClipboard(entry); }); @@ -239,21 +239,9 @@ vm.overlayMenu.pasteItems = []; - var singleEntriesForPaste = clipboardService.retriveEntriesOfType("elementType", contentTypeAliases); - _.each(singleEntriesForPaste, function (entry) { + var entriesForPaste = clipboardService.retriveEntriesOfType(clipboardService.TYPES.ELEMENT_TYPE, contentTypeAliases); + _.each(entriesForPaste, function (entry) { vm.overlayMenu.pasteItems.push({ - type: "elementType", - date: entry.date, - name: entry.label, - data: entry.data, - icon: entry.icon - }); - }); - - var arrayEntriesForPaste = clipboardService.retriveEntriesOfType("elementTypeArray", contentTypeAliases); - _.each(arrayEntriesForPaste, function (entry) { - vm.overlayMenu.pasteItems.push({ - type: "elementTypeArray", date: entry.date, name: entry.label, data: entry.data, @@ -271,8 +259,7 @@ vm.overlayMenu.clickClearPaste = function ($event) { $event.stopPropagation(); $event.preventDefault(); - clipboardService.clearEntriesOfType("elementType", contentTypeAliases); - clipboardService.clearEntriesOfType("elementTypeArray", contentTypeAliases); + clipboardService.clearEntriesOfType(clipboardService.TYPES.ELEMENT_TYPE, contentTypeAliases); vm.overlayMenu.pasteItems = [];// This dialog is not connected via the clipboardService events, so we need to update manually. vm.overlayMenu.hideHeader = false; }; @@ -463,7 +450,7 @@ syncCurrentNode(); - clipboardService.copy("elementType", node.contentTypeAlias, node, null, null, null, clearNodeForCopy); + clipboardService.copy(clipboardService.TYPES.ELEMENT_TYPE, node.contentTypeAlias, node, null, null, null, clearNodeForCopy); $event.stopPropagation(); } @@ -474,7 +461,7 @@ return; } - newNode = clipboardService.parseContentForPaste(newNode); + newNode = clipboardService.parseContentForPaste(newNode, clipboardService.TYPES.ELEMENT_TYPE); // generate a new key. newNode.key = String.CreateGuid(); @@ -487,7 +474,7 @@ } function checkAbilityToPasteContent() { - vm.showPaste = clipboardService.hasEntriesOfType("elementType", contentTypeAliases) || clipboardService.hasEntriesOfType("elementTypeArray", contentTypeAliases); + vm.showPaste = clipboardService.hasEntriesOfType(clipboardService.TYPES.ELEMENT_TYPE, contentTypeAliases); } eventsService.on("clipboardService.storageUpdate", checkAbilityToPasteContent); From 906f3eac2febd53d7e6eafea83f0d4500bd78612 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Thu, 3 Sep 2020 14:41:23 +0200 Subject: [PATCH 14/31] putting the attributes right --- .../src/common/services/blockeditormodelobject.service.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/services/blockeditormodelobject.service.js b/src/Umbraco.Web.UI.Client/src/common/services/blockeditormodelobject.service.js index a03c64ff9d..6bce7ec89b 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/blockeditormodelobject.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/blockeditormodelobject.service.js @@ -745,7 +745,7 @@ */ createFromElementType: function (elementTypeDataModel) { - elementTypeDataModel = clipboardService.parseContentForPaste(clipboardService.TYPES.ELEMENT_TYPE, elementTypeDataModel); + elementTypeDataModel = clipboardService.parseContentForPaste(elementTypeDataModel, clipboardService.TYPES.ELEMENT_TYPE); var contentElementTypeKey = elementTypeDataModel.contentTypeKey; From 208edfc3a266a7004b868a333cb37f6e8daf984c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Thu, 3 Sep 2020 21:36:30 +0200 Subject: [PATCH 15/31] rename pasteTypeResolvers to clipboardTypeResolvers. as its not specific to paste, but clipboard. --- .../src/common/services/clipboard.service.js | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/services/clipboard.service.js b/src/Umbraco.Web.UI.Client/src/common/services/clipboard.service.js index f2e8a86361..58ed07367e 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/clipboard.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/clipboard.service.js @@ -19,9 +19,9 @@ function clipboardService(notificationsService, eventsService, localStorageServi var clearPropertyResolvers = {}; var pastePropertyResolvers = {}; - var pasteTypeResolvers = {}; + var clipboardTypeResolvers = {}; - pasteTypeResolvers[TYPES.ELEMENT_TYPE] = function(data, propMethod) { + clipboardTypeResolvers[TYPES.ELEMENT_TYPE] = function(data, propMethod) { for (var t = 0; t < data.variants[0].tabs.length; t++) { var tab = data.variants[0].tabs[t]; for (var p = 0; p < tab.properties.length; p++) { @@ -30,7 +30,7 @@ function clipboardService(notificationsService, eventsService, localStorageServi } } } - pasteTypeResolvers[TYPES.RAW] = function(data, propMethod) { + clipboardTypeResolvers[TYPES.RAW] = function(data, propMethod) { for (var p = 0; p < data.length; p++) { propMethod(data[p], TYPES.RAW); } @@ -96,7 +96,7 @@ function clipboardService(notificationsService, eventsService, localStorageServi firstLevelClearupMethod(cloneData); } - var typeResolver = pasteTypeResolvers[type]; + var typeResolver = clipboardTypeResolvers[type]; if(typeResolver) { typeResolver(cloneData, resolvePropertyForStorage); } else { @@ -153,7 +153,7 @@ function clipboardService(notificationsService, eventsService, localStorageServi service.parseContentForPaste = function(pasteEntryData, type) { var cloneData = Utilities.copy(pasteEntryData); - var typeResolver = pasteTypeResolvers[type]; + var typeResolver = clipboardTypeResolvers[type]; if(typeResolver) { typeResolver(cloneData, resolvePropertyForPaste); } else { @@ -232,10 +232,10 @@ function clipboardService(notificationsService, eventsService, localStorageServi * Executed for all properties including inner properties when performing a paste action. */ service.registrerTypeResolvers = function(resolver, type) { - if(!pasteTypeResolvers[type]) { - pasteTypeResolvers[type] = []; + if(!clipboardTypeResolvers[type]) { + clipboardTypeResolvers[type] = []; } - pasteTypeResolvers[type].push(resolver); + clipboardTypeResolvers[type].push(resolver); }; From e7ad346d930ff1d850b9edcbb4f20a7cfb65a8ce Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Fri, 4 Sep 2020 09:39:58 +0200 Subject: [PATCH 16/31] Fix JS error when creating macros and relation types (cherry picked from commit a5e4633377c917bf64cc632a4c8a810af48d24bf) --- .../src/views/macros/macros.create.controller.js | 6 +++--- .../src/views/relationtypes/create.controller.js | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/macros/macros.create.controller.js b/src/Umbraco.Web.UI.Client/src/views/macros/macros.create.controller.js index e8c5c550d0..baae8e085a 100644 --- a/src/Umbraco.Web.UI.Client/src/views/macros/macros.create.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/macros/macros.create.controller.js @@ -13,7 +13,7 @@ function MacrosCreateController($scope, $location, macroResource, navigationServ function createItem() { - if (formHelper.submitForm({ scope: $scope, formCtrl: this.createMacroForm })) { + if (formHelper.submitForm({ scope: $scope, formCtrl: $scope.createMacroForm })) { var node = $scope.currentNode; @@ -25,7 +25,7 @@ function MacrosCreateController($scope, $location, macroResource, navigationServ navigationService.syncTree({ tree: "macros", path: currPath + "," + data, forceReload: true, activate: true }); // reset form state - formHelper.resetForm({ scope: $scope, formCtrl: this.createMacroForm }); + formHelper.resetForm({ scope: $scope, formCtrl: $scope.createMacroForm }); // navigate to edit view var currentSection = appState.getSectionState("currentSection"); @@ -33,7 +33,7 @@ function MacrosCreateController($scope, $location, macroResource, navigationServ }, function (err) { - formHelper.resetForm({ scope: $scope, formCtrl: this.createMacroForm, hasErrors: true }); + formHelper.resetForm({ scope: $scope, formCtrl: $scope.createMacroForm, hasErrors: true }); if (err.data && err.data.message) { notificationsService.error(err.data.message); navigationService.hideMenu(); diff --git a/src/Umbraco.Web.UI.Client/src/views/relationtypes/create.controller.js b/src/Umbraco.Web.UI.Client/src/views/relationtypes/create.controller.js index b2fbee4b36..bd990961bf 100644 --- a/src/Umbraco.Web.UI.Client/src/views/relationtypes/create.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/relationtypes/create.controller.js @@ -26,7 +26,7 @@ function RelationTypeCreateController($scope, $location, relationTypeResource, n } function createRelationType() { - if (formHelper.submitForm({ scope: $scope, formCtrl: this.createRelationTypeForm, statusMessage: "Creating relation type..." })) { + if (formHelper.submitForm({ scope: $scope, formCtrl: $scope.createRelationTypeForm, statusMessage: "Creating relation type..." })) { var node = $scope.currentNode; relationTypeResource.create(vm.relationType).then(function (data) { @@ -36,12 +36,12 @@ function RelationTypeCreateController($scope, $location, relationTypeResource, n var currentPath = node.path ? node.path : "-1"; navigationService.syncTree({ tree: "relationTypes", path: currentPath + "," + data, forceReload: true, activate: true }); - formHelper.resetForm({ scope: $scope, formCtrl: this.createRelationTypeForm }); + formHelper.resetForm({ scope: $scope, formCtrl: $scope.createRelationTypeForm }); var currentSection = appState.getSectionState("currentSection"); $location.path("/" + currentSection + "/relationTypes/edit/" + data); }, function (err) { - formHelper.resetForm({ scope: $scope, formCtrl: this.createRelationTypeForm, hasErrors: true }); + formHelper.resetForm({ scope: $scope, formCtrl: $scope.createRelationTypeForm, hasErrors: true }); if (err.data && err.data.message) { notificationsService.error(err.data.message); navigationService.hideMenu(); From 330c113da2915a22b590355809e02bc37d00edff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Fri, 21 Aug 2020 12:28:10 +0200 Subject: [PATCH 17/31] append property editor api methods to the block object for better developer experience. --- .../umbBlockListPropertyEditor.component.js | 16 ++++++++++++++++ .../blocklist/umbblocklistblock.component.js | 3 +++ 2 files changed, 19 insertions(+) diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/umbBlockListPropertyEditor.component.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/umbBlockListPropertyEditor.component.js index bfe5b43521..ccd69be1e0 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/umbBlockListPropertyEditor.component.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/umbBlockListPropertyEditor.component.js @@ -243,6 +243,22 @@ block.showSettings = block.config.settingsElementTypeKey != null; block.showCopy = vm.supportCopy && block.config.contentElementTypeKey != null;// if we have content, otherwise it doesn't make sense to copy. + block.setParentForm = function (parentForm) { + this._parentForm = parentForm; + } + block.activate = activateBlock.bind(null, block); + block.edit = function () { + var blockIndex = vm.layout.indexOf(this.layout); + editBlock(this, false, blockIndex, this._parentForm); + } + block.editSettings = function () { + var blockIndex = vm.layout.indexOf(this.layout); + editBlock(this, true, blockIndex, this._parentForm); + } + block.requestDelete = requestDeleteBlock.bind(null, block); + block.delete = deleteBlock.bind(null, block); + block.copy = copyBlock.bind(null, block); + return block; } diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/umbblocklistblock.component.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/umbblocklistblock.component.js index 206fb0bb3a..4531612cba 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/umbblocklistblock.component.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/umbblocklistblock.component.js @@ -41,6 +41,9 @@ // Guess we'll leave it for now but means all things need to be copied to the $scope and then all // primitives need to be watched. + // let the Block know about its form + model.block.setParentForm(model.parentForm); + $scope.block = model.block; $scope.api = model.api; $scope.index = model.index; From b56b15437de10f7101621e43c0131f4b12f2bec7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Tue, 1 Sep 2020 13:27:00 +0200 Subject: [PATCH 18/31] Only request getCount if user has access to settings --- .../content/content.create.controller.js | 27 ++++++++++--------- 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/content/content.create.controller.js b/src/Umbraco.Web.UI.Client/src/views/content/content.create.controller.js index 487a73f948..6f3c8d9154 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/content.create.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/content/content.create.controller.js @@ -22,20 +22,17 @@ function contentCreateController($scope, function initialize() { $scope.loading = true; $scope.allowedTypes = null; - + var getAllowedTypes = contentTypeResource.getAllowedTypes($scope.currentNode.id).then(function (data) { $scope.allowedTypes = iconHelper.formatContentTypeIcons(data); - if ($scope.allowedTypes.length === 0) { - contentTypeResource.getCount().then(function(count) { - $scope.countTypes = count; - }); - } }); var getCurrentUser = authResource.getCurrentUser().then(function (currentUser) { - if (currentUser.allowedSections.indexOf("settings") > -1) { - $scope.hasSettingsAccess = true; + + $scope.hasSettingsAccess = currentUser.allowedSections.indexOf("settings") > -1; + if ($scope.hasSettingsAccess) { + if ($scope.currentNode.id > -1) { - contentResource.getById($scope.currentNode.id).then(function (data) { + return contentResource.getById($scope.currentNode.id).then(function (data) { $scope.contentTypeId = data.contentTypeId; }); } @@ -43,6 +40,12 @@ function contentCreateController($scope, }); $q.all([getAllowedTypes, getCurrentUser]).then(function() { + if ($scope.hasSettingsAccess === true && $scope.allowedTypes.length === 0) { + return contentTypeResource.getCount().then(function(count) { + $scope.countTypes = count; + }); + } + }).then(function() { $scope.loading = false; }); @@ -60,13 +63,13 @@ function contentCreateController($scope, .path("/content/content/edit/" + $scope.currentNode.id) .search("doctype", docType.alias) .search("create", "true") - /* when we create a new node we want to make sure it uses the same + /* when we create a new node we want to make sure it uses the same language as what is selected in the tree */ .search("cculture", mainCulture) - /* when we create a new node we must make sure that any previously + /* when we create a new node we must make sure that any previously opened segments is reset */ .search("csegment", null) - /* when we create a new node we must make sure that any previously + /* when we create a new node we must make sure that any previously used blueprint is reset */ .search("blueprintId", null); close(); From e7ac6a614a585c0739e774ebbbdb865db7655901 Mon Sep 17 00:00:00 2001 From: Sebastiaan Janssen Date: Thu, 3 Sep 2020 10:36:58 +0200 Subject: [PATCH 19/31] Switch from the Chrome runner to JSDom (#8800) (cherry picked from commit 4313463a949589c22e1d8f91baee9d47302346df) --- build/build.ps1 | 18 +++++++++++++++++- src/Umbraco.Web.UI.Client/package.json | 4 ++-- .../test/config/karma.conf.js | 13 ++----------- 3 files changed, 21 insertions(+), 14 deletions(-) diff --git a/build/build.ps1 b/build/build.ps1 index c2c5bdd232..15d455b976 100644 --- a/build/build.ps1 +++ b/build/build.ps1 @@ -125,7 +125,23 @@ $error.Clear() Write-Output "### gulp build for version $($this.Version.Release)" >> $log 2>&1 - npx gulp build --buildversion=$this.Version.Release >> $log 2>&1 + npm run build --buildversion=$this.Version.Release >> $log 2>&1 + + # We can ignore this warning, we need to update to node 12 at some point - https://github.com/jsdom/jsdom/issues/2939 + $indexes = [System.Collections.ArrayList]::new() + $index = 0; + $error | ForEach-Object { + # Find which of the errors is the ExperimentalWarning + if($_.ToString().Contains("ExperimentalWarning: The fs.promises API is experimental")) { + [void]$indexes.Add($index) + } + $index++ + } + $indexes | ForEach-Object { + # Loop through the list of indexes and remove the errors that we expect and feel confident we can ignore + $error.Remove($error[$_]) + } + if (-not $?) { throw "Failed to build" } # that one is expected to work } finally { Pop-Location diff --git a/src/Umbraco.Web.UI.Client/package.json b/src/Umbraco.Web.UI.Client/package.json index 0e0eda12f6..0bbe62baaf 100644 --- a/src/Umbraco.Web.UI.Client/package.json +++ b/src/Umbraco.Web.UI.Client/package.json @@ -71,11 +71,11 @@ "gulp-wrap": "0.15.0", "gulp-wrap-js": "0.4.1", "jasmine-core": "3.5.0", + "jsdom": "16.4.0", "karma": "4.4.1", - "karma-chrome-launcher": "^3.1.0", + "karma-jsdom-launcher": "^8.0.2", "karma-jasmine": "2.0.1", "karma-junit-reporter": "2.0.1", - "karma-phantomjs-launcher": "1.0.4", "karma-spec-reporter": "0.0.32", "less": "3.10.3", "lodash": "4.17.19", diff --git a/src/Umbraco.Web.UI.Client/test/config/karma.conf.js b/src/Umbraco.Web.UI.Client/test/config/karma.conf.js index d0f59c110f..c039e59ee1 100644 --- a/src/Umbraco.Web.UI.Client/test/config/karma.conf.js +++ b/src/Umbraco.Web.UI.Client/test/config/karma.conf.js @@ -100,14 +100,7 @@ module.exports = function (config) { // - PhantomJS // - IE (only Windows) // CLI --browsers Chrome,Firefox,Safari - browsers: ['ChromeHeadless'], - - customLaunchers: { - ChromeDebugging: { - base: 'Chrome', - flags: ['--remote-debugging-port=9333'] - } - }, + browsers: ['jsdom'], // allow waiting a bit longer, some machines require this @@ -123,11 +116,9 @@ module.exports = function (config) { plugins: [ require('karma-jasmine'), - require('karma-phantomjs-launcher'), - require('karma-chrome-launcher'), + require('karma-jsdom-launcher'), require('karma-junit-reporter'), require('karma-spec-reporter') - ], // the default configuration From 3f99972ebb387511c68194519a1184a8eb06374e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Fri, 4 Sep 2020 13:51:00 +0200 Subject: [PATCH 20/31] if user discards a new block it should be removed. --- .../blockeditor/blockeditor.controller.js | 19 +++++++------ .../umbBlockListPropertyEditor.component.js | 27 +++++++++++-------- src/Umbraco.Web.UI/Umbraco/config/lang/da.xml | 2 ++ src/Umbraco.Web.UI/Umbraco/config/lang/en.xml | 2 ++ .../Umbraco/config/lang/en_us.xml | 2 ++ 5 files changed, 31 insertions(+), 21 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/blockeditor/blockeditor.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/blockeditor/blockeditor.controller.js index a08a05b0f7..88cda027a8 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/blockeditor/blockeditor.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/blockeditor/blockeditor.controller.js @@ -7,8 +7,8 @@ angular.module("umbraco") vm.tabs = []; localizationService.localizeMany([ - vm.model.liveEditing ? "prompt_discardChanges" : "general_close", - vm.model.liveEditing ? "buttons_confirmActionConfirm" : "buttons_submitChanges" + vm.model.createFlow ? "general_cancel" : (vm.model.liveEditing ? "prompt_discardChanges" : "general_close"), + vm.model.createFlow ? "general_create" : (vm.model.liveEditing ? "buttons_confirmActionConfirm" : "buttons_submitChanges") ]).then(function (data) { vm.closeLabel = data[0]; vm.submitLabel = data[1]; @@ -68,16 +68,16 @@ angular.module("umbraco") // * It would have a 'commit' method to commit the removed errors - which we would call in the formHelper.submitForm when it's successful // * It would have a 'rollback' method to reset the removed errors - which we would call here - - if (vm.blockForm.$dirty === true) { - localizationService.localizeMany(["prompt_discardChanges", "blockEditor_blockHasChanges"]).then(function (localizations) { + if (vm.model.createFlow === true || vm.blockForm.$dirty === true) { + var labels = vm.model.createFlow === true ? ["blockEditor_confirmCancelBlockCreationHeadline", "blockEditor_confirmCancelBlockCreationMessage"] : ["prompt_discardChanges", "blockEditor_blockHasChanges"]; + localizationService.localizeMany(labels).then(function (localizations) { const confirm = { title: localizations[0], view: "default", content: localizations[1], submitButtonLabelKey: "general_discard", submitButtonStyle: "danger", - closeButtonLabelKey: "general_cancel", + closeButtonLabelKey: "prompt_stay", submit: function () { overlayService.close(); vm.model.close(vm.model); @@ -88,11 +88,10 @@ angular.module("umbraco") }; overlayService.open(confirm); }); - - return; + } else { + vm.model.close(vm.model); } - // TODO: check if content/settings has changed and ask user if they are sure. - vm.model.close(vm.model); + } } diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/umbBlockListPropertyEditor.component.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/umbBlockListPropertyEditor.component.js index ccd69be1e0..92ffb51f65 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/umbBlockListPropertyEditor.component.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/umbBlockListPropertyEditor.component.js @@ -326,7 +326,9 @@ blockObject.active = true; } - function editBlock(blockObject, openSettings, blockIndex, parentForm) { + function editBlock(blockObject, openSettings, blockIndex, parentForm, options) { + + options = options || {}; // this must be set if (blockIndex === undefined) { @@ -360,6 +362,7 @@ $parentForm: parentForm || vm.propertyForm, // pass in a $parentForm, this maintains the FormController hierarchy with the infinite editing view (if it contains a form) hideContent: blockObject.hideContentInOverlay, openSettings: openSettings === true, + createFlow: options.createFlow === true, liveEditing: liveEditing, title: blockObject.label, view: "views/common/infiniteeditors/blockeditor/blockeditor.html", @@ -374,15 +377,17 @@ blockObject.active = false; editorService.close(); }, - close: function() { - - if (liveEditing === true) { - // revert values when closing in liveediting mode. - blockObject.retrieveValuesFrom(blockContentClone, blockSettingsClone); - } - - if (wasNotActiveBefore === true) { - blockObject.active = false; + close: function(blockEditorModel) { + if (blockEditorModel.createFlow) { + deleteBlock(blockObject); + } else { + if (liveEditing === true) { + // revert values when closing in liveediting mode. + blockObject.retrieveValuesFrom(blockContentClone, blockSettingsClone); + } + if (wasNotActiveBefore === true) { + blockObject.active = false; + } } editorService.close(); } @@ -448,7 +453,7 @@ if (inlineEditing === true) { activateBlock(vm.layout[createIndex].$block); } else if (inlineEditing === false && vm.layout[createIndex].$block.hideContentInOverlay !== true) { - editBlock(vm.layout[createIndex].$block, false, createIndex, blockPickerModel.$parentForm); + editBlock(vm.layout[createIndex].$block, false, createIndex, blockPickerModel.$parentForm, {createFlow: true}); } } } diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/da.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/da.xml index 997fa577b7..9f41926e5d 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/da.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/da.xml @@ -1853,6 +1853,8 @@ Mange hilsner fra Umbraco robotten Avanceret Skjuld indholds editoren Du har lavet ændringer til dette indhold. Er du sikker på at du vil kassere dem? + Annuller oprettelse? + diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml index d72ee33d6c..a6ef4d67f1 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml @@ -2469,6 +2469,8 @@ To manage your website, simply open the Umbraco back office and start adding con Advanced Force hide content editor You have made changes to this content. Are you sure you want to discard them? + Discard creation? + What are Content Templates? 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 cb31695268..3411b98717 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml @@ -2489,6 +2489,8 @@ To manage your website, simply open the Umbraco back office and start adding con Advanced Force hide content editor You have made changes to this content. Are you sure you want to discard them? + Discard creation? + What are Content Templates? From 86d105dcf383939222f3dea07699c8f609a080da Mon Sep 17 00:00:00 2001 From: Bjarne Fyrstenborg Date: Mon, 7 Sep 2020 10:46:03 +0200 Subject: [PATCH 21/31] Checkboxlist and radiobuttonlist validation (#8833) (cherry picked from commit 4782560f2fe7337402045244b02ab4bbcc935db5) --- .../checkboxlist/checkboxlist.controller.js | 10 +++++++++- .../checkboxlist/checkboxlist.html | 19 ++++++++++++++----- .../radiobuttons/radiobuttons.html | 2 +- 3 files changed, 24 insertions(+), 7 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/checkboxlist/checkboxlist.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/checkboxlist/checkboxlist.controller.js index 10668808a5..0d55a75233 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/checkboxlist/checkboxlist.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/checkboxlist/checkboxlist.controller.js @@ -1,5 +1,5 @@ angular.module("umbraco").controller("Umbraco.PropertyEditors.CheckboxListController", - function ($scope) { + function ($scope, validationMessageService) { var vm = this; @@ -8,6 +8,8 @@ angular.module("umbraco").controller("Umbraco.PropertyEditors.CheckboxListContro vm.change = change; function init() { + + vm.uniqueId = String.CreateGuid(); // currently the property editor will onyl work if our input is an object. if (Utilities.isObject($scope.model.config.items)) { @@ -35,6 +37,12 @@ angular.module("umbraco").controller("Umbraco.PropertyEditors.CheckboxListContro //watch the model.value in case it changes so that we can keep our view model in sync $scope.$watchCollection("model.value", updateViewModel); } + + // Set the message to use for when a mandatory field isn't completed. + // Will either use the one provided on the property type or a localised default. + validationMessageService.getMandatoryMessage($scope.model.validation).then(function (value) { + $scope.mandatoryMessage = value; + }); } diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/checkboxlist/checkboxlist.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/checkboxlist/checkboxlist.html index 20f24728dd..4207a327ad 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/checkboxlist/checkboxlist.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/checkboxlist/checkboxlist.html @@ -1,7 +1,16 @@ 
-
    -
  • - -
  • -
+ + + +
    +
  • + +
  • +
+ +
+

{{mandatoryMessage}}

+
+ +
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 c9d05a303c..88f8e676a5 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 @@ -12,7 +12,7 @@ -
+

{{mandatoryMessage}}

From 40d13bfb2fb32438416d29e210c224e819be9a3e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Mon, 7 Sep 2020 12:33:58 +0200 Subject: [PATCH 22/31] =?UTF-8?q?V8.7RC=20Show=20Blocks=20that=20does=20no?= =?UTF-8?q?t=20have=20a=20BlockConfiguration=20as=20Unsupported=20Bl?= =?UTF-8?q?=E2=80=A6=20(#8786)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../blockeditormodelobject.service.js | 27 +++++++++---------- .../unsupportedblock.editor.html | 6 +++-- ...blocklist.blockconfiguration.controller.js | 2 +- .../umbBlockListPropertyEditor.component.js | 4 +-- src/Umbraco.Web.UI/Umbraco/config/lang/da.xml | 5 ++-- src/Umbraco.Web.UI/Umbraco/config/lang/en.xml | 5 ++-- .../Umbraco/config/lang/en_us.xml | 5 ++-- 7 files changed, 29 insertions(+), 25 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/services/blockeditormodelobject.service.js b/src/Umbraco.Web.UI.Client/src/common/services/blockeditormodelobject.service.js index 988e8f9fa9..84e9dd07c0 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/blockeditormodelobject.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/blockeditormodelobject.service.js @@ -524,12 +524,11 @@ } var blockConfiguration = this.getBlockConfiguration(dataModel.contentTypeKey); - var contentScaffold; + var contentScaffold = null; if (blockConfiguration === null) { - console.error("The block of " + contentUdi + " is not being initialized because its contentTypeKey('" + dataModel.contentTypeKey + "') is not allowed for this PropertyEditor"); - } - else { + console.warn("The block of " + contentUdi + " is not being initialized because its contentTypeKey('" + dataModel.contentTypeKey + "') is not allowed for this PropertyEditor"); + } else { contentScaffold = this.getScaffoldFromKey(blockConfiguration.contentElementTypeKey); if (contentScaffold === null) { console.error("The block of " + contentUdi + " is not begin initialized cause its Element Type was not loaded."); @@ -539,10 +538,9 @@ if (blockConfiguration === null || contentScaffold === null) { blockConfiguration = { - label: "Unsupported Block", + label: "Unsupported", unsupported: true }; - contentScaffold = {}; } var blockObject = {}; @@ -567,10 +565,14 @@ , 10); // make basics from scaffold - blockObject.content = Utilities.copy(contentScaffold); - ensureUdiAndKey(blockObject.content, contentUdi); + if(contentScaffold !== null) {// We might not have contentScaffold + blockObject.content = Utilities.copy(contentScaffold); + ensureUdiAndKey(blockObject.content, contentUdi); - mapToElementModel(blockObject.content, dataModel); + mapToElementModel(blockObject.content, dataModel); + } else { + blockObject.content = null; + } blockObject.data = dataModel; blockObject.layout = layoutEntry; @@ -672,11 +674,8 @@ * @param {Object} blockObject The BlockObject to be removed and destroyed. */ removeDataAndDestroyModel: function (blockObject) { - var udi = blockObject.content.udi; - var settingsUdi = null; - if (blockObject.settings) { - settingsUdi = blockObject.settings.udi; - } + var udi = blockObject.layout.contentUdi; + var settingsUdi = blockObject.layout.settingsUdi || null; this.destroyBlockObject(blockObject); this.removeDataByUdi(udi); if (settingsUdi) { diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklistentryeditors/unsupportedblock/unsupportedblock.editor.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklistentryeditors/unsupportedblock/unsupportedblock.editor.html index d860b44b60..0317105a66 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklistentryeditors/unsupportedblock/unsupportedblock.editor.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklistentryeditors/unsupportedblock/unsupportedblock.editor.html @@ -4,9 +4,11 @@ {{block.label}}
- This Block is no longer supported in this context.
+ This content is no longer supported in this context.
You might want to remove this block, or contact your developer to take actions for making this block available again.

- Learn about this circumstance +
Block data:

     
diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/blocklist.blockconfiguration.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/blocklist.blockconfiguration.controller.js index ba0d4415f5..7c608cb3f4 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/blocklist.blockconfiguration.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/blocklist.blockconfiguration.controller.js @@ -50,7 +50,7 @@ unsubscribe.push(eventsService.on("editors.documentType.saved", updateUsedElementTypes)); vm.requestRemoveBlockByIndex = function (index) { - localizationService.localizeMany(["general_delete", "blockEditor_confirmDeleteBlockMessage", "blockEditor_confirmDeleteBlockNotice"]).then(function (data) { + localizationService.localizeMany(["general_delete", "blockEditor_confirmDeleteBlockTypeMessage", "blockEditor_confirmDeleteBlockTypeNotice"]).then(function (data) { var contentElementType = vm.getElementTypeByKey($scope.model.value[index].contentElementTypeKey); overlayService.confirmDelete({ title: data[0], diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/umbBlockListPropertyEditor.component.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/umbBlockListPropertyEditor.component.js index 92ffb51f65..0e39e16fcd 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/umbBlockListPropertyEditor.component.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/umbBlockListPropertyEditor.component.js @@ -294,9 +294,9 @@ function deleteBlock(block) { - var layoutIndex = vm.layout.findIndex(entry => entry.contentUdi === block.content.udi); + var layoutIndex = vm.layout.findIndex(entry => entry.contentUdi === block.layout.contentUdi); if (layoutIndex === -1) { - throw new Error("Could not find layout entry of block with udi: "+block.content.udi) + throw new Error("Could not find layout entry of block with udi: "+block.layout.contentUdi) } setDirty(); diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/da.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/da.xml index 9f41926e5d..10070e8c6b 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/da.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/da.xml @@ -1842,8 +1842,9 @@ Mange hilsner fra Umbraco robotten Tilføj speciel visning Tilføj instillinger Overskriv label form - %0%.]]> - Indhold der benytter sig af denne blok vil gå bort. + %0%.]]> + %0%.]]> + Indholdet vil stadigt eksistere, men redigering af dette indhold vil ikke være muligt. Indholdet vil blive vist som ikke understøttet indhold. Billede Tilføj billede diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml index a6ef4d67f1..a920e86775 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml @@ -2458,8 +2458,9 @@ To manage your website, simply open the Umbraco back office and start adding con Add custom view Add settings Overwrite label template - %0%.]]> - Content using this block will be lost. + %0%.]]> + %0%.]]> + The content of this block will still be present, editing of this content will no longer be available and will be shown as unsupported content. Thumbnail Add thumbnail 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 3411b98717..edf644b760 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml @@ -2478,8 +2478,9 @@ To manage your website, simply open the Umbraco back office and start adding con Add custom view Add settings Overwrite label template - %0%.]]> - Content using this block will be lost. + %0%.]]> + %0%.]]> + The content of this block will still be present, editing of this content will no longer be available and will be shown as unsupported content. Thumbnail Add thumbnail From a2c24bcc7682a1c7ca01fbd4aabe9b3f39c60ec0 Mon Sep 17 00:00:00 2001 From: Shannon Date: Tue, 8 Sep 2020 02:07:02 +1000 Subject: [PATCH 23/31] Gets copying logic working for the block editor, needs more tests though --- src/Umbraco.Core/ContentExtensions.cs | 9 + .../Models/Blocks/BlockEditorDataConverter.cs | 25 +++ .../Models/Blocks/BlockListLayoutItem.cs | 2 +- src/Umbraco.Core/Models/PropertyCollection.cs | 1 + ...omplexPropertyEditorContentEventHandler.cs | 98 +++++++++ src/Umbraco.Core/Umbraco.Core.csproj | 1 + .../BlockEditorComponentTests.cs | 158 +++++++++++++++ .../NestedContentPropertyComponentTests.cs | 1 + src/Umbraco.Tests/Umbraco.Tests.csproj | 1 + .../Compose/BlockEditorComponent.cs | 187 ++++++++++++++++++ .../Compose/BlockEditorComposer.cs | 12 ++ .../Compose/NestedContentPropertyComponent.cs | 60 ++---- .../Compose/NestedContentPropertyComposer.cs | 3 + src/Umbraco.Web/Umbraco.Web.csproj | 2 + 14 files changed, 511 insertions(+), 49 deletions(-) create mode 100644 src/Umbraco.Core/PropertyEditors/ComplexPropertyEditorContentEventHandler.cs create mode 100644 src/Umbraco.Tests/PropertyEditors/BlockEditorComponentTests.cs create mode 100644 src/Umbraco.Web/Compose/BlockEditorComponent.cs create mode 100644 src/Umbraco.Web/Compose/BlockEditorComposer.cs diff --git a/src/Umbraco.Core/ContentExtensions.cs b/src/Umbraco.Core/ContentExtensions.cs index 7bce23e98e..eb6339741a 100644 --- a/src/Umbraco.Core/ContentExtensions.cs +++ b/src/Umbraco.Core/ContentExtensions.cs @@ -119,6 +119,15 @@ namespace Umbraco.Core return false; } + /// + /// Returns all properties based on the editorAlias + /// + /// + /// + /// + public static IEnumerable GetPropertiesByEditor(this IContentBase content, string editorAlias) + => content.Properties.Where(x => x.PropertyType.PropertyEditorAlias == editorAlias); + /// /// Returns properties that do not belong to a group /// diff --git a/src/Umbraco.Core/Models/Blocks/BlockEditorDataConverter.cs b/src/Umbraco.Core/Models/Blocks/BlockEditorDataConverter.cs index 22e364c0f8..802e8c2ee3 100644 --- a/src/Umbraco.Core/Models/Blocks/BlockEditorDataConverter.cs +++ b/src/Umbraco.Core/Models/Blocks/BlockEditorDataConverter.cs @@ -18,10 +18,35 @@ namespace Umbraco.Core.Models.Blocks _propertyEditorAlias = propertyEditorAlias; } + public BlockEditorData ConvertFrom(JToken json) + { + var value = json.ToObject(); + return Convert(value); + } + + public bool TryDeserialize(string json, out BlockEditorData blockEditorData) + { + try + { + var value = JsonConvert.DeserializeObject(json); + blockEditorData = Convert(value); + return true; + } + catch (System.Exception) + { + blockEditorData = null; + return false; + } + } + public BlockEditorData Deserialize(string json) { var value = JsonConvert.DeserializeObject(json); + return Convert(value); + } + private BlockEditorData Convert(BlockValue value) + { if (value.Layout == null) return BlockEditorData.Empty; diff --git a/src/Umbraco.Core/Models/Blocks/BlockListLayoutItem.cs b/src/Umbraco.Core/Models/Blocks/BlockListLayoutItem.cs index 3453ff2a78..5de44e16c1 100644 --- a/src/Umbraco.Core/Models/Blocks/BlockListLayoutItem.cs +++ b/src/Umbraco.Core/Models/Blocks/BlockListLayoutItem.cs @@ -12,7 +12,7 @@ namespace Umbraco.Core.Models.Blocks [JsonConverter(typeof(UdiJsonConverter))] public Udi ContentUdi { get; set; } - [JsonProperty("settingsUdi")] + [JsonProperty("settingsUdi", NullValueHandling = NullValueHandling.Ignore)] [JsonConverter(typeof(UdiJsonConverter))] public Udi SettingsUdi { get; set; } } diff --git a/src/Umbraco.Core/Models/PropertyCollection.cs b/src/Umbraco.Core/Models/PropertyCollection.cs index c587a45424..13fbf949d6 100644 --- a/src/Umbraco.Core/Models/PropertyCollection.cs +++ b/src/Umbraco.Core/Models/PropertyCollection.cs @@ -7,6 +7,7 @@ using System.Runtime.Serialization; namespace Umbraco.Core.Models { + /// /// Represents a collection of property values. /// diff --git a/src/Umbraco.Core/PropertyEditors/ComplexPropertyEditorContentEventHandler.cs b/src/Umbraco.Core/PropertyEditors/ComplexPropertyEditorContentEventHandler.cs new file mode 100644 index 0000000000..2b819d4555 --- /dev/null +++ b/src/Umbraco.Core/PropertyEditors/ComplexPropertyEditorContentEventHandler.cs @@ -0,0 +1,98 @@ +using System; +using System.Collections.Generic; +using Umbraco.Core.Events; +using Umbraco.Core.Models; +using Umbraco.Core.Services; +using Umbraco.Core.Services.Implement; + +namespace Umbraco.Core.PropertyEditors +{ + /// + /// Utility class for dealing with Copying/Saving events for complex editors + /// + internal class ComplexPropertyEditorContentEventHandler : IDisposable + { + private readonly string _editorAlias; + private readonly Func _formatPropertyValue; + private bool _disposedValue; + + public ComplexPropertyEditorContentEventHandler(string editorAlias, + Func formatPropertyValue) + { + _editorAlias = editorAlias; + _formatPropertyValue = formatPropertyValue; + ContentService.Copying += ContentService_Copying; + ContentService.Saving += ContentService_Saving; + } + + /// + /// Copying event handler + /// + /// + /// + private void ContentService_Copying(IContentService sender, CopyEventArgs e) + { + var props = e.Copy.GetPropertiesByEditor(_editorAlias); + UpdatePropertyValues(props, false); + } + + /// + /// Saving event handler + /// + /// + /// + private void ContentService_Saving(IContentService sender, ContentSavingEventArgs e) + { + foreach (var entity in e.SavedEntities) + { + var props = entity.GetPropertiesByEditor(_editorAlias); + UpdatePropertyValues(props, true); + } + } + + private void UpdatePropertyValues(IEnumerable props, bool onlyMissingKeys) + { + foreach (var prop in props) + { + // A Property may have one or more values due to cultures + var propVals = prop.Values; + foreach (var cultureVal in propVals) + { + // Remove keys from published value & any nested properties + var updatedPublishedVal = _formatPropertyValue(cultureVal.PublishedValue?.ToString(), onlyMissingKeys); + cultureVal.PublishedValue = updatedPublishedVal; + + // Remove keys from edited/draft value & any nested properties + var updatedEditedVal = _formatPropertyValue(cultureVal.EditedValue?.ToString(), onlyMissingKeys); + cultureVal.EditedValue = updatedEditedVal; + } + } + } + + /// + /// Unbinds from events + /// + /// + protected virtual void Dispose(bool disposing) + { + if (!_disposedValue) + { + if (disposing) + { + ContentService.Copying -= ContentService_Copying; + ContentService.Saving -= ContentService_Saving; + } + _disposedValue = true; + } + } + + /// + /// Unbinds from events + /// + public void Dispose() + { + Dispose(disposing: true); + GC.SuppressFinalize(this); + } + } +} diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index 1b1ee4fb28..ba344eca2a 100755 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -148,6 +148,7 @@ + diff --git a/src/Umbraco.Tests/PropertyEditors/BlockEditorComponentTests.cs b/src/Umbraco.Tests/PropertyEditors/BlockEditorComponentTests.cs new file mode 100644 index 0000000000..174ad256b3 --- /dev/null +++ b/src/Umbraco.Tests/PropertyEditors/BlockEditorComponentTests.cs @@ -0,0 +1,158 @@ +using Newtonsoft.Json; +using NUnit.Framework; +using System; +using Umbraco.Web.Compose; + +namespace Umbraco.Tests.PropertyEditors +{ + [TestFixture] + public class BlockEditorComponentTests + { + private readonly JsonSerializerSettings _serializerSettings = new JsonSerializerSettings + { + Formatting = Formatting.None, + NullValueHandling = NullValueHandling.Ignore, + + }; + + [Test] + public void No_Nesting() + { + var guids = new[] { Guid.NewGuid(), Guid.NewGuid() }; + var guidCounter = 0; + Func guidFactory = () => guids[guidCounter++]; + + var json = @"{ + ""layout"": + { + ""Umbraco.BlockList"": [ + { + ""contentUdi"": ""umb://element/036ce82586a64dfba2d523a99ed80f58"" + }, + { + ""contentUdi"": ""umb://element/48288c21a38a40ef82deb3eda90a58f6"" + } + ] + }, + ""contentData"": [ + { + ""contentTypeKey"": ""d6ce4a86-91a2-45b3-a99c-8691fc1fb020"", + ""udi"": ""umb://element/036ce82586a64dfba2d523a99ed80f58"", + ""featureName"": ""Hello"", + ""featureDetails"": ""World"" + }, + { + ""contentTypeKey"": ""d6ce4a86-91a2-45b3-a99c-8691fc1fb020"", + ""udi"": ""umb://element/48288c21a38a40ef82deb3eda90a58f6"", + ""featureName"": ""Another"", + ""featureDetails"": ""Feature"" + } + ], + ""settingsData"": [] +}"; + + + var expected = json + .Replace("036ce82586a64dfba2d523a99ed80f58", guids[0].ToString("N")) + .Replace("48288c21a38a40ef82deb3eda90a58f6", guids[1].ToString("N")); + + var component = new BlockEditorComponent(); + var result = component.CreateNestedContentKeys(json, false, guidFactory); + + var expectedJson = JsonConvert.DeserializeObject(expected, _serializerSettings).ToString(); + var resultJson = JsonConvert.DeserializeObject(result, _serializerSettings).ToString(); + Console.WriteLine(expectedJson); + Console.WriteLine(resultJson); + Assert.AreEqual(expectedJson, resultJson); + } + + [Test] + public void One_Level_Nesting_Escaped() + { + var guids = new[] { Guid.NewGuid(), Guid.NewGuid(), Guid.NewGuid(), Guid.NewGuid() }; + var guidCounter = 0; + Func guidFactory = () => guids[guidCounter++]; + + // we need to ensure the escaped json is consistent with how it will be re-escaped after parsing + // and this is how to do that, the result will also include quotes around it. + var innerJson = JsonConvert.DeserializeObject(@"{ + ""layout"": + { + ""Umbraco.BlockList"": [ + { + ""contentUdi"": ""umb://element/4C44CE6B3A5C4F5F8F15E3DC24819A9E"" + }, + { + ""contentUdi"": ""umb://element/A062C06D6B0B44AC892B35D90309C7F8"" + } + ] + }, + ""contentData"": [ + { + ""contentTypeKey"": ""d6ce4a86-91a2-45b3-a99c-8691fc1fb020"", + ""udi"": ""umb://element/4C44CE6B3A5C4F5F8F15E3DC24819A9E"", + ""featureName"": ""Hello"", + ""featureDetails"": ""World"" + }, + { + ""contentTypeKey"": ""d6ce4a86-91a2-45b3-a99c-8691fc1fb020"", + ""udi"": ""umb://element/A062C06D6B0B44AC892B35D90309C7F8"", + ""featureName"": ""Another"", + ""featureDetails"": ""Feature"" + } + ], + ""settingsData"": [] +}", _serializerSettings); + + var serializedInnerJson = JsonConvert.SerializeObject(innerJson, _serializerSettings); + + var subJsonEscaped = JsonConvert.ToString(serializedInnerJson); + + var json = @"{ + ""layout"": + { + ""Umbraco.BlockList"": [ + { + ""contentUdi"": ""umb://element/036ce82586a64dfba2d523a99ed80f58"" + }, + { + ""contentUdi"": ""umb://element/48288c21a38a40ef82deb3eda90a58f6"" + } + ] + }, + ""contentData"": [ + { + ""contentTypeKey"": ""d6ce4a86-91a2-45b3-a99c-8691fc1fb020"", + ""udi"": ""umb://element/036ce82586a64dfba2d523a99ed80f58"", + ""featureName"": ""Hello"", + ""featureDetails"": ""World"", + ""subFeatures"": " + subJsonEscaped + @" + }, + { + ""contentTypeKey"": ""d6ce4a86-91a2-45b3-a99c-8691fc1fb020"", + ""udi"": ""umb://element/48288c21a38a40ef82deb3eda90a58f6"", + ""featureName"": ""Another"", + ""featureDetails"": ""Feature"" + } + ], + ""settingsData"": [] +}"; + + var expected = json + .Replace("036ce82586a64dfba2d523a99ed80f58", guids[0].ToString("N")) + .Replace("48288c21a38a40ef82deb3eda90a58f6", guids[1].ToString("N")) + .Replace("4C44CE6B3A5C4F5F8F15E3DC24819A9E", guids[2].ToString("N")) + .Replace("A062C06D6B0B44AC892B35D90309C7F8", guids[3].ToString("N")); + + var component = new BlockEditorComponent(); + var result = component.CreateNestedContentKeys(json, false, guidFactory); + + var expectedJson = JsonConvert.DeserializeObject(expected, _serializerSettings).ToString(); + var resultJson = JsonConvert.DeserializeObject(result, _serializerSettings).ToString(); + Console.WriteLine(expectedJson); + Console.WriteLine(resultJson); + Assert.AreEqual(expectedJson, resultJson); + } + + } +} diff --git a/src/Umbraco.Tests/PropertyEditors/NestedContentPropertyComponentTests.cs b/src/Umbraco.Tests/PropertyEditors/NestedContentPropertyComponentTests.cs index 1b83c048d2..5b7e220123 100644 --- a/src/Umbraco.Tests/PropertyEditors/NestedContentPropertyComponentTests.cs +++ b/src/Umbraco.Tests/PropertyEditors/NestedContentPropertyComponentTests.cs @@ -9,6 +9,7 @@ using Umbraco.Web.Compose; namespace Umbraco.Tests.PropertyEditors { + [TestFixture] public class NestedContentPropertyComponentTests { diff --git a/src/Umbraco.Tests/Umbraco.Tests.csproj b/src/Umbraco.Tests/Umbraco.Tests.csproj index f803f1cd1d..004945bb46 100644 --- a/src/Umbraco.Tests/Umbraco.Tests.csproj +++ b/src/Umbraco.Tests/Umbraco.Tests.csproj @@ -147,6 +147,7 @@ + diff --git a/src/Umbraco.Web/Compose/BlockEditorComponent.cs b/src/Umbraco.Web/Compose/BlockEditorComponent.cs new file mode 100644 index 0000000000..43962533d6 --- /dev/null +++ b/src/Umbraco.Web/Compose/BlockEditorComponent.cs @@ -0,0 +1,187 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using Umbraco.Core; +using Umbraco.Core.Composing; +using Umbraco.Core.Events; +using Umbraco.Core.Models; +using Umbraco.Core.Models.Blocks; +using Umbraco.Core.PropertyEditors; +using Umbraco.Core.Services; +using Umbraco.Core.Services.Implement; +using Umbraco.Web.PropertyEditors; + +namespace Umbraco.Web.Compose +{ + + + /// + /// A component for Block editors used to bind to events + /// + public class BlockEditorComponent : IComponent + { + private ComplexPropertyEditorContentEventHandler _handler; + private readonly BlockListEditorDataConverter _converter = new BlockListEditorDataConverter(); + + public void Initialize() + { + _handler = new ComplexPropertyEditorContentEventHandler( + Constants.PropertyEditors.Aliases.BlockList, + CreateNestedContentKeys); + } + + public void Terminate() => _handler?.Dispose(); + + private string CreateNestedContentKeys(string rawJson, bool onlyMissingKeys) => CreateNestedContentKeys(rawJson, onlyMissingKeys, null); + + // internal for tests + internal string CreateNestedContentKeys(string rawJson, bool onlyMissingKeys, Func createGuid = null, JsonSerializerSettings serializerSettings = null) + { + // used so we can test nicely + if (createGuid == null) + createGuid = () => Guid.NewGuid(); + + if (string.IsNullOrWhiteSpace(rawJson) || !rawJson.DetectIsJson()) + return rawJson; + + // Parse JSON + var blockListValue = _converter.Deserialize(rawJson); + + UpdateBlockListRecursively(blockListValue, onlyMissingKeys, createGuid, serializerSettings); + + return JsonConvert.SerializeObject(blockListValue.BlockValue, serializerSettings); + } + + private void UpdateBlockListRecursively(BlockEditorData blockListData, bool onlyMissingKeys, Func createGuid, JsonSerializerSettings serializerSettings) + { + var oldToNew = new Dictionary(); + MapOldToNewUdis(oldToNew, blockListData.BlockValue.ContentData, onlyMissingKeys, createGuid); + MapOldToNewUdis(oldToNew, blockListData.BlockValue.SettingsData, onlyMissingKeys, createGuid); + + for (var i = 0; i < blockListData.References.Count; i++) + { + var reference = blockListData.References[i]; + var hasContentMap = oldToNew.TryGetValue(reference.ContentUdi, out var contentMap); + Udi settingsMap = null; + var hasSettingsMap = reference.SettingsUdi != null && oldToNew.TryGetValue(reference.SettingsUdi, out settingsMap); + + if (hasContentMap) + { + // replace the reference + blockListData.References.RemoveAt(i); + blockListData.References.Insert(i, new ContentAndSettingsReference(contentMap, hasSettingsMap ? settingsMap : null)); + } + } + + // build the layout with the new UDIs + var layout = (JArray)blockListData.Layout; + layout.Clear(); + foreach (var reference in blockListData.References) + { + layout.Add(JObject.FromObject(new BlockListLayoutItem + { + ContentUdi = reference.ContentUdi, + SettingsUdi = reference.SettingsUdi + })); + } + + + RecursePropertyValues(blockListData.BlockValue.ContentData, onlyMissingKeys, createGuid, serializerSettings); + RecursePropertyValues(blockListData.BlockValue.SettingsData, onlyMissingKeys, createGuid, serializerSettings); + } + + private void RecursePropertyValues(IEnumerable blockData, bool onlyMissingKeys, Func createGuid, JsonSerializerSettings serializerSettings) + { + foreach (var data in blockData) + { + // check if we need to recurse (make a copy of the dictionary since it will be modified) + foreach (var propertyAliasToBlockItemData in new Dictionary(data.RawPropertyValues)) + { + var asString = propertyAliasToBlockItemData.Value?.ToString(); + + //// if this is a nested block list + //if (propertyAliasToBlockItemData.Value.PropertyType.PropertyEditorAlias == Constants.PropertyEditors.Aliases.BlockList) + //{ + // // recurse + // var blockListValue = _converter.Deserialize(asString); + // UpdateBlockListRecursively(blockListValue, onlyMissingKeys, createGuid); + // // set new value + // data.RawPropertyValues[propertyAliasToBlockItemData.Key] = JsonConvert.SerializeObject(blockListValue.BlockValue); + //} + + if (asString != null && asString.DetectIsJson()) + { + // this gets a little ugly because there could be some other complex editor that contains another block editor + // and since we would have no idea how to parse that, all we can do is try JSON Path to find another block editor + // of our type + var json = JToken.Parse(asString); + + // select all tokens (flatten) + var allProperties = json.SelectTokens("$..*").Select(x => x.Parent as JProperty).WhereNotNull().ToList(); + foreach (var prop in allProperties) + { + if (prop.Name == Constants.PropertyEditors.Aliases.BlockList) + { + // get it's parent 'layout' and it's parent's container + var layout = prop.Parent?.Parent as JProperty; + if (layout != null && layout.Parent is JObject layoutJson) + { + // recurse + var blockListValue = _converter.ConvertFrom(layoutJson); + UpdateBlockListRecursively(blockListValue, onlyMissingKeys, createGuid, serializerSettings); + + // set new value + if (layoutJson.Parent != null) + { + // we can replace the sub string + layoutJson.Replace(JsonConvert.SerializeObject(blockListValue.BlockValue, serializerSettings)); + } + else + { + // this was the root string + data.RawPropertyValues[propertyAliasToBlockItemData.Key] = JsonConvert.SerializeObject(blockListValue.BlockValue, serializerSettings); + } + } + } + else if (prop.Name != "layout" && prop.Name != "contentData" && prop.Name != "settingsData" && prop.Name != "contentTypeKey") + { + // this is an arbitrary property that could contain a nested complex editor + var propVal = prop.Value?.ToString(); + // check if this might contain a nested Block Editor + if (!propVal.IsNullOrWhiteSpace() && propVal.DetectIsJson() && propVal.InvariantContains(Constants.PropertyEditors.Aliases.BlockList)) + { + if (_converter.TryDeserialize(propVal, out var nestedBlockData)) + { + // recurse + UpdateBlockListRecursively(nestedBlockData, onlyMissingKeys, createGuid, serializerSettings); + // set the value to the updated one + prop.Value = JsonConvert.SerializeObject(nestedBlockData.BlockValue, serializerSettings); + } + } + } + } + } + } + } + } + + private void MapOldToNewUdis(Dictionary oldToNew, IEnumerable blockData, bool onlyMissingKeys, Func createGuid) + { + foreach (var data in blockData) + { + if (data.Udi == null) + throw new InvalidOperationException("Block data cannot contain a null UDI"); + + // replace the UDIs + if (!onlyMissingKeys) + { + var newUdi = GuidUdi.Create(Constants.UdiEntityType.Element, createGuid()); + oldToNew[data.Udi] = newUdi; + data.Udi = newUdi; + } + } + } + } +} diff --git a/src/Umbraco.Web/Compose/BlockEditorComposer.cs b/src/Umbraco.Web/Compose/BlockEditorComposer.cs new file mode 100644 index 0000000000..debda081da --- /dev/null +++ b/src/Umbraco.Web/Compose/BlockEditorComposer.cs @@ -0,0 +1,12 @@ +using Umbraco.Core; +using Umbraco.Core.Composing; + +namespace Umbraco.Web.Compose +{ + /// + /// A composer for Block editors to run a component + /// + [RuntimeLevel(MinLevel = RuntimeLevel.Run)] + public class BlockEditorComposer : ComponentComposer, ICoreComposer + { } +} diff --git a/src/Umbraco.Web/Compose/NestedContentPropertyComponent.cs b/src/Umbraco.Web/Compose/NestedContentPropertyComponent.cs index 5794a2734e..9414a1a836 100644 --- a/src/Umbraco.Web/Compose/NestedContentPropertyComponent.cs +++ b/src/Umbraco.Web/Compose/NestedContentPropertyComponent.cs @@ -6,67 +6,32 @@ using Umbraco.Core; using Umbraco.Core.Composing; using Umbraco.Core.Events; using Umbraco.Core.Models; +using Umbraco.Core.PropertyEditors; using Umbraco.Core.Services; using Umbraco.Core.Services.Implement; using Umbraco.Web.PropertyEditors; namespace Umbraco.Web.Compose { + + /// + /// A component for NestedContent used to bind to events + /// public class NestedContentPropertyComponent : IComponent { + private ComplexPropertyEditorContentEventHandler _handler; + public void Initialize() { - ContentService.Copying += ContentService_Copying; - ContentService.Saving += ContentService_Saving; + _handler = new ComplexPropertyEditorContentEventHandler( + Constants.PropertyEditors.Aliases.NestedContent, + CreateNestedContentKeys); } - private void ContentService_Copying(IContentService sender, CopyEventArgs e) - { - // When a content node contains nested content property - // Check if the copied node contains a nested content - var nestedContentProps = e.Copy.Properties.Where(x => x.PropertyType.PropertyEditorAlias == Constants.PropertyEditors.Aliases.NestedContent); - UpdateNestedContentProperties(nestedContentProps, false); - } + public void Terminate() => _handler?.Dispose(); - private void ContentService_Saving(IContentService sender, ContentSavingEventArgs e) - { - // One or more content nodes could be saved in a bulk publish - foreach (var entity in e.SavedEntities) - { - // When a content node contains nested content property - // Check if the copied node contains a nested content - var nestedContentProps = entity.Properties.Where(x => x.PropertyType.PropertyEditorAlias == Constants.PropertyEditors.Aliases.NestedContent); - UpdateNestedContentProperties(nestedContentProps, true); - } - } + private string CreateNestedContentKeys(string rawJson, bool onlyMissingKeys) => CreateNestedContentKeys(rawJson, onlyMissingKeys, null); - public void Terminate() - { - ContentService.Copying -= ContentService_Copying; - ContentService.Saving -= ContentService_Saving; - } - - private void UpdateNestedContentProperties(IEnumerable nestedContentProps, bool onlyMissingKeys) - { - // Each NC Property on a doctype - foreach (var nestedContentProp in nestedContentProps) - { - // A NC Prop may have one or more values due to cultures - var propVals = nestedContentProp.Values; - foreach (var cultureVal in propVals) - { - // Remove keys from published value & any nested NC's - var updatedPublishedVal = CreateNestedContentKeys(cultureVal.PublishedValue?.ToString(), onlyMissingKeys); - cultureVal.PublishedValue = updatedPublishedVal; - - // Remove keys from edited/draft value & any nested NC's - var updatedEditedVal = CreateNestedContentKeys(cultureVal.EditedValue?.ToString(), onlyMissingKeys); - cultureVal.EditedValue = updatedEditedVal; - } - } - } - - // internal for tests internal string CreateNestedContentKeys(string rawJson, bool onlyMissingKeys, Func createGuid = null) { @@ -98,7 +63,6 @@ namespace Umbraco.Web.Compose { // get it's sibling 'key' property var ncKeyVal = prop.Parent["key"] as JValue; - // TODO: This bool seems odd, if the key is null, shouldn't we fill it in regardless of onlyMissingKeys? if ((onlyMissingKeys && ncKeyVal == null) || (!onlyMissingKeys && ncKeyVal != null)) { // create or replace diff --git a/src/Umbraco.Web/Compose/NestedContentPropertyComposer.cs b/src/Umbraco.Web/Compose/NestedContentPropertyComposer.cs index 4c9d9dee1c..8e4cfbfffc 100644 --- a/src/Umbraco.Web/Compose/NestedContentPropertyComposer.cs +++ b/src/Umbraco.Web/Compose/NestedContentPropertyComposer.cs @@ -3,6 +3,9 @@ using Umbraco.Core.Composing; namespace Umbraco.Web.Compose { + /// + /// A composer for nested content to run a component + /// [RuntimeLevel(MinLevel = RuntimeLevel.Run)] public class NestedContentPropertyComposer : ComponentComposer, ICoreComposer { } diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index a0ffacf8f3..5e1176cdc6 100644 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -130,6 +130,8 @@ + + From 56dd39bb1a05742e3a0a543c79364bc94b9f8b6a Mon Sep 17 00:00:00 2001 From: Shannon Date: Tue, 8 Sep 2020 02:09:05 +1000 Subject: [PATCH 24/31] cleanup --- .../BlockEditorComponentTests.cs | 5 ++-- .../Compose/BlockEditorComponent.cs | 23 ++++--------------- 2 files changed, 7 insertions(+), 21 deletions(-) diff --git a/src/Umbraco.Tests/PropertyEditors/BlockEditorComponentTests.cs b/src/Umbraco.Tests/PropertyEditors/BlockEditorComponentTests.cs index 174ad256b3..aafc05a640 100644 --- a/src/Umbraco.Tests/PropertyEditors/BlockEditorComponentTests.cs +++ b/src/Umbraco.Tests/PropertyEditors/BlockEditorComponentTests.cs @@ -57,7 +57,7 @@ namespace Umbraco.Tests.PropertyEditors .Replace("48288c21a38a40ef82deb3eda90a58f6", guids[1].ToString("N")); var component = new BlockEditorComponent(); - var result = component.CreateNestedContentKeys(json, false, guidFactory); + var result = component.ReplaceBlockListUdis(json, false, guidFactory); var expectedJson = JsonConvert.DeserializeObject(expected, _serializerSettings).ToString(); var resultJson = JsonConvert.DeserializeObject(result, _serializerSettings).ToString(); @@ -82,6 +82,7 @@ namespace Umbraco.Tests.PropertyEditors { ""contentUdi"": ""umb://element/4C44CE6B3A5C4F5F8F15E3DC24819A9E"" }, + { ""contentUdi"": ""umb://element/A062C06D6B0B44AC892B35D90309C7F8"" } @@ -145,7 +146,7 @@ namespace Umbraco.Tests.PropertyEditors .Replace("A062C06D6B0B44AC892B35D90309C7F8", guids[3].ToString("N")); var component = new BlockEditorComponent(); - var result = component.CreateNestedContentKeys(json, false, guidFactory); + var result = component.ReplaceBlockListUdis(json, false, guidFactory); var expectedJson = JsonConvert.DeserializeObject(expected, _serializerSettings).ToString(); var resultJson = JsonConvert.DeserializeObject(result, _serializerSettings).ToString(); diff --git a/src/Umbraco.Web/Compose/BlockEditorComponent.cs b/src/Umbraco.Web/Compose/BlockEditorComponent.cs index 43962533d6..ecd94ff2fb 100644 --- a/src/Umbraco.Web/Compose/BlockEditorComponent.cs +++ b/src/Umbraco.Web/Compose/BlockEditorComponent.cs @@ -5,13 +5,8 @@ using Newtonsoft.Json; using Newtonsoft.Json.Linq; using Umbraco.Core; using Umbraco.Core.Composing; -using Umbraco.Core.Events; -using Umbraco.Core.Models; using Umbraco.Core.Models.Blocks; using Umbraco.Core.PropertyEditors; -using Umbraco.Core.Services; -using Umbraco.Core.Services.Implement; -using Umbraco.Web.PropertyEditors; namespace Umbraco.Web.Compose { @@ -29,15 +24,15 @@ namespace Umbraco.Web.Compose { _handler = new ComplexPropertyEditorContentEventHandler( Constants.PropertyEditors.Aliases.BlockList, - CreateNestedContentKeys); + ReplaceBlockListUdis); } public void Terminate() => _handler?.Dispose(); - private string CreateNestedContentKeys(string rawJson, bool onlyMissingKeys) => CreateNestedContentKeys(rawJson, onlyMissingKeys, null); + private string ReplaceBlockListUdis(string rawJson, bool onlyMissingUdis) => ReplaceBlockListUdis(rawJson, onlyMissingUdis, null); // internal for tests - internal string CreateNestedContentKeys(string rawJson, bool onlyMissingKeys, Func createGuid = null, JsonSerializerSettings serializerSettings = null) + internal string ReplaceBlockListUdis(string rawJson, bool onlyMissingUdis, Func createGuid = null, JsonSerializerSettings serializerSettings = null) { // used so we can test nicely if (createGuid == null) @@ -49,7 +44,7 @@ namespace Umbraco.Web.Compose // Parse JSON var blockListValue = _converter.Deserialize(rawJson); - UpdateBlockListRecursively(blockListValue, onlyMissingKeys, createGuid, serializerSettings); + UpdateBlockListRecursively(blockListValue, onlyMissingUdis, createGuid, serializerSettings); return JsonConvert.SerializeObject(blockListValue.BlockValue, serializerSettings); } @@ -101,16 +96,6 @@ namespace Umbraco.Web.Compose { var asString = propertyAliasToBlockItemData.Value?.ToString(); - //// if this is a nested block list - //if (propertyAliasToBlockItemData.Value.PropertyType.PropertyEditorAlias == Constants.PropertyEditors.Aliases.BlockList) - //{ - // // recurse - // var blockListValue = _converter.Deserialize(asString); - // UpdateBlockListRecursively(blockListValue, onlyMissingKeys, createGuid); - // // set new value - // data.RawPropertyValues[propertyAliasToBlockItemData.Key] = JsonConvert.SerializeObject(blockListValue.BlockValue); - //} - if (asString != null && asString.DetectIsJson()) { // this gets a little ugly because there could be some other complex editor that contains another block editor From 98bda6759eeff194536974cc39509630d88ffe1d Mon Sep 17 00:00:00 2001 From: Shannon Date: Tue, 8 Sep 2020 11:25:37 +1000 Subject: [PATCH 25/31] Adds more tests, cleans up tests --- .../BlockEditorComponentTests.cs | 318 ++++++++++++------ .../Compose/BlockEditorComponent.cs | 156 +++++---- .../Compose/NestedContentPropertyComponent.cs | 2 +- 3 files changed, 301 insertions(+), 175 deletions(-) diff --git a/src/Umbraco.Tests/PropertyEditors/BlockEditorComponentTests.cs b/src/Umbraco.Tests/PropertyEditors/BlockEditorComponentTests.cs index aafc05a640..bfd8b8c77b 100644 --- a/src/Umbraco.Tests/PropertyEditors/BlockEditorComponentTests.cs +++ b/src/Umbraco.Tests/PropertyEditors/BlockEditorComponentTests.cs @@ -1,6 +1,9 @@ using Newtonsoft.Json; using NUnit.Framework; using System; +using System.Collections.Generic; +using System.Linq; +using Umbraco.Core; using Umbraco.Web.Compose; namespace Umbraco.Tests.PropertyEditors @@ -15,52 +18,37 @@ namespace Umbraco.Tests.PropertyEditors }; + private const string _contentGuid1 = "036ce82586a64dfba2d523a99ed80f58"; + private const string _contentGuid2 = "48288c21a38a40ef82deb3eda90a58f6"; + private const string _settingsGuid1 = "ffd35c4e2eea4900abfa5611b67b2492"; + private const string _subContentGuid1 = "4c44ce6b3a5c4f5f8f15e3dc24819a9e"; + private const string _subContentGuid2 = "a062c06d6b0b44ac892b35d90309c7f8"; + private const string _subSettingsGuid1 = "4d998d980ffa4eee8afdc23c4abd6d29"; + + [Test] + public void Cannot_Have_Null_Udi() + { + var component = new BlockEditorComponent(); + var json = GetBlockListJson(null, string.Empty); + Assert.Throws(() => component.ReplaceBlockListUdis(json)); + } + [Test] public void No_Nesting() { - var guids = new[] { Guid.NewGuid(), Guid.NewGuid() }; + var guids = Enumerable.Range(0, 3).Select(x => Guid.NewGuid()).ToList(); var guidCounter = 0; Func guidFactory = () => guids[guidCounter++]; - var json = @"{ - ""layout"": - { - ""Umbraco.BlockList"": [ - { - ""contentUdi"": ""umb://element/036ce82586a64dfba2d523a99ed80f58"" - }, - { - ""contentUdi"": ""umb://element/48288c21a38a40ef82deb3eda90a58f6"" - } - ] - }, - ""contentData"": [ - { - ""contentTypeKey"": ""d6ce4a86-91a2-45b3-a99c-8691fc1fb020"", - ""udi"": ""umb://element/036ce82586a64dfba2d523a99ed80f58"", - ""featureName"": ""Hello"", - ""featureDetails"": ""World"" - }, - { - ""contentTypeKey"": ""d6ce4a86-91a2-45b3-a99c-8691fc1fb020"", - ""udi"": ""umb://element/48288c21a38a40ef82deb3eda90a58f6"", - ""featureName"": ""Another"", - ""featureDetails"": ""Feature"" - } - ], - ""settingsData"": [] -}"; + var json = GetBlockListJson(null); - - var expected = json - .Replace("036ce82586a64dfba2d523a99ed80f58", guids[0].ToString("N")) - .Replace("48288c21a38a40ef82deb3eda90a58f6", guids[1].ToString("N")); + var expected = ReplaceGuids(json, guids, _contentGuid1, _contentGuid2, _settingsGuid1); var component = new BlockEditorComponent(); - var result = component.ReplaceBlockListUdis(json, false, guidFactory); + var result = component.ReplaceBlockListUdis(json, guidFactory); - var expectedJson = JsonConvert.DeserializeObject(expected, _serializerSettings).ToString(); - var resultJson = JsonConvert.DeserializeObject(result, _serializerSettings).ToString(); + var expectedJson = JsonConvert.SerializeObject(JsonConvert.DeserializeObject(expected, _serializerSettings), _serializerSettings); + var resultJson = JsonConvert.SerializeObject(JsonConvert.DeserializeObject(result, _serializerSettings), _serializerSettings); Console.WriteLine(expectedJson); Console.WriteLine(resultJson); Assert.AreEqual(expectedJson, resultJson); @@ -69,91 +57,205 @@ namespace Umbraco.Tests.PropertyEditors [Test] public void One_Level_Nesting_Escaped() { - var guids = new[] { Guid.NewGuid(), Guid.NewGuid(), Guid.NewGuid(), Guid.NewGuid() }; + var guids = Enumerable.Range(0, 6).Select(x => Guid.NewGuid()).ToList(); + var guidCounter = 0; Func guidFactory = () => guids[guidCounter++]; + var innerJson = GetBlockListJson(null, _subContentGuid1, _subContentGuid2, _subSettingsGuid1); + // we need to ensure the escaped json is consistent with how it will be re-escaped after parsing // and this is how to do that, the result will also include quotes around it. - var innerJson = JsonConvert.DeserializeObject(@"{ - ""layout"": - { - ""Umbraco.BlockList"": [ - { - ""contentUdi"": ""umb://element/4C44CE6B3A5C4F5F8F15E3DC24819A9E"" - }, + var innerJsonEscaped = JsonConvert.ToString(innerJson); - { - ""contentUdi"": ""umb://element/A062C06D6B0B44AC892B35D90309C7F8"" - } - ] - }, - ""contentData"": [ - { - ""contentTypeKey"": ""d6ce4a86-91a2-45b3-a99c-8691fc1fb020"", - ""udi"": ""umb://element/4C44CE6B3A5C4F5F8F15E3DC24819A9E"", - ""featureName"": ""Hello"", - ""featureDetails"": ""World"" - }, - { - ""contentTypeKey"": ""d6ce4a86-91a2-45b3-a99c-8691fc1fb020"", - ""udi"": ""umb://element/A062C06D6B0B44AC892B35D90309C7F8"", - ""featureName"": ""Another"", - ""featureDetails"": ""Feature"" - } - ], - ""settingsData"": [] -}", _serializerSettings); - - var serializedInnerJson = JsonConvert.SerializeObject(innerJson, _serializerSettings); - - var subJsonEscaped = JsonConvert.ToString(serializedInnerJson); - - var json = @"{ - ""layout"": - { - ""Umbraco.BlockList"": [ - { - ""contentUdi"": ""umb://element/036ce82586a64dfba2d523a99ed80f58"" - }, - { - ""contentUdi"": ""umb://element/48288c21a38a40ef82deb3eda90a58f6"" - } - ] - }, - ""contentData"": [ - { - ""contentTypeKey"": ""d6ce4a86-91a2-45b3-a99c-8691fc1fb020"", - ""udi"": ""umb://element/036ce82586a64dfba2d523a99ed80f58"", - ""featureName"": ""Hello"", - ""featureDetails"": ""World"", - ""subFeatures"": " + subJsonEscaped + @" - }, - { - ""contentTypeKey"": ""d6ce4a86-91a2-45b3-a99c-8691fc1fb020"", - ""udi"": ""umb://element/48288c21a38a40ef82deb3eda90a58f6"", - ""featureName"": ""Another"", - ""featureDetails"": ""Feature"" - } - ], - ""settingsData"": [] -}"; - - var expected = json - .Replace("036ce82586a64dfba2d523a99ed80f58", guids[0].ToString("N")) - .Replace("48288c21a38a40ef82deb3eda90a58f6", guids[1].ToString("N")) - .Replace("4C44CE6B3A5C4F5F8F15E3DC24819A9E", guids[2].ToString("N")) - .Replace("A062C06D6B0B44AC892B35D90309C7F8", guids[3].ToString("N")); + // get the json with the subFeatures as escaped + var json = GetBlockListJson(innerJsonEscaped); var component = new BlockEditorComponent(); - var result = component.ReplaceBlockListUdis(json, false, guidFactory); + var result = component.ReplaceBlockListUdis(json, guidFactory); - var expectedJson = JsonConvert.DeserializeObject(expected, _serializerSettings).ToString(); - var resultJson = JsonConvert.DeserializeObject(result, _serializerSettings).ToString(); + // the expected result is that the subFeatures data is no longer escaped + var expected = ReplaceGuids(GetBlockListJson(innerJson), guids, + _contentGuid1, _contentGuid2, _settingsGuid1, + _subContentGuid1, _subContentGuid2, _subSettingsGuid1); + + var expectedJson = JsonConvert.SerializeObject(JsonConvert.DeserializeObject(expected, _serializerSettings), _serializerSettings); + var resultJson = JsonConvert.SerializeObject(JsonConvert.DeserializeObject(result, _serializerSettings), _serializerSettings); Console.WriteLine(expectedJson); Console.WriteLine(resultJson); Assert.AreEqual(expectedJson, resultJson); } + [Test] + public void One_Level_Nesting_Unescaped() + { + var guids = Enumerable.Range(0, 6).Select(x => Guid.NewGuid()).ToList(); + var guidCounter = 0; + Func guidFactory = () => guids[guidCounter++]; + + // nested blocks without property value escaping used in the conversion + var innerJson = GetBlockListJson(null, _subContentGuid1, _subContentGuid2, _subSettingsGuid1); + + // get the json with the subFeatures as unescaped + var json = GetBlockListJson(innerJson); + + var expected = ReplaceGuids(GetBlockListJson(innerJson), guids, + _contentGuid1, _contentGuid2, _settingsGuid1, + _subContentGuid1, _subContentGuid2, _subSettingsGuid1); + + var component = new BlockEditorComponent(); + var result = component.ReplaceBlockListUdis(json, guidFactory); + + var expectedJson = JsonConvert.SerializeObject(JsonConvert.DeserializeObject(expected, _serializerSettings), _serializerSettings); + var resultJson = JsonConvert.SerializeObject(JsonConvert.DeserializeObject(result, _serializerSettings), _serializerSettings); + Console.WriteLine(expectedJson); + Console.WriteLine(resultJson); + Assert.AreEqual(expectedJson, resultJson); + } + + [Test] + public void Nested_In_Complex_Editor_Escaped() + { + var guids = Enumerable.Range(0, 6).Select(x => Guid.NewGuid()).ToList(); + var guidCounter = 0; + Func guidFactory = () => guids[guidCounter++]; + + var innerJson = GetBlockListJson(null, _subContentGuid1, _subContentGuid2, _subSettingsGuid1); + + // we need to ensure the escaped json is consistent with how it will be re-escaped after parsing + // and this is how to do that, the result will also include quotes around it. + var innerJsonEscaped = JsonConvert.ToString(innerJson); + + // Complex editor such as the grid + var complexEditorJsonEscaped = GetGridJson(innerJsonEscaped); + + var json = GetBlockListJson(complexEditorJsonEscaped); + + var component = new BlockEditorComponent(); + var result = component.ReplaceBlockListUdis(json, guidFactory); + + // the expected result is that the subFeatures data is no longer escaped + var expected = ReplaceGuids(GetBlockListJson(GetGridJson(innerJson)), guids, + _contentGuid1, _contentGuid2, _settingsGuid1, + _subContentGuid1, _subContentGuid2, _subSettingsGuid1); + + var expectedJson = JsonConvert.SerializeObject(JsonConvert.DeserializeObject(expected, _serializerSettings), _serializerSettings); + var resultJson = JsonConvert.SerializeObject(JsonConvert.DeserializeObject(result, _serializerSettings), _serializerSettings); + Console.WriteLine(expectedJson); + Console.WriteLine(resultJson); + Assert.AreEqual(expectedJson, resultJson); + } + + private string GetBlockListJson(string subFeatures, + string contentGuid1 = _contentGuid1, + string contentGuid2 = _contentGuid2, + string settingsGuid1 = _settingsGuid1) + { + return @"{ + ""layout"": + { + ""Umbraco.BlockList"": [ + { + ""contentUdi"": """ + (contentGuid1.IsNullOrWhiteSpace() ? string.Empty : GuidUdi.Create(Constants.UdiEntityType.Element, Guid.Parse(contentGuid1)).ToString()) + @""" + }, + { + ""contentUdi"": ""umb://element/" + contentGuid2 + @""", + ""settingsUdi"": ""umb://element/" + settingsGuid1 + @""" + } + ] + }, + ""contentData"": [ + { + ""contentTypeKey"": ""d6ce4a86-91a2-45b3-a99c-8691fc1fb020"", + ""udi"": """ + (contentGuid1.IsNullOrWhiteSpace() ? string.Empty : GuidUdi.Create(Constants.UdiEntityType.Element, Guid.Parse(contentGuid1)).ToString()) + @""", + ""featureName"": ""Hello"", + ""featureDetails"": ""World"" + }, + { + ""contentTypeKey"": ""d6ce4a86-91a2-45b3-a99c-8691fc1fb020"", + ""udi"": ""umb://element/" + contentGuid2 + @""", + ""featureName"": ""Another"", + ""featureDetails"": ""Feature""" + (subFeatures == null ? string.Empty : (@", ""subFeatures"": " + subFeatures)) + @" + } + ], + ""settingsData"": [ + { + ""contentTypeKey"": ""d6ce4a86-91a2-45b3-a99c-8691fc1fb020"", + ""udi"": ""umb://element/" + settingsGuid1 + @""", + ""featureName"": ""Setting 1"", + ""featureDetails"": ""Setting 2"" + }, + ] +}"; + } + + private string GetGridJson(string subBlockList) + { + return @"{ + ""name"": ""1 column layout"", + ""sections"": [ + { + ""grid"": ""12"", + ""rows"": [ + { + ""name"": ""Article"", + ""id"": ""b4f6f651-0de3-ef46-e66a-464f4aaa9c57"", + ""areas"": [ + { + ""grid"": ""4"", + ""controls"": [ + { + ""value"": ""I am quote"", + ""editor"": { + ""alias"": ""quote"", + ""view"": ""textstring"" + }, + ""styles"": null, + ""config"": null + }], + ""styles"": null, + ""config"": null + }, + { + ""grid"": ""8"", + ""controls"": [ + { + ""value"": ""Header"", + ""editor"": { + ""alias"": ""headline"", + ""view"": ""textstring"" + }, + ""styles"": null, + ""config"": null + }, + { + ""value"": " + subBlockList + @", + ""editor"": { + ""alias"": ""madeUpNestedContent"", + ""view"": ""madeUpNestedContentInGrid"" + }, + ""styles"": null, + ""config"": null + }], + ""styles"": null, + ""config"": null + }], + ""styles"": null, + ""config"": null + }] + }] +}"; + } + + private string ReplaceGuids(string json, List newGuids, params string[] oldGuids) + { + for (var i = 0; i < oldGuids.Length; i++) + { + var old = oldGuids[i]; + json = json.Replace(old, newGuids[i].ToString("N")); + } + return json; + } + } } diff --git a/src/Umbraco.Web/Compose/BlockEditorComponent.cs b/src/Umbraco.Web/Compose/BlockEditorComponent.cs index ecd94ff2fb..0666ba4461 100644 --- a/src/Umbraco.Web/Compose/BlockEditorComponent.cs +++ b/src/Umbraco.Web/Compose/BlockEditorComponent.cs @@ -10,8 +10,6 @@ using Umbraco.Core.PropertyEditors; namespace Umbraco.Web.Compose { - - /// /// A component for Block editors used to bind to events /// @@ -29,10 +27,10 @@ namespace Umbraco.Web.Compose public void Terminate() => _handler?.Dispose(); - private string ReplaceBlockListUdis(string rawJson, bool onlyMissingUdis) => ReplaceBlockListUdis(rawJson, onlyMissingUdis, null); + private string ReplaceBlockListUdis(string rawJson, bool onlyMissingUdis) => ReplaceBlockListUdis(rawJson, null); // internal for tests - internal string ReplaceBlockListUdis(string rawJson, bool onlyMissingUdis, Func createGuid = null, JsonSerializerSettings serializerSettings = null) + internal string ReplaceBlockListUdis(string rawJson, Func createGuid = null) { // used so we can test nicely if (createGuid == null) @@ -42,18 +40,19 @@ namespace Umbraco.Web.Compose return rawJson; // Parse JSON + // This will throw a FormatException if there are null UDIs (expected) var blockListValue = _converter.Deserialize(rawJson); - UpdateBlockListRecursively(blockListValue, onlyMissingUdis, createGuid, serializerSettings); + UpdateBlockListRecursively(blockListValue, createGuid); - return JsonConvert.SerializeObject(blockListValue.BlockValue, serializerSettings); + return JsonConvert.SerializeObject(blockListValue.BlockValue); } - private void UpdateBlockListRecursively(BlockEditorData blockListData, bool onlyMissingKeys, Func createGuid, JsonSerializerSettings serializerSettings) + private void UpdateBlockListRecursively(BlockEditorData blockListData, Func createGuid) { var oldToNew = new Dictionary(); - MapOldToNewUdis(oldToNew, blockListData.BlockValue.ContentData, onlyMissingKeys, createGuid); - MapOldToNewUdis(oldToNew, blockListData.BlockValue.SettingsData, onlyMissingKeys, createGuid); + MapOldToNewUdis(oldToNew, blockListData.BlockValue.ContentData, createGuid); + MapOldToNewUdis(oldToNew, blockListData.BlockValue.SettingsData, createGuid); for (var i = 0; i < blockListData.References.Count; i++) { @@ -83,68 +82,39 @@ namespace Umbraco.Web.Compose } - RecursePropertyValues(blockListData.BlockValue.ContentData, onlyMissingKeys, createGuid, serializerSettings); - RecursePropertyValues(blockListData.BlockValue.SettingsData, onlyMissingKeys, createGuid, serializerSettings); + RecursePropertyValues(blockListData.BlockValue.ContentData, createGuid); + RecursePropertyValues(blockListData.BlockValue.SettingsData, createGuid); } - private void RecursePropertyValues(IEnumerable blockData, bool onlyMissingKeys, Func createGuid, JsonSerializerSettings serializerSettings) + private void RecursePropertyValues(IEnumerable blockData, Func createGuid) { foreach (var data in blockData) { // check if we need to recurse (make a copy of the dictionary since it will be modified) foreach (var propertyAliasToBlockItemData in new Dictionary(data.RawPropertyValues)) { - var asString = propertyAliasToBlockItemData.Value?.ToString(); - - if (asString != null && asString.DetectIsJson()) + if (propertyAliasToBlockItemData.Value is JToken jtoken) { - // this gets a little ugly because there could be some other complex editor that contains another block editor - // and since we would have no idea how to parse that, all we can do is try JSON Path to find another block editor - // of our type - var json = JToken.Parse(asString); - - // select all tokens (flatten) - var allProperties = json.SelectTokens("$..*").Select(x => x.Parent as JProperty).WhereNotNull().ToList(); - foreach (var prop in allProperties) + if (ProcessJToken(jtoken, createGuid, out var result)) { - if (prop.Name == Constants.PropertyEditors.Aliases.BlockList) - { - // get it's parent 'layout' and it's parent's container - var layout = prop.Parent?.Parent as JProperty; - if (layout != null && layout.Parent is JObject layoutJson) - { - // recurse - var blockListValue = _converter.ConvertFrom(layoutJson); - UpdateBlockListRecursively(blockListValue, onlyMissingKeys, createGuid, serializerSettings); + // need to re-save this back to the RawPropertyValues + data.RawPropertyValues[propertyAliasToBlockItemData.Key] = result; + } + } + else + { + var asString = propertyAliasToBlockItemData.Value?.ToString(); - // set new value - if (layoutJson.Parent != null) - { - // we can replace the sub string - layoutJson.Replace(JsonConvert.SerializeObject(blockListValue.BlockValue, serializerSettings)); - } - else - { - // this was the root string - data.RawPropertyValues[propertyAliasToBlockItemData.Key] = JsonConvert.SerializeObject(blockListValue.BlockValue, serializerSettings); - } - } - } - else if (prop.Name != "layout" && prop.Name != "contentData" && prop.Name != "settingsData" && prop.Name != "contentTypeKey") + if (asString != null && asString.DetectIsJson()) + { + // this gets a little ugly because there could be some other complex editor that contains another block editor + // and since we would have no idea how to parse that, all we can do is try JSON Path to find another block editor + // of our type + var json = JToken.Parse(asString); + if (ProcessJToken(json, createGuid, out var result)) { - // this is an arbitrary property that could contain a nested complex editor - var propVal = prop.Value?.ToString(); - // check if this might contain a nested Block Editor - if (!propVal.IsNullOrWhiteSpace() && propVal.DetectIsJson() && propVal.InvariantContains(Constants.PropertyEditors.Aliases.BlockList)) - { - if (_converter.TryDeserialize(propVal, out var nestedBlockData)) - { - // recurse - UpdateBlockListRecursively(nestedBlockData, onlyMissingKeys, createGuid, serializerSettings); - // set the value to the updated one - prop.Value = JsonConvert.SerializeObject(nestedBlockData.BlockValue, serializerSettings); - } - } + // need to re-save this back to the RawPropertyValues + data.RawPropertyValues[propertyAliasToBlockItemData.Key] = result; } } } @@ -152,20 +122,74 @@ namespace Umbraco.Web.Compose } } - private void MapOldToNewUdis(Dictionary oldToNew, IEnumerable blockData, bool onlyMissingKeys, Func createGuid) + private bool ProcessJToken(JToken json, Func createGuid, out JToken result) + { + var updated = false; + result = json; + + // select all tokens (flatten) + var allProperties = json.SelectTokens("$..*").Select(x => x.Parent as JProperty).WhereNotNull().ToList(); + foreach (var prop in allProperties) + { + if (prop.Name == Constants.PropertyEditors.Aliases.BlockList) + { + // get it's parent 'layout' and it's parent's container + var layout = prop.Parent?.Parent as JProperty; + if (layout != null && layout.Parent is JObject layoutJson) + { + // recurse + var blockListValue = _converter.ConvertFrom(layoutJson); + UpdateBlockListRecursively(blockListValue, createGuid); + + // set new value + if (layoutJson.Parent != null) + { + // we can replace the object + layoutJson.Replace(JObject.FromObject(blockListValue.BlockValue)); + updated = true; + } + else + { + // if there is no parent it means that this json property was the root, in which case we just return + result = JObject.FromObject(blockListValue.BlockValue); + return true; + } + } + } + else if (prop.Name != "layout" && prop.Name != "contentData" && prop.Name != "settingsData" && prop.Name != "contentTypeKey") + { + // this is an arbitrary property that could contain a nested complex editor + var propVal = prop.Value?.ToString(); + // check if this might contain a nested Block Editor + if (!propVal.IsNullOrWhiteSpace() && propVal.DetectIsJson() && propVal.InvariantContains(Constants.PropertyEditors.Aliases.BlockList)) + { + if (_converter.TryDeserialize(propVal, out var nestedBlockData)) + { + // recurse + UpdateBlockListRecursively(nestedBlockData, createGuid); + // set the value to the updated one + prop.Value = JObject.FromObject(nestedBlockData.BlockValue); + updated = true; + } + } + } + } + + return updated; + } + + private void MapOldToNewUdis(Dictionary oldToNew, IEnumerable blockData, Func createGuid) { foreach (var data in blockData) { + // This should never happen since a FormatException will be thrown if one is empty but we'll keep this here if (data.Udi == null) throw new InvalidOperationException("Block data cannot contain a null UDI"); // replace the UDIs - if (!onlyMissingKeys) - { - var newUdi = GuidUdi.Create(Constants.UdiEntityType.Element, createGuid()); - oldToNew[data.Udi] = newUdi; - data.Udi = newUdi; - } + var newUdi = GuidUdi.Create(Constants.UdiEntityType.Element, createGuid()); + oldToNew[data.Udi] = newUdi; + data.Udi = newUdi; } } } diff --git a/src/Umbraco.Web/Compose/NestedContentPropertyComponent.cs b/src/Umbraco.Web/Compose/NestedContentPropertyComponent.cs index 9414a1a836..633e814bd9 100644 --- a/src/Umbraco.Web/Compose/NestedContentPropertyComponent.cs +++ b/src/Umbraco.Web/Compose/NestedContentPropertyComponent.cs @@ -42,7 +42,7 @@ namespace Umbraco.Web.Compose if (string.IsNullOrWhiteSpace(rawJson) || !rawJson.DetectIsJson()) return rawJson; - // Parse JSON + // Parse JSON var complexEditorValue = JToken.Parse(rawJson); UpdateNestedContentKeysRecursively(complexEditorValue, onlyMissingKeys, createGuid); From 6e2576fb0d8e65d2908e0170b968eccc0e8621ae Mon Sep 17 00:00:00 2001 From: Shannon Date: Tue, 8 Sep 2020 11:47:31 +1000 Subject: [PATCH 26/31] Ensure the component runs and don't replace UDIs on saving since its not necessary --- src/Umbraco.Web/Compose/BlockEditorComponent.cs | 8 +++++++- src/Umbraco.Web/Compose/BlockEditorComposer.cs | 2 +- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Web/Compose/BlockEditorComponent.cs b/src/Umbraco.Web/Compose/BlockEditorComponent.cs index 0666ba4461..a8b4cfb8ca 100644 --- a/src/Umbraco.Web/Compose/BlockEditorComponent.cs +++ b/src/Umbraco.Web/Compose/BlockEditorComponent.cs @@ -27,7 +27,13 @@ namespace Umbraco.Web.Compose public void Terminate() => _handler?.Dispose(); - private string ReplaceBlockListUdis(string rawJson, bool onlyMissingUdis) => ReplaceBlockListUdis(rawJson, null); + private string ReplaceBlockListUdis(string rawJson, bool onlyMissingUdis) + { + // the block editor doesn't ever have missing UDIs so when this is true there's nothing to process + if (onlyMissingUdis) return rawJson; + + return ReplaceBlockListUdis(rawJson, null); + } // internal for tests internal string ReplaceBlockListUdis(string rawJson, Func createGuid = null) diff --git a/src/Umbraco.Web/Compose/BlockEditorComposer.cs b/src/Umbraco.Web/Compose/BlockEditorComposer.cs index debda081da..e281bcb19f 100644 --- a/src/Umbraco.Web/Compose/BlockEditorComposer.cs +++ b/src/Umbraco.Web/Compose/BlockEditorComposer.cs @@ -7,6 +7,6 @@ namespace Umbraco.Web.Compose /// A composer for Block editors to run a component /// [RuntimeLevel(MinLevel = RuntimeLevel.Run)] - public class BlockEditorComposer : ComponentComposer, ICoreComposer + public class BlockEditorComposer : ComponentComposer, ICoreComposer { } } From 3d385cc1db49b73316b505c1fa654bd19736c039 Mon Sep 17 00:00:00 2001 From: Claus Date: Fri, 4 Sep 2020 13:55:45 +0200 Subject: [PATCH 27/31] Merge pull request #8767 from umbraco/v8/bugfix/return-IHtmlString-from-GetBlockListHtml V8/bugfix/return IHtmlString from GetBlockListHtml (cherry picked from commit 46b4b22dcd8078eec916022eba3b061451b5da30) --- src/Umbraco.Web/BlockListTemplateExtensions.cs | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/Umbraco.Web/BlockListTemplateExtensions.cs b/src/Umbraco.Web/BlockListTemplateExtensions.cs index 413584bc8e..1ccf900aeb 100644 --- a/src/Umbraco.Web/BlockListTemplateExtensions.cs +++ b/src/Umbraco.Web/BlockListTemplateExtensions.cs @@ -4,6 +4,7 @@ using System.Web.Mvc; using System.Web.Mvc.Html; using Umbraco.Core.Models.Blocks; using Umbraco.Core.Models.PublishedContent; +using System.Web; namespace Umbraco.Web { @@ -12,7 +13,7 @@ namespace Umbraco.Web public const string DefaultFolder = "BlockList/"; public const string DefaultTemplate = "Default"; - public static MvcHtmlString GetBlockListHtml(this HtmlHelper html, BlockListModel model, string template = DefaultTemplate) + public static IHtmlString GetBlockListHtml(this HtmlHelper html, BlockListModel model, string template = DefaultTemplate) { if (model?.Count == 0) return new MvcHtmlString(string.Empty); @@ -20,11 +21,11 @@ namespace Umbraco.Web return html.Partial(view, model); } - public static MvcHtmlString GetBlockListHtml(this HtmlHelper html, IPublishedProperty property, string template = DefaultTemplate) => GetBlockListHtml(html, property?.GetValue() as BlockListModel, template); + public static IHtmlString GetBlockListHtml(this HtmlHelper html, IPublishedProperty property, string template = DefaultTemplate) => GetBlockListHtml(html, property?.GetValue() as BlockListModel, template); - public static MvcHtmlString GetBlockListHtml(this HtmlHelper html, IPublishedContent contentItem, string propertyAlias) => GetBlockListHtml(html, contentItem, propertyAlias, DefaultTemplate); + public static IHtmlString GetBlockListHtml(this HtmlHelper html, IPublishedContent contentItem, string propertyAlias) => GetBlockListHtml(html, contentItem, propertyAlias, DefaultTemplate); - public static MvcHtmlString GetBlockListHtml(this HtmlHelper html, IPublishedContent contentItem, string propertyAlias, string template) + public static IHtmlString GetBlockListHtml(this HtmlHelper html, IPublishedContent contentItem, string propertyAlias, string template) { if (propertyAlias == null) throw new ArgumentNullException(nameof(propertyAlias)); if (string.IsNullOrWhiteSpace(propertyAlias)) throw new ArgumentException("Value can't be empty or consist only of white-space characters.", nameof(propertyAlias)); @@ -35,10 +36,10 @@ namespace Umbraco.Web return GetBlockListHtml(html, prop?.GetValue() as BlockListModel, template); } - public static MvcHtmlString GetBlockListHtml(this IPublishedProperty property, HtmlHelper html, string template = DefaultTemplate) => GetBlockListHtml(html, property?.GetValue() as BlockListModel, template); + public static IHtmlString GetBlockListHtml(this IPublishedProperty property, HtmlHelper html, string template = DefaultTemplate) => GetBlockListHtml(html, property?.GetValue() as BlockListModel, template); - public static MvcHtmlString GetBlockListHtml(this IPublishedContent contentItem, HtmlHelper html, string propertyAlias) => GetBlockListHtml(html, contentItem, propertyAlias, DefaultTemplate); + public static IHtmlString GetBlockListHtml(this IPublishedContent contentItem, HtmlHelper html, string propertyAlias) => GetBlockListHtml(html, contentItem, propertyAlias, DefaultTemplate); - public static MvcHtmlString GetBlockListHtml(this IPublishedContent contentItem, HtmlHelper html, string propertyAlias, string template) => GetBlockListHtml(html, contentItem, propertyAlias, template); + public static IHtmlString GetBlockListHtml(this IPublishedContent contentItem, HtmlHelper html, string propertyAlias, string template) => GetBlockListHtml(html, contentItem, propertyAlias, template); } } From bb30b17fe27a0f398e0e2774dbc35507b83cec63 Mon Sep 17 00:00:00 2001 From: Claus Date: Tue, 8 Sep 2020 10:37:35 +0200 Subject: [PATCH 28/31] removing duplicated extension methods that could encourage bad practice when used in the wrong way. (cherry picked from commit 11e42c0a563e91b1e2a3222c50dc602e2f5c0ffb) --- src/Umbraco.Web/BlockListTemplateExtensions.cs | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/Umbraco.Web/BlockListTemplateExtensions.cs b/src/Umbraco.Web/BlockListTemplateExtensions.cs index 1ccf900aeb..6e105a24d6 100644 --- a/src/Umbraco.Web/BlockListTemplateExtensions.cs +++ b/src/Umbraco.Web/BlockListTemplateExtensions.cs @@ -1,5 +1,4 @@ using System; -using System.Linq; using System.Web.Mvc; using System.Web.Mvc.Html; using Umbraco.Core.Models.Blocks; @@ -35,11 +34,5 @@ namespace Umbraco.Web return GetBlockListHtml(html, prop?.GetValue() as BlockListModel, template); } - - public static IHtmlString GetBlockListHtml(this IPublishedProperty property, HtmlHelper html, string template = DefaultTemplate) => GetBlockListHtml(html, property?.GetValue() as BlockListModel, template); - - public static IHtmlString GetBlockListHtml(this IPublishedContent contentItem, HtmlHelper html, string propertyAlias) => GetBlockListHtml(html, contentItem, propertyAlias, DefaultTemplate); - - public static IHtmlString GetBlockListHtml(this IPublishedContent contentItem, HtmlHelper html, string propertyAlias, string template) => GetBlockListHtml(html, contentItem, propertyAlias, template); } } From ddc922a583e24582d9c1918667d50d91a7e5921b Mon Sep 17 00:00:00 2001 From: Ronald Barendse Date: Tue, 8 Sep 2020 12:08:27 +0200 Subject: [PATCH 29/31] 8.7RC Add generic BlockListItem classes (#8841) (cherry picked from commit 5faa9ae22fbf0f7f0a41a4bbe7fef243d2171fc0) --- .../Models/Blocks/BlockListItem.cs | 111 ++++++++++++++++-- .../Models/Blocks/BlockListModel.cs | 77 ++++++------ .../Blocks/ContentAndSettingsReference.cs | 26 ++-- .../Models/Blocks/IBlockReference.cs | 41 ++++--- .../BlockListPropertyValueConverter.cs | 4 +- 5 files changed, 173 insertions(+), 86 deletions(-) diff --git a/src/Umbraco.Core/Models/Blocks/BlockListItem.cs b/src/Umbraco.Core/Models/Blocks/BlockListItem.cs index f4b5c489e7..620c3d9fe0 100644 --- a/src/Umbraco.Core/Models/Blocks/BlockListItem.cs +++ b/src/Umbraco.Core/Models/Blocks/BlockListItem.cs @@ -5,41 +5,126 @@ using Umbraco.Core.Models.PublishedContent; namespace Umbraco.Core.Models.Blocks { /// - /// Represents a layout item for the Block List editor + /// Represents a layout item for the Block List editor. /// + /// [DataContract(Name = "block", Namespace = "")] public class BlockListItem : IBlockReference { + /// + /// Initializes a new instance of the class. + /// + /// The content UDI. + /// The content. + /// The settings UDI. + /// The settings. + /// contentUdi + /// or + /// content public BlockListItem(Udi contentUdi, IPublishedElement content, Udi settingsUdi, IPublishedElement settings) { - ContentUdi = contentUdi ?? throw new ArgumentNullException(nameof(contentUdi)); + ContentUdi = contentUdi ?? throw new ArgumentNullException(nameof(contentUdi)); Content = content ?? throw new ArgumentNullException(nameof(content)); - Settings = settings; // can be null - SettingsUdi = settingsUdi; // can be null + SettingsUdi = settingsUdi; + Settings = settings; } /// - /// The Id of the content data item + /// Gets the content UDI. /// + /// + /// The content UDI. + /// [DataMember(Name = "contentUdi")] public Udi ContentUdi { get; } /// - /// The Id of the settings data item - /// - [DataMember(Name = "settingsUdi")] - public Udi SettingsUdi { get; } - - /// - /// The content data item referenced + /// Gets the content. /// + /// + /// The content. + /// [DataMember(Name = "content")] public IPublishedElement Content { get; } /// - /// The settings data item referenced + /// Gets the settings UDI. /// + /// + /// The settings UDI. + /// + [DataMember(Name = "settingsUdi")] + public Udi SettingsUdi { get; } + + /// + /// Gets the settings. + /// + /// + /// The settings. + /// [DataMember(Name = "settings")] public IPublishedElement Settings { get; } } + + /// + /// Represents a layout item with a generic content type for the Block List editor. + /// + /// The type of the content. + /// + public class BlockListItem : BlockListItem + where T : IPublishedElement + { + /// + /// Initializes a new instance of the class. + /// + /// The content UDI. + /// The content. + /// The settings UDI. + /// The settings. + public BlockListItem(Udi contentUdi, T content, Udi settingsUdi, IPublishedElement settings) + : base(contentUdi, content, settingsUdi, settings) + { + Content = content; + } + + /// + /// Gets the content. + /// + /// + /// The content. + /// + public new T Content { get; } + } + + /// + /// Represents a layout item with generic content and settings types for the Block List editor. + /// + /// The type of the content. + /// The type of the settings. + /// + public class BlockListItem : BlockListItem + where TContent : IPublishedElement + where TSettings : IPublishedElement + { + /// + /// Initializes a new instance of the class. + /// + /// The content udi. + /// The content. + /// The settings udi. + /// The settings. + public BlockListItem(Udi contentUdi, TContent content, Udi settingsUdi, TSettings settings) + : base(contentUdi, content, settingsUdi, settings) + { + Settings = settings; + } + + /// + /// Gets the settings. + /// + /// + /// The settings. + /// + public new TSettings Settings { get; } + } } diff --git a/src/Umbraco.Core/Models/Blocks/BlockListModel.cs b/src/Umbraco.Core/Models/Blocks/BlockListModel.cs index 9a5a3af22a..9a3a26ab30 100644 --- a/src/Umbraco.Core/Models/Blocks/BlockListModel.cs +++ b/src/Umbraco.Core/Models/Blocks/BlockListModel.cs @@ -1,64 +1,63 @@ using System; -using System.Collections; using System.Collections.Generic; +using System.Collections.ObjectModel; using System.Linq; using System.Runtime.Serialization; -using Umbraco.Core.Models.PublishedContent; namespace Umbraco.Core.Models.Blocks { /// - /// The strongly typed model for the Block List editor + /// The strongly typed model for the Block List editor. /// + /// [DataContract(Name = "blockList", Namespace = "")] - public class BlockListModel : IReadOnlyList + public class BlockListModel : ReadOnlyCollection { - private readonly IReadOnlyList _layout = new List(); - + /// + /// Gets the empty . + /// + /// + /// The empty . + /// public static BlockListModel Empty { get; } = new BlockListModel(); + /// + /// Prevents a default instance of the class from being created. + /// private BlockListModel() - { - } - - public BlockListModel(IEnumerable layout) - { - _layout = layout.ToList(); - } - - public int Count => _layout.Count; + : this(new List()) + { } /// - /// Get the block by index + /// Initializes a new instance of the class. /// - /// - /// - public BlockListItem this[int index] => _layout[index]; + /// The list to wrap. + public BlockListModel(IList list) + : base(list) + { } /// - /// Get the block by content Guid + /// Gets the with the specified content key. /// - /// - /// - public BlockListItem this[Guid contentKey] => _layout.FirstOrDefault(x => x.Content.Key == contentKey); + /// + /// The . + /// + /// The content key. + /// + /// The with the specified content key. + /// + public BlockListItem this[Guid contentKey] => this.FirstOrDefault(x => x.Content.Key == contentKey); /// - /// Get the block by content element Udi + /// Gets the with the specified content UDI. /// - /// - /// - public BlockListItem this[Udi contentUdi] - { - get - { - if (!(contentUdi is GuidUdi guidUdi)) return null; - return _layout.FirstOrDefault(x => x.Content.Key == guidUdi.Guid); - } - } - - public IEnumerator GetEnumerator() => _layout.GetEnumerator(); - - IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); - + /// + /// The . + /// + /// The content UDI. + /// + /// The with the specified content UDI. + /// + public BlockListItem this[Udi contentUdi] => contentUdi is GuidUdi guidUdi ? this.FirstOrDefault(x => x.Content.Key == guidUdi.Guid) : null; } } diff --git a/src/Umbraco.Core/Models/Blocks/ContentAndSettingsReference.cs b/src/Umbraco.Core/Models/Blocks/ContentAndSettingsReference.cs index 523a964c7b..f7222fe140 100644 --- a/src/Umbraco.Core/Models/Blocks/ContentAndSettingsReference.cs +++ b/src/Umbraco.Core/Models/Blocks/ContentAndSettingsReference.cs @@ -1,5 +1,5 @@ -using System.Collections.Generic; -using System; +using System; +using System.Collections.Generic; namespace Umbraco.Core.Models.Blocks { @@ -12,26 +12,16 @@ namespace Umbraco.Core.Models.Blocks } public Udi ContentUdi { get; } + public Udi SettingsUdi { get; } - public override bool Equals(object obj) - { - return obj is ContentAndSettingsReference reference && Equals(reference); - } + public override bool Equals(object obj) => obj is ContentAndSettingsReference reference && Equals(reference); - public bool Equals(ContentAndSettingsReference other) - { - return EqualityComparer.Default.Equals(ContentUdi, other.ContentUdi) && - EqualityComparer.Default.Equals(SettingsUdi, other.SettingsUdi); - } + public bool Equals(ContentAndSettingsReference other) => other != null + && EqualityComparer.Default.Equals(ContentUdi, other.ContentUdi) + && EqualityComparer.Default.Equals(SettingsUdi, other.SettingsUdi); - public override int GetHashCode() - { - var hashCode = 272556606; - hashCode = hashCode * -1521134295 + EqualityComparer.Default.GetHashCode(ContentUdi); - hashCode = hashCode * -1521134295 + EqualityComparer.Default.GetHashCode(SettingsUdi); - return hashCode; - } + public override int GetHashCode() => (ContentUdi, SettingsUdi).GetHashCode(); public static bool operator ==(ContentAndSettingsReference left, ContentAndSettingsReference right) { diff --git a/src/Umbraco.Core/Models/Blocks/IBlockReference.cs b/src/Umbraco.Core/Models/Blocks/IBlockReference.cs index 8d8ddd47f0..7f5c835b3c 100644 --- a/src/Umbraco.Core/Models/Blocks/IBlockReference.cs +++ b/src/Umbraco.Core/Models/Blocks/IBlockReference.cs @@ -1,26 +1,37 @@ namespace Umbraco.Core.Models.Blocks { - /// - /// Represents a data item reference for a Block editor implementation - /// - /// - /// - /// see: https://github.com/umbraco/rfcs/blob/907f3758cf59a7b6781296a60d57d537b3b60b8c/cms/0011-block-data-structure.md#strongly-typed - /// - public interface IBlockReference : IBlockReference - { - TSettings Settings { get; } - } - - /// - /// Represents a data item reference for a Block Editor implementation + /// Represents a data item reference for a Block Editor implementation. /// /// - /// see: https://github.com/umbraco/rfcs/blob/907f3758cf59a7b6781296a60d57d537b3b60b8c/cms/0011-block-data-structure.md#strongly-typed + /// See: https://github.com/umbraco/rfcs/blob/907f3758cf59a7b6781296a60d57d537b3b60b8c/cms/0011-block-data-structure.md#strongly-typed /// public interface IBlockReference { + /// + /// Gets the content UDI. + /// + /// + /// The content UDI. + /// Udi ContentUdi { get; } } + + /// + /// Represents a data item reference with settings for a Block editor implementation. + /// + /// The type of the settings. + /// + /// See: https://github.com/umbraco/rfcs/blob/907f3758cf59a7b6781296a60d57d537b3b60b8c/cms/0011-block-data-structure.md#strongly-typed + /// + public interface IBlockReference : IBlockReference + { + /// + /// Gets the settings. + /// + /// + /// The settings. + /// + TSettings Settings { get; } + } } diff --git a/src/Umbraco.Web/PropertyEditors/ValueConverters/BlockListPropertyValueConverter.cs b/src/Umbraco.Web/PropertyEditors/ValueConverters/BlockListPropertyValueConverter.cs index 0c90a41fbd..f46c118174 100644 --- a/src/Umbraco.Web/PropertyEditors/ValueConverters/BlockListPropertyValueConverter.cs +++ b/src/Umbraco.Web/PropertyEditors/ValueConverters/BlockListPropertyValueConverter.cs @@ -120,7 +120,9 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters settingsData = null; } - var layoutRef = new BlockListItem(contentGuidUdi, contentData, settingGuidUdi, settingsData); + var layoutType = typeof(BlockListItem<,>).MakeGenericType(contentData.GetType(), settingsData?.GetType() ?? typeof(IPublishedElement)); + var layoutRef = (BlockListItem)Activator.CreateInstance(layoutType, contentGuidUdi, contentData, settingGuidUdi, settingsData); + layout.Add(layoutRef); } From bef8cb9589b521c0c1c0bcd24caa11d1ebaa0565 Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Tue, 8 Sep 2020 14:32:09 +0200 Subject: [PATCH 30/31] Added old ctor to avoid a breaking change --- src/Umbraco.Web/Install/InstallHelper.cs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/Umbraco.Web/Install/InstallHelper.cs b/src/Umbraco.Web/Install/InstallHelper.cs index b74a20c27c..a998a172fc 100644 --- a/src/Umbraco.Web/Install/InstallHelper.cs +++ b/src/Umbraco.Web/Install/InstallHelper.cs @@ -30,6 +30,18 @@ namespace Umbraco.Web.Install private readonly IInstallationService _installationService; private InstallationType? _installationType; + + [Obsolete("Use the constructor with IInstallationService injected.")] + public InstallHelper( + IUmbracoContextAccessor umbracoContextAccessor, + DatabaseBuilder databaseBuilder, + ILogger logger, + IGlobalSettings globalSettings) + : this(umbracoContextAccessor, databaseBuilder, logger, globalSettings, Current.Factory.GetInstance()) + { + + } + public InstallHelper(IUmbracoContextAccessor umbracoContextAccessor, DatabaseBuilder databaseBuilder, ILogger logger, IGlobalSettings globalSettings, IInstallationService installationService) From ecff24089c32df35923041077d89bb85758b9c4b Mon Sep 17 00:00:00 2001 From: Sebastiaan Janssen Date: Tue, 8 Sep 2020 14:46:22 +0200 Subject: [PATCH 31/31] Bump version to 8.7.0 --- src/SolutionInfo.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/SolutionInfo.cs b/src/SolutionInfo.cs index 365c90423a..5587979a6c 100644 --- a/src/SolutionInfo.cs +++ b/src/SolutionInfo.cs @@ -19,4 +19,4 @@ using System.Resources; // these are FYI and changed automatically [assembly: AssemblyFileVersion("8.7.0")] -[assembly: AssemblyInformationalVersion("8.7.0-rc")] +[assembly: AssemblyInformationalVersion("8.7.0")]