From 21114cbbd774114fa90be616311e3ca280c49b23 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Tue, 14 Jan 2020 10:54:11 +0100 Subject: [PATCH 001/377] gulp support for less files in views folder --- src/Umbraco.Web.UI.Client/gulp/config.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/gulp/config.js b/src/Umbraco.Web.UI.Client/gulp/config.js index 59e8bf6c05..a807d63f5f 100755 --- a/src/Umbraco.Web.UI.Client/gulp/config.js +++ b/src/Umbraco.Web.UI.Client/gulp/config.js @@ -17,7 +17,7 @@ module.exports = { installer: { files: "./src/less/installer.less", watch: "./src/less/**/*.less", out: "installer.css" }, nonodes: { files: "./src/less/pages/nonodes.less", watch: "./src/less/**/*.less", out: "nonodes.style.min.css"}, preview: { files: "./src/less/canvas-designer.less", watch: "./src/less/**/*.less", out: "canvasdesigner.css" }, - umbraco: { files: "./src/less/belle.less", watch: "./src/less/**/*.less", out: "umbraco.css" }, + umbraco: { files: "./src/less/belle.less", watch: "./src/**/*.less", out: "umbraco.css" }, rteContent: { files: "./src/less/rte-content.less", watch: "./src/less/**/*.less", out: "rte-content.css" } }, From 678e31834d966adc59b6f423d403e1dfc88a3744 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Mon, 20 Jan 2020 09:06:27 +0100 Subject: [PATCH 002/377] registrer Block List Editor --- src/Umbraco.Core/Constants-PropertyEditors.cs | 5 ++++ .../BlockListPropertyEditor.cs | 30 +++++++++++++++++++ 2 files changed, 35 insertions(+) create mode 100644 src/Umbraco.Web/PropertyEditors/BlockListPropertyEditor.cs diff --git a/src/Umbraco.Core/Constants-PropertyEditors.cs b/src/Umbraco.Core/Constants-PropertyEditors.cs index eb2b3525a7..dcd7eb9d05 100644 --- a/src/Umbraco.Core/Constants-PropertyEditors.cs +++ b/src/Umbraco.Core/Constants-PropertyEditors.cs @@ -36,6 +36,11 @@ namespace Umbraco.Core /// public static class Aliases { + /// + /// Block List. + /// + public const string BlockList = "Umbraco.BlockList"; + /// /// CheckBox List. /// diff --git a/src/Umbraco.Web/PropertyEditors/BlockListPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/BlockListPropertyEditor.cs new file mode 100644 index 0000000000..da44151ec9 --- /dev/null +++ b/src/Umbraco.Web/PropertyEditors/BlockListPropertyEditor.cs @@ -0,0 +1,30 @@ +using Umbraco.Core; +using Umbraco.Core.Logging; +using Umbraco.Core.PropertyEditors; +using Umbraco.Core.Services; + +namespace Umbraco.Web.PropertyEditors +{ + /// + /// Represents a block list property editor. + /// + [DataEditor( + Constants.PropertyEditors.Aliases.BlockList, + "Block List", + "blocklist", + ValueType = ValueTypes.Json, + Group = Constants.PropertyEditors.Groups.Lists, + Icon = "icon-thumbnail-list")] + public class BlockListPropertyEditor : DataEditor + { + public BlockListPropertyEditor(ILogger logger) + : base(logger) + { } + + #region Pre Value Editor + //protected override IConfigurationEditor CreateConfigurationEditor() => new BlockEditorListConfigurationEditor(); + + #endregion + + } +} From 897517d4ec0675cf10c375dd96cf3ad29df5f32a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Mon, 20 Jan 2020 09:07:13 +0100 Subject: [PATCH 003/377] do not set focus if already set + clear timeout if running --- .../components/forms/focuswhen.directive.js | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/focuswhen.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/focuswhen.directive.js index d8dbcc1012..dda5c51175 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/focuswhen.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/forms/focuswhen.directive.js @@ -2,13 +2,20 @@ angular.module("umbraco.directives").directive('focusWhen', function ($timeout) return { restrict: 'A', link: function (scope, elm, attrs, ctrl) { + + var delayTimer; + attrs.$observe("focusWhen", function (newValue) { - if (newValue === "true") { - $timeout(function () { - elm.trigger("focus"); - }); + if (newValue === "true" && document.activeelement !== elm[0]) { + delayTimer = $timeout(function () { + elm[0].focus(); + }); } }); + + scope.$on('$destroy', function() { + $timeout.cancel(delayTimer); + }); } }; }); From 85d4cd9be3995f10fc4699c259267decbaa49a56 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Mon, 20 Jan 2020 09:07:41 +0100 Subject: [PATCH 004/377] compile JS after HTML, cause HTML is embeded in JS. --- src/Umbraco.Web.UI.Client/gulp/tasks/watchTask.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Umbraco.Web.UI.Client/gulp/tasks/watchTask.js b/src/Umbraco.Web.UI.Client/gulp/tasks/watchTask.js index 24a6e65540..42f25ccb23 100644 --- a/src/Umbraco.Web.UI.Client/gulp/tasks/watchTask.js +++ b/src/Umbraco.Web.UI.Client/gulp/tasks/watchTask.js @@ -12,6 +12,7 @@ var processLess = require('../util/processLess'); //const { less } = require('./less'); //const { views } = require('./views'); +var {js} = require('./js'); function watchTask(cb) { @@ -39,6 +40,7 @@ function watchTask(cb) { viewWatcher.on('change', function(path, stats) { console.log("copying " + group.files + " to " + config.root + config.targets.views + group.folder); src(group.files).pipe( dest(config.root + config.targets.views + group.folder) ); + js(); }); } }); From 4caad6e42953c15d5124f6c7a70c9bcba464f055 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Mon, 20 Jan 2020 09:09:14 +0100 Subject: [PATCH 005/377] ability to turn on/off focus outlines --- .../src/main.controller.js | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/main.controller.js b/src/Umbraco.Web.UI.Client/src/main.controller.js index 93870f8a56..3601cb9652 100644 --- a/src/Umbraco.Web.UI.Client/src/main.controller.js +++ b/src/Umbraco.Web.UI.Client/src/main.controller.js @@ -32,13 +32,17 @@ function MainController($scope, $location, appState, treeService, notificationsS // For more information about this approach, see https://hackernoon.com/removing-that-ugly-focus-ring-and-keeping-it-too-6c8727fefcd2 function handleFirstTab(evt) { if (evt.keyCode === 9) { - $scope.tabbingActive = true; - $scope.$digest(); - window.removeEventListener('keydown', handleFirstTab); - window.addEventListener('mousedown', disableTabbingActive); + enableTabbingActive(); } } + function enableTabbingActive() { + $scope.tabbingActive = true; + $scope.$digest(); + window.addEventListener('mousedown', disableTabbingActive); + window.removeEventListener("keydown", handleFirstTab); + } + function disableTabbingActive(evt) { $scope.tabbingActive = false; $scope.$digest(); @@ -48,6 +52,12 @@ function MainController($scope, $location, appState, treeService, notificationsS window.addEventListener("keydown", handleFirstTab); + $scope.$on("showFocusOutline", function() { + $scope.tabbingActive = true; + window.addEventListener('mousedown', disableTabbingActive); + window.removeEventListener("keydown", handleFirstTab); + }); + $scope.removeNotification = function (index) { notificationsService.remove(index); From 761b5ecc01323f8393f77c6d63996177eab1e9a8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Mon, 20 Jan 2020 09:09:54 +0100 Subject: [PATCH 006/377] Add BlockListPropertyEditor to csproj --- src/Umbraco.Web/Umbraco.Web.csproj | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index 5d29e53d4a..997b13ce8d 100755 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -888,6 +888,7 @@ + From 034567bb0ee3e918f715b47ca3a2605ef059b7d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Mon, 20 Jan 2020 11:11:05 +0100 Subject: [PATCH 007/377] reverted gulp change --- src/Umbraco.Web.UI.Client/gulp/tasks/watchTask.js | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/gulp/tasks/watchTask.js b/src/Umbraco.Web.UI.Client/gulp/tasks/watchTask.js index 42f25ccb23..3c90003e30 100644 --- a/src/Umbraco.Web.UI.Client/gulp/tasks/watchTask.js +++ b/src/Umbraco.Web.UI.Client/gulp/tasks/watchTask.js @@ -9,11 +9,6 @@ var MergeStream = require('merge-stream'); var processJs = require('../util/processJs'); var processLess = require('../util/processLess'); -//const { less } = require('./less'); -//const { views } = require('./views'); - -var {js} = require('./js'); - function watchTask(cb) { var watchInterval = 500; @@ -40,7 +35,6 @@ function watchTask(cb) { viewWatcher.on('change', function(path, stats) { console.log("copying " + group.files + " to " + config.root + config.targets.views + group.folder); src(group.files).pipe( dest(config.root + config.targets.views + group.folder) ); - js(); }); } }); From 382ead27f20ee688a223405177fddf39850ad993 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Mon, 20 Jan 2020 13:30:50 +0100 Subject: [PATCH 008/377] Run JS when Views has been updated, cause we have embeded templates. --- src/Umbraco.Web.UI.Client/gulp/tasks/watchTask.js | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/gulp/tasks/watchTask.js b/src/Umbraco.Web.UI.Client/gulp/tasks/watchTask.js index 3c90003e30..a94314abd6 100644 --- a/src/Umbraco.Web.UI.Client/gulp/tasks/watchTask.js +++ b/src/Umbraco.Web.UI.Client/gulp/tasks/watchTask.js @@ -9,6 +9,8 @@ var MergeStream = require('merge-stream'); var processJs = require('../util/processJs'); var processLess = require('../util/processLess'); +var {js} = require('./js'); + function watchTask(cb) { var watchInterval = 500; @@ -33,8 +35,15 @@ function watchTask(cb) { if(group.watch !== false) { viewWatcher = watch(group.files, { ignoreInitial: true, interval: watchInterval }); viewWatcher.on('change', function(path, stats) { + console.log("copying " + group.files + " to " + config.root + config.targets.views + group.folder); - src(group.files).pipe( dest(config.root + config.targets.views + group.folder) ); + + return MergeStream( + src(group.files) + .pipe( dest(config.root + config.targets.views + group.folder) ) + , js() + ); + }); } }); From 2583d0c7d03727eaab5bb8ca3b4b5fe9b0e4a6f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Mon, 20 Jan 2020 17:34:49 +0100 Subject: [PATCH 009/377] BlockList PropertyEditor --- src/Umbraco.Web.UI.Client/src/less/belle.less | 9 + .../imageblock/imageblock.editor.html | 3 + .../imageblock/imageblock.editor.less | 12 + .../labelblock/labelblock.editor.html | 4 + .../labelblock/labelblock.editor.less | 34 ++ .../textareablock.editor.controller.js | 29 + .../textareablock/textareablock.editor.html | 15 + .../textareablock/textareablock.editor.less | 27 + .../elementeditor/elementeditor.controller.js | 23 + .../elementeditor/elementeditor.html | 84 +++ .../blocklist/blocklist.component.html | 104 ++++ .../blocklist/blocklist.component.js | 553 ++++++++++++++++++ .../blocklist/blocklist.component.less | 217 +++++++ .../propertyeditors/blocklist/blocklist.html | 1 + 14 files changed, 1115 insertions(+) create mode 100644 src/Umbraco.Web.UI.Client/src/views/blockelements/imageblock/imageblock.editor.html create mode 100644 src/Umbraco.Web.UI.Client/src/views/blockelements/imageblock/imageblock.editor.less create mode 100644 src/Umbraco.Web.UI.Client/src/views/blockelements/labelblock/labelblock.editor.html create mode 100644 src/Umbraco.Web.UI.Client/src/views/blockelements/labelblock/labelblock.editor.less create mode 100644 src/Umbraco.Web.UI.Client/src/views/blockelements/textareablock/textareablock.editor.controller.js create mode 100644 src/Umbraco.Web.UI.Client/src/views/blockelements/textareablock/textareablock.editor.html create mode 100644 src/Umbraco.Web.UI.Client/src/views/blockelements/textareablock/textareablock.editor.less create mode 100644 src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/elementeditor/elementeditor.controller.js create mode 100644 src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/elementeditor/elementeditor.html create mode 100644 src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.html create mode 100644 src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.js create mode 100644 src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.less create mode 100644 src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.html diff --git a/src/Umbraco.Web.UI.Client/src/less/belle.less b/src/Umbraco.Web.UI.Client/src/less/belle.less index b5e032f9fb..78a10da78d 100644 --- a/src/Umbraco.Web.UI.Client/src/less/belle.less +++ b/src/Umbraco.Web.UI.Client/src/less/belle.less @@ -195,6 +195,10 @@ @import "components/contextdialogs/umb-dialog-datatype-delete.less"; +// Property Editors +@import "../views/propertyeditors/blocklist/blocklist.component.less"; + + // Utilities @import "utilities/layout/_display.less"; @import "utilities/theme/_opacity.less"; @@ -218,6 +222,11 @@ // Used for prevalue editors @import "components/prevalues/multivalues.less"; +// Block Elements +@import "../views/blockelements/labelblock/labelblock.editor.less"; +@import "../views/blockelements/textareablock/textareablock.editor.less"; +@import "../views/blockelements/imageblock/imageblock.editor.less"; + // Dashboards @import "dashboards/getstarted.less"; @import "dashboards/umbraco-forms.less"; diff --git a/src/Umbraco.Web.UI.Client/src/views/blockelements/imageblock/imageblock.editor.html b/src/Umbraco.Web.UI.Client/src/views/blockelements/imageblock/imageblock.editor.html new file mode 100644 index 0000000000..c77f19ba67 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/blockelements/imageblock/imageblock.editor.html @@ -0,0 +1,3 @@ + diff --git a/src/Umbraco.Web.UI.Client/src/views/blockelements/imageblock/imageblock.editor.less b/src/Umbraco.Web.UI.Client/src/views/blockelements/imageblock/imageblock.editor.less new file mode 100644 index 0000000000..99b1bb53f2 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/blockelements/imageblock/imageblock.editor.less @@ -0,0 +1,12 @@ +.blockelement-imageblock-editor { + + padding-top: 2px; + padding-bottom: 2px; + + img { + width: 100%; + border: none; + resize: none; + border-radius: @baseBorderRadius; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/views/blockelements/labelblock/labelblock.editor.html b/src/Umbraco.Web.UI.Client/src/views/blockelements/labelblock/labelblock.editor.html new file mode 100644 index 0000000000..f0678c76e9 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/blockelements/labelblock/labelblock.editor.html @@ -0,0 +1,4 @@ + diff --git a/src/Umbraco.Web.UI.Client/src/views/blockelements/labelblock/labelblock.editor.less b/src/Umbraco.Web.UI.Client/src/views/blockelements/labelblock/labelblock.editor.less new file mode 100644 index 0000000000..610773528b --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/blockelements/labelblock/labelblock.editor.less @@ -0,0 +1,34 @@ +.blockelement-labelblock-editor { + + width: 100%; + min-height: 48px; + border: 1px solid @gray-9; + border-radius: @baseBorderRadius; + + color: @ui-action-discreet-type; + + text-align: left; + padding-left: 20px; + padding-bottom: 2px; + margin-bottom: 2px; + margin-top: 2px; + + user-select: none; + + transition: border-color 120ms; + + i { + font-size: 22px; + display: inline-block; + vertical-align: middle; + } + span { + display: inline-block; + vertical-align: middle; + } + + &:hover { + color: @ui-action-discreet-type-hover; + border-color: @gray-8; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/views/blockelements/textareablock/textareablock.editor.controller.js b/src/Umbraco.Web.UI.Client/src/views/blockelements/textareablock/textareablock.editor.controller.js new file mode 100644 index 0000000000..761fc57e03 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/blockelements/textareablock/textareablock.editor.controller.js @@ -0,0 +1,29 @@ +//used for the media picker dialog +angular.module("umbraco") +.controller("Umbraco.Editors.TextAreaBlockElementEditorController", + function ($scope) { + + var vm = this; + + vm.firstProperty = $scope.block.content.tabs[0].properties[0]; +/* + vm.submitOnEnter = function($event) { + if($event && $event.keyCode === 13 && !$event.shiftKey && !$event.ctrlKey) { + var target = $event.target; + if(target.selectionStart === target.selectionEnd && target.selectionEnd === target.textLength) { + //&& (target.textLength === 0 || /\r|\n/.test(target.value.charAt(target.textLength - 1))) + $scope.$emit("showFocusOutline"); + $scope.blockApi.showCreateOptionsFor($scope.block, $event); + } + } + } +*/ + vm.onBlur = function() { + if (vm.firstProperty.value === null || vm.firstProperty.value === "") { + $scope.blockApi.deleteBlock($scope.block); + } + } + + } + +); diff --git a/src/Umbraco.Web.UI.Client/src/views/blockelements/textareablock/textareablock.editor.html b/src/Umbraco.Web.UI.Client/src/views/blockelements/textareablock/textareablock.editor.html new file mode 100644 index 0000000000..405c8634b4 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/blockelements/textareablock/textareablock.editor.html @@ -0,0 +1,15 @@ + +
+ + + + + +
diff --git a/src/Umbraco.Web.UI.Client/src/views/blockelements/textareablock/textareablock.editor.less b/src/Umbraco.Web.UI.Client/src/views/blockelements/textareablock/textareablock.editor.less new file mode 100644 index 0000000000..492025274a --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/blockelements/textareablock/textareablock.editor.less @@ -0,0 +1,27 @@ +.blockelement-textareablock-editor { + + width: 100%; + padding-bottom: 24px; + padding-top: 24px; + + padding-left: 24px; + padding-right: 24px; + + min-height: 64px; + box-sizing: border-box; + + textarea { + display: block; + width: 100%; + max-width: 640px; + margin-left: auto; + margin-right: auto; + border: none; + resize: none; + overflow: auto; + + font-size: 18px; + font-family: Georgia,Cambria,"Times New Roman",Times,serif; + line-height: 1.25; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/elementeditor/elementeditor.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/elementeditor/elementeditor.controller.js new file mode 100644 index 0000000000..d421418297 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/elementeditor/elementeditor.controller.js @@ -0,0 +1,23 @@ +//used for the media picker dialog +angular.module("umbraco") +.controller("Umbraco.Editors.ElementEditorController", + function ($scope) { + + var vm = this; + + vm.content = $scope.model.block.content; + + vm.saveAndClose = function() { + if ($scope.model && $scope.model.submit) { + $scope.model.submit($scope.model); + } + } + + vm.close = function() { + if ($scope.model && $scope.model.close) { + $scope.model.close($scope.model); + } + } + + } +); diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/elementeditor/elementeditor.html b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/elementeditor/elementeditor.html new file mode 100644 index 0000000000..71aab124e6 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/elementeditor/elementeditor.html @@ -0,0 +1,84 @@ +
+ + + + + + +
+ + + +
+ +
+
+ +
+ +
+
{{ group.label }}
+
+ +
+ + +
+ + +
+ +
+
+ +
+ + + + + +
+
+
+
+ +
+ + + + + + + + + + + + + + + + +
+
+
diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.html new file mode 100644 index 0000000000..e38858c8b7 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.html @@ -0,0 +1,104 @@ +
+ + +
+ +
+ +
+ + + + +
+ +
+
+
+ +

No editor

+
+ +
+ + +
+ +
+ +
+
+ + + + + +
+ + + + + +
diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.js new file mode 100644 index 0000000000..6283bd1026 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.js @@ -0,0 +1,553 @@ +(function () { + 'use strict'; + + angular + .module('umbraco') + .component('blockListPropertyEditor', { + templateUrl: 'views/propertyeditors/blocklist/blocklist.component.html', + controller: BlockListController, + controllerAs: 'vm', + bindings: { + + }, + require: { + umbProperty: '?^umbProperty', + propertyForm: '?^propertyForm' + } + }); + + function BlockListController($scope, $interpolate, editorService, clipboardService, localizationService, overlayService) { + + var vm = this; + var model = $scope.$parent.$parent.model; + + $scope.moveFocusToBlock = null; + + vm.quickMenuVisible = false; + vm.quickMenuIndex = 0; + + vm.quickMenuAddNewBlock = function(type) { + addNewBlock(vm.quickMenuIndex, type); + vm.quickMenuVisible = false; + } + + vm.availableBlockTypes = [ + { + alias: "pageModule", + name: "Module", + icon: "icon-document", + prototype_paste_data: { + + elementType: { + alias: 'contentTypeAlias', + icon: "icon-document", + label: "Text" + }, + label: "{{pageTitle | truncate:true:36}}", + labelInterpolate: $interpolate("{{pageTitle | truncate:true:36}}"), + editor: "views/blockelements/labelblock/labelblock.editor.html", + content: { + variants: [ + { + language: { + isDefault: true + } + } + ], + tabs: [ + { + id: 1234, + label: "Group 1", + properties: [ + { + label: "Page Title", + description: "The title of the page", + view: "textbox", + config: {maxChars: 500}, + hideLabel: false, + validation: {mandatory: true, mandatoryMessage: "", pattern: null, patternMessage: ""}, + readonly: false, + id: 441, + dataTypeKey: "0cc0eba1-9960-42c9-bf9b-60e150b429ae", + value: "Consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.", + alias: "pageTitle", + editor: "Umbraco.TextBox", + isSensitive: false, + culture: null, + segment: null + } + ] + } + ] + } + } + }, + { + alias: "contentTypeAlias", + name: "contentTypeName", + icon: "icon-text", + prototype_paste_data: { + elementType: { + alias: 'contentTypeAlias', + icon: "icon-document", + label: "Text" + }, + label: "Label", + editor: "views/blockelements/textareablock/textareablock.editor.html", + content: { + variants: [ + { + language: { + isDefault: true + } + } + ], + tabs: [ + { + id: 1234, + label: "Group 1", + properties: [ + { + label: "Page Title", + description: "The title of the page", + view: "textbox", + config: {maxChars: 500}, + hideLabel: false, + validation: {mandatory: true, mandatoryMessage: "", pattern: null, patternMessage: ""}, + readonly: false, + id: 441, + dataTypeKey: "0cc0eba1-9960-42c9-bf9b-60e150b429ae", + value: "", + alias: "pageTitle", + editor: "Umbraco.TextBox", + isSensitive: false, + culture: null, + segment: null + } + ] + } + ] + } + } + }, + { + alias: "contentTypeAlias", + name: "contentTypeName", + icon: "icon-picture", + prototype_paste_data: { + elementType: { + alias: 'contentTypeAlias', + icon: "icon-document", + label: "Text" + }, + label: "Label", + editor: "views/blockelements/imageblock/imageblock.editor.html", + content: { + variants: [ + { + language: { + isDefault: true + } + } + ], + tabs: [ + { + id: 1234, + label: "Group 1", + properties: [ + { + label: "Page Title", + description: "The title of the page", + view: "textbox", + config: {maxChars: 500}, + hideLabel: false, + validation: {mandatory: true, mandatoryMessage: "", pattern: null, patternMessage: ""}, + readonly: false, + id: 441, + dataTypeKey: "0cc0eba1-9960-42c9-bf9b-60e150b429ae", + value: "Let's have a chat", + alias: "pageTitle", + editor: "Umbraco.TextBox", + isSensitive: false, + culture: null, + segment: null + } + ] + } + ], + temp_image: "/umbraco/assets/img/login.jpg" + } + } + } + ]; + + // var defaultBlockType... + + // TODO: get icon, properties etc. from available types? + vm.blocks = [ + { + elementType: { + alias: 'contentTypeAlias', + icon: "icon-document", + label: "Text" + }, + label: "{{pageTitle | truncate:true:36}}", + labelInterpolate: $interpolate("{{pageTitle | truncate:true:36}}"), + key: 1, + editor: "views/blockelements/labelblock/labelblock.editor.html", + content: { + variants: [ + { + language: { + isDefault: true + } + } + ], + tabs: [ + { + id: 1234, + label: "Group 1", + properties: [ + { + label: "Page Title", + description: "The title of the page", + view: "textbox", + config: {maxChars: 500}, + hideLabel: false, + validation: {mandatory: true, mandatoryMessage: "", pattern: null, patternMessage: ""}, + readonly: false, + id: 441, + dataTypeKey: "0cc0eba1-9960-42c9-bf9b-60e150b429ae", + value: "The purpose of lorem ipsum is to create a natural looking block of text (sentence, paragraph, page, etc.) that doesn't distract from the layout. A practice not without controversy, laying out pages with meaningless filler text can be very useful when the focus is meant to be on design, not content.", + alias: "pageTitle", + editor: "Umbraco.TextBox", + isSensitive: false, + culture: null, + segment: null + } + ] + } + ] + } + }, + { + elementType: { + alias: 'contentTypeAlias', + icon: "icon-document", + label: "Text" + }, + label: "{{pageTitle | truncate:true:36}}", + labelInterpolate: $interpolate("{{pageTitle | truncate:true:36}}"), + key: 2, + editor: "views/blockelements/labelblock/labelblock.editor.html", + content: { + variants: [ + { + language: { + isDefault: true + } + } + ], + tabs: [ + { + id: 1234, + label: "Group 1", + properties: [ + { + label: "Page Title", + description: "The title of the page", + view: "textbox", + config: {maxChars: 500}, + hideLabel: false, + validation: {mandatory: true, mandatoryMessage: "", pattern: null, patternMessage: ""}, + readonly: false, + id: 441, + dataTypeKey: "0cc0eba1-9960-42c9-bf9b-60e150b429ae", + value: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.", + alias: "pageTitle", + editor: "Umbraco.TextBox", + isSensitive: false, + culture: null, + segment: null + } + ] + } + ] + } + }, + { + + elementType: { + alias: 'contentTypeAlias', + icon: "icon-document", + label: "Text" + }, + label: "{{pageTitle | truncate:true:36}}", + labelInterpolate: $interpolate("{{pageTitle | truncate:true:36}}"), + key: 3, + editor: "views/blockelements/labelblock/labelblock.editor.html", + content: { + variants: [ + { + language: { + isDefault: true + } + } + ], + tabs: [ + { + id: 1234, + label: "Group 1", + properties: [ + { + label: "Page Title", + description: "The title of the page", + view: "textbox", + config: {maxChars: 500}, + hideLabel: false, + validation: {mandatory: true, mandatoryMessage: "", pattern: null, patternMessage: ""}, + readonly: false, + id: 441, + dataTypeKey: "0cc0eba1-9960-42c9-bf9b-60e150b429ae", + value: "Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.", + alias: "pageTitle", + editor: "Umbraco.TextBox", + isSensitive: false, + culture: null, + segment: null + } + ] + } + ] + } + } + ]; + + function setDirty() { + if (vm.propertyForm) { + vm.propertyForm.$setDirty(); + } + }; + + function addNewBlock(index, type) { + + var block = angular.copy(type.prototype_paste_data); + + vm.blocks.splice(index, 0, block); + $scope.moveFocusToBlock = block; + + } + /* + function moveFocusToNextBlock(blockModel, $event) { + var index = vm.blocks.indexOf(blockModel); + if(index < vm.blocks.length) { + var nextBlock = vm.blocks[index+1]; + $scope.moveFocusToBlock = nextBlock; + } else { + showCreateOptions(blockModel, $event); + } + } + */ + + vm.showCreateOptionsFor = function(blockModel, $event) { + var index = vm.blocks.indexOf(blockModel); + $event.preventDefault(); + showCreateOptionsAt(index); + } + function showCreateOptionsAt(index) { + vm.quickMenuIndex = index; + vm.quickMenuVisible = true; + window.addEventListener("keydown", handleTypingInCreateOptions); + } + + function handleTypingInCreateOptions(event) { + if (event.ctrlKey || event.metaKey || event.altKey) + return; + + if ( + (event.keyCode === 13) // enter + || + (event.keyCode >= 48 && event.keyCode <= 90)// 0 to z + || + (event.keyCode >= 96 && event.keyCode <= 111)// numpads + || + (event.keyCode >= 186 && event.keyCode <= 222)// semi-colon and a lot of other special characters + ) { + // Continue writting... needs to know default text-element. if we have one. + } + } + + function hideCreateOptions() { + vm.quickMenuVisible = false; + window.removeEventListener("keydown", handleTypingInCreateOptions); + } + + vm.onCreateOptionsBlur = function($event) { + + if(!$($event.relatedTarget).is(".umb-block-list__block--create-bar > button")) { + hideCreateOptions(); + } + + } + + vm.getBlockLabel = function(block) { + + var name = ""; + + var props = new Object(); + + var tab = block.content.tabs[0]; + // TODO: need to look up all tabs... + for(const property of tab.properties) { + props[property.alias] = property.value; + } + + if(block.labelInterpolate) { + return block.labelInterpolate(props); + } + + return "block.label"; + } + + vm.deleteBlock = function(block) { + var index = vm.blocks.indexOf(block); + if(index !== -1) { + vm.blocks.splice(index, 1); + } + if(vm.quickMenuIndex > index) { + vm.quickMenuIndex--; + } + } + + vm.editBlock = function(blockModel) { + + var elementEditor = { + block: blockModel, + view: "views/common/infiniteeditors/elementeditor/elementeditor.html", + size: "large", + submit: function(model) { + blockModel.content = model.block.content; + editorService.close(); + }, + close: function() { + editorService.close(); + } + }; + + // open property settings editor + editorService.open(elementEditor); + } + + vm.showCreateDialog = function (createIndex, $event) { + + if (vm.blockTypePicker) { + return; + } + + if (vm.availableBlockTypes.length === 0) { + return; + } + + vm.blockTypePicker = { + show: true, + size: vm.availableBlockTypes.length > 6 ? "medium" : "small", + filter: vm.availableBlockTypes.length > 12 ? true : false, + orderBy: "$index", + view: "itempicker", + event: $event, + availableItems: vm.availableBlockTypes, + submit: function (model) { + if (model && model.selectedItem) { + addNewBlock(createIndex, model.selectedItem); + } + vm.blockTypePicker.close(); + }, + close: function () { + vm.blockTypePicker.show = false; + vm.blockTypePicker = null; + } + }; + + }; + + vm.requestCopyBlock = function(block) { + console.log("copy") + } + vm.requestDeleteBlock = function(block) { + localizationService.localizeMany(["content_nestedContentDeleteItem", "general_delete", "general_cancel", "contentTypeEditor_yesDelete"]).then(function (data) { + const overlay = { + title: data[1], + content: data[0], + closeButtonLabel: data[2], + submitButtonLabel: data[3], + submitButtonStyle: "danger", + close: function () { + overlayService.close(); + }, + submit: function () { + vm.deleteBlock(block); + overlayService.close(); + } + }; + + overlayService.open(overlay); + }); + } + + vm.showCopy = clipboardService.isSupported(); + + + + vm.sorting = false; + vm.sortableOptions = { + axis: "y", + cursor: "grabbing", + handle: '.umb-block-list__block', + cancel: 'input,textarea,select,option', + classes: '.blockelement--dragging', + distance: 5, + tolerance: "pointer", + scroll: true, + start: function (ev, ui) { + $scope.$apply(function () { + vm.sorting = true; + }); + }, + update: function (ev, ui) { + setDirty(); + }, + stop: function (ev, ui) { + $scope.$apply(function () { + vm.sorting = false; + }); + } + }; + + $scope.blockApi = { + showCreateOptionsFor: vm.showCreateOptionsFor, + removeBlock: vm.removeBlock + } + + + var copyAllEntriesAction = { + labelKey: 'clipboard_labelForCopyAllEntries', + labelTokens: [model.label], + icon: 'documents', + method: function () {}, + isDisabled: true + } + + var propertyActions = [ + copyAllEntriesAction + ]; + + this.$onInit = function () { + if (this.umbProperty) { + this.umbProperty.setPropertyActions(propertyActions); + } + }; + + + } + +})(); diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.less b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.less new file mode 100644 index 0000000000..a26dda4202 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.less @@ -0,0 +1,217 @@ +@umb-block-list__item_minimum_height: 48px; + +.umb-block-list { + padding-bottom:10px; +} + +.umb-block-list__wrapper { + position: relative; + max-width: 1024px; + > .ui-sortable > .ui-sortable-helper > .umb-block-list__block > .umb-block-list__block--content > * { + box-shadow: 0px 5px 10px 0 rgba(0,0,0,.2); + } +} + +.umb-block-list__block { + position: relative; + width: 100%; + cursor: grab; + + .umb-block-list__block--head { + opacity: 0; + transition: opacity 120ms; + } + .umb-block-list__block--actions { + opacity: 0; + transition: opacity 120ms; + } + + &:hover, &:focus, &:focus-within { + .umb-block-list__block--head { + opacity: 1; + } + + .umb-block-list__block--actions { + opacity: 1; + } + } + + &:focus, &:focus-within { + .umb-block-list__block--head { + &::before { + background-color: @blueMid; + } + } + } +} + +.umb-block-list__block--head { + position: absolute; + top: 0; + left: -180px;// 160px from control-header + 20px from spacing. + bottom: 0; + width: 180px;// 160px from control-header + 20px from spacing. + user-select: none; + padding-top: 6px; + padding-right: 14px; + box-sizing: border-box; + color: @gray-5; + background-color: rgba(255, 255, 255, .96); + box-shadow: 0 0 6px 6px rgba(255, 255, 255, .96); + text-align: right; + &::before { + content: ''; + position: absolute; + top: 6px; + bottom: 6px; + right: 4px; + width: 1px; + background-color: @gray-10; + } + + small { + text-align: left; + margin-left: 4px; + margin-bottom: 4px; + } +} +label.umb-block-list__block--head { + cursor: grab; +} + +.umb-block-list__block--actions { + position: absolute; + top: 10px; + right: 10px; + font-size: 0; + background-color: rgba(255, 255, 255, .96); + border-radius: 14px; + padding-left: 5px; + padding-right: 5px; + .action { + display: inline-block; + color: @ui-action-discreet-type; + font-size: 18px; + padding: 5px; + &:hover { + color: @ui-action-discreet-type-hover; + } + } +} + +.umb-block-list__block--content { + position: relative; + width: 100%; + min-height: @umb-block-list__item_minimum_height; + background-color: @white; + border-radius: @baseBorderRadius; +} + + +.umb-block-list__block--create-button { + position: absolute; + width: 100%; + z-index:1; + opacity: 0; + outline: none; + height: 20px; + margin-top: -10px; + padding-top: 10px; + margin-bottom: -10px; + transition: opacity 240ms; + + &::before { + content: ''; + position: absolute; + background-color: @ui-outline; + border-radius: 2px; + top:9px; + right: 0; + left: 0; + height: 2px; + animation: umb-block-list__block--create-button 800ms ease-in-out infinite; + @keyframes umb-block-list__block--create-button { + 0% { opacity: 0.5; } + 50% { opacity: 1; } + 100% { opacity: 0.5; } + } + } + &::after { + content: "+"; + margin-left: auto; + margin-right: auto; + margin-top: -16px; + width: 28px; + height: 25px; + padding-bottom: 3px; + border-radius: 3em; + border: 2px solid @ui-outline; + display: flex; + justify-content: center; + align-items: center; + color: @ui-outline; + 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); + transition: transform 240ms ease-in; + } + &:focus { + &::after { + border: 2px solid @ui-outline; + } + } + &:hover, &:focus { + opacity: 1; + transition-duration: 120ms; + &::after { + transform: scale(1); + transition-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1.275); + } + } +} +/* +.umb-block-list__block--create-bar { + button { + display: inline-block; + width: 120px; + height: 120px; + border-radius: @baseBorderRadius; + text-align: center; + font-size: 12px; + i { + font-size: 30px; + line-height: 20px; + margin-bottom: 10px; + display: block; + } + } +} +*/ +.umb-block-list__create-button { + display: flex; + width: 100%; + align-items: center; + justify-content: center; + border: 1px dashed @ui-action-discreet-border; + color: @ui-action-discreet-type; + font-weight: bold; + margin: 2px 0; + padding: 5px 15px; + box-sizing: border-box; + border-radius: @baseBorderRadius; +} + +.umb-block-list__create-button:hover { + color: @ui-action-discreet-type-hover; + border-color: @ui-action-discreet-border-hover; + text-decoration: none; +} + +.umb-block-list__create-button.--disabled, +.umb-block-list__create-button.--disabled:hover { + color: @gray-7; + border-color: @gray-7; + cursor: default; +} diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.html new file mode 100644 index 0000000000..198dab4f5f --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.html @@ -0,0 +1 @@ + From f988a6a31b1a81459f0afb4c77e860cbd45a5bde Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Mon, 27 Jan 2020 07:37:29 +0100 Subject: [PATCH 010/377] more demo content --- .../blocklist/blocklist.component.js | 55 ++++++++++++++++++- 1 file changed, 52 insertions(+), 3 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.js index 6283bd1026..5fc17fcafd 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.js @@ -84,8 +84,8 @@ }, { alias: "contentTypeAlias", - name: "contentTypeName", - icon: "icon-text", + name: "Text", + icon: "icon-info", prototype_paste_data: { elementType: { alias: 'contentTypeAlias', @@ -132,7 +132,7 @@ }, { alias: "contentTypeAlias", - name: "contentTypeName", + name: "Image", icon: "icon-picture", prototype_paste_data: { elementType: { @@ -178,6 +178,55 @@ temp_image: "/umbraco/assets/img/login.jpg" } } + }, + { + alias: "contentTypeAlias", + name: "Inline editing", + icon: "icon-picture", + prototype_paste_data: { + elementType: { + alias: 'contentTypeAlias', + icon: "icon-document", + label: "Text" + }, + label: "Label", + editor: "views/blockelements/imageblock/imageblock.editor.html", + content: { + variants: [ + { + language: { + isDefault: true + } + } + ], + tabs: [ + { + id: 1234, + label: "Group 1", + properties: [ + { + label: "Page Title", + description: "The title of the page", + view: "textbox", + config: {maxChars: 500}, + hideLabel: false, + validation: {mandatory: true, mandatoryMessage: "", pattern: null, patternMessage: ""}, + readonly: false, + id: 441, + dataTypeKey: "0cc0eba1-9960-42c9-bf9b-60e150b429ae", + value: "Let's have a chat", + alias: "pageTitle", + editor: "Umbraco.TextBox", + isSensitive: false, + culture: null, + segment: null + } + ] + } + ], + temp_image: "/umbraco/assets/img/demo.png" + } + } } ]; From 772e46b93a4323e59120cf73181fdb629baac6fd Mon Sep 17 00:00:00 2001 From: Shannon Date: Thu, 30 Jan 2020 17:36:40 +1100 Subject: [PATCH 011/377] init commit of strongly typed models for implementations of a block editor and the upcoming block list editor --- .../Models/Blocks/BlockEditorModel.cs | 16 +++++++++++++++ .../Models/Blocks/BlockListLayoutReference.cs | 20 +++++++++++++++++++ .../Models/Blocks/BlockListModel.cs | 17 ++++++++++++++++ .../Models/Blocks/IBlockElement.cs | 18 +++++++++++++++++ src/Umbraco.Core/Umbraco.Core.csproj | 4 ++++ 5 files changed, 75 insertions(+) create mode 100644 src/Umbraco.Core/Models/Blocks/BlockEditorModel.cs create mode 100644 src/Umbraco.Core/Models/Blocks/BlockListLayoutReference.cs create mode 100644 src/Umbraco.Core/Models/Blocks/BlockListModel.cs create mode 100644 src/Umbraco.Core/Models/Blocks/IBlockElement.cs diff --git a/src/Umbraco.Core/Models/Blocks/BlockEditorModel.cs b/src/Umbraco.Core/Models/Blocks/BlockEditorModel.cs new file mode 100644 index 0000000000..7d00623ccf --- /dev/null +++ b/src/Umbraco.Core/Models/Blocks/BlockEditorModel.cs @@ -0,0 +1,16 @@ +using System.Collections.Generic; +using Umbraco.Core.Models.PublishedContent; + +namespace Umbraco.Core.Models.Blocks +{ + /// + /// The base class for any strongly typed model for a Block editor implementation + /// + public abstract class BlockEditorModel + { + /// + /// The data items of the Block List editor + /// + public IEnumerable Data { get; } + } +} diff --git a/src/Umbraco.Core/Models/Blocks/BlockListLayoutReference.cs b/src/Umbraco.Core/Models/Blocks/BlockListLayoutReference.cs new file mode 100644 index 0000000000..444bb7249f --- /dev/null +++ b/src/Umbraco.Core/Models/Blocks/BlockListLayoutReference.cs @@ -0,0 +1,20 @@ +using System; +using Umbraco.Core.Models.PublishedContent; + +namespace Umbraco.Core.Models.Blocks +{ + /// + /// Represents a layout item for the Block List editor + /// + public class BlockListLayoutReference : IBlockElement + { + public BlockListLayoutReference(Udi udi, IPublishedElement settings) + { + Udi = udi ?? throw new ArgumentNullException(nameof(udi)); + Settings = settings ?? throw new ArgumentNullException(nameof(settings)); + } + + public Udi Udi { get; } + public IPublishedElement Settings { get; } + } +} diff --git a/src/Umbraco.Core/Models/Blocks/BlockListModel.cs b/src/Umbraco.Core/Models/Blocks/BlockListModel.cs new file mode 100644 index 0000000000..5331ca3891 --- /dev/null +++ b/src/Umbraco.Core/Models/Blocks/BlockListModel.cs @@ -0,0 +1,17 @@ +using System.Collections.Generic; + +namespace Umbraco.Core.Models.Blocks +{ + /// + /// The strongly typed model for the Block List editor + /// + public class BlockListModel : BlockEditorModel + { + /// + /// The layout items of the Block List editor + /// + public IEnumerable Layout { get; } + + + } +} diff --git a/src/Umbraco.Core/Models/Blocks/IBlockElement.cs b/src/Umbraco.Core/Models/Blocks/IBlockElement.cs new file mode 100644 index 0000000000..eeb5a73e2c --- /dev/null +++ b/src/Umbraco.Core/Models/Blocks/IBlockElement.cs @@ -0,0 +1,18 @@ +namespace Umbraco.Core.Models.Blocks +{ + // TODO: IBlockElement doesn't make sense, this is a reference to an actual element with some settings + // and always has to do with the "Layout", should possibly be called IBlockReference or IBlockLayout or IBlockLayoutReference + + /// + /// Represents a data item for a Block editor implementation + /// + /// + /// + /// see: https://github.com/umbraco/rfcs/blob/907f3758cf59a7b6781296a60d57d537b3b60b8c/cms/0011-block-data-structure.md#strongly-typed + /// + public interface IBlockElement + { + Udi Udi { get; } + TSettings Settings { get; } + } +} diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index a8e3fc2988..8bf0b9105f 100755 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -129,6 +129,10 @@ --> + + + + From 292c76df0b6f48335171373e604355774822cb7b Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 31 Jan 2020 15:59:27 +1100 Subject: [PATCH 012/377] Getting models and value converter setup along with tests --- src/Umbraco.Core/Constants-PropertyEditors.cs | 5 + .../Models/Blocks/BlockEditorModel.cs | 17 ++- .../Models/Blocks/BlockListLayoutReference.cs | 9 +- .../Models/Blocks/BlockListModel.cs | 16 ++- .../PropertyEditors/BlockListEditor.cs | 12 ++ src/Umbraco.Core/Umbraco.Core.csproj | 1 + .../BlockListPropertyValueConverterTests.cs | 91 +++++++++++++++ .../Published/NestedContentTests.cs | 6 +- src/Umbraco.Tests/Umbraco.Tests.csproj | 1 + .../BlockEditorPropertyEditor.cs | 17 +++ .../PropertyEditors/BlockListConfiguration.cs | 24 ++++ .../BlockListPropertyEditor.cs | 21 ++++ .../NestedContentConfiguration.cs | 1 + .../ValueConverters/BlockEditorConverter.cs | 45 ++++++++ .../BlockListPropertyValueConverter.cs | 107 ++++++++++++++++++ .../NestedContentManyValueConverter.cs | 6 +- .../NestedContentSingleValueConverter.cs | 6 +- .../NestedContentValueConverterBase.cs | 34 +----- src/Umbraco.Web/Umbraco.Web.csproj | 5 + 19 files changed, 382 insertions(+), 42 deletions(-) create mode 100644 src/Umbraco.Core/PropertyEditors/BlockListEditor.cs create mode 100644 src/Umbraco.Tests/PropertyEditors/BlockListPropertyValueConverterTests.cs create mode 100644 src/Umbraco.Web/PropertyEditors/BlockEditorPropertyEditor.cs create mode 100644 src/Umbraco.Web/PropertyEditors/BlockListConfiguration.cs create mode 100644 src/Umbraco.Web/PropertyEditors/BlockListPropertyEditor.cs create mode 100644 src/Umbraco.Web/PropertyEditors/ValueConverters/BlockEditorConverter.cs create mode 100644 src/Umbraco.Web/PropertyEditors/ValueConverters/BlockListPropertyValueConverter.cs diff --git a/src/Umbraco.Core/Constants-PropertyEditors.cs b/src/Umbraco.Core/Constants-PropertyEditors.cs index eb2b3525a7..753cd72116 100644 --- a/src/Umbraco.Core/Constants-PropertyEditors.cs +++ b/src/Umbraco.Core/Constants-PropertyEditors.cs @@ -36,6 +36,11 @@ namespace Umbraco.Core /// public static class Aliases { + /// + /// CheckBox List. + /// + public const string BlockList = "Umbraco.BlockList"; + /// /// CheckBox List. /// diff --git a/src/Umbraco.Core/Models/Blocks/BlockEditorModel.cs b/src/Umbraco.Core/Models/Blocks/BlockEditorModel.cs index 7d00623ccf..307b69f6e0 100644 --- a/src/Umbraco.Core/Models/Blocks/BlockEditorModel.cs +++ b/src/Umbraco.Core/Models/Blocks/BlockEditorModel.cs @@ -1,4 +1,6 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; +using System.Runtime.Serialization; using Umbraco.Core.Models.PublishedContent; namespace Umbraco.Core.Models.Blocks @@ -8,9 +10,20 @@ namespace Umbraco.Core.Models.Blocks /// public abstract class BlockEditorModel { + protected BlockEditorModel(IEnumerable data) + { + Data = data ?? throw new ArgumentNullException(nameof(data)); + } + + public BlockEditorModel() + { + } + + /// /// The data items of the Block List editor /// - public IEnumerable Data { get; } + [DataMember(Name = "data")] + public IEnumerable Data { get; set; } } } diff --git a/src/Umbraco.Core/Models/Blocks/BlockListLayoutReference.cs b/src/Umbraco.Core/Models/Blocks/BlockListLayoutReference.cs index 444bb7249f..163b564c9f 100644 --- a/src/Umbraco.Core/Models/Blocks/BlockListLayoutReference.cs +++ b/src/Umbraco.Core/Models/Blocks/BlockListLayoutReference.cs @@ -1,4 +1,5 @@ using System; +using System.Runtime.Serialization; using Umbraco.Core.Models.PublishedContent; namespace Umbraco.Core.Models.Blocks @@ -6,6 +7,7 @@ namespace Umbraco.Core.Models.Blocks /// /// Represents a layout item for the Block List editor /// + [DataContract(Name = "blockListLayout", Namespace = "")] public class BlockListLayoutReference : IBlockElement { public BlockListLayoutReference(Udi udi, IPublishedElement settings) @@ -14,7 +16,10 @@ namespace Umbraco.Core.Models.Blocks Settings = settings ?? throw new ArgumentNullException(nameof(settings)); } - public Udi Udi { get; } - public IPublishedElement Settings { get; } + [DataMember(Name = "udi")] + public Udi Udi { get; set; } + + [DataMember(Name = "settings")] + public IPublishedElement Settings { get; set; } } } diff --git a/src/Umbraco.Core/Models/Blocks/BlockListModel.cs b/src/Umbraco.Core/Models/Blocks/BlockListModel.cs index 5331ca3891..36dd8af7c1 100644 --- a/src/Umbraco.Core/Models/Blocks/BlockListModel.cs +++ b/src/Umbraco.Core/Models/Blocks/BlockListModel.cs @@ -1,16 +1,30 @@ using System.Collections.Generic; +using System.Runtime.Serialization; +using Umbraco.Core.Models.PublishedContent; namespace Umbraco.Core.Models.Blocks { /// /// The strongly typed model for the Block List editor /// + [DataContract(Name = "blockList", Namespace = "")] public class BlockListModel : BlockEditorModel { + public BlockListModel(IEnumerable data, IEnumerable layout) + : base(data) + { + Layout = layout; + } + + public BlockListModel() + { + } + /// /// The layout items of the Block List editor /// - public IEnumerable Layout { get; } + [DataMember(Name = "layout")] + public IEnumerable Layout { get; set; } } diff --git a/src/Umbraco.Core/PropertyEditors/BlockListEditor.cs b/src/Umbraco.Core/PropertyEditors/BlockListEditor.cs new file mode 100644 index 0000000000..16c6176624 --- /dev/null +++ b/src/Umbraco.Core/PropertyEditors/BlockListEditor.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; + +namespace Umbraco.Core.PropertyEditors +{ + public class BlockListEditor + { + } +} diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index 8bf0b9105f..33796a93bf 100755 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -133,6 +133,7 @@ + diff --git a/src/Umbraco.Tests/PropertyEditors/BlockListPropertyValueConverterTests.cs b/src/Umbraco.Tests/PropertyEditors/BlockListPropertyValueConverterTests.cs new file mode 100644 index 0000000000..f345bf136c --- /dev/null +++ b/src/Umbraco.Tests/PropertyEditors/BlockListPropertyValueConverterTests.cs @@ -0,0 +1,91 @@ +using Moq; +using NUnit.Framework; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Umbraco.Core; +using Umbraco.Core.Logging; +using Umbraco.Core.Models.PublishedContent; +using Umbraco.Web.PropertyEditors; +using Umbraco.Web.PropertyEditors.ValueConverters; +using Umbraco.Web.PublishedCache; + +namespace Umbraco.Tests.PropertyEditors +{ + [TestFixture] + public class BlockListPropertyValueConverterTests + { + private BlockListPropertyValueConverter Create() + { + var publishedSnapshotAccessor = Mock.Of(); + var publishedModelFactory = Mock.Of(); + var editor = new BlockListPropertyValueConverter( + Mock.Of(), + publishedModelFactory, + new BlockEditorConverter(publishedSnapshotAccessor, publishedModelFactory)); + return editor; + } + + [Test] + public void Is_Converter_For() + { + var editor = Create(); + Assert.IsTrue(editor.IsConverter(Mock.Of(x => x.EditorAlias == Constants.PropertyEditors.Aliases.BlockList))); + Assert.IsFalse(editor.IsConverter(Mock.Of(x => x.EditorAlias == Constants.PropertyEditors.Aliases.NestedContent))); + } + + [Test] + public void Get_Value_Type_Multiple() + { + var editor = Create(); + var config = new BlockListConfiguration + { + ElementTypes = new[] { + new BlockListConfiguration.ElementType + { + Alias = "Test1" + }, + new BlockListConfiguration.ElementType + { + Alias = "Test2" + } + } + }; + + var dataType = new PublishedDataType(1, "test", new Lazy(() => config)); + var propType = Mock.Of(x => x.DataType == dataType); + + var valueType = editor.GetPropertyValueType(propType); + + Assert.AreEqual(typeof(IEnumerable), valueType); + } + + [Test] + public void Get_Value_Type_Single() + { + var editor = Create(); + var config = new BlockListConfiguration + { + ElementTypes = new[] { + new BlockListConfiguration.ElementType + { + Alias = "Test1" + } + } + }; + + var dataType = new PublishedDataType(1, "test", new Lazy(() => config)); + var propType = Mock.Of(x => x.DataType == dataType); + + var valueType = editor.GetPropertyValueType(propType); + + var modelType = typeof(IEnumerable<>).MakeGenericType(ModelType.For(config.ElementTypes[0].Alias)); + + // we can't compare the exact match of types because ModelType.For generates a new/different type even if the same alias is used + Assert.AreEqual(modelType.FullName, valueType.FullName); + } + + + } +} diff --git a/src/Umbraco.Tests/Published/NestedContentTests.cs b/src/Umbraco.Tests/Published/NestedContentTests.cs index adfb9d3b6b..a102b9f93e 100644 --- a/src/Umbraco.Tests/Published/NestedContentTests.cs +++ b/src/Umbraco.Tests/Published/NestedContentTests.cs @@ -119,10 +119,12 @@ namespace Umbraco.Tests.Published .Setup(x => x.PublishedSnapshot) .Returns(publishedSnapshot.Object); + var blockEditorConverter = new BlockEditorConverter(publishedSnapshotAccessor.Object, publishedModelFactory.Object); + var converters = new PropertyValueConverterCollection(new IPropertyValueConverter[] { - new NestedContentSingleValueConverter(publishedSnapshotAccessor.Object, publishedModelFactory.Object, proflog), - new NestedContentManyValueConverter(publishedSnapshotAccessor.Object, publishedModelFactory.Object, proflog), + new NestedContentSingleValueConverter(blockEditorConverter, publishedModelFactory.Object, proflog), + new NestedContentManyValueConverter(blockEditorConverter, publishedModelFactory.Object, proflog), }); var factory = new PublishedContentTypeFactory(publishedModelFactory.Object, converters, dataTypeService); diff --git a/src/Umbraco.Tests/Umbraco.Tests.csproj b/src/Umbraco.Tests/Umbraco.Tests.csproj index ff923bb04b..2d980461d2 100644 --- a/src/Umbraco.Tests/Umbraco.Tests.csproj +++ b/src/Umbraco.Tests/Umbraco.Tests.csproj @@ -145,6 +145,7 @@ + diff --git a/src/Umbraco.Web/PropertyEditors/BlockEditorPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/BlockEditorPropertyEditor.cs new file mode 100644 index 0000000000..b9ee1b84fb --- /dev/null +++ b/src/Umbraco.Web/PropertyEditors/BlockEditorPropertyEditor.cs @@ -0,0 +1,17 @@ +using Umbraco.Core.Logging; +using Umbraco.Core.PropertyEditors; + +namespace Umbraco.Web.PropertyEditors +{ + /// + /// Abstract class for block editor based editors + /// + public abstract class BlockEditorPropertyEditor : DataEditor + { + public const string ContentTypeAliasPropertyKey = "contentTypeAlias"; + + public BlockEditorPropertyEditor(ILogger logger) : base(logger) + { + } + } +} diff --git a/src/Umbraco.Web/PropertyEditors/BlockListConfiguration.cs b/src/Umbraco.Web/PropertyEditors/BlockListConfiguration.cs new file mode 100644 index 0000000000..95366db486 --- /dev/null +++ b/src/Umbraco.Web/PropertyEditors/BlockListConfiguration.cs @@ -0,0 +1,24 @@ +using Newtonsoft.Json; +using Umbraco.Core.PropertyEditors; + +namespace Umbraco.Web.PropertyEditors +{ + + /// + /// The configuration object for the Block List editor + /// + public class BlockListConfiguration + { + [ConfigurationField("elementTypes", "Element Types", "views/propertyeditors/blocklist/blocklist.elementtypepicker.html", Description = "Select the Element Types to use as models for the items.")] + public ElementType[] ElementTypes { get; set; } + + // TODO: Fill me in + + public class ElementType + { + [JsonProperty("elementTypeAlias")] + public string Alias { get; set; } + } + + } +} diff --git a/src/Umbraco.Web/PropertyEditors/BlockListPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/BlockListPropertyEditor.cs new file mode 100644 index 0000000000..f0148f7a9a --- /dev/null +++ b/src/Umbraco.Web/PropertyEditors/BlockListPropertyEditor.cs @@ -0,0 +1,21 @@ +using Umbraco.Core; +using Umbraco.Core.Logging; +using Umbraco.Core.PropertyEditors; +using Umbraco.Core.Services; + +namespace Umbraco.Web.PropertyEditors +{ + + [DataEditor( + Constants.PropertyEditors.Aliases.BlockList, + "Block List", + "blocklist", + Icon = "icon-list", + Group = Constants.PropertyEditors.Groups.Lists)] + public class BlockListPropertyEditor : BlockEditorPropertyEditor + { + public BlockListPropertyEditor(ILogger logger) : base(logger) + { + } + } +} diff --git a/src/Umbraco.Web/PropertyEditors/NestedContentConfiguration.cs b/src/Umbraco.Web/PropertyEditors/NestedContentConfiguration.cs index 0f53207462..89190883c8 100644 --- a/src/Umbraco.Web/PropertyEditors/NestedContentConfiguration.cs +++ b/src/Umbraco.Web/PropertyEditors/NestedContentConfiguration.cs @@ -3,6 +3,7 @@ using Umbraco.Core.PropertyEditors; namespace Umbraco.Web.PropertyEditors { + /// /// Represents the configuration for the nested content value editor. /// diff --git a/src/Umbraco.Web/PropertyEditors/ValueConverters/BlockEditorConverter.cs b/src/Umbraco.Web/PropertyEditors/ValueConverters/BlockEditorConverter.cs new file mode 100644 index 0000000000..0ab9b86572 --- /dev/null +++ b/src/Umbraco.Web/PropertyEditors/ValueConverters/BlockEditorConverter.cs @@ -0,0 +1,45 @@ +using Newtonsoft.Json.Linq; +using System; +using System.Collections.Generic; +using Umbraco.Core.Models.PublishedContent; +using Umbraco.Core.PropertyEditors; +using Umbraco.Web.PublishedCache; + +namespace Umbraco.Web.PropertyEditors.ValueConverters +{ + public sealed class BlockEditorConverter + { + private readonly IPublishedSnapshotAccessor _publishedSnapshotAccessor; + private readonly IPublishedModelFactory _publishedModelFactory; + + public BlockEditorConverter(IPublishedSnapshotAccessor publishedSnapshotAccessor, IPublishedModelFactory publishedModelFactory) + { + _publishedSnapshotAccessor = publishedSnapshotAccessor; + _publishedModelFactory = publishedModelFactory; + } + + public IPublishedElement ConvertToElement( + JObject sourceObject, string contentTypeAliasPropertyKey, + PropertyCacheLevel referenceCacheLevel, bool preview) + { + var elementTypeAlias = sourceObject[contentTypeAliasPropertyKey]?.ToObject(); + if (string.IsNullOrEmpty(elementTypeAlias)) + return null; + + // only convert element types - content types will cause an exception when PublishedModelFactory creates the model + var publishedContentType = _publishedSnapshotAccessor.PublishedSnapshot.Content.GetContentType(elementTypeAlias); + if (publishedContentType == null || publishedContentType.IsElement == false) + return null; + + var propertyValues = sourceObject.ToObject>(); + + if (!propertyValues.TryGetValue("key", out var keyo) + || !Guid.TryParse(keyo.ToString(), out var key)) + key = Guid.Empty; + + IPublishedElement element = new PublishedElement(publishedContentType, key, propertyValues, preview, referenceCacheLevel, _publishedSnapshotAccessor); + element = _publishedModelFactory.CreateModel(element); + return element; + } + } +} diff --git a/src/Umbraco.Web/PropertyEditors/ValueConverters/BlockListPropertyValueConverter.cs b/src/Umbraco.Web/PropertyEditors/ValueConverters/BlockListPropertyValueConverter.cs new file mode 100644 index 0000000000..dceaa9995e --- /dev/null +++ b/src/Umbraco.Web/PropertyEditors/ValueConverters/BlockListPropertyValueConverter.cs @@ -0,0 +1,107 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using System; +using System.Collections.Generic; +using System.Linq; +using Umbraco.Core; +using Umbraco.Core.Logging; +using Umbraco.Core.Models.Blocks; +using Umbraco.Core.Models.PublishedContent; +using Umbraco.Core.PropertyEditors; +using Umbraco.Core.PropertyEditors.ValueConverters; + +namespace Umbraco.Web.PropertyEditors.ValueConverters +{ + + [DefaultPropertyValueConverter(typeof(JsonValueConverter))] + public class BlockListPropertyValueConverter : PropertyValueConverterBase + { + private readonly IProfilingLogger _proflog; + private readonly IPublishedModelFactory _publishedModelFactory; + private readonly BlockEditorConverter _blockConverter; + + public BlockListPropertyValueConverter(IProfilingLogger proflog, IPublishedModelFactory publishedModelFactory, BlockEditorConverter blockConverter) + { + _proflog = proflog; + _publishedModelFactory = publishedModelFactory; + _blockConverter = blockConverter; + } + + /// + public override bool IsConverter(IPublishedPropertyType propertyType) + => propertyType.EditorAlias.InvariantEquals(Constants.PropertyEditors.Aliases.BlockList); + + /// + public override Type GetPropertyValueType(IPublishedPropertyType propertyType) + { + var contentTypes = propertyType.DataType.ConfigurationAs().ElementTypes; + return contentTypes.Length == 1 + ? typeof(IEnumerable<>).MakeGenericType(ModelType.For(contentTypes[0].Alias)) + : typeof(IEnumerable); + } + + /// + public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) + => PropertyCacheLevel.Element; + + /// + public override object ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object source, bool preview) + { + return source?.ToString(); + } + + /// + public override object ConvertIntermediateToObject(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object inter, bool preview) + { + using (_proflog.DebugDuration($"ConvertPropertyToBlockList ({propertyType.DataType.Id})")) + { + var configuration = propertyType.DataType.ConfigurationAs(); + var contentTypes = configuration.ElementTypes; + var elements = contentTypes.Length == 1 + ? (IList)_publishedModelFactory.CreateModelList(contentTypes[0].Alias) + : new List(); + + var layout = new List(); + var model = new BlockListModel(elements, layout); + + var value = (string)inter; + if (string.IsNullOrWhiteSpace(value)) return model; + + var objects = JsonConvert.DeserializeObject(value); + if (objects.Count == 0) return model; + + var jsonLayout = objects["layout"] as JObject; + if (jsonLayout == null) return model; + + var jsonData = objects["data"] as JArray; + if (jsonData == null) return model; + + var blockListLayouts = jsonLayout[Constants.PropertyEditors.Aliases.BlockList] as JArray; + if (blockListLayouts == null) return model; + + foreach(var blockListLayout in blockListLayouts) + { + var settingsJson = blockListLayout["settings"] as JObject; + if (settingsJson == null) continue; + + var element = _blockConverter.ConvertToElement(settingsJson, BlockEditorPropertyEditor.ContentTypeAliasPropertyKey, referenceCacheLevel, preview); + if (element == null) continue; + + var layoutRef = new BlockListLayoutReference(blockListLayout.Value("udi"), element); + layout.Add(layoutRef); + } + + foreach (var data in jsonData.Cast()) + { + var element = _blockConverter.ConvertToElement(data, BlockEditorPropertyEditor.ContentTypeAliasPropertyKey, referenceCacheLevel, preview); + if (element == null) continue; + elements.Add(element); + } + + return model; + } + } + + + } +} diff --git a/src/Umbraco.Web/PropertyEditors/ValueConverters/NestedContentManyValueConverter.cs b/src/Umbraco.Web/PropertyEditors/ValueConverters/NestedContentManyValueConverter.cs index 4a25049695..b961048851 100644 --- a/src/Umbraco.Web/PropertyEditors/ValueConverters/NestedContentManyValueConverter.cs +++ b/src/Umbraco.Web/PropertyEditors/ValueConverters/NestedContentManyValueConverter.cs @@ -23,8 +23,8 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters /// /// Initializes a new instance of the class. /// - public NestedContentManyValueConverter(IPublishedSnapshotAccessor publishedSnapshotAccessor, IPublishedModelFactory publishedModelFactory, IProfilingLogger proflog) - : base(publishedSnapshotAccessor, publishedModelFactory) + public NestedContentManyValueConverter(BlockEditorConverter blockEditorConverter, IPublishedModelFactory publishedModelFactory, IProfilingLogger proflog) + : base(blockEditorConverter, publishedModelFactory) { _proflog = proflog; } @@ -71,7 +71,7 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters foreach (var sourceObject in objects) { - var element = ConvertToElement(sourceObject, referenceCacheLevel, preview); + var element = BlockEditorConverter.ConvertToElement(sourceObject, NestedContentPropertyEditor.ContentTypeAliasPropertyKey, referenceCacheLevel, preview); if (element != null) elements.Add(element); } diff --git a/src/Umbraco.Web/PropertyEditors/ValueConverters/NestedContentSingleValueConverter.cs b/src/Umbraco.Web/PropertyEditors/ValueConverters/NestedContentSingleValueConverter.cs index c9c99615f6..b3a2a9294d 100644 --- a/src/Umbraco.Web/PropertyEditors/ValueConverters/NestedContentSingleValueConverter.cs +++ b/src/Umbraco.Web/PropertyEditors/ValueConverters/NestedContentSingleValueConverter.cs @@ -22,8 +22,8 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters /// /// Initializes a new instance of the class. /// - public NestedContentSingleValueConverter(IPublishedSnapshotAccessor publishedSnapshotAccessor, IPublishedModelFactory publishedModelFactory, IProfilingLogger proflog) - : base(publishedSnapshotAccessor, publishedModelFactory) + public NestedContentSingleValueConverter(BlockEditorConverter blockEditorConverter, IPublishedModelFactory publishedModelFactory, IProfilingLogger proflog) + : base(blockEditorConverter, publishedModelFactory) { _proflog = proflog; } @@ -65,7 +65,7 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters if (objects.Count > 1) throw new InvalidOperationException(); - return ConvertToElement(objects[0], referenceCacheLevel, preview); + return BlockEditorConverter.ConvertToElement(objects[0], NestedContentPropertyEditor.ContentTypeAliasPropertyKey, referenceCacheLevel, preview); } } } diff --git a/src/Umbraco.Web/PropertyEditors/ValueConverters/NestedContentValueConverterBase.cs b/src/Umbraco.Web/PropertyEditors/ValueConverters/NestedContentValueConverterBase.cs index 7c18d8ebca..4295daf5fe 100644 --- a/src/Umbraco.Web/PropertyEditors/ValueConverters/NestedContentValueConverterBase.cs +++ b/src/Umbraco.Web/PropertyEditors/ValueConverters/NestedContentValueConverterBase.cs @@ -1,23 +1,19 @@ -using System; -using System.Collections.Generic; -using Newtonsoft.Json.Linq; -using Umbraco.Core; +using Umbraco.Core; using Umbraco.Core.Models.PublishedContent; using Umbraco.Core.PropertyEditors; -using Umbraco.Web.PublishedCache; namespace Umbraco.Web.PropertyEditors.ValueConverters { public abstract class NestedContentValueConverterBase : PropertyValueConverterBase { - private readonly IPublishedSnapshotAccessor _publishedSnapshotAccessor; - protected NestedContentValueConverterBase(IPublishedSnapshotAccessor publishedSnapshotAccessor, IPublishedModelFactory publishedModelFactory) + protected NestedContentValueConverterBase(BlockEditorConverter blockEditorConverter, IPublishedModelFactory publishedModelFactory) { - _publishedSnapshotAccessor = publishedSnapshotAccessor; + BlockEditorConverter = blockEditorConverter; PublishedModelFactory = publishedModelFactory; } + protected BlockEditorConverter BlockEditorConverter { get; } protected IPublishedModelFactory PublishedModelFactory { get; } public static bool IsNested(IPublishedPropertyType publishedProperty) @@ -39,26 +35,6 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters return IsNested(publishedProperty) && !IsNestedSingle(publishedProperty); } - protected IPublishedElement ConvertToElement(JObject sourceObject, PropertyCacheLevel referenceCacheLevel, bool preview) - { - var elementTypeAlias = sourceObject[NestedContentPropertyEditor.ContentTypeAliasPropertyKey]?.ToObject(); - if (string.IsNullOrEmpty(elementTypeAlias)) - return null; - - // only convert element types - content types will cause an exception when PublishedModelFactory creates the model - var publishedContentType = _publishedSnapshotAccessor.PublishedSnapshot.Content.GetContentType(elementTypeAlias); - if (publishedContentType == null || publishedContentType.IsElement == false) - return null; - - var propertyValues = sourceObject.ToObject>(); - - if (!propertyValues.TryGetValue("key", out var keyo) - || !Guid.TryParse(keyo.ToString(), out var key)) - key = Guid.Empty; - - IPublishedElement element = new PublishedElement(publishedContentType, key, propertyValues, preview, referenceCacheLevel, _publishedSnapshotAccessor); - element = PublishedModelFactory.CreateModel(element); - return element; - } + } } diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index 0111a36993..e660108cad 100755 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -233,8 +233,13 @@ + + + + + From a23f93c557ee5826b9227da58c770b9d3bc775d0 Mon Sep 17 00:00:00 2001 From: Shannon Date: Mon, 3 Feb 2020 16:45:54 +1100 Subject: [PATCH 013/377] Gets the block list property value converter tests running and written --- src/Umbraco.Core/Contants-UdiEntityType.cs | 3 + .../Models/Blocks/BlockListLayoutReference.cs | 2 +- .../PropertyValueConverterBase.cs | 5 + .../BlockListPropertyValueConverterTests.cs | 216 +++++++++++++++--- .../BlockListPropertyValueConverter.cs | 35 ++- 5 files changed, 220 insertions(+), 41 deletions(-) diff --git a/src/Umbraco.Core/Contants-UdiEntityType.cs b/src/Umbraco.Core/Contants-UdiEntityType.cs index 75a137bd2e..1ed862f328 100644 --- a/src/Umbraco.Core/Contants-UdiEntityType.cs +++ b/src/Umbraco.Core/Contants-UdiEntityType.cs @@ -25,6 +25,7 @@ namespace Umbraco.Core { Unknown, UdiType.Unknown }, { AnyGuid, UdiType.GuidUdi }, + { Element, UdiType.GuidUdi }, { Document, UdiType.GuidUdi }, { DocumentBlueprint, UdiType.GuidUdi }, { Media, UdiType.GuidUdi }, @@ -64,6 +65,8 @@ namespace Umbraco.Core public const string AnyGuid = "any-guid"; // that one is for tests + public const string Element = "element"; + public const string Document = "document"; public const string DocumentBlueprint = "document-blueprint"; diff --git a/src/Umbraco.Core/Models/Blocks/BlockListLayoutReference.cs b/src/Umbraco.Core/Models/Blocks/BlockListLayoutReference.cs index 163b564c9f..a2fd6c9df9 100644 --- a/src/Umbraco.Core/Models/Blocks/BlockListLayoutReference.cs +++ b/src/Umbraco.Core/Models/Blocks/BlockListLayoutReference.cs @@ -13,7 +13,7 @@ namespace Umbraco.Core.Models.Blocks public BlockListLayoutReference(Udi udi, IPublishedElement settings) { Udi = udi ?? throw new ArgumentNullException(nameof(udi)); - Settings = settings ?? throw new ArgumentNullException(nameof(settings)); + Settings = settings; // can be null } [DataMember(Name = "udi")] diff --git a/src/Umbraco.Core/PropertyEditors/PropertyValueConverterBase.cs b/src/Umbraco.Core/PropertyEditors/PropertyValueConverterBase.cs index 3b6ebc610c..a584ea2a9c 100644 --- a/src/Umbraco.Core/PropertyEditors/PropertyValueConverterBase.cs +++ b/src/Umbraco.Core/PropertyEditors/PropertyValueConverterBase.cs @@ -30,18 +30,23 @@ namespace Umbraco.Core.PropertyEditors return value != null && (!(value is string) || string.IsNullOrWhiteSpace((string) value) == false); } + /// public virtual Type GetPropertyValueType(IPublishedPropertyType propertyType) => typeof (object); + /// public virtual PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) => PropertyCacheLevel.Snapshot; + /// public virtual object ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object source, bool preview) => source; + /// public virtual object ConvertIntermediateToObject(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object inter, bool preview) => inter; + /// public virtual object ConvertIntermediateToXPath(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object inter, bool preview) => inter?.ToString() ?? string.Empty; } diff --git a/src/Umbraco.Tests/PropertyEditors/BlockListPropertyValueConverterTests.cs b/src/Umbraco.Tests/PropertyEditors/BlockListPropertyValueConverterTests.cs index f345bf136c..114642f89d 100644 --- a/src/Umbraco.Tests/PropertyEditors/BlockListPropertyValueConverterTests.cs +++ b/src/Umbraco.Tests/PropertyEditors/BlockListPropertyValueConverterTests.cs @@ -6,7 +6,9 @@ using System.Linq; using System.Text; using Umbraco.Core; using Umbraco.Core.Logging; +using Umbraco.Core.Models.Blocks; using Umbraco.Core.Models.PublishedContent; +using Umbraco.Core.PropertyEditors; using Umbraco.Web.PropertyEditors; using Umbraco.Web.PropertyEditors.ValueConverters; using Umbraco.Web.PublishedCache; @@ -16,10 +18,26 @@ namespace Umbraco.Tests.PropertyEditors [TestFixture] public class BlockListPropertyValueConverterTests { - private BlockListPropertyValueConverter Create() + /// + /// Setup mocks for IPublishedSnapshotAccessor + /// + /// + private IPublishedSnapshotAccessor GetPublishedSnapshotAccessor() { - var publishedSnapshotAccessor = Mock.Of(); - var publishedModelFactory = Mock.Of(); + var homeContentType = Mock.Of(x => + x.IsElement == true + && x.Alias == "home"); + var contentCache = new Mock(); + contentCache.Setup(x => x.GetContentType("home")).Returns(homeContentType); + var publishedSnapshot = Mock.Of(x => x.Content == contentCache.Object); + var publishedSnapshotAccessor = Mock.Of(x => x.PublishedSnapshot == publishedSnapshot); + return publishedSnapshotAccessor; + } + + private BlockListPropertyValueConverter CreateConverter() + { + var publishedSnapshotAccessor = GetPublishedSnapshotAccessor(); + var publishedModelFactory = new NoopPublishedModelFactory(); var editor = new BlockListPropertyValueConverter( Mock.Of(), publishedModelFactory, @@ -27,21 +45,9 @@ namespace Umbraco.Tests.PropertyEditors return editor; } - [Test] - public void Is_Converter_For() + private BlockListConfiguration ConfigForMany() => new BlockListConfiguration { - var editor = Create(); - Assert.IsTrue(editor.IsConverter(Mock.Of(x => x.EditorAlias == Constants.PropertyEditors.Aliases.BlockList))); - Assert.IsFalse(editor.IsConverter(Mock.Of(x => x.EditorAlias == Constants.PropertyEditors.Aliases.NestedContent))); - } - - [Test] - public void Get_Value_Type_Multiple() - { - var editor = Create(); - var config = new BlockListConfiguration - { - ElementTypes = new[] { + ElementTypes = new[] { new BlockListConfiguration.ElementType { Alias = "Test1" @@ -51,7 +57,40 @@ namespace Umbraco.Tests.PropertyEditors Alias = "Test2" } } - }; + }; + + private BlockListConfiguration ConfigForSingle() => new BlockListConfiguration + { + ElementTypes = new[] { + new BlockListConfiguration.ElementType + { + Alias = "Test1" + } + } + }; + + private IPublishedPropertyType GetPropertyType(BlockListConfiguration config) + { + var dataType = new PublishedDataType(1, "test", new Lazy(() => config)); + var propertyType = Mock.Of(x => + x.EditorAlias == Constants.PropertyEditors.Aliases.BlockList + && x.DataType == dataType); + return propertyType; + } + + [Test] + public void Is_Converter_For() + { + var editor = CreateConverter(); + Assert.IsTrue(editor.IsConverter(Mock.Of(x => x.EditorAlias == Constants.PropertyEditors.Aliases.BlockList))); + Assert.IsFalse(editor.IsConverter(Mock.Of(x => x.EditorAlias == Constants.PropertyEditors.Aliases.NestedContent))); + } + + [Test] + public void Get_Value_Type_Multiple() + { + var editor = CreateConverter(); + var config = ConfigForMany(); var dataType = new PublishedDataType(1, "test", new Lazy(() => config)); var propType = Mock.Of(x => x.DataType == dataType); @@ -64,16 +103,8 @@ namespace Umbraco.Tests.PropertyEditors [Test] public void Get_Value_Type_Single() { - var editor = Create(); - var config = new BlockListConfiguration - { - ElementTypes = new[] { - new BlockListConfiguration.ElementType - { - Alias = "Test1" - } - } - }; + var editor = CreateConverter(); + var config = ConfigForSingle(); var dataType = new PublishedDataType(1, "test", new Lazy(() => config)); var propType = Mock.Of(x => x.DataType == dataType); @@ -86,6 +117,135 @@ namespace Umbraco.Tests.PropertyEditors Assert.AreEqual(modelType.FullName, valueType.FullName); } + [Test] + public void Convert_Null_Empty() + { + var editor = CreateConverter(); + var config = ConfigForMany(); + var propertyType = GetPropertyType(config); + var publishedElement = Mock.Of(); + + string json = null; + var converted = editor.ConvertIntermediateToObject(publishedElement, propertyType, PropertyCacheLevel.None, json, false) as BlockListModel; + + Assert.IsNotNull(converted); + Assert.AreEqual(0, converted.Data.Count()); + Assert.AreEqual(0, converted.Layout.Count()); + + json = string.Empty; + converted = editor.ConvertIntermediateToObject(publishedElement, propertyType, PropertyCacheLevel.None, json, false) as BlockListModel; + + Assert.IsNotNull(converted); + Assert.AreEqual(0, converted.Data.Count()); + Assert.AreEqual(0, converted.Layout.Count()); + } + + [Test] + public void Convert_Valid_Empty_Json() + { + var editor = CreateConverter(); + var config = ConfigForMany(); + var propertyType = GetPropertyType(config); + var publishedElement = Mock.Of(); + + var json = "{}"; + var converted = editor.ConvertIntermediateToObject(publishedElement, propertyType, PropertyCacheLevel.None, json, false) as BlockListModel; + + Assert.IsNotNull(converted); + Assert.AreEqual(0, converted.Data.Count()); + Assert.AreEqual(0, converted.Layout.Count()); + + json = @"{ +layout: [], +data: []}"; + converted = editor.ConvertIntermediateToObject(publishedElement, propertyType, PropertyCacheLevel.None, json, false) as BlockListModel; + + Assert.IsNotNull(converted); + Assert.AreEqual(0, converted.Data.Count()); + Assert.AreEqual(0, converted.Layout.Count()); + + // Even though there is a layout, there is no data, so the conversion will result in zero elements in total + json = @" +{ + layout: { + '" + Constants.PropertyEditors.Aliases.BlockList + @"': [ + { + 'udi': 'umb://element/e7dba547615b4e9ab4ab2a7674845bc9', + 'settings': {} + } + ] + }, + data: [] +}"; + + converted = editor.ConvertIntermediateToObject(publishedElement, propertyType, PropertyCacheLevel.None, json, false) as BlockListModel; + + Assert.IsNotNull(converted); + Assert.AreEqual(0, converted.Data.Count()); + Assert.AreEqual(0, converted.Layout.Count()); + + // Even though there is a layout and data, the data is invalid (missing required keys) so the conversion will result in zero elements in total + json = @" +{ + layout: { + '" + Constants.PropertyEditors.Aliases.BlockList + @"': [ + { + 'udi': 'umb://element/e7dba547615b4e9ab4ab2a7674845bc9', + 'settings': {} + } + ] + }, + data: [ + { + 'udi': 'umb://element/e7dba547615b4e9ab4ab2a7674845bc9' + } + ] +}"; + + converted = editor.ConvertIntermediateToObject(publishedElement, propertyType, PropertyCacheLevel.None, json, false) as BlockListModel; + + Assert.IsNotNull(converted); + Assert.AreEqual(0, converted.Data.Count()); + Assert.AreEqual(0, converted.Layout.Count()); + } + + [Test] + public void Convert_Valid_Json() + { + var editor = CreateConverter(); + var config = ConfigForMany(); + var propertyType = GetPropertyType(config); + var publishedElement = Mock.Of(); + + var json = @" +{ + layout: { + '" + Constants.PropertyEditors.Aliases.BlockList + @"': [ + { + 'udi': 'umb://element/1304E1DDAC87439684FE8A399231CB3D', + 'settings': {} + } + ] + }, + data: [ + { + 'contentTypeAlias': 'home', + 'key': '1304E1DD-AC87-4396-84FE-8A399231CB3D' + } + ] +}"; + var converted = editor.ConvertIntermediateToObject(publishedElement, propertyType, PropertyCacheLevel.None, json, false) as BlockListModel; + + Assert.IsNotNull(converted); + Assert.AreEqual(1, converted.Data.Count()); + var item0 = converted.Data.ElementAt(0); + Assert.AreEqual(Guid.Parse("1304E1DD-AC87-4396-84FE-8A399231CB3D"), item0.Key); + Assert.AreEqual("home", item0.ContentType.Alias); + Assert.AreEqual(1, converted.Layout.Count()); + var layout0 = converted.Layout.ElementAt(0); + Assert.IsNull(layout0.Settings); + Assert.AreEqual(Udi.Parse("umb://element/1304E1DDAC87439684FE8A399231CB3D"), layout0.Udi); + } } } diff --git a/src/Umbraco.Web/PropertyEditors/ValueConverters/BlockListPropertyValueConverter.cs b/src/Umbraco.Web/PropertyEditors/ValueConverters/BlockListPropertyValueConverter.cs index dceaa9995e..ab289f048c 100644 --- a/src/Umbraco.Web/PropertyEditors/ValueConverters/BlockListPropertyValueConverter.cs +++ b/src/Umbraco.Web/PropertyEditors/ValueConverters/BlockListPropertyValueConverter.cs @@ -53,6 +53,8 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters /// public override object ConvertIntermediateToObject(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object inter, bool preview) { + // NOTE: The intermediate object is just a json string, we don't actually convert from source -> intermediate since source is always just a json string + using (_proflog.DebugDuration($"ConvertPropertyToBlockList ({propertyType.DataType.Id})")) { var configuration = propertyType.DataType.ConfigurationAs(); @@ -79,18 +81,7 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters var blockListLayouts = jsonLayout[Constants.PropertyEditors.Aliases.BlockList] as JArray; if (blockListLayouts == null) return model; - foreach(var blockListLayout in blockListLayouts) - { - var settingsJson = blockListLayout["settings"] as JObject; - if (settingsJson == null) continue; - - var element = _blockConverter.ConvertToElement(settingsJson, BlockEditorPropertyEditor.ContentTypeAliasPropertyKey, referenceCacheLevel, preview); - if (element == null) continue; - - var layoutRef = new BlockListLayoutReference(blockListLayout.Value("udi"), element); - layout.Add(layoutRef); - } - + // parse the data elements foreach (var data in jsonData.Cast()) { var element = _blockConverter.ConvertToElement(data, BlockEditorPropertyEditor.ContentTypeAliasPropertyKey, referenceCacheLevel, preview); @@ -98,6 +89,26 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters elements.Add(element); } + // if there's no elements just return since if there's no data it doesn't matter what is stored in layout + if (elements.Count == 0) return model; + + foreach (var blockListLayout in blockListLayouts) + { + var settingsJson = blockListLayout["settings"] as JObject; + if (settingsJson == null) continue; + + // the result of this can be null, that's ok + var element = _blockConverter.ConvertToElement(settingsJson, BlockEditorPropertyEditor.ContentTypeAliasPropertyKey, referenceCacheLevel, preview); + + if (!Udi.TryParse(blockListLayout.Value("udi"), out var udi)) + continue; + + var layoutRef = new BlockListLayoutReference(udi, element); + layout.Add(layoutRef); + } + + + return model; } } From 9f0becaf522eeb09ceeee271554aff5ce1a948e1 Mon Sep 17 00:00:00 2001 From: Shannon Date: Mon, 3 Feb 2020 18:31:18 +1100 Subject: [PATCH 014/377] Adds another test to show how to access the data block from a layout element --- .../Models/Blocks/BlockListLayoutReference.cs | 18 ++++- .../BlockListPropertyValueConverterTests.cs | 79 +++++++++++++++++++ .../BlockListPropertyValueConverter.cs | 19 +++-- src/Umbraco.Web/Runtime/WebInitialComposer.cs | 1 + 4 files changed, 108 insertions(+), 9 deletions(-) diff --git a/src/Umbraco.Core/Models/Blocks/BlockListLayoutReference.cs b/src/Umbraco.Core/Models/Blocks/BlockListLayoutReference.cs index a2fd6c9df9..09c6c76478 100644 --- a/src/Umbraco.Core/Models/Blocks/BlockListLayoutReference.cs +++ b/src/Umbraco.Core/Models/Blocks/BlockListLayoutReference.cs @@ -10,16 +10,32 @@ namespace Umbraco.Core.Models.Blocks [DataContract(Name = "blockListLayout", Namespace = "")] public class BlockListLayoutReference : IBlockElement { - public BlockListLayoutReference(Udi udi, IPublishedElement settings) + public BlockListLayoutReference(Udi udi, IPublishedElement data, IPublishedElement settings) { Udi = udi ?? throw new ArgumentNullException(nameof(udi)); + Data = data ?? throw new ArgumentNullException(nameof(data)); Settings = settings; // can be null } + /// + /// The Id of the data item + /// [DataMember(Name = "udi")] public Udi Udi { get; set; } + /// + /// The settings for the layout item + /// [DataMember(Name = "settings")] public IPublishedElement Settings { get; set; } + + /// + /// The data item referenced + /// + /// + /// This is ignored from serialization since it is just a reference to the actual data element + /// + [IgnoreDataMember] + public IPublishedElement Data { get; set; } } } diff --git a/src/Umbraco.Tests/PropertyEditors/BlockListPropertyValueConverterTests.cs b/src/Umbraco.Tests/PropertyEditors/BlockListPropertyValueConverterTests.cs index 114642f89d..ca687f94a6 100644 --- a/src/Umbraco.Tests/PropertyEditors/BlockListPropertyValueConverterTests.cs +++ b/src/Umbraco.Tests/PropertyEditors/BlockListPropertyValueConverterTests.cs @@ -207,6 +207,31 @@ data: []}"; Assert.IsNotNull(converted); Assert.AreEqual(0, converted.Data.Count()); Assert.AreEqual(0, converted.Layout.Count()); + + // Everthing is ok except the udi reference in the layout doesn't match the data so it will be empty + json = @" +{ + layout: { + '" + Constants.PropertyEditors.Aliases.BlockList + @"': [ + { + 'udi': 'umb://element/1304E1DDAC87439684FE8A399231CB3D', + 'settings': {} + } + ] + }, + data: [ + { + 'contentTypeAlias': 'home', + 'key': '1304E1DD-0000-4396-84FE-8A399231CB3D' + } + ] +}"; + + converted = editor.ConvertIntermediateToObject(publishedElement, propertyType, PropertyCacheLevel.None, json, false) as BlockListModel; + + Assert.IsNotNull(converted); + Assert.AreEqual(1, converted.Data.Count()); + Assert.AreEqual(0, converted.Layout.Count()); } [Test] @@ -247,5 +272,59 @@ data: []}"; Assert.AreEqual(Udi.Parse("umb://element/1304E1DDAC87439684FE8A399231CB3D"), layout0.Udi); } + [Test] + public void Get_Data_From_Layout_Item() + { + var editor = CreateConverter(); + var config = ConfigForMany(); + var propertyType = GetPropertyType(config); + var publishedElement = Mock.Of(); + + var json = @" +{ + layout: { + '" + Constants.PropertyEditors.Aliases.BlockList + @"': [ + { + 'udi': 'umb://element/1304E1DDAC87439684FE8A399231CB3D', + 'settings': {} + }, + { + 'udi': 'umb://element/0A4A416E547D464FABCC6F345C17809A', + 'settings': {} + } + ] + }, + data: [ + { + 'contentTypeAlias': 'home', + 'key': '1304E1DD-AC87-4396-84FE-8A399231CB3D' + }, + { + 'contentTypeAlias': 'home', + 'key': 'E05A0347-0442-4AB3-A520-E048E6197E79' + }, + { + 'contentTypeAlias': 'home', + 'key': '0A4A416E-547D-464F-ABCC-6F345C17809A' + } + ] +}"; + + var converted = editor.ConvertIntermediateToObject(publishedElement, propertyType, PropertyCacheLevel.None, json, false) as BlockListModel; + + Assert.IsNotNull(converted); + Assert.AreEqual(3, converted.Data.Count()); + Assert.AreEqual(2, converted.Layout.Count()); + + var item0 = converted.Layout.ElementAt(0); + Assert.AreEqual(Guid.Parse("1304E1DD-AC87-4396-84FE-8A399231CB3D"), item0.Data.Key); + Assert.AreEqual("home", item0.Data.ContentType.Alias); + + var item1 = converted.Layout.ElementAt(1); + Assert.AreEqual(Guid.Parse("0A4A416E-547D-464F-ABCC-6F345C17809A"), item1.Data.Key); + Assert.AreEqual("home", item1.Data.ContentType.Alias); + + } + } } diff --git a/src/Umbraco.Web/PropertyEditors/ValueConverters/BlockListPropertyValueConverter.cs b/src/Umbraco.Web/PropertyEditors/ValueConverters/BlockListPropertyValueConverter.cs index ab289f048c..3878b8d605 100644 --- a/src/Umbraco.Web/PropertyEditors/ValueConverters/BlockListPropertyValueConverter.cs +++ b/src/Umbraco.Web/PropertyEditors/ValueConverters/BlockListPropertyValueConverter.cs @@ -59,12 +59,13 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters { var configuration = propertyType.DataType.ConfigurationAs(); var contentTypes = configuration.ElementTypes; - var elements = contentTypes.Length == 1 + var elements = (contentTypes.Length == 1 ? (IList)_publishedModelFactory.CreateModelList(contentTypes[0].Alias) - : new List(); + : new List()) + .ToDictionary(x => x.Key, x => x); var layout = new List(); - var model = new BlockListModel(elements, layout); + var model = new BlockListModel(elements.Values, layout); var value = (string)inter; if (string.IsNullOrWhiteSpace(value)) return model; @@ -86,7 +87,7 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters { var element = _blockConverter.ConvertToElement(data, BlockEditorPropertyEditor.ContentTypeAliasPropertyKey, referenceCacheLevel, preview); if (element == null) continue; - elements.Add(element); + elements[element.Key] = element; } // if there's no elements just return since if there's no data it doesn't matter what is stored in layout @@ -100,15 +101,17 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters // the result of this can be null, that's ok var element = _blockConverter.ConvertToElement(settingsJson, BlockEditorPropertyEditor.ContentTypeAliasPropertyKey, referenceCacheLevel, preview); - if (!Udi.TryParse(blockListLayout.Value("udi"), out var udi)) + if (!Udi.TryParse(blockListLayout.Value("udi"), out var udi) || !(udi is GuidUdi guidUdi)) continue; - var layoutRef = new BlockListLayoutReference(udi, element); + // get the data reference + if (!elements.TryGetValue(guidUdi.Guid, out var data)) + continue; + + var layoutRef = new BlockListLayoutReference(udi, data, element); layout.Add(layoutRef); } - - return model; } } diff --git a/src/Umbraco.Web/Runtime/WebInitialComposer.cs b/src/Umbraco.Web/Runtime/WebInitialComposer.cs index 1c4121da0c..79b24175ea 100644 --- a/src/Umbraco.Web/Runtime/WebInitialComposer.cs +++ b/src/Umbraco.Web/Runtime/WebInitialComposer.cs @@ -108,6 +108,7 @@ namespace Umbraco.Web.Runtime composition.RegisterUnique(); composition.RegisterUnique(); composition.RegisterUnique(); + composition.RegisterUnique(); // register the umbraco helper - this is Transient! very important! // also, if not level.Run, we cannot really use the helper (during upgrade...) From 199ea404ed072ddbec500f56f975f64d764824c6 Mon Sep 17 00:00:00 2001 From: Shannon Date: Mon, 3 Feb 2020 18:37:07 +1100 Subject: [PATCH 015/377] Makes model props readonly/immutable --- src/Umbraco.Core/Models/Blocks/BlockListLayoutReference.cs | 6 +++--- src/Umbraco.Core/Models/Blocks/BlockListModel.cs | 6 +----- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/src/Umbraco.Core/Models/Blocks/BlockListLayoutReference.cs b/src/Umbraco.Core/Models/Blocks/BlockListLayoutReference.cs index 09c6c76478..19b30e6ea6 100644 --- a/src/Umbraco.Core/Models/Blocks/BlockListLayoutReference.cs +++ b/src/Umbraco.Core/Models/Blocks/BlockListLayoutReference.cs @@ -21,13 +21,13 @@ namespace Umbraco.Core.Models.Blocks /// The Id of the data item /// [DataMember(Name = "udi")] - public Udi Udi { get; set; } + public Udi Udi { get; } /// /// The settings for the layout item /// [DataMember(Name = "settings")] - public IPublishedElement Settings { get; set; } + public IPublishedElement Settings { get; } /// /// The data item referenced @@ -36,6 +36,6 @@ namespace Umbraco.Core.Models.Blocks /// This is ignored from serialization since it is just a reference to the actual data element /// [IgnoreDataMember] - public IPublishedElement Data { get; set; } + public IPublishedElement Data { get; } } } diff --git a/src/Umbraco.Core/Models/Blocks/BlockListModel.cs b/src/Umbraco.Core/Models/Blocks/BlockListModel.cs index 36dd8af7c1..089ca7e6a3 100644 --- a/src/Umbraco.Core/Models/Blocks/BlockListModel.cs +++ b/src/Umbraco.Core/Models/Blocks/BlockListModel.cs @@ -16,15 +16,11 @@ namespace Umbraco.Core.Models.Blocks Layout = layout; } - public BlockListModel() - { - } - /// /// The layout items of the Block List editor /// [DataMember(Name = "layout")] - public IEnumerable Layout { get; set; } + public IEnumerable Layout { get; } } From 89c489cfe55fada40d1d4f98f816eeecbfa8373b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Mon, 3 Feb 2020 14:53:45 +0100 Subject: [PATCH 016/377] inline-editor + interpolation for label + element editor as a component --- src/Umbraco.Web.UI.Client/src/less/belle.less | 1 + .../inlineblock.editor.controller.js | 20 +++++ .../inlineblock/inlineblock.editor.html | 10 +++ .../inlineblock/inlineblock.editor.less | 67 ++++++++++++++++ .../labelblock/labelblock.editor.html | 2 +- .../labelblock/labelblock.editor.less | 1 + .../elementeditor.component.html | 38 ++++++++++ .../elementeditor/elementeditor.component.js | 23 ++++++ .../elementeditor/elementeditor.controller.js | 2 + .../elementeditor/elementeditor.html | 49 +++--------- .../blocklist/blocklist.component.js | 76 +++++++++++++++++-- .../blocklist/blocklist.component.less | 16 ++-- 12 files changed, 252 insertions(+), 53 deletions(-) create mode 100644 src/Umbraco.Web.UI.Client/src/views/blockelements/inlineblock/inlineblock.editor.controller.js create mode 100644 src/Umbraco.Web.UI.Client/src/views/blockelements/inlineblock/inlineblock.editor.html create mode 100644 src/Umbraco.Web.UI.Client/src/views/blockelements/inlineblock/inlineblock.editor.less create mode 100644 src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/elementeditor/elementeditor.component.html create mode 100644 src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/elementeditor/elementeditor.component.js diff --git a/src/Umbraco.Web.UI.Client/src/less/belle.less b/src/Umbraco.Web.UI.Client/src/less/belle.less index 695b456fd6..f264038e93 100644 --- a/src/Umbraco.Web.UI.Client/src/less/belle.less +++ b/src/Umbraco.Web.UI.Client/src/less/belle.less @@ -226,6 +226,7 @@ // Block Elements @import "../views/blockelements/labelblock/labelblock.editor.less"; +@import "../views/blockelements/inlineblock/inlineblock.editor.less"; @import "../views/blockelements/textareablock/textareablock.editor.less"; @import "../views/blockelements/imageblock/imageblock.editor.less"; diff --git a/src/Umbraco.Web.UI.Client/src/views/blockelements/inlineblock/inlineblock.editor.controller.js b/src/Umbraco.Web.UI.Client/src/views/blockelements/inlineblock/inlineblock.editor.controller.js new file mode 100644 index 0000000000..c7cac47d85 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/blockelements/inlineblock/inlineblock.editor.controller.js @@ -0,0 +1,20 @@ +(function () { + 'use strict'; + + function InlineBlockEditor($scope) { + + const bc = this; + + bc.isOpen = false; + bc.caretIconType = "icon-navigation-right"; + + bc.openBlock = function() { + bc.isOpen = !bc.isOpen; + bc.caretIconType = bc.isOpen ? "icon-navigation-down" : "icon-navigation-right"; + } + + } + + angular.module("umbraco").controller("Umbraco.PropertyEditors.BlockEditor.InlineBlockEditor", InlineBlockEditor); + +})(); diff --git a/src/Umbraco.Web.UI.Client/src/views/blockelements/inlineblock/inlineblock.editor.html b/src/Umbraco.Web.UI.Client/src/views/blockelements/inlineblock/inlineblock.editor.html new file mode 100644 index 0000000000..78e23b17f3 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/blockelements/inlineblock/inlineblock.editor.html @@ -0,0 +1,10 @@ +
+ +
+ +
+
diff --git a/src/Umbraco.Web.UI.Client/src/views/blockelements/inlineblock/inlineblock.editor.less b/src/Umbraco.Web.UI.Client/src/views/blockelements/inlineblock/inlineblock.editor.less new file mode 100644 index 0000000000..dc014c5daf --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/blockelements/inlineblock/inlineblock.editor.less @@ -0,0 +1,67 @@ +.blockelement-inlineblock-editor { + + margin-bottom: 2px; + margin-top: 2px; + border: 1px solid @gray-9; + border-radius: @baseBorderRadius; + transition: border-color 120ms; + + &:not(.--open):hover { + border-color: @gray-8; + } + + > button { + width: 100%; + min-height: 48px; + + cursor: pointer; + color: @ui-action-discreet-type; + + text-align: left; + padding-left: 10px; + padding-bottom: 2px; + + user-select: none; + + .caret { + transform: rotate(-90deg); + transition: transform 80ms ease-out; + } + i { + font-size: 22px; + display: inline-block; + vertical-align: middle; + } + span { + display: inline-block; + vertical-align: middle; + } + + &:hover { + color: @ui-action-discreet-type-hover; + border-color: @gray-8; + } + } + + &.--open { + border-color: @gray-8; + box-shadow: 0 0 2px 0px rgba(0, 0, 0, 0.05); + > button { + > .caret { + transform: rotate(0deg); + } + } + } +} +.blockelement-inlineblock-editor__inner { + border-top: 1px solid @gray-8; + + .umb-group-panel { + background-color: transparent; + box-shadow: none; + margin-bottom: 0; + } + .umb-group-panel__header { + display:none; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/views/blockelements/labelblock/labelblock.editor.html b/src/Umbraco.Web.UI.Client/src/views/blockelements/labelblock/labelblock.editor.html index f0678c76e9..bd8f9558fd 100644 --- a/src/Umbraco.Web.UI.Client/src/views/blockelements/labelblock/labelblock.editor.html +++ b/src/Umbraco.Web.UI.Client/src/views/blockelements/labelblock/labelblock.editor.html @@ -1,4 +1,4 @@ diff --git a/src/Umbraco.Web.UI.Client/src/views/blockelements/labelblock/labelblock.editor.less b/src/Umbraco.Web.UI.Client/src/views/blockelements/labelblock/labelblock.editor.less index 610773528b..5ce53aece4 100644 --- a/src/Umbraco.Web.UI.Client/src/views/blockelements/labelblock/labelblock.editor.less +++ b/src/Umbraco.Web.UI.Client/src/views/blockelements/labelblock/labelblock.editor.less @@ -5,6 +5,7 @@ border: 1px solid @gray-9; border-radius: @baseBorderRadius; + cursor: pointer; color: @ui-action-discreet-type; text-align: left; diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/elementeditor/elementeditor.component.html b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/elementeditor/elementeditor.component.html new file mode 100644 index 0000000000..09724f6619 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/elementeditor/elementeditor.component.html @@ -0,0 +1,38 @@ +
+ +
+ +
+
{{ group.label }}
+
+ +
+ + +
+ + +
+ +
+
+ +
+ + + + + +
+
diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/elementeditor/elementeditor.component.js b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/elementeditor/elementeditor.component.js new file mode 100644 index 0000000000..6ba59f0703 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/elementeditor/elementeditor.component.js @@ -0,0 +1,23 @@ +(function () { + 'use strict'; + + angular + .module('umbraco.directives') + .component('umbElementEditor', { + templateUrl: 'views/common/infiniteeditors/elementeditor/elementeditor.component.html', + controller: ElementEditorComponentController, + controllerAs: 'vm', + bindings: { + content: '=' + } + }); + + function ElementEditorComponentController() { + + const vm = this; + + // TODO: we might not need this.. + + } + +})(); diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/elementeditor/elementeditor.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/elementeditor/elementeditor.controller.js index d421418297..ebabe86617 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/elementeditor/elementeditor.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/elementeditor/elementeditor.controller.js @@ -7,6 +7,8 @@ angular.module("umbraco") vm.content = $scope.model.block.content; + vm.title = $scope.model.block.label; + vm.saveAndClose = function() { if ($scope.model && $scope.model.submit) { $scope.model.submit($scope.model); diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/elementeditor/elementeditor.html b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/elementeditor/elementeditor.html index 71aab124e6..2df5dcd923 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/elementeditor/elementeditor.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/elementeditor/elementeditor.html @@ -4,55 +4,24 @@ + +
-
-
- -
- -
-
{{ group.label }}
-
- -
- - -
- - -
- -
-
- -
- - - - - -
-
+
-
diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.js index 5fc17fcafd..f3f8c2e5fe 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.js @@ -43,9 +43,10 @@ icon: "icon-document", label: "Text" }, - label: "{{pageTitle | truncate:true:36}}", + labelTemplate: "{{pageTitle | truncate:true:36}}", labelInterpolate: $interpolate("{{pageTitle | truncate:true:36}}"), editor: "views/blockelements/labelblock/labelblock.editor.html", + overlaySize: 'medium', content: { variants: [ { @@ -94,6 +95,7 @@ }, label: "Label", editor: "views/blockelements/textareablock/textareablock.editor.html", + overlaySize: 'medium', content: { variants: [ { @@ -142,6 +144,7 @@ }, label: "Label", editor: "views/blockelements/imageblock/imageblock.editor.html", + overlaySize: 'medium', content: { variants: [ { @@ -191,6 +194,7 @@ }, label: "Label", editor: "views/blockelements/imageblock/imageblock.editor.html", + overlaySize: 'medium', content: { variants: [ { @@ -234,6 +238,52 @@ // TODO: get icon, properties etc. from available types? vm.blocks = [ + { + elementType: { + alias: 'contentTypeAlias', + icon: "icon-document", + label: "Text" + }, + label: "{{pageTitle | truncate:true:36}}", + labelInterpolate: $interpolate("{{pageTitle | truncate:true:36}}"), + key: 1, + editor: "views/blockelements/inlineblock/inlineblock.editor.html", + overlaySize: 'medium', + content: { + variants: [ + { + language: { + isDefault: true + } + } + ], + tabs: [ + { + id: 1234, + label: "Group 1", + properties: [ + { + label: "Page Title", + description: "The title of the page", + view: "textbox", + config: {maxChars: 500}, + hideLabel: false, + validation: {mandatory: true, mandatoryMessage: "", pattern: null, patternMessage: ""}, + readonly: false, + id: 441, + dataTypeKey: "0cc0eba1-9960-42c9-bf9b-60e150b429ae", + value: "The purpose of lorem ipsum is to create a natural looking block of text (sentence, paragraph, page, etc.) that doesn't distract from the layout. A practice not without controversy, laying out pages with meaningless filler text can be very useful when the focus is meant to be on design, not content.", + alias: "pageTitle", + editor: "Umbraco.TextBox", + isSensitive: false, + culture: null, + segment: null + } + ] + } + ] + } + }, { elementType: { alias: 'contentTypeAlias', @@ -244,6 +294,7 @@ labelInterpolate: $interpolate("{{pageTitle | truncate:true:36}}"), key: 1, editor: "views/blockelements/labelblock/labelblock.editor.html", + overlaySize: 'medium', content: { variants: [ { @@ -289,6 +340,7 @@ labelInterpolate: $interpolate("{{pageTitle | truncate:true:36}}"), key: 2, editor: "views/blockelements/labelblock/labelblock.editor.html", + overlaySize: 'medium', content: { variants: [ { @@ -335,6 +387,7 @@ labelInterpolate: $interpolate("{{pageTitle | truncate:true:36}}"), key: 3, editor: "views/blockelements/labelblock/labelblock.editor.html", + overlaySize: 'medium', content: { variants: [ { @@ -438,10 +491,12 @@ } } - - vm.getBlockLabel = function(block) { - var name = ""; + function getBlockLabel(block) { + + console.log("getBlockLabel", block) + + // TODO: we should do something about this for performance. var props = new Object(); @@ -473,7 +528,7 @@ var elementEditor = { block: blockModel, view: "views/common/infiniteeditors/elementeditor/elementeditor.html", - size: "large", + size: blockModel.overlaySize, submit: function(model) { blockModel.content = model.block.content; editorService.close(); @@ -551,7 +606,7 @@ vm.sortableOptions = { axis: "y", cursor: "grabbing", - handle: '.umb-block-list__block', + handle: '.blockelement__draggable-element', cancel: 'input,textarea,select,option', classes: '.blockelement--dragging', distance: 5, @@ -596,6 +651,15 @@ } }; + // TODO: We need to investigate if we can do a specific watch on each block, so we dont re-render all blocks. + $scope.$watch('vm.blocks', onBlocksUpdated, true); + function onBlocksUpdated(newVal, oldVal){ + console.log("onBlocksUpdated"); + for(const block of vm.blocks) { + block.label = getBlockLabel(block); + } + } + } diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.less b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.less index a26dda4202..78f4138897 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.less +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.less @@ -15,7 +15,6 @@ .umb-block-list__block { position: relative; width: 100%; - cursor: grab; .umb-block-list__block--head { opacity: 0; @@ -107,6 +106,10 @@ label.umb-block-list__block--head { border-radius: @baseBorderRadius; } +.blockelement__draggable-element { + cursor: grab; +} + .umb-block-list__block--create-button { position: absolute; @@ -114,10 +117,10 @@ label.umb-block-list__block--head { z-index:1; opacity: 0; outline: none; - height: 20px; - margin-top: -10px; - padding-top: 10px; - margin-bottom: -10px; + height: 12px; + margin-top: -6px; + padding-top: 6px; + margin-bottom: -6px; transition: opacity 240ms; &::before { @@ -125,7 +128,7 @@ label.umb-block-list__block--head { position: absolute; background-color: @ui-outline; border-radius: 2px; - top:9px; + top:5px; right: 0; left: 0; height: 2px; @@ -141,6 +144,7 @@ label.umb-block-list__block--head { margin-left: auto; margin-right: auto; margin-top: -16px; + margin-bottom: -16px; width: 28px; height: 25px; padding-bottom: 3px; From 898594cea3135511e2f9ad77fb530ca807dae425 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Mon, 3 Feb 2020 15:08:43 +0100 Subject: [PATCH 017/377] adjustment of border-radius --- .../views/propertyeditors/blocklist/blocklist.component.less | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.less b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.less index 78f4138897..8708fdf000 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.less +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.less @@ -84,7 +84,7 @@ label.umb-block-list__block--head { right: 10px; font-size: 0; background-color: rgba(255, 255, 255, .96); - border-radius: 14px; + border-radius: 16px; padding-left: 5px; padding-right: 5px; .action { From fcad2c4863903ed0fcd5059afa60e2be847020d1 Mon Sep 17 00:00:00 2001 From: Warren Buckley Date: Tue, 4 Feb 2020 10:00:39 +0000 Subject: [PATCH 018/377] Fix up unit test as a new Data Editor was added for Block Editor --- src/Umbraco.Tests/Composing/TypeLoaderTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Tests/Composing/TypeLoaderTests.cs b/src/Umbraco.Tests/Composing/TypeLoaderTests.cs index 7459ae848b..9cd4f39c17 100644 --- a/src/Umbraco.Tests/Composing/TypeLoaderTests.cs +++ b/src/Umbraco.Tests/Composing/TypeLoaderTests.cs @@ -268,7 +268,7 @@ AnotherContentFinder public void GetDataEditors() { var types = _typeLoader.GetDataEditors(); - Assert.AreEqual(38, types.Count()); + Assert.AreEqual(39, types.Count()); } /// From 03fe861b9407877067b6c25d770fbf4a50db5859 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Tue, 4 Feb 2020 14:40:20 +0100 Subject: [PATCH 019/377] dragable --- .../src/views/blockelements/imageblock/imageblock.editor.html | 2 +- .../views/blockelements/textareablock/textareablock.editor.html | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/blockelements/imageblock/imageblock.editor.html b/src/Umbraco.Web.UI.Client/src/views/blockelements/imageblock/imageblock.editor.html index c77f19ba67..0b82d2202d 100644 --- a/src/Umbraco.Web.UI.Client/src/views/blockelements/imageblock/imageblock.editor.html +++ b/src/Umbraco.Web.UI.Client/src/views/blockelements/imageblock/imageblock.editor.html @@ -1,3 +1,3 @@ - diff --git a/src/Umbraco.Web.UI.Client/src/views/blockelements/textareablock/textareablock.editor.html b/src/Umbraco.Web.UI.Client/src/views/blockelements/textareablock/textareablock.editor.html index 405c8634b4..c64d42b6ac 100644 --- a/src/Umbraco.Web.UI.Client/src/views/blockelements/textareablock/textareablock.editor.html +++ b/src/Umbraco.Web.UI.Client/src/views/blockelements/textareablock/textareablock.editor.html @@ -1,5 +1,5 @@ -
+
From 8c02c33143403cfe6fe5de26cc1e27fd593bfc5d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Wed, 5 Feb 2020 11:16:31 +0100 Subject: [PATCH 020/377] style create-bar --- .../blocklist/blocklist.component.less | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.less b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.less index 8708fdf000..02670e88b0 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.less +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.less @@ -132,8 +132,8 @@ label.umb-block-list__block--head { right: 0; left: 0; height: 2px; - animation: umb-block-list__block--create-button 800ms ease-in-out infinite; - @keyframes umb-block-list__block--create-button { + animation: umb-block-list__block--create-button_before 800ms ease-in-out infinite; + @keyframes umb-block-list__block--create-button_before { 0% { opacity: 0.5; } 50% { opacity: 1; } 100% { opacity: 0.5; } @@ -143,8 +143,8 @@ label.umb-block-list__block--head { content: "+"; margin-left: auto; margin-right: auto; - margin-top: -16px; - margin-bottom: -16px; + margin-top: -18px; + margin-bottom: -18px; width: 28px; height: 25px; padding-bottom: 3px; @@ -160,6 +160,12 @@ label.umb-block-list__block--head { box-shadow: 0 0 0 2px rgba(255, 255, 255, .96); transform: scale(0); transition: transform 240ms ease-in; + animation: umb-block-list__block--create-button_after 800ms ease-in-out infinite; + @keyframes umb-block-list__block--create-button_after { + 0% { color: rgba(@ui-outline, 0.8); } + 50% { color: rgba(@ui-outline, 1); } + 100% { color: rgba(@ui-outline, 0.8); } + } } &:focus { &::after { From 41e866c43aba4200a2ce6d2df601f8322a81fdbd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Wed, 5 Feb 2020 11:16:40 +0100 Subject: [PATCH 021/377] more demo content --- .../blocklist/blocklist.component.js | 363 +++++++++++++----- 1 file changed, 264 insertions(+), 99 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.js index f3f8c2e5fe..35259eaf9f 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.js @@ -58,7 +58,7 @@ tabs: [ { id: 1234, - label: "Group 1", + label: "Content", properties: [ { label: "Page Title", @@ -76,6 +76,120 @@ isSensitive: false, culture: null, segment: null + }, + { + label: "Image", + description: "", + view: "mediapicker", + config: {multiPicker: false, + onlyImages: true, + disableFolderSelect: true, + startNodeId: "umb://media/1fd2ecaff3714c009306867fa4585e7a", + ignoreUserStartNodes: false, + idType: "udi" + }, + hideLabel: false, + validation: {mandatory: false, mandatoryMessage: "", pattern: null, patternMessage: ""}, + readonly: false, + id: 495, + dataTypeKey: "e26a8d91-a9d7-475b-bc3b-2a09f4743754", + value: "umb://media/fa763e0d0ceb408c8720365d57e06e32", + alias: "photo", + editor: "Umbraco.MediaPicker", + isSensitive: false, + culture: null, + segment: null + }, + { + label: "Image Description", + description: "The title of the page", + view: "textbox", + config: {maxChars: 500}, + hideLabel: false, + validation: {mandatory: true, mandatoryMessage: "", pattern: null, patternMessage: ""}, + readonly: false, + id: 442, + dataTypeKey: "0cc0eba1-9960-42c9-bf9b-60e150b429ae", + value: "Let's have a chat", + alias: "imageDesc", + editor: "Umbraco.TextBox", + isSensitive: false, + culture: null, + segment: null + } + ] + } + ] + } + } + }, + { + alias: "pageModule", + name: "Inline module", + icon: "icon-document", + prototype_paste_data: { + elementType: { + alias: 'contentTypeAlias', + icon: "icon-document", + label: "Text" + }, + label: "{{pageTitle | truncate:true:36}}", + labelInterpolate: $interpolate("{{pageTitle | truncate:true:36}}"), + key: 1, + editor: "views/blockelements/inlineblock/inlineblock.editor.html", + overlaySize: 'medium', + content: { + variants: [ + { + language: { + isDefault: true + } + } + ], + tabs: [ + { + id: 1234, + label: "Group 1", + properties: [ + { + label: "Image Title", + description: "The title on top of image", + view: "textbox", + config: {maxChars: 500}, + hideLabel: false, + validation: {mandatory: true, mandatoryMessage: "", pattern: null, patternMessage: ""}, + readonly: false, + id: 441, + dataTypeKey: "0cc0eba1-9960-42c9-bf9b-60e150b429ae", + value: "The purpose of lorem ipsum is to create a natural looking block of text (sentence, paragraph, page, etc.) that doesn't distract from the layout. A practice not without controversy, laying out pages with meaningless filler text can be very useful when the focus is meant to be on design, not content.", + alias: "imageTitle", + editor: "Umbraco.TextBox", + isSensitive: false, + culture: null, + segment: null + }, + { + label: "Image", + description: "", + view: "mediapicker", + config: {multiPicker: false, + onlyImages: true, + disableFolderSelect: true, + startNodeId: "umb://media/1fd2ecaff3714c009306867fa4585e7a", + ignoreUserStartNodes: false, + idType: "udi" + }, + hideLabel: false, + validation: {mandatory: false, mandatoryMessage: "", pattern: null, patternMessage: ""}, + readonly: false, + id: 495, + dataTypeKey: "e26a8d91-a9d7-475b-bc3b-2a09f4743754", + value: "umb://media/fa763e0d0ceb408c8720365d57e06e32", + alias: "photo", + editor: "Umbraco.MediaPicker", + isSensitive: false, + culture: null, + segment: null } ] } @@ -159,7 +273,30 @@ label: "Group 1", properties: [ { - label: "Page Title", + label: "Image", + description: "", + view: "mediapicker", + config: {multiPicker: false, + onlyImages: true, + disableFolderSelect: true, + startNodeId: "umb://media/1fd2ecaff3714c009306867fa4585e7a", + ignoreUserStartNodes: false, + idType: "udi" + }, + hideLabel: false, + validation: {mandatory: false, mandatoryMessage: "", pattern: null, patternMessage: ""}, + readonly: false, + id: 495, + dataTypeKey: "e26a8d91-a9d7-475b-bc3b-2a09f4743754", + value: "umb://media/fa763e0d0ceb408c8720365d57e06e32", + alias: "photo", + editor: "Umbraco.MediaPicker", + isSensitive: false, + culture: null, + segment: null + }, + { + label: "Image Description", description: "The title of the page", view: "textbox", config: {maxChars: 500}, @@ -169,7 +306,7 @@ id: 441, dataTypeKey: "0cc0eba1-9960-42c9-bf9b-60e150b429ae", value: "Let's have a chat", - alias: "pageTitle", + alias: "imageDesc", editor: "Umbraco.TextBox", isSensitive: false, culture: null, @@ -181,56 +318,6 @@ temp_image: "/umbraco/assets/img/login.jpg" } } - }, - { - alias: "contentTypeAlias", - name: "Inline editing", - icon: "icon-picture", - prototype_paste_data: { - elementType: { - alias: 'contentTypeAlias', - icon: "icon-document", - label: "Text" - }, - label: "Label", - editor: "views/blockelements/imageblock/imageblock.editor.html", - overlaySize: 'medium', - content: { - variants: [ - { - language: { - isDefault: true - } - } - ], - tabs: [ - { - id: 1234, - label: "Group 1", - properties: [ - { - label: "Page Title", - description: "The title of the page", - view: "textbox", - config: {maxChars: 500}, - hideLabel: false, - validation: {mandatory: true, mandatoryMessage: "", pattern: null, patternMessage: ""}, - readonly: false, - id: 441, - dataTypeKey: "0cc0eba1-9960-42c9-bf9b-60e150b429ae", - value: "Let's have a chat", - alias: "pageTitle", - editor: "Umbraco.TextBox", - isSensitive: false, - culture: null, - segment: null - } - ] - } - ], - temp_image: "/umbraco/assets/img/demo.png" - } - } } ]; @@ -238,52 +325,6 @@ // TODO: get icon, properties etc. from available types? vm.blocks = [ - { - elementType: { - alias: 'contentTypeAlias', - icon: "icon-document", - label: "Text" - }, - label: "{{pageTitle | truncate:true:36}}", - labelInterpolate: $interpolate("{{pageTitle | truncate:true:36}}"), - key: 1, - editor: "views/blockelements/inlineblock/inlineblock.editor.html", - overlaySize: 'medium', - content: { - variants: [ - { - language: { - isDefault: true - } - } - ], - tabs: [ - { - id: 1234, - label: "Group 1", - properties: [ - { - label: "Page Title", - description: "The title of the page", - view: "textbox", - config: {maxChars: 500}, - hideLabel: false, - validation: {mandatory: true, mandatoryMessage: "", pattern: null, patternMessage: ""}, - readonly: false, - id: 441, - dataTypeKey: "0cc0eba1-9960-42c9-bf9b-60e150b429ae", - value: "The purpose of lorem ipsum is to create a natural looking block of text (sentence, paragraph, page, etc.) that doesn't distract from the layout. A practice not without controversy, laying out pages with meaningless filler text can be very useful when the focus is meant to be on design, not content.", - alias: "pageTitle", - editor: "Umbraco.TextBox", - isSensitive: false, - culture: null, - segment: null - } - ] - } - ] - } - }, { elementType: { alias: 'contentTypeAlias', @@ -324,6 +365,46 @@ isSensitive: false, culture: null, segment: null + }, + { + label: "Image", + description: "", + view: "mediapicker", + config: {multiPicker: false, + onlyImages: true, + disableFolderSelect: true, + startNodeId: "umb://media/1fd2ecaff3714c009306867fa4585e7a", + ignoreUserStartNodes: false, + idType: "udi" + }, + hideLabel: false, + validation: {mandatory: false, mandatoryMessage: "", pattern: null, patternMessage: ""}, + readonly: false, + id: 495, + dataTypeKey: "e26a8d91-a9d7-475b-bc3b-2a09f4743754", + value: "umb://media/fa763e0d0ceb408c8720365d57e06e32", + alias: "photo", + editor: "Umbraco.MediaPicker", + isSensitive: false, + culture: null, + segment: null + }, + { + label: "Image Description", + description: "The title of the page", + view: "textbox", + config: {maxChars: 500}, + hideLabel: false, + validation: {mandatory: true, mandatoryMessage: "", pattern: null, patternMessage: ""}, + readonly: false, + id: 442, + dataTypeKey: "0cc0eba1-9960-42c9-bf9b-60e150b429ae", + value: "Let's have a chat", + alias: "imageDesc", + editor: "Umbraco.TextBox", + isSensitive: false, + culture: null, + segment: null } ] } @@ -370,6 +451,46 @@ isSensitive: false, culture: null, segment: null + }, + { + label: "Image", + description: "", + view: "mediapicker", + config: {multiPicker: false, + onlyImages: true, + disableFolderSelect: true, + startNodeId: "umb://media/1fd2ecaff3714c009306867fa4585e7a", + ignoreUserStartNodes: false, + idType: "udi" + }, + hideLabel: false, + validation: {mandatory: false, mandatoryMessage: "", pattern: null, patternMessage: ""}, + readonly: false, + id: 495, + dataTypeKey: "e26a8d91-a9d7-475b-bc3b-2a09f4743754", + value: "umb://media/fa763e0d0ceb408c8720365d57e06e32", + alias: "photo", + editor: "Umbraco.MediaPicker", + isSensitive: false, + culture: null, + segment: null + }, + { + label: "Image Description", + description: "The title of the page", + view: "textbox", + config: {maxChars: 500}, + hideLabel: false, + validation: {mandatory: true, mandatoryMessage: "", pattern: null, patternMessage: ""}, + readonly: false, + id: 442, + dataTypeKey: "0cc0eba1-9960-42c9-bf9b-60e150b429ae", + value: "Let's have a chat", + alias: "imageDesc", + editor: "Umbraco.TextBox", + isSensitive: false, + culture: null, + segment: null } ] } @@ -417,6 +538,46 @@ isSensitive: false, culture: null, segment: null + }, + { + label: "Image", + description: "", + view: "mediapicker", + config: {multiPicker: false, + onlyImages: true, + disableFolderSelect: true, + startNodeId: "umb://media/1fd2ecaff3714c009306867fa4585e7a", + ignoreUserStartNodes: false, + idType: "udi" + }, + hideLabel: false, + validation: {mandatory: false, mandatoryMessage: "", pattern: null, patternMessage: ""}, + readonly: false, + id: 495, + dataTypeKey: "e26a8d91-a9d7-475b-bc3b-2a09f4743754", + value: "umb://media/fa763e0d0ceb408c8720365d57e06e32", + alias: "photo", + editor: "Umbraco.MediaPicker", + isSensitive: false, + culture: null, + segment: null + }, + { + label: "Image Description", + description: "The title of the page", + view: "textbox", + config: {maxChars: 500}, + hideLabel: false, + validation: {mandatory: true, mandatoryMessage: "", pattern: null, patternMessage: ""}, + readonly: false, + id: 442, + dataTypeKey: "0cc0eba1-9960-42c9-bf9b-60e150b429ae", + value: "Let's have a chat", + alias: "imageDesc", + editor: "Umbraco.TextBox", + isSensitive: false, + culture: null, + segment: null } ] } @@ -425,6 +586,10 @@ } ]; + + + + function setDirty() { if (vm.propertyForm) { vm.propertyForm.$setDirty(); From 6fb9afc3d8e8306d8b123590aaa79be1a86d45a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Wed, 5 Feb 2020 11:20:53 +0100 Subject: [PATCH 022/377] labelTemplate rename --- .../blocklist/blocklist.component.js | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.js index 35259eaf9f..56a936982b 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.js @@ -133,8 +133,8 @@ icon: "icon-document", label: "Text" }, - label: "{{pageTitle | truncate:true:36}}", - labelInterpolate: $interpolate("{{pageTitle | truncate:true:36}}"), + labelTemplate: "{{imageTitle | truncate:true:36}}", + labelInterpolate: $interpolate("{{imageTitle | truncate:true:36}}"), key: 1, editor: "views/blockelements/inlineblock/inlineblock.editor.html", overlaySize: 'medium', @@ -207,7 +207,8 @@ icon: "icon-document", label: "Text" }, - label: "Label", + labelTemplate: "Label", + labelInterpolate: $interpolate("Label"), editor: "views/blockelements/textareablock/textareablock.editor.html", overlaySize: 'medium', content: { @@ -256,7 +257,8 @@ icon: "icon-document", label: "Text" }, - label: "Label", + labelTemplate: "Label", + labelInterpolate: $interpolate("Label"), editor: "views/blockelements/imageblock/imageblock.editor.html", overlaySize: 'medium', content: { @@ -331,7 +333,7 @@ icon: "icon-document", label: "Text" }, - label: "{{pageTitle | truncate:true:36}}", + labelTemplate: "{{pageTitle | truncate:true:36}}", labelInterpolate: $interpolate("{{pageTitle | truncate:true:36}}"), key: 1, editor: "views/blockelements/labelblock/labelblock.editor.html", @@ -417,7 +419,7 @@ icon: "icon-document", label: "Text" }, - label: "{{pageTitle | truncate:true:36}}", + labelTemplate: "{{pageTitle | truncate:true:36}}", labelInterpolate: $interpolate("{{pageTitle | truncate:true:36}}"), key: 2, editor: "views/blockelements/labelblock/labelblock.editor.html", @@ -504,7 +506,7 @@ icon: "icon-document", label: "Text" }, - label: "{{pageTitle | truncate:true:36}}", + labelTemplate: "{{pageTitle | truncate:true:36}}", labelInterpolate: $interpolate("{{pageTitle | truncate:true:36}}"), key: 3, editor: "views/blockelements/labelblock/labelblock.editor.html", From 3b12d25dddd92a1ec989ebcc44486c4e43514003 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Wed, 5 Feb 2020 11:24:38 +0100 Subject: [PATCH 023/377] which around actions --- .../blocklist/blocklist.component.html | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.html index e38858c8b7..8aa40e26d9 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.html @@ -52,18 +52,18 @@
- - +
From a2e2b63aabaa506d2f646506fbe99a7bd6d74581 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Wed, 5 Feb 2020 14:05:56 +0100 Subject: [PATCH 024/377] added content-apps --- .../blocklist/blocklist.component.js | 175 ++++++++++++++++++ 1 file changed, 175 insertions(+) diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.js index 56a936982b..1c78435586 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.js @@ -48,6 +48,31 @@ editor: "views/blockelements/labelblock/labelblock.editor.html", overlaySize: 'medium', content: { + apps: [ + { + name: "Content", + alias: "umbContent", + weight: -100, + icon: "icon-document", + view: "views/content/apps/content/content.html", + viewModel: 0, + active: true, + badge: null, + anchors: [], + hasError: false + }, + { + name: "Info", + alias: "umbInfo", + weight: 100, + icon: "icon-info", + view: "views/content/apps/info/info.html", + viewModel: null, + active: false, + badge: null, + hasError: false + } + ], variants: [ { language: { @@ -139,6 +164,31 @@ editor: "views/blockelements/inlineblock/inlineblock.editor.html", overlaySize: 'medium', content: { + apps: [ + { + name: "Content", + alias: "umbContent", + weight: -100, + icon: "icon-document", + view: "views/content/apps/content/content.html", + viewModel: 0, + active: true, + badge: null, + anchors: [], + hasError: false + }, + { + name: "Info", + alias: "umbInfo", + weight: 100, + icon: "icon-info", + view: "views/content/apps/info/info.html", + viewModel: null, + active: false, + badge: null, + hasError: false + } + ], variants: [ { language: { @@ -212,6 +262,31 @@ editor: "views/blockelements/textareablock/textareablock.editor.html", overlaySize: 'medium', content: { + apps: [ + { + name: "Content", + alias: "umbContent", + weight: -100, + icon: "icon-document", + view: "views/content/apps/content/content.html", + viewModel: 0, + active: true, + badge: null, + anchors: [], + hasError: false + }, + { + name: "Info", + alias: "umbInfo", + weight: 100, + icon: "icon-info", + view: "views/content/apps/info/info.html", + viewModel: null, + active: false, + badge: null, + hasError: false + } + ], variants: [ { language: { @@ -262,6 +337,31 @@ editor: "views/blockelements/imageblock/imageblock.editor.html", overlaySize: 'medium', content: { + apps: [ + { + name: "Content", + alias: "umbContent", + weight: -100, + icon: "icon-document", + view: "views/content/apps/content/content.html", + viewModel: 0, + active: true, + badge: null, + anchors: [], + hasError: false + }, + { + name: "Info", + alias: "umbInfo", + weight: 100, + icon: "icon-info", + view: "views/content/apps/info/info.html", + viewModel: null, + active: false, + badge: null, + hasError: false + } + ], variants: [ { language: { @@ -339,6 +439,31 @@ editor: "views/blockelements/labelblock/labelblock.editor.html", overlaySize: 'medium', content: { + apps: [ + { + name: "Content", + alias: "umbContent", + weight: -100, + icon: "icon-document", + view: "views/content/apps/content/content.html", + viewModel: 0, + active: true, + badge: null, + anchors: [], + hasError: false + }, + { + name: "Info", + alias: "umbInfo", + weight: 100, + icon: "icon-info", + view: "views/content/apps/info/info.html", + viewModel: null, + active: false, + badge: null, + hasError: false + } + ], variants: [ { language: { @@ -425,6 +550,31 @@ editor: "views/blockelements/labelblock/labelblock.editor.html", overlaySize: 'medium', content: { + apps: [ + { + name: "Content", + alias: "umbContent", + weight: -100, + icon: "icon-document", + view: "views/content/apps/content/content.html", + viewModel: 0, + active: true, + badge: null, + anchors: [], + hasError: false + }, + { + name: "Info", + alias: "umbInfo", + weight: 100, + icon: "icon-info", + view: "views/content/apps/info/info.html", + viewModel: null, + active: false, + badge: null, + hasError: false + } + ], variants: [ { language: { @@ -512,6 +662,31 @@ editor: "views/blockelements/labelblock/labelblock.editor.html", overlaySize: 'medium', content: { + apps: [ + { + name: "Content", + alias: "umbContent", + weight: -100, + icon: "icon-document", + view: "views/content/apps/content/content.html", + viewModel: 0, + active: true, + badge: null, + anchors: [], + hasError: false + }, + { + name: "Info", + alias: "umbInfo", + weight: 100, + icon: "icon-info", + view: "views/content/apps/info/info.html", + viewModel: null, + active: false, + badge: null, + hasError: false + } + ], variants: [ { language: { From 39c0e686ff6de6cab4ba26d070d4e9c2715b14ec Mon Sep 17 00:00:00 2001 From: Shannon Date: Thu, 6 Feb 2020 16:40:16 +1100 Subject: [PATCH 025/377] Remove empty BlockListEditor --- src/Umbraco.Core/PropertyEditors/BlockListEditor.cs | 12 ------------ src/Umbraco.Core/Umbraco.Core.csproj | 1 - 2 files changed, 13 deletions(-) delete mode 100644 src/Umbraco.Core/PropertyEditors/BlockListEditor.cs diff --git a/src/Umbraco.Core/PropertyEditors/BlockListEditor.cs b/src/Umbraco.Core/PropertyEditors/BlockListEditor.cs deleted file mode 100644 index 16c6176624..0000000000 --- a/src/Umbraco.Core/PropertyEditors/BlockListEditor.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; - -namespace Umbraco.Core.PropertyEditors -{ - public class BlockListEditor - { - } -} diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index 33796a93bf..8bf0b9105f 100755 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -133,7 +133,6 @@ - From 45e892f3505059674779c6e1a43084a367c2862f Mon Sep 17 00:00:00 2001 From: Shannon Date: Thu, 6 Feb 2020 16:52:34 +1100 Subject: [PATCH 026/377] Changes api to GetData --- .../Models/Blocks/BlockListLayoutReference.cs | 11 +---------- src/Umbraco.Core/Models/Blocks/BlockListModel.cs | 14 +++++++++++++- .../BlockListPropertyValueConverterTests.cs | 11 +++++++---- .../BlockListPropertyValueConverter.cs | 2 +- 4 files changed, 22 insertions(+), 16 deletions(-) diff --git a/src/Umbraco.Core/Models/Blocks/BlockListLayoutReference.cs b/src/Umbraco.Core/Models/Blocks/BlockListLayoutReference.cs index 19b30e6ea6..85d17fad24 100644 --- a/src/Umbraco.Core/Models/Blocks/BlockListLayoutReference.cs +++ b/src/Umbraco.Core/Models/Blocks/BlockListLayoutReference.cs @@ -10,10 +10,9 @@ namespace Umbraco.Core.Models.Blocks [DataContract(Name = "blockListLayout", Namespace = "")] public class BlockListLayoutReference : IBlockElement { - public BlockListLayoutReference(Udi udi, IPublishedElement data, IPublishedElement settings) + public BlockListLayoutReference(Udi udi, IPublishedElement settings) { Udi = udi ?? throw new ArgumentNullException(nameof(udi)); - Data = data ?? throw new ArgumentNullException(nameof(data)); Settings = settings; // can be null } @@ -29,13 +28,5 @@ namespace Umbraco.Core.Models.Blocks [DataMember(Name = "settings")] public IPublishedElement Settings { get; } - /// - /// The data item referenced - /// - /// - /// This is ignored from serialization since it is just a reference to the actual data element - /// - [IgnoreDataMember] - public IPublishedElement Data { get; } } } diff --git a/src/Umbraco.Core/Models/Blocks/BlockListModel.cs b/src/Umbraco.Core/Models/Blocks/BlockListModel.cs index 089ca7e6a3..153fe6be8a 100644 --- a/src/Umbraco.Core/Models/Blocks/BlockListModel.cs +++ b/src/Umbraco.Core/Models/Blocks/BlockListModel.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Linq; using System.Runtime.Serialization; using Umbraco.Core.Models.PublishedContent; @@ -22,6 +23,17 @@ namespace Umbraco.Core.Models.Blocks [DataMember(Name = "layout")] public IEnumerable Layout { get; } - + /// + /// Returns the data item associated with the layout udi reference + /// + /// + /// + public IPublishedElement GetData(Udi udi) + { + if (!(udi is GuidUdi guidUdi)) + return null; + return Data.FirstOrDefault(x => x.Key == guidUdi.Guid); + } + } } diff --git a/src/Umbraco.Tests/PropertyEditors/BlockListPropertyValueConverterTests.cs b/src/Umbraco.Tests/PropertyEditors/BlockListPropertyValueConverterTests.cs index ca687f94a6..300c19ac1e 100644 --- a/src/Umbraco.Tests/PropertyEditors/BlockListPropertyValueConverterTests.cs +++ b/src/Umbraco.Tests/PropertyEditors/BlockListPropertyValueConverterTests.cs @@ -317,12 +317,15 @@ data: []}"; Assert.AreEqual(2, converted.Layout.Count()); var item0 = converted.Layout.ElementAt(0); - Assert.AreEqual(Guid.Parse("1304E1DD-AC87-4396-84FE-8A399231CB3D"), item0.Data.Key); - Assert.AreEqual("home", item0.Data.ContentType.Alias); + var item0Data = converted.GetData(item0.Udi); + Assert.IsNotNull(item0Data); + Assert.AreEqual(Guid.Parse("1304E1DD-AC87-4396-84FE-8A399231CB3D"), item0Data.Key); + Assert.AreEqual("home", item0Data.ContentType.Alias); var item1 = converted.Layout.ElementAt(1); - Assert.AreEqual(Guid.Parse("0A4A416E-547D-464F-ABCC-6F345C17809A"), item1.Data.Key); - Assert.AreEqual("home", item1.Data.ContentType.Alias); + var item1Data = converted.GetData(item1.Udi); + Assert.AreEqual(Guid.Parse("0A4A416E-547D-464F-ABCC-6F345C17809A"), item1Data.Key); + Assert.AreEqual("home", item1Data.ContentType.Alias); } diff --git a/src/Umbraco.Web/PropertyEditors/ValueConverters/BlockListPropertyValueConverter.cs b/src/Umbraco.Web/PropertyEditors/ValueConverters/BlockListPropertyValueConverter.cs index 3878b8d605..3d8e8e2b13 100644 --- a/src/Umbraco.Web/PropertyEditors/ValueConverters/BlockListPropertyValueConverter.cs +++ b/src/Umbraco.Web/PropertyEditors/ValueConverters/BlockListPropertyValueConverter.cs @@ -108,7 +108,7 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters if (!elements.TryGetValue(guidUdi.Guid, out var data)) continue; - var layoutRef = new BlockListLayoutReference(udi, data, element); + var layoutRef = new BlockListLayoutReference(udi, element); layout.Add(layoutRef); } From 88df0fe69196cc2966562eb930c0b23048f18a9f Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 7 Feb 2020 00:17:21 +1100 Subject: [PATCH 027/377] Adds new methods to MembershipHelper for dealing with checking bulk paths for access --- src/Umbraco.Web/Security/MembershipHelper.cs | 66 +++++++++++++++++--- 1 file changed, 57 insertions(+), 9 deletions(-) diff --git a/src/Umbraco.Web/Security/MembershipHelper.cs b/src/Umbraco.Web/Security/MembershipHelper.cs index f74897d565..01044dd7f2 100644 --- a/src/Umbraco.Web/Security/MembershipHelper.cs +++ b/src/Umbraco.Web/Security/MembershipHelper.cs @@ -77,6 +77,17 @@ namespace Umbraco.Web.Security return _publicAccessService.IsProtected(path); } + public virtual IDictionary IsProtected(IEnumerable paths) + { + var result = new Dictionary(); + foreach (var path in paths) + { + //this is a cached call + result[path] = _publicAccessService.IsProtected(path); + } + return result; + } + /// /// Check if the current user has access to a document /// @@ -84,15 +95,33 @@ namespace Umbraco.Web.Security /// True if the current user has access or if the current document isn't protected public virtual bool MemberHasAccess(string path) { - //cache this in the request cache - return _appCaches.RequestCache.GetCacheItem($"{typeof(MembershipHelper)}.MemberHasAccess-{path}", () => + if (IsProtected(path)) { - if (IsProtected(path)) - { - return IsLoggedIn() && HasAccess(path, Roles.Provider); - } - return true; - }); + return IsLoggedIn() && HasAccess(path, Roles.Provider); + } + return true; + } + + /// + /// Checks if the current user has access to the paths + /// + /// + /// + public virtual IDictionary MemberHasAccess(IEnumerable paths) + { + var protectedPaths = IsProtected(paths); + + var pathsWithProtection = protectedPaths.Where(x => x.Value).Select(x => x.Key); + var pathsWithAccess = HasAccess(pathsWithProtection, Roles.Provider); + + var result = new Dictionary(); + foreach(var path in paths) + { + pathsWithAccess.TryGetValue(path, out var hasAccess); + // if it's not found it's false anyways + result[path] = hasAccess; + } + return result; } /// @@ -106,6 +135,25 @@ namespace Umbraco.Web.Security return _publicAccessService.HasAccess(path, CurrentUserName, roleProvider.GetRolesForUser); } + private IDictionary HasAccess(IEnumerable paths, RoleProvider roleProvider) + { + // ensure we only lookup user roles once + string[] userRoles = null; + string[] getUserRoles(string username) + { + if (userRoles != null) return userRoles; + userRoles = roleProvider.GetRolesForUser(username).ToArray(); + return userRoles; + } + + var result = new Dictionary(); + foreach (var path in paths) + { + result[path] = IsLoggedIn() && _publicAccessService.HasAccess(path, CurrentUserName, getUserRoles); + } + return result; + } + /// /// Returns true if the current membership provider is the Umbraco built-in one. /// @@ -796,7 +844,7 @@ namespace Umbraco.Web.Security private static string GetCacheKey(string key, params object[] additional) { var sb = new StringBuilder(); - sb.Append(typeof (MembershipHelper).Name); + sb.Append(typeof(MembershipHelper).Name); sb.Append("-"); sb.Append(key); foreach (var s in additional) From 4784a7b1eb74a4d2bb3c26e0334dd69c85755fed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Fri, 7 Feb 2020 12:45:10 +0100 Subject: [PATCH 028/377] more data for demo --- .../blocklist/blocklist.component.js | 104 +++++++++++++++++- 1 file changed, 98 insertions(+), 6 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.js index 1c78435586..287d5aba08 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.js @@ -143,6 +143,29 @@ segment: null } ] + }, + { + id: 1234, + label: "Styling", + properties: [ + { + label: "Background color", + description: "", + view: "textbox", + config: {maxChars: 500}, + hideLabel: false, + validation: {mandatory: true, mandatoryMessage: "", pattern: null, patternMessage: ""}, + readonly: false, + id: 441, + dataTypeKey: "0cc0eba1-9960-42c9-bf9b-60e150b429ae", + value: "The purpose of lorem ipsum is to create a natural looking block of text (sentence, paragraph, page, etc.) that doesn't distract from the layout. A practice not without controversy, laying out pages with meaningless filler text can be very useful when the focus is meant to be on design, not content.", + alias: "pageTitle", + editor: "Umbraco.TextBox", + isSensitive: false, + culture: null, + segment: null + } + ] } ] } @@ -199,7 +222,7 @@ tabs: [ { id: 1234, - label: "Group 1", + label: "Content", properties: [ { label: "Image Title", @@ -297,7 +320,7 @@ tabs: [ { id: 1234, - label: "Group 1", + label: "Content", properties: [ { label: "Page Title", @@ -372,7 +395,7 @@ tabs: [ { id: 1234, - label: "Group 1", + label: "Content", properties: [ { label: "Image", @@ -474,7 +497,7 @@ tabs: [ { id: 1234, - label: "Group 1", + label: "Content", properties: [ { label: "Page Title", @@ -534,6 +557,29 @@ segment: null } ] + }, + { + id: 1234, + label: "Styling", + properties: [ + { + label: "Background color", + description: "", + view: "textbox", + config: {maxChars: 500}, + hideLabel: false, + validation: {mandatory: true, mandatoryMessage: "", pattern: null, patternMessage: ""}, + readonly: false, + id: 441, + dataTypeKey: "0cc0eba1-9960-42c9-bf9b-60e150b429ae", + value: "The purpose of lorem ipsum is to create a natural looking block of text (sentence, paragraph, page, etc.) that doesn't distract from the layout. A practice not without controversy, laying out pages with meaningless filler text can be very useful when the focus is meant to be on design, not content.", + alias: "pageTitle", + editor: "Umbraco.TextBox", + isSensitive: false, + culture: null, + segment: null + } + ] } ] } @@ -585,7 +631,7 @@ tabs: [ { id: 1234, - label: "Group 1", + label: "Content", properties: [ { label: "Page Title", @@ -645,6 +691,29 @@ segment: null } ] + }, + { + id: 1234, + label: "Styling", + properties: [ + { + label: "Background color", + description: "", + view: "textbox", + config: {maxChars: 500}, + hideLabel: false, + validation: {mandatory: true, mandatoryMessage: "", pattern: null, patternMessage: ""}, + readonly: false, + id: 441, + dataTypeKey: "0cc0eba1-9960-42c9-bf9b-60e150b429ae", + value: "The purpose of lorem ipsum is to create a natural looking block of text (sentence, paragraph, page, etc.) that doesn't distract from the layout. A practice not without controversy, laying out pages with meaningless filler text can be very useful when the focus is meant to be on design, not content.", + alias: "pageTitle", + editor: "Umbraco.TextBox", + isSensitive: false, + culture: null, + segment: null + } + ] } ] } @@ -697,7 +766,7 @@ tabs: [ { id: 1234, - label: "Group 1", + label: "Content", properties: [ { label: "Page Title", @@ -757,6 +826,29 @@ segment: null } ] + }, + { + id: 1234, + label: "Styling", + properties: [ + { + label: "Background color", + description: "", + view: "textbox", + config: {maxChars: 500}, + hideLabel: false, + validation: {mandatory: true, mandatoryMessage: "", pattern: null, patternMessage: ""}, + readonly: false, + id: 441, + dataTypeKey: "0cc0eba1-9960-42c9-bf9b-60e150b429ae", + value: "The purpose of lorem ipsum is to create a natural looking block of text (sentence, paragraph, page, etc.) that doesn't distract from the layout. A practice not without controversy, laying out pages with meaningless filler text can be very useful when the focus is meant to be on design, not content.", + alias: "pageTitle", + editor: "Umbraco.TextBox", + isSensitive: false, + culture: null, + segment: null + } + ] } ] } From bc1f5f2086035f2664e93a510bc4a9e1614233cd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Wed, 12 Feb 2020 17:17:04 +0100 Subject: [PATCH 029/377] Initial work on Block List Prevalue Editor --- .../common/resources/elementtype.resource.js | 32 +++ src/Umbraco.Web.UI.Client/src/less/belle.less | 1 + .../overlays/itempicker/itempicker.html | 8 + .../blocklist.elementtypepicker.controller.js | 236 ++++++++++++++++++ .../prevalue/blocklist.elementtypepicker.html | 81 ++++++ .../prevalue/blocklist.elementtypepicker.less | 114 +++++++++ src/Umbraco.Web.UI/Umbraco/config/lang/en.xml | 9 + .../Umbraco/config/lang/en_us.xml | 9 + .../PropertyEditors/BlockListConfiguration.cs | 23 +- .../BlockListConfigurationEditor.cs | 20 ++ .../BlockListPropertyEditor.cs | 3 +- src/Umbraco.Web/Umbraco.Web.csproj | 1 + 12 files changed, 534 insertions(+), 3 deletions(-) create mode 100644 src/Umbraco.Web.UI.Client/src/common/resources/elementtype.resource.js create mode 100644 src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/blocklist.elementtypepicker.controller.js create mode 100644 src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/blocklist.elementtypepicker.html create mode 100644 src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/blocklist.elementtypepicker.less create mode 100644 src/Umbraco.Web/PropertyEditors/BlockListConfigurationEditor.cs mode change 100755 => 100644 src/Umbraco.Web/Umbraco.Web.csproj diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/elementtype.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/elementtype.resource.js new file mode 100644 index 0000000000..680b75ac78 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/common/resources/elementtype.resource.js @@ -0,0 +1,32 @@ +/** + * @ngdoc service + * @name umbraco.resources.elementTypeResource + * @description Loads in data for element types + **/ +function elementTypeResource($q, $http, umbRequestHelper) { + + return { + + getAll: function () { + + // TODO: Change this into a real api (ElementTypeApi). This is a temporary fix to get data. + var url = Umbraco.Sys.ServerVariables.umbracoSettings.umbracoPath + "/backoffice/UmbracoApi/NestedContent/GetContentTypes"; + return umbRequestHelper.resourcePromise( + $http.get(url), + 'Failed to retrieve content types' + ); + + /* + return umbRequestHelper.resourcePromise( + $http.get( + umbRequestHelper.getApiUrl( + "elementTypeApiBaseUrl", + "GetAll")), + "Failed to retrieve data"); + */ + } + + }; +} + +angular.module("umbraco.resources").factory("elementTypeResource", elementTypeResource); diff --git a/src/Umbraco.Web.UI.Client/src/less/belle.less b/src/Umbraco.Web.UI.Client/src/less/belle.less index f264038e93..9b9d10b965 100644 --- a/src/Umbraco.Web.UI.Client/src/less/belle.less +++ b/src/Umbraco.Web.UI.Client/src/less/belle.less @@ -199,6 +199,7 @@ // Property Editors @import "../views/propertyeditors/blocklist/blocklist.component.less"; +@import "../views/propertyeditors/blocklist/prevalue/blocklist.elementtypepicker.less"; // Utilities diff --git a/src/Umbraco.Web.UI.Client/src/views/common/overlays/itempicker/itempicker.html b/src/Umbraco.Web.UI.Client/src/views/common/overlays/itempicker/itempicker.html index 715dc12a07..eec87246c0 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/overlays/itempicker/itempicker.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/overlays/itempicker/itempicker.html @@ -42,5 +42,13 @@ +
  • + +
  • diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/blocklist.elementtypepicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/blocklist.elementtypepicker.controller.js new file mode 100644 index 0000000000..13937a0625 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/blocklist.elementtypepicker.controller.js @@ -0,0 +1,236 @@ +/** + * @ngdoc controller + * @name Umbraco.Editors.PropertySettingsController + * @function + * + * @description + * The controller for the content type editor property settings dialog + */ + +(function () { + "use strict"; + + function ElementTypePickerController($scope, elementTypeResource, overlayService, localizationService, editorService) { + + var vm = this; + + vm.enableAddEntry = true; + + function evaluateStatus() { + + if (!vm.elementTypes) return;// cancel if elementTypes isnt loaded jet. + + vm.enableAddEntry = vm.getAvailableElementTypes().length > 0; + + } + + function onInit() { + + if (!$scope.model.value) { + $scope.model.value = []; + } + + localizationService.localize("content_nestedContentSelectElementTypeModalTitle").then(function (value) { + //selectElementTypeModalTitle = value; + }); + + loadElementTypes(); + + } + + function loadElementTypes() { + return elementTypeResource.getAll().then(function (elementTypes) { + vm.elementTypes = elementTypes; + console.log("vm.elementTypes:", vm.elementTypes) + evaluateStatus(); + }); + } + + vm.removeEntryByIndex = function (index) { + $scope.model.value.splice(index, 1); + }; + + vm.sortableOptions = { + axis: "y", + cursor: "grabbing", + placeholder: 'sortable-placeholder', + forcePlaceholderSize: true + }; + + + vm.getAvailableElementTypes = function () { + return vm.elementTypes.filter(function (type) { + return !$scope.model.value.find(function (entry) { + return type.alias === entry.elementTypeAlias; + }); + }); + }; + + vm.getElementTypeByAlias = function(alias) { + return _.find(vm.elementTypes, function (type) { + return type.alias === alias; + }); + }; + + vm.openAddDialog = function ($event, entry) { + + //we have to add the alias to the objects (they are stored as elementTypeAlias) + var selectedItems = _.each($scope.model.value, function (obj) { + obj.alias = obj.elementTypeAlias; + return obj; + }); + + var availableItems = vm.getAvailableElementTypes() + + var elemTypeSelectorOverlay = { + view: "itempicker", + title: "no title jet", + availableItems: availableItems, + selectedItems: selectedItems, + createNewItem: { + action: function() { + overlayService.close(); + vm.createElementTypeAndAdd(vm.addEntryFromElementTypeAlias); + }, + icon: "icon-add", + name: "Create new" + }, + position: "target", + event: $event, + size: availableItems.length < 7 ? "small" : "medium", + submit: function (overlay) { + vm.addEntryFromElementTypeAlias(overlay.selectedItem.alias); + overlayService.close(); + }, + close: function () { + overlayService.close(); + } + }; + + overlayService.open(elemTypeSelectorOverlay); + }; + + vm.createElementTypeAndAdd = function(callback) { + const editor = { + create: true, + infiniteMode: true, + isElement: true, + submit: function (model) { + console.log(model) + loadElementTypes().then( function () { + callback(model.documentTypeAlias); + }); + editorService.close(); + }, + close: function () { + editorService.close(); + } + }; + editorService.documentTypeEditor(editor); + } + + vm.addEntryFromElementTypeAlias = function(alias) { + + var entry = { + "elementTypeAlias": alias, + "view": null, + "labelTemplate": "", + "settingsElementTypeAlias": null + }; + + $scope.model.value.push(entry); + }; + + vm.removeSettingsForEntry = function(entry) { + entry.settingsElementTypeAlias = null; + }; + + vm.openPickSettingsDialog = function ($event, entry) { + + var elemTypeSelectorOverlay = { + view: "itempicker", + title: "Pick settings (missing translation)", + availableItems: vm.elementTypes, + position: "target", + event: $event, + size: vm.elementTypes.length < 7 ? "small" : "medium", + createNewItem: { + action: function() { + overlayService.close(); + vm.createElementTypeAndAdd((alias) => { + vm.addSettingsAtEntry(entry, alias); + }); + }, + icon: "icon-add", + name: "Create new" + }, + submit: function (overlay) { + vm.addSettingsAtEntry(entry, overlay.selectedItem.alias); + overlayService.close(); + }, + close: function () { + overlayService.close(); + } + }; + + overlayService.open(elemTypeSelectorOverlay); + }; + vm.addSettingsAtEntry = function(entry, alias) { + entry.settingsElementTypeAlias = alias; + }; + + vm.openElementType = function(elementTypeAlias) { + var elementTypeId = vm.getElementTypeByAlias(elementTypeAlias).id; + const editor = { + id: elementTypeId, + submit: function (model) { + loadElementTypes(); + editorService.close(); + }, + close: function () { + editorService.close(); + } + }; + editorService.documentTypeEditor(editor); + } + + vm.removeViewForEntry = function(entry) { + entry.view = null; + }; + vm.addViewForEntry = function(entry) { + const viewPicker = { + title: "Pick view (TODO need translation)", + section: "settings", + treeAlias: "partialView", + entityType: "partialView", + onlyInitialized: false, + filter: function (i) { + if (i.name.indexOf(".cshtml") === -1 && i.name.indexOf(".vbhtml") === -1) { + return true; + } + }, + filterCssClass: "not-allowed", + select: function (node) { + console.log(node); + //entry.view = node.name; + editorService.close(); + }, + close: function () { + editorService.close(); + } + }; + editorService.treePicker(viewPicker); + } + + + onInit(); + + $scope.$watchCollection('model.value', function(newVal, oldVal) { + evaluateStatus(); + }); + + } + + angular.module("umbraco").controller("Umbraco.PropertyEditors.BlockList.ElementTypePickerController", ElementTypePickerController); + +})(); diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/blocklist.elementtypepicker.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/blocklist.elementtypepicker.html new file mode 100644 index 0000000000..d40ec19311 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/blocklist.elementtypepicker.html @@ -0,0 +1,81 @@ +
    +
    +
    +
    +
    +
    + Custom view +
    +
    + Label + +
    +
    + Custom view +
    +
    + Custom view +
    +
    +
    +
    +
    +
    +
    +
    + {{ contentPreview = vm.getElementTypeByAlias(entry.elementTypeAlias); "" }} + + +
    +
    + +
    +
    +
    + + + + +
    + +
    +
    +
    + {{ settingsPreview = vm.getElementTypeByAlias(entry.settingsElementTypeAlias); "" }} + + + + +
    + +
    +
    + +
    +
    +
    +
    +
    + +
    +
    +
    diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/blocklist.elementtypepicker.less b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/blocklist.elementtypepicker.less new file mode 100644 index 0000000000..fb22ea8b46 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/blocklist.elementtypepicker.less @@ -0,0 +1,114 @@ +.umb-block-list-element-type-picker { + + .block-entry { + cursor: grab; + background-color: white; + border-radius: @baseBorderRadius; + } + + .umb-table-head { + button { + margin-left: 5px; + color: @ui-action-discreet-type; + &:hover { + color: @ui-action-discreet-type-hover; + } + } + } + + .umb-table-cell { + padding-left: 10px; + padding-right: 0; + &.action-cell { + padding-right: 15px; + } + } + + .action-cell { + flex: 0 0 30px; + } + + .text-input { + width: 100%; + } + + .umb-node-preview { + flex-grow: 1; + } + + .cell-btn { + position: relative; + opacity: 0; + color: @ui-action-discreet-type; + height: 30px; + width: 26px; + margin-top: 1px; + &:hover { + color: @ui-action-discreet-type-hover; + } + &:last-of-type { + margin-right: 7px; + } + } + .umb-table-cell:hover, + .umb-table-cell:focus, + .umb-table-cell:focus-within { + .cell-btn { + opacity: 1; + } + } + + .settings-input { + position: relative; + padding: 5px 8px; + color: @ui-action-discreet-type; + border: 1px dashed @ui-action-discreet-border; + width: 100%; + font-weight: bold; + display: flex; + flex-flow: row nowrap; + + localize { + width: 100%; + } + + .umb-node-preview { + padding: 3px 0; + margin-left: 5px; + } + + &.--noValue { + text-align: center; + border-radius: @baseBorderRadius; + &:hover { + color: @ui-action-discreet-type-hover; + border-color: @ui-action-discreet-border-hover; + } + } + + &.--hasValue { + border: 1px solid @inputBorder; + padding: 0; + } + } + + .add-button { + width:100%; + color: @ui-action-discreet-type; + border: 1px dashed @ui-action-discreet-border; + border-radius: @baseBorderRadius; + display: flex; + align-items: center; + justify-content: center; + padding: 5px 15px; + box-sizing: border-box; + margin: 10px 0; + font-weight: bold; + } + + .add-button:hover { + color: @ui-action-discreet-type-hover; + border-color: @ui-action-discreet-border-hover; + } + +} diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml index a85df5714b..a7abf75281 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml @@ -2393,4 +2393,13 @@ To manage your website, simply open the Umbraco back office and start adding con Umbraco Forms Create forms using an intuitive drag and drop interface. From simple contact forms that sends e-mails to advanced questionaires that integrate with CRM systems. Your clients will love it! + + Content model + Label + Custom view + Settings model + Add custom view + Add settings + Overwrite label template + 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 d14fb03727..e4fb330296 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml @@ -2405,4 +2405,13 @@ To manage your website, simply open the Umbraco back office and start adding con Umbraco Forms Create forms using an intuitive drag and drop interface. From simple contact forms that sends e-mails to advanced questionaires that integrate with CRM systems. Your clients will love it! + + Content model + Label + Custom view + Settings model + Add custom view + Add settings + Overwrite label template + diff --git a/src/Umbraco.Web/PropertyEditors/BlockListConfiguration.cs b/src/Umbraco.Web/PropertyEditors/BlockListConfiguration.cs index 95366db486..ebdc39e445 100644 --- a/src/Umbraco.Web/PropertyEditors/BlockListConfiguration.cs +++ b/src/Umbraco.Web/PropertyEditors/BlockListConfiguration.cs @@ -9,16 +9,35 @@ namespace Umbraco.Web.PropertyEditors ///
    public class BlockListConfiguration { - [ConfigurationField("elementTypes", "Element Types", "views/propertyeditors/blocklist/blocklist.elementtypepicker.html", Description = "Select the Element Types to use as models for the items.")] + + // TODO: rename this to blockDefinitions, cause its not elementTypes, its a dictionary of objects that define blocks, part of a block is the elementType used as content model. + [ConfigurationField("elementTypes", "Available Blocks", "views/propertyeditors/blocklist/prevalue/blocklist.elementtypepicker.html", Description = "Define the available blocks.")] public ElementType[] ElementTypes { get; set; } - // TODO: Fill me in + [ConfigurationField("minNumber", "Minimum amount", "number")] + public int MinNumber { get; set; } + + [ConfigurationField("maxNumber", "Maximum amount", "number")] + public int MaxNumber { get; set; } public class ElementType { + // TODO: rename this to contentElementTypeAlias, I would like this to be specific, since we have the settings. [JsonProperty("elementTypeAlias")] public string Alias { get; set; } + + [JsonProperty("settingsElementTypeAlias")] + public string SettingsElementTypeAlias { get; set; } + + [JsonProperty("view")] + public string View { get; set; } + + [JsonProperty("labelTemplate")] + public string Template { get; set; } } + [ConfigurationField("useAccordionsAsDefault", "Inline editing mode", "boolean", Description = "Use the inline editor as the default block view")] + public bool useInlineEditingAsDefault { get; set; } + } } diff --git a/src/Umbraco.Web/PropertyEditors/BlockListConfigurationEditor.cs b/src/Umbraco.Web/PropertyEditors/BlockListConfigurationEditor.cs new file mode 100644 index 0000000000..3a4e3eae9b --- /dev/null +++ b/src/Umbraco.Web/PropertyEditors/BlockListConfigurationEditor.cs @@ -0,0 +1,20 @@ +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Text.RegularExpressions; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using Umbraco.Core; +using Umbraco.Core.PropertyEditors; + +namespace Umbraco.Web.PropertyEditors +{ + internal class BlockListConfigurationEditor : ConfigurationEditor + { + public BlockListConfigurationEditor() + { + + } + + } +} diff --git a/src/Umbraco.Web/PropertyEditors/BlockListPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/BlockListPropertyEditor.cs index 9c4e2f460f..782122bccd 100644 --- a/src/Umbraco.Web/PropertyEditors/BlockListPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/BlockListPropertyEditor.cs @@ -22,7 +22,8 @@ namespace Umbraco.Web.PropertyEditors { } #region Pre Value Editor - //protected override IConfigurationEditor CreateConfigurationEditor() => new BlockEditorListConfigurationEditor(); + + protected override IConfigurationEditor CreateConfigurationEditor() => new BlockListConfigurationEditor(); #endregion diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj old mode 100755 new mode 100644 index e660108cad..c8812ad9ec --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -235,6 +235,7 @@ + From bc73c6f060d699d622b4d3dffa2ce841b76b6643 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Wed, 12 Feb 2020 17:17:36 +0100 Subject: [PATCH 030/377] ability to not embed templates in dev mode --- src/Umbraco.Web.UI.Client/gulp/config.js | 6 ++++-- src/Umbraco.Web.UI.Client/gulp/util/processJs.js | 4 +++- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/gulp/config.js b/src/Umbraco.Web.UI.Client/gulp/config.js index a807d63f5f..92e0b6d21d 100755 --- a/src/Umbraco.Web.UI.Client/gulp/config.js +++ b/src/Umbraco.Web.UI.Client/gulp/config.js @@ -3,10 +3,12 @@ module.exports = { compile: { build: { - sourcemaps: false + sourcemaps: false, + embedtemplates: true }, dev: { - sourcemaps: true + sourcemaps: true, + embedtemplates: false } }, sources: { diff --git a/src/Umbraco.Web.UI.Client/gulp/util/processJs.js b/src/Umbraco.Web.UI.Client/gulp/util/processJs.js index e3e393b661..67dd6dd420 100644 --- a/src/Umbraco.Web.UI.Client/gulp/util/processJs.js +++ b/src/Umbraco.Web.UI.Client/gulp/util/processJs.js @@ -25,7 +25,9 @@ module.exports = function (files, out) { .pipe(sort()); //in production, embed the templates - task = task.pipe(embedTemplates({ basePath: "./src/", minimize: { loose: true } })) + if(config.compile.current.embedtemplates === true) { + task = task.pipe(embedTemplates({ basePath: "./src/", minimize: { loose: true } })); + } task = task.pipe(concat(out)) .pipe(wrap('(function(){\n%= body %\n})();')) From 7b8cd45f278c06b84f1b484ac987cd58b45dd28a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Wed, 12 Feb 2020 17:18:10 +0100 Subject: [PATCH 031/377] add style to create-option in itempicker + removing overflow hidden --- .../src/less/components/card.less | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/less/components/card.less b/src/Umbraco.Web.UI.Client/src/less/components/card.less index ed80359833..04992d591f 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/card.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/card.less @@ -93,7 +93,7 @@ } .umb-card-grid li { - overflow: hidden; + font-size: 12px; text-align: center; box-sizing: border-box; @@ -135,11 +135,20 @@ } -.umb-card-grid .umb-card-grid-item:hover, -.umb-card-grid .umb-card-grid-item:focus { +.umb-card-grid .umb-card-grid-item:hover { background-color: @ui-option-hover; color: @ui-option-type-hover; } +.umb-card-grid .umb-card-grid-item:focus { + color: @ui-option-type-hover; +} + +.umb-card-grid .umb-card-grid-item.--creator { + border: 1px dashed @ui-action-discreet-border; + &:hover { + border-color: @ui-action-discreet-border-hover; + } +} .umb-card-grid a { color: @ui-option-type; From bb3bccb1cc398da761082519231c6be67a3e0613 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Thu, 13 Feb 2020 12:36:48 +0100 Subject: [PATCH 032/377] style adjustment --- .../prevalue/blocklist.elementtypepicker.less | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/blocklist.elementtypepicker.less b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/blocklist.elementtypepicker.less index fb22ea8b46..f76e9ee1fd 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/blocklist.elementtypepicker.less +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/blocklist.elementtypepicker.less @@ -6,6 +6,10 @@ border-radius: @baseBorderRadius; } + .umb-table { + border:1px solid @gray-11; + } + .umb-table-head { button { margin-left: 5px; @@ -80,7 +84,9 @@ &.--noValue { text-align: center; border-radius: @baseBorderRadius; - &:hover { + color: white; + transition: color 120ms; + &:hover, &:focus { color: @ui-action-discreet-type-hover; border-color: @ui-action-discreet-border-hover; } @@ -102,7 +108,7 @@ justify-content: center; padding: 5px 15px; box-sizing: border-box; - margin: 10px 0; + margin: 20px 0; font-weight: bold; } From 596c6b937d3924fe3396ef41829ec90287f5c8f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Thu, 13 Feb 2020 12:36:57 +0100 Subject: [PATCH 033/377] clean up of html --- .../prevalue/blocklist.elementtypepicker.html | 134 +++++++++--------- 1 file changed, 65 insertions(+), 69 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/blocklist.elementtypepicker.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/blocklist.elementtypepicker.html index d40ec19311..830ee4124e 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/blocklist.elementtypepicker.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/blocklist.elementtypepicker.html @@ -1,81 +1,77 @@
    -
    -
    -
    -
    -
    - Custom view -
    -
    - Label - -
    -
    - Custom view -
    -
    - Custom view -
    -
    -
    +
    +
    +
    +
    + Custom view +
    +
    + Label + +
    +
    + Custom view +
    +
    + Custom view +
    +
    -
    -
    -
    - {{ contentPreview = vm.getElementTypeByAlias(entry.elementTypeAlias); "" }} - -
    +
    +
    +
    + {{ contentPreview = vm.getElementTypeByAlias(entry.elementTypeAlias); "" }} + + +
    +
    + +
    +
    +
    + + + + +
    + +
    +
    +
    + {{ settingsPreview = vm.getElementTypeByAlias(entry.settingsElementTypeAlias); "" }} + + + -
    -
    - -
    -
    -
    - - - - -
    - -
    -
    -
    - {{ settingsPreview = vm.getElementTypeByAlias(entry.settingsElementTypeAlias); "" }} - - - - -
    - -
    -
    -
    + +
    +
    +
    -
    - -
    +
    From abdf8bc22cd7b7a3a43e5e9dff25278d2c0e7392 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Thu, 13 Feb 2020 12:37:28 +0100 Subject: [PATCH 034/377] correct sentence to use the number 7 --- .../src/views/propertyeditors/blocklist/blocklist.component.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.js index 287d5aba08..0aa6003dca 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.js @@ -988,7 +988,7 @@ vm.blockTypePicker = { show: true, - size: vm.availableBlockTypes.length > 6 ? "medium" : "small", + size: vm.availableBlockTypes.length < 7 ? "small" : "medium", filter: vm.availableBlockTypes.length > 12 ? true : false, orderBy: "$index", view: "itempicker", From 35ba840a25c8c484c2c61ff4613f4247b3cf28d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Thu, 13 Feb 2020 12:37:49 +0100 Subject: [PATCH 035/377] correct overlays, so they can use size --- src/Umbraco.Web.UI.Client/src/less/components/overlays.less | 2 +- .../src/views/components/overlays/umb-overlay.html | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/less/components/overlays.less b/src/Umbraco.Web.UI.Client/src/less/components/overlays.less index eb8740b385..bbd866a5fd 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/overlays.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/overlays.less @@ -128,7 +128,7 @@ border-radius: @baseBorderRadius; &.umb-overlay--medium { - width: 480px; + width: 520px; } } diff --git a/src/Umbraco.Web.UI.Client/src/views/components/overlays/umb-overlay.html b/src/Umbraco.Web.UI.Client/src/views/components/overlays/umb-overlay.html index a19cf40e1a..5735f8462b 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/overlays/umb-overlay.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/overlays/umb-overlay.html @@ -1,4 +1,4 @@ - diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.html index 8aa40e26d9..7fcee650fc 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.html @@ -90,6 +90,21 @@ + + + + +
    +
    + Minimum %0% entries, needs %1% more. +
    +
    +
    +
    + Maximum %0% entries, %1% too many. +
    +
    +
    = 48 && event.keyCode <= 90)// 0 to z - || - (event.keyCode >= 96 && event.keyCode <= 111)// numpads - || - (event.keyCode >= 186 && event.keyCode <= 222)// semi-colon and a lot of other special characters - ) { - // Continue writting... needs to know default text-element. if we have one. - } - } - - function hideCreateOptions() { - vm.quickMenuVisible = false; - window.removeEventListener("keydown", handleTypingInCreateOptions); - } - - vm.onCreateOptionsBlur = function($event) { - - if(!$($event.relatedTarget).is(".umb-block-list__block--create-bar > button")) { - hideCreateOptions(); - } - } function getBlockLabel(block) { - console.log("getBlockLabel", block) - // TODO: we should do something about this for performance. var props = new Object(); @@ -1012,13 +958,11 @@ console.log("copy") } vm.requestDeleteBlock = function(block) { - localizationService.localizeMany(["content_nestedContentDeleteItem", "general_delete", "general_cancel", "contentTypeEditor_yesDelete"]).then(function (data) { + localizationService.localizeMany(["general_delete", "blockEditor_confirmDeleteBlockMessage", "contentTypeEditor_yesDelete"]).then(function (data) { const overlay = { - title: data[1], - content: data[0], - closeButtonLabel: data[2], - submitButtonLabel: data[3], - submitButtonStyle: "danger", + title: data[0], + content: localizationService.tokenReplace(data[1], [block.label]), + submitButtonLabel: data[2], close: function () { overlayService.close(); }, @@ -1028,7 +972,7 @@ } }; - overlayService.open(overlay); + overlayService.confirmDelete(overlay); }); } @@ -1040,9 +984,9 @@ vm.sortableOptions = { axis: "y", cursor: "grabbing", - handle: '.blockelement__draggable-element', - cancel: 'input,textarea,select,option', - classes: '.blockelement--dragging', + handle: ".blockelement__draggable-element", + cancel: "input,textarea,select,option", + classes: ".blockelement--dragging", distance: 5, tolerance: "pointer", scroll: true, @@ -1062,15 +1006,14 @@ }; $scope.blockApi = { - showCreateOptionsFor: vm.showCreateOptionsFor, removeBlock: vm.removeBlock } var copyAllEntriesAction = { - labelKey: 'clipboard_labelForCopyAllEntries', + labelKey: "clipboard_labelForCopyAllEntries", labelTokens: [model.label], - icon: 'documents', + icon: "documents", method: function () {}, isDisabled: true } @@ -1085,14 +1028,40 @@ } }; + + function validateLimits() { + if (vm.validationLimit.min && vm.blocks.length < vm.validationLimit.min) { + vm.propertyForm.minCount.$setValidity("minCount", false); + } + else { + vm.propertyForm.minCount.$setValidity("minCount", true); + } + + if (vm.validationLimit.max && vm.blocks.length > vm.validationLimit.max) { + vm.propertyForm.maxCount.$setValidity("maxCount", false); + } + else { + vm.propertyForm.maxCount.$setValidity("maxCount", true); + } + } + + + + // TODO: We need to investigate if we can do a specific watch on each block, so we dont re-render all blocks. - $scope.$watch('vm.blocks', onBlocksUpdated, true); + unsubscribe.push($scope.$watch("vm.blocks", onBlocksUpdated, true)); function onBlocksUpdated(newVal, oldVal){ - console.log("onBlocksUpdated"); for(const block of vm.blocks) { block.label = getBlockLabel(block); } } + unsubscribe.push($scope.$watch(() => vm.blocks.length, validateLimits)); + + $scope.$on("$destroy", function () { + for (const subscription of unsubscribe) { + subscription(); + } + }); } diff --git a/src/Umbraco.Web/PropertyEditors/BlockListConfiguration.cs b/src/Umbraco.Web/PropertyEditors/BlockListConfiguration.cs index 5898ee4a70..344604325e 100644 --- a/src/Umbraco.Web/PropertyEditors/BlockListConfiguration.cs +++ b/src/Umbraco.Web/PropertyEditors/BlockListConfiguration.cs @@ -15,8 +15,8 @@ namespace Umbraco.Web.PropertyEditors public ElementType[] ElementTypes { get; set; } - [ConfigurationField("range", "Amount", "numberrange", Description = "Set a required range of blocks")] - public NumberRange Range { get; set; } = new NumberRange(); + [ConfigurationField("validationLimit", "Amount", "numberrange", Description = "Set a required range of blocks")] + public NumberRange ValidationLimit { get; set; } = new NumberRange(); public class NumberRange { From 82676ae6498c05b351cd814755ea65bc8eb09b41 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Tue, 25 Feb 2020 12:55:27 +0100 Subject: [PATCH 047/377] rename ElementTypes to Blocks --- .../BlockListPropertyValueConverterTests.cs | 6 +++--- src/Umbraco.Web/PropertyEditors/BlockListConfiguration.cs | 7 +++---- .../ValueConverters/BlockListPropertyValueConverter.cs | 6 +++--- 3 files changed, 9 insertions(+), 10 deletions(-) diff --git a/src/Umbraco.Tests/PropertyEditors/BlockListPropertyValueConverterTests.cs b/src/Umbraco.Tests/PropertyEditors/BlockListPropertyValueConverterTests.cs index 300c19ac1e..a8a500d5c0 100644 --- a/src/Umbraco.Tests/PropertyEditors/BlockListPropertyValueConverterTests.cs +++ b/src/Umbraco.Tests/PropertyEditors/BlockListPropertyValueConverterTests.cs @@ -47,7 +47,7 @@ namespace Umbraco.Tests.PropertyEditors private BlockListConfiguration ConfigForMany() => new BlockListConfiguration { - ElementTypes = new[] { + Blocks = new[] { new BlockListConfiguration.ElementType { Alias = "Test1" @@ -61,7 +61,7 @@ namespace Umbraco.Tests.PropertyEditors private BlockListConfiguration ConfigForSingle() => new BlockListConfiguration { - ElementTypes = new[] { + Blocks = new[] { new BlockListConfiguration.ElementType { Alias = "Test1" @@ -111,7 +111,7 @@ namespace Umbraco.Tests.PropertyEditors var valueType = editor.GetPropertyValueType(propType); - var modelType = typeof(IEnumerable<>).MakeGenericType(ModelType.For(config.ElementTypes[0].Alias)); + var modelType = typeof(IEnumerable<>).MakeGenericType(ModelType.For(config.Blocks[0].Alias)); // we can't compare the exact match of types because ModelType.For generates a new/different type even if the same alias is used Assert.AreEqual(modelType.FullName, valueType.FullName); diff --git a/src/Umbraco.Web/PropertyEditors/BlockListConfiguration.cs b/src/Umbraco.Web/PropertyEditors/BlockListConfiguration.cs index 344604325e..6485bd061a 100644 --- a/src/Umbraco.Web/PropertyEditors/BlockListConfiguration.cs +++ b/src/Umbraco.Web/PropertyEditors/BlockListConfiguration.cs @@ -10,9 +10,8 @@ namespace Umbraco.Web.PropertyEditors public class BlockListConfiguration { - // TODO: rename this to blockDefinitions, cause its not elementTypes, its a dictionary of objects that define blocks, part of a block is the elementType used as content model. - [ConfigurationField("elementTypes", "Available Blocks", "views/propertyeditors/blocklist/prevalue/blocklist.elementtypepicker.html", Description = "Define the available blocks.")] - public ElementType[] ElementTypes { get; set; } + [ConfigurationField("blocks", "Available Blocks", "views/propertyeditors/blocklist/prevalue/blocklist.elementtypepicker.html", Description = "Define the available blocks.")] + public BlockConfiguration[] Blocks { get; set; } [ConfigurationField("validationLimit", "Amount", "numberrange", Description = "Set a required range of blocks")] @@ -27,7 +26,7 @@ namespace Umbraco.Web.PropertyEditors public int? Max { get; set; } } - public class ElementType + public class BlockConfiguration { // TODO: rename this to contentElementTypeAlias, I would like this to be specific, since we have the settings. [JsonProperty("elementTypeAlias")] diff --git a/src/Umbraco.Web/PropertyEditors/ValueConverters/BlockListPropertyValueConverter.cs b/src/Umbraco.Web/PropertyEditors/ValueConverters/BlockListPropertyValueConverter.cs index 3d8e8e2b13..94d64ee29d 100644 --- a/src/Umbraco.Web/PropertyEditors/ValueConverters/BlockListPropertyValueConverter.cs +++ b/src/Umbraco.Web/PropertyEditors/ValueConverters/BlockListPropertyValueConverter.cs @@ -34,7 +34,7 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters /// public override Type GetPropertyValueType(IPublishedPropertyType propertyType) { - var contentTypes = propertyType.DataType.ConfigurationAs().ElementTypes; + var contentTypes = propertyType.DataType.ConfigurationAs().Blocks; return contentTypes.Length == 1 ? typeof(IEnumerable<>).MakeGenericType(ModelType.For(contentTypes[0].Alias)) : typeof(IEnumerable); @@ -58,7 +58,7 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters using (_proflog.DebugDuration($"ConvertPropertyToBlockList ({propertyType.DataType.Id})")) { var configuration = propertyType.DataType.ConfigurationAs(); - var contentTypes = configuration.ElementTypes; + var contentTypes = configuration.Blocks; var elements = (contentTypes.Length == 1 ? (IList)_publishedModelFactory.CreateModelList(contentTypes[0].Alias) : new List()) @@ -116,6 +116,6 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters } } - + } } From 8ffae1ce31befedda4d13025e5f7964a12943f12 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Wed, 26 Feb 2020 13:02:22 +0100 Subject: [PATCH 048/377] implement block list editor --- .../common/services/blockeditor.service.js | 227 ++++++++++++++++++ .../src/common/services/udi.service.js | 32 +++ .../inlineblock/inlineblock.editor.html | 2 +- .../labelblock/labelblock.editor.html | 2 +- .../textareablock.editor.controller.js | 2 +- .../elementContentEditor.component.js | 2 +- .../blocklist/blocklist.component.js | 74 +++--- .../blocklist.elementtypepicker.controller.js | 10 +- .../prevalue/blocklist.elementtypepicker.html | 6 +- .../PropertyEditors/BlockListConfiguration.cs | 6 +- 10 files changed, 321 insertions(+), 42 deletions(-) create mode 100644 src/Umbraco.Web.UI.Client/src/common/services/blockeditor.service.js create mode 100644 src/Umbraco.Web.UI.Client/src/common/services/udi.service.js 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 new file mode 100644 index 0000000000..8846dc7d3a --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/common/services/blockeditor.service.js @@ -0,0 +1,227 @@ +(function () { + 'use strict'; + + + function blockEditorService($interpolate, udiService) { + + + function applyModelToScaffold(scaffold, contentModel) { + + scaffold.key = contentModel.key; + + var variant = scaffold.variants[0]; + + for (var t = 0; t < variant.tabs.length; t++) { + var tab = variant.tabs[t]; + + for (var p = 0; p < tab.properties.length; p++) { + var prop = tab.properties[p]; + if (contentModel[prop.propertyAlias]) { + prop.value = contentModel[prop.propertyAlias]; + } + } + } + } + + + /** + * @ngdoc factory + * @name umbraco.factory.BlockEditorModelObject + * @description A model object used to handle Block Editor data. + **/ + function BlockEditorModelObject(propertyModelValue, propertyEditorAlias, blockConfigurations) { + + if (!propertyModelValue) { + throw new Error("propertyModelValue cannot be undefined, to ensure we keep the binding to the angular model we need minimum an empty object."); + } + + // ensure basic part of data-structure is in place: + this.value = propertyModelValue; + this.value.layout = this.value.layout || []; + this.value.data = this.value.data || []; + + this.propertyEditorAlias = propertyEditorAlias; + this.blockConfigurations = blockConfigurations; + + this.scaffolds = []; + + }; + + BlockEditorModelObject.prototype = { + + getBlockConfiguration: function(alias) { + return this.blockConfigurations.find(blockConfiguration => blockConfiguration.contentTypeAlias === alias); + }, + + loadScaffolds: function(contentResource) { + var tasks = []; + + var scaffoldAliases = []; + + this.blockConfigurations.forEach(blockConfiguration => { + scaffoldAliases.push(blockConfiguration.contentTypeAlias); + if (blockConfiguration.settingsElementTypeAlias != null) { + scaffoldAliases.push(elementType.settingsElementTypeAlias); + } + }); + + // remove dublicates. + scaffoldAliases = scaffoldAliases.filter((value, index, self) => self.indexOf(value) === index); + + scaffoldAliases.forEach((elementTypeAlias => { + tasks.push(contentResource.getScaffold(-20, elementTypeAlias).then(scaffold => { + console.log(scaffold); + this.scaffolds.push(scaffold); + })); + })); + + return Promise.all(tasks); + }, + + getAvailableBlocksForItemPicker: function() { + + var blocks = []; + + this.blockConfigurations.forEach(blockConfiguration => { + + var scaffold = this.getScaffoldFor(blockConfiguration.contentTypeAlias); + + blocks.push({ + alias: scaffold.contentTypeAlias, + name: scaffold.contentTypeName, + icon: scaffold.icon + }); + }); + + return blocks; + }, + + getScaffoldFor: function(contentTypeAlias, data) { + return this.scaffolds.find(o => o.contentTypeAlias === contentTypeAlias); + }, + + /** + * Retrieve editing model of a layout entry + * @return {Object} Scaffolded Block Content object. + */ + getEditingModel: function(layoutEntry) { + + var contentModel = this.getContentByUdi(layoutEntry.udi); + + var blockConfiguration = this.getBlockConfiguration(contentModel.contentTypeAlias); + + // TODO: make blockConfiguration the base for model, remeber to make a copy. + var model = { + label: "", + labelInterpolate: $interpolate(blockConfiguration.label), + editor: blockConfiguration.view, + overlaySize: "medium" + }; + + var scaffold = this.getScaffoldFor(blockConfiguration.contentTypeAlias); + if(scaffold === null) { + return null; + } + + model.content = angular.copy(scaffold); + applyModelToScaffold(model.content, contentModel); + + // TODO: settings + + return model; + + }, + + /** + * Retrieve layout data + * @return layout object. + */ + getLayout: function() { + if (!this.value.layout[this.propertyEditorAlias]) { + this.value.layout[this.propertyEditorAlias] = []; + } + return this.value.layout[this.propertyEditorAlias]; + }, + + /** + * Create layout entry + * @param {object} blockConfiguration + * @return layout entry, to be added in the layout. + */ + createLayoutEntry: function(contentTypeAlias) { + + var blockConfiguration = this.getBlockConfiguration(contentTypeAlias); + + var entry = { + udi: this.createContent(contentTypeAlias) + } + + if (blockConfiguration.settingsElementTypeAlias != null) { + // TODO: Settings. + } + + return entry; + }, + + // You make entries in your layout your self. + + getContentByUdi: function(udi) { + return this.value.data.find(entry => entry.udi === udi); + }, + + createContent: function(elementTypeAlias) { + var content = { + contentTypeAlias: elementTypeAlias, + udi: udiService.create("element") + }; + this.value.data.push(content); + return content.udi; + }, + + removeContent: function(entry) { + const index = this.value.data.indexOf(entry) + if (index > -1) { + this.value.splice(index, 1); + } + }, + + removeContentByUdi: function(udi) { + const index = this.value.data.findIndex(o => o.udi === udi); + if (index > -1) { + this.value.splice(index, 1); + } + } + } + + return { + createModelObject: function(propertyModelValue, propertyEditorAlias, blockConfigurations) { + return new BlockEditorModelObject(propertyModelValue, propertyEditorAlias, blockConfigurations); + }, + getBlockLabel: function(blockModelObject, labelIndex) { + + console.log("getBlockLabel", blockModelObject); + + // TODO: we should do something about this for performance. + + var vars = new Object(); + vars["$index"] = labelIndex; + + var variant = blockModelObject.content.variants[0]; + var tab = variant.tabs[0]; + // TODO: need to look up all tabs... + for(const property of tab.properties) { + vars[property.alias] = property.value; + } + + if(blockModelObject.labelInterpolate) { + return blockModelObject.labelInterpolate(vars); + } + + return blockModelObject.contentTypeName; + } + } + } + + angular.module('umbraco.services').service('blockEditorService', blockEditorService); + +})(); diff --git a/src/Umbraco.Web.UI.Client/src/common/services/udi.service.js b/src/Umbraco.Web.UI.Client/src/common/services/udi.service.js new file mode 100644 index 0000000000..b7d0cd05e8 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/common/services/udi.service.js @@ -0,0 +1,32 @@ +(function () { + "use strict"; + + /** + * @ngdoc service + * @name umbraco.services.udiService + * @description A service for UDIs + **/ + function udiService() { + return { + + /** + * @ngdoc method + * @name umbraco.services.udiService#parse + * @methodOf umbraco.services.udiService + * @function + * + * @description + * Generates a Udi string. + * + * @param {string} entityType The entityType as a string. + * @returns {string} The generated UDI + */ + create: function(entityType) { + return "umb://" + entityType + "/" + String.CreateGuid(); + } + } + } + + angular.module("umbraco.services").factory("udiService", udiService); + +})(); diff --git a/src/Umbraco.Web.UI.Client/src/views/blockelements/inlineblock/inlineblock.editor.html b/src/Umbraco.Web.UI.Client/src/views/blockelements/inlineblock/inlineblock.editor.html index ff555fc773..9561d28a9e 100644 --- a/src/Umbraco.Web.UI.Client/src/views/blockelements/inlineblock/inlineblock.editor.html +++ b/src/Umbraco.Web.UI.Client/src/views/blockelements/inlineblock/inlineblock.editor.html @@ -1,7 +1,7 @@
    diff --git a/src/Umbraco.Web.UI.Client/src/views/blockelements/labelblock/labelblock.editor.html b/src/Umbraco.Web.UI.Client/src/views/blockelements/labelblock/labelblock.editor.html index bd8f9558fd..419a1fd8a9 100644 --- a/src/Umbraco.Web.UI.Client/src/views/blockelements/labelblock/labelblock.editor.html +++ b/src/Umbraco.Web.UI.Client/src/views/blockelements/labelblock/labelblock.editor.html @@ -1,4 +1,4 @@ diff --git a/src/Umbraco.Web.UI.Client/src/views/blockelements/textareablock/textareablock.editor.controller.js b/src/Umbraco.Web.UI.Client/src/views/blockelements/textareablock/textareablock.editor.controller.js index caad1c83a7..868573fb72 100644 --- a/src/Umbraco.Web.UI.Client/src/views/blockelements/textareablock/textareablock.editor.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/blockelements/textareablock/textareablock.editor.controller.js @@ -5,7 +5,7 @@ angular.module("umbraco") var vm = this; - vm.firstProperty = $scope.block.content.tabs[0].properties[0]; + vm.firstProperty = $scope.block.content.variants[0].tabs[0].properties[0]; /* vm.onBlur = function() { if (vm.firstProperty.value === null || vm.firstProperty.value === "") { diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/elementeditor/elementContentEditor.component.js b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/elementeditor/elementContentEditor.component.js index 34cfed2ad9..637df93a72 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/elementeditor/elementContentEditor.component.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/elementeditor/elementContentEditor.component.js @@ -4,7 +4,7 @@ angular .module('umbraco.directives') .component('umbElementContentEditor', { - templateUrl: 'views/common/infiniteeditors/elementeditor/elementeditor.component.html', + templateUrl: 'views/common/infiniteeditors/elementeditor/elementContentEditor.component.html', controller: ElementEditorComponentController, controllerAs: 'vm', bindings: { diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.js index abfd738861..7aa03f9402 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.js @@ -15,7 +15,7 @@ } }); - function BlockListController($scope, $interpolate, editorService, clipboardService, localizationService, overlayService) { + function BlockListController($scope, $interpolate, editorService, clipboardService, localizationService, overlayService, blockEditorService, contentResource) { var unsubscribe = []; var vm = this; @@ -24,13 +24,15 @@ $scope.moveFocusToBlock = null; + console.log("model JSON:", JSON.stringify(model)); + console.log("model:", model); console.log("config:", model.config); vm.validationLimit = model.config.validationLimit; console.log("value:", model.value); - + /* vm.availableBlockTypes = [ { alias: "pageModule", @@ -856,7 +858,34 @@ ]; + */ + + model.value = model.value || {}; + + var modelObject = blockEditorService.createModelObject(model.value, model.editor, model.config.blocks); + + modelObject.loadScaffolds(contentResource).then(loaded); + + vm.layout = []; + vm.blocks = []; + vm.availableBlockTypes = []; + + function loaded() { + + console.log("Loading done!!!"); + console.log(modelObject); + + + vm.layout = modelObject.getLayout(); + vm.layout.forEach(entry => { + vm.blocks.push(modelObject.getEditingModel(entry)); + }); + + vm.availableBlockTypes = modelObject.getAvailableBlocksForItemPicker(); + console.log(vm.availableBlockTypes); + + } function setDirty() { @@ -865,33 +894,23 @@ } }; - function addNewBlock(index, type) { + function addNewBlock(index, contentTypeAlias) { - var block = angular.copy(type.prototype_paste_data); + // Create layout entry. + var layoutEntry = modelObject.createLayoutEntry(contentTypeAlias); + // add layout entry a decired location in layout. + vm.layout.splice(index, 0, layoutEntry); - vm.blocks.splice(index, 0, block); - $scope.moveFocusToBlock = block; + // make editing object + var blockEditingObject = modelObject.getEditingModel(layoutEntry); + // apply editing model at decired location in editing model. + vm.blocks.splice(index, 0, blockEditingObject); + + $scope.moveFocusToBlock = blockEditingObject; } - function getBlockLabel(block) { - - // TODO: we should do something about this for performance. - - var props = new Object(); - - var tab = block.content.tabs[0]; - // TODO: need to look up all tabs... - for(const property of tab.properties) { - props[property.alias] = property.value; - } - - if(block.labelInterpolate) { - return block.labelInterpolate(props); - } - - return "block.label"; - } + vm.deleteBlock = function(block) { var index = vm.blocks.indexOf(block); @@ -942,7 +961,7 @@ availableItems: vm.availableBlockTypes, submit: function (model) { if (model && model.selectedItem) { - addNewBlock(createIndex, model.selectedItem); + addNewBlock(createIndex, model.selectedItem.alias); } vm.blockTypePicker.close(); }, @@ -1050,9 +1069,10 @@ // TODO: We need to investigate if we can do a specific watch on each block, so we dont re-render all blocks. unsubscribe.push($scope.$watch("vm.blocks", onBlocksUpdated, true)); - function onBlocksUpdated(newVal, oldVal){ + function onBlocksUpdated(newVal, oldVal) { + var labelIndex = 1; for(const block of vm.blocks) { - block.label = getBlockLabel(block); + block.label = blockEditorService.getBlockLabel(block, labelIndex++); } } unsubscribe.push($scope.$watch(() => vm.blocks.length, validateLimits)); diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/blocklist.elementtypepicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/blocklist.elementtypepicker.controller.js index 970b42d2cb..77751579c9 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/blocklist.elementtypepicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/blocklist.elementtypepicker.controller.js @@ -47,7 +47,7 @@ vm.requestRemoveEntryByIndex = function (index) { localizationService.localizeMany(["general_delete", "blockEditor_confirmDeleteBlockMessage", "blockEditor_confirmDeleteBlockNotice"]).then(function (data) { - var contentElementType = vm.getElementTypeByAlias($scope.model.value[index].elementTypeAlias); + var contentElementType = vm.getElementTypeByAlias($scope.model.value[index].contentTypeAlias); overlayService.confirmDelete({ title: data[0], content: localizationService.tokenReplace(data[1], [contentElementType.name]), @@ -78,7 +78,7 @@ vm.getAvailableElementTypes = function () { return vm.elementTypes.filter(function (type) { return !$scope.model.value.find(function (entry) { - return type.alias === entry.elementTypeAlias; + return type.alias === entry.contentTypeAlias; }); }); }; @@ -91,9 +91,9 @@ vm.openAddDialog = function ($event, entry) { - //we have to add the alias to the objects (they are stored as elementTypeAlias) + //we have to add the alias to the objects (they are stored as contentTypeAlias) var selectedItems = _.each($scope.model.value, function (obj) { - obj.alias = obj.elementTypeAlias; + obj.alias = obj.contentTypeAlias; return obj; }); @@ -149,7 +149,7 @@ vm.addEntryFromElementTypeAlias = function(alias) { var entry = { - "elementTypeAlias": alias, + "contentTypeAlias": alias, "view": null, "labelTemplate": "", "settingsElementTypeAlias": null diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/blocklist.elementtypepicker.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/blocklist.elementtypepicker.html index 9ca35778be..8184761a1a 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/blocklist.elementtypepicker.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/blocklist.elementtypepicker.html @@ -24,16 +24,16 @@
    - {{ contentPreview = vm.getElementTypeByAlias(entry.elementTypeAlias); "" }} + {{ contentPreview = vm.getElementTypeByAlias(entry.contentTypeAlias); "" }}
    -
    - +
    diff --git a/src/Umbraco.Web/PropertyEditors/BlockListConfiguration.cs b/src/Umbraco.Web/PropertyEditors/BlockListConfiguration.cs index 6485bd061a..f069804348 100644 --- a/src/Umbraco.Web/PropertyEditors/BlockListConfiguration.cs +++ b/src/Umbraco.Web/PropertyEditors/BlockListConfiguration.cs @@ -29,7 +29,7 @@ namespace Umbraco.Web.PropertyEditors public class BlockConfiguration { // TODO: rename this to contentElementTypeAlias, I would like this to be specific, since we have the settings. - [JsonProperty("elementTypeAlias")] + [JsonProperty("contentTypeAlias")] public string Alias { get; set; } [JsonProperty("settingsElementTypeAlias")] @@ -38,8 +38,8 @@ namespace Umbraco.Web.PropertyEditors [JsonProperty("view")] public string View { get; set; } - [JsonProperty("labelTemplate")] - public string Template { get; set; } + [JsonProperty("label")] + public string Label { get; set; } } [ConfigurationField("useAccordionsAsDefault", "Inline editing mode", "boolean", Description = "Use the inline editor as the default block view")] From 28adc1b4f3b892e01b09cab5ee341812b5bb45ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Wed, 26 Feb 2020 14:47:52 +0100 Subject: [PATCH 049/377] renaming --- .../src/common/services/blockeditor.service.js | 4 ++-- .../views/propertyeditors/blocklist/blocklist.component.js | 2 +- 2 files changed, 3 insertions(+), 3 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 8846dc7d3a..6ed5b27211 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 @@ -50,10 +50,10 @@ BlockEditorModelObject.prototype = { getBlockConfiguration: function(alias) { - return this.blockConfigurations.find(blockConfiguration => blockConfiguration.contentTypeAlias === alias); + return this.blockConfigurations.find(bc => bc.contentTypeAlias === alias); }, - loadScaffolds: function(contentResource) { + loadScaffolding: function(contentResource) { var tasks = []; var scaffoldAliases = []; diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.js index 7aa03f9402..1cb7a8d259 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.js @@ -865,7 +865,7 @@ var modelObject = blockEditorService.createModelObject(model.value, model.editor, model.config.blocks); - modelObject.loadScaffolds(contentResource).then(loaded); + modelObject.loadScaffolding(contentResource).then(loaded); vm.layout = []; vm.blocks = []; From 3779ea80508ce2963f7f4f1d02ac83963abb7e5b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Wed, 26 Feb 2020 14:49:17 +0100 Subject: [PATCH 050/377] use Chrome Headless for unit tests --- src/Umbraco.Web.UI.Client/gulp/tasks/test.js | 16 +- src/Umbraco.Web.UI.Client/gulpfile.js | 4 +- src/Umbraco.Web.UI.Client/package-lock.json | 364 +++++++++++++----- src/Umbraco.Web.UI.Client/package.json | 5 +- .../test/config/karma.conf.js | 3 +- 5 files changed, 278 insertions(+), 114 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/gulp/tasks/test.js b/src/Umbraco.Web.UI.Client/gulp/tasks/test.js index 1e8d074f7e..b5239d35e7 100644 --- a/src/Umbraco.Web.UI.Client/gulp/tasks/test.js +++ b/src/Umbraco.Web.UI.Client/gulp/tasks/test.js @@ -6,11 +6,23 @@ var karmaServer = require('karma').Server; * Build tests **************************/ - // Karma test +// Karma test function testUnit() { + return new karmaServer({ + configFile: __dirname + "/../../test/config/karma.conf.js" + }) + .start(); +}; + +// Run karma test server +function runUnitTestServer() { + return new karmaServer({ configFile: __dirname + "/../../test/config/karma.conf.js", + autoWatch: true, + port: 9999, + singleRun: false, keepalive: true }) .start(); @@ -24,4 +36,4 @@ function testE2e() { .start(); }; -module.exports = { testUnit: testUnit, testE2e: testE2e }; +module.exports = { testUnit: testUnit, testE2e: testE2e, runUnitTestServer: runUnitTestServer }; diff --git a/src/Umbraco.Web.UI.Client/gulpfile.js b/src/Umbraco.Web.UI.Client/gulpfile.js index 705c54bf04..f6964df7c5 100644 --- a/src/Umbraco.Web.UI.Client/gulpfile.js +++ b/src/Umbraco.Web.UI.Client/gulpfile.js @@ -17,7 +17,7 @@ const { setDevelopmentMode } = require('./gulp/modes'); const { dependencies } = require('./gulp/tasks/dependencies'); const { js } = require('./gulp/tasks/js'); const { less } = require('./gulp/tasks/less'); -const { testE2e, testUnit } = require('./gulp/tasks/test'); +const { testE2e, testUnit, runUnitTestServer } = require('./gulp/tasks/test'); const { views } = require('./gulp/tasks/views'); const { watchTask } = require('./gulp/tasks/watchTask'); @@ -32,5 +32,5 @@ exports.dev = series(setDevelopmentMode, parallel(dependencies, js, less, views) exports.watch = series(watchTask); // exports.runTests = series(js, testUnit); -exports.testUnit = series(testUnit); +exports.runUnit = series(runUnitTestServer); exports.testE2e = series(testE2e); diff --git a/src/Umbraco.Web.UI.Client/package-lock.json b/src/Umbraco.Web.UI.Client/package-lock.json index 42a89c5d13..be8174739a 100644 --- a/src/Umbraco.Web.UI.Client/package-lock.json +++ b/src/Umbraco.Web.UI.Client/package-lock.json @@ -1806,7 +1806,8 @@ "version": "1.3.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.3.1.tgz", "integrity": "sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g==", - "dev": true + "dev": true, + "optional": true }, "base64id": { "version": "1.0.0", @@ -2053,6 +2054,7 @@ "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-2.0.1.tgz", "integrity": "sha512-88em58dDVB/KzPEx1X0N3LwFfYZPyDc4B6eF38M1rk9VTZMbxXXgjugz8mmwpS9Ox4BDZ+t6t3QP5+/gazweIA==", "dev": true, + "optional": true, "requires": { "p-finally": "^1.0.0" } @@ -2094,6 +2096,7 @@ "resolved": "http://registry.npmjs.org/bl/-/bl-1.2.2.tgz", "integrity": "sha512-e8tQYnZodmebYDWGH7KMRvtzKXaJHx3BbilrgZCfvyLUYdKpK1t5PSPmpkny/SgiTSCnjfLW7v5rlONXVFkQEA==", "dev": true, + "optional": true, "requires": { "readable-stream": "^2.3.5", "safe-buffer": "^5.1.1" @@ -2103,13 +2106,15 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", - "dev": true + "dev": true, + "optional": true }, "readable-stream": { "version": "2.3.6", "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", "dev": true, + "optional": true, "requires": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", @@ -2125,6 +2130,7 @@ "resolved": "http://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", "dev": true, + "optional": true, "requires": { "safe-buffer": "~5.1.0" } @@ -2259,6 +2265,7 @@ "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.4.3.tgz", "integrity": "sha512-zvj65TkFeIt3i6aj5bIvJDzjjQQGs4o/sNoezg1F1kYap9Nu2jcUdpwzRSJTHMMzG0H7bZkn4rNQpImhuxWX2A==", "dev": true, + "optional": true, "requires": { "base64-js": "^1.0.2", "ieee754": "^1.1.4" @@ -2284,7 +2291,8 @@ "version": "0.2.13", "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", "integrity": "sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI=", - "dev": true + "dev": true, + "optional": true }, "buffer-equal": { "version": "1.0.0", @@ -2475,6 +2483,7 @@ "resolved": "https://registry.npmjs.org/caw/-/caw-2.0.1.tgz", "integrity": "sha512-Cg8/ZSBEa8ZVY9HspcGUYaK63d/bN7rqS3CYCzEGUxuYv6UlmcjzDUz2fCFFHyTvUW5Pk0I+3hkA3iXlIj6guA==", "dev": true, + "optional": true, "requires": { "get-proxy": "^2.0.0", "isurl": "^1.0.0-alpha5", @@ -2908,6 +2917,7 @@ "resolved": "https://registry.npmjs.org/commander/-/commander-2.8.1.tgz", "integrity": "sha1-Br42f+v9oMMwqh4qBy09yXYkJdQ=", "dev": true, + "optional": true, "requires": { "graceful-readlink": ">= 1.0.0" } @@ -3002,6 +3012,7 @@ "resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.12.tgz", "integrity": "sha512-a1eOIcu8+7lUInge4Rpf/n4Krkf3Dd9lqhljRzII1/Zno/kRtUWnznPO3jOKBmTEktkt3fkxisUcivoj0ebzoA==", "dev": true, + "optional": true, "requires": { "ini": "^1.3.4", "proto-list": "~1.2.1" @@ -3057,6 +3068,7 @@ "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz", "integrity": "sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g==", "dev": true, + "optional": true, "requires": { "safe-buffer": "5.1.2" } @@ -3460,6 +3472,7 @@ "resolved": "https://registry.npmjs.org/decompress/-/decompress-4.2.0.tgz", "integrity": "sha1-eu3YVCflqS2s/lVnSnxQXpbQH50=", "dev": true, + "optional": true, "requires": { "decompress-tar": "^4.0.0", "decompress-tarbz2": "^4.0.0", @@ -3476,6 +3489,7 @@ "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.3.0.tgz", "integrity": "sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ==", "dev": true, + "optional": true, "requires": { "pify": "^3.0.0" }, @@ -3484,7 +3498,8 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", - "dev": true + "dev": true, + "optional": true } } } @@ -3495,6 +3510,7 @@ "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-3.3.0.tgz", "integrity": "sha1-gKTdMjdIOEv6JICDYirt7Jgq3/M=", "dev": true, + "optional": true, "requires": { "mimic-response": "^1.0.0" } @@ -3504,6 +3520,7 @@ "resolved": "https://registry.npmjs.org/decompress-tar/-/decompress-tar-4.1.1.tgz", "integrity": "sha512-JdJMaCrGpB5fESVyxwpCx4Jdj2AagLmv3y58Qy4GE6HMVjWz1FeVQk1Ct4Kye7PftcdOo/7U7UKzYBJgqnGeUQ==", "dev": true, + "optional": true, "requires": { "file-type": "^5.2.0", "is-stream": "^1.1.0", @@ -3514,7 +3531,8 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/file-type/-/file-type-5.2.0.tgz", "integrity": "sha1-LdvqfHP/42No365J3DOMBYwritY=", - "dev": true + "dev": true, + "optional": true } } }, @@ -3523,6 +3541,7 @@ "resolved": "https://registry.npmjs.org/decompress-tarbz2/-/decompress-tarbz2-4.1.1.tgz", "integrity": "sha512-s88xLzf1r81ICXLAVQVzaN6ZmX4A6U4z2nMbOwobxkLoIIfjVMBg7TeguTUXkKeXni795B6y5rnvDw7rxhAq9A==", "dev": true, + "optional": true, "requires": { "decompress-tar": "^4.1.0", "file-type": "^6.1.0", @@ -3535,7 +3554,8 @@ "version": "6.2.0", "resolved": "https://registry.npmjs.org/file-type/-/file-type-6.2.0.tgz", "integrity": "sha512-YPcTBDV+2Tm0VqjybVd32MHdlEGAtuxS3VAYsumFokDSMG+ROT5wawGlnHDoz7bfMcMDt9hxuXvXwoKUx2fkOg==", - "dev": true + "dev": true, + "optional": true } } }, @@ -3544,6 +3564,7 @@ "resolved": "https://registry.npmjs.org/decompress-targz/-/decompress-targz-4.1.1.tgz", "integrity": "sha512-4z81Znfr6chWnRDNfFNqLwPvm4db3WuZkqV+UgXQzSngG3CEKdBkw5jrv3axjjL96glyiiKjsxJG3X6WBZwX3w==", "dev": true, + "optional": true, "requires": { "decompress-tar": "^4.1.1", "file-type": "^5.2.0", @@ -3554,7 +3575,8 @@ "version": "5.2.0", "resolved": "https://registry.npmjs.org/file-type/-/file-type-5.2.0.tgz", "integrity": "sha1-LdvqfHP/42No365J3DOMBYwritY=", - "dev": true + "dev": true, + "optional": true } } }, @@ -3563,6 +3585,7 @@ "resolved": "https://registry.npmjs.org/decompress-unzip/-/decompress-unzip-4.0.1.tgz", "integrity": "sha1-3qrM39FK6vhVePczroIQ+bSEj2k=", "dev": true, + "optional": true, "requires": { "file-type": "^3.8.0", "get-stream": "^2.2.0", @@ -3574,13 +3597,15 @@ "version": "3.9.0", "resolved": "https://registry.npmjs.org/file-type/-/file-type-3.9.0.tgz", "integrity": "sha1-JXoHg4TR24CHvESdEH1SpSZyuek=", - "dev": true + "dev": true, + "optional": true }, "get-stream": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-2.3.1.tgz", "integrity": "sha1-Xzj5PzRgCWZu4BUKBUFn+Rvdld4=", "dev": true, + "optional": true, "requires": { "object-assign": "^4.0.1", "pinkie-promise": "^2.0.0" @@ -3590,7 +3615,8 @@ "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", - "dev": true + "dev": true, + "optional": true } } }, @@ -3855,7 +3881,8 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", - "dev": true + "dev": true, + "optional": true } } }, @@ -3872,7 +3899,8 @@ "version": "0.1.4", "resolved": "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.4.tgz", "integrity": "sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI=", - "dev": true + "dev": true, + "optional": true }, "duplexify": { "version": "3.7.1", @@ -4475,6 +4503,7 @@ "resolved": "https://registry.npmjs.org/execa/-/execa-0.7.0.tgz", "integrity": "sha1-lEvs00zEHuMqY6n68nrVpl/Fl3c=", "dev": true, + "optional": true, "requires": { "cross-spawn": "^5.0.1", "get-stream": "^3.0.0", @@ -4490,6 +4519,7 @@ "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz", "integrity": "sha1-6L0O/uWPz/b4+UUQoKVUu/ojVEk=", "dev": true, + "optional": true, "requires": { "lru-cache": "^4.0.1", "shebang-command": "^1.2.0", @@ -4629,6 +4659,7 @@ "resolved": "https://registry.npmjs.org/ext-list/-/ext-list-2.2.2.tgz", "integrity": "sha512-u+SQgsubraE6zItfVA0tBuCBhfU9ogSRnsvygI7wht9TS510oLkBRXBsqopeUG/GBOIQyKZO9wjTqIu/sf5zFA==", "dev": true, + "optional": true, "requires": { "mime-db": "^1.28.0" } @@ -4638,6 +4669,7 @@ "resolved": "https://registry.npmjs.org/ext-name/-/ext-name-5.0.0.tgz", "integrity": "sha512-yblEwXAbGv1VQDmow7s38W77hzAgJAO50ztBLMcUyUBfxv1HC+LGwtiEN+Co6LtlqT/5uwVOxsD4TNIilWhwdQ==", "dev": true, + "optional": true, "requires": { "ext-list": "^2.0.0", "sort-keys-length": "^1.0.0" @@ -4919,6 +4951,7 @@ "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", "integrity": "sha1-JcfInLH5B3+IkbvmHY85Dq4lbx4=", "dev": true, + "optional": true, "requires": { "pend": "~1.2.0" } @@ -4957,13 +4990,15 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/filename-reserved-regex/-/filename-reserved-regex-2.0.0.tgz", "integrity": "sha1-q/c9+rc10EVECr/qLZHzieu/oik=", - "dev": true + "dev": true, + "optional": true }, "filenamify": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/filenamify/-/filenamify-2.1.0.tgz", "integrity": "sha512-ICw7NTT6RsDp2rnYKVd8Fu4cr6ITzGy3+u4vUujPkabyaz+03F24NWEX7fs5fp+kBonlaqPH8fAO2NM+SXt/JA==", "dev": true, + "optional": true, "requires": { "filename-reserved-regex": "^2.0.0", "strip-outer": "^1.0.0", @@ -5313,7 +5348,8 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", "integrity": "sha1-a+Dem+mYzhavivwkSXue6bfM2a0=", - "dev": true + "dev": true, + "optional": true }, "fs-extra": { "version": "1.0.0", @@ -5379,7 +5415,8 @@ "ansi-regex": { "version": "2.1.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "aproba": { "version": "1.2.0", @@ -5400,12 +5437,14 @@ "balanced-match": { "version": "1.0.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "brace-expansion": { "version": "1.1.11", "bundled": true, "dev": true, + "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -5420,17 +5459,20 @@ "code-point-at": { "version": "1.1.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "concat-map": { "version": "0.0.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "console-control-strings": { "version": "1.1.0", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "core-util-is": { "version": "1.0.2", @@ -5547,7 +5589,8 @@ "inherits": { "version": "2.0.3", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "ini": { "version": "1.3.5", @@ -5559,6 +5602,7 @@ "version": "1.0.0", "bundled": true, "dev": true, + "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -5573,6 +5617,7 @@ "version": "3.0.4", "bundled": true, "dev": true, + "optional": true, "requires": { "brace-expansion": "^1.1.7" } @@ -5580,12 +5625,14 @@ "minimist": { "version": "0.0.8", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "minipass": { "version": "2.3.5", "bundled": true, "dev": true, + "optional": true, "requires": { "safe-buffer": "^5.1.2", "yallist": "^3.0.0" @@ -5604,6 +5651,7 @@ "version": "0.5.1", "bundled": true, "dev": true, + "optional": true, "requires": { "minimist": "0.0.8" } @@ -5684,7 +5732,8 @@ "number-is-nan": { "version": "1.0.1", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "object-assign": { "version": "4.1.1", @@ -5696,6 +5745,7 @@ "version": "1.4.0", "bundled": true, "dev": true, + "optional": true, "requires": { "wrappy": "1" } @@ -5781,7 +5831,8 @@ "safe-buffer": { "version": "5.1.2", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "safer-buffer": { "version": "2.1.2", @@ -5817,6 +5868,7 @@ "version": "1.0.2", "bundled": true, "dev": true, + "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -5836,6 +5888,7 @@ "version": "3.0.1", "bundled": true, "dev": true, + "optional": true, "requires": { "ansi-regex": "^2.0.0" } @@ -5879,12 +5932,14 @@ "wrappy": { "version": "1.0.2", "bundled": true, - "dev": true + "dev": true, + "optional": true }, "yallist": { "version": "3.0.3", "bundled": true, - "dev": true + "dev": true, + "optional": true } } }, @@ -5911,6 +5966,7 @@ "resolved": "https://registry.npmjs.org/get-proxy/-/get-proxy-2.1.0.tgz", "integrity": "sha512-zmZIaQTWnNQb4R4fJUEp/FC51eZsc6EkErspy3xtIYStaq8EB/hDIWipxsal+E8rz0qD7f2sL/NA9Xee4RInJw==", "dev": true, + "optional": true, "requires": { "npm-conf": "^1.1.0" } @@ -5919,13 +5975,15 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-4.0.1.tgz", "integrity": "sha1-uWjGsKBDhDJJAui/Gl3zJXmkUP4=", - "dev": true + "dev": true, + "optional": true }, "get-stream": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", "integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=", - "dev": true + "dev": true, + "optional": true }, "get-value": { "version": "2.0.6", @@ -6240,7 +6298,8 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/graceful-readlink/-/graceful-readlink-1.0.1.tgz", "integrity": "sha1-TK+tdrxi8C+gObL5Tpo906ORpyU=", - "dev": true + "dev": true, + "optional": true }, "growly": { "version": "1.3.0", @@ -7059,7 +7118,8 @@ "version": "1.4.2", "resolved": "https://registry.npmjs.org/has-symbol-support-x/-/has-symbol-support-x-1.4.2.tgz", "integrity": "sha512-3ToOva++HaW+eCpgqZrCfN51IPB+7bJNVT6CUATzueB5Heb8o6Nam0V3HG5dlDvZU1Gn5QLcbahiKw/XVk5JJw==", - "dev": true + "dev": true, + "optional": true }, "has-symbols": { "version": "1.0.0", @@ -7072,6 +7132,7 @@ "resolved": "https://registry.npmjs.org/has-to-string-tag-x/-/has-to-string-tag-x-1.4.1.tgz", "integrity": "sha512-vdbKfmw+3LoOYVr+mtxHaX5a96+0f3DljYd8JOqvOLsf5mw2Otda2qCDT9qRqLAhrjyQ0h7ual5nOiASpsGNFw==", "dev": true, + "optional": true, "requires": { "has-symbol-support-x": "^1.4.1" } @@ -7272,7 +7333,8 @@ "version": "1.1.13", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz", "integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==", - "dev": true + "dev": true, + "optional": true }, "ignore": { "version": "4.0.6", @@ -7402,6 +7464,7 @@ "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-2.1.0.tgz", "integrity": "sha1-ji1INIdCEhtKghi3oTfppSBJ3IA=", "dev": true, + "optional": true, "requires": { "repeating": "^2.0.0" } @@ -7723,6 +7786,7 @@ "resolved": "https://registry.npmjs.org/is-finite/-/is-finite-1.0.2.tgz", "integrity": "sha1-zGZ3aVYCvlUO8R6LSqYwU0K20Ko=", "dev": true, + "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -7775,7 +7839,8 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/is-natural-number/-/is-natural-number-4.0.1.tgz", "integrity": "sha1-q5124dtM7VHjXeDHLr7PCfc0zeg=", - "dev": true + "dev": true, + "optional": true }, "is-negated-glob": { "version": "1.0.0", @@ -7813,13 +7878,15 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/is-object/-/is-object-1.0.1.tgz", "integrity": "sha1-iVJojF7C/9awPsyF52ngKQMINHA=", - "dev": true + "dev": true, + "optional": true }, "is-plain-obj": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", "integrity": "sha1-caUMhCnfync8kqOQpKA7OfzVHT4=", - "dev": true + "dev": true, + "optional": true }, "is-plain-object": { "version": "2.0.4", @@ -7883,7 +7950,8 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/is-retry-allowed/-/is-retry-allowed-1.2.0.tgz", "integrity": "sha512-RUbUeKwvm3XG2VYamhJL1xFktgjvPzL0Hq8C+6yrWIswDy3BIXGqCxhxkc30N9jqK311gVU137K8Ei55/zVJRg==", - "dev": true + "dev": true, + "optional": true }, "is-stream": { "version": "1.1.0", @@ -7986,6 +8054,7 @@ "resolved": "https://registry.npmjs.org/isurl/-/isurl-1.0.0.tgz", "integrity": "sha512-1P/yWsxPlDtn7QeRD+ULKQPaIaN6yF368GZ2vDfv0AL0NwpStafjWCDDdn0k8wgFMWpVAqG7oJhxHnlud42i9w==", "dev": true, + "optional": true, "requires": { "has-to-string-tag-x": "^1.2.0", "is-object": "^1.0.1" @@ -8308,6 +8377,15 @@ } } }, + "karma-chrome-launcher": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/karma-chrome-launcher/-/karma-chrome-launcher-3.1.0.tgz", + "integrity": "sha512-3dPs/n7vgz1rxxtynpzZTvb9y/GIaW8xjAwcIGttLbycqoFtI7yo1NGnQi6oFTherRE+GIhCAHZC4vEqWGhNvg==", + "dev": true, + "requires": { + "which": "^1.2.1" + } + }, "karma-jasmine": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/karma-jasmine/-/karma-jasmine-2.0.1.tgz", @@ -8803,7 +8881,8 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.1.tgz", "integrity": "sha1-b54wtHCE2XGnyCD/FabFFnt0wm8=", - "dev": true + "dev": true, + "optional": true }, "lpad-align": { "version": "1.1.2", @@ -8873,7 +8952,8 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz", "integrity": "sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0=", - "dev": true + "dev": true, + "optional": true }, "map-visit": { "version": "1.0.0", @@ -9029,7 +9109,8 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==", - "dev": true + "dev": true, + "optional": true }, "minimatch": { "version": "3.0.4", @@ -9266,9 +9347,9 @@ } }, "npm": { - "version": "6.13.6", - "resolved": "https://registry.npmjs.org/npm/-/npm-6.13.6.tgz", - "integrity": "sha512-NomC08kv7HIl1FOyLOe9Hp89kYsOsvx52huVIJ7i8hFW8Xp65lDwe/8wTIrh9q9SaQhA8hTrfXPh3BEL3TmMpw==", + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/npm/-/npm-6.14.0.tgz", + "integrity": "sha512-OgfdLadz7j6dikbpaimmLzMxwLKbXthQXHiJwtegorwtBVnhecfUeYkHopwd5ICaiClQnqlYQCHERXDiYK3Jcw==", "requires": { "JSONStream": "^1.3.5", "abbrev": "~1.1.1", @@ -9276,12 +9357,12 @@ "ansistyles": "~0.1.3", "aproba": "^2.0.0", "archy": "~1.0.0", - "bin-links": "^1.1.6", + "bin-links": "^1.1.7", "bluebird": "^3.5.5", "byte-size": "^5.0.1", "cacache": "^12.0.3", "call-limit": "^1.1.1", - "chownr": "^1.1.3", + "chownr": "^1.1.4", "ci-info": "^2.0.0", "cli-columns": "^3.1.2", "cli-table3": "^0.5.1", @@ -9301,7 +9382,7 @@ "glob": "^7.1.4", "graceful-fs": "^4.2.3", "has-unicode": "~2.0.1", - "hosted-git-info": "^2.8.5", + "hosted-git-info": "^2.8.6", "iferr": "^1.0.2", "imurmurhash": "*", "infer-owner": "^1.0.4", @@ -9319,7 +9400,7 @@ "libnpmorg": "^1.0.1", "libnpmsearch": "^2.0.2", "libnpmteam": "^1.0.2", - "libnpx": "^10.2.0", + "libnpx": "^10.2.2", "lock-verify": "^2.1.0", "lockfile": "^1.0.4", "lodash._baseindexof": "*", @@ -9338,7 +9419,7 @@ "mississippi": "^3.0.0", "mkdirp": "~0.5.1", "move-concurrently": "^1.0.1", - "node-gyp": "^5.0.5", + "node-gyp": "^5.0.7", "nopt": "~4.0.1", "normalize-package-data": "^2.5.0", "npm-audit-report": "^1.3.2", @@ -9346,10 +9427,10 @@ "npm-install-checks": "^3.0.2", "npm-lifecycle": "^3.1.4", "npm-package-arg": "^6.1.1", - "npm-packlist": "^1.4.7", + "npm-packlist": "^1.4.8", "npm-pick-manifest": "^3.0.2", "npm-profile": "^4.0.2", - "npm-registry-fetch": "^4.0.2", + "npm-registry-fetch": "^4.0.3", "npm-user-validate": "~1.0.0", "npmlog": "~4.1.2", "once": "~1.4.0", @@ -9366,7 +9447,7 @@ "read-installed": "~4.0.3", "read-package-json": "^2.1.1", "read-package-tree": "^5.3.1", - "readable-stream": "^3.4.0", + "readable-stream": "^3.6.0", "readdir-scoped-modules": "^1.1.0", "request": "^2.88.0", "retry": "^0.12.0", @@ -9535,7 +9616,7 @@ } }, "bin-links": { - "version": "1.1.6", + "version": "1.1.7", "bundled": true, "requires": { "bluebird": "^3.5.3", @@ -9634,7 +9715,7 @@ } }, "chownr": { - "version": "1.1.3", + "version": "1.1.4", "bundled": true }, "ci-info": { @@ -10015,7 +10096,7 @@ } }, "env-paths": { - "version": "1.0.0", + "version": "2.2.0", "bundled": true }, "err-code": { @@ -10318,7 +10399,7 @@ } }, "get-caller-file": { - "version": "1.0.2", + "version": "1.0.3", "bundled": true }, "get-stream": { @@ -10413,7 +10494,7 @@ "bundled": true }, "hosted-git-info": { - "version": "2.8.5", + "version": "2.8.6", "bundled": true }, "http-cache-semantics": { @@ -10513,7 +10594,7 @@ } }, "invert-kv": { - "version": "1.0.0", + "version": "2.0.0", "bundled": true }, "ip": { @@ -10671,10 +10752,10 @@ "bundled": true }, "lcid": { - "version": "1.0.0", + "version": "2.0.0", "bundled": true, "requires": { - "invert-kv": "^1.0.0" + "invert-kv": "^2.0.0" } }, "libcipm": { @@ -10833,7 +10914,7 @@ } }, "libnpx": { - "version": "10.2.0", + "version": "10.2.2", "bundled": true, "requires": { "dotenv": "^5.0.1", @@ -10963,15 +11044,30 @@ "ssri": "^6.0.0" } }, + "map-age-cleaner": { + "version": "0.1.3", + "bundled": true, + "requires": { + "p-defer": "^1.0.0" + } + }, "meant": { "version": "1.0.1", "bundled": true }, "mem": { - "version": "1.1.0", + "version": "4.3.0", "bundled": true, "requires": { - "mimic-fn": "^1.0.0" + "map-age-cleaner": "^0.1.1", + "mimic-fn": "^2.0.0", + "p-is-promise": "^2.0.0" + }, + "dependencies": { + "mimic-fn": { + "version": "2.1.0", + "bundled": true + } } }, "mime-db": { @@ -10985,10 +11081,6 @@ "mime-db": "~1.35.0" } }, - "mimic-fn": { - "version": "1.2.0", - "bundled": true - }, "minimatch": { "version": "3.0.4", "bundled": true, @@ -11066,6 +11158,10 @@ "version": "0.0.7", "bundled": true }, + "nice-try": { + "version": "1.0.5", + "bundled": true + }, "node-fetch-npm": { "version": "2.0.2", "bundled": true, @@ -11076,33 +11172,20 @@ } }, "node-gyp": { - "version": "5.0.5", + "version": "5.0.7", "bundled": true, "requires": { - "env-paths": "^1.0.0", - "glob": "^7.0.3", - "graceful-fs": "^4.1.2", - "mkdirp": "^0.5.0", - "nopt": "2 || 3", - "npmlog": "0 || 1 || 2 || 3 || 4", - "request": "^2.87.0", - "rimraf": "2", - "semver": "~5.3.0", + "env-paths": "^2.2.0", + "glob": "^7.1.4", + "graceful-fs": "^4.2.2", + "mkdirp": "^0.5.1", + "nopt": "^4.0.1", + "npmlog": "^4.1.2", + "request": "^2.88.0", + "rimraf": "^2.6.3", + "semver": "^5.7.1", "tar": "^4.4.12", - "which": "1" - }, - "dependencies": { - "nopt": { - "version": "3.0.6", - "bundled": true, - "requires": { - "abbrev": "1" - } - }, - "semver": { - "version": "5.3.0", - "bundled": true - } + "which": "^1.3.1" } }, "nopt": { @@ -11191,11 +11274,12 @@ } }, "npm-packlist": { - "version": "1.4.7", + "version": "1.4.8", "bundled": true, "requires": { "ignore-walk": "^3.0.1", - "npm-bundled": "^1.0.1" + "npm-bundled": "^1.0.1", + "npm-normalize-package-bin": "^1.0.1" } }, "npm-pick-manifest": { @@ -11217,7 +11301,7 @@ } }, "npm-registry-fetch": { - "version": "4.0.2", + "version": "4.0.3", "bundled": true, "requires": { "JSONStream": "^1.3.4", @@ -11296,12 +11380,38 @@ "bundled": true }, "os-locale": { - "version": "2.1.0", + "version": "3.1.0", "bundled": true, "requires": { - "execa": "^0.7.0", - "lcid": "^1.0.0", - "mem": "^1.1.0" + "execa": "^1.0.0", + "lcid": "^2.0.0", + "mem": "^4.0.0" + }, + "dependencies": { + "cross-spawn": { + "version": "6.0.5", + "bundled": true, + "requires": { + "nice-try": "^1.0.4", + "path-key": "^2.0.1", + "semver": "^5.5.0", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + } + }, + "execa": { + "version": "1.0.0", + "bundled": true, + "requires": { + "cross-spawn": "^6.0.0", + "get-stream": "^4.0.0", + "is-stream": "^1.1.0", + "npm-run-path": "^2.0.0", + "p-finally": "^1.0.0", + "signal-exit": "^3.0.0", + "strip-eof": "^1.0.0" + } + } } }, "os-tmpdir": { @@ -11316,10 +11426,18 @@ "os-tmpdir": "^1.0.0" } }, + "p-defer": { + "version": "1.0.0", + "bundled": true + }, "p-finally": { "version": "1.0.0", "bundled": true }, + "p-is-promise": { + "version": "2.1.0", + "bundled": true + }, "p-limit": { "version": "1.2.0", "bundled": true, @@ -11625,7 +11743,7 @@ } }, "readable-stream": { - "version": "3.4.0", + "version": "3.6.0", "bundled": true, "requires": { "inherits": "^2.0.3", @@ -11960,10 +12078,16 @@ } }, "string_decoder": { - "version": "1.2.0", + "version": "1.3.0", "bundled": true, "requires": { - "safe-buffer": "~5.1.0" + "safe-buffer": "~5.2.0" + }, + "dependencies": { + "safe-buffer": { + "version": "5.2.0", + "bundled": true + } } }, "stringify-package": { @@ -12294,14 +12418,14 @@ "bundled": true }, "yargs": { - "version": "11.0.0", + "version": "11.1.1", "bundled": true, "requires": { "cliui": "^4.0.0", "decamelize": "^1.1.1", "find-up": "^2.1.0", "get-caller-file": "^1.0.1", - "os-locale": "^2.0.0", + "os-locale": "^3.1.0", "require-directory": "^2.1.1", "require-main-filename": "^1.0.1", "set-blocking": "^2.0.0", @@ -12331,6 +12455,7 @@ "resolved": "https://registry.npmjs.org/npm-conf/-/npm-conf-1.1.3.tgz", "integrity": "sha512-Yic4bZHJOt9RCFbRP3GgpqhScOY4HH3V2P8yBj6CeYq118Qr+BLXqT2JvpJ00mryLESpgOxf5XlFv4ZjXxLScw==", "dev": true, + "optional": true, "requires": { "config-chain": "^1.1.11", "pify": "^3.0.0" @@ -12340,7 +12465,8 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", - "dev": true + "dev": true, + "optional": true } } }, @@ -12349,6 +12475,7 @@ "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", "integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=", "dev": true, + "optional": true, "requires": { "path-key": "^2.0.0" } @@ -12704,7 +12831,8 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=", - "dev": true + "dev": true, + "optional": true }, "p-is-promise": { "version": "1.1.0", @@ -12741,6 +12869,7 @@ "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-1.2.1.tgz", "integrity": "sha1-XrOzU7f86Z8QGhA4iAuwVOu+o4Y=", "dev": true, + "optional": true, "requires": { "p-finally": "^1.0.0" } @@ -13464,7 +13593,8 @@ "version": "1.2.4", "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz", "integrity": "sha1-IS1b/hMYMGpCD2QCuOJv85ZHqEk=", - "dev": true + "dev": true, + "optional": true }, "prr": { "version": "1.0.1", @@ -13820,6 +13950,7 @@ "resolved": "https://registry.npmjs.org/repeating/-/repeating-2.0.1.tgz", "integrity": "sha1-UhTFOpJtNVJwdSf7q0FdvAjQbdo=", "dev": true, + "optional": true, "requires": { "is-finite": "^1.0.0" } @@ -14159,6 +14290,7 @@ "resolved": "https://registry.npmjs.org/seek-bzip/-/seek-bzip-1.0.5.tgz", "integrity": "sha1-z+kXyz0nS8/6x5J1ivUxc+sfq9w=", "dev": true, + "optional": true, "requires": { "commander": "~2.8.1" } @@ -14561,6 +14693,7 @@ "resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-1.1.2.tgz", "integrity": "sha1-RBttTTRnmPG05J6JIK37oOVD+a0=", "dev": true, + "optional": true, "requires": { "is-plain-obj": "^1.0.0" } @@ -14570,6 +14703,7 @@ "resolved": "https://registry.npmjs.org/sort-keys-length/-/sort-keys-length-1.0.1.tgz", "integrity": "sha1-nLb09OnkgVWmqgZx7dM2/xR5oYg=", "dev": true, + "optional": true, "requires": { "sort-keys": "^1.0.0" } @@ -14894,6 +15028,7 @@ "resolved": "https://registry.npmjs.org/strip-dirs/-/strip-dirs-2.1.0.tgz", "integrity": "sha512-JOCxOeKLm2CAS73y/U4ZeZPTkE+gNVCzKt7Eox84Iej1LT/2pTWYpZKJuxwQpvX1LiZb1xokNR7RLfuBAa7T3g==", "dev": true, + "optional": true, "requires": { "is-natural-number": "^4.0.1" } @@ -14902,7 +15037,8 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=", - "dev": true + "dev": true, + "optional": true }, "strip-indent": { "version": "1.0.1", @@ -14925,6 +15061,7 @@ "resolved": "https://registry.npmjs.org/strip-outer/-/strip-outer-1.0.1.tgz", "integrity": "sha1-sv0qv2YEudHmATBXGV34Nrip1jE=", "dev": true, + "optional": true, "requires": { "escape-string-regexp": "^1.0.2" } @@ -15044,6 +15181,7 @@ "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-1.6.2.tgz", "integrity": "sha512-rzS0heiNf8Xn7/mpdSVVSMAWAoy9bfb1WOTYC78Z0UQKeKa/CWS8FOq0lKGNa8DWKAn9gxjCvMLYc5PGXYlK2A==", "dev": true, + "optional": true, "requires": { "bl": "^1.0.0", "buffer-alloc": "^1.2.0", @@ -15058,13 +15196,15 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", - "dev": true + "dev": true, + "optional": true }, "readable-stream": { "version": "2.3.6", "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", "dev": true, + "optional": true, "requires": { "core-util-is": "~1.0.0", "inherits": "~2.0.3", @@ -15080,6 +15220,7 @@ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", "dev": true, + "optional": true, "requires": { "safe-buffer": "~5.1.0" } @@ -15090,13 +15231,15 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/temp-dir/-/temp-dir-1.0.0.tgz", "integrity": "sha1-CnwOom06Oa+n4OvqnB/AvE2qAR0=", - "dev": true + "dev": true, + "optional": true }, "tempfile": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/tempfile/-/tempfile-2.0.0.tgz", "integrity": "sha1-awRGhWqbERTRhW/8vlCczLCXcmU=", "dev": true, + "optional": true, "requires": { "temp-dir": "^1.0.0", "uuid": "^3.0.1" @@ -15197,7 +15340,8 @@ "version": "4.0.1", "resolved": "https://registry.npmjs.org/timed-out/-/timed-out-4.0.1.tgz", "integrity": "sha1-8y6srFoXW+ol1/q1Zas+2HQe9W8=", - "dev": true + "dev": true, + "optional": true }, "timers-ext": { "version": "0.1.7", @@ -15254,7 +15398,8 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.1.1.tgz", "integrity": "sha1-STvUj2LXxD/N7TE6A9ytsuEhOoA=", - "dev": true + "dev": true, + "optional": true }, "to-fast-properties": { "version": "2.0.0", @@ -15349,6 +15494,7 @@ "resolved": "https://registry.npmjs.org/trim-repeated/-/trim-repeated-1.0.0.tgz", "integrity": "sha1-42RqLqTokTEr9+rObPsFOAvAHCE=", "dev": true, + "optional": true, "requires": { "escape-string-regexp": "^1.0.2" } @@ -15370,6 +15516,7 @@ "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", "dev": true, + "optional": true, "requires": { "safe-buffer": "^5.0.1" } @@ -15485,6 +15632,7 @@ "resolved": "https://registry.npmjs.org/unbzip2-stream/-/unbzip2-stream-1.3.3.tgz", "integrity": "sha512-fUlAF7U9Ah1Q6EieQ4x4zLNejrRvDWUYmxXUpN3uziFYCHapjWFaCAnreY9bGgxzaMCFAPPpYNng57CypwJVhg==", "dev": true, + "optional": true, "requires": { "buffer": "^5.2.1", "through": "^2.3.8" @@ -15685,7 +15833,8 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/url-to-options/-/url-to-options-1.0.1.tgz", "integrity": "sha1-FQWgOiiaSMvXpDTvuu7FBV9WM6k=", - "dev": true + "dev": true, + "optional": true }, "use": { "version": "3.1.1", @@ -16104,6 +16253,7 @@ "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", "integrity": "sha1-x+sXyT4RLLEIb6bY5R+wZnt5pfk=", "dev": true, + "optional": true, "requires": { "buffer-crc32": "~0.2.3", "fd-slicer": "~1.1.0" diff --git a/src/Umbraco.Web.UI.Client/package.json b/src/Umbraco.Web.UI.Client/package.json index c150af79de..a956b5d803 100644 --- a/src/Umbraco.Web.UI.Client/package.json +++ b/src/Umbraco.Web.UI.Client/package.json @@ -2,7 +2,7 @@ "private": true, "scripts": { "test": "gulp runTests", - "unit": "gulp testUnit", + "unit": "gulp runUnit", "e2e": "gulp testE2e", "build": "gulp build", "dev": "gulp dev", @@ -39,7 +39,7 @@ "moment": "2.22.2", "ng-file-upload": "12.2.13", "nouislider": "14.1.1", - "npm": "6.13.6", + "npm": "^6.14.0", "signalr": "2.4.0", "spectrum-colorpicker": "1.8.0", "tinymce": "4.9.7", @@ -72,6 +72,7 @@ "gulp-wrap-js": "0.4.1", "jasmine-core": "3.5.0", "karma": "4.4.1", + "karma-chrome-launcher": "^3.1.0", "karma-jasmine": "2.0.1", "karma-junit-reporter": "2.0.1", "karma-phantomjs-launcher": "1.0.4", 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 4e3a78144b..8a120b3165 100644 --- a/src/Umbraco.Web.UI.Client/test/config/karma.conf.js +++ b/src/Umbraco.Web.UI.Client/test/config/karma.conf.js @@ -98,7 +98,7 @@ module.exports = function (config) { // - PhantomJS // - IE (only Windows) // CLI --browsers Chrome,Firefox,Safari - browsers: ['PhantomJS'], + browsers: ['ChromeHeadless'], // allow waiting a bit longer, some machines require this @@ -115,6 +115,7 @@ module.exports = function (config) { plugins: [ require('karma-jasmine'), require('karma-phantomjs-launcher'), + require('karma-chrome-launcher'), require('karma-junit-reporter'), require('karma-spec-reporter') From 4a6484b577dc91d03f232f26afb4aa9e3af2c139 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Wed, 26 Feb 2020 17:03:06 +0100 Subject: [PATCH 051/377] test for blockEditorService --- src/Umbraco.Web.UI.Client/gulpfile.js | 2 +- src/Umbraco.Web.UI.Client/package-lock.json | 6 + src/Umbraco.Web.UI.Client/package.json | 1 + .../src/common/mocks/resources/_utils.js | 168 ++++++++++++++++++ .../mocks/resources/variantcontent.mocks.js | 56 ++++++ .../test/config/karma.conf.js | 3 + .../common/filters/truncate-filters.spec.js | 1 - .../services/block-editor-service.spec.js | 66 +++++++ 8 files changed, 301 insertions(+), 2 deletions(-) create mode 100644 src/Umbraco.Web.UI.Client/src/common/mocks/resources/variantcontent.mocks.js create mode 100644 src/Umbraco.Web.UI.Client/test/unit/common/services/block-editor-service.spec.js diff --git a/src/Umbraco.Web.UI.Client/gulpfile.js b/src/Umbraco.Web.UI.Client/gulpfile.js index f6964df7c5..ec9b7bc508 100644 --- a/src/Umbraco.Web.UI.Client/gulpfile.js +++ b/src/Umbraco.Web.UI.Client/gulpfile.js @@ -32,5 +32,5 @@ exports.dev = series(setDevelopmentMode, parallel(dependencies, js, less, views) exports.watch = series(watchTask); // exports.runTests = series(js, testUnit); -exports.runUnit = series(runUnitTestServer); +exports.runUnit = series(js, runUnitTestServer, watchTask); exports.testE2e = series(testE2e); diff --git a/src/Umbraco.Web.UI.Client/package-lock.json b/src/Umbraco.Web.UI.Client/package-lock.json index be8174739a..5bab6a2e22 100644 --- a/src/Umbraco.Web.UI.Client/package-lock.json +++ b/src/Umbraco.Web.UI.Client/package-lock.json @@ -8066,6 +8066,12 @@ "integrity": "sha512-nCeAiw37MIMA9w9IXso7bRaLl+c/ef3wnxsoSAlYrzS+Ot0zTG6nU8G/cIfGkqpkjX2wNaIW9RFG0TwIFnG6bA==", "dev": true }, + "jasmine-promise-matchers": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/jasmine-promise-matchers/-/jasmine-promise-matchers-2.6.0.tgz", + "integrity": "sha1-J1ASqFEeXoh9g11TWKutIMAmz2M=", + "dev": true + }, "jpegtran-bin": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/jpegtran-bin/-/jpegtran-bin-4.0.0.tgz", diff --git a/src/Umbraco.Web.UI.Client/package.json b/src/Umbraco.Web.UI.Client/package.json index a956b5d803..e7a6cce6a6 100644 --- a/src/Umbraco.Web.UI.Client/package.json +++ b/src/Umbraco.Web.UI.Client/package.json @@ -71,6 +71,7 @@ "gulp-wrap": "0.15.0", "gulp-wrap-js": "0.4.1", "jasmine-core": "3.5.0", + "jasmine-promise-matchers": "^2.6.0", "karma": "4.4.1", "karma-chrome-launcher": "^3.1.0", "karma-jasmine": "2.0.1", diff --git a/src/Umbraco.Web.UI.Client/src/common/mocks/resources/_utils.js b/src/Umbraco.Web.UI.Client/src/common/mocks/resources/_utils.js index 6e6eb00da7..cf73e6a8ce 100644 --- a/src/Umbraco.Web.UI.Client/src/common/mocks/resources/_utils.js +++ b/src/Umbraco.Web.UI.Client/src/common/mocks/resources/_utils.js @@ -280,6 +280,174 @@ angular.module('umbraco.mocks'). return node; }, + + /** Creats a mock variant content object */ + getMockVariantContent: function(id) { + var node = { + name: "My content with id: " + id, + updateDate: new Date().toIsoDateTimeString(), + publishDate: new Date().toIsoDateTimeString(), + createDate: new Date().toIsoDateTimeString(), + id: id, + parentId: 1234, + icon: "icon-umb-content", + owner: { name: "Administrator", id: 0 }, + updater: { name: "Per Ploug Krogslund", id: 1 }, + path: "-1,1234,2455", + allowedActions: ["U", "H", "A"], + variants: [ + { + name: "", + language: null, + segment: null, + state: "NotCreated", + updateDate: "0001-01-01 00:00:00", + createDate: "0001-01-01 00:00:00", + publishDate: null, + releaseDate: null, + expireDate: null, + notifications: [], + tabs: [ + { + label: "Content", + id: 2, + properties: [ + { alias: "valTest", label: "Validation test", view: "validationtest", value: "asdfasdf" }, + { alias: "bodyText", label: "Body Text", description: "Here you enter the primary article contents", view: "rte", value: "

    askjdkasj lasjd

    ", config: {} }, + { alias: "textarea", label: "textarea", view: "textarea", value: "ajsdka sdjkds", config: { rows: 4 } }, + { alias: "media", label: "Media picker", view: "mediapicker", value: "1234,23242,23232,23231", config: {multiPicker: 1} } + ] + }, + { + label: "Sample Editor", + id: 3, + properties: [ + { alias: "datepicker", label: "Datepicker", view: "datepicker", config: { pickTime: false, format: "yyyy-MM-dd" } }, + { alias: "tags", label: "Tags", view: "tags", value: "" } + ] + }, + { + label: "This", + id: 4, + properties: [ + { alias: "valTest4", label: "Validation test", view: "validationtest", value: "asdfasdf" }, + { alias: "bodyText4", label: "Body Text", description: "Here you enter the primary article contents", view: "rte", value: "

    askjdkasj lasjd

    ", config: {} }, + { alias: "textarea4", label: "textarea", view: "textarea", value: "ajsdka sdjkds", config: { rows: 4 } }, + { alias: "content4", label: "Content picker", view: "contentpicker", value: "1234,23242,23232,23231" } + ] + }, + { + label: "Is", + id: 5, + properties: [ + { alias: "valTest5", label: "Validation test", view: "validationtest", value: "asdfasdf" }, + { alias: "bodyText5", label: "Body Text", description: "Here you enter the primary article contents", view: "rte", value: "

    askjdkasj lasjd

    ", config: {} }, + { alias: "textarea5", label: "textarea", view: "textarea", value: "ajsdka sdjkds", config: { rows: 4 } }, + { alias: "content5", label: "Content picker", view: "contentpicker", value: "1234,23242,23232,23231" } + ] + }, + { + label: "Overflown", + id: 6, + properties: [ + { alias: "valTest6", label: "Validation test", view: "validationtest", value: "asdfasdf" }, + { alias: "bodyText6", label: "Body Text", description: "Here you enter the primary article contents", view: "rte", value: "

    askjdkasj lasjd

    ", config: {} }, + { alias: "textarea6", label: "textarea", view: "textarea", value: "ajsdka sdjkds", config: { rows: 4 } }, + { alias: "content6", label: "Content picker", view: "contentpicker", value: "1234,23242,23232,23231" } + ] + }, + { + label: "Generic Properties", + id: 0, + properties: [ + { + label: 'Id', + value: 1234, + view: "readonlyvalue", + alias: "_umb_id" + }, + { + label: 'Created by', + description: 'Original author', + value: "Administrator", + view: "readonlyvalue", + alias: "_umb_createdby" + }, + { + label: 'Created', + description: 'Date/time this document was created', + value: new Date().toIsoDateTimeString(), + view: "readonlyvalue", + alias: "_umb_createdate" + }, + { + label: 'Updated', + description: 'Date/time this document was created', + value: new Date().toIsoDateTimeString(), + view: "readonlyvalue", + alias: "_umb_updatedate" + }, + { + label: 'Document Type', + value: "Home page", + view: "readonlyvalue", + alias: "_umb_doctype" + }, + { + label: 'Publish at', + description: 'Date/time to publish this document', + value: new Date().toIsoDateTimeString(), + view: "datepicker", + alias: "_umb_releasedate" + }, + { + label: 'Unpublish at', + description: 'Date/time to un-publish this document', + value: new Date().toIsoDateTimeString(), + view: "datepicker", + alias: "_umb_expiredate" + }, + { + label: 'Template', + value: "myTemplate", + view: "dropdown", + alias: "_umb_template", + config: { + items: { + "" : "-- Choose template --", + "myTemplate" : "My Templates", + "home" : "Home Page", + "news" : "News Page" + } + } + }, + { + label: 'Link to document', + value: ["/testing" + id, "http://localhost/testing" + id, "http://mydomain.com/testing" + id].join(), + view: "urllist", + alias: "_umb_urllist" + }, + { + alias: "test", label: "Stuff", view: "test", value: "", + config: { + fields: [ + { alias: "embedded", label: "Embbeded", view: "textstring", value: "" }, + { alias: "embedded2", label: "Embbeded 2", view: "contentpicker", value: "" }, + { alias: "embedded3", label: "Embbeded 3", view: "textarea", value: "" }, + { alias: "embedded4", label: "Embbeded 4", view: "datepicker", value: "" } + ] + } + } + ] + } + ] + } + ] + }; + + return node; + }, + getMockEntity : function(id){ return {name: "hello", id: id, icon: "icon-file"}; }, diff --git a/src/Umbraco.Web.UI.Client/src/common/mocks/resources/variantcontent.mocks.js b/src/Umbraco.Web.UI.Client/src/common/mocks/resources/variantcontent.mocks.js new file mode 100644 index 0000000000..1ff2d3a0a7 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/common/mocks/resources/variantcontent.mocks.js @@ -0,0 +1,56 @@ +angular.module('umbraco.mocks'). + factory('variantContentMocks', ['$httpBackend', 'mocksUtils', function ($httpBackend, mocksUtils) { + 'use strict'; + + function returnEmptyVariantNode(status, data, headers) { + + if (!mocksUtils.checkAuth()) { + return [401, null, null]; + } + + var response = returnVariantNodebyId(200, "", null); + var node = response[1]; + var parentId = mocksUtils.getParameterByName(data, "parentId") || 1234; + + node.name = ""; + node.id = 0; + node.parentId = parentId; + + $(node.tabs).each(function(i,tab){ + $(tab.properties).each(function(i, property){ + property.value = ""; + }); + }); + + return response; + } + + function returnVariantNodebyId(status, data, headers) { + + if (!mocksUtils.checkAuth()) { + return [401, null, null]; + } + + var id = mocksUtils.getParameterByName(data, "id") || "1234"; + id = parseInt(id, 10); + + var node = mocksUtils.getMockVariantContent(id); + + return [200, node, null]; + } + + + return { + register: function () { + + $httpBackend + .whenGET(mocksUtils.urlRegex('/umbraco/UmbracoApi/Content/GetById?')) + .respond(returnVariantNodebyId); + + $httpBackend + .whenGET(mocksUtils.urlRegex('/umbraco/UmbracoApi/Content/GetEmpty')) + .respond(returnEmptyVariantNode); + + } + }; +}]); 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 8a120b3165..501e43b043 100644 --- a/src/Umbraco.Web.UI.Client/test/config/karma.conf.js +++ b/src/Umbraco.Web.UI.Client/test/config/karma.conf.js @@ -10,6 +10,9 @@ module.exports = function (config) { // list of files / patterns to load in the browser files: [ + // Jasmine plugins + 'node_modules/jasmine-promise-matchers/dist/jasmine-promise-matchers.js', + //libraries 'node_modules/jquery/dist/jquery.min.js', 'node_modules/angular/angular.js', diff --git a/src/Umbraco.Web.UI.Client/test/unit/common/filters/truncate-filters.spec.js b/src/Umbraco.Web.UI.Client/test/unit/common/filters/truncate-filters.spec.js index 1e9ea2ea46..64a984b499 100644 --- a/src/Umbraco.Web.UI.Client/test/unit/common/filters/truncate-filters.spec.js +++ b/src/Umbraco.Web.UI.Client/test/unit/common/filters/truncate-filters.spec.js @@ -53,7 +53,6 @@ testCases.forEach(function(test){ it('Expects \'' + test.input + '\' to be truncated as \''+ test.expectedResult + '\', when noOfChars=' + test.noOfChars + ', and appendDots=' + test.appendDots, function() { - console.log($truncate(test.input, test.noOfChars, test.appendDots)); expect($truncate(test.input, test.noOfChars, test.appendDots)).toBe(test.expectedResult); }); }); diff --git a/src/Umbraco.Web.UI.Client/test/unit/common/services/block-editor-service.spec.js b/src/Umbraco.Web.UI.Client/test/unit/common/services/block-editor-service.spec.js new file mode 100644 index 0000000000..5c48b4a8dc --- /dev/null +++ b/src/Umbraco.Web.UI.Client/test/unit/common/services/block-editor-service.spec.js @@ -0,0 +1,66 @@ +describe('blockEditorService tests', function () { + + var blockEditorService, $rootScope, $httpBackend, varaintMocks, contentResource; + + beforeEach(module('umbraco.services')); + beforeEach(module('umbraco.mocks')); + + beforeEach(inject(function ($injector, mocksUtils) { + + mocksUtils.disableAuth(); + + blockEditorService = $injector.get('blockEditorService'); + $rootScope = $injector.get('$rootScope'); + $httpBackend = $injector.get('$httpBackend'); + varaintMocks = $injector.get("variantContentMocks"); + varaintMocks.register(); + contentResource = $injector.get('contentResource'); + + })); + + + var simpleBlockConfigurationMock = {contentTypeAlias: "testAlias", label:"Test", settingsElementTypeAlias: null, view: "testview.html"}; + + + describe('init blockEditoModelObject', function () { + + it('fail if no model value', function () { + function createWithNoModelValue() { + blockEditorService.createModelObject(null, "test", []); + } + expect(createWithNoModelValue).toThrow(); + }); + + it('return a object, with methods', function () { + var modelObject = blockEditorService.createModelObject({}, "test", []); + + expect(modelObject).not.toBeUndefined(); + expect(modelObject.loadScaffolding).not.toBeUndefined(); + }); + + it('getBlockConfiguration provide the requested block configurtion', function () { + var modelObject = blockEditorService.createModelObject({}, "test", [simpleBlockConfigurationMock]); + + expect(modelObject.getBlockConfiguration(simpleBlockConfigurationMock.contentTypeAlias).label).toBe(simpleBlockConfigurationMock.label); + }); + + it('loadScaffolding provides data for itemPicker', function () { + var modelObject = blockEditorService.createModelObject({}, "test", [simpleBlockConfigurationMock]); + + var itemPickerOptions; + + var pendingPromise = modelObject.loadScaffolding(contentResource).then(() => { + itemPickerOptions = modelObject.getAvailableBlocksForItemPicker(); + }); + + $rootScope.$digest(); + $httpBackend.flush(); + + expect(itemPickerOptions.length).toBe(1); + + + }); + + }); + +}); From e30d90ac7e84fa1f88e153a8db0c66e6d1295e89 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Wed, 26 Feb 2020 17:03:24 +0100 Subject: [PATCH 052/377] safer code --- .../src/common/services/blockeditor.service.js | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 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 6ed5b27211..a4f7ca9e7b 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 @@ -85,12 +85,13 @@ this.blockConfigurations.forEach(blockConfiguration => { var scaffold = this.getScaffoldFor(blockConfiguration.contentTypeAlias); - - blocks.push({ - alias: scaffold.contentTypeAlias, - name: scaffold.contentTypeName, - icon: scaffold.icon - }); + if(scaffold) { + blocks.push({ + alias: scaffold.contentTypeAlias, + name: scaffold.contentTypeName, + icon: scaffold.icon + }); + } }); return blocks; @@ -110,6 +111,11 @@ var blockConfiguration = this.getBlockConfiguration(contentModel.contentTypeAlias); + if (blockConfiguration === null) { + // This is not an allowed block type, therefor we return null; + return null; + } + // TODO: make blockConfiguration the base for model, remeber to make a copy. var model = { label: "", From f43c0338f9fc43239e258278db969370bee19ab4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Wed, 26 Feb 2020 17:03:48 +0100 Subject: [PATCH 053/377] block list editor --- .../blocklist/blocklist.component.js | 21 ++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.js index 1cb7a8d259..5e450eb133 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.js @@ -879,7 +879,10 @@ vm.layout = modelObject.getLayout(); vm.layout.forEach(entry => { - vm.blocks.push(modelObject.getEditingModel(entry)); + var block = modelObject.getEditingModel(entry); + if(block !== null) { + vm.blocks.push(block); + } }); vm.availableBlockTypes = modelObject.getAvailableBlocksForItemPicker(); @@ -916,13 +919,20 @@ var index = vm.blocks.indexOf(block); if(index !== -1) { vm.blocks.splice(index, 1); - } - if(vm.quickMenuIndex > index) { - vm.quickMenuIndex--; + + var layoutIndex = this.layout.findIndex(entry => entry.udi === block.udi); + if(layoutIndex !== -1) { + vm.layout.splice(layoutIndex, 1); + } + + this.modelObject.removeContentByUdi(block.udi); } } vm.editBlock = function(blockModel) { + + // TODO: test wether i need to clone or if that is done by overlay. + //var blockModelClone = angular.copy(blockModel); var elementEditor = { block: blockModel, @@ -930,6 +940,7 @@ size: blockModel.overlaySize, submit: function(model) { blockModel.content = model.block.content; + blockModel.settings = model.block.settings; editorService.close(); }, close: function() { @@ -1025,7 +1036,7 @@ }; $scope.blockApi = { - removeBlock: vm.removeBlock + deleteBlock: vm.deleteBlock } From 5b910c178a4f193d190367c4f1da3402aa8c4d0e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Mon, 2 Mar 2020 11:12:04 +0100 Subject: [PATCH 054/377] rename view to overlayView --- .../blocklist/blocklist.component.js | 2 +- .../blocklist.elementtypepicker.controller.js | 13 +++++-------- .../prevalue/blocklist.elementtypepicker.html | 6 +++--- .../PropertyEditors/BlockListConfiguration.cs | 4 ++-- 4 files changed, 11 insertions(+), 14 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.js index 5e450eb133..eefe339569 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.js @@ -936,7 +936,7 @@ var elementEditor = { block: blockModel, - view: "views/common/infiniteeditors/elementeditor/elementeditor.html", + view: blockModel.overlayView || "views/common/infiniteeditors/elementeditor/elementeditor.html", size: blockModel.overlaySize, submit: function(model) { blockModel.content = model.block.content; diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/blocklist.elementtypepicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/blocklist.elementtypepicker.controller.js index 77751579c9..1f76afef8e 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/blocklist.elementtypepicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/blocklist.elementtypepicker.controller.js @@ -150,7 +150,7 @@ var entry = { "contentTypeAlias": alias, - "view": null, + "overlayView": null, "labelTemplate": "", "settingsElementTypeAlias": null }; @@ -233,7 +233,7 @@ localizationService.localizeMany(["general_remove", "defaultdialogs_confirmremoveusageof"]).then(function (data) { overlayService.confirmRemove({ title: data[0], - content: localizationService.tokenReplace(data[1], [entry.view]), + content: localizationService.tokenReplace(data[1], [entry.overlayView]), close: function () { overlayService.close(); }, @@ -245,7 +245,7 @@ }); }; vm.removeViewForEntry = function(entry) { - entry.view = null; + entry.overlayView = null; }; vm.addViewForEntry = function(entry) { const filePicker = { @@ -255,13 +255,10 @@ entityType: "file", isDialog: true, filter: function (i) { - if (i.name.indexOf(".html") !== -1) { - return true; - } + return (i.name.indexOf(".html") !== -1); }, select: function (file) { - console.log(file); - entry.view = file.name; + entry.overlayView = file.name; editorService.close(); }, close: function () { diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/blocklist.elementtypepicker.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/blocklist.elementtypepicker.html index 8184761a1a..75a59a0f2d 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/blocklist.elementtypepicker.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/blocklist.elementtypepicker.html @@ -36,16 +36,16 @@
    -
    +
    - +
    -
    diff --git a/src/Umbraco.Web/PropertyEditors/BlockListConfiguration.cs b/src/Umbraco.Web/PropertyEditors/BlockListConfiguration.cs index f069804348..f76612db59 100644 --- a/src/Umbraco.Web/PropertyEditors/BlockListConfiguration.cs +++ b/src/Umbraco.Web/PropertyEditors/BlockListConfiguration.cs @@ -35,8 +35,8 @@ namespace Umbraco.Web.PropertyEditors [JsonProperty("settingsElementTypeAlias")] public string SettingsElementTypeAlias { get; set; } - [JsonProperty("view")] - public string View { get; set; } + [JsonProperty("overlayView")] + public string OverlayView { get; set; } [JsonProperty("label")] public string Label { get; set; } From c72a36c373802bff6a8daa2c8f9b67f2597d950e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Mon, 2 Mar 2020 11:12:17 +0100 Subject: [PATCH 055/377] block editor work --- .../common/services/blockeditor.service.js | 25 +++++++------------ 1 file changed, 9 insertions(+), 16 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 a4f7ca9e7b..2e761fa9e3 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 @@ -37,7 +37,7 @@ // ensure basic part of data-structure is in place: this.value = propertyModelValue; - this.value.layout = this.value.layout || []; + this.value.layout = this.value.layout || {}; this.value.data = this.value.data || []; this.propertyEditorAlias = propertyEditorAlias; @@ -97,7 +97,7 @@ return blocks; }, - getScaffoldFor: function(contentTypeAlias, data) { + getScaffoldFor: function(contentTypeAlias) { return this.scaffolds.find(o => o.contentTypeAlias === contentTypeAlias); }, @@ -116,14 +116,10 @@ return null; } - // TODO: make blockConfiguration the base for model, remeber to make a copy. - var model = { - label: "", - labelInterpolate: $interpolate(blockConfiguration.label), - editor: blockConfiguration.view, - overlaySize: "medium" - }; - + var model = angular.copy(blockConfiguration); + model.labelInterpolate = $interpolate(model.label); + model.overlaySize = model.overlaySize || "medium"; + var scaffold = this.getScaffoldFor(blockConfiguration.contentTypeAlias); if(scaffold === null) { return null; @@ -169,12 +165,11 @@ return entry; }, - // You make entries in your layout your self. - + // private getContentByUdi: function(udi) { return this.value.data.find(entry => entry.udi === udi); }, - + // private createContent: function(elementTypeAlias) { var content = { contentTypeAlias: elementTypeAlias, @@ -183,7 +178,7 @@ this.value.data.push(content); return content.udi; }, - + // private removeContent: function(entry) { const index = this.value.data.indexOf(entry) if (index > -1) { @@ -205,8 +200,6 @@ }, getBlockLabel: function(blockModelObject, labelIndex) { - console.log("getBlockLabel", blockModelObject); - // TODO: we should do something about this for performance. var vars = new Object(); From 13aa5cf2bafb32d4b08ee95c5822432b07d7b95a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Mon, 2 Mar 2020 11:23:57 +0100 Subject: [PATCH 056/377] Revert "rename view to overlayView" This reverts commit 5b910c178a4f193d190367c4f1da3402aa8c4d0e. --- .../blocklist/blocklist.component.js | 2 +- .../blocklist.elementtypepicker.controller.js | 13 ++++++++----- .../prevalue/blocklist.elementtypepicker.html | 6 +++--- .../PropertyEditors/BlockListConfiguration.cs | 4 ++-- 4 files changed, 14 insertions(+), 11 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.js index eefe339569..5e450eb133 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.js @@ -936,7 +936,7 @@ var elementEditor = { block: blockModel, - view: blockModel.overlayView || "views/common/infiniteeditors/elementeditor/elementeditor.html", + view: "views/common/infiniteeditors/elementeditor/elementeditor.html", size: blockModel.overlaySize, submit: function(model) { blockModel.content = model.block.content; diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/blocklist.elementtypepicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/blocklist.elementtypepicker.controller.js index 1f76afef8e..77751579c9 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/blocklist.elementtypepicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/blocklist.elementtypepicker.controller.js @@ -150,7 +150,7 @@ var entry = { "contentTypeAlias": alias, - "overlayView": null, + "view": null, "labelTemplate": "", "settingsElementTypeAlias": null }; @@ -233,7 +233,7 @@ localizationService.localizeMany(["general_remove", "defaultdialogs_confirmremoveusageof"]).then(function (data) { overlayService.confirmRemove({ title: data[0], - content: localizationService.tokenReplace(data[1], [entry.overlayView]), + content: localizationService.tokenReplace(data[1], [entry.view]), close: function () { overlayService.close(); }, @@ -245,7 +245,7 @@ }); }; vm.removeViewForEntry = function(entry) { - entry.overlayView = null; + entry.view = null; }; vm.addViewForEntry = function(entry) { const filePicker = { @@ -255,10 +255,13 @@ entityType: "file", isDialog: true, filter: function (i) { - return (i.name.indexOf(".html") !== -1); + if (i.name.indexOf(".html") !== -1) { + return true; + } }, select: function (file) { - entry.overlayView = file.name; + console.log(file); + entry.view = file.name; editorService.close(); }, close: function () { diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/blocklist.elementtypepicker.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/blocklist.elementtypepicker.html index 75a59a0f2d..8184761a1a 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/blocklist.elementtypepicker.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/blocklist.elementtypepicker.html @@ -36,16 +36,16 @@
    -
    +
    - +
    -
    diff --git a/src/Umbraco.Web/PropertyEditors/BlockListConfiguration.cs b/src/Umbraco.Web/PropertyEditors/BlockListConfiguration.cs index f76612db59..f069804348 100644 --- a/src/Umbraco.Web/PropertyEditors/BlockListConfiguration.cs +++ b/src/Umbraco.Web/PropertyEditors/BlockListConfiguration.cs @@ -35,8 +35,8 @@ namespace Umbraco.Web.PropertyEditors [JsonProperty("settingsElementTypeAlias")] public string SettingsElementTypeAlias { get; set; } - [JsonProperty("overlayView")] - public string OverlayView { get; set; } + [JsonProperty("view")] + public string View { get; set; } [JsonProperty("label")] public string Label { get; set; } From 36dab09cc31f18e8de36b7b5a68be8a5fee512d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Tue, 3 Mar 2020 13:02:13 +0100 Subject: [PATCH 057/377] block editor implementation --- .../common/services/blockeditor.service.js | 80 +- .../src/common/services/udi.service.js | 2 +- .../imageblock/imageblock.editor.html | 4 +- .../inlineblock/inlineblock.editor.html | 8 +- .../inlineblock/inlineblock.editor.less | 1 + .../labelblock/labelblock.editor.html | 6 +- .../elementContentEditor.component.html | 2 +- .../elementContentEditor.component.js | 2 - .../elementeditor/elementeditor.controller.js | 4 +- .../blocklist/blocklist.block.component.html | 20 + .../blocklist/blocklist.block.component.js | 65 + .../blocklist/blocklist.component.html | 71 +- .../blocklist/blocklist.component.js | 1063 +++-------------- .../propertyeditors/blocklist/blocklist.html | 2 +- .../PropertyEditors/BlockListConfiguration.cs | 4 +- 15 files changed, 322 insertions(+), 1012 deletions(-) create mode 100644 src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.block.component.html create mode 100644 src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.block.component.js 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 2e761fa9e3..bcea8efc5e 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 @@ -5,24 +5,43 @@ function blockEditorService($interpolate, udiService) { - function applyModelToScaffold(scaffold, contentModel) { - - scaffold.key = contentModel.key; + function mapToEditingModel(editingModel, contentModel) { - var variant = scaffold.variants[0]; + var variant = editingModel.variants[0]; + + for (var t = 0; t < variant.tabs.length; t++) { + var tab = variant.tabs[t]; + + for (var p = 0; p < tab.properties.length; p++) { + var prop = tab.properties[p]; + if (contentModel[prop.alias]) { + console.log("mapping:", prop.alias, contentModel[prop.alias]) + prop.value = contentModel[prop.alias]; + } + } + } + } + + function mapToPropertyModel(editingModel, contentModel) { + + var variant = editingModel.variants[0]; for (var t = 0; t < variant.tabs.length; t++) { var tab = variant.tabs[t]; for (var p = 0; p < tab.properties.length; p++) { var prop = tab.properties[p]; - if (contentModel[prop.propertyAlias]) { - prop.value = contentModel[prop.propertyAlias]; + if (prop.value) { + contentModel[prop.propertyAlias] = prop.value; } } } } + function mapValueToPropertyModel(value, alias, contentModel) { + contentModel[alias] = value; + } + /** * @ngdoc factory @@ -107,7 +126,9 @@ */ getEditingModel: function(layoutEntry) { - var contentModel = this.getContentByUdi(layoutEntry.udi); + var udi = layoutEntry.udi; + + var contentModel = this.getContentByUdi(udi); var blockConfiguration = this.getBlockConfiguration(contentModel.contentTypeAlias); @@ -116,21 +137,44 @@ return null; } - var model = angular.copy(blockConfiguration); - model.labelInterpolate = $interpolate(model.label); - model.overlaySize = model.overlaySize || "medium"; - + var editingModel = {}; + editingModel.config = angular.copy(blockConfiguration); + editingModel.labelInterpolator = $interpolate(editingModel.config.label); + var scaffold = this.getScaffoldFor(blockConfiguration.contentTypeAlias); if(scaffold === null) { return null; } - model.content = angular.copy(scaffold); - applyModelToScaffold(model.content, contentModel); + // make basics from scaffold + editingModel.content = angular.copy(scaffold); + editingModel.content.udi = udi; + + mapToEditingModel(editingModel.content, contentModel); + + editingModel.contentModel = contentModel; + editingModel.layoutModel = layoutEntry; // TODO: settings - return model; + return editingModel; + + }, + + + /** + * Retrieve editing model of a layout entry + * @return {Object} Scaffolded Block Content object. + */ + setDataFromEditingModel: function(editingModel) { + + var udi = editingModel.content.key; + + var contentModel = this.getContentByUdi(udi); + + mapToPropertyModel(editingModel.content, contentModel); + + // TODO: sync settings to layout entry. }, @@ -165,7 +209,6 @@ return entry; }, - // private getContentByUdi: function(udi) { return this.value.data.find(entry => entry.udi === udi); }, @@ -198,12 +241,11 @@ createModelObject: function(propertyModelValue, propertyEditorAlias, blockConfigurations) { return new BlockEditorModelObject(propertyModelValue, propertyEditorAlias, blockConfigurations); }, - getBlockLabel: function(blockModelObject, labelIndex) { + getBlockLabel: function(blockModelObject) { // TODO: we should do something about this for performance. var vars = new Object(); - vars["$index"] = labelIndex; var variant = blockModelObject.content.variants[0]; var tab = variant.tabs[0]; @@ -212,8 +254,8 @@ vars[property.alias] = property.value; } - if(blockModelObject.labelInterpolate) { - return blockModelObject.labelInterpolate(vars); + if(blockModelObject.labelInterpolator) { + return blockModelObject.labelInterpolator(vars); } return blockModelObject.contentTypeName; diff --git a/src/Umbraco.Web.UI.Client/src/common/services/udi.service.js b/src/Umbraco.Web.UI.Client/src/common/services/udi.service.js index b7d0cd05e8..dd289c96a6 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/udi.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/udi.service.js @@ -22,7 +22,7 @@ * @returns {string} The generated UDI */ create: function(entityType) { - return "umb://" + entityType + "/" + String.CreateGuid(); + return "umb://" + entityType + "/" + (String.CreateGuid().replace(/-/g, "")); } } } diff --git a/src/Umbraco.Web.UI.Client/src/views/blockelements/imageblock/imageblock.editor.html b/src/Umbraco.Web.UI.Client/src/views/blockelements/imageblock/imageblock.editor.html index 0b82d2202d..e5e68dde9c 100644 --- a/src/Umbraco.Web.UI.Client/src/views/blockelements/imageblock/imageblock.editor.html +++ b/src/Umbraco.Web.UI.Client/src/views/blockelements/imageblock/imageblock.editor.html @@ -1,3 +1,3 @@ - diff --git a/src/Umbraco.Web.UI.Client/src/views/blockelements/inlineblock/inlineblock.editor.html b/src/Umbraco.Web.UI.Client/src/views/blockelements/inlineblock/inlineblock.editor.html index 9561d28a9e..31754466ab 100644 --- a/src/Umbraco.Web.UI.Client/src/views/blockelements/inlineblock/inlineblock.editor.html +++ b/src/Umbraco.Web.UI.Client/src/views/blockelements/inlineblock/inlineblock.editor.html @@ -1,10 +1,10 @@
    -
    - +
    diff --git a/src/Umbraco.Web.UI.Client/src/views/blockelements/inlineblock/inlineblock.editor.less b/src/Umbraco.Web.UI.Client/src/views/blockelements/inlineblock/inlineblock.editor.less index dc014c5daf..ab6b21d898 100644 --- a/src/Umbraco.Web.UI.Client/src/views/blockelements/inlineblock/inlineblock.editor.less +++ b/src/Umbraco.Web.UI.Client/src/views/blockelements/inlineblock/inlineblock.editor.less @@ -55,6 +55,7 @@ } .blockelement-inlineblock-editor__inner { border-top: 1px solid @gray-8; + background-color: @gray-11; .umb-group-panel { background-color: transparent; diff --git a/src/Umbraco.Web.UI.Client/src/views/blockelements/labelblock/labelblock.editor.html b/src/Umbraco.Web.UI.Client/src/views/blockelements/labelblock/labelblock.editor.html index 419a1fd8a9..78595fce29 100644 --- a/src/Umbraco.Web.UI.Client/src/views/blockelements/labelblock/labelblock.editor.html +++ b/src/Umbraco.Web.UI.Client/src/views/blockelements/labelblock/labelblock.editor.html @@ -1,4 +1,4 @@ - diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/elementeditor/elementContentEditor.component.html b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/elementeditor/elementContentEditor.component.html index 09724f6619..88a2b306ef 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/elementeditor/elementContentEditor.component.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/elementeditor/elementContentEditor.component.html @@ -2,7 +2,7 @@
    + ng-repeat="group in vm.content.variants[0].tabs track by group.label">
    {{ group.label }}
    diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/elementeditor/elementContentEditor.component.js b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/elementeditor/elementContentEditor.component.js index 637df93a72..347d8f1d7b 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/elementeditor/elementContentEditor.component.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/elementeditor/elementContentEditor.component.js @@ -14,8 +14,6 @@ function ElementEditorComponentController() { - const vm = this; - // TODO: we might not need this.. } diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/elementeditor/elementeditor.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/elementeditor/elementeditor.controller.js index ebabe86617..a054e64784 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/elementeditor/elementeditor.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/elementeditor/elementeditor.controller.js @@ -5,9 +5,9 @@ angular.module("umbraco") var vm = this; - vm.content = $scope.model.block.content; + vm.content = $scope.model.content; - vm.title = $scope.model.block.label; + vm.title = $scope.model.title; vm.saveAndClose = function() { if ($scope.model && $scope.model.submit) { diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.block.component.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.block.component.html new file mode 100644 index 0000000000..4231e5c83e --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.block.component.html @@ -0,0 +1,20 @@ +
    +
    +
    + +
    + + +
    + +
    diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.block.component.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.block.component.js new file mode 100644 index 0000000000..a1b38e509f --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.block.component.js @@ -0,0 +1,65 @@ +(function () { + "use strict"; + angular + .module("umbraco") + .component("blockListPropertyEditorBlock", { + templateUrl: "views/propertyeditors/blocklist/blocklist.block.component.html", + controller: BlockListBlockController, + controllerAs: "vm", + bindings: { + block: "=", + blockEditorApi: "=", + focusThisBlock: " -
    +
    - -
    - -
    -
    -
    - -

    No editor

    -
    - -
    - - -
    - -
    + +
    - - - -
    Minimum %0% entries, needs %1% more.
    -
    +
    Maximum %0% entries, %1% too many.
    diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.js index 5e450eb133..5546fdeb8b 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.js @@ -8,7 +8,8 @@ controller: BlockListController, controllerAs: "vm", bindings: { - + model: "=", + propertyForm: "=" }, require: { umbProperty: "?^umbProperty" @@ -17,905 +18,135 @@ function BlockListController($scope, $interpolate, editorService, clipboardService, localizationService, overlayService, blockEditorService, contentResource) { + var modelObject; var unsubscribe = []; var vm = this; - var model = $scope.$parent.$parent.model; - vm.propertyForm = $scope.$parent.$parent.propertyForm; - $scope.moveFocusToBlock = null; + vm.moveFocusToBlock = null; - console.log("model JSON:", JSON.stringify(model)); - console.log("model:", model); - console.log("config:", model.config); + vm.$onInit = function() { - vm.validationLimit = model.config.validationLimit; + vm.validationLimit = vm.model.config.validationLimit; + + vm.model.value = vm.model.value || {}; + + modelObject = blockEditorService.createModelObject(vm.model.value, vm.model.editor, vm.model.config.blocks); + modelObject.loadScaffolding(contentResource).then(loaded); + + vm.layout = [];// Property models layout object specific to this Block Editor. + vm.blocks = [];// Runtime model of editing models, needs to be synced to property model on form submit. + vm.availableBlockTypes = [];// Available block entries of this property editor. - console.log("value:", model.value); - - /* - vm.availableBlockTypes = [ - { - alias: "pageModule", - name: "Module", - icon: "icon-document", - prototype_paste_data: { - - elementType: { - alias: "contentTypeAlias", - icon: "icon-document", - label: "Text" - }, - labelTemplate: "{{pageTitle | truncate:true:36}}", - labelInterpolate: $interpolate("{{pageTitle | truncate:true:36}}"), - editor: "views/blockelements/labelblock/labelblock.editor.html", - overlaySize: "medium", - content: { - apps: [ - { - name: "Content", - alias: "umbContent", - weight: -100, - icon: "icon-document", - view: "views/content/apps/content/content.html", - viewModel: 0, - active: true, - badge: null, - anchors: [], - hasError: false - }, - { - name: "Info", - alias: "umbInfo", - weight: 100, - icon: "icon-info", - view: "views/content/apps/info/info.html", - viewModel: null, - active: false, - badge: null, - hasError: false - } - ], - variants: [ - { - language: { - isDefault: true - } - } - ], - tabs: [ - { - id: 1234, - label: "Content", - properties: [ - { - label: "Page Title", - description: "The title of the page", - view: "textbox", - config: {maxChars: 500}, - hideLabel: false, - validation: {mandatory: true, mandatoryMessage: "", pattern: null, patternMessage: ""}, - readonly: false, - id: 441, - dataTypeKey: "0cc0eba1-9960-42c9-bf9b-60e150b429ae", - value: "Consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.", - alias: "pageTitle", - editor: "Umbraco.TextBox", - isSensitive: false, - culture: null, - segment: null - }, - { - label: "Image", - description: "", - view: "mediapicker", - config: {multiPicker: false, - onlyImages: true, - disableFolderSelect: true, - startNodeId: "umb://media/1fd2ecaff3714c009306867fa4585e7a", - ignoreUserStartNodes: false, - idType: "udi" - }, - hideLabel: false, - validation: {mandatory: false, mandatoryMessage: "", pattern: null, patternMessage: ""}, - readonly: false, - id: 495, - dataTypeKey: "e26a8d91-a9d7-475b-bc3b-2a09f4743754", - value: "umb://media/fa763e0d0ceb408c8720365d57e06e32", - alias: "photo", - editor: "Umbraco.MediaPicker", - isSensitive: false, - culture: null, - segment: null - }, - { - label: "Image Description", - description: "The title of the page", - view: "textbox", - config: {maxChars: 500}, - hideLabel: false, - validation: {mandatory: true, mandatoryMessage: "", pattern: null, patternMessage: ""}, - readonly: false, - id: 442, - dataTypeKey: "0cc0eba1-9960-42c9-bf9b-60e150b429ae", - value: "Let´s have a chat", - alias: "imageDesc", - editor: "Umbraco.TextBox", - isSensitive: false, - culture: null, - segment: null - } - ] - }, - { - id: 1234, - label: "Styling", - properties: [ - { - label: "Background color", - description: "", - view: "textbox", - config: {maxChars: 500}, - hideLabel: false, - validation: {mandatory: true, mandatoryMessage: "", pattern: null, patternMessage: ""}, - readonly: false, - id: 441, - dataTypeKey: "0cc0eba1-9960-42c9-bf9b-60e150b429ae", - value: "The purpose of lorem ipsum is to create a natural looking block of text (sentence, paragraph, page, etc.) that doesn´t distract from the layout. A practice not without controversy, laying out pages with meaningless filler text can be very useful when the focus is meant to be on design, not content.", - alias: "pageTitle", - editor: "Umbraco.TextBox", - isSensitive: false, - culture: null, - segment: null - } - ] - } - ] - } - } - }, - { - alias: "pageModule", - name: "Inline module", - icon: "icon-document", - prototype_paste_data: { - elementType: { - alias: "contentTypeAlias", - icon: "icon-document", - label: "Text" - }, - labelTemplate: "{{imageTitle | truncate:true:36}}", - labelInterpolate: $interpolate("{{imageTitle | truncate:true:36}}"), - key: 1, - editor: "views/blockelements/inlineblock/inlineblock.editor.html", - overlaySize: "medium", - content: { - apps: [ - { - name: "Content", - alias: "umbContent", - weight: -100, - icon: "icon-document", - view: "views/content/apps/content/content.html", - viewModel: 0, - active: true, - badge: null, - anchors: [], - hasError: false - }, - { - name: "Info", - alias: "umbInfo", - weight: 100, - icon: "icon-info", - view: "views/content/apps/info/info.html", - viewModel: null, - active: false, - badge: null, - hasError: false - } - ], - variants: [ - { - language: { - isDefault: true - } - } - ], - tabs: [ - { - id: 1234, - label: "Content", - properties: [ - { - label: "Image Title", - description: "The title on top of image", - view: "textbox", - config: {maxChars: 500}, - hideLabel: false, - validation: {mandatory: true, mandatoryMessage: "", pattern: null, patternMessage: ""}, - readonly: false, - id: 441, - dataTypeKey: "0cc0eba1-9960-42c9-bf9b-60e150b429ae", - value: "The purpose of lorem ipsum is to create a natural looking block of text (sentence, paragraph, page, etc.) that doesn´t distract from the layout. A practice not without controversy, laying out pages with meaningless filler text can be very useful when the focus is meant to be on design, not content.", - alias: "imageTitle", - editor: "Umbraco.TextBox", - isSensitive: false, - culture: null, - segment: null - }, - { - label: "Image", - description: "", - view: "mediapicker", - config: {multiPicker: false, - onlyImages: true, - disableFolderSelect: true, - startNodeId: "umb://media/1fd2ecaff3714c009306867fa4585e7a", - ignoreUserStartNodes: false, - idType: "udi" - }, - hideLabel: false, - validation: {mandatory: false, mandatoryMessage: "", pattern: null, patternMessage: ""}, - readonly: false, - id: 495, - dataTypeKey: "e26a8d91-a9d7-475b-bc3b-2a09f4743754", - value: "umb://media/fa763e0d0ceb408c8720365d57e06e32", - alias: "photo", - editor: "Umbraco.MediaPicker", - isSensitive: false, - culture: null, - segment: null - } - ] - } - ] - } - } - }, - { - alias: "contentTypeAlias", - name: "Text", - icon: "icon-info", - prototype_paste_data: { - elementType: { - alias: "contentTypeAlias", - icon: "icon-document", - label: "Text" - }, - labelTemplate: "Label", - labelInterpolate: $interpolate("Label"), - editor: "views/blockelements/textareablock/textareablock.editor.html", - overlaySize: "medium", - content: { - apps: [ - { - name: "Content", - alias: "umbContent", - weight: -100, - icon: "icon-document", - view: "views/content/apps/content/content.html", - viewModel: 0, - active: true, - badge: null, - anchors: [], - hasError: false - }, - { - name: "Info", - alias: "umbInfo", - weight: 100, - icon: "icon-info", - view: "views/content/apps/info/info.html", - viewModel: null, - active: false, - badge: null, - hasError: false - } - ], - variants: [ - { - language: { - isDefault: true - } - } - ], - tabs: [ - { - id: 1234, - label: "Content", - properties: [ - { - label: "Page Title", - description: "The title of the page", - view: "textbox", - config: {maxChars: 500}, - hideLabel: false, - validation: {mandatory: true, mandatoryMessage: "", pattern: null, patternMessage: ""}, - readonly: false, - id: 441, - dataTypeKey: "0cc0eba1-9960-42c9-bf9b-60e150b429ae", - value: "", - alias: "pageTitle", - editor: "Umbraco.TextBox", - isSensitive: false, - culture: null, - segment: null - } - ] - } - ] - } - } - }, - { - alias: "contentTypeAlias", - name: "Image", - icon: "icon-picture", - prototype_paste_data: { - elementType: { - alias: "contentTypeAlias", - icon: "icon-document", - label: "Text" - }, - labelTemplate: "Label", - labelInterpolate: $interpolate("Label"), - editor: "views/blockelements/imageblock/imageblock.editor.html", - overlaySize: "medium", - content: { - apps: [ - { - name: "Content", - alias: "umbContent", - weight: -100, - icon: "icon-document", - view: "views/content/apps/content/content.html", - viewModel: 0, - active: true, - badge: null, - anchors: [], - hasError: false - }, - { - name: "Info", - alias: "umbInfo", - weight: 100, - icon: "icon-info", - view: "views/content/apps/info/info.html", - viewModel: null, - active: false, - badge: null, - hasError: false - } - ], - variants: [ - { - language: { - isDefault: true - } - } - ], - tabs: [ - { - id: 1234, - label: "Content", - properties: [ - { - label: "Image", - description: "", - view: "mediapicker", - config: {multiPicker: false, - onlyImages: true, - disableFolderSelect: true, - startNodeId: "umb://media/1fd2ecaff3714c009306867fa4585e7a", - ignoreUserStartNodes: false, - idType: "udi" - }, - hideLabel: false, - validation: {mandatory: false, mandatoryMessage: "", pattern: null, patternMessage: ""}, - readonly: false, - id: 495, - dataTypeKey: "e26a8d91-a9d7-475b-bc3b-2a09f4743754", - value: "umb://media/fa763e0d0ceb408c8720365d57e06e32", - alias: "photo", - editor: "Umbraco.MediaPicker", - isSensitive: false, - culture: null, - segment: null - }, - { - label: "Image Description", - description: "The title of the page", - view: "textbox", - config: {maxChars: 500}, - hideLabel: false, - validation: {mandatory: true, mandatoryMessage: "", pattern: null, patternMessage: ""}, - readonly: false, - id: 441, - dataTypeKey: "0cc0eba1-9960-42c9-bf9b-60e150b429ae", - value: "Let´s have a chat", - alias: "imageDesc", - editor: "Umbraco.TextBox", - isSensitive: false, - culture: null, - segment: null - } - ] - } - ], - temp_image: "/umbraco/assets/img/login.jpg" - } - } + var copyAllEntriesAction = { + labelKey: "clipboard_labelForCopyAllEntries", + labelTokens: [vm.model.label], + icon: "documents", + method: function () {}, + isDisabled: true } - ]; - - // var defaultBlockType... - - // TODO: get icon, properties etc. from available types? - vm.blocks = [ - { - elementType: { - alias: "contentTypeAlias", - icon: "icon-document", - label: "Text" - }, - labelTemplate: "{{pageTitle | truncate:true:36}}", - labelInterpolate: $interpolate("{{pageTitle | truncate:true:36}}"), - key: 1, - editor: "views/blockelements/labelblock/labelblock.editor.html", - overlaySize: "medium", - content: { - apps: [ - { - name: "Content", - alias: "umbContent", - weight: -100, - icon: "icon-document", - view: "views/common/infiniteeditors/elementeditor/elementeditor.content.html", - viewModel: 0, - active: true, - badge: null, - anchors: [], - hasError: false - }, - { - name: "Info", - alias: "umbInfo", - weight: 100, - icon: "icon-info", - view: "views/content/apps/info/info.html", - viewModel: null, - active: false, - badge: null, - hasError: false - } - ], - variants: [ - { - language: { - isDefault: true - } - } - ], - tabs: [ - { - id: 1234, - label: "Content", - properties: [ - { - label: "Page Title", - description: "The title of the page", - view: "textbox", - config: {maxChars: 500}, - hideLabel: false, - validation: {mandatory: true, mandatoryMessage: "", pattern: null, patternMessage: ""}, - readonly: false, - id: 441, - dataTypeKey: "0cc0eba1-9960-42c9-bf9b-60e150b429ae", - value: "The purpose of lorem ipsum is to create a natural looking block of text (sentence, paragraph, page, etc.) that doesn´t distract from the layout. A practice not without controversy, laying out pages with meaningless filler text can be very useful when the focus is meant to be on design, not content.", - alias: "pageTitle", - editor: "Umbraco.TextBox", - isSensitive: false, - culture: null, - segment: null - }, - { - label: "Image", - description: "", - view: "mediapicker", - config: {multiPicker: false, - onlyImages: true, - disableFolderSelect: true, - startNodeId: "umb://media/1fd2ecaff3714c009306867fa4585e7a", - ignoreUserStartNodes: false, - idType: "udi" - }, - hideLabel: false, - validation: {mandatory: false, mandatoryMessage: "", pattern: null, patternMessage: ""}, - readonly: false, - id: 495, - dataTypeKey: "e26a8d91-a9d7-475b-bc3b-2a09f4743754", - value: "umb://media/fa763e0d0ceb408c8720365d57e06e32", - alias: "photo", - editor: "Umbraco.MediaPicker", - isSensitive: false, - culture: null, - segment: null - }, - { - label: "Image Description", - description: "The title of the page", - view: "textbox", - config: {maxChars: 500}, - hideLabel: false, - validation: {mandatory: true, mandatoryMessage: "", pattern: null, patternMessage: ""}, - readonly: false, - id: 442, - dataTypeKey: "0cc0eba1-9960-42c9-bf9b-60e150b429ae", - value: "Let´s have a chat", - alias: "imageDesc", - editor: "Umbraco.TextBox", - isSensitive: false, - culture: null, - segment: null - } - ] - }, - { - id: 1234, - label: "Styling", - properties: [ - { - label: "Background color", - description: "", - view: "textbox", - config: {maxChars: 500}, - hideLabel: false, - validation: {mandatory: true, mandatoryMessage: "", pattern: null, patternMessage: ""}, - readonly: false, - id: 441, - dataTypeKey: "0cc0eba1-9960-42c9-bf9b-60e150b429ae", - value: "The purpose of lorem ipsum is to create a natural looking block of text (sentence, paragraph, page, etc.) that doesn´t distract from the layout. A practice not without controversy, laying out pages with meaningless filler text can be very useful when the focus is meant to be on design, not content.", - alias: "pageTitle", - editor: "Umbraco.TextBox", - isSensitive: false, - culture: null, - segment: null - } - ] - } - ] - } - }, - { - elementType: { - alias: "contentTypeAlias", - icon: "icon-document", - label: "Text" - }, - labelTemplate: "{{pageTitle | truncate:true:36}}", - labelInterpolate: $interpolate("{{pageTitle | truncate:true:36}}"), - key: 2, - editor: "views/blockelements/labelblock/labelblock.editor.html", - overlaySize: "medium", - content: { - apps: [ - { - name: "Content", - alias: "umbContent", - weight: -100, - icon: "icon-document", - view: "views/content/apps/content/content.html", - viewModel: 0, - active: true, - badge: null, - anchors: [], - hasError: false - }, - { - name: "Info", - alias: "umbInfo", - weight: 100, - icon: "icon-info", - view: "views/content/apps/info/info.html", - viewModel: null, - active: false, - badge: null, - hasError: false - } - ], - variants: [ - { - language: { - isDefault: true - } - } - ], - tabs: [ - { - id: 1234, - label: "Content", - properties: [ - { - label: "Page Title", - description: "The title of the page", - view: "textbox", - config: {maxChars: 500}, - hideLabel: false, - validation: {mandatory: true, mandatoryMessage: "", pattern: null, patternMessage: ""}, - readonly: false, - id: 441, - dataTypeKey: "0cc0eba1-9960-42c9-bf9b-60e150b429ae", - value: "Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.", - alias: "pageTitle", - editor: "Umbraco.TextBox", - isSensitive: false, - culture: null, - segment: null - }, - { - label: "Image", - description: "", - view: "mediapicker", - config: {multiPicker: false, - onlyImages: true, - disableFolderSelect: true, - startNodeId: "umb://media/1fd2ecaff3714c009306867fa4585e7a", - ignoreUserStartNodes: false, - idType: "udi" - }, - hideLabel: false, - validation: {mandatory: false, mandatoryMessage: "", pattern: null, patternMessage: ""}, - readonly: false, - id: 495, - dataTypeKey: "e26a8d91-a9d7-475b-bc3b-2a09f4743754", - value: "umb://media/fa763e0d0ceb408c8720365d57e06e32", - alias: "photo", - editor: "Umbraco.MediaPicker", - isSensitive: false, - culture: null, - segment: null - }, - { - label: "Image Description", - description: "The title of the page", - view: "textbox", - config: {maxChars: 500}, - hideLabel: false, - validation: {mandatory: true, mandatoryMessage: "", pattern: null, patternMessage: ""}, - readonly: false, - id: 442, - dataTypeKey: "0cc0eba1-9960-42c9-bf9b-60e150b429ae", - value: "Let´s have a chat", - alias: "imageDesc", - editor: "Umbraco.TextBox", - isSensitive: false, - culture: null, - segment: null - } - ] - }, - { - id: 1234, - label: "Styling", - properties: [ - { - label: "Background color", - description: "", - view: "textbox", - config: {maxChars: 500}, - hideLabel: false, - validation: {mandatory: true, mandatoryMessage: "", pattern: null, patternMessage: ""}, - readonly: false, - id: 441, - dataTypeKey: "0cc0eba1-9960-42c9-bf9b-60e150b429ae", - value: "The purpose of lorem ipsum is to create a natural looking block of text (sentence, paragraph, page, etc.) that doesn´t distract from the layout. A practice not without controversy, laying out pages with meaningless filler text can be very useful when the focus is meant to be on design, not content.", - alias: "pageTitle", - editor: "Umbraco.TextBox", - isSensitive: false, - culture: null, - segment: null - } - ] - } - ] - } - }, - { - - elementType: { - alias: "contentTypeAlias", - icon: "icon-document", - label: "Text" - }, - labelTemplate: "{{pageTitle | truncate:true:36}}", - labelInterpolate: $interpolate("{{pageTitle | truncate:true:36}}"), - key: 3, - editor: "views/blockelements/labelblock/labelblock.editor.html", - overlaySize: "medium", - content: { - apps: [ - { - name: "Content", - alias: "umbContent", - weight: -100, - icon: "icon-document", - view: "views/content/apps/content/content.html", - viewModel: 0, - active: true, - badge: null, - anchors: [], - hasError: false - }, - { - name: "Info", - alias: "umbInfo", - weight: 100, - icon: "icon-info", - view: "views/content/apps/info/info.html", - viewModel: null, - active: false, - badge: null, - hasError: false - } - ], - variants: [ - { - language: { - isDefault: true - } - } - ], - tabs: [ - { - id: 1234, - label: "Content", - properties: [ - { - label: "Page Title", - description: "The title of the page", - view: "textbox", - config: {maxChars: 500}, - hideLabel: false, - validation: {mandatory: true, mandatoryMessage: "", pattern: null, patternMessage: ""}, - readonly: false, - id: 441, - dataTypeKey: "0cc0eba1-9960-42c9-bf9b-60e150b429ae", - value: "Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.", - alias: "pageTitle", - editor: "Umbraco.TextBox", - isSensitive: false, - culture: null, - segment: null - }, - { - label: "Image", - description: "", - view: "mediapicker", - config: {multiPicker: false, - onlyImages: true, - disableFolderSelect: true, - startNodeId: "umb://media/1fd2ecaff3714c009306867fa4585e7a", - ignoreUserStartNodes: false, - idType: "udi" - }, - hideLabel: false, - validation: {mandatory: false, mandatoryMessage: "", pattern: null, patternMessage: ""}, - readonly: false, - id: 495, - dataTypeKey: "e26a8d91-a9d7-475b-bc3b-2a09f4743754", - value: "umb://media/fa763e0d0ceb408c8720365d57e06e32", - alias: "photo", - editor: "Umbraco.MediaPicker", - isSensitive: false, - culture: null, - segment: null - }, - { - label: "Image Description", - description: "The title of the page", - view: "textbox", - config: {maxChars: 500}, - hideLabel: false, - validation: {mandatory: true, mandatoryMessage: "", pattern: null, patternMessage: ""}, - readonly: false, - id: 442, - dataTypeKey: "0cc0eba1-9960-42c9-bf9b-60e150b429ae", - value: "Let´s have a chat", - alias: "imageDesc", - editor: "Umbraco.TextBox", - isSensitive: false, - culture: null, - segment: null - } - ] - }, - { - id: 1234, - label: "Styling", - properties: [ - { - label: "Background color", - description: "", - view: "textbox", - config: {maxChars: 500}, - hideLabel: false, - validation: {mandatory: true, mandatoryMessage: "", pattern: null, patternMessage: ""}, - readonly: false, - id: 441, - dataTypeKey: "0cc0eba1-9960-42c9-bf9b-60e150b429ae", - value: "The purpose of lorem ipsum is to create a natural looking block of text (sentence, paragraph, page, etc.) that doesn´t distract from the layout. A practice not without controversy, laying out pages with meaningless filler text can be very useful when the focus is meant to be on design, not content.", - alias: "pageTitle", - editor: "Umbraco.TextBox", - isSensitive: false, - culture: null, - segment: null - } - ] - } - ] - } + + var propertyActions = [ + copyAllEntriesAction + ]; + + if (this.umbProperty) { + this.umbProperty.setPropertyActions(propertyActions); } - ]; + }; + + - */ - - - model.value = model.value || {}; - - var modelObject = blockEditorService.createModelObject(model.value, model.editor, model.config.blocks); - - modelObject.loadScaffolding(contentResource).then(loaded); - - vm.layout = []; - vm.blocks = []; - vm.availableBlockTypes = []; - + function loaded() { - console.log("Loading done!!!"); - console.log(modelObject); - - vm.layout = modelObject.getLayout(); + mapToBlocks(); + + vm.availableBlockTypes = modelObject.getAvailableBlocksForItemPicker(); + + } + + + function getEditingModel(entry) { + var block = modelObject.getEditingModel(entry); + + if (block === null) return null; + + block.view = block.config.view || vm.model.config.useInlineEditingAsDefault ? "views/blockelements/inlineblock/inlineblock.editor.html" : "views/blockelements/labelblock/labelblock.editor.html"; + + return block; + } + + /** + * Maps property model to the runtime editing model (blocks). + */ + function mapToBlocks() { + // clear blocks. + vm.blocks = []; + + // make all blocks. vm.layout.forEach(entry => { - var block = modelObject.getEditingModel(entry); + var block = getEditingModel(entry); if(block !== null) { vm.blocks.push(block); } }); - vm.availableBlockTypes = modelObject.getAvailableBlocksForItemPicker(); - console.log(vm.availableBlockTypes); - + $scope.$apply(); + } + + /** + * Maps content from runtime editing model (blocks) to the property model. + * Does not take care of ordering, we need the sort-UI to sync that, on the fly. + */ + function mapToContent() { + + // sync data from blocks to content models. + vm.blocks.forEach(block => { + modelObject.setDataFromEditingModel(block); + }); + } + + function sync() { + mapToContent(); + } + + function syncBlockData(block) { + modelObject.setDataFromEditingModel(block); } - function setDirty() { if (vm.propertyForm) { vm.propertyForm.$setDirty(); } - }; + } function addNewBlock(index, contentTypeAlias) { - // Create layout entry. + // Create layout entry. (not added to property model jet.) var layoutEntry = modelObject.createLayoutEntry(contentTypeAlias); - // add layout entry a decired location in layout. - vm.layout.splice(index, 0, layoutEntry); // make editing object - var blockEditingObject = modelObject.getEditingModel(layoutEntry); - // apply editing model at decired location in editing model. - vm.blocks.splice(index, 0, blockEditingObject); + var blockEditingObject = getEditingModel(layoutEntry); + + if (blockEditingObject !== null) { - $scope.moveFocusToBlock = blockEditingObject; + // add layout entry at the decired location in layout. + vm.layout.splice(index, 0, layoutEntry); + + // apply editing model at decired location in editing model. + vm.blocks.splice(index, 0, blockEditingObject); + + vm.moveFocusToBlock = blockEditingObject; + + } } - vm.deleteBlock = function(block) { + function deleteBlock(block) { var index = vm.blocks.indexOf(block); if(index !== -1) { vm.blocks.splice(index, 1); @@ -929,18 +160,18 @@ } } - vm.editBlock = function(blockModel) { + function editBlock(blockModel) { - // TODO: test wether i need to clone or if that is done by overlay. - //var blockModelClone = angular.copy(blockModel); + // TODO: make a clone to ensure edits arent made directly. + var blockContentModelClone = angular.copy(blockModel.content); - var elementEditor = { - block: blockModel, + var elementEditorModel = { + content: blockContentModelClone, + title: blockModel.label, view: "views/common/infiniteeditors/elementeditor/elementeditor.html", - size: blockModel.overlaySize, - submit: function(model) { - blockModel.content = model.block.content; - blockModel.settings = model.block.settings; + size: blockModel.config.overlaySize || "medium", + submit: function(elementEditorModel) { + blockModel.content = elementEditorModel.content; editorService.close(); }, close: function() { @@ -949,10 +180,11 @@ }; // open property settings editor - editorService.open(elementEditor); + editorService.open(elementEditorModel); } - vm.showCreateDialog = function (createIndex, $event) { + vm.showCreateDialog = showCreateDialog; + function showCreateDialog(createIndex, $event) { if (vm.blockTypePicker) { return; @@ -984,10 +216,10 @@ }; - vm.requestCopyBlock = function(block) { - console.log("copy") + function requestCopyBlock(block) { + console.log("copy still needs to be done.") } - vm.requestDeleteBlock = function(block) { + function requestDeleteBlock (block) { localizationService.localizeMany(["general_delete", "blockEditor_confirmDeleteBlockMessage", "contentTypeEditor_yesDelete"]).then(function (data) { const overlay = { title: data[0], @@ -997,7 +229,7 @@ overlayService.close(); }, submit: function () { - vm.deleteBlock(block); + deleteBlock(block); overlayService.close(); } }; @@ -1008,7 +240,7 @@ vm.showCopy = clipboardService.isSupported(); - + var runtimeSortVars = {}; vm.sorting = false; vm.sortableOptions = { @@ -1021,6 +253,7 @@ tolerance: "pointer", scroll: true, start: function (ev, ui) { + runtimeSortVars.moveFromIndex = ui.item.index(); $scope.$apply(function () { vm.sorting = true; }); @@ -1029,45 +262,42 @@ setDirty(); }, stop: function (ev, ui) { + + // Lets update the layout part of the property model to match the update. + var moveFromIndex = runtimeSortVars.moveFromIndex; + var moveToIndex = ui.item.index(); + + if (moveToIndex > -1 && moveFromIndex !== moveToIndex) { + var movedEntry = vm.layout[moveFromIndex]; + vm.layout.splice(moveFromIndex, 1); + vm.layout.splice(moveToIndex, 0, movedEntry); + } + $scope.$apply(function () { vm.sorting = false; }); } }; - $scope.blockApi = { - deleteBlock: vm.deleteBlock + vm.blockEditorApi = { + editBlock: editBlock, + requestCopyBlock: requestCopyBlock, + requestDeleteBlock: requestDeleteBlock, + deleteBlock: deleteBlock, + syncBlockData: syncBlockData } - var copyAllEntriesAction = { - labelKey: "clipboard_labelForCopyAllEntries", - labelTokens: [model.label], - icon: "documents", - method: function () {}, - isDisabled: true - } - - var propertyActions = [ - copyAllEntriesAction - ]; - - this.$onInit = function () { - if (this.umbProperty) { - this.umbProperty.setPropertyActions(propertyActions); - } - }; - - function validateLimits() { - if (vm.validationLimit.min && vm.blocks.length < vm.validationLimit.min) { - vm.propertyForm.minCount.$setValidity("minCount", false); + if (vm.validationLimit.min !== null) { + if (vm.blocks.length < vm.validationLimit.min) { + vm.propertyForm.minCount.$setValidity("minCount", false); + } + else { + vm.propertyForm.minCount.$setValidity("minCount", true); + } } - else { - vm.propertyForm.minCount.$setValidity("minCount", true); - } - - if (vm.validationLimit.max && vm.blocks.length > vm.validationLimit.max) { + if (vm.validationLimit.max !== null && vm.blocks.length > vm.validationLimit.max) { vm.propertyForm.maxCount.$setValidity("maxCount", false); } else { @@ -1079,15 +309,32 @@ // TODO: We need to investigate if we can do a specific watch on each block, so we dont re-render all blocks. - unsubscribe.push($scope.$watch("vm.blocks", onBlocksUpdated, true)); + /* + unsubscribe.push($scope.$watch("vm.blocks[0]", onBlocksUpdated, true)); function onBlocksUpdated(newVal, oldVal) { + + console.log("blocks update", oldVal, " > ", newVal); + //setDirty(); + var labelIndex = 1; for(const block of vm.blocks) { block.label = blockEditorService.getBlockLabel(block, labelIndex++); } } + */ + + unsubscribe.push($scope.$watch(() => vm.blocks.length, validateLimits)); + unsubscribe.push($scope.$on("formSubmitting", function (ev, args) { + + console.log("formSubmitting is happening, we need to make sure sub property editors are synced first.") + + console.log(vm.layout, vm.model.value); + + //sync(); + })); + $scope.$on("$destroy", function () { for (const subscription of unsubscribe) { subscription(); diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.html index 198dab4f5f..eea9f01ff3 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.html @@ -1 +1 @@ - + diff --git a/src/Umbraco.Web/PropertyEditors/BlockListConfiguration.cs b/src/Umbraco.Web/PropertyEditors/BlockListConfiguration.cs index f069804348..20b6c00d68 100644 --- a/src/Umbraco.Web/PropertyEditors/BlockListConfiguration.cs +++ b/src/Umbraco.Web/PropertyEditors/BlockListConfiguration.cs @@ -42,8 +42,8 @@ namespace Umbraco.Web.PropertyEditors public string Label { get; set; } } - [ConfigurationField("useAccordionsAsDefault", "Inline editing mode", "boolean", Description = "Use the inline editor as the default block view")] - public bool useInlineEditingAsDefault { get; set; } + [ConfigurationField("useInlineEditingAsDefault", "Inline editing mode", "boolean", Description = "Use the inline editor as the default block view")] + public bool UseInlineEditingAsDefault { get; set; } } } From fd6d5dcedfce244af784ef74b37368d311bd173e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Tue, 3 Mar 2020 14:30:21 +0100 Subject: [PATCH 058/377] sync models --- .../common/services/blockeditor.service.js | 4 +--- .../blocklist/blocklist.block.component.js | 23 +++++++++++++++---- .../blocklist/blocklist.component.js | 12 +++++++--- 3 files changed, 29 insertions(+), 10 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 bcea8efc5e..3ac9742171 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 @@ -11,11 +11,10 @@ for (var t = 0; t < variant.tabs.length; t++) { var tab = variant.tabs[t]; - + for (var p = 0; p < tab.properties.length; p++) { var prop = tab.properties[p]; if (contentModel[prop.alias]) { - console.log("mapping:", prop.alias, contentModel[prop.alias]) prop.value = contentModel[prop.alias]; } } @@ -89,7 +88,6 @@ scaffoldAliases.forEach((elementTypeAlias => { tasks.push(contentResource.getScaffold(-20, elementTypeAlias).then(scaffold => { - console.log(scaffold); this.scaffolds.push(scaffold); })); })); diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.block.component.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.block.component.js index a1b38e509f..8fe08888a9 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.block.component.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.block.component.js @@ -18,8 +18,6 @@ var unsubscribe = []; var vm = this; - console.log("BlockListBlockController", vm); - vm.$onInit = function() { // Start watching each property value. @@ -30,7 +28,9 @@ for (var p = 0; p < tab.properties.length; p++) { var prop = tab.properties[p]; - unsubscribe.push($scope.$watch("vm.block.content.variants[0].tabs["+t+"].properties["+p+"].value", createPropWatcher(prop))); + + // Sadly we need to deep watch, cause its our only way to make sure that complex values gets synced. Alternative solution would be to sync on a broadcasted event, fired on Save and Copy eventually more. + unsubscribe.push($scope.$watch("vm.block.content.variants[0].tabs["+t+"].properties["+p+"].value", createPropWatcher(prop), true)); } } } @@ -40,9 +40,10 @@ return function() { // sync data: - console.log(prop.alias, prop.value); vm.block.contentModel[prop.alias] = prop.value; + vm.blockEditorApi.sync(); + // update label: updateLabel(); } @@ -52,6 +53,20 @@ function updateLabel() { vm.block.label = blockEditorService.getBlockLabel(vm.block); } + + /** + * Listening for properties + */ + /* + function onBlockEditorValueUpdated($event) { + // Lets sync the value of the property that the event comes from, if we know that.. + + //$event.stopPropagation(); + //$event.preventDefault(); + }; + + unsubscribe.push($scope.$on("blockEditorValueUpdated", onBlockEditorValueUpdated)); + */ $scope.$on("$destroy", function () { for (const subscription of unsubscribe) { diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.js index 5546fdeb8b..204988927d 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.js @@ -100,6 +100,7 @@ * Maps content from runtime editing model (blocks) to the property model. * Does not take care of ordering, we need the sort-UI to sync that, on the fly. */ + /* function mapToContent() { // sync data from blocks to content models. @@ -107,10 +108,15 @@ modelObject.setDataFromEditingModel(block); }); } + */ + /* function sync() { - mapToContent(); + // to avoid deep watches of block editors we use an event for those instead? + // Lets inform container of this property editor that we updated. + $scope.$emit("blockEditorValueUpdated"); } + */ function syncBlockData(block) { modelObject.setDataFromEditingModel(block); @@ -325,7 +331,7 @@ unsubscribe.push($scope.$watch(() => vm.blocks.length, validateLimits)); - +/* unsubscribe.push($scope.$on("formSubmitting", function (ev, args) { console.log("formSubmitting is happening, we need to make sure sub property editors are synced first.") @@ -334,7 +340,7 @@ //sync(); })); - +*/ $scope.$on("$destroy", function () { for (const subscription of unsubscribe) { subscription(); From 5a65719b0d41c01a63bf8a3b319df321f5459d94 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Wed, 4 Mar 2020 13:30:26 +0100 Subject: [PATCH 059/377] block list editor copy paste feature --- .../common/services/blockeditor.service.js | 78 +++-- .../imageblock/imageblock.editor.html | 4 +- .../inlineblock/inlineblock.editor.html | 8 +- .../labelblock/labelblock.editor.html | 6 +- .../textareablock.editor.controller.js | 2 +- .../textareablock/textareablock.editor.html | 2 +- .../blocklist/blocklist.block.component.html | 19 +- .../blocklist/blocklist.block.component.js | 13 +- .../blocklist/blocklist.component.html | 19 +- .../blocklist/blocklist.component.js | 266 ++++++++++++++---- 10 files changed, 304 insertions(+), 113 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 3ac9742171..00308ae4ff 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 @@ -5,9 +5,13 @@ function blockEditorService($interpolate, udiService) { - function mapToEditingModel(editingModel, contentModel) { + /** + * Simple mapping from property model content entry to editing model, + * needs to stay simple to avoid deep watching. + */ + function mapToElementTypeModel(elementTypeModel, contentModel) { - var variant = editingModel.variants[0]; + var variant = elementTypeModel.variants[0]; for (var t = 0; t < variant.tabs.length; t++) { var tab = variant.tabs[t]; @@ -21,9 +25,13 @@ } } - function mapToPropertyModel(editingModel, contentModel) { + /** + * Simple mapping from elementTypeModel to property model content entry, + * needs to stay simple to avoid deep watching. + */ + function mapToPropertyModel(elementTypeModel, contentModel) { - var variant = editingModel.variants[0]; + var variant = elementTypeModel.variants[0]; for (var t = 0; t < variant.tabs.length; t++) { var tab = variant.tabs[t]; @@ -31,7 +39,7 @@ for (var p = 0; p < tab.properties.length; p++) { var prop = tab.properties[p]; if (prop.value) { - contentModel[prop.propertyAlias] = prop.value; + contentModel[prop.alias] = prop.value; } } } @@ -95,12 +103,19 @@ return Promise.all(tasks); }, + /** + * Retrive a list of aliases that are available for content of blocks in this property editor, does not contain aliases of block settings. + * @return {Array} array of strings representing alias. + */ + getAvailableAliasesForBlockContent: function() { + return this.blockConfigurations.map(blockConfiguration => blockConfiguration.contentTypeAlias); + }, + getAvailableBlocksForItemPicker: function() { var blocks = []; this.blockConfigurations.forEach(blockConfiguration => { - var scaffold = this.getScaffoldFor(blockConfiguration.contentTypeAlias); if(scaffold) { blocks.push({ @@ -148,7 +163,7 @@ editingModel.content = angular.copy(scaffold); editingModel.content.udi = udi; - mapToEditingModel(editingModel.content, contentModel); + mapToElementTypeModel(editingModel.content, contentModel); editingModel.contentModel = contentModel; editingModel.layoutModel = layoutEntry; @@ -168,17 +183,15 @@ var udi = editingModel.content.key; - var contentModel = this.getContentByUdi(udi); - - mapToPropertyModel(editingModel.content, contentModel); + mapToPropertyModel(editingModel.content, editingModel.contentModel); // TODO: sync settings to layout entry. }, /** - * Retrieve layout data - * @return layout object. + * Retrieve the layout object for this specific property editor. + * @return {Object} Layout object. */ getLayout: function() { if (!this.value.layout[this.propertyEditorAlias]) { @@ -188,13 +201,16 @@ }, /** - * Create layout entry - * @param {object} blockConfiguration - * @return layout entry, to be added in the layout. + * Create a empty layout entry + * @param {Object} blockConfiguration + * @return {Object} Layout entry object, to be inserted at a decired location in the layout object. */ - createLayoutEntry: function(contentTypeAlias) { + create: function(contentTypeAlias) { var blockConfiguration = this.getBlockConfiguration(contentTypeAlias); + if(blockConfiguration === null) { + return null; + } var entry = { udi: this.createContent(contentTypeAlias) @@ -222,16 +238,42 @@ // private removeContent: function(entry) { const index = this.value.data.indexOf(entry) - if (index > -1) { + if (index !== -1) { this.value.splice(index, 1); } }, removeContentByUdi: function(udi) { const index = this.value.data.findIndex(o => o.udi === udi); - if (index > -1) { + if (index !== -1) { this.value.splice(index, 1); } + }, + + /** + * Insert data from ElementType Model + * @return {Object} Layout entry object, to be inserted at a decired location in the layout object. + */ + createFromElementType: function(elementTypeContentModel) { + + elementTypeContentModel = angular.copy(elementTypeContentModel); + + var contentTypeAlias = elementTypeContentModel.contentTypeAlias; + + var layoutEntry = this.create(contentTypeAlias); + if(layoutEntry === null) { + return null; + } + + var contentModel = this.getContentByUdi(layoutEntry.udi); + + mapToPropertyModel(elementTypeContentModel, contentModel); + + console.log(elementTypeContentModel) + console.log(contentModel) + + return layoutEntry; + } } diff --git a/src/Umbraco.Web.UI.Client/src/views/blockelements/imageblock/imageblock.editor.html b/src/Umbraco.Web.UI.Client/src/views/blockelements/imageblock/imageblock.editor.html index e5e68dde9c..62cd4384b8 100644 --- a/src/Umbraco.Web.UI.Client/src/views/blockelements/imageblock/imageblock.editor.html +++ b/src/Umbraco.Web.UI.Client/src/views/blockelements/imageblock/imageblock.editor.html @@ -1,3 +1,3 @@ - diff --git a/src/Umbraco.Web.UI.Client/src/views/blockelements/inlineblock/inlineblock.editor.html b/src/Umbraco.Web.UI.Client/src/views/blockelements/inlineblock/inlineblock.editor.html index 31754466ab..56fbe13e2b 100644 --- a/src/Umbraco.Web.UI.Client/src/views/blockelements/inlineblock/inlineblock.editor.html +++ b/src/Umbraco.Web.UI.Client/src/views/blockelements/inlineblock/inlineblock.editor.html @@ -1,10 +1,10 @@
    -
    - +
    diff --git a/src/Umbraco.Web.UI.Client/src/views/blockelements/labelblock/labelblock.editor.html b/src/Umbraco.Web.UI.Client/src/views/blockelements/labelblock/labelblock.editor.html index 78595fce29..2036e3ef3a 100644 --- a/src/Umbraco.Web.UI.Client/src/views/blockelements/labelblock/labelblock.editor.html +++ b/src/Umbraco.Web.UI.Client/src/views/blockelements/labelblock/labelblock.editor.html @@ -1,4 +1,4 @@ - diff --git a/src/Umbraco.Web.UI.Client/src/views/blockelements/textareablock/textareablock.editor.controller.js b/src/Umbraco.Web.UI.Client/src/views/blockelements/textareablock/textareablock.editor.controller.js index 868573fb72..9fc213cd4a 100644 --- a/src/Umbraco.Web.UI.Client/src/views/blockelements/textareablock/textareablock.editor.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/blockelements/textareablock/textareablock.editor.controller.js @@ -5,7 +5,7 @@ angular.module("umbraco") var vm = this; - vm.firstProperty = $scope.block.content.variants[0].tabs[0].properties[0]; + vm.firstProperty = $scope.blockItem.content.variants[0].tabs[0].properties[0]; /* vm.onBlur = function() { if (vm.firstProperty.value === null || vm.firstProperty.value === "") { diff --git a/src/Umbraco.Web.UI.Client/src/views/blockelements/textareablock/textareablock.editor.html b/src/Umbraco.Web.UI.Client/src/views/blockelements/textareablock/textareablock.editor.html index c64d42b6ac..80aba51a84 100644 --- a/src/Umbraco.Web.UI.Client/src/views/blockelements/textareablock/textareablock.editor.html +++ b/src/Umbraco.Web.UI.Client/src/views/blockelements/textareablock/textareablock.editor.html @@ -8,7 +8,7 @@ ng-model="vm.firstProperty.value" ng-keypress="vm.submitOnEnter($event)" ng-blur="vm.onBlur()" - focus-when="{{moveFocusToBlock === block}}" + focus-when="{{blockvm.focusThis}}" > diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.block.component.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.block.component.html index 4231e5c83e..36905978d5 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.block.component.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.block.component.html @@ -1,20 +1,5 @@ -
    -
    -
    +
    -
    - - -
    +
    diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.block.component.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.block.component.js index 8fe08888a9..330d8f7b69 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.block.component.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.block.component.js @@ -4,12 +4,15 @@ .module("umbraco") .component("blockListPropertyEditorBlock", { templateUrl: "views/propertyeditors/blocklist/blocklist.block.component.html", + transclude: true, controller: BlockListBlockController, controllerAs: "vm", bindings: { block: "=", - blockEditorApi: "=", - focusThisBlock: " - + +
    +
    + +
    + + +
    diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.js index 204988927d..157d31b9cf 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.js @@ -12,59 +12,95 @@ propertyForm: "=" }, require: { - umbProperty: "?^umbProperty" + umbProperty: "?^umbProperty", + umbVariantContent: '?^^umbVariantContent' } }); - function BlockListController($scope, $interpolate, editorService, clipboardService, localizationService, overlayService, blockEditorService, contentResource) { + function BlockListController($scope, $interpolate, editorService, clipboardService, localizationService, overlayService, blockEditorService, contentResource, eventsService) { var modelObject; var unsubscribe = []; var vm = this; vm.moveFocusToBlock = null; + vm.showCopy = clipboardService.isSupported(); + vm.showPaste = false; + + vm.layout = [];// Property models layout object specific to this Block Editor. + vm.blocks = [];// Runtime model of editing models, needs to be synced to property model on form submit. + vm.availableBlockTypes = [];// Available block entries of this property editor. + + var labels = {}; + vm.labels = labels; + localizationService.localizeMany(["grid_addElement", "content_createEmpty"]).then(function (data) { + labels.grid_addElement = data[0]; + labels.content_createEmpty = data[1]; + }); + + function checkAbilityToPasteContent() { + vm.showPaste = clipboardService.hasEntriesOfType("elementType", vm.availableContentTypes) || clipboardService.hasEntriesOfType("elementTypeArray", vm.availableContentTypes); + } + eventsService.on("clipboardService.storageUpdate", checkAbilityToPasteContent); + + + var copyAllBlocksAction; + var deleteAllBlocksAction; + vm.$onInit = function() { vm.validationLimit = vm.model.config.validationLimit; - + vm.model.value = vm.model.value || {}; modelObject = blockEditorService.createModelObject(vm.model.value, vm.model.editor, vm.model.config.blocks); modelObject.loadScaffolding(contentResource).then(loaded); - - vm.layout = [];// Property models layout object specific to this Block Editor. - vm.blocks = [];// Runtime model of editing models, needs to be synced to property model on form submit. - vm.availableBlockTypes = [];// Available block entries of this property editor. - var copyAllEntriesAction = { + copyAllBlocksAction = { labelKey: "clipboard_labelForCopyAllEntries", labelTokens: [vm.model.label], icon: "documents", - method: function () {}, + method: requestCopyAllBlocks, isDisabled: true } - + deleteAllBlocksAction = { + labelKey: 'clipboard_labelForRemoveAllEntries', + labelTokens: [], + icon: 'trash', + method: requestDeleteAllBlocks, + isDisabled: true + } + var propertyActions = [ - copyAllEntriesAction + copyAllBlocksAction, + deleteAllBlocksAction ]; - if (this.umbProperty) { - this.umbProperty.setPropertyActions(propertyActions); + if (vm.umbProperty) { + vm.umbProperty.setPropertyActions(propertyActions); } }; - + + function setDirty() { + if (vm.propertyForm) { + vm.propertyForm.$setDirty(); + } + } function loaded() { vm.layout = modelObject.getLayout(); mapToBlocks(); + vm.availableContentTypes = modelObject.getAvailableAliasesForBlockContent(); vm.availableBlockTypes = modelObject.getAvailableBlocksForItemPicker(); + checkAbilityToPasteContent(); + } @@ -117,36 +153,35 @@ $scope.$emit("blockEditorValueUpdated"); } */ - + /* function syncBlockData(block) { modelObject.setDataFromEditingModel(block); } - - function setDirty() { - if (vm.propertyForm) { - vm.propertyForm.$setDirty(); - } - } + */ function addNewBlock(index, contentTypeAlias) { // Create layout entry. (not added to property model jet.) - var layoutEntry = modelObject.createLayoutEntry(contentTypeAlias); + var layoutEntry = modelObject.create(contentTypeAlias); + if (layoutEntry === null) { + return false; + } // make editing object var blockEditingObject = getEditingModel(layoutEntry); - - if (blockEditingObject !== null) { - - // add layout entry at the decired location in layout. - vm.layout.splice(index, 0, layoutEntry); - - // apply editing model at decired location in editing model. - vm.blocks.splice(index, 0, blockEditingObject); - - vm.moveFocusToBlock = blockEditingObject; - + if (blockEditingObject === null) { + return false; } + + // add layout entry at the decired location in layout. + vm.layout.splice(index, 0, layoutEntry); + + // apply editing model at decired location in editing model. + vm.blocks.splice(index, 0, blockEditingObject); + + vm.moveFocusToBlock = blockEditingObject; + + return true; } @@ -157,18 +192,22 @@ if(index !== -1) { vm.blocks.splice(index, 1); - var layoutIndex = this.layout.findIndex(entry => entry.udi === block.udi); + var layoutIndex = vm.layout.findIndex(entry => entry.udi === block.udi); if(layoutIndex !== -1) { - vm.layout.splice(layoutIndex, 1); + vm.layout.splice(index, 1); } - this.modelObject.removeContentByUdi(block.udi); + modelObject.removeContentByUdi(block.udi); } } + function deleteAllBlocks() { + vm.blocks.forEach(deleteBlock); + } + function editBlock(blockModel) { - // TODO: make a clone to ensure edits arent made directly. + // make a clone to avoid editing model directly. var blockContentModelClone = angular.copy(blockModel.content); var elementEditorModel = { @@ -178,6 +217,8 @@ size: blockModel.config.overlaySize || "medium", submit: function(elementEditorModel) { blockModel.content = elementEditorModel.content; + // TODO, investigate if we need to call a sync, for this scenario to work.. Concern is regarding wether the property-value watcher will pick this up. + //modelObject.setDataFromEditingModel(block); editorService.close(); }, close: function() { @@ -201,13 +242,26 @@ } vm.blockTypePicker = { - show: true, + show: false, size: vm.availableBlockTypes.length < 7 ? "small" : "medium", filter: vm.availableBlockTypes.length > 12 ? true : false, orderBy: "$index", view: "itempicker", event: $event, availableItems: vm.availableBlockTypes, + clickPasteItem: function(item) { + if (item.type === "elementTypeArray") { + var indexIncrementor = 0; + item.data.forEach(function (entry) { + if (requestPasteFromClipboard(createIndex + indexIncrementor, entry)) { + indexIncrementor++; + } + }); + } else { + requestPasteFromClipboard(createIndex, item.data); + } + vm.blockTypePicker.close(); + }, submit: function (model) { if (model && model.selectedItem) { addNewBlock(createIndex, model.selectedItem.alias); @@ -220,12 +274,97 @@ } }; + vm.blockTypePicker.pasteItems = []; + + var singleEntriesForPaste = clipboardService.retriveEntriesOfType("elementType", vm.availableContentTypes); + singleEntriesForPaste.forEach(function (entry) { + vm.blockTypePicker.pasteItems.push({ + type: "elementType", + name: entry.label, + data: entry.data, + icon: entry.icon + }); + }); + + var arrayEntriesForPaste = clipboardService.retriveEntriesOfType("elementTypeArray", vm.availableContentTypes); + arrayEntriesForPaste.forEach(function (entry) { + vm.blockTypePicker.pasteItems.push({ + type: "elementTypeArray", + name: entry.label, + data: entry.data, + icon: entry.icon + }); + }); + + vm.blockTypePicker.title = vm.blockTypePicker.pasteItems.length > 0 ? labels.grid_addElement : labels.content_createEmpty; + + vm.blockTypePicker.clickClearPaste = function ($event) { + $event.stopPropagation(); + $event.preventDefault(); + clipboardService.clearEntriesOfType("elementType", vm.availableContentTypes); + clipboardService.clearEntriesOfType("elementTypeArray", vm.availableContentTypes); + vm.blockTypePicker.pasteItems = [];// This dialog is not connected via the clipboardService events, so we need to update manually. + }; + + vm.blockTypePicker.show = true; + }; function requestCopyBlock(block) { - console.log("copy still needs to be done.") + clipboardService.copy("elementTypeArray", block.content.contentTypeAlias, block.content, block.label); } - function requestDeleteBlock (block) { + + var requestCopyAllBlocks = function() { + + // list aliases + var aliases = vm.blocks.map(block => block.content.contentTypeAlias); + + // remove dublicates + aliases = aliases.filter((item, index) => aliases.indexOf(item) === index); + + var contentNodeName = ""; + if(vm.umbVariantContent) { + contentNodeName = vm.umbVariantContent.editor.content.name; + } + + var elementTypesToCopy = vm.blocks.map(block => block.content); + + localizationService.localize("clipboard_labelForArrayOfItemsFrom", [vm.model.label, contentNodeName]).then(function(localizedLabel) { + clipboardService.copyArray("elementTypeArray", aliases, elementTypesToCopy, localizedLabel, "icon-thumbnail-list", vm.model.id); + }); + } + function requestCopyBlock(block) { + clipboardService.copy("elementType", block.content.contentTypeAlias, block.content, block.label); + } + function requestPasteFromClipboard(index, pasteEntry) { + + if (pasteEntry === undefined) { + return false; + } + + var layoutEntry = modelObject.createFromElementType(pasteEntry); + if (layoutEntry === null) { + return false; + } + + // make editing object + var blockEditingObject = getEditingModel(layoutEntry); + if (blockEditingObject === null) { + return false; + } + + // add layout entry at the decired location in layout. + vm.layout.splice(index, 0, layoutEntry); + + // apply editing model at decired location in editing model. + vm.blocks.splice(index, 0, blockEditingObject); + + vm.moveFocusToBlock = blockEditingObject; + + return true; + + } + function requestDeleteBlock(block) { localizationService.localizeMany(["general_delete", "blockEditor_confirmDeleteBlockMessage", "contentTypeEditor_yesDelete"]).then(function (data) { const overlay = { title: data[0], @@ -243,8 +382,22 @@ overlayService.confirmDelete(overlay); }); } + function requestDeleteAllBlocks() { + localizationService.localizeMany(["content_nestedContentDeleteAllItems", "general_delete"]).then(function (data) { + overlayService.confirmDelete({ + title: data[1], + content: data[0], + close: function () { + overlayService.close(); + }, + submit: function () { + deleteAllBlocks(); + overlayService.close(); + } + }); + }); + } - vm.showCopy = clipboardService.isSupported(); var runtimeSortVars = {}; @@ -289,12 +442,17 @@ editBlock: editBlock, requestCopyBlock: requestCopyBlock, requestDeleteBlock: requestDeleteBlock, - deleteBlock: deleteBlock, - syncBlockData: syncBlockData + deleteBlock: deleteBlock } - function validateLimits() { + function onAmountOfBlocksChanged() { + + // enable/disable property actions + copyAllBlocksAction.isDisabled = vm.blocks.length === 0; + deleteAllBlocksAction.isDisabled = vm.blocks.length === 0; + + // validate limits: if (vm.validationLimit.min !== null) { if (vm.blocks.length < vm.validationLimit.min) { vm.propertyForm.minCount.$setValidity("minCount", false); @@ -314,24 +472,8 @@ - // TODO: We need to investigate if we can do a specific watch on each block, so we dont re-render all blocks. + unsubscribe.push($scope.$watch(() => vm.blocks.length, onAmountOfBlocksChanged)); /* - unsubscribe.push($scope.$watch("vm.blocks[0]", onBlocksUpdated, true)); - function onBlocksUpdated(newVal, oldVal) { - - console.log("blocks update", oldVal, " > ", newVal); - //setDirty(); - - var labelIndex = 1; - for(const block of vm.blocks) { - block.label = blockEditorService.getBlockLabel(block, labelIndex++); - } - } - */ - - - unsubscribe.push($scope.$watch(() => vm.blocks.length, validateLimits)); -/* unsubscribe.push($scope.$on("formSubmitting", function (ev, args) { console.log("formSubmitting is happening, we need to make sure sub property editors are synced first.") @@ -340,7 +482,7 @@ //sync(); })); -*/ + */ $scope.$on("$destroy", function () { for (const subscription of unsubscribe) { subscription(); From beda2fdf72eefb8d8149e90374704a853e365009 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Wed, 4 Mar 2020 13:32:58 +0100 Subject: [PATCH 060/377] order var declarations --- .../propertyeditors/blocklist/blocklist.component.js | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.js index 157d31b9cf..3e04e62b45 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.js @@ -19,8 +19,14 @@ function BlockListController($scope, $interpolate, editorService, clipboardService, localizationService, overlayService, blockEditorService, contentResource, eventsService) { - var modelObject; var unsubscribe = []; + var modelObject; + + // Property actions: + var copyAllBlocksAction; + var deleteAllBlocksAction; + + var vm = this; vm.moveFocusToBlock = null; @@ -44,8 +50,6 @@ eventsService.on("clipboardService.storageUpdate", checkAbilityToPasteContent); - var copyAllBlocksAction; - var deleteAllBlocksAction; vm.$onInit = function() { From 5c47352b0e73c9fc3ae8064291331d0ac51d3e47 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Wed, 4 Mar 2020 13:34:03 +0100 Subject: [PATCH 061/377] remove unused paste function --- .../propertyeditors/blocklist/blocklist.component.js | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.js index 3e04e62b45..fcb2bc926c 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.js @@ -21,7 +21,7 @@ var unsubscribe = []; var modelObject; - + // Property actions: var copyAllBlocksAction; var deleteAllBlocksAction; @@ -31,7 +31,6 @@ vm.moveFocusToBlock = null; vm.showCopy = clipboardService.isSupported(); - vm.showPaste = false; vm.layout = [];// Property models layout object specific to this Block Editor. vm.blocks = [];// Runtime model of editing models, needs to be synced to property model on form submit. @@ -44,10 +43,6 @@ labels.content_createEmpty = data[1]; }); - function checkAbilityToPasteContent() { - vm.showPaste = clipboardService.hasEntriesOfType("elementType", vm.availableContentTypes) || clipboardService.hasEntriesOfType("elementTypeArray", vm.availableContentTypes); - } - eventsService.on("clipboardService.storageUpdate", checkAbilityToPasteContent); @@ -103,8 +98,6 @@ vm.availableContentTypes = modelObject.getAvailableAliasesForBlockContent(); vm.availableBlockTypes = modelObject.getAvailableBlocksForItemPicker(); - checkAbilityToPasteContent(); - } From 0c2a03bd46bd97d6db8a4b48337be5ecb0fc20bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Wed, 4 Mar 2020 13:44:53 +0100 Subject: [PATCH 062/377] block list editor better naming --- .../common/services/blockeditor.service.js | 44 +++++---- .../blocklist/blocklist.component.js | 91 ++++++------------- 2 files changed, 49 insertions(+), 86 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 00308ae4ff..675746dca4 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 @@ -134,10 +134,11 @@ }, /** - * Retrieve editing model of a layout entry + * Retrieve editor friendly model of a block. + * @param {Object} layoutEntry the layout entry to build the block model from. * @return {Object} Scaffolded Block Content object. */ - getEditingModel: function(layoutEntry) { + getBlockModel: function(layoutEntry) { var udi = layoutEntry.udi; @@ -150,9 +151,9 @@ return null; } - var editingModel = {}; - editingModel.config = angular.copy(blockConfiguration); - editingModel.labelInterpolator = $interpolate(editingModel.config.label); + var blockModel = {}; + blockModel.config = angular.copy(blockConfiguration); + blockModel.labelInterpolator = $interpolate(blockModel.config.label); var scaffold = this.getScaffoldFor(blockConfiguration.contentTypeAlias); if(scaffold === null) { @@ -160,30 +161,30 @@ } // make basics from scaffold - editingModel.content = angular.copy(scaffold); - editingModel.content.udi = udi; + blockModel.content = angular.copy(scaffold); + blockModel.content.udi = udi; - mapToElementTypeModel(editingModel.content, contentModel); + mapToElementTypeModel(blockModel.content, contentModel); - editingModel.contentModel = contentModel; - editingModel.layoutModel = layoutEntry; + blockModel.contentModel = contentModel; + blockModel.layoutModel = layoutEntry; // TODO: settings - return editingModel; + return blockModel; }, /** - * Retrieve editing model of a layout entry + * Retrieve block model of a layout entry * @return {Object} Scaffolded Block Content object. */ - setDataFromEditingModel: function(editingModel) { + setDataFromBlockModel: function(blockModel) { - var udi = editingModel.content.key; + var udi = blockModel.content.key; - mapToPropertyModel(editingModel.content, editingModel.contentModel); + mapToPropertyModel(blockModel.content, blockModel.contentModel); // TODO: sync settings to layout entry. @@ -269,9 +270,6 @@ mapToPropertyModel(elementTypeContentModel, contentModel); - console.log(elementTypeContentModel) - console.log(contentModel) - return layoutEntry; } @@ -281,24 +279,24 @@ createModelObject: function(propertyModelValue, propertyEditorAlias, blockConfigurations) { return new BlockEditorModelObject(propertyModelValue, propertyEditorAlias, blockConfigurations); }, - getBlockLabel: function(blockModelObject) { + getBlockLabel: function(blockModel) { // TODO: we should do something about this for performance. var vars = new Object(); - var variant = blockModelObject.content.variants[0]; + var variant = blockModel.content.variants[0]; var tab = variant.tabs[0]; // TODO: need to look up all tabs... for(const property of tab.properties) { vars[property.alias] = property.value; } - if(blockModelObject.labelInterpolator) { - return blockModelObject.labelInterpolator(vars); + if(blockModel.labelInterpolator) { + return blockModel.labelInterpolator(vars); } - return blockModelObject.contentTypeName; + return blockModel.contentTypeName; } } } diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.js index fcb2bc926c..821a9362fa 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.js @@ -32,8 +32,8 @@ vm.moveFocusToBlock = null; vm.showCopy = clipboardService.isSupported(); - vm.layout = [];// Property models layout object specific to this Block Editor. - vm.blocks = [];// Runtime model of editing models, needs to be synced to property model on form submit. + vm.layout = [];// The layout object specific to this Block Editor, will be a direct reference from Property Model. + vm.blocks = [];// Runtime list of block models, needs to be synced to property model on form submit. vm.availableBlockTypes = [];// Available block entries of this property editor. var labels = {}; @@ -93,16 +93,25 @@ function loaded() { vm.layout = modelObject.getLayout(); - mapToBlocks(); + + // maps layout entries to editor friendly models. + vm.layout.forEach(entry => { + var block = getBlockModel(entry); + if(block !== null) { + vm.blocks.push(block); + } + }); vm.availableContentTypes = modelObject.getAvailableAliasesForBlockContent(); vm.availableBlockTypes = modelObject.getAvailableBlocksForItemPicker(); + $scope.$apply(); + } - function getEditingModel(entry) { - var block = modelObject.getEditingModel(entry); + function getBlockModel(entry) { + var block = modelObject.getBlockModel(entry); if (block === null) return null; @@ -111,50 +120,6 @@ return block; } - /** - * Maps property model to the runtime editing model (blocks). - */ - function mapToBlocks() { - // clear blocks. - vm.blocks = []; - - // make all blocks. - vm.layout.forEach(entry => { - var block = getEditingModel(entry); - if(block !== null) { - vm.blocks.push(block); - } - }); - - $scope.$apply(); - } - - /** - * Maps content from runtime editing model (blocks) to the property model. - * Does not take care of ordering, we need the sort-UI to sync that, on the fly. - */ - /* - function mapToContent() { - - // sync data from blocks to content models. - vm.blocks.forEach(block => { - modelObject.setDataFromEditingModel(block); - }); - } - */ - - /* - function sync() { - // to avoid deep watches of block editors we use an event for those instead? - // Lets inform container of this property editor that we updated. - $scope.$emit("blockEditorValueUpdated"); - } - */ - /* - function syncBlockData(block) { - modelObject.setDataFromEditingModel(block); - } - */ function addNewBlock(index, contentTypeAlias) { @@ -164,19 +129,19 @@ return false; } - // make editing object - var blockEditingObject = getEditingModel(layoutEntry); - if (blockEditingObject === null) { + // make block model + var blockModel = getBlockModel(layoutEntry); + if (blockModel === null) { return false; } // add layout entry at the decired location in layout. vm.layout.splice(index, 0, layoutEntry); - // apply editing model at decired location in editing model. - vm.blocks.splice(index, 0, blockEditingObject); + // apply block model at decired location in blocks. + vm.blocks.splice(index, 0, blockModel); - vm.moveFocusToBlock = blockEditingObject; + vm.moveFocusToBlock = blockModel; return true; @@ -215,7 +180,7 @@ submit: function(elementEditorModel) { blockModel.content = elementEditorModel.content; // TODO, investigate if we need to call a sync, for this scenario to work.. Concern is regarding wether the property-value watcher will pick this up. - //modelObject.setDataFromEditingModel(block); + //modelObject.setDataFromBlockModel(block); editorService.close(); }, close: function() { @@ -344,19 +309,19 @@ return false; } - // make editing object - var blockEditingObject = getEditingModel(layoutEntry); - if (blockEditingObject === null) { + // make block model + var blockModel = getBlockModel(layoutEntry); + if (blockModel === null) { return false; } - // add layout entry at the decired location in layout. + // insert layout entry at the decired location in layout. vm.layout.splice(index, 0, layoutEntry); - // apply editing model at decired location in editing model. - vm.blocks.splice(index, 0, blockEditingObject); + // insert block model at the decired location in blocks. + vm.blocks.splice(index, 0, blockModel); - vm.moveFocusToBlock = blockEditingObject; + vm.moveFocusToBlock = blockModel; return true; From eda7c02fab93da3fd5cead03c8c3de1bb07903c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Wed, 4 Mar 2020 13:47:45 +0100 Subject: [PATCH 063/377] simpler label generation --- .../src/common/services/blockeditor.service.js | 13 ++----------- 1 file changed, 2 insertions(+), 11 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 675746dca4..f8c2c4789a 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 @@ -280,20 +280,11 @@ return new BlockEditorModelObject(propertyModelValue, propertyEditorAlias, blockConfigurations); }, getBlockLabel: function(blockModel) { - - // TODO: we should do something about this for performance. - var vars = new Object(); - var variant = blockModel.content.variants[0]; - var tab = variant.tabs[0]; - // TODO: need to look up all tabs... - for(const property of tab.properties) { - vars[property.alias] = property.value; - } - if(blockModel.labelInterpolator) { - return blockModel.labelInterpolator(vars); + // We are just using the contentModel, since its a key/value object that is live synced. (if we need to add additional vars, we could make a shallow copy and apply those.) + return blockModel.labelInterpolator(blockModel.contentModel); } return blockModel.contentTypeName; From 51b0c81ead8bafc640a29b68b2ad2e043809235f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Wed, 4 Mar 2020 13:52:26 +0100 Subject: [PATCH 064/377] clean up --- .../common/services/blockeditor.service.js | 63 ++++++++++--------- 1 file changed, 32 insertions(+), 31 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 f8c2c4789a..e076930b58 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 @@ -91,7 +91,7 @@ } }); - // remove dublicates. + // removing dublicates. scaffoldAliases = scaffoldAliases.filter((value, index, self) => self.indexOf(value) === index); scaffoldAliases.forEach((elementTypeAlias => { @@ -142,7 +142,7 @@ var udi = layoutEntry.udi; - var contentModel = this.getContentByUdi(udi); + var contentModel = this._getContentByUdi(udi); var blockConfiguration = this.getBlockConfiguration(contentModel.contentTypeAlias); @@ -214,7 +214,7 @@ } var entry = { - udi: this.createContent(contentTypeAlias) + udi: this._createContent(contentTypeAlias) } if (blockConfiguration.settingsElementTypeAlias != null) { @@ -224,33 +224,6 @@ return entry; }, - getContentByUdi: function(udi) { - return this.value.data.find(entry => entry.udi === udi); - }, - // private - createContent: function(elementTypeAlias) { - var content = { - contentTypeAlias: elementTypeAlias, - udi: udiService.create("element") - }; - this.value.data.push(content); - return content.udi; - }, - // private - removeContent: function(entry) { - const index = this.value.data.indexOf(entry) - if (index !== -1) { - this.value.splice(index, 1); - } - }, - - removeContentByUdi: function(udi) { - const index = this.value.data.findIndex(o => o.udi === udi); - if (index !== -1) { - this.value.splice(index, 1); - } - }, - /** * Insert data from ElementType Model * @return {Object} Layout entry object, to be inserted at a decired location in the layout object. @@ -266,12 +239,40 @@ return null; } - var contentModel = this.getContentByUdi(layoutEntry.udi); + var contentModel = this._getContentByUdi(layoutEntry.udi); mapToPropertyModel(elementTypeContentModel, contentModel); return layoutEntry; + }, + + // private + _createContent: function(elementTypeAlias) { + var content = { + contentTypeAlias: elementTypeAlias, + udi: udiService.create("element") + }; + this.value.data.push(content); + return content.udi; + }, + // private + _getContentByUdi: function(udi) { + return this.value.data.find(entry => entry.udi === udi); + }, + + removeContent: function(entry) { + const index = this.value.data.indexOf(entry) + if (index !== -1) { + this.value.splice(index, 1); + } + }, + + removeContentByUdi: function(udi) { + const index = this.value.data.findIndex(o => o.udi === udi); + if (index !== -1) { + this.value.splice(index, 1); + } } } From e82cbbd5000fdde3cf7568c60690a56b2035498b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Thu, 5 Mar 2020 11:54:20 +0100 Subject: [PATCH 065/377] compile config for test mode --- src/Umbraco.Web.UI.Client/gulp/config.js | 4 ++++ src/Umbraco.Web.UI.Client/gulp/modes.js | 12 +++++++++++- src/Umbraco.Web.UI.Client/gulp/tasks/test.js | 1 + src/Umbraco.Web.UI.Client/gulpfile.js | 8 ++++---- src/Umbraco.Web.UI.Client/test/config/karma.conf.js | 7 +++++++ 5 files changed, 27 insertions(+), 5 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/gulp/config.js b/src/Umbraco.Web.UI.Client/gulp/config.js index 92e0b6d21d..a397462fd4 100755 --- a/src/Umbraco.Web.UI.Client/gulp/config.js +++ b/src/Umbraco.Web.UI.Client/gulp/config.js @@ -9,6 +9,10 @@ module.exports = { dev: { sourcemaps: true, embedtemplates: false + }, + test: { + sourcemaps: false, + embedtemplates: true } }, sources: { diff --git a/src/Umbraco.Web.UI.Client/gulp/modes.js b/src/Umbraco.Web.UI.Client/gulp/modes.js index dc2947f2cc..21609cdcf8 100644 --- a/src/Umbraco.Web.UI.Client/gulp/modes.js +++ b/src/Umbraco.Web.UI.Client/gulp/modes.js @@ -10,4 +10,14 @@ function setDevelopmentMode(cb) { return cb(); }; -module.exports = { setDevelopmentMode: setDevelopmentMode }; +function setTestMode(cb) { + + config.compile.current = config.compile.test; + + return cb(); +}; + +module.exports = { + setDevelopmentMode: setDevelopmentMode, + setTestMode: setTestMode + }; diff --git a/src/Umbraco.Web.UI.Client/gulp/tasks/test.js b/src/Umbraco.Web.UI.Client/gulp/tasks/test.js index b5239d35e7..255fe17435 100644 --- a/src/Umbraco.Web.UI.Client/gulp/tasks/test.js +++ b/src/Umbraco.Web.UI.Client/gulp/tasks/test.js @@ -23,6 +23,7 @@ function runUnitTestServer() { autoWatch: true, port: 9999, singleRun: false, + browsers: ['ChromeDebugging'], keepalive: true }) .start(); diff --git a/src/Umbraco.Web.UI.Client/gulpfile.js b/src/Umbraco.Web.UI.Client/gulpfile.js index ec9b7bc508..542d45c479 100644 --- a/src/Umbraco.Web.UI.Client/gulpfile.js +++ b/src/Umbraco.Web.UI.Client/gulpfile.js @@ -13,7 +13,7 @@ const { src, dest, series, parallel, lastRun } = require('gulp'); const config = require('./gulp/config'); -const { setDevelopmentMode } = require('./gulp/modes'); +const { setDevelopmentMode, setTestMode } = require('./gulp/modes'); const { dependencies } = require('./gulp/tasks/dependencies'); const { js } = require('./gulp/tasks/js'); const { less } = require('./gulp/tasks/less'); @@ -31,6 +31,6 @@ exports.build = series(parallel(dependencies, js, less, views), testUnit); exports.dev = series(setDevelopmentMode, parallel(dependencies, js, less, views), watchTask); exports.watch = series(watchTask); // -exports.runTests = series(js, testUnit); -exports.runUnit = series(js, runUnitTestServer, watchTask); -exports.testE2e = series(testE2e); +exports.runTests = series(setTestMode, parallel(js, testUnit)); +exports.runUnit = series(setTestMode, parallel(js, runUnitTestServer), watchTask); +exports.testE2e = series(setTestMode, parallel(testE2e)); 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 501e43b043..f6fc5ca1be 100644 --- a/src/Umbraco.Web.UI.Client/test/config/karma.conf.js +++ b/src/Umbraco.Web.UI.Client/test/config/karma.conf.js @@ -103,6 +103,13 @@ module.exports = function (config) { // CLI --browsers Chrome,Firefox,Safari browsers: ['ChromeHeadless'], + customLaunchers: { + ChromeDebugging: { + base: 'Chrome', + flags: ['--remote-debugging-port=9333'] + } + }, + // allow waiting a bit longer, some machines require this browserNoActivityTimeout: 100000, // default 10,000ms From 07bca56c5525839f65cc75ac27f3b71822c90d43 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Thu, 5 Mar 2020 11:54:36 +0100 Subject: [PATCH 066/377] Chrome Debug for VS code --- src/.vscode/launch.json | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 src/.vscode/launch.json diff --git a/src/.vscode/launch.json b/src/.vscode/launch.json new file mode 100644 index 0000000000..cd07033f8f --- /dev/null +++ b/src/.vscode/launch.json @@ -0,0 +1,19 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "type": "chrome", + "request": "attach", + "name": "Attach Karma Chrome against localhost", + "address": "127.0.0.1", + "port": 9333, + "pathMapping": { + "/": "${workspaceRoot}", + "/base/": "${workspaceRoot}" + } + } + ] +} From d01d55b621a159c6eb04808386b114a7ce6e81b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Thu, 5 Mar 2020 11:55:37 +0100 Subject: [PATCH 067/377] promise test working --- .../src/common/mocks/resources/_utils.js | 1 + .../mocks/resources/variantcontent.mocks.js | 42 +++++++++---------- .../common/services/blockeditor.service.js | 4 +- .../blocklist/blocklist.component.js | 2 +- .../services/block-editor-service.spec.js | 39 ++++++++--------- 5 files changed, 43 insertions(+), 45 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/mocks/resources/_utils.js b/src/Umbraco.Web.UI.Client/src/common/mocks/resources/_utils.js index cf73e6a8ce..49a90f273e 100644 --- a/src/Umbraco.Web.UI.Client/src/common/mocks/resources/_utils.js +++ b/src/Umbraco.Web.UI.Client/src/common/mocks/resources/_utils.js @@ -295,6 +295,7 @@ angular.module('umbraco.mocks'). updater: { name: "Per Ploug Krogslund", id: 1 }, path: "-1,1234,2455", allowedActions: ["U", "H", "A"], + contentTypeAlias: "testAlias", variants: [ { name: "", diff --git a/src/Umbraco.Web.UI.Client/src/common/mocks/resources/variantcontent.mocks.js b/src/Umbraco.Web.UI.Client/src/common/mocks/resources/variantcontent.mocks.js index 1ff2d3a0a7..9b5dcf5ad6 100644 --- a/src/Umbraco.Web.UI.Client/src/common/mocks/resources/variantcontent.mocks.js +++ b/src/Umbraco.Web.UI.Client/src/common/mocks/resources/variantcontent.mocks.js @@ -1,31 +1,31 @@ angular.module('umbraco.mocks'). - factory('variantContentMocks', ['$httpBackend', 'mocksUtils', function ($httpBackend, mocksUtils) { - 'use strict'; - - function returnEmptyVariantNode(status, data, headers) { + factory('variantContentMocks', ['$httpBackend', 'mocksUtils', function ($httpBackend, mocksUtils) { + 'use strict'; - if (!mocksUtils.checkAuth()) { - return [401, null, null]; - } + function returnEmptyVariantNode(status, data, headers) { - var response = returnVariantNodebyId(200, "", null); - var node = response[1]; - var parentId = mocksUtils.getParameterByName(data, "parentId") || 1234; + if (!mocksUtils.checkAuth()) { + return [401, null, null]; + } - node.name = ""; - node.id = 0; - node.parentId = parentId; + var response = returnVariantNodebyId(200, "", null); + var node = response[1]; + var parentId = mocksUtils.getParameterByName(data, "parentId") || 1234; - $(node.tabs).each(function(i,tab){ - $(tab.properties).each(function(i, property){ - property.value = ""; - }); - }); + node.name = ""; + node.id = 0; + node.parentId = parentId; - return response; - } + node.tabs.forEach(function(tab){ + tab.properties.forEach(function( property){ + property.value = ""; + }); + }); - function returnVariantNodebyId(status, data, headers) { + return response; + } + + function returnVariantNodebyId(status, data, headers) { if (!mocksUtils.checkAuth()) { return [401, null, null]; 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 e076930b58..9d0a695d02 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 @@ -2,7 +2,7 @@ 'use strict'; - function blockEditorService($interpolate, udiService) { + function blockEditorService($interpolate, udiService, contentResource) { /** @@ -79,7 +79,7 @@ return this.blockConfigurations.find(bc => bc.contentTypeAlias === alias); }, - loadScaffolding: function(contentResource) { + loadScaffolding: function() { var tasks = []; var scaffoldAliases = []; diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.js index 821a9362fa..617f92350d 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.js @@ -54,7 +54,7 @@ vm.model.value = vm.model.value || {}; modelObject = blockEditorService.createModelObject(vm.model.value, vm.model.editor, vm.model.config.blocks); - modelObject.loadScaffolding(contentResource).then(loaded); + modelObject.loadScaffolding().then(loaded); copyAllBlocksAction = { labelKey: "clipboard_labelForCopyAllEntries", diff --git a/src/Umbraco.Web.UI.Client/test/unit/common/services/block-editor-service.spec.js b/src/Umbraco.Web.UI.Client/test/unit/common/services/block-editor-service.spec.js index 5c48b4a8dc..b656d5e56e 100644 --- a/src/Umbraco.Web.UI.Client/test/unit/common/services/block-editor-service.spec.js +++ b/src/Umbraco.Web.UI.Client/test/unit/common/services/block-editor-service.spec.js @@ -1,33 +1,34 @@ describe('blockEditorService tests', function () { - var blockEditorService, $rootScope, $httpBackend, varaintMocks, contentResource; + var blockEditorService, contentResource; beforeEach(module('umbraco.services')); + beforeEach(module('umbraco.resources')); beforeEach(module('umbraco.mocks')); beforeEach(inject(function ($injector, mocksUtils) { mocksUtils.disableAuth(); - blockEditorService = $injector.get('blockEditorService'); - $rootScope = $injector.get('$rootScope'); - $httpBackend = $injector.get('$httpBackend'); - varaintMocks = $injector.get("variantContentMocks"); - varaintMocks.register(); - contentResource = $injector.get('contentResource'); + contentResource = $injector.get("contentResource"); + spyOn(contentResource, "getScaffold").and.callFake( + function () { + return Promise.resolve(mocksUtils.getMockVariantContent(1234)) + } + ); + + blockEditorService = $injector.get('blockEditorService'); })); - var simpleBlockConfigurationMock = {contentTypeAlias: "testAlias", label:"Test", settingsElementTypeAlias: null, view: "testview.html"}; - describe('init blockEditoModelObject', function () { it('fail if no model value', function () { function createWithNoModelValue() { blockEditorService.createModelObject(null, "test", []); - } + } expect(createWithNoModelValue).toThrow(); }); @@ -44,23 +45,19 @@ expect(modelObject.getBlockConfiguration(simpleBlockConfigurationMock.contentTypeAlias).label).toBe(simpleBlockConfigurationMock.label); }); - it('loadScaffolding provides data for itemPicker', function () { + it('loadScaffolding provides data for itemPicker', function (done) { var modelObject = blockEditorService.createModelObject({}, "test", [simpleBlockConfigurationMock]); - var itemPickerOptions; - - var pendingPromise = modelObject.loadScaffolding(contentResource).then(() => { - itemPickerOptions = modelObject.getAvailableBlocksForItemPicker(); + var pendingPromise = modelObject.loadScaffolding().then(() => { + var itemPickerOptions = modelObject.getAvailableBlocksForItemPicker(); + expect(itemPickerOptions.length).toBe(1); + expect(itemPickerOptions[0].alias).toBe(simpleBlockConfigurationMock.contentTypeAlias); + done(); }); - - $rootScope.$digest(); - $httpBackend.flush(); - - expect(itemPickerOptions.length).toBe(1); - }); + }); }); From c4965dad24220a782d190afe3a21347923536b8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Thu, 5 Mar 2020 11:56:23 +0100 Subject: [PATCH 068/377] space change --- .../src/common/mocks/resources/variantcontent.mocks.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/mocks/resources/variantcontent.mocks.js b/src/Umbraco.Web.UI.Client/src/common/mocks/resources/variantcontent.mocks.js index 9b5dcf5ad6..3a434bdadc 100644 --- a/src/Umbraco.Web.UI.Client/src/common/mocks/resources/variantcontent.mocks.js +++ b/src/Umbraco.Web.UI.Client/src/common/mocks/resources/variantcontent.mocks.js @@ -16,8 +16,8 @@ angular.module('umbraco.mocks'). node.id = 0; node.parentId = parentId; - node.tabs.forEach(function(tab){ - tab.properties.forEach(function( property){ + node.tabs.forEach(function(tab) { + tab.properties.forEach(function(property) { property.value = ""; }); }); From 74165ae8596ba09dfc8f6e7aa9c472fbe9afe215 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Thu, 5 Mar 2020 13:14:51 +0100 Subject: [PATCH 069/377] another two tests --- .../services/block-editor-service.spec.js | 74 +++++++++++++++++-- 1 file changed, 66 insertions(+), 8 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/test/unit/common/services/block-editor-service.spec.js b/src/Umbraco.Web.UI.Client/test/unit/common/services/block-editor-service.spec.js index b656d5e56e..6bfc7aea34 100644 --- a/src/Umbraco.Web.UI.Client/test/unit/common/services/block-editor-service.spec.js +++ b/src/Umbraco.Web.UI.Client/test/unit/common/services/block-editor-service.spec.js @@ -21,37 +21,95 @@ })); - var simpleBlockConfigurationMock = {contentTypeAlias: "testAlias", label:"Test", settingsElementTypeAlias: null, view: "testview.html"}; + + var blockConfigurationMock = {contentTypeAlias: "testAlias", label:"Test label", settingsElementTypeAlias: null, view: "testview.html"}; + + var propertyModelMock = { + layout: { + "Umbraco.TestBlockEditor": [ + { + udi: 1234 + } + ] + }, + data: [ + { + udi: 1234, + contentTypeAlias: "testAlias", + testvalue: "myTestValue" + } + ] + }; describe('init blockEditoModelObject', function () { it('fail if no model value', function () { function createWithNoModelValue() { - blockEditorService.createModelObject(null, "test", []); + blockEditorService.createModelObject(null, "Umbraco.TestBlockEditor", []); } expect(createWithNoModelValue).toThrow(); }); it('return a object, with methods', function () { - var modelObject = blockEditorService.createModelObject({}, "test", []); + var modelObject = blockEditorService.createModelObject({}, "Umbraco.TestBlockEditor", []); expect(modelObject).not.toBeUndefined(); expect(modelObject.loadScaffolding).not.toBeUndefined(); }); it('getBlockConfiguration provide the requested block configurtion', function () { - var modelObject = blockEditorService.createModelObject({}, "test", [simpleBlockConfigurationMock]); + var modelObject = blockEditorService.createModelObject({}, "Umbraco.TestBlockEditor", [blockConfigurationMock]); - expect(modelObject.getBlockConfiguration(simpleBlockConfigurationMock.contentTypeAlias).label).toBe(simpleBlockConfigurationMock.label); + expect(modelObject.getBlockConfiguration(blockConfigurationMock.contentTypeAlias).label).toBe(blockConfigurationMock.label); }); it('loadScaffolding provides data for itemPicker', function (done) { - var modelObject = blockEditorService.createModelObject({}, "test", [simpleBlockConfigurationMock]); + var modelObject = blockEditorService.createModelObject({}, "Umbraco.TestBlockEditor", [blockConfigurationMock]); - var pendingPromise = modelObject.loadScaffolding().then(() => { + modelObject.loadScaffolding().then(() => { var itemPickerOptions = modelObject.getAvailableBlocksForItemPicker(); expect(itemPickerOptions.length).toBe(1); - expect(itemPickerOptions[0].alias).toBe(simpleBlockConfigurationMock.contentTypeAlias); + expect(itemPickerOptions[0].alias).toBe(blockConfigurationMock.contentTypeAlias); + done(); + }); + + }); + + it('getLayoutEntry has right values', function (done) { + + + var modelObject = blockEditorService.createModelObject(propertyModelMock, "Umbraco.TestBlockEditor", [blockConfigurationMock]); + + modelObject.loadScaffolding().then(() => { + + var layout = modelObject.getLayout(); + + expect(layout).not.toBeUndefined(); + expect(layout.length).toBe(1); + expect(layout[0]).toBe(propertyModelMock.layout["Umbraco.TestBlockEditor"][0]); + expect(layout[0].udi).toBe(propertyModelMock.layout["Umbraco.TestBlockEditor"][0].udi); + + done(); + }); + + }); + + it('getBlockModel provide value', function (done) { + + + var modelObject = blockEditorService.createModelObject(propertyModelMock, "Umbraco.TestBlockEditor", [blockConfigurationMock]); + + modelObject.loadScaffolding().then(() => { + + var layout = modelObject.getLayout(); + expect(layout).not.toBeUndefined(); + + var blockModel = modelObject.getBlockModel(layout[0]); + + expect(blockModel).not.toBeUndefined(); + expect(blockModel[0].udi).toBe(propertyModelMock.data[0].udi); + expect(blockModel[0].testvalue).toBe(propertyModelMock.data[0].testvalue); + done(); }); From bc97861316b383a27cd01982c2ba8b464874bbec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Thu, 5 Mar 2020 17:38:14 +0100 Subject: [PATCH 070/377] more tests for block list editor --- .../src/common/mocks/resources/_utils.js | 1 + .../blockeditor.block.component.html} | 0 .../blockeditor.block.component.js} | 31 ++----- .../blocklist/blocklist.component.html | 4 +- .../blocklist/blocklist.component.js | 10 +-- .../blocklist/blocklist.component.less | 19 +++-- .../services/block-editor-service.spec.js | 83 +++++++++++++++++-- 7 files changed, 101 insertions(+), 47 deletions(-) rename src/Umbraco.Web.UI.Client/src/views/propertyeditors/{blocklist/blocklist.block.component.html => blockeditor/blockeditor.block.component.html} (100%) rename src/Umbraco.Web.UI.Client/src/views/propertyeditors/{blocklist/blocklist.block.component.js => blockeditor/blockeditor.block.component.js} (51%) diff --git a/src/Umbraco.Web.UI.Client/src/common/mocks/resources/_utils.js b/src/Umbraco.Web.UI.Client/src/common/mocks/resources/_utils.js index 49a90f273e..fc28567ea3 100644 --- a/src/Umbraco.Web.UI.Client/src/common/mocks/resources/_utils.js +++ b/src/Umbraco.Web.UI.Client/src/common/mocks/resources/_utils.js @@ -313,6 +313,7 @@ angular.module('umbraco.mocks'). label: "Content", id: 2, properties: [ + { alias: "testproperty", label: "Test property", view: "textbox", value: "asdfghjk" }, { alias: "valTest", label: "Validation test", view: "validationtest", value: "asdfasdf" }, { alias: "bodyText", label: "Body Text", description: "Here you enter the primary article contents", view: "rte", value: "

    askjdkasj lasjd

    ", config: {} }, { alias: "textarea", label: "textarea", view: "textarea", value: "ajsdka sdjkds", config: { rows: 4 } }, diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.block.component.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blockeditor/blockeditor.block.component.html similarity index 100% rename from src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.block.component.html rename to src/Umbraco.Web.UI.Client/src/views/propertyeditors/blockeditor/blockeditor.block.component.html diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.block.component.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blockeditor/blockeditor.block.component.js similarity index 51% rename from src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.block.component.js rename to src/Umbraco.Web.UI.Client/src/views/propertyeditors/blockeditor/blockeditor.block.component.js index 330d8f7b69..c3397e9433 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.block.component.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blockeditor/blockeditor.block.component.js @@ -2,10 +2,10 @@ "use strict"; angular .module("umbraco") - .component("blockListPropertyEditorBlock", { - templateUrl: "views/propertyeditors/blocklist/blocklist.block.component.html", + .component("blockEditorBlock", { + templateUrl: "views/propertyeditors/blockeditor/blockeditor.block.component.html", transclude: true, - controller: BlockListBlockController, + controller: BlockEditorBlockBlockController, controllerAs: "vm", bindings: { block: "=", @@ -16,7 +16,7 @@ } }); - function BlockListBlockController($scope, blockEditorService) { + function BlockEditorBlockBlockController($scope, blockEditorService) { var unsubscribe = []; var vm = this; @@ -32,9 +32,10 @@ for (var p = 0; p < tab.properties.length; p++) { var prop = tab.properties[p]; - // Sadly we need to deep watch, cause its our only way to make sure that complex values gets synced. Alternative solution would be to sync on a broadcasted event, fired on Save and Copy eventually more. - // But to minimize the watch we only watch the value of properties. But because we are deep watching it means that we are watching everything of nested block editors, so this would only have a performance improvement for first levels of block editors. - // New thoughts, since the value of a property editors is just a pointer (if not primative) then we could properly live without deep watching? cause they reference the same?.. Lets investigate.. + // Watch value of property since this is the only value we want to keep synced. + // Do notice that it is not performing a deep watch, meaning that we are only watching primatives and changes directly to the object of property-value. + // But we like to sync non-primative values as well! Yes, and this does happen, just not through this code, but through the nature of JavaScript. + // Non-primative values act as references to the same data and are therefor synced. unsubscribe.push($scope.$watch("vm.block.content.variants[0].tabs["+t+"].properties["+p+"].value", createPropWatcher(prop))); } } @@ -47,8 +48,6 @@ // sync data: vm.block.contentModel[prop.alias] = prop.value; - //vm.blockEditorApi.sync(); - // update label: updateLabel(); } @@ -58,20 +57,6 @@ function updateLabel() { vm.block.label = blockEditorService.getBlockLabel(vm.block); } - - /** - * Listening for properties - */ - /* - function onBlockEditorValueUpdated($event) { - // Lets sync the value of the property that the event comes from, if we know that.. - - //$event.stopPropagation(); - //$event.preventDefault(); - }; - - unsubscribe.push($scope.$on("blockEditorValueUpdated", onBlockEditorValueUpdated)); - */ $scope.$on("$destroy", function () { for (const subscription of unsubscribe) { diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.html index 3761b42561..2004becba7 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.html @@ -14,7 +14,7 @@ > - +
    @@ -32,7 +32,7 @@
    - +
    diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.js index 617f92350d..9904a0132b 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.js @@ -54,7 +54,7 @@ vm.model.value = vm.model.value || {}; modelObject = blockEditorService.createModelObject(vm.model.value, vm.model.editor, vm.model.config.blocks); - modelObject.loadScaffolding().then(loaded); + modelObject.loadScaffolding().then(onLoaded); copyAllBlocksAction = { labelKey: "clipboard_labelForCopyAllEntries", @@ -90,7 +90,7 @@ } } - function loaded() { + function onLoaded() { vm.layout = modelObject.getLayout(); @@ -105,7 +105,7 @@ vm.availableContentTypes = modelObject.getAvailableAliasesForBlockContent(); vm.availableBlockTypes = modelObject.getAvailableBlocksForItemPicker(); - $scope.$apply(); + $scope.$evalAsync(); } @@ -375,7 +375,7 @@ scroll: true, start: function (ev, ui) { runtimeSortVars.moveFromIndex = ui.item.index(); - $scope.$apply(function () { + $scope.$evalAsync(function () { vm.sorting = true; }); }, @@ -394,7 +394,7 @@ vm.layout.splice(moveToIndex, 0, movedEntry); } - $scope.$apply(function () { + $scope.$evalAsync(function () { vm.sorting = false; }); } diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.less b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.less index 02670e88b0..2ab2c37d3b 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.less +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.less @@ -16,25 +16,25 @@ position: relative; width: 100%; - .umb-block-list__block--head { + /*.umb-block-list__block--head { opacity: 0; transition: opacity 120ms; - } - .umb-block-list__block--actions { + }*/ + > ng-transclude > .umb-block-list__block--actions { opacity: 0; transition: opacity 120ms; } &:hover, &:focus, &:focus-within { - .umb-block-list__block--head { + /*.umb-block-list__block--head { opacity: 1; - } + }*/ - .umb-block-list__block--actions { + > ng-transclude > .umb-block-list__block--actions { opacity: 1; } } - + /* &:focus, &:focus-within { .umb-block-list__block--head { &::before { @@ -42,8 +42,9 @@ } } } + */ } - +/* .umb-block-list__block--head { position: absolute; top: 0; @@ -77,7 +78,7 @@ label.umb-block-list__block--head { cursor: grab; } - +*/ .umb-block-list__block--actions { position: absolute; top: 10px; diff --git a/src/Umbraco.Web.UI.Client/test/unit/common/services/block-editor-service.spec.js b/src/Umbraco.Web.UI.Client/test/unit/common/services/block-editor-service.spec.js index 6bfc7aea34..b51ba1f9ee 100644 --- a/src/Umbraco.Web.UI.Client/test/unit/common/services/block-editor-service.spec.js +++ b/src/Umbraco.Web.UI.Client/test/unit/common/services/block-editor-service.spec.js @@ -1,15 +1,19 @@ describe('blockEditorService tests', function () { - var blockEditorService, contentResource; + var blockEditorService, contentResource, $rootScope, $componentController; beforeEach(module('umbraco.services')); beforeEach(module('umbraco.resources')); beforeEach(module('umbraco.mocks')); + beforeEach(module('umbraco')); - beforeEach(inject(function ($injector, mocksUtils) { + beforeEach(inject(function ($injector, mocksUtils, _$rootScope_, _$componentController_) { mocksUtils.disableAuth(); + $rootScope = _$rootScope_; + $componentController = _$componentController_; + contentResource = $injector.get("contentResource"); spyOn(contentResource, "getScaffold").and.callFake( function () { @@ -36,7 +40,7 @@ { udi: 1234, contentTypeAlias: "testAlias", - testvalue: "myTestValue" + testproperty: "myTestValue" } ] }; @@ -75,7 +79,7 @@ }); - it('getLayoutEntry has right values', function (done) { + it('getLayoutEntry has values', function (done) { var modelObject = blockEditorService.createModelObject(propertyModelMock, "Umbraco.TestBlockEditor", [blockConfigurationMock]); @@ -94,7 +98,7 @@ }); - it('getBlockModel provide value', function (done) { + it('getBlockModel has values', function (done) { var modelObject = blockEditorService.createModelObject(propertyModelMock, "Umbraco.TestBlockEditor", [blockConfigurationMock]); @@ -102,13 +106,76 @@ modelObject.loadScaffolding().then(() => { var layout = modelObject.getLayout(); - expect(layout).not.toBeUndefined(); var blockModel = modelObject.getBlockModel(layout[0]); expect(blockModel).not.toBeUndefined(); - expect(blockModel[0].udi).toBe(propertyModelMock.data[0].udi); - expect(blockModel[0].testvalue).toBe(propertyModelMock.data[0].testvalue); + expect(blockModel.contentModel.udi).toBe(propertyModelMock.data[0].udi); + expect(blockModel.content.variants[0].tabs[0].properties[0].value).toBe(propertyModelMock.data[0].testproperty); + + done(); + }); + + }); + + + it('getBlockModel syncs primative values', function (done) { + + var propertyModel = angular.copy(propertyModelMock); + + var modelObject = blockEditorService.createModelObject(propertyModel, "Umbraco.TestBlockEditor", [blockConfigurationMock]); + + modelObject.loadScaffolding().then(() => { + + var layout = modelObject.getLayout(); + + var blockModel = modelObject.getBlockModel(layout[0]); + + blockEditorBlockComponenet = $componentController("blockEditorBlock", null, {"block": blockModel, "blockEditorApi": {}, "class": "testClass"}); + blockEditorBlockComponenet.$onInit(); + + blockModel.content.variants[0].tabs[0].properties[0].value = "anotherTestValue"; + + $rootScope.$digest();// invoke angularJS Store. + + expect(blockModel.contentModel).toBe(propertyModel.data[0]); + expect(blockModel.contentModel.testproperty).toBe("anotherTestValue"); + expect(propertyModel.data[0].testproperty).toBe("anotherTestValue"); + + // + + done(); + }); + + }); + + + it('getBlockModel syncs values of object', function (done) { + + var propertyModel = angular.copy(propertyModelMock); + + var complexValue = {"list": ["A", "B", "C"]}; + propertyModel.data[0].testproperty = complexValue; + + + var modelObject = blockEditorService.createModelObject(propertyModel, "Umbraco.TestBlockEditor", [blockConfigurationMock]); + + modelObject.loadScaffolding().then(() => { + + var layout = modelObject.getLayout(); + + var blockModel = modelObject.getBlockModel(layout[0]); + + blockEditorBlockComponenet = $componentController("blockEditorBlock", null, {"block": blockModel, "blockEditorApi": {}, "class": "testClass"}); + blockEditorBlockComponenet.$onInit(); + + blockModel.content.variants[0].tabs[0].properties[0].value.list[0] = "AA"; + blockModel.content.variants[0].tabs[0].properties[0].value.list.push("D"); + + $rootScope.$digest();// invoke angularJS Store. + + expect(propertyModel.data[0].testproperty.list[0]).toBe("AA"); + expect(propertyModel.data[0].testproperty.list.length).toBe(4); done(); }); From 1715e1747fb8125c6127ae1b2a148e1b3bbb0d0f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Fri, 6 Mar 2020 10:41:22 +0100 Subject: [PATCH 071/377] provide key on blockModel for angularJS performance optimization --- .../src/common/services/blockeditor.service.js | 1 + .../views/propertyeditors/blocklist/blocklist.component.html | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) 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 9d0a695d02..0f87634aa6 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 @@ -152,6 +152,7 @@ } var blockModel = {}; + blockModel.key = String.CreateGuid(); blockModel.config = angular.copy(blockConfiguration); blockModel.labelInterpolator = $interpolate(blockModel.config.label); diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.html index 2004becba7..05f7fff551 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.html @@ -5,7 +5,7 @@
    -
    +
    diff --git a/src/Umbraco.Web.UI.Client/src/views/blockelements/inlineblock/inlineblock.editor.html b/src/Umbraco.Web.UI.Client/src/views/blockelements/inlineblock/inlineblock.editor.html index 56fbe13e2b..cd7468af8d 100644 --- a/src/Umbraco.Web.UI.Client/src/views/blockelements/inlineblock/inlineblock.editor.html +++ b/src/Umbraco.Web.UI.Client/src/views/blockelements/inlineblock/inlineblock.editor.html @@ -1,10 +1,10 @@
    -
    - +
    diff --git a/src/Umbraco.Web.UI.Client/src/views/blockelements/labelblock/labelblock.editor.html b/src/Umbraco.Web.UI.Client/src/views/blockelements/labelblock/labelblock.editor.html index 2036e3ef3a..fecf1ea51e 100644 --- a/src/Umbraco.Web.UI.Client/src/views/blockelements/labelblock/labelblock.editor.html +++ b/src/Umbraco.Web.UI.Client/src/views/blockelements/labelblock/labelblock.editor.html @@ -1,4 +1,4 @@ - diff --git a/src/Umbraco.Web.UI.Client/src/views/blockelements/textareablock/textareablock.editor.html b/src/Umbraco.Web.UI.Client/src/views/blockelements/textareablock/textareablock.editor.html index 80aba51a84..bbcef1643b 100644 --- a/src/Umbraco.Web.UI.Client/src/views/blockelements/textareablock/textareablock.editor.html +++ b/src/Umbraco.Web.UI.Client/src/views/blockelements/textareablock/textareablock.editor.html @@ -8,7 +8,7 @@ ng-model="vm.firstProperty.value" ng-keypress="vm.submitOnEnter($event)" ng-blur="vm.onBlur()" - focus-when="{{blockvm.focusThis}}" + focus-when="{{vm.moveFocusToBlock === block}}" > diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blockeditor/blockeditor.block.component.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blockeditor/blockeditor.block.component.js index c3397e9433..5945ea9b69 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blockeditor/blockeditor.block.component.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blockeditor/blockeditor.block.component.js @@ -17,7 +17,7 @@ }); function BlockEditorBlockBlockController($scope, blockEditorService) { - + /* var unsubscribe = []; var vm = this; @@ -63,7 +63,7 @@ subscription(); } }); - + */ } diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.html index 05f7fff551..07a07ee529 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.html @@ -5,7 +5,7 @@
    -
    +
    - -
    +
    +
    - -
    - +
    diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.js index 93773b1598..b5c42f5cf6 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.js @@ -51,15 +51,13 @@ vm.validationLimit = vm.model.config.validationLimit; - console.log("Model TESTS:", vm.model.value === null, typeof vm.model.value !== 'object') - // We need to ensure that the property model value is an object, this is needed for modelObject to recive a reference and keep that updated. - if(vm.model.value === null || typeof vm.model.value !== 'object') {// testing if we have null or undefined value or if the value is set to another type than Object. + if(typeof vm.model.value !== 'object' || vm.model.value === null) {// testing if we have null or undefined value or if the value is set to another type than Object. vm.model.value = {}; } // Create Model Object, to manage our data for this Block Editor. - modelObject = blockEditorService.createModelObject(vm.model.value, vm.model.editor, vm.model.config.blocks); + modelObject = blockEditorService.createModelObject(vm.model.value, vm.model.editor, vm.model.config.blocks, $scope); modelObject.loadScaffolding().then(onLoaded); copyAllBlocksAction = { @@ -161,16 +159,20 @@ function deleteBlock(block) { + var index = vm.blocks.indexOf(block); if(index !== -1) { - vm.blocks.splice(index, 1); - var layoutIndex = layout.findIndex(entry => entry.udi === block.udi); + var layoutIndex = layout.findIndex(entry => entry.udi === block.content.udi); if(layoutIndex !== -1) { - vm.layout.splice(index, 1); + layout.splice(index, 1); + } else { + throw new Error("Could not find layout entry of block with udi: "+block.content.udi) } - modelObject.removeContentByUdi(block.udi); + vm.blocks.splice(index, 1); + + modelObject.removeDataAndDestroyModel(block); } } @@ -189,6 +191,7 @@ view: "views/common/infiniteeditors/elementeditor/elementeditor.html", size: blockModel.config.overlaySize || "medium", submit: function(elementEditorModel) { + // To ensure syncronization gets tricked we transfer blockEditorService.mapElementTypeValues(elementEditorModel.content, blockModel.content) editorService.close(); }, @@ -397,7 +400,7 @@ var moveFromIndex = runtimeSortVars.moveFromIndex; var moveToIndex = ui.item.index(); - if (moveToIndex > -1 && moveFromIndex !== moveToIndex) { + if (moveToIndex !== -1 && moveFromIndex !== moveToIndex) { var movedEntry = layout[moveFromIndex]; layout.splice(moveFromIndex, 1); layout.splice(moveToIndex, 0, movedEntry); @@ -424,19 +427,19 @@ deleteAllBlocksAction.isDisabled = vm.blocks.length === 0; // validate limits: - if (vm.validationLimit.min !== null) { - if (vm.blocks.length < vm.validationLimit.min) { + if (vm.propertyForm) { + if (vm.validationLimit.min !== null && vm.blocks.length < vm.validationLimit.min) { vm.propertyForm.minCount.$setValidity("minCount", false); } else { vm.propertyForm.minCount.$setValidity("minCount", true); } - } - if (vm.validationLimit.max !== null && vm.blocks.length > vm.validationLimit.max) { - vm.propertyForm.maxCount.$setValidity("maxCount", false); - } - else { - vm.propertyForm.maxCount.$setValidity("maxCount", true); + if (vm.validationLimit.max !== null && vm.blocks.length > vm.validationLimit.max) { + vm.propertyForm.maxCount.$setValidity("maxCount", false); + } + else { + vm.propertyForm.maxCount.$setValidity("maxCount", true); + } } } From ad93436178c5ae9f5481b48f62c1f01e9fa46516 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Mon, 9 Mar 2020 12:43:53 +0100 Subject: [PATCH 074/377] more tests --- .../services/block-editor-service.spec.js | 77 +++++++++++++++---- 1 file changed, 60 insertions(+), 17 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/test/unit/common/services/block-editor-service.spec.js b/src/Umbraco.Web.UI.Client/test/unit/common/services/block-editor-service.spec.js index b51ba1f9ee..6abf85c9b6 100644 --- a/src/Umbraco.Web.UI.Client/test/unit/common/services/block-editor-service.spec.js +++ b/src/Umbraco.Web.UI.Client/test/unit/common/services/block-editor-service.spec.js @@ -1,18 +1,18 @@ describe('blockEditorService tests', function () { - var blockEditorService, contentResource, $rootScope, $componentController; + var blockEditorService, contentResource, $rootScope, $scope; beforeEach(module('umbraco.services')); beforeEach(module('umbraco.resources')); beforeEach(module('umbraco.mocks')); beforeEach(module('umbraco')); - beforeEach(inject(function ($injector, mocksUtils, _$rootScope_, _$componentController_) { + beforeEach(inject(function ($injector, mocksUtils, _$rootScope_) { mocksUtils.disableAuth(); $rootScope = _$rootScope_; - $componentController = _$componentController_; + $scope = $rootScope.$new(); contentResource = $injector.get("contentResource"); spyOn(contentResource, "getScaffold").and.callFake( @@ -49,26 +49,26 @@ it('fail if no model value', function () { function createWithNoModelValue() { - blockEditorService.createModelObject(null, "Umbraco.TestBlockEditor", []); + blockEditorService.createModelObject(null, "Umbraco.TestBlockEditor", [], $scope); } expect(createWithNoModelValue).toThrow(); }); it('return a object, with methods', function () { - var modelObject = blockEditorService.createModelObject({}, "Umbraco.TestBlockEditor", []); + var modelObject = blockEditorService.createModelObject({}, "Umbraco.TestBlockEditor", [], $scope); expect(modelObject).not.toBeUndefined(); expect(modelObject.loadScaffolding).not.toBeUndefined(); }); it('getBlockConfiguration provide the requested block configurtion', function () { - var modelObject = blockEditorService.createModelObject({}, "Umbraco.TestBlockEditor", [blockConfigurationMock]); + var modelObject = blockEditorService.createModelObject({}, "Umbraco.TestBlockEditor", [blockConfigurationMock], $scope); expect(modelObject.getBlockConfiguration(blockConfigurationMock.contentTypeAlias).label).toBe(blockConfigurationMock.label); }); it('loadScaffolding provides data for itemPicker', function (done) { - var modelObject = blockEditorService.createModelObject({}, "Umbraco.TestBlockEditor", [blockConfigurationMock]); + var modelObject = blockEditorService.createModelObject({}, "Umbraco.TestBlockEditor", [blockConfigurationMock], $scope); modelObject.loadScaffolding().then(() => { var itemPickerOptions = modelObject.getAvailableBlocksForItemPicker(); @@ -82,7 +82,7 @@ it('getLayoutEntry has values', function (done) { - var modelObject = blockEditorService.createModelObject(propertyModelMock, "Umbraco.TestBlockEditor", [blockConfigurationMock]); + var modelObject = blockEditorService.createModelObject(propertyModelMock, "Umbraco.TestBlockEditor", [blockConfigurationMock], $scope); modelObject.loadScaffolding().then(() => { @@ -101,7 +101,7 @@ it('getBlockModel has values', function (done) { - var modelObject = blockEditorService.createModelObject(propertyModelMock, "Umbraco.TestBlockEditor", [blockConfigurationMock]); + var modelObject = blockEditorService.createModelObject(propertyModelMock, "Umbraco.TestBlockEditor", [blockConfigurationMock], $scope); modelObject.loadScaffolding().then(() => { @@ -123,7 +123,7 @@ var propertyModel = angular.copy(propertyModelMock); - var modelObject = blockEditorService.createModelObject(propertyModel, "Umbraco.TestBlockEditor", [blockConfigurationMock]); + var modelObject = blockEditorService.createModelObject(propertyModel, "Umbraco.TestBlockEditor", [blockConfigurationMock], $scope); modelObject.loadScaffolding().then(() => { @@ -131,9 +131,6 @@ var blockModel = modelObject.getBlockModel(layout[0]); - blockEditorBlockComponenet = $componentController("blockEditorBlock", null, {"block": blockModel, "blockEditorApi": {}, "class": "testClass"}); - blockEditorBlockComponenet.$onInit(); - blockModel.content.variants[0].tabs[0].properties[0].value = "anotherTestValue"; $rootScope.$digest();// invoke angularJS Store. @@ -158,7 +155,7 @@ propertyModel.data[0].testproperty = complexValue; - var modelObject = blockEditorService.createModelObject(propertyModel, "Umbraco.TestBlockEditor", [blockConfigurationMock]); + var modelObject = blockEditorService.createModelObject(propertyModel, "Umbraco.TestBlockEditor", [blockConfigurationMock], $scope); modelObject.loadScaffolding().then(() => { @@ -166,9 +163,6 @@ var blockModel = modelObject.getBlockModel(layout[0]); - blockEditorBlockComponenet = $componentController("blockEditorBlock", null, {"block": blockModel, "blockEditorApi": {}, "class": "testClass"}); - blockEditorBlockComponenet.$onInit(); - blockModel.content.variants[0].tabs[0].properties[0].value.list[0] = "AA"; blockModel.content.variants[0].tabs[0].properties[0].value.list.push("D"); @@ -182,6 +176,55 @@ }); + it('layout is referencing layout of propertyModel', function (done) { + + var propertyModel = angular.copy(propertyModelMock); + + var modelObject = blockEditorService.createModelObject(propertyModel, "Umbraco.TestBlockEditor", [blockConfigurationMock], $scope); + + modelObject.loadScaffolding().then(() => { + + var layout = modelObject.getLayout(); + + // remove from layout; + layout.splice(0, 1); + + expect(propertyModel.layout["Umbraco.TestBlockEditor"].length).toBe(0); + expect(propertyModel.layout["Umbraco.TestBlockEditor"][0]).toBeUndefined(); + + done(); + }); + + }); + + it('removeDataAndDestroyModel removes data', function (done) { + + var propertyModel = angular.copy(propertyModelMock); + + var modelObject = blockEditorService.createModelObject(propertyModel, "Umbraco.TestBlockEditor", [blockConfigurationMock], $scope); + + modelObject.loadScaffolding().then(() => { + + var layout = modelObject.getLayout(); + + var blockModel = modelObject.getBlockModel(layout[0]); + + // remove from layout; + layout.splice(0, 1); + + // remove from data; + modelObject.removeDataAndDestroyModel(blockModel); + + expect(propertyModel.data.length).toBe(0); + expect(propertyModel.data[0]).toBeUndefined(); + expect(propertyModel.layout["Umbraco.TestBlockEditor"].length).toBe(0); + expect(propertyModel.layout["Umbraco.TestBlockEditor"][0]).toBeUndefined(); + + done(); + }); + + }); + }); From d92023e7195cde96ca7c191be2f82ca1469de59d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Mon, 9 Mar 2020 15:28:42 +0100 Subject: [PATCH 075/377] fix C# test --- .../PropertyEditors/BlockListPropertyValueConverterTests.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Umbraco.Tests/PropertyEditors/BlockListPropertyValueConverterTests.cs b/src/Umbraco.Tests/PropertyEditors/BlockListPropertyValueConverterTests.cs index a8a500d5c0..7f98ef5f18 100644 --- a/src/Umbraco.Tests/PropertyEditors/BlockListPropertyValueConverterTests.cs +++ b/src/Umbraco.Tests/PropertyEditors/BlockListPropertyValueConverterTests.cs @@ -48,11 +48,11 @@ namespace Umbraco.Tests.PropertyEditors private BlockListConfiguration ConfigForMany() => new BlockListConfiguration { Blocks = new[] { - new BlockListConfiguration.ElementType + new BlockListConfiguration.BlockConfiguration { Alias = "Test1" }, - new BlockListConfiguration.ElementType + new BlockListConfiguration.BlockConfiguration { Alias = "Test2" } @@ -62,7 +62,7 @@ namespace Umbraco.Tests.PropertyEditors private BlockListConfiguration ConfigForSingle() => new BlockListConfiguration { Blocks = new[] { - new BlockListConfiguration.ElementType + new BlockListConfiguration.BlockConfiguration { Alias = "Test1" } From 1117f37b9e37cb825ff0a02cdc012da69e1bf2b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Tue, 10 Mar 2020 15:04:50 +0100 Subject: [PATCH 076/377] remove unused block watcher component --- .../blockeditor.block.component.html | 5 -- .../blockeditor.block.component.js | 70 ------------------- 2 files changed, 75 deletions(-) delete mode 100644 src/Umbraco.Web.UI.Client/src/views/propertyeditors/blockeditor/blockeditor.block.component.html delete mode 100644 src/Umbraco.Web.UI.Client/src/views/propertyeditors/blockeditor/blockeditor.block.component.js diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blockeditor/blockeditor.block.component.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blockeditor/blockeditor.block.component.html deleted file mode 100644 index 36905978d5..0000000000 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blockeditor/blockeditor.block.component.html +++ /dev/null @@ -1,5 +0,0 @@ -
    - - - -
    diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blockeditor/blockeditor.block.component.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blockeditor/blockeditor.block.component.js deleted file mode 100644 index 5945ea9b69..0000000000 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blockeditor/blockeditor.block.component.js +++ /dev/null @@ -1,70 +0,0 @@ -(function () { - "use strict"; - angular - .module("umbraco") - .component("blockEditorBlock", { - templateUrl: "views/propertyeditors/blockeditor/blockeditor.block.component.html", - transclude: true, - controller: BlockEditorBlockBlockController, - controllerAs: "vm", - bindings: { - block: "=", - blockEditorApi: "<", - focusThisBlock: " Date: Tue, 10 Mar 2020 15:05:32 +0100 Subject: [PATCH 077/377] clean css --- .../blocklist/blocklist.component.less | 69 ------------------- 1 file changed, 69 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.less b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.less index 2ab2c37d3b..df19af462c 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.less +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.less @@ -16,69 +16,18 @@ position: relative; width: 100%; - /*.umb-block-list__block--head { - opacity: 0; - transition: opacity 120ms; - }*/ > ng-transclude > .umb-block-list__block--actions { opacity: 0; transition: opacity 120ms; } &:hover, &:focus, &:focus-within { - /*.umb-block-list__block--head { - opacity: 1; - }*/ > ng-transclude > .umb-block-list__block--actions { opacity: 1; } } - /* - &:focus, &:focus-within { - .umb-block-list__block--head { - &::before { - background-color: @blueMid; - } - } - } - */ } -/* -.umb-block-list__block--head { - position: absolute; - top: 0; - left: -180px;// 160px from control-header + 20px from spacing. - bottom: 0; - width: 180px;// 160px from control-header + 20px from spacing. - user-select: none; - padding-top: 6px; - padding-right: 14px; - box-sizing: border-box; - color: @gray-5; - background-color: rgba(255, 255, 255, .96); - box-shadow: 0 0 6px 6px rgba(255, 255, 255, .96); - text-align: right; - &::before { - content: ''; - position: absolute; - top: 6px; - bottom: 6px; - right: 4px; - width: 1px; - background-color: @gray-10; - } - - small { - text-align: left; - margin-left: 4px; - margin-bottom: 4px; - } -} -label.umb-block-list__block--head { - cursor: grab; -} -*/ .umb-block-list__block--actions { position: absolute; top: 10px; @@ -182,24 +131,6 @@ label.umb-block-list__block--head { } } } -/* -.umb-block-list__block--create-bar { - button { - display: inline-block; - width: 120px; - height: 120px; - border-radius: @baseBorderRadius; - text-align: center; - font-size: 12px; - i { - font-size: 30px; - line-height: 20px; - margin-bottom: 10px; - display: block; - } - } -} -*/ .umb-block-list__create-button { display: flex; width: 100%; From d8705831da5f23f2bdfb89c5fa8ef018e72b8faf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Thu, 12 Mar 2020 11:31:06 +0100 Subject: [PATCH 078/377] only show on hover or focus for block-actions --- .../views/propertyeditors/blocklist/blocklist.component.less | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.less b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.less index df19af462c..474657e417 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.less +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.less @@ -16,14 +16,14 @@ position: relative; width: 100%; - > ng-transclude > .umb-block-list__block--actions { + > .umb-block-list__block--actions { opacity: 0; transition: opacity 120ms; } &:hover, &:focus, &:focus-within { - > ng-transclude > .umb-block-list__block--actions { + > .umb-block-list__block--actions { opacity: 1; } } From 9859f1b0d385972f14b85e971ac2c23bce46ee1d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Thu, 12 Mar 2020 15:48:26 +0100 Subject: [PATCH 079/377] clean up and prepare for implementing settings --- .../common/services/blockeditor.service.js | 31 ++++++++++---- .../blockeditor.controller.js} | 5 ++- .../blockeditor.html} | 14 +++++-- .../blocklist/blocklist.component.html | 6 +++ .../blocklist/blocklist.component.js | 42 +++++++++++++------ src/Umbraco.Web.UI/Umbraco/config/lang/da.xml | 1 + src/Umbraco.Web.UI/Umbraco/config/lang/en.xml | 1 + .../Umbraco/config/lang/en_us.xml | 1 + 8 files changed, 75 insertions(+), 26 deletions(-) rename src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/{elementeditor/elementeditor.controller.js => blockeditor/blockeditor.controller.js} (75%) rename src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/{elementeditor/elementeditor.html => blockeditor/blockeditor.html} (69%) 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 c6613e0c60..8df95bb684 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 @@ -82,12 +82,10 @@ /** - * Used to create a scoped watcher for a property on a blockModel. + * Used to create a scoped watcher for a content property on a blockModel. */ - function createPropWatcher(blockModel, prop) { - + function createContentModelPropWatcher(blockModel, prop) { return function() { - // sync data: blockModel.contentModel[prop.alias] = prop.value; @@ -95,7 +93,16 @@ // TODO: could use a debounce. blockModel.label = getBlockLabel(blockModel); } + } + /** + * Used to create a scoped watcher for a settings property on a blockModel. + */ + function createSettingsModelPropWatcher(blockModel, prop) { + return function() { + // sync data: + blockModel.layoutModel.settings[prop.alias] = prop.value; + } } @@ -143,7 +150,7 @@ this.blockConfigurations.forEach(blockConfiguration => { scaffoldAliases.push(blockConfiguration.contentTypeAlias); if (blockConfiguration.settingsElementTypeAlias != null) { - scaffoldAliases.push(elementType.settingsElementTypeAlias); + scaffoldAliases.push(blockConfiguration.settingsElementTypeAlias); } }); @@ -233,7 +240,12 @@ blockModel.layoutModel = layoutEntry; blockModel.watchers = []; - // TODO: settings + // TODO: implement settings + + // create ElementTypeModel of settings + // store ElementTypeModel in blockModel.settings + // setup watchers for mapping + // Add blockModel to our isolated scope to enable watching its values: this.isolatedScope.blockModels["_"+blockModel.key] = blockModel; @@ -249,7 +261,7 @@ // Do notice that it is not performing a deep watch, meaning that we are only watching primatives and changes directly to the object of property-value. // But we like to sync non-primative values as well! Yes, and this does happen, just not through this code, but through the nature of JavaScript. // Non-primative values act as references to the same data and are therefor synced. - blockModel.watchers.push(this.isolatedScope.$watch("blockModels._"+blockModel.key+".content.variants[0].tabs["+t+"].properties["+p+"].value", createPropWatcher(blockModel, prop))); + blockModel.watchers.push(this.isolatedScope.$watch("blockModels._"+blockModel.key+".content.variants[0].tabs["+t+"].properties["+p+"].value", createContentModelPropWatcher(blockModel, prop))); } } @@ -286,7 +298,8 @@ mapToPropertyModel(blockModel.content, blockModel.contentModel); - // TODO: sync settings to layout entry. + // TODO: implement settings, sync settings to layout entry. + // mapToPropertyModel(blockModel.settings, blockModel.layoutModel.settings) }, @@ -318,7 +331,7 @@ } if (blockConfiguration.settingsElementTypeAlias != null) { - // TODO: Settings. + entry.settings = {}; } return entry; diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/elementeditor/elementeditor.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/blockeditor/blockeditor.controller.js similarity index 75% rename from src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/elementeditor/elementeditor.controller.js rename to src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/blockeditor/blockeditor.controller.js index a054e64784..fea337f31f 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/elementeditor/elementeditor.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/blockeditor/blockeditor.controller.js @@ -1,12 +1,15 @@ //used for the media picker dialog angular.module("umbraco") -.controller("Umbraco.Editors.ElementEditorController", +.controller("Umbraco.Editors.BlockEditorController", function ($scope) { var vm = this; vm.content = $scope.model.content; + // TODO: implement settings — do notice that settings is optional. + //vm.settings = $scope.model.settings; + vm.title = $scope.model.title; vm.saveAndClose = function() { diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/elementeditor/elementeditor.html b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/blockeditor/blockeditor.html similarity index 69% rename from src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/elementeditor/elementeditor.html rename to src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/blockeditor/blockeditor.html index e5c43468af..b52480c2a7 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/elementeditor/elementeditor.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/blockeditor/blockeditor.html @@ -1,6 +1,6 @@ -
    +
    - + - + + + + +
    diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.html index 07a07ee529..7f002846d7 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.html @@ -19,6 +19,12 @@
    +
    - +
    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 7380c9b0c8..74c61a419a 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 @@ -4,45 +4,48 @@ angular.module("umbraco") function ($scope) { var vm = this; - function showContent() { - if (vm.settingsTab) vm.settingsTab.active = false; - vm.contentTab.active = true; - } - - function showSettings() { - if (vm.settingsTab) vm.settingsTab.active = true; - vm.contentTab.active = false; - } - vm.content = $scope.model.content; vm.settings = $scope.model.settings; + + vm.model = $scope.model; + console.log("blockeditor model:", vm.model) + vm.tabs = []; - var settingsOnly = vm.content && vm.content.variants ? false : true; - if (!settingsOnly) { - vm.contentTab = { - "name": "Content", - "alias": "content", - "icon": "icon-document", - "action": showContent, - "active": true - }; + if (vm.content && vm.content.variants) { - vm.tabs.push(vm.contentTab); + var apps = vm.content.apps; + + vm.tabs = apps; + + // replace view of content app. + var contentApp = apps.find(entry => entry.alias === "umbContent"); + contentApp.view = "views/common/infiniteeditors/elementeditor/elementeditor.content.html"; + + if($scope.model.hideContent) { + apps.splice(apps.indexOf(contentApp), 1); + } + + // remove info app: + var infoAppIndex = apps.findIndex(entry => entry.alias === "umbInfo"); + apps.splice(infoAppIndex, 1); + } if (vm.settings && vm.settings.variants) { - vm.settingsTab = { + var settingsTab = { "name": "Settings", "alias": "settings", "icon": "icon-settings", - "action": showSettings, - "active": settingsOnly + "view": "views/common/infiniteeditors/elementeditor/elementeditor.settings.html" }; - vm.tabs.push(vm.settingsTab); + vm.tabs.push(settingsTab); } - vm.title = (settingsOnly ? 'SETTINGS: ' : '') + $scope.model.title; + // activate frst app: + if (vm.tabs.length > 0) { + vm.tabs[0].active = true; + } vm.saveAndClose = function () { if ($scope.model && $scope.model.submit) { @@ -52,6 +55,7 @@ angular.module("umbraco") vm.close = function() { if ($scope.model && $scope.model.close) { + // TODO: If content has changed, we should notify user. $scope.model.close($scope.model); } } diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/blockeditor/blockeditor.html b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/blockeditor/blockeditor.html index 51750756a8..4480648da2 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/blockeditor/blockeditor.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/blockeditor/blockeditor.html @@ -4,7 +4,7 @@ - - -
    -
    -
    - -
    -
    +
    + +
    -
    -
    -
    - -
    -
    -
    diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/elementeditor/elementContentEditor.component.html b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/elementeditor/elementEditor.content.component.html similarity index 66% rename from src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/elementeditor/elementContentEditor.component.html rename to src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/elementeditor/elementEditor.content.component.html index 88a2b306ef..5308173c72 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/elementeditor/elementContentEditor.component.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/elementeditor/elementEditor.content.component.html @@ -2,7 +2,7 @@
    + ng-repeat="group in vm.model.variants[0].tabs track by group.label">
    {{ group.label }}
    @@ -13,13 +13,13 @@ data-element="property-{{property.alias}}" ng-repeat="property in group.properties track by property.alias" property="property" - show-inherit="vm.content.variants.length > 1 && !property.culture && !activeVariant.language.isDefault" + show-inherit="vm.model.variants.length > 1 && !property.culture && !activeVariant.language.isDefault" inherits-from="defaultVariant.language.name"> -
    +
    + preview="vm.model.variants.length > 1 && !activeVariant.language.isDefault && !property.culture && !property.unlockInvariantValue">
    @@ -29,7 +29,7 @@
    diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/elementeditor/elementContentEditor.component.js b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/elementeditor/elementEditor.content.component.js similarity index 54% rename from src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/elementeditor/elementContentEditor.component.js rename to src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/elementeditor/elementEditor.content.component.js index 347d8f1d7b..5056576ca3 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/elementeditor/elementContentEditor.component.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/elementeditor/elementEditor.content.component.js @@ -3,16 +3,16 @@ angular .module('umbraco.directives') - .component('umbElementContentEditor', { - templateUrl: 'views/common/infiniteeditors/elementeditor/elementContentEditor.component.html', - controller: ElementEditorComponentController, + .component('umbElementEditorContent', { + templateUrl: 'views/common/infiniteeditors/elementeditor/elementEditor.content.component.html', + controller: ElementEditorContentComponentController, controllerAs: 'vm', bindings: { - content: '=' + model: '=' } }); - function ElementEditorComponentController() { + function ElementEditorContentComponentController() { // TODO: we might not need this.. diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/elementeditor/elementeditor.content.html b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/elementeditor/elementeditor.content.html index d30b343c4d..eb8c72c579 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/elementeditor/elementeditor.content.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/elementeditor/elementeditor.content.html @@ -1 +1,3 @@ - +
    + +
    diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/elementeditor/elementeditor.settings.html b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/elementeditor/elementeditor.settings.html new file mode 100644 index 0000000000..df69e2e648 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/elementeditor/elementeditor.settings.html @@ -0,0 +1 @@ + diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.html index 7f002846d7..4b54b85fe8 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.html @@ -19,7 +19,7 @@
    - -
    +
    diff --git a/src/Umbraco.Web.UI.Client/src/views/blockelements/inlineblock/inlineblock.editor.less b/src/Umbraco.Web.UI.Client/src/views/blockelements/inlineblock/inlineblock.editor.less index ab6b21d898..30d1e6bd2d 100644 --- a/src/Umbraco.Web.UI.Client/src/views/blockelements/inlineblock/inlineblock.editor.less +++ b/src/Umbraco.Web.UI.Client/src/views/blockelements/inlineblock/inlineblock.editor.less @@ -6,7 +6,7 @@ border-radius: @baseBorderRadius; transition: border-color 120ms; - &:not(.--open):hover { + .umb-block-list__block:not(.--open) &:hover { border-color: @gray-8; } @@ -43,7 +43,7 @@ } } - &.--open { + .umb-block-list__block.--open & { border-color: @gray-8; box-shadow: 0 0 2px 0px rgba(0, 0, 0, 0.05); > button { @@ -55,7 +55,7 @@ } .blockelement-inlineblock-editor__inner { border-top: 1px solid @gray-8; - background-color: @gray-11; + background-color: @gray-12; .umb-group-panel { background-color: transparent; diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.html index 4b54b85fe8..4aa3b0f6b1 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.html @@ -14,7 +14,7 @@ > -
    +
    diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.less b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.less index 474657e417..d470ee97cf 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.less +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.less @@ -21,7 +21,10 @@ transition: opacity 120ms; } - &:hover, &:focus, &:focus-within { + &:hover, + &:focus, + &:focus-within, + &.--open { > .umb-block-list__block--actions { opacity: 1; From e20c7578f8cc697d38a7da3983f22b7b7fdbc0ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Wed, 1 Apr 2020 13:44:56 +0200 Subject: [PATCH 087/377] NotSupported property editor, to be used when an editor is not supported in the given context. --- .../common/services/blockeditor.service.js | 24 ++++++++++++++++++- src/Umbraco.Web.UI.Client/src/less/belle.less | 1 + .../notsupported/notsupported.controller.js | 9 +++++++ .../notsupported/notsupported.html | 3 +++ .../notsupported/notsupported.less | 7 ++++++ src/Umbraco.Web.UI/Umbraco/config/lang/da.xml | 1 + src/Umbraco.Web.UI/Umbraco/config/lang/en.xml | 1 + .../Umbraco/config/lang/en_us.xml | 1 + 8 files changed, 46 insertions(+), 1 deletion(-) create mode 100644 src/Umbraco.Web.UI.Client/src/views/propertyeditors/notsupported/notsupported.controller.js create mode 100644 src/Umbraco.Web.UI.Client/src/views/propertyeditors/notsupported/notsupported.html create mode 100644 src/Umbraco.Web.UI.Client/src/views/propertyeditors/notsupported/notsupported.less 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 8703aeb385..a05535d3b4 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 @@ -145,6 +145,28 @@ } + /** + * Used to highlight unsupported properties for the user, changes unsupported properties into a unsupported-property. + */ + var notSupportedProperties = [ + "Umbraco.Tags", + "Umbraco.UploadField", + "Umbraco.ImageCropper" + ]; + function replaceUnsupportedProperties(scaffold) { + scaffold.variants.forEach((variant) => { + variant.tabs.forEach((tab) => { + tab.properties.forEach((property) => { + if (notSupportedProperties.indexOf(property.editor) !== -1) { + property.view = "notsupported"; + } + }); + }); + }); + return scaffold; + } + + /** * @ngdoc factory * @name umbraco.factory.BlockEditorModelObject @@ -198,7 +220,7 @@ scaffoldAliases.forEach((elementTypeAlias => { tasks.push(contentResource.getScaffold(-20, elementTypeAlias).then(scaffold => { - this.scaffolds.push(scaffold); + this.scaffolds.push(replaceUnsupportedProperties(scaffold)); })); })); diff --git a/src/Umbraco.Web.UI.Client/src/less/belle.less b/src/Umbraco.Web.UI.Client/src/less/belle.less index e0e679924b..14a62ae790 100644 --- a/src/Umbraco.Web.UI.Client/src/less/belle.less +++ b/src/Umbraco.Web.UI.Client/src/less/belle.less @@ -200,6 +200,7 @@ // Property Editors @import "../views/propertyeditors/blocklist/blocklist.component.less"; @import "../views/propertyeditors/blocklist/prevalue/blocklist.elementtypepicker.less"; +@import "../views/propertyeditors/notsupported/notsupported.less"; // Utilities diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/notsupported/notsupported.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/notsupported/notsupported.controller.js new file mode 100644 index 0000000000..17e9f47e19 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/notsupported/notsupported.controller.js @@ -0,0 +1,9 @@ +angular.module('umbraco').controller("Umbraco.PropertyEditors.NotSupportedController", + function ($scope) { + + var vm = this; + + console.log($scope.umbProperty); + + } +); diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/notsupported/notsupported.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/notsupported/notsupported.html new file mode 100644 index 0000000000..81b6da1996 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/notsupported/notsupported.html @@ -0,0 +1,3 @@ +
    + +
    diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/notsupported/notsupported.less b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/notsupported/notsupported.less new file mode 100644 index 0000000000..9bf3ffc2dc --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/notsupported/notsupported.less @@ -0,0 +1,7 @@ +.umb-property-editor-notsupported { + background-color: @red; + color: white; + padding: 5px 10px; + width: auto; + border-radius: @baseBorderRadius * 2; +} diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/da.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/da.xml index 779e97b035..4c74c45d88 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/da.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/da.xml @@ -607,6 +607,7 @@ Du skal stå til venstre for de 2 celler du ønsker at samle! Du kan ikke opdele en celle, som ikke allerede er delt. Denne egenskab er ugyldig + Feltet %0% bruger editor %1% som ikke er supporteret for ElementTyper. Om diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml index 1dbfd85cd5..a09088efa1 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml @@ -608,6 +608,7 @@ %0% is a mandatory field %0% at %1% is not in a correct format %0% is not in a correct format + Property %0% uses editor %1% which is not supported in ElementTypes. Received an error from the server 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 348880965e..89fbceaea8 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml @@ -625,6 +625,7 @@ Please place cursor at the left of the two cells you wish to merge You cannot split a cell that hasn't been merged. This property is invalid + Property %0% uses editor %1% which is not supported in ElementTypes. Options From d6cad317509ced1a9b65d21f973c4518274f2a3e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Wed, 1 Apr 2020 14:31:56 +0200 Subject: [PATCH 088/377] remove unused controller --- .../notsupported/notsupported.controller.js | 9 --------- .../views/propertyeditors/notsupported/notsupported.html | 2 +- 2 files changed, 1 insertion(+), 10 deletions(-) delete mode 100644 src/Umbraco.Web.UI.Client/src/views/propertyeditors/notsupported/notsupported.controller.js diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/notsupported/notsupported.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/notsupported/notsupported.controller.js deleted file mode 100644 index 17e9f47e19..0000000000 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/notsupported/notsupported.controller.js +++ /dev/null @@ -1,9 +0,0 @@ -angular.module('umbraco').controller("Umbraco.PropertyEditors.NotSupportedController", - function ($scope) { - - var vm = this; - - console.log($scope.umbProperty); - - } -); diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/notsupported/notsupported.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/notsupported/notsupported.html index 81b6da1996..a9d86de217 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/notsupported/notsupported.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/notsupported/notsupported.html @@ -1,3 +1,3 @@ -
    +
    From 9bdb0cb1b8d415416483123bdc59ef767b490d6a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Wed, 1 Apr 2020 15:29:58 +0200 Subject: [PATCH 089/377] Hide group header if only one group is presented --- .../inlineblock/inlineblock.editor.html | 2 +- .../inlineblock/inlineblock.editor.less | 15 +++++++++++---- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/blockelements/inlineblock/inlineblock.editor.html b/src/Umbraco.Web.UI.Client/src/views/blockelements/inlineblock/inlineblock.editor.html index 5a83a80234..028a10d434 100644 --- a/src/Umbraco.Web.UI.Client/src/views/blockelements/inlineblock/inlineblock.editor.html +++ b/src/Umbraco.Web.UI.Client/src/views/blockelements/inlineblock/inlineblock.editor.html @@ -4,7 +4,7 @@ {{block.label}} -
    +
    diff --git a/src/Umbraco.Web.UI.Client/src/views/blockelements/inlineblock/inlineblock.editor.less b/src/Umbraco.Web.UI.Client/src/views/blockelements/inlineblock/inlineblock.editor.less index 30d1e6bd2d..7de7707896 100644 --- a/src/Umbraco.Web.UI.Client/src/views/blockelements/inlineblock/inlineblock.editor.less +++ b/src/Umbraco.Web.UI.Client/src/views/blockelements/inlineblock/inlineblock.editor.less @@ -56,13 +56,20 @@ .blockelement-inlineblock-editor__inner { border-top: 1px solid @gray-8; background-color: @gray-12; - - .umb-group-panel { + + > * > * > * > .umb-group-panel { background-color: transparent; box-shadow: none; + margin-top: 10px; margin-bottom: 0; + > .umb-group-panel__content > .umb-property { + margin-bottom: 10px; + } } - .umb-group-panel__header { - display:none; + .umb-group-panel + .umb-group-panel { + margin-top: 20px; + } + &.--singleGroup > * > * > * > .umb-group-panel .umb-group-panel__header { + display: none; } } From c04a68730dac81cd75c73d398dd51dd4058362a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Wed, 1 Apr 2020 15:34:50 +0200 Subject: [PATCH 090/377] rename notsupport property editor css class --- .../src/views/propertyeditors/notsupported/notsupported.html | 2 +- .../src/views/propertyeditors/notsupported/notsupported.less | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/notsupported/notsupported.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/notsupported/notsupported.html index a9d86de217..a2fbb0e907 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/notsupported/notsupported.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/notsupported/notsupported.html @@ -1,3 +1,3 @@ -
    +
    diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/notsupported/notsupported.less b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/notsupported/notsupported.less index 9bf3ffc2dc..5eaec3f67b 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/notsupported/notsupported.less +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/notsupported/notsupported.less @@ -1,4 +1,4 @@ -.umb-property-editor-notsupported { +.umb-property-editor.umb-property-editor--notsupported { background-color: @red; color: white; padding: 5px 10px; From 52832a9b1c588f45e9428d1e66ca1504c63c06f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Wed, 1 Apr 2020 15:42:32 +0200 Subject: [PATCH 091/377] smaller header for property group --- .../src/less/components/html/umb-group-panel.less | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/less/components/html/umb-group-panel.less b/src/Umbraco.Web.UI.Client/src/less/components/html/umb-group-panel.less index 15f85e1c77..76c0c55fca 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/html/umb-group-panel.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/html/umb-group-panel.less @@ -8,11 +8,11 @@ .umb-group-panel__header { padding: 12px 20px; font-weight: bold; - font-size: 16px; + font-size: 14px; display: flex; align-items: center; justify-content: space-between; - color: @grayDark; + color: @grayDarker; border-bottom: 1px solid @gray-9; } From 47147612774532eb694db25a0b22955f33b6a9af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Wed, 1 Apr 2020 15:42:43 +0200 Subject: [PATCH 092/377] hide description if no description is presented --- .../src/views/components/property/umb-property.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/components/property/umb-property.html b/src/Umbraco.Web.UI.Client/src/views/components/property/umb-property.html index ca57679f51..c7a7530181 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/property/umb-property.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/property/umb-property.html @@ -23,7 +23,7 @@ - +
    From 62c4ecac0109e89666dd0ee1a0c69acb70e72358 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Wed, 1 Apr 2020 16:13:16 +0200 Subject: [PATCH 093/377] css adjustments --- .../blockelements/inlineblock/inlineblock.editor.less | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/blockelements/inlineblock/inlineblock.editor.less b/src/Umbraco.Web.UI.Client/src/views/blockelements/inlineblock/inlineblock.editor.less index 7de7707896..a1e5b54c35 100644 --- a/src/Umbraco.Web.UI.Client/src/views/blockelements/inlineblock/inlineblock.editor.less +++ b/src/Umbraco.Web.UI.Client/src/views/blockelements/inlineblock/inlineblock.editor.less @@ -63,13 +63,17 @@ margin-top: 10px; margin-bottom: 0; > .umb-group-panel__content > .umb-property { - margin-bottom: 10px; + margin-bottom: 20px; } } .umb-group-panel + .umb-group-panel { margin-top: 20px; } - &.--singleGroup > * > * > * > .umb-group-panel .umb-group-panel__header { - display: none; + &.--singleGroup > * > * > * > .umb-group-panel { + margin-top: 0; + > .umb-group-panel__header { + display: none; + } } + } From ee534f88cc42365997453be17ef015377be24fbe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Wed, 1 Apr 2020 16:42:09 +0200 Subject: [PATCH 094/377] Inline create button styling: Better spacing, darker blue color for Focus Outline, moving the plus icon to mouse position for better visual appearance. --- .../inlineblock/inlineblock.editor.less | 4 +-- ...klist.component.createButton.controller.js | 18 ++++++++++++ .../blocklist/blocklist.component.html | 3 ++ .../blocklist/blocklist.component.less | 28 ++++++++++--------- 4 files changed, 38 insertions(+), 15 deletions(-) create mode 100644 src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.createButton.controller.js diff --git a/src/Umbraco.Web.UI.Client/src/views/blockelements/inlineblock/inlineblock.editor.less b/src/Umbraco.Web.UI.Client/src/views/blockelements/inlineblock/inlineblock.editor.less index a1e5b54c35..2702ca163a 100644 --- a/src/Umbraco.Web.UI.Client/src/views/blockelements/inlineblock/inlineblock.editor.less +++ b/src/Umbraco.Web.UI.Client/src/views/blockelements/inlineblock/inlineblock.editor.less @@ -1,7 +1,7 @@ .blockelement-inlineblock-editor { - margin-bottom: 2px; - margin-top: 2px; + margin-bottom: 4px; + margin-top: 4px; border: 1px solid @gray-9; border-radius: @baseBorderRadius; transition: border-color 120ms; diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.createButton.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.createButton.controller.js new file mode 100644 index 0000000000..1a0b4f6241 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.createButton.controller.js @@ -0,0 +1,18 @@ +(function () { + "use strict"; + + angular + .module("umbraco") + .controller("Umbraco.PropertyEditors.BlockListPropertyEditor.CreateButtonController", + function BlockListController($scope) { + + var vm = this; + vm.plusPosX = 0; + + vm.onMouseMove = function($event) { + vm.plusPosX = $event.offsetX; + } + + }); + +})(); diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.html index 4aa3b0f6b1..fc957ce003 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.html @@ -11,7 +11,10 @@ type="button" class="btn-reset umb-block-list__block--create-button" ng-click="vm.showCreateDialog($index, $event)" + ng-controller="Umbraco.PropertyEditors.BlockListPropertyEditor.CreateButtonController as vm" + ng-mousemove="vm.onMouseMove($event)" > +
    +
    diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.less b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.less index d470ee97cf..4194b5d89e 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.less +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.less @@ -71,7 +71,7 @@ opacity: 0; outline: none; height: 12px; - margin-top: -6px; + margin-top: -9px; padding-top: 6px; margin-bottom: -6px; transition: opacity 240ms; @@ -79,7 +79,9 @@ &::before { content: ''; position: absolute; - background-color: @ui-outline; + background-color: @blueMid; + border-top:1px solid white; + border-bottom:1px solid white; border-radius: 2px; top:5px; right: 0; @@ -92,21 +94,21 @@ 100% { opacity: 0.5; } } } - &::after { - content: "+"; - margin-left: auto; - margin-right: auto; + > .__plus { + position: absolute; + pointer-events: none;// lets stop avoiding the mouse values in JS move event. + margin-left: -18px - 10px; margin-top: -18px; margin-bottom: -18px; width: 28px; height: 25px; padding-bottom: 3px; border-radius: 3em; - border: 2px solid @ui-outline; + border: 2px solid @blueMid; display: flex; justify-content: center; align-items: center; - color: @ui-outline; + color: @blueMid; font-size: 20px; font-weight: 800; background-color: rgba(255, 255, 255, .96); @@ -115,20 +117,20 @@ transition: transform 240ms ease-in; animation: umb-block-list__block--create-button_after 800ms ease-in-out infinite; @keyframes umb-block-list__block--create-button_after { - 0% { color: rgba(@ui-outline, 0.8); } - 50% { color: rgba(@ui-outline, 1); } - 100% { color: rgba(@ui-outline, 0.8); } + 0% { color: rgba(@blueMid, 0.8); } + 50% { color: rgba(@blueMid, 1); } + 100% { color: rgba(@blueMid, 0.8); } } } &:focus { - &::after { + > .__plus { border: 2px solid @ui-outline; } } &:hover, &:focus { opacity: 1; transition-duration: 120ms; - &::after { + > .__plus { transform: scale(1); transition-timing-function: cubic-bezier(0.175, 0.885, 0.32, 1.275); } From f58f0547aca2a1ce125754b3bd74e661875237ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Wed, 1 Apr 2020 17:06:42 +0200 Subject: [PATCH 095/377] css correction --- .../views/propertyeditors/blocklist/blocklist.component.less | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.less b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.less index 4194b5d89e..d08c862f88 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.less +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.less @@ -71,7 +71,7 @@ opacity: 0; outline: none; height: 12px; - margin-top: -9px; + margin-top: -7px; padding-top: 6px; margin-bottom: -6px; transition: opacity 240ms; From 2ed740b22a5621357067473493a30f6e12ee2289 Mon Sep 17 00:00:00 2001 From: Benjamin Carleski Date: Wed, 1 Apr 2020 15:54:49 -0700 Subject: [PATCH 096/377] Add references for picked items --- .../V_8_7_0/StackedContentToBlockList.cs | 2 +- .../Models/Blocks/IBlockEditorDataHelper.cs | 11 + .../Models/Blocks/IBlockElement.cs | 15 +- src/Umbraco.Core/Umbraco.Core.csproj | 1 + .../BlockEditorPropertyEditor.cs | 262 +++++++++++++++++- .../BlockListPropertyEditor.cs | 41 ++- 6 files changed, 323 insertions(+), 9 deletions(-) create mode 100644 src/Umbraco.Core/Models/Blocks/IBlockEditorDataHelper.cs diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_7_0/StackedContentToBlockList.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_7_0/StackedContentToBlockList.cs index 0714c4b632..076b4f205c 100644 --- a/src/Umbraco.Core/Migrations/Upgrade/V_8_7_0/StackedContentToBlockList.cs +++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_7_0/StackedContentToBlockList.cs @@ -132,7 +132,7 @@ namespace Umbraco.Core.Migrations.Upgrade.V_8_7_0 private void UpdateDataType(DataTypeDto dataType) { - dataType.DbType = ValueStorageType.Nvarchar.ToString(); + dataType.DbType = ValueStorageType.Ntext.ToString(); dataType.EditorAlias = Constants.PropertyEditors.Aliases.BlockList; Database.Update(dataType); diff --git a/src/Umbraco.Core/Models/Blocks/IBlockEditorDataHelper.cs b/src/Umbraco.Core/Models/Blocks/IBlockEditorDataHelper.cs new file mode 100644 index 0000000000..32f8431e65 --- /dev/null +++ b/src/Umbraco.Core/Models/Blocks/IBlockEditorDataHelper.cs @@ -0,0 +1,11 @@ +using Newtonsoft.Json.Linq; +using System.Collections.Generic; + +namespace Umbraco.Core.Models.Blocks +{ + public interface IBlockEditorDataHelper + { + IEnumerable GetBlockReferences(JObject layout); + bool IsEditorSpecificPropertyKey(string propertyKey); + } +} diff --git a/src/Umbraco.Core/Models/Blocks/IBlockElement.cs b/src/Umbraco.Core/Models/Blocks/IBlockElement.cs index eeb5a73e2c..38b4e96aae 100644 --- a/src/Umbraco.Core/Models/Blocks/IBlockElement.cs +++ b/src/Umbraco.Core/Models/Blocks/IBlockElement.cs @@ -1,8 +1,18 @@ 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 + { + Udi Udi { get; } + } + // TODO: IBlockElement doesn't make sense, this is a reference to an actual element with some settings // and always has to do with the "Layout", should possibly be called IBlockReference or IBlockLayout or IBlockLayoutReference - /// /// Represents a data item for a Block editor implementation /// @@ -10,9 +20,8 @@ /// /// see: https://github.com/umbraco/rfcs/blob/907f3758cf59a7b6781296a60d57d537b3b60b8c/cms/0011-block-data-structure.md#strongly-typed /// - public interface IBlockElement + public interface IBlockElement : IBlockReference { - Udi Udi { get; } TSettings Settings { get; } } } diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index 148aa0052d..73bd8a1126 100755 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -132,6 +132,7 @@ + diff --git a/src/Umbraco.Web/PropertyEditors/BlockEditorPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/BlockEditorPropertyEditor.cs index b9ee1b84fb..f8105c5651 100644 --- a/src/Umbraco.Web/PropertyEditors/BlockEditorPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/BlockEditorPropertyEditor.cs @@ -1,5 +1,17 @@ -using Umbraco.Core.Logging; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Text.RegularExpressions; +using Umbraco.Core; +using Umbraco.Core.Logging; +using Umbraco.Core.Models; +using Umbraco.Core.Models.Blocks; +using Umbraco.Core.Models.Editors; using Umbraco.Core.PropertyEditors; +using Umbraco.Core.Services; namespace Umbraco.Web.PropertyEditors { @@ -9,9 +21,255 @@ namespace Umbraco.Web.PropertyEditors public abstract class BlockEditorPropertyEditor : DataEditor { public const string ContentTypeAliasPropertyKey = "contentTypeAlias"; + public const string UdiPropertyKey = "udi"; + private readonly IBlockEditorDataHelper _dataHelper; + private readonly Lazy _propertyEditors; + private readonly IDataTypeService _dataTypeService; + private readonly IContentTypeService _contentTypeService; - public BlockEditorPropertyEditor(ILogger logger) : base(logger) + public BlockEditorPropertyEditor(ILogger logger, Lazy propertyEditors, IDataTypeService dataTypeService, IContentTypeService contentTypeService, IBlockEditorDataHelper dataHelper) + : base(logger) { + _dataHelper = dataHelper; + _propertyEditors = propertyEditors; + _dataTypeService = dataTypeService; + _contentTypeService = contentTypeService; } + + // has to be lazy else circular dep in ctor + private PropertyEditorCollection PropertyEditors => _propertyEditors.Value; + + #region Value Editor + + protected override IDataValueEditor CreateValueEditor() => new BlockEditorPropertyValueEditor(Attribute, _dataHelper, PropertyEditors, _dataTypeService, _contentTypeService); + + internal class BlockEditorPropertyValueEditor : DataValueEditor, IDataValueReference + { + private readonly IBlockEditorDataHelper _dataHelper; + private readonly PropertyEditorCollection _propertyEditors; + private readonly IDataTypeService _dataTypeService; + private readonly BlockEditorValues _blockEditorValues; + + public BlockEditorPropertyValueEditor(DataEditorAttribute attribute, IBlockEditorDataHelper dataHelper, PropertyEditorCollection propertyEditors, IDataTypeService dataTypeService, IContentTypeService contentTypeService) + : base(attribute) + { + _dataHelper = dataHelper; + _propertyEditors = propertyEditors; + _dataTypeService = dataTypeService; + _blockEditorValues = new BlockEditorValues(dataHelper, contentTypeService); + Validators.Add(new BlockEditorValidator(propertyEditors, dataTypeService, _blockEditorValues)); + } + + public IEnumerable GetReferences(object value) + { + var rawJson = value == null ? string.Empty : value is string str ? str : value.ToString(); + + var result = new List(); + + foreach (var row in _blockEditorValues.GetPropertyValues(rawJson, out _)) + { + if (row.PropType == null) continue; + + var propEditor = _propertyEditors[row.PropType.PropertyEditorAlias]; + + var valueEditor = propEditor?.GetValueEditor(); + if (!(valueEditor is IDataValueReference reference)) continue; + + var val = row.JsonRowValue[row.PropKey]?.ToString(); + + var refs = reference.GetReferences(val); + + result.AddRange(refs); + } + + return result; + } + } + + internal class BlockEditorValidator : IValueValidator + { + private readonly PropertyEditorCollection _propertyEditors; + private readonly IDataTypeService _dataTypeService; + private readonly BlockEditorValues _blockEditorValues; + + public BlockEditorValidator(PropertyEditorCollection propertyEditors, IDataTypeService dataTypeService, BlockEditorValues blockEditorValues) + { + _propertyEditors = propertyEditors; + _dataTypeService = dataTypeService; + _blockEditorValues = blockEditorValues; + } + + public IEnumerable Validate(object rawValue, string valueType, object dataTypeConfiguration) + { + var validationResults = new List(); + + foreach (var row in _blockEditorValues.GetPropertyValues(rawValue, out _)) + { + if (row.PropType == null) continue; + + var config = _dataTypeService.GetDataType(row.PropType.DataTypeId).Configuration; + var propertyEditor = _propertyEditors[row.PropType.PropertyEditorAlias]; + if (propertyEditor == null) continue; + + foreach (var validator in propertyEditor.GetValueEditor().Validators) + { + foreach (var result in validator.Validate(row.JsonRowValue[row.PropKey], propertyEditor.GetValueEditor().ValueType, config)) + { + result.ErrorMessage = "Item " + (row.RowIndex + 1) + " '" + row.PropType.Name + "' " + result.ErrorMessage; + validationResults.Add(result); + } + } + + // Check mandatory + if (row.PropType.Mandatory) + { + if (row.JsonRowValue[row.PropKey] == null) + { + var message = string.IsNullOrWhiteSpace(row.PropType.MandatoryMessage) + ? $"'{row.PropType.Name}' cannot be null" + : row.PropType.MandatoryMessage; + validationResults.Add(new ValidationResult($"Item {(row.RowIndex + 1)}: {message}", new[] { row.PropKey })); + } + else if (row.JsonRowValue[row.PropKey].ToString().IsNullOrWhiteSpace() || (row.JsonRowValue[row.PropKey].Type == JTokenType.Array && !row.JsonRowValue[row.PropKey].HasValues)) + { + var message = string.IsNullOrWhiteSpace(row.PropType.MandatoryMessage) + ? $"'{row.PropType.Name}' cannot be empty" + : row.PropType.MandatoryMessage; + validationResults.Add(new ValidationResult($"Item {(row.RowIndex + 1)}: {message}", new[] { row.PropKey })); + } + } + + // Check regex + if (!row.PropType.ValidationRegExp.IsNullOrWhiteSpace() + && row.JsonRowValue[row.PropKey] != null && !row.JsonRowValue[row.PropKey].ToString().IsNullOrWhiteSpace()) + { + var regex = new Regex(row.PropType.ValidationRegExp); + if (!regex.IsMatch(row.JsonRowValue[row.PropKey].ToString())) + { + var message = string.IsNullOrWhiteSpace(row.PropType.ValidationRegExpMessage) + ? $"'{row.PropType.Name}' is invalid, it does not match the correct pattern" + : row.PropType.ValidationRegExpMessage; + validationResults.Add(new ValidationResult($"Item {(row.RowIndex + 1)}: {message}", new[] { row.PropKey })); + } + } + } + + return validationResults; + } + } + + internal class BlockEditorValues + { + private readonly IBlockEditorDataHelper _dataHelper; + private readonly Lazy> _contentTypes; + + public BlockEditorValues(IBlockEditorDataHelper dataHelper, IContentTypeService contentTypeService) + { + _dataHelper = dataHelper; + _contentTypes = new Lazy>(() => contentTypeService.GetAll().ToDictionary(c => c.Alias)); + } + + private IContentType GetElementType(JObject item) + { + var contentTypeAlias = item[ContentTypeAliasPropertyKey]?.ToObject() ?? string.Empty; + _contentTypes.Value.TryGetValue(contentTypeAlias, out var contentType); + return contentType; + } + + public IEnumerable GetPropertyValues(object propertyValue, out List deserialized) + { + var rowValues = new List(); + + deserialized = null; + + if (propertyValue == null || string.IsNullOrWhiteSpace(propertyValue.ToString())) + return Enumerable.Empty(); + + var data = JsonConvert.DeserializeObject(propertyValue.ToString()); + if (data?.Layout == null || data.Data == null || data.Data.Count == 0) + return Enumerable.Empty(); + + var blockRefs = _dataHelper.GetBlockReferences(data.Layout); + if (blockRefs == null) + return Enumerable.Empty(); + + var dataMap = new Dictionary(data.Data.Count); + data.Data.ForEach(d => + { + var udiObj = d?[UdiPropertyKey]; + if (Udi.TryParse(udiObj == null || udiObj.Type != JTokenType.String ? null : udiObj.ToString(), out var udi)) + dataMap[udi] = d; + }); + + deserialized = blockRefs.Select(r => dataMap.TryGetValue(r.Udi, out var block) ? block : null).Where(r => r != null).ToList(); + if (deserialized == null || deserialized.Count == 0) + return Enumerable.Empty(); + + var index = 0; + + foreach (var o in deserialized) + { + var propValues = o; + + var contentType = GetElementType(propValues); + if (contentType == null) + continue; + + var propertyTypes = contentType.CompositionPropertyTypes.ToDictionary(x => x.Alias, x => x); + var propAliases = propValues.Properties().Select(x => x.Name); + foreach (var propAlias in propAliases) + { + propertyTypes.TryGetValue(propAlias, out var propType); + rowValues.Add(new RowValue(propAlias, propType, propValues, index)); + } + index++; + } + + return rowValues; + } + + internal class RowValue + { + public RowValue(string propKey, PropertyType propType, JObject propValues, int index) + { + PropKey = propKey ?? throw new ArgumentNullException(nameof(propKey)); + PropType = propType; + JsonRowValue = propValues ?? throw new ArgumentNullException(nameof(propValues)); + RowIndex = index; + } + + /// + /// The current property key being iterated for the row value + /// + public string PropKey { get; } + + /// + /// The of the value (if any), this may be null + /// + public PropertyType PropType { get; } + + /// + /// The json values for the current row + /// + public JObject JsonRowValue { get; } + + /// + /// The Nested Content row index + /// + public int RowIndex { get; } + } + + private class BlockEditorData + { + [JsonProperty("layout")] + public JObject Layout { get; set; } + + [JsonProperty("data")] + public List Data { get; set; } + } + } + #endregion + + private static bool IsSystemPropertyKey(string propertyKey) => ContentTypeAliasPropertyKey == propertyKey || UdiPropertyKey == propertyKey; } } diff --git a/src/Umbraco.Web/PropertyEditors/BlockListPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/BlockListPropertyEditor.cs index 782122bccd..3f8288b0ac 100644 --- a/src/Umbraco.Web/PropertyEditors/BlockListPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/BlockListPropertyEditor.cs @@ -1,5 +1,10 @@ -using Umbraco.Core; +using Newtonsoft.Json.Linq; +using System; +using System.Collections.Generic; +using System.Linq; +using Umbraco.Core; using Umbraco.Core.Logging; +using Umbraco.Core.Models.Blocks; using Umbraco.Core.PropertyEditors; using Umbraco.Core.Services; @@ -17,8 +22,8 @@ namespace Umbraco.Web.PropertyEditors Icon = "icon-thumbnail-list")] public class BlockListPropertyEditor : BlockEditorPropertyEditor { - public BlockListPropertyEditor(ILogger logger) - : base(logger) + public BlockListPropertyEditor(ILogger logger, Lazy propertyEditors, IDataTypeService dataTypeService, IContentTypeService contentTypeService) + : base(logger, propertyEditors, dataTypeService, contentTypeService, new DataHelper()) { } #region Pre Value Editor @@ -27,5 +32,35 @@ namespace Umbraco.Web.PropertyEditors #endregion + #region IBlockEditorDataHelper + + private class DataHelper : IBlockEditorDataHelper + { + public IEnumerable GetBlockReferences(JObject layout) + { + if (!(layout?[Constants.PropertyEditors.Aliases.BlockList] is JArray blLayouts)) + yield break; + + foreach (var blLayout in blLayouts) + { + if (!(blLayout is JObject blockRef) || !(blockRef[UdiPropertyKey] is JValue udiRef) || udiRef.Type != JTokenType.String || !Udi.TryParse(udiRef.ToString(), out var udi)) continue; + yield return new SimpleRef(udi); + } + } + + public bool IsEditorSpecificPropertyKey(string propertyKey) => false; + + private class SimpleRef : IBlockReference + { + public SimpleRef(Udi udi) + { + Udi = udi; + } + + public Udi Udi { get; } + } + } + + #endregion } } From 1bd490174620d4bc0340b44ae563999d00c7ff3c Mon Sep 17 00:00:00 2001 From: Warren Buckley Date: Thu, 2 Apr 2020 11:11:08 +0100 Subject: [PATCH 097/377] Revert commit 45e892f3505059674779c6e1a43084a367c2862f - Changes api to GetData --- .../Models/Blocks/BlockListLayoutReference.cs | 11 ++++++++++- src/Umbraco.Core/Models/Blocks/BlockListModel.cs | 14 +------------- .../BlockListPropertyValueConverterTests.cs | 11 ++++------- .../BlockListPropertyValueConverter.cs | 2 +- 4 files changed, 16 insertions(+), 22 deletions(-) diff --git a/src/Umbraco.Core/Models/Blocks/BlockListLayoutReference.cs b/src/Umbraco.Core/Models/Blocks/BlockListLayoutReference.cs index 85d17fad24..19b30e6ea6 100644 --- a/src/Umbraco.Core/Models/Blocks/BlockListLayoutReference.cs +++ b/src/Umbraco.Core/Models/Blocks/BlockListLayoutReference.cs @@ -10,9 +10,10 @@ namespace Umbraco.Core.Models.Blocks [DataContract(Name = "blockListLayout", Namespace = "")] public class BlockListLayoutReference : IBlockElement { - public BlockListLayoutReference(Udi udi, IPublishedElement settings) + public BlockListLayoutReference(Udi udi, IPublishedElement data, IPublishedElement settings) { Udi = udi ?? throw new ArgumentNullException(nameof(udi)); + Data = data ?? throw new ArgumentNullException(nameof(data)); Settings = settings; // can be null } @@ -28,5 +29,13 @@ namespace Umbraco.Core.Models.Blocks [DataMember(Name = "settings")] public IPublishedElement Settings { get; } + /// + /// The data item referenced + /// + /// + /// This is ignored from serialization since it is just a reference to the actual data element + /// + [IgnoreDataMember] + public IPublishedElement Data { get; } } } diff --git a/src/Umbraco.Core/Models/Blocks/BlockListModel.cs b/src/Umbraco.Core/Models/Blocks/BlockListModel.cs index 153fe6be8a..089ca7e6a3 100644 --- a/src/Umbraco.Core/Models/Blocks/BlockListModel.cs +++ b/src/Umbraco.Core/Models/Blocks/BlockListModel.cs @@ -1,5 +1,4 @@ using System.Collections.Generic; -using System.Linq; using System.Runtime.Serialization; using Umbraco.Core.Models.PublishedContent; @@ -23,17 +22,6 @@ namespace Umbraco.Core.Models.Blocks [DataMember(Name = "layout")] public IEnumerable Layout { get; } - /// - /// Returns the data item associated with the layout udi reference - /// - /// - /// - public IPublishedElement GetData(Udi udi) - { - if (!(udi is GuidUdi guidUdi)) - return null; - return Data.FirstOrDefault(x => x.Key == guidUdi.Guid); - } - + } } diff --git a/src/Umbraco.Tests/PropertyEditors/BlockListPropertyValueConverterTests.cs b/src/Umbraco.Tests/PropertyEditors/BlockListPropertyValueConverterTests.cs index 7f98ef5f18..f63485f4bf 100644 --- a/src/Umbraco.Tests/PropertyEditors/BlockListPropertyValueConverterTests.cs +++ b/src/Umbraco.Tests/PropertyEditors/BlockListPropertyValueConverterTests.cs @@ -317,15 +317,12 @@ data: []}"; Assert.AreEqual(2, converted.Layout.Count()); var item0 = converted.Layout.ElementAt(0); - var item0Data = converted.GetData(item0.Udi); - Assert.IsNotNull(item0Data); - Assert.AreEqual(Guid.Parse("1304E1DD-AC87-4396-84FE-8A399231CB3D"), item0Data.Key); - Assert.AreEqual("home", item0Data.ContentType.Alias); + Assert.AreEqual(Guid.Parse("1304E1DD-AC87-4396-84FE-8A399231CB3D"), item0.Data.Key); + Assert.AreEqual("home", item0.Data.ContentType.Alias); var item1 = converted.Layout.ElementAt(1); - var item1Data = converted.GetData(item1.Udi); - Assert.AreEqual(Guid.Parse("0A4A416E-547D-464F-ABCC-6F345C17809A"), item1Data.Key); - Assert.AreEqual("home", item1Data.ContentType.Alias); + Assert.AreEqual(Guid.Parse("0A4A416E-547D-464F-ABCC-6F345C17809A"), item1.Data.Key); + Assert.AreEqual("home", item1.Data.ContentType.Alias); } diff --git a/src/Umbraco.Web/PropertyEditors/ValueConverters/BlockListPropertyValueConverter.cs b/src/Umbraco.Web/PropertyEditors/ValueConverters/BlockListPropertyValueConverter.cs index 1e013e851c..d4e130cc0d 100644 --- a/src/Umbraco.Web/PropertyEditors/ValueConverters/BlockListPropertyValueConverter.cs +++ b/src/Umbraco.Web/PropertyEditors/ValueConverters/BlockListPropertyValueConverter.cs @@ -114,7 +114,7 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters if (element != null && string.IsNullOrWhiteSpace(blockConfig.SettingsElementTypeAlias)) element = null; - var layoutRef = new BlockListLayoutReference(udi, element); + var layoutRef = new BlockListLayoutReference(udi, data, element); layout.Add(layoutRef); } From 1b53b5c93869a88e0b6892e9b7ff7775c7bd28c0 Mon Sep 17 00:00:00 2001 From: Warren Buckley Date: Thu, 2 Apr 2020 11:13:36 +0100 Subject: [PATCH 098/377] Use the .Data propertry as opposed to GetData in this PartialView --- src/Umbraco.Web.UI/Views/Partials/BlockList/Default.cshtml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI/Views/Partials/BlockList/Default.cshtml b/src/Umbraco.Web.UI/Views/Partials/BlockList/Default.cshtml index a9f66d7419..d8a304826f 100644 --- a/src/Umbraco.Web.UI/Views/Partials/BlockList/Default.cshtml +++ b/src/Umbraco.Web.UI/Views/Partials/BlockList/Default.cshtml @@ -8,7 +8,7 @@ @foreach (var layout in Model.Layout) { if (layout?.Udi == null) { continue; } - var data = Model.GetData(layout.Udi); + var data = layout.Data; @Html.Partial("BlockList/" + data.ContentType.Alias, (data, layout.Settings)) }
    From 5dfa998109a684439486d32c84c2a1c05764dcc6 Mon Sep 17 00:00:00 2001 From: Benjamin Carleski Date: Thu, 2 Apr 2020 14:29:52 -0700 Subject: [PATCH 099/377] Fix block list test failures --- .../BlockListPropertyValueConverterTests.cs | 32 +++++++++++-------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/src/Umbraco.Tests/PropertyEditors/BlockListPropertyValueConverterTests.cs b/src/Umbraco.Tests/PropertyEditors/BlockListPropertyValueConverterTests.cs index f63485f4bf..6a7ec33a5a 100644 --- a/src/Umbraco.Tests/PropertyEditors/BlockListPropertyValueConverterTests.cs +++ b/src/Umbraco.Tests/PropertyEditors/BlockListPropertyValueConverterTests.cs @@ -24,11 +24,15 @@ namespace Umbraco.Tests.PropertyEditors /// private IPublishedSnapshotAccessor GetPublishedSnapshotAccessor() { - var homeContentType = Mock.Of(x => + var test1ContentType = Mock.Of(x => x.IsElement == true - && x.Alias == "home"); + && x.Alias == "Test1"); + var test2ContentType = Mock.Of(x => + x.IsElement == true + && x.Alias == "Test2"); var contentCache = new Mock(); - contentCache.Setup(x => x.GetContentType("home")).Returns(homeContentType); + contentCache.Setup(x => x.GetContentType("Test1")).Returns(test1ContentType); + contentCache.Setup(x => x.GetContentType("Test2")).Returns(test2ContentType); var publishedSnapshot = Mock.Of(x => x.Content == contentCache.Object); var publishedSnapshotAccessor = Mock.Of(x => x.PublishedSnapshot == publishedSnapshot); return publishedSnapshotAccessor; @@ -254,8 +258,8 @@ data: []}"; }, data: [ { - 'contentTypeAlias': 'home', - 'key': '1304E1DD-AC87-4396-84FE-8A399231CB3D' + 'contentTypeAlias': 'Test1', + 'udi': 'umb://element/1304E1DDAC87439684FE8A399231CB3D' } ] }"; @@ -265,7 +269,7 @@ data: []}"; Assert.AreEqual(1, converted.Data.Count()); var item0 = converted.Data.ElementAt(0); Assert.AreEqual(Guid.Parse("1304E1DD-AC87-4396-84FE-8A399231CB3D"), item0.Key); - Assert.AreEqual("home", item0.ContentType.Alias); + Assert.AreEqual("Test1", item0.ContentType.Alias); Assert.AreEqual(1, converted.Layout.Count()); var layout0 = converted.Layout.ElementAt(0); Assert.IsNull(layout0.Settings); @@ -296,16 +300,16 @@ data: []}"; }, data: [ { - 'contentTypeAlias': 'home', - 'key': '1304E1DD-AC87-4396-84FE-8A399231CB3D' + 'contentTypeAlias': 'Test1', + 'udi': 'umb://element/1304E1DDAC87439684FE8A399231CB3D' }, { - 'contentTypeAlias': 'home', - 'key': 'E05A0347-0442-4AB3-A520-E048E6197E79' + 'contentTypeAlias': 'Test2', + 'udi': 'umb://element/E05A034704424AB3A520E048E6197E79' }, { - 'contentTypeAlias': 'home', - 'key': '0A4A416E-547D-464F-ABCC-6F345C17809A' + 'contentTypeAlias': 'Test2', + 'udi': 'umb://element/0A4A416E547D464FABCC6F345C17809A' } ] }"; @@ -318,11 +322,11 @@ data: []}"; var item0 = converted.Layout.ElementAt(0); Assert.AreEqual(Guid.Parse("1304E1DD-AC87-4396-84FE-8A399231CB3D"), item0.Data.Key); - Assert.AreEqual("home", item0.Data.ContentType.Alias); + Assert.AreEqual("Test1", item0.Data.ContentType.Alias); var item1 = converted.Layout.ElementAt(1); Assert.AreEqual(Guid.Parse("0A4A416E-547D-464F-ABCC-6F345C17809A"), item1.Data.Key); - Assert.AreEqual("home", item1.Data.ContentType.Alias); + Assert.AreEqual("Test2", item1.Data.ContentType.Alias); } From a87a6caf85ecc0b821e089b47451089e365b15f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Fri, 3 Apr 2020 12:36:11 +0200 Subject: [PATCH 100/377] Just parsing layout as model for partial views. --- .../Views/Partials/BlockList/Default.cshtml | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/src/Umbraco.Web.UI/Views/Partials/BlockList/Default.cshtml b/src/Umbraco.Web.UI/Views/Partials/BlockList/Default.cshtml index d8a304826f..e8a03a7dcd 100644 --- a/src/Umbraco.Web.UI/Views/Partials/BlockList/Default.cshtml +++ b/src/Umbraco.Web.UI/Views/Partials/BlockList/Default.cshtml @@ -1,14 +1,14 @@ -@inherits UmbracoViewPage -@using ContentModels = Umbraco.Web.PublishedModels; -@using Umbraco.Core.Models.Blocks -@{ - if (Model?.Layout == null || !Model.Layout.Any()) { return; } -} -
    - @foreach (var layout in Model.Layout) - { - if (layout?.Udi == null) { continue; } - var data = layout.Data; - @Html.Partial("BlockList/" + data.ContentType.Alias, (data, layout.Settings)) - } -
    +@inherits UmbracoViewPage +@using ContentModels = Umbraco.Web.PublishedModels; +@using Umbraco.Core.Models.Blocks +@{ + if (Model?.Layout == null || !Model.Layout.Any()) { return; } +} +
    + @foreach (var layout in Model.Layout) + { + if (layout?.Udi == null) { continue; } + var data = layout.Data; + @Html.Partial("BlockList/" + data.ContentType.Alias, layout) + } +
    From fb175c5af845a589e6064e42bbe6714474087183 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Mon, 6 Apr 2020 09:32:03 +0200 Subject: [PATCH 101/377] minor adjustments --- .../imageblock.editor.controller.js | 21 +++++++++++++++++++ .../imageblock/imageblock.editor.html | 10 +++++++-- .../imageblock/imageblock.editor.less | 7 +++++-- .../textareablock.editor.controller.js | 3 ++- .../textareablock/textareablock.editor.html | 2 +- .../textareablock/textareablock.editor.less | 4 ++-- .../blocklist/blocklist.component.js | 4 ++-- 7 files changed, 41 insertions(+), 10 deletions(-) create mode 100644 src/Umbraco.Web.UI.Client/src/views/blockelements/imageblock/imageblock.editor.controller.js diff --git a/src/Umbraco.Web.UI.Client/src/views/blockelements/imageblock/imageblock.editor.controller.js b/src/Umbraco.Web.UI.Client/src/views/blockelements/imageblock/imageblock.editor.controller.js new file mode 100644 index 0000000000..8ebf7ec8c4 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/blockelements/imageblock/imageblock.editor.controller.js @@ -0,0 +1,21 @@ + +(function () { + 'use strict'; + + function ImageBlockEditor($scope, entityResource) { + + const bc = this; + + var firstProperty = $scope.block.content.variants[0].tabs[0].properties[0]; + + entityResource.getById(firstProperty.value, "Media").then(function(ent) { + console.log(ent) + bc.imageUrl = ent.metaData.MediaPath; + }); + + } + + angular.module("umbraco").controller("Umbraco.PropertyEditors.BlockEditor.ImageBlockEditor", ImageBlockEditor); + +})(); + \ No newline at end of file diff --git a/src/Umbraco.Web.UI.Client/src/views/blockelements/imageblock/imageblock.editor.html b/src/Umbraco.Web.UI.Client/src/views/blockelements/imageblock/imageblock.editor.html index a1977cec55..32db64b16b 100644 --- a/src/Umbraco.Web.UI.Client/src/views/blockelements/imageblock/imageblock.editor.html +++ b/src/Umbraco.Web.UI.Client/src/views/blockelements/imageblock/imageblock.editor.html @@ -1,3 +1,9 @@ - diff --git a/src/Umbraco.Web.UI.Client/src/views/blockelements/imageblock/imageblock.editor.less b/src/Umbraco.Web.UI.Client/src/views/blockelements/imageblock/imageblock.editor.less index 99b1bb53f2..2ea03bd703 100644 --- a/src/Umbraco.Web.UI.Client/src/views/blockelements/imageblock/imageblock.editor.less +++ b/src/Umbraco.Web.UI.Client/src/views/blockelements/imageblock/imageblock.editor.less @@ -1,10 +1,13 @@ .blockelement-imageblock-editor { - padding-top: 2px; - padding-bottom: 2px; + width: 100%; + min-height: 42px; + padding-bottom: 10px; + padding-top: 10px; img { width: 100%; + max-width: 500px; border: none; resize: none; border-radius: @baseBorderRadius; diff --git a/src/Umbraco.Web.UI.Client/src/views/blockelements/textareablock/textareablock.editor.controller.js b/src/Umbraco.Web.UI.Client/src/views/blockelements/textareablock/textareablock.editor.controller.js index 9fc213cd4a..1b074a0cb6 100644 --- a/src/Umbraco.Web.UI.Client/src/views/blockelements/textareablock/textareablock.editor.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/blockelements/textareablock/textareablock.editor.controller.js @@ -5,7 +5,7 @@ angular.module("umbraco") var vm = this; - vm.firstProperty = $scope.blockItem.content.variants[0].tabs[0].properties[0]; + vm.firstProperty = $scope.block.content.variants[0].tabs[0].properties[0]; /* vm.onBlur = function() { if (vm.firstProperty.value === null || vm.firstProperty.value === "") { @@ -13,6 +13,7 @@ angular.module("umbraco") } } */ + // TODO: if text is empty and user hits backspace, then remove this block. } ); diff --git a/src/Umbraco.Web.UI.Client/src/views/blockelements/textareablock/textareablock.editor.html b/src/Umbraco.Web.UI.Client/src/views/blockelements/textareablock/textareablock.editor.html index bbcef1643b..ca49a3f5b8 100644 --- a/src/Umbraco.Web.UI.Client/src/views/blockelements/textareablock/textareablock.editor.html +++ b/src/Umbraco.Web.UI.Client/src/views/blockelements/textareablock/textareablock.editor.html @@ -1,5 +1,5 @@ -
    +
    diff --git a/src/Umbraco.Web.UI.Client/src/views/blockelements/textareablock/textareablock.editor.less b/src/Umbraco.Web.UI.Client/src/views/blockelements/textareablock/textareablock.editor.less index 492025274a..fe11d0cd0c 100644 --- a/src/Umbraco.Web.UI.Client/src/views/blockelements/textareablock/textareablock.editor.less +++ b/src/Umbraco.Web.UI.Client/src/views/blockelements/textareablock/textareablock.editor.less @@ -1,8 +1,8 @@ .blockelement-textareablock-editor { width: 100%; - padding-bottom: 24px; - padding-top: 24px; + padding-bottom: 10px; + padding-top: 10px; padding-left: 24px; padding-right: 24px; diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.js index c7ac475d46..2b25a8b013 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.js @@ -121,7 +121,7 @@ if (block === null) return null; // Lets apply fallback views, and make the view available directly on the blockModel. - block.view = block.config.view || vm.model.config.useInlineEditingAsDefault ? "views/blockelements/inlineblock/inlineblock.editor.html" : "views/blockelements/labelblock/labelblock.editor.html"; + block.view = block.config.view || (vm.model.config.useInlineEditingAsDefault ? "views/blockelements/inlineblock/inlineblock.editor.html" : "views/blockelements/labelblock/labelblock.editor.html"); block.showSettings = block.config.settingsElementTypeAlias != null; @@ -256,7 +256,7 @@ added = addNewBlock(createIndex, model.selectedItem.alias); } vm.blockTypePicker.close(); - if (added && vm.blocks.length > createIndex) { + if (added && vm.model.config.useInlineEditingAsDefault !== true && vm.blocks.length > createIndex) { editBlock(vm.blocks[createIndex]); } }, From 0fa045dc942234fcf3a3cc3ae001203a61c4afd9 Mon Sep 17 00:00:00 2001 From: Benjamin Carleski Date: Tue, 7 Apr 2020 11:53:41 -0700 Subject: [PATCH 102/377] Remove DB migrations so that they can be reviewed as a block --- src/Umbraco.Core/Migrations/Upgrade/UmbracoPlan.cs | 3 --- src/Umbraco.Core/Umbraco.Core.csproj | 1 - 2 files changed, 4 deletions(-) diff --git a/src/Umbraco.Core/Migrations/Upgrade/UmbracoPlan.cs b/src/Umbraco.Core/Migrations/Upgrade/UmbracoPlan.cs index f0fcdc3d52..f65a60197f 100644 --- a/src/Umbraco.Core/Migrations/Upgrade/UmbracoPlan.cs +++ b/src/Umbraco.Core/Migrations/Upgrade/UmbracoPlan.cs @@ -194,9 +194,6 @@ namespace Umbraco.Core.Migrations.Upgrade To("{a78e3369-8ea3-40ec-ad3f-5f76929d2b20}"); - - // to 8.7.0... - To("{DFA35FA2-BFBB-433F-84E5-BD75940CDDF6}"); //FINAL } } diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index 73bd8a1126..18c51fd905 100755 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -131,7 +131,6 @@ - From f78e4fcdf67992baf7628c2053bb9347e73ca2be Mon Sep 17 00:00:00 2001 From: Benjamin Carleski Date: Tue, 7 Apr 2020 11:55:45 -0700 Subject: [PATCH 103/377] Add migrations for new block editor --- .../Migrations/Upgrade/UmbracoPlan.cs | 5 + .../Upgrade/V_8_7_0/ColorPickerPreValues.cs | 106 ++++++++++++++++++ .../Upgrade/V_8_7_0/ConvertToElements.cs | 92 +++++++++++++++ .../V_8_7_0/StackedContentToBlockList.cs | 74 ++++++++++-- src/Umbraco.Core/Umbraco.Core.csproj | 3 + 5 files changed, 268 insertions(+), 12 deletions(-) create mode 100644 src/Umbraco.Core/Migrations/Upgrade/V_8_7_0/ColorPickerPreValues.cs create mode 100644 src/Umbraco.Core/Migrations/Upgrade/V_8_7_0/ConvertToElements.cs diff --git a/src/Umbraco.Core/Migrations/Upgrade/UmbracoPlan.cs b/src/Umbraco.Core/Migrations/Upgrade/UmbracoPlan.cs index f65a60197f..1d30efa573 100644 --- a/src/Umbraco.Core/Migrations/Upgrade/UmbracoPlan.cs +++ b/src/Umbraco.Core/Migrations/Upgrade/UmbracoPlan.cs @@ -194,6 +194,11 @@ namespace Umbraco.Core.Migrations.Upgrade To("{a78e3369-8ea3-40ec-ad3f-5f76929d2b20}"); + + // to 8.7.0... + To("{DFA35FA2-BFBB-433F-84E5-BD75940CDDF6}"); + To("{711AC937-B11C-47AC-8D4A-5B8868A3C2C6}"); + To("{DA434576-3DEF-46D7-942A-CE34D7F7FB8A}"); //FINAL } } diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_7_0/ColorPickerPreValues.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_7_0/ColorPickerPreValues.cs new file mode 100644 index 0000000000..6e959e86d7 --- /dev/null +++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_7_0/ColorPickerPreValues.cs @@ -0,0 +1,106 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using System; +using System.Linq; +using System.Text.RegularExpressions; +using Umbraco.Core.Persistence; +using Umbraco.Core.Persistence.Dtos; +using Umbraco.Core.PropertyEditors; + +namespace Umbraco.Core.Migrations.Upgrade.V_8_7_0 +{ + public class ColorPickerPreValues : MigrationBase + { + private static readonly Regex OldPreValuesPattern1 = new Regex("\\s*{(\\s*\"[0-9]+\"\\s*:\\s*\"[0-9a-fA-F]+\"\\s*,)*\\s*\"useLabel\"\\s*:\\s*\"[01]\"\\s*}\\s*", RegexOptions.Compiled); + private static readonly Regex OldPreValuesPattern2 = new Regex("\\s*{(\\s*\"[0-9]+\"\\s*:\\s*{\\s*\"value\"\\s*:\\s*\"[0-9a-fA-F]+\"\\s*(,\\s*\"label\"\\s*:\\s*\"[^\"]*\"\\s*)?(,\\s*\"sortOrder\"\\s*:\\s*[0-9]+\\s*)?}\\s*,)*\\s*\"useLabel\"\\s*:\\s*\"[01]\"\\s*}\\s*", RegexOptions.Compiled); + + public ColorPickerPreValues(IMigrationContext context) : base(context) + { + } + + public override void Migrate() + { + var sql = Sql() + .Select() + .From() + .Where(d => d.EditorAlias == Constants.PropertyEditors.Aliases.ColorPicker); + + var dtos = Database.Fetch(sql); + + foreach (var dto in dtos) + { + if (dto.Configuration.IsNullOrWhiteSpace()) continue; + + if (OldPreValuesPattern1.IsMatch(dto.Configuration)) ConvertPreValues(dto, ConvertStyle1); + else if (OldPreValuesPattern2.IsMatch(dto.Configuration)) ConvertPreValues(dto, ConvertStyle2); + else continue; + + Database.Update(dto); + } + } + + private void ConvertPreValues(DataTypeDto dto, Func converter) + { + var obj = JObject.Parse(dto.Configuration); + var config = new ColorPickerConfiguration(); + var id = 0; + + foreach (var prop in obj.Properties()) + { + if (prop.Name.ToLowerInvariant() == "uselabel") + { + config.UseLabel = prop.Value.ToString() == "1"; + } + else + { + id++; + config.Items.Add(new ValueListConfiguration.ValueListItem + { + Id = id, + Value = JsonConvert.SerializeObject(converter(id, prop.Value)) + }); + } + } + + dto.Configuration = JsonConvert.SerializeObject(config); + } + + private ItemValue ConvertStyle1(int index, JToken token) + { + var value = token.ToString(); + return new ItemValue + { + Color = value, + Label = value, + SortOrder = index + }; + } + + private ItemValue ConvertStyle2(int index, JToken token) + { + var obj = (JObject)token; + var value = obj["value"].ToString(); + var label = obj["label"]?.ToString(); + var order = obj["sortOrder"]?.ToString(); + + return new ItemValue + { + Color = value, + Label = label.IsNullOrWhiteSpace() ? value : label, + SortOrder = int.TryParse(order, out var o) ? o : index + }; + } + + private class ItemValue + { + [JsonProperty("value")] + public string Color { get; set; } + + [JsonProperty("label")] + public string Label { get; set; } + + [JsonProperty("sortOrder")] + public int SortOrder { get; set; } + } + } +} diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_7_0/ConvertToElements.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_7_0/ConvertToElements.cs new file mode 100644 index 0000000000..e42453a3fe --- /dev/null +++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_7_0/ConvertToElements.cs @@ -0,0 +1,92 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Umbraco.Core.Persistence; +using Umbraco.Core.Persistence.Dtos; + +namespace Umbraco.Core.Migrations.Upgrade.V_8_7_0 +{ + public class ConvertToElements : MigrationBase + { + public ConvertToElements(IMigrationContext context) : base(context) + { + } + + public override void Migrate() + { + // Get all document type IDs by alias + var docTypes = Database.Fetch(); + var docTypeMap = new Dictionary(docTypes.Count); + docTypes.ForEach(d => docTypeMap[d.Alias] = d.NodeId); + + // Find all Nested Content or Block List data types + var dataTypes = GetDataTypes(Constants.PropertyEditors.Aliases.NestedContent, Constants.PropertyEditors.Aliases.BlockList); + + // Find all document types listed in each + var elementTypeIds = dataTypes.SelectMany(d => GetDocTypeIds(d.Configuration, docTypeMap)).ToList(); + + // Find all compositions those document types use + var parentElementTypeIds = Database.Fetch(Sql() + .Select() + .From() + .WhereIn(c => c.ChildId, elementTypeIds) + ).Select(c => c.ParentId); + + elementTypeIds = elementTypeIds.Union(parentElementTypeIds).ToList(); + + // Convert all those document types to element type + foreach (var docType in docTypes) + { + if (!elementTypeIds.Contains(docType.NodeId)) continue; + + docType.IsElement = true; + Database.Update(docType); + } + } + + private List GetDataTypes(params string[] aliases) + { + var sql = Sql() + .Select() + .From() + .WhereIn(d => d.EditorAlias, aliases); + + return Database.Fetch(sql); + } + + private IEnumerable GetDocTypeIds(string configuration, Dictionary idMap) + { + if (configuration.IsNullOrWhiteSpace() || configuration[0] != '{') return Enumerable.Empty(); + + var obj = JObject.Parse(configuration); + if (obj["contentTypes"] is JArray ncArr) + { + var arr = ncArr.ToObject(); + return arr.Select(i => idMap.TryGetValue(i.Alias, out var id) ? id : 0).Where(i => i != 0); + } + else if (obj["blocks"] is JArray blArr) + { + var arr = blArr.ToObject(); + return arr.Select(i => idMap.TryGetValue(i.Alias, out var id) ? id : 0).Where(i => i != 0); + } + + return Enumerable.Empty(); + } + + public class ContentType + { + [JsonProperty("ncAlias")] + public string Alias { get; set; } + } + + public class BlockConfiguration + { + [JsonProperty("contentTypeAlias")] + public string Alias { get; set; } + } + } +} diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_7_0/StackedContentToBlockList.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_7_0/StackedContentToBlockList.cs index 076b4f205c..43ee85c2ea 100644 --- a/src/Umbraco.Core/Migrations/Upgrade/V_8_7_0/StackedContentToBlockList.cs +++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_7_0/StackedContentToBlockList.cs @@ -20,6 +20,7 @@ namespace Umbraco.Core.Migrations.Upgrade.V_8_7_0 public override void Migrate() { + // Convert all Stacked Content properties to Block List properties, both in the data types and in the property data var refreshCache = Migrate(GetDataTypes("Our.Umbraco.StackedContent"), GetKnownDocumentTypes()); // if some data types have been updated directly in the database (editing DataTypeDto and/or PropertyDataDto), @@ -38,7 +39,7 @@ namespace Umbraco.Core.Migrations.Upgrade.V_8_7_0 return Database.Fetch(sql); } - private Dictionary GetKnownDocumentTypes() + private Dictionary GetKnownDocumentTypes() { var sql = Sql() .Select(r => r.Select(x => x.NodeDto)) @@ -47,12 +48,40 @@ namespace Umbraco.Core.Migrations.Upgrade.V_8_7_0 .On(c => c.NodeId, n => n.NodeId); var types = Database.Fetch(sql); - var map = new Dictionary(types.Count); - types.ForEach(t => map[t.NodeDto.UniqueId] = t.Alias); - return map; + var typeMap = new Dictionary(types.Count); + types.ForEach(t => typeMap[t.NodeId] = t); + + sql = Sql() + .Select() + .From(); + var joins = Database.Fetch(sql); + // Find all relationships between types, either inherited or composited + var joinLk = joins + .Union(types + .Where(t => typeMap.ContainsKey(t.NodeDto.ParentId)) + .Select(t => new ContentType2ContentTypeDto { ChildId = t.NodeId, ParentId = t.NodeDto.ParentId })) + .ToLookup(j => j.ChildId, j => j.ParentId); + + sql = Sql() + .Select(r => r.Select(x => x.DataTypeDto)) + .From() + .InnerJoin() + .On(c => c.DataTypeId, n => n.NodeId) + .Where(d => d.EditorAlias == Constants.PropertyEditors.Aliases.NestedContent); + var props = Database.Fetch(sql); + // Get all nested content property aliases by content type ID + var propLk = props.ToLookup(p => p.ContentTypeId, p => p.Alias); + + var knownMap = new Dictionary(types.Count); + types.ForEach(t => knownMap[t.NodeDto.UniqueId] = new KnownContentType + { + Alias = t.Alias, + NestedContentProperties = propLk[t.NodeId].Union(joinLk[t.NodeId].SelectMany(r => propLk[r])).ToArray() + }); + return knownMap; } - private bool Migrate(IEnumerable dataTypesToMigrate, Dictionary knownDocumentTypes) + private bool Migrate(IEnumerable dataTypesToMigrate, Dictionary knownDocumentTypes) { var refreshCache = false; @@ -73,14 +102,14 @@ namespace Umbraco.Core.Migrations.Upgrade.V_8_7_0 return refreshCache; } - private BlockListConfiguration UpdateConfiguration(DataTypeDto dataType, Dictionary knownDocumentTypes) + private BlockListConfiguration UpdateConfiguration(DataTypeDto dataType, Dictionary knownDocumentTypes) { var old = JsonConvert.DeserializeObject(dataType.Configuration); var config = new BlockListConfiguration { Blocks = old.ContentTypes?.Select(t => new BlockListConfiguration.BlockConfiguration { - Alias = knownDocumentTypes[t.IcContentTypeGuid], + Alias = knownDocumentTypes[t.IcContentTypeGuid].Alias, Label = t.NameTemplate }).ToArray(), UseInlineEditingAsDefault = old.SingleItemMode == "1" || old.SingleItemMode == bool.TrueString @@ -96,7 +125,7 @@ namespace Umbraco.Core.Migrations.Upgrade.V_8_7_0 return config; } - private void UpdatePropertyData(DataTypeDto dataType, BlockListConfiguration config, Dictionary knownDocumentTypes) + private void UpdatePropertyData(DataTypeDto dataType, BlockListConfiguration config, Dictionary knownDocumentTypes) { // get property data dtos var propertyDataDtos = Database.Fetch(Sql() @@ -115,7 +144,7 @@ namespace Umbraco.Core.Migrations.Upgrade.V_8_7_0 } - private bool UpdatePropertyDataDto(PropertyDataDto dto, BlockListConfiguration config, Dictionary knownDocumentTypes) + private bool UpdatePropertyDataDto(PropertyDataDto dto, BlockListConfiguration config, Dictionary knownDocumentTypes) { var model = new SimpleModel(); @@ -202,18 +231,33 @@ namespace Umbraco.Core.Migrations.Upgrade.V_8_7_0 [JsonProperty("data")] public List Data { get; } = new List(); - public void AddDataItem(JObject obj, Dictionary knownDocumentTypes) + public void AddDataItem(JObject obj, Dictionary knownDocumentTypes) { if (!Guid.TryParse(obj["key"].ToString(), out var key)) throw new ArgumentException("Could not find a valid key in the data item"); if (!Guid.TryParse(obj["icContentTypeGuid"].ToString(), out var ctGuid)) throw new ArgumentException("Could not find a valid content type GUID in the data item"); - if (!knownDocumentTypes.TryGetValue(ctGuid, out var ctAlias)) throw new ArgumentException($"Unknown content type GUID '{ctGuid}'"); + if (!knownDocumentTypes.TryGetValue(ctGuid, out var ct)) throw new ArgumentException($"Unknown content type GUID '{ctGuid}'"); obj.Remove("key"); obj.Remove("icContentTypeGuid"); var udi = new GuidUdi(Constants.UdiEntityType.Element, key).ToString(); obj["udi"] = udi; - obj["contentTypeAlias"] = ctAlias; + obj["contentTypeAlias"] = ct.Alias; + + if (ct.NestedContentProperties != null && ct.NestedContentProperties.Length > 0) + { + // Nested content inside a stacked content item used to be stored as a deserialized string of the JSON array + // Now we store the content as the raw JSON array, so we need to convert from the string form to the array + foreach (var prop in ct.NestedContentProperties) + { + var val = obj[prop]; + var value = val?.ToString(); + if (val != null && val.Type == JTokenType.String && !value.IsNullOrWhiteSpace() && value[0] == '[') + obj[prop] = JArray.Parse(value); + else if (val.Type != JTokenType.Array) + obj[prop] = new JArray(); + } + } Data.Add(obj); Layout.Refs.Add(new SimpleLayout.SimpleLayoutRef { Udi = udi }); @@ -231,5 +275,11 @@ namespace Umbraco.Core.Migrations.Upgrade.V_8_7_0 } } } + + private class KnownContentType + { + public string Alias { get; set; } + public string[] NestedContentProperties { get; set; } + } } } diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index 18c51fd905..1ae5267b6a 100755 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -131,6 +131,9 @@ + + + From cec7251bc3c1c5fb5d32704e144bc55361beae86 Mon Sep 17 00:00:00 2001 From: Benjamin Carleski Date: Tue, 7 Apr 2020 14:45:06 -0700 Subject: [PATCH 104/377] Update default rendering partial view --- src/Umbraco.Web.UI/Views/Partials/BlockList/Default.cshtml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Umbraco.Web.UI/Views/Partials/BlockList/Default.cshtml b/src/Umbraco.Web.UI/Views/Partials/BlockList/Default.cshtml index e8a03a7dcd..3bc6308b55 100644 --- a/src/Umbraco.Web.UI/Views/Partials/BlockList/Default.cshtml +++ b/src/Umbraco.Web.UI/Views/Partials/BlockList/Default.cshtml @@ -1,5 +1,4 @@ @inherits UmbracoViewPage -@using ContentModels = Umbraco.Web.PublishedModels; @using Umbraco.Core.Models.Blocks @{ if (Model?.Layout == null || !Model.Layout.Any()) { return; } @@ -9,6 +8,6 @@ { if (layout?.Udi == null) { continue; } var data = layout.Data; - @Html.Partial("BlockList/" + data.ContentType.Alias, layout) + @Html.Partial("BlockList/Components/" + data.ContentType.Alias, layout) }
    From cf418a8097fc00b3068ef7f81b494580ecf3e9e8 Mon Sep 17 00:00:00 2001 From: Benjamin Carleski Date: Tue, 7 Apr 2020 16:36:10 -0700 Subject: [PATCH 105/377] Add error handling to default template --- .../Views/Partials/BlockList/Default.cshtml | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI/Views/Partials/BlockList/Default.cshtml b/src/Umbraco.Web.UI/Views/Partials/BlockList/Default.cshtml index 3bc6308b55..22fd4283de 100644 --- a/src/Umbraco.Web.UI/Views/Partials/BlockList/Default.cshtml +++ b/src/Umbraco.Web.UI/Views/Partials/BlockList/Default.cshtml @@ -8,6 +8,13 @@ { if (layout?.Udi == null) { continue; } var data = layout.Data; - @Html.Partial("BlockList/Components/" + data.ContentType.Alias, layout) + try + { + @Html.Partial("BlockList/Components/" + data.ContentType.Alias, (data, layout.Settings)) + } + catch (Exception ex) + { + global::Umbraco.Core.Composing.Current.Logger.Error(typeof(BlockListModel), ex, "Could not display block list component for content type {0}", data?.ContentType?.Alias); + } }
    From d6c95bb38069ab15724cf898d374dcc0540ea597 Mon Sep 17 00:00:00 2001 From: Benjamin Carleski Date: Tue, 7 Apr 2020 16:43:06 -0700 Subject: [PATCH 106/377] Handle color picker data in stacked content --- .../V_8_7_0/StackedContentToBlockList.cs | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_7_0/StackedContentToBlockList.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_7_0/StackedContentToBlockList.cs index 43ee85c2ea..a3a8bd62c4 100644 --- a/src/Umbraco.Core/Migrations/Upgrade/V_8_7_0/StackedContentToBlockList.cs +++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_7_0/StackedContentToBlockList.cs @@ -67,16 +67,16 @@ namespace Umbraco.Core.Migrations.Upgrade.V_8_7_0 .From() .InnerJoin() .On(c => c.DataTypeId, n => n.NodeId) - .Where(d => d.EditorAlias == Constants.PropertyEditors.Aliases.NestedContent); + .WhereIn(d => d.EditorAlias, new[] { Constants.PropertyEditors.Aliases.NestedContent, Constants.PropertyEditors.Aliases.ColorPicker }); var props = Database.Fetch(sql); - // Get all nested content property aliases by content type ID + // Get all nested content and color picker property aliases by content type ID var propLk = props.ToLookup(p => p.ContentTypeId, p => p.Alias); var knownMap = new Dictionary(types.Count); types.ForEach(t => knownMap[t.NodeDto.UniqueId] = new KnownContentType { Alias = t.Alias, - NestedContentProperties = propLk[t.NodeId].Union(joinLk[t.NodeId].SelectMany(r => propLk[r])).ToArray() + StringToRawProperties = propLk[t.NodeId].Union(joinLk[t.NodeId].SelectMany(r => propLk[r])).ToArray() }); return knownMap; } @@ -244,18 +244,16 @@ namespace Umbraco.Core.Migrations.Upgrade.V_8_7_0 obj["udi"] = udi; obj["contentTypeAlias"] = ct.Alias; - if (ct.NestedContentProperties != null && ct.NestedContentProperties.Length > 0) + if (ct.StringToRawProperties != null && ct.StringToRawProperties.Length > 0) { // Nested content inside a stacked content item used to be stored as a deserialized string of the JSON array // Now we store the content as the raw JSON array, so we need to convert from the string form to the array - foreach (var prop in ct.NestedContentProperties) + foreach (var prop in ct.StringToRawProperties) { var val = obj[prop]; var value = val?.ToString(); - if (val != null && val.Type == JTokenType.String && !value.IsNullOrWhiteSpace() && value[0] == '[') - obj[prop] = JArray.Parse(value); - else if (val.Type != JTokenType.Array) - obj[prop] = new JArray(); + if (val != null && val.Type == JTokenType.String && !value.IsNullOrWhiteSpace()) + obj[prop] = JsonConvert.DeserializeObject(value); } } @@ -279,7 +277,7 @@ namespace Umbraco.Core.Migrations.Upgrade.V_8_7_0 private class KnownContentType { public string Alias { get; set; } - public string[] NestedContentProperties { get; set; } + public string[] StringToRawProperties { get; set; } } } } From b26db075a3777f917d9e16cf2c4e5a9f7d4dfc71 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Wed, 8 Apr 2020 11:58:25 +0200 Subject: [PATCH 107/377] BlockList PreValue Editor opens configurations as overlay --- .../common/resources/elementtype.resource.js | 11 +- src/Umbraco.Web.UI.Client/src/less/belle.less | 5 +- .../src/less/variables.less | 2 + .../blockcard/blockcard.component.html | 11 + .../blockcard/blockcard.component.js | 28 ++ .../blockcard/blockcard.component.less | 120 +++++++ .../blockcard/umb-block-card-grid.less | 15 + .../blocklist/blocklist.component.less | 5 +- ...blocklist.blockconfiguration.controller.js | 208 ++++++++++++ .../blocklist.blockconfiguration.html | 30 ++ .../blocklist.blockconfiguration.less | 28 ++ ...t.blockconfiguration.overlay.controller.js | 190 +++++++++++ .../blocklist.blockconfiguration.overlay.html | 310 ++++++++++++++++++ ...blocklist.blockconfiguration.overlay.less} | 59 +--- .../blocklist.elementtypepicker.controller.js | 285 ---------------- .../prevalue/blocklist.elementtypepicker.html | 81 ----- src/Umbraco.Web.UI/Umbraco/config/lang/en.xml | 6 + .../Umbraco/config/lang/en_us.xml | 6 + .../Editors/ElementTypeController.cs | 29 ++ .../PropertyEditors/BlockListConfiguration.cs | 16 +- src/Umbraco.Web/Umbraco.Web.csproj | 1 + 21 files changed, 1022 insertions(+), 424 deletions(-) create mode 100644 src/Umbraco.Web.UI.Client/src/views/propertyeditors/blockeditor/blockcard/blockcard.component.html create mode 100644 src/Umbraco.Web.UI.Client/src/views/propertyeditors/blockeditor/blockcard/blockcard.component.js create mode 100644 src/Umbraco.Web.UI.Client/src/views/propertyeditors/blockeditor/blockcard/blockcard.component.less create mode 100644 src/Umbraco.Web.UI.Client/src/views/propertyeditors/blockeditor/blockcard/umb-block-card-grid.less create mode 100644 src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/blocklist.blockconfiguration.controller.js create mode 100644 src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/blocklist.blockconfiguration.html create mode 100644 src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/blocklist.blockconfiguration.less create mode 100644 src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/blocklist.blockconfiguration.overlay.controller.js create mode 100644 src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/blocklist.blockconfiguration.overlay.html rename src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/{blocklist.elementtypepicker.less => blocklist.blockconfiguration.overlay.less} (68%) delete mode 100644 src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/blocklist.elementtypepicker.controller.js delete mode 100644 src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/blocklist.elementtypepicker.html create mode 100644 src/Umbraco.Web/Editors/ElementTypeController.cs diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/elementtype.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/elementtype.resource.js index 680b75ac78..a235bcda3e 100644 --- a/src/Umbraco.Web.UI.Client/src/common/resources/elementtype.resource.js +++ b/src/Umbraco.Web.UI.Client/src/common/resources/elementtype.resource.js @@ -9,12 +9,11 @@ function elementTypeResource($q, $http, umbRequestHelper) { getAll: function () { - // TODO: Change this into a real api (ElementTypeApi). This is a temporary fix to get data. - var url = Umbraco.Sys.ServerVariables.umbracoSettings.umbracoPath + "/backoffice/UmbracoApi/NestedContent/GetContentTypes"; - return umbRequestHelper.resourcePromise( - $http.get(url), - 'Failed to retrieve content types' - ); + var url = Umbraco.Sys.ServerVariables.umbracoSettings.umbracoPath + "/backoffice/UmbracoApi/ElementType/GetAll"; + return umbRequestHelper.resourcePromise( + $http.get(url), + 'Failed to retrieve element types' + ); /* return umbRequestHelper.resourcePromise( diff --git a/src/Umbraco.Web.UI.Client/src/less/belle.less b/src/Umbraco.Web.UI.Client/src/less/belle.less index 14a62ae790..6c021203a5 100644 --- a/src/Umbraco.Web.UI.Client/src/less/belle.less +++ b/src/Umbraco.Web.UI.Client/src/less/belle.less @@ -198,8 +198,11 @@ // Property Editors +@import "../views/propertyeditors/blockeditor/blockcard/umb-block-card-grid.less"; +@import "../views/propertyeditors/blockeditor/blockcard/blockcard.component.less"; @import "../views/propertyeditors/blocklist/blocklist.component.less"; -@import "../views/propertyeditors/blocklist/prevalue/blocklist.elementtypepicker.less"; +@import "../views/propertyeditors/blocklist/prevalue/blocklist.blockconfiguration.less"; +@import "../views/propertyeditors/blocklist/prevalue/blocklist.blockconfiguration.overlay.less"; @import "../views/propertyeditors/notsupported/notsupported.less"; diff --git a/src/Umbraco.Web.UI.Client/src/less/variables.less b/src/Umbraco.Web.UI.Client/src/less/variables.less index b645e3113b..c320a31807 100644 --- a/src/Umbraco.Web.UI.Client/src/less/variables.less +++ b/src/Umbraco.Web.UI.Client/src/less/variables.less @@ -202,6 +202,7 @@ @ui-icon: @blueNight; @ui-icon-hover: @blueMid; +@ui-drop-area-color: @blueMidLight; // Scaffolding @@ -252,6 +253,7 @@ // Disabled this to keep consistency throughout the backoffice UI. Untill a better solution is thought up, this will do. @baseBorderRadius: 3px; // 2px; +@doubleBorderRadius: 6px; @borderRadiusLarge: 3px; // 6px; @borderRadiusSmall: 3px; // 3px; diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blockeditor/blockcard/blockcard.component.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blockeditor/blockcard/blockcard.component.html new file mode 100644 index 0000000000..c66879e864 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blockeditor/blockcard/blockcard.component.html @@ -0,0 +1,11 @@ + +
    + +
    +
    +
    +
    +
    + + + diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blockeditor/blockcard/blockcard.component.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blockeditor/blockcard/blockcard.component.js new file mode 100644 index 0000000000..98e7303219 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blockeditor/blockcard/blockcard.component.js @@ -0,0 +1,28 @@ +(function () { + "use strict"; + + angular + .module("umbraco") + .component("umbBlockCard", { + templateUrl: "views/propertyeditors/blockeditor/blockcard/blockcard.component.html", + controller: BlockCardController, + controllerAs: "vm", + transclude: true, + bindings: { + blockConfigModel: "<", + elementTypeModel: "<" + } + }); + + function BlockCardController() { + + var vm = this; + + vm.$onInit = function() { + console.log(vm.blockConfigModel); + console.log(vm.elementTypeModel); + } + + } + +})(); diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blockeditor/blockcard/blockcard.component.less b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blockeditor/blockcard/blockcard.component.less new file mode 100644 index 0000000000..5adbded9bb --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blockeditor/blockcard/blockcard.component.less @@ -0,0 +1,120 @@ +.umb-block-card, +umb-block-card { + position: relative; + display: inline-block; + width: 100%; + height: auto; + margin-right: 20px; + margin-bottom: 20px; + background-color: white; + border-radius: @doubleBorderRadius; + box-shadow: 0 1px 2px rgba(0,0,0,.2); + overflow: hidden; + + cursor: pointer; + + &.--isOpen { + &::after { + content: ""; + position: absolute; + border: 2px solid @ui-active-border; + border-radius: @doubleBorderRadius; + top:0; + bottom: 0; + left: 0; + right: 0; + } + } + + &.--sortable-placeholder { + &::after { + content: ""; + position: absolute; + background-color:rgba(@ui-drop-area-color, .05); + border: 2px solid rgba(@ui-drop-area-color, .1); + border-radius: @doubleBorderRadius; + box-shadow: 0 0 4px rgba(@ui-drop-area-color, 0.05); + top:0; + bottom: 0; + left: 0; + right: 0; + animation: umb-block-card--sortable-placeholder 400ms ease-in-out alternate infinite; + @keyframes umb-block-card--sortable-placeholder { + 0% { opacity: 1; } + 100% { opacity: 0.5; } + } + } + box-shadow: none; + } + + .__showcase { + position: relative; + width: 100%; + padding-bottom: 10/16*100%; + background-color: @gray-11; + .__icon { + position: absolute; + width: 100%; + height: 100%; + display: flex; + align-items: center; + justify-content: center; + font-size:42px; + } + } + + .__info { + width: 100%; + background-color: #fff; + padding-bottom: 6px; + + .__name { + font-weight: bold; + font-size: 14px; + color: @ui-action-type; + margin-left: 16px; + margin-top: 8px; + margin-bottom: -1px; + } + .__subname { + color: @gray-4; + font-size: 12px; + margin-left: 16px; + margin-bottom: -1px; + } + } + + &:hover { + .__info { + .__name { + color: @ui-action-type-hover; + } + } + } + + .__actions { + position: absolute; + top: 10px; + right: 0; + opacity: 0; + transition: opacity 120ms; + .__action { + display: inline-block; + border-radius: 50%; + width: 28px; + height: 28px; + margin-right: 10px; + background-color: white; + color:@ui-action-type; + &:hover { + color: @ui-action-type-hover; + } + } + } + &:hover, &:focus, &:focus-within { + .__actions { + opacity: 1; + } + } + +} diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blockeditor/blockcard/umb-block-card-grid.less b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blockeditor/blockcard/umb-block-card-grid.less new file mode 100644 index 0000000000..e4953999fd --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blockeditor/blockcard/umb-block-card-grid.less @@ -0,0 +1,15 @@ +.umb-block-card-grid { + /* FlexBox Fallback */ + display: flex; + flex-wrap: wrap; + > * { + flex: 1 1 240px; + } + + /* Grid Setup */ + display: grid; + grid-template-columns: repeat(auto-fill, minmax(200px, 1fr)); + grid-auto-rows: minmax(200px, auto); + grid-gap: 20px; + +} diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.less b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.less index d08c862f88..d118e6356c 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.less +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.less @@ -87,10 +87,9 @@ right: 0; left: 0; height: 2px; - animation: umb-block-list__block--create-button_before 800ms ease-in-out infinite; + animation: umb-block-list__block--create-button_before 400ms ease-in-out alternate infinite; @keyframes umb-block-list__block--create-button_before { - 0% { opacity: 0.5; } - 50% { opacity: 1; } + 0% { opacity: 1; } 100% { opacity: 0.5; } } } 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 new file mode 100644 index 0000000000..a058f465e0 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/blocklist.blockconfiguration.controller.js @@ -0,0 +1,208 @@ +/** + * @ngdoc controller + * @name Umbraco.Editors.BlockList.BlockConfigurationController + * @function + * + * @description + * The controller for the content type editor property settings dialog + */ + +(function () { + "use strict"; + + function TransferProperties(fromObject, toObject) { + for (var p in fromObject) { + toObject[p] = fromObject[p]; + } + } + + function BlockConfigurationController($scope, elementTypeResource, overlayService, localizationService, editorService) { + + var vm = this; + + vm.enableAddBlock = true; + vm.openBlock = null; + + function evaluateStatus() { + + if (!vm.elementTypes) return;// cancel if elementTypes isnt loaded jet. + + vm.enableAddBlock = vm.getAvailableElementTypes().length > 0; + + } + + function onInit() { + + if (!$scope.model.value) { + $scope.model.value = []; + } + + loadElementTypes(); + + } + + function loadElementTypes() { + return elementTypeResource.getAll().then(function (elementTypes) { + vm.elementTypes = elementTypes; + evaluateStatus(); + }); + } + + vm.requestRemoveBlockByIndex = function (index) { + localizationService.localizeMany(["general_delete", "blockEditor_confirmDeleteBlockMessage", "blockEditor_confirmDeleteBlockNotice"]).then(function (data) { + var contentElementType = vm.getElementTypeByAlias($scope.model.value[index].contentTypeAlias); + overlayService.confirmDelete({ + title: data[0], + content: localizationService.tokenReplace(data[1], [contentElementType.name]), + confirmMessage: data[2], + close: function () { + overlayService.close(); + }, + submit: function () { + vm.removeBlockByIndex(index); + overlayService.close(); + } + }); + }); + } + + vm.removeBlockByIndex = function (index) { + $scope.model.value.splice(index, 1); + }; + + vm.sortableOptions = { + "ui-floating": true, + items: "umb-block-card", + cursor: "grabbing", + placeholder: 'umb-block-card --sortable-placeholder' + }; + + + vm.getAvailableElementTypes = function () { + return vm.elementTypes.filter(function (type) { + return !$scope.model.value.find(function (entry) { + return type.alias === entry.contentTypeAlias; + }); + }); + }; + + vm.getElementTypeByAlias = function(alias) { + return _.find(vm.elementTypes, function (type) { + return type.alias === alias; + }); + }; + + vm.openAddDialog = function ($event, entry) { + + //we have to add the alias to the objects (they are stored as contentTypeAlias) + var selectedItems = _.each($scope.model.value, function (obj) { + obj.alias = obj.contentTypeAlias; + return obj; + }); + + var availableItems = vm.getAvailableElementTypes() + + var elemTypeSelectorOverlay = { + view: "itempicker", + title: "no title jet", + availableItems: availableItems, + selectedItems: selectedItems, + createNewItem: { + action: function() { + overlayService.close(); + vm.createElementTypeAndAdd(vm.addBlockFromElementTypeAlias); + }, + icon: "icon-add", + name: "Create new" + }, + position: "target", + event: $event, + size: availableItems.length < 7 ? "small" : "medium", + submit: function (overlay) { + vm.addBlockFromElementTypeAlias(overlay.selectedItem.alias); + overlayService.close(); + }, + close: function () { + overlayService.close(); + } + }; + + overlayService.open(elemTypeSelectorOverlay); + }; + + vm.createElementTypeAndAdd = function(callback) { + const editor = { + create: true, + infiniteMode: true, + isElement: true, + submit: function (model) { + loadElementTypes().then( function () { + callback(model.documentTypeAlias); + }); + editorService.close(); + }, + close: function () { + editorService.close(); + } + }; + editorService.documentTypeEditor(editor); + } + + vm.addBlockFromElementTypeAlias = function(alias) { + + var entry = { + "contentTypeAlias": alias, + "view": null, + "labelTemplate": "", + "settingsElementTypeAlias": null + }; + + $scope.model.value.push(entry); + }; + + + + + + vm.openBlockOverlay = function (block) { + + localizationService.localize("blockEditor_blockConfigurationOverlayTitle", [vm.getElementTypeByAlias(block.contentTypeAlias).name]).then(function (data) { + + var clonedBlockData = angular.copy(block); + vm.openBlock = block; + + var overlayModel = { + block: clonedBlockData, + title: data, + view: "views/propertyeditors/blocklist/prevalue/blocklist.blockconfiguration.overlay.html", + size: "small", + submit: function(overlayModel) { + TransferProperties(overlayModel.block, block);// transfer properties back to block object. (Doing this cause we dont know if block object is added to model jet, therefor we cant use index or replace the object.) + overlayModel.close(); + }, + close: function() { + editorService.close(); + vm.openBlock = null; + } + }; + + // open property settings editor + editorService.open(overlayModel); + + }); + + }; + + + + onInit(); + + $scope.$watchCollection('model.value', function(newVal, oldVal) { + evaluateStatus(); + }); + + } + + angular.module("umbraco").controller("Umbraco.PropertyEditors.BlockList.BlockConfigurationController", BlockConfigurationController); + +})(); diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/blocklist.blockconfiguration.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/blocklist.blockconfiguration.html new file mode 100644 index 0000000000..06e9714b77 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/blocklist.blockconfiguration.html @@ -0,0 +1,30 @@ +
    + +
    + + +
    + +
    +
    + + +
    +
    diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/blocklist.blockconfiguration.less b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/blocklist.blockconfiguration.less new file mode 100644 index 0000000000..f4d9caa73b --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/blocklist.blockconfiguration.less @@ -0,0 +1,28 @@ +.umb-block-list-block-configuration { + + .__add-button { + position: relative; + display: inline-flex; + width: 100%; + height: auto; + margin-right: 20px; + margin-bottom: 20px; + + color: @ui-action-discreet-type; + border: 1px dashed @ui-action-discreet-border; + border-radius: @doubleBorderRadius; + + align-items: center; + justify-content: center; + + padding: 5px 15px; + box-sizing: border-box; + font-weight: bold; + } + + .__add-button:hover { + color: @ui-action-discreet-type-hover; + border-color: @ui-action-discreet-border-hover; + } + +} diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/blocklist.blockconfiguration.overlay.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/blocklist.blockconfiguration.overlay.controller.js new file mode 100644 index 0000000000..015d25e9fb --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/blocklist.blockconfiguration.overlay.controller.js @@ -0,0 +1,190 @@ +/** + * @ngdoc controller + * @name Umbraco.Editors.BlockList.BlockConfigurationOverlayController + * @function + * + * @description + * The controller for the content type editor property settings dialog + */ + +(function () { + "use strict"; + + function BlockConfigurationOverlayController($scope, overlayService, localizationService, editorService, elementTypeResource) { + + var vm = this; + vm.block = $scope.model.block; + + loadElementTypes(); + + function loadElementTypes() { + return elementTypeResource.getAll().then(function (elementTypes) { + vm.elementTypes = elementTypes; + }); + } + + vm.getElementTypeByAlias = function(alias) { + return _.find(vm.elementTypes, function (type) { + return type.alias === alias; + }); + }; + + vm.openElementType = function(elementTypeAlias) { + var elementTypeId = vm.getElementTypeByAlias(elementTypeAlias).id; + const editor = { + id: elementTypeId, + submit: function (model) { + loadElementTypes(); + editorService.close(); + }, + close: function () { + editorService.close(); + } + }; + editorService.documentTypeEditor(editor); + } + + vm.addSettingsForBlock = function ($event, block) { + + var elemTypeSelectorOverlay = { + view: "itempicker", + title: "Pick settings (missing translation)", + availableItems: vm.elementTypes, + position: "target", + event: $event, + size: vm.elementTypes.length < 7 ? "small" : "medium", + createNewItem: { + action: function() { + overlayService.close(); + vm.createElementTypeAndAdd((alias) => { + vm.applySettingsToBlock(block, alias); + }); + }, + icon: "icon-add", + name: "Create new" + }, + submit: function (overlay) { + vm.applySettingsToBlock(block, overlay.selectedItem.alias); + overlayService.close(); + }, + close: function () { + overlayService.close(); + } + }; + + overlayService.open(elemTypeSelectorOverlay); + }; + vm.applySettingsToBlock = function(block, alias) { + block.settingsElementTypeAlias = alias; + }; + + vm.requestRemoveSettingsForBlock = function(block) { + localizationService.localizeMany(["general_remove", "defaultdialogs_confirmremoveusageof"]).then(function (data) { + + var settingsElementType = vm.getElementTypeByAlias(entry.settingsElementTypeAlias); + + overlayService.confirmRemove({ + title: data[0], + content: localizationService.tokenReplace(data[1], [settingsElementType.name]), + close: function () { + overlayService.close(); + }, + submit: function () { + vm.removeSettingsForEntry(entry); + overlayService.close(); + } + }); + }); + }; + vm.removeSettingsForEntry = function(entry) { + entry.settingsElementTypeAlias = null; + }; + + + vm.addViewForBlock = function(block) { + const filePicker = { + title: "Select view (TODO need translation)", + section: "settings", + treeAlias: "files", + entityType: "file", + isDialog: true, + filter: function (i) { + return i.name.indexOf(".html" !== -1); + }, + select: function (file) { + console.log(file); + block.view = file.name; + editorService.close(); + }, + close: function () { + editorService.close(); + } + }; + editorService.treePicker(filePicker); + } + vm.requestRemoveViewForBlock = function(block) { + localizationService.localizeMany(["general_remove", "defaultdialogs_confirmremoveusageof"]).then(function (data) { + overlayService.confirmRemove({ + title: data[0], + content: localizationService.tokenReplace(data[1], [block.view]), + close: function () { + overlayService.close(); + }, + submit: function () { + vm.removeViewForBlock(block); + overlayService.close(); + } + }); + }); + }; + vm.removeViewForBlock = function(block) { + block.view = null; + }; + + + + vm.addThumbnailForBlock = function(block) { + const thumbnailPicker = { + title: "Select thumbnail (TODO need translation)", + section: "settings", + treeAlias: "files", + entityType: "file", + isDialog: true, + filter: function (i) { + return (i.name.indexOf(".jpg") !== -1 || i.name.indexOf(".jpeg") !== -1 || i.name.indexOf(".png") !== -1 || i.name.indexOf(".svg") !== -1 || i.name.indexOf(".webp") !== -1 || i.name.indexOf(".gif") !== -1); + }, + select: function (file) { + block.thumbnail = file.name; + editorService.close(); + }, + close: function () { + editorService.close(); + } + }; + editorService.treePicker(thumbnailPicker); + } + vm.removeThumbnailForBlock = function(entry) { + entry.thumbnail = null; + }; + + + + + vm.submit = function () { + if ($scope.model && $scope.model.submit) { + $scope.model.submit($scope.model); + } + } + + vm.close = function() { + if ($scope.model && $scope.model.close) { + // TODO: If content has changed, we should notify user. + $scope.model.close($scope.model); + } + } + + } + + angular.module("umbraco").controller("Umbraco.PropertyEditors.BlockList.BlockConfigurationOverlayController", BlockConfigurationOverlayController); + +})(); diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/blocklist.blockconfiguration.overlay.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/blocklist.blockconfiguration.overlay.html new file mode 100644 index 0000000000..255d27cb8d --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/blocklist.blockconfiguration.overlay.html @@ -0,0 +1,310 @@ +
    + +
    + + + + + + + +
    + +
    + +
    +
    Block Editor
    +
    + +
    + + +
    +
    + +
    + +
    +
    +
    + + +
    +
    + +
    +
    + + +
    + +
    +
    + +
    +
    +
    + + +
    +
    + +
    + +
    +
    +
    + +
    + +
    + +
    + +
    +
    Data Settings
    +
    + +
    + + +
    +
    + +
    +
    + {{ contentPreview = vm.getElementTypeByAlias(vm.block.contentTypeAlias); "" }} + +
    + +
    +
    +
    +
    +
    + + +
    +
    + +
    +
    + {{ settingsPreview = vm.getElementTypeByAlias(vm.block.settingsElementTypeAlias); "" }} + +
    + + +
    +
    + +
    +
    +
    + + +
    +
    + +
    + +
    +
    Showcase
    +
    + +
    + + +
    +
    + +
    + +
    +
    +
    + + +
    +
    + +
    + +
    +
    +
    + + +
    +
    + +
    +
    + + +
    + +
    +
    + +
    +
    +
    + +
    + +
    + +
    +
    + + + + + + + + + + + + + + +
    +
    +
    + + + diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/blocklist.elementtypepicker.less b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/blocklist.blockconfiguration.overlay.less similarity index 68% rename from src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/blocklist.elementtypepicker.less rename to src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/blocklist.blockconfiguration.overlay.less index 216856dee9..c4b6349747 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/blocklist.elementtypepicker.less +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/blocklist.blockconfiguration.overlay.less @@ -1,46 +1,11 @@ -.umb-block-list-element-type-picker { +.umb-block-list-block-configuration-overlay { - .block-entry { - cursor: grab; - background-color: white; - border-radius: @baseBorderRadius; - } - - .umb-table { - border:1px solid @gray-11; - } - - .umb-table-head { - button { - margin-left: 5px; - color: @ui-action-discreet-type; - &:hover { - color: @ui-action-discreet-type-hover; - } - } - } - - .umb-table-cell { - padding-left: 10px; - padding-right: 0; - &.action-cell { - padding-right: 15px; - } - } - - .action-cell { - flex: 0 0 30px; - } - - .text-input { - width: 100%; - } .umb-node-preview { flex-grow: 1; } - .cell-actions { + .__control-actions { position: absolute; display: flex; align-items: center; @@ -51,14 +16,14 @@ opacity: 0; transition: opacity 120ms; } - .umb-table-cell:hover, - .umb-table-cell:focus, - .umb-table-cell:focus-within { - .cell-actions { + .control-group:hover, + .control-group:focus, + .control-group:focus-within { + .__control-actions { opacity: 1; } } - .cell-actions-btn { + .__control-actions-btn { position: relative; color: @ui-action-discreet-type; height: 30px; @@ -76,14 +41,15 @@ border-bottom: none; } - .settings-input { + .__settings-input { position: relative; padding: 5px 8px; + margin-bottom: 10px; color: @ui-action-discreet-type; border: 1px dashed @ui-action-discreet-border; width: 100%; font-weight: bold; - display: flex; + display: inline-flex; flex-flow: row nowrap; localize { @@ -93,6 +59,7 @@ .umb-node-preview { padding: 3px 0; margin-left: 5px; + overflow: hidden; } &.--noValue { @@ -112,7 +79,7 @@ } } - .add-button { + .__add-button { width:100%; color: @ui-action-discreet-type; border: 1px dashed @ui-action-discreet-border; @@ -126,7 +93,7 @@ font-weight: bold; } - .add-button:hover { + .__add-button:hover { color: @ui-action-discreet-type-hover; border-color: @ui-action-discreet-border-hover; } diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/blocklist.elementtypepicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/blocklist.elementtypepicker.controller.js deleted file mode 100644 index 77751579c9..0000000000 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/blocklist.elementtypepicker.controller.js +++ /dev/null @@ -1,285 +0,0 @@ -/** - * @ngdoc controller - * @name Umbraco.Editors.PropertySettingsController - * @function - * - * @description - * The controller for the content type editor property settings dialog - */ - -(function () { - "use strict"; - - function ElementTypePickerController($scope, elementTypeResource, overlayService, localizationService, editorService) { - - var vm = this; - - vm.enableAddEntry = true; - - function evaluateStatus() { - - if (!vm.elementTypes) return;// cancel if elementTypes isnt loaded jet. - - vm.enableAddEntry = vm.getAvailableElementTypes().length > 0; - - } - - function onInit() { - - if (!$scope.model.value) { - $scope.model.value = []; - } - - localizationService.localize("content_nestedContentSelectElementTypeModalTitle").then(function (value) { - //selectElementTypeModalTitle = value; - }); - - loadElementTypes(); - - } - - function loadElementTypes() { - return elementTypeResource.getAll().then(function (elementTypes) { - vm.elementTypes = elementTypes; - evaluateStatus(); - }); - } - - vm.requestRemoveEntryByIndex = function (index) { - localizationService.localizeMany(["general_delete", "blockEditor_confirmDeleteBlockMessage", "blockEditor_confirmDeleteBlockNotice"]).then(function (data) { - var contentElementType = vm.getElementTypeByAlias($scope.model.value[index].contentTypeAlias); - overlayService.confirmDelete({ - title: data[0], - content: localizationService.tokenReplace(data[1], [contentElementType.name]), - confirmMessage: data[2], - close: function () { - overlayService.close(); - }, - submit: function () { - vm.removeEntryByIndex(index); - overlayService.close(); - } - }); - }); - } - - vm.removeEntryByIndex = function (index) { - $scope.model.value.splice(index, 1); - }; - - vm.sortableOptions = { - axis: "y", - cursor: "grabbing", - placeholder: 'sortable-placeholder', - forcePlaceholderSize: true - }; - - - vm.getAvailableElementTypes = function () { - return vm.elementTypes.filter(function (type) { - return !$scope.model.value.find(function (entry) { - return type.alias === entry.contentTypeAlias; - }); - }); - }; - - vm.getElementTypeByAlias = function(alias) { - return _.find(vm.elementTypes, function (type) { - return type.alias === alias; - }); - }; - - vm.openAddDialog = function ($event, entry) { - - //we have to add the alias to the objects (they are stored as contentTypeAlias) - var selectedItems = _.each($scope.model.value, function (obj) { - obj.alias = obj.contentTypeAlias; - return obj; - }); - - var availableItems = vm.getAvailableElementTypes() - - var elemTypeSelectorOverlay = { - view: "itempicker", - title: "no title jet", - availableItems: availableItems, - selectedItems: selectedItems, - createNewItem: { - action: function() { - overlayService.close(); - vm.createElementTypeAndAdd(vm.addEntryFromElementTypeAlias); - }, - icon: "icon-add", - name: "Create new" - }, - position: "target", - event: $event, - size: availableItems.length < 7 ? "small" : "medium", - submit: function (overlay) { - vm.addEntryFromElementTypeAlias(overlay.selectedItem.alias); - overlayService.close(); - }, - close: function () { - overlayService.close(); - } - }; - - overlayService.open(elemTypeSelectorOverlay); - }; - - vm.createElementTypeAndAdd = function(callback) { - const editor = { - create: true, - infiniteMode: true, - isElement: true, - submit: function (model) { - console.log(model) - loadElementTypes().then( function () { - callback(model.documentTypeAlias); - }); - editorService.close(); - }, - close: function () { - editorService.close(); - } - }; - editorService.documentTypeEditor(editor); - } - - vm.addEntryFromElementTypeAlias = function(alias) { - - var entry = { - "contentTypeAlias": alias, - "view": null, - "labelTemplate": "", - "settingsElementTypeAlias": null - }; - - $scope.model.value.push(entry); - }; - - vm.requestRemoveSettingsForEntry = function(entry) { - localizationService.localizeMany(["general_remove", "defaultdialogs_confirmremoveusageof"]).then(function (data) { - - var settingsElementType = vm.getElementTypeByAlias(entry.settingsElementTypeAlias); - - overlayService.confirmRemove({ - title: data[0], - content: localizationService.tokenReplace(data[1], [settingsElementType.name]), - close: function () { - overlayService.close(); - }, - submit: function () { - vm.removeSettingsForEntry(entry); - overlayService.close(); - } - }); - }); - }; - vm.removeSettingsForEntry = function(entry) { - entry.settingsElementTypeAlias = null; - }; - - vm.addSettingsForEntry = function ($event, entry) { - - var elemTypeSelectorOverlay = { - view: "itempicker", - title: "Pick settings (missing translation)", - availableItems: vm.elementTypes, - position: "target", - event: $event, - size: vm.elementTypes.length < 7 ? "small" : "medium", - createNewItem: { - action: function() { - overlayService.close(); - vm.createElementTypeAndAdd((alias) => { - vm.addSettingsAtEntry(entry, alias); - }); - }, - icon: "icon-add", - name: "Create new" - }, - submit: function (overlay) { - vm.addSettingsAtEntry(entry, overlay.selectedItem.alias); - overlayService.close(); - }, - close: function () { - overlayService.close(); - } - }; - - overlayService.open(elemTypeSelectorOverlay); - }; - vm.addSettingsAtEntry = function(entry, alias) { - entry.settingsElementTypeAlias = alias; - }; - - vm.openElementType = function(elementTypeAlias) { - var elementTypeId = vm.getElementTypeByAlias(elementTypeAlias).id; - const editor = { - id: elementTypeId, - submit: function (model) { - loadElementTypes(); - editorService.close(); - }, - close: function () { - editorService.close(); - } - }; - editorService.documentTypeEditor(editor); - } - - vm.requestRemoveViewForEntry = function(entry) { - localizationService.localizeMany(["general_remove", "defaultdialogs_confirmremoveusageof"]).then(function (data) { - overlayService.confirmRemove({ - title: data[0], - content: localizationService.tokenReplace(data[1], [entry.view]), - close: function () { - overlayService.close(); - }, - submit: function () { - vm.removeViewForEntry(entry); - overlayService.close(); - } - }); - }); - }; - vm.removeViewForEntry = function(entry) { - entry.view = null; - }; - vm.addViewForEntry = function(entry) { - const filePicker = { - title: "Select view (TODO need translation)", - section: "settings", - treeAlias: "files", - entityType: "file", - isDialog: true, - filter: function (i) { - if (i.name.indexOf(".html") !== -1) { - return true; - } - }, - select: function (file) { - console.log(file); - entry.view = file.name; - editorService.close(); - }, - close: function () { - editorService.close(); - } - }; - editorService.treePicker(filePicker); - } - - - onInit(); - - $scope.$watchCollection('model.value', function(newVal, oldVal) { - evaluateStatus(); - }); - - } - - angular.module("umbraco").controller("Umbraco.PropertyEditors.BlockList.ElementTypePickerController", ElementTypePickerController); - -})(); diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/blocklist.elementtypepicker.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/blocklist.elementtypepicker.html deleted file mode 100644 index 8184761a1a..0000000000 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/blocklist.elementtypepicker.html +++ /dev/null @@ -1,81 +0,0 @@ -
    -
    -
    -
    -
    - Custom view -
    -
    - Label - -
    -
    - Custom view -
    -
    - Custom view -
    -
    -
    -
    -
    -
    -
    -
    - {{ contentPreview = vm.getElementTypeByAlias(entry.contentTypeAlias); "" }} - -
    - -
    -
    -
    - -
    -
    -
    - - -
    - -
    -
    - -
    -
    -
    - {{ settingsPreview = vm.getElementTypeByAlias(entry.settingsElementTypeAlias); "" }} - -
    - - -
    -
    - -
    -
    - -
    -
    -
    -
    - -
    diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml index a09088efa1..409ba0cefe 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml @@ -2403,14 +2403,20 @@ To manage your website, simply open the Umbraco back office and start adding con Create forms using an intuitive drag and drop interface. From simple contact forms that sends e-mails to advanced questionaires that integrate with CRM systems. Your clients will love it! + Background color + Icon color Content model Label Custom view Settings model + Overlay editor size Add custom view Add settings Overwrite label template %0%.]]> Content using this block will be lost. + + 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 89fbceaea8..404993a201 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml @@ -2414,14 +2414,20 @@ To manage your website, simply open the Umbraco back office and start adding con Create forms using an intuitive drag and drop interface. From simple contact forms that sends e-mails to advanced questionaires that integrate with CRM systems. Your clients will love it! + Background color + Icon color Content model Label Custom view Settings model + Overlay editor size Add custom view Add settings Overwrite label template %0%.]]> Content using this block will be lost. + + Thumbnail + Add thumbnail diff --git a/src/Umbraco.Web/Editors/ElementTypeController.cs b/src/Umbraco.Web/Editors/ElementTypeController.cs new file mode 100644 index 0000000000..64dfbbb4ab --- /dev/null +++ b/src/Umbraco.Web/Editors/ElementTypeController.cs @@ -0,0 +1,29 @@ +using System.Collections.Generic; +using System.Linq; +using Umbraco.Core.Services; +using Umbraco.Web.Editors; +using Umbraco.Web.Mvc; + +namespace Umbraco.Web.Editors +{ + [PluginController("UmbracoApi")] + public class ElementTypeController : UmbracoAuthorizedJsonController + { + [System.Web.Http.HttpGet] + public IEnumerable GetAll() + { + return Services.ContentTypeService + .GetAllElementTypes() + .OrderBy(x => x.SortOrder) + .Select(x => new + { + id = x.Id, + key = x.Key, + name = x.Name, + description = x.Description, + alias = x.Alias, + icon = x.Icon + }); + } + } +} diff --git a/src/Umbraco.Web/PropertyEditors/BlockListConfiguration.cs b/src/Umbraco.Web/PropertyEditors/BlockListConfiguration.cs index 20b6c00d68..584ed5f1f8 100644 --- a/src/Umbraco.Web/PropertyEditors/BlockListConfiguration.cs +++ b/src/Umbraco.Web/PropertyEditors/BlockListConfiguration.cs @@ -10,7 +10,7 @@ namespace Umbraco.Web.PropertyEditors public class BlockListConfiguration { - [ConfigurationField("blocks", "Available Blocks", "views/propertyeditors/blocklist/prevalue/blocklist.elementtypepicker.html", Description = "Define the available blocks.")] + [ConfigurationField("blocks", "Available Blocks", "views/propertyeditors/blocklist/prevalue/blocklist.blockconfiguration.html", Description = "Define the available blocks.")] public BlockConfiguration[] Blocks { get; set; } @@ -28,7 +28,16 @@ namespace Umbraco.Web.PropertyEditors public class BlockConfiguration { - // TODO: rename this to contentElementTypeAlias, I would like this to be specific, since we have the settings. + + [JsonProperty("backgroundColor")] + public string BackgroundColor { get; set; } + + [JsonProperty("iconColor")] + public string IconColor { get; set; } + + [JsonProperty("thumbnail")] + public string Thumbnail { get; set; } + [JsonProperty("contentTypeAlias")] public string Alias { get; set; } @@ -40,6 +49,9 @@ namespace Umbraco.Web.PropertyEditors [JsonProperty("label")] public string Label { get; set; } + + [JsonProperty("editorSize")] + public string EditorSize { get; set; } } [ConfigurationField("useInlineEditingAsDefault", "Inline editing mode", "boolean", Description = "Use the inline editor as the default block view")] diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index 975f242dee..c657a52dcc 100644 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -149,6 +149,7 @@ + From 7855870077cac7b21a428a206fe9765aeeab2cd4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Wed, 8 Apr 2020 13:23:52 +0200 Subject: [PATCH 108/377] translation for prevalue editor property group headlines --- .../prevalue/blocklist.blockconfiguration.overlay.html | 6 +++--- .../prevalue/blocklist.blockconfiguration.overlay.less | 3 +-- src/Umbraco.Web.UI/Umbraco/config/lang/en.xml | 3 +++ src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml | 3 +++ 4 files changed, 10 insertions(+), 5 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/blocklist.blockconfiguration.overlay.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/blocklist.blockconfiguration.overlay.html index 255d27cb8d..806930ffb2 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/blocklist.blockconfiguration.overlay.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/blocklist.blockconfiguration.overlay.html @@ -18,7 +18,7 @@
    -
    Block Editor
    + Block appearance
    @@ -75,7 +75,7 @@
    -
    Data Settings
    + Data Models
    @@ -147,7 +147,7 @@
    -
    Showcase
    + Showcase
    diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/blocklist.blockconfiguration.overlay.less b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/blocklist.blockconfiguration.overlay.less index c4b6349747..8d063c77fb 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/blocklist.blockconfiguration.overlay.less +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/blocklist.blockconfiguration.overlay.less @@ -26,9 +26,8 @@ .__control-actions-btn { position: relative; color: @ui-action-discreet-type; - height: 30px; + height: 32px; width: 26px; - margin-top: 2px; &:hover { color: @ui-action-discreet-type-hover; } diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml index 409ba0cefe..a1de35c266 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml @@ -2403,6 +2403,9 @@ To manage your website, simply open the Umbraco back office and start adding con Create forms using an intuitive drag and drop interface. From simple contact forms that sends e-mails to advanced questionaires that integrate with CRM systems. Your clients will love it! + Block apperance + Data models + Showcase Background color Icon color Content model 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 404993a201..cb00bae3d1 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml @@ -2414,6 +2414,9 @@ To manage your website, simply open the Umbraco back office and start adding con Create forms using an intuitive drag and drop interface. From simple contact forms that sends e-mails to advanced questionaires that integrate with CRM systems. Your clients will love it! + Block apperance + Data models + Showcase Background color Icon color Content model From 33f507a5757135f9ca5b3ee36edd86dfd8126fca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Wed, 8 Apr 2020 15:33:04 +0200 Subject: [PATCH 109/377] blockcard corrections --- .../blockeditor/blockcard/blockcard.component.js | 12 ------------ .../blockeditor/blockcard/blockcard.component.less | 3 +++ 2 files changed, 3 insertions(+), 12 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blockeditor/blockcard/blockcard.component.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blockeditor/blockcard/blockcard.component.js index 98e7303219..3e7728c583 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blockeditor/blockcard/blockcard.component.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blockeditor/blockcard/blockcard.component.js @@ -5,8 +5,6 @@ .module("umbraco") .component("umbBlockCard", { templateUrl: "views/propertyeditors/blockeditor/blockcard/blockcard.component.html", - controller: BlockCardController, - controllerAs: "vm", transclude: true, bindings: { blockConfigModel: "<", @@ -14,15 +12,5 @@ } }); - function BlockCardController() { - - var vm = this; - - vm.$onInit = function() { - console.log(vm.blockConfigModel); - console.log(vm.elementTypeModel); - } - - } })(); diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blockeditor/blockcard/blockcard.component.less b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blockeditor/blockcard/blockcard.component.less index 5adbded9bb..703c1bca26 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blockeditor/blockcard/blockcard.component.less +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blockeditor/blockcard/blockcard.component.less @@ -52,6 +52,9 @@ umb-block-card { width: 100%; padding-bottom: 10/16*100%; background-color: @gray-11; + + background-size: cover; + background-position: 50% 50%; .__icon { position: absolute; width: 100%; From 83d590ff084adddb8b9cb50e53a130b0a3f47709 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Wed, 8 Apr 2020 15:33:44 +0200 Subject: [PATCH 110/377] block list prevalue corrections --- .../blocklist.blockconfiguration.overlay.controller.js | 2 +- .../prevalue/blocklist.blockconfiguration.overlay.html | 6 +++--- src/Umbraco.Web/PropertyEditors/BlockListConfiguration.cs | 5 ++++- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/blocklist.blockconfiguration.overlay.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/blocklist.blockconfiguration.overlay.controller.js index 015d25e9fb..a752448e2c 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/blocklist.blockconfiguration.overlay.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/blocklist.blockconfiguration.overlay.controller.js @@ -151,7 +151,7 @@ entityType: "file", isDialog: true, filter: function (i) { - return (i.name.indexOf(".jpg") !== -1 || i.name.indexOf(".jpeg") !== -1 || i.name.indexOf(".png") !== -1 || i.name.indexOf(".svg") !== -1 || i.name.indexOf(".webp") !== -1 || i.name.indexOf(".gif") !== -1); + return !(i.name.indexOf(".jpg") !== -1 || i.name.indexOf(".jpeg") !== -1 || i.name.indexOf(".png") !== -1 || i.name.indexOf(".svg") !== -1 || i.name.indexOf(".webp") !== -1 || i.name.indexOf(".gif") !== -1); }, select: function (file) { block.thumbnail = file.name; diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/blocklist.blockconfiguration.overlay.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/blocklist.blockconfiguration.overlay.html index 806930ffb2..ae147d488f 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/blocklist.blockconfiguration.overlay.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/blocklist.blockconfiguration.overlay.html @@ -18,7 +18,7 @@
    - Block appearance
    + Block appearance
    @@ -75,7 +75,7 @@
    - Data Models
    + Data Models
    @@ -147,7 +147,7 @@
    - Showcase
    + Showcase
    diff --git a/src/Umbraco.Web/PropertyEditors/BlockListConfiguration.cs b/src/Umbraco.Web/PropertyEditors/BlockListConfiguration.cs index 584ed5f1f8..e0157ca36c 100644 --- a/src/Umbraco.Web/PropertyEditors/BlockListConfiguration.cs +++ b/src/Umbraco.Web/PropertyEditors/BlockListConfiguration.cs @@ -54,8 +54,11 @@ namespace Umbraco.Web.PropertyEditors public string EditorSize { get; set; } } - [ConfigurationField("useInlineEditingAsDefault", "Inline editing mode", "boolean", Description = "Use the inline editor as the default block view")] + [ConfigurationField("useInlineEditingAsDefault", "Inline editing mode", "boolean", Description = "Use the inline editor as the default block view.")] public bool UseInlineEditingAsDefault { get; set; } + [ConfigurationField("propertyWidth", "Property editor width", "textstring", Description = "optional css overwrite, example: 800px or 100%")] + public string propertyWidth { get; set; } + } } From 676f2ab847660c564ed1c8638c4de5b8f0a61f7b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Wed, 8 Apr 2020 15:46:24 +0200 Subject: [PATCH 111/377] revert agressive clean up --- .../blockeditor/blockcard/blockcard.component.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blockeditor/blockcard/blockcard.component.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blockeditor/blockcard/blockcard.component.js index 3e7728c583..8f1cb00c6d 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blockeditor/blockcard/blockcard.component.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blockeditor/blockcard/blockcard.component.js @@ -5,6 +5,8 @@ .module("umbraco") .component("umbBlockCard", { templateUrl: "views/propertyeditors/blockeditor/blockcard/blockcard.component.html", + controller: BlockCardController, + controllerAs: "vm", transclude: true, bindings: { blockConfigModel: "<", @@ -12,5 +14,10 @@ } }); + function BlockCardController() { + + var vm = this; + + } })(); From b4387e757a3b70c8e7433aa303132688c657abd6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Wed, 8 Apr 2020 15:46:37 +0200 Subject: [PATCH 112/377] Block Picker --- .../common/services/blockeditor.service.js | 7 ++-- .../blockpicker/blockpicker.controller.js | 21 ++++++++++ .../blockpicker/blockpicker.html | 42 +++++++++++++++++++ .../blocklist/blocklist.component.js | 41 +++++++++--------- 4 files changed, 85 insertions(+), 26 deletions(-) create mode 100644 src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/blockpicker/blockpicker.controller.js create mode 100644 src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/blockpicker/blockpicker.html 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 a05535d3b4..3524982a90 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 @@ -235,7 +235,7 @@ return this.blockConfigurations.map(blockConfiguration => blockConfiguration.contentTypeAlias); }, - getAvailableBlocksForItemPicker: function() { + getAvailableBlocksForBlockPicker: function() { var blocks = []; @@ -243,9 +243,8 @@ var scaffold = this.getScaffoldFor(blockConfiguration.contentTypeAlias); if(scaffold) { blocks.push({ - alias: scaffold.contentTypeAlias, - name: scaffold.contentTypeName, - icon: scaffold.icon + blockConfigModel: blockConfiguration, + elementTypeModel: scaffold }); } }); diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/blockpicker/blockpicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/blockpicker/blockpicker.controller.js new file mode 100644 index 0000000000..5d02010485 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/blockpicker/blockpicker.controller.js @@ -0,0 +1,21 @@ +//used for the media picker dialog +angular.module("umbraco") +.controller("Umbraco.Editors.BlockPickerController", + function ($scope) { + var vm = this; + + vm.model = $scope.model; + + vm.selectItem = function() { + vm.model.selectedItem = item; + vm.model.submit($scope.model); + } + + vm.close = function() { + if ($scope.model && $scope.model.close) { + $scope.model.close($scope.model); + } + } + + } +); diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/blockpicker/blockpicker.html b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/blockpicker/blockpicker.html new file mode 100644 index 0000000000..3eb0425751 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/blockpicker/blockpicker.html @@ -0,0 +1,42 @@ +
    + + + + + + +
    + +
    + + + +
    + +
    + + + + + + + + + + + +
    +
    diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.js index 2b25a8b013..f97ab1e19a 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.js @@ -108,7 +108,7 @@ }); vm.availableContentTypes = modelObject.getAvailableAliasesForBlockContent(); - vm.availableBlockTypes = modelObject.getAvailableBlocksForItemPicker(); + vm.availableBlockTypes = modelObject.getAvailableBlocksForBlockPicker(); $scope.$evalAsync(); @@ -198,7 +198,7 @@ settings: blockSettingsModelClone, title: blockModel.label, view: "views/common/infiniteeditors/blockeditor/blockeditor.html", - size: blockModel.config.overlaySize || "medium", + size: blockModel.config.editorSize || "medium", submit: function(blockEditorModel) { // To ensure syncronization gets tricked we transfer if (blockEditorModel.content !== null) { @@ -229,14 +229,13 @@ return; } - vm.blockTypePicker = { - show: false, - size: vm.availableBlockTypes.length < 7 ? "small" : "medium", - filter: vm.availableBlockTypes.length > 12 ? true : false, - orderBy: "$index", - view: "itempicker", - event: $event, + var amountOfAvailableTypes = vm.availableBlockTypes.length; + var blockPickerModel = { availableItems: vm.availableBlockTypes, + title: vm.labels.grid_addElement, + orderBy: "$index", + view: "views/common/infiniteeditors/blockpicker/blockpicker.html", + size: (amountOfAvailableTypes > 8 ? "medium" : "small"), clickPasteItem: function(item) { if (item.type === "elementTypeArray") { var indexIncrementor = 0; @@ -248,29 +247,28 @@ } else { requestPasteFromClipboard(createIndex, item.data); } - vm.blockTypePicker.close(); + blockPickerModel.close(); }, - submit: function (model) { + submit: function(blockPickerModel) { var added = false; if (model && model.selectedItem) { added = addNewBlock(createIndex, model.selectedItem.alias); } - vm.blockTypePicker.close(); + blockPickerModel.close(); if (added && vm.model.config.useInlineEditingAsDefault !== true && vm.blocks.length > createIndex) { editBlock(vm.blocks[createIndex]); } }, - close: function () { - vm.blockTypePicker.show = false; - delete vm.blockTypePicker; + close: function() { + editorService.close(); } }; - vm.blockTypePicker.pasteItems = []; + blockPickerModel.pasteItems = []; var singleEntriesForPaste = clipboardService.retriveEntriesOfType("elementType", vm.availableContentTypes); singleEntriesForPaste.forEach(function (entry) { - vm.blockTypePicker.pasteItems.push({ + blockPickerModel.pasteItems.push({ type: "elementType", name: entry.label, data: entry.data, @@ -280,7 +278,7 @@ var arrayEntriesForPaste = clipboardService.retriveEntriesOfType("elementTypeArray", vm.availableContentTypes); arrayEntriesForPaste.forEach(function (entry) { - vm.blockTypePicker.pasteItems.push({ + blockPickerModel.pasteItems.push({ type: "elementTypeArray", name: entry.label, data: entry.data, @@ -288,9 +286,7 @@ }); }); - vm.blockTypePicker.title = vm.blockTypePicker.pasteItems.length > 0 ? labels.grid_addElement : labels.content_createEmpty; - - vm.blockTypePicker.clickClearPaste = function ($event) { + blockPickerModel.clickClearPaste = function ($event) { $event.stopPropagation(); $event.preventDefault(); clipboardService.clearEntriesOfType("elementType", vm.availableContentTypes); @@ -298,7 +294,8 @@ vm.blockTypePicker.pasteItems = [];// This dialog is not connected via the clipboardService events, so we need to update manually. }; - vm.blockTypePicker.show = true; + // open block picker overlay + editorService.open(blockPickerModel); }; From 525513cdeabb7b8842f7d7e038715e4359bfd4c1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Wed, 8 Apr 2020 16:18:36 +0200 Subject: [PATCH 113/377] MaxPropertyWidth PreValue + Implementation --- .../views/propertyeditors/blocklist/blocklist.component.html | 2 +- .../views/propertyeditors/blocklist/blocklist.component.js | 2 ++ src/Umbraco.Web/PropertyEditors/BlockListConfiguration.cs | 4 ++-- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.html index fc957ce003..360201c27d 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.html @@ -1,7 +1,7 @@
    -
    +
    diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.js index f97ab1e19a..d175e42241 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.js @@ -50,6 +50,8 @@ vm.$onInit = function() { vm.validationLimit = vm.model.config.validationLimit; + + vm.listWrapperSyles = {'max-width': vm.model.config.maxPropertyWidth}; // We need to ensure that the property model value is an object, this is needed for modelObject to recive a reference and keep that updated. if(typeof vm.model.value !== 'object' || vm.model.value === null) {// testing if we have null or undefined value or if the value is set to another type than Object. diff --git a/src/Umbraco.Web/PropertyEditors/BlockListConfiguration.cs b/src/Umbraco.Web/PropertyEditors/BlockListConfiguration.cs index e0157ca36c..fba80bd57d 100644 --- a/src/Umbraco.Web/PropertyEditors/BlockListConfiguration.cs +++ b/src/Umbraco.Web/PropertyEditors/BlockListConfiguration.cs @@ -57,8 +57,8 @@ namespace Umbraco.Web.PropertyEditors [ConfigurationField("useInlineEditingAsDefault", "Inline editing mode", "boolean", Description = "Use the inline editor as the default block view.")] public bool UseInlineEditingAsDefault { get; set; } - [ConfigurationField("propertyWidth", "Property editor width", "textstring", Description = "optional css overwrite, example: 800px or 100%")] - public string propertyWidth { get; set; } + [ConfigurationField("maxPropertyWidth", "Property editor width", "textstring", Description = "optional css overwrite, example: 800px or 100%")] + public string MaxPropertyWidth { get; set; } } } From ff8ef00847c78920b9871ea1f34a64a8f6566fb8 Mon Sep 17 00:00:00 2001 From: Benjamin Carleski Date: Wed, 8 Apr 2020 07:34:54 -0700 Subject: [PATCH 114/377] Incorporate latest block list editor changes, update migration for changed configuration --- .../V_8_7_0/StackedContentToBlockList.cs | 20 ++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_7_0/StackedContentToBlockList.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_7_0/StackedContentToBlockList.cs index a3a8bd62c4..c75998a66d 100644 --- a/src/Umbraco.Core/Migrations/Upgrade/V_8_7_0/StackedContentToBlockList.cs +++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_7_0/StackedContentToBlockList.cs @@ -170,11 +170,11 @@ namespace Umbraco.Core.Migrations.Upgrade.V_8_7_0 private class BlockListConfiguration { - [ConfigurationField("blocks", "Available Blocks", "views/propertyeditors/blocklist/prevalue/blocklist.elementtypepicker.html", Description = "Define the available blocks.")] + [JsonProperty("blocks")] public BlockConfiguration[] Blocks { get; set; } - [ConfigurationField("validationLimit", "Amount", "numberrange", Description = "Set a required range of blocks")] + [JsonProperty("validationLimit")] public NumberRange ValidationLimit { get; set; } = new NumberRange(); public class NumberRange @@ -188,6 +188,15 @@ namespace Umbraco.Core.Migrations.Upgrade.V_8_7_0 public class BlockConfiguration { + [JsonProperty("backgroundColor")] + public string BackgroundColor { get; set; } + + [JsonProperty("iconColor")] + public string IconColor { get; set; } + + [JsonProperty("thumbnail")] + public string Thumbnail { get; set; } + [JsonProperty("contentTypeAlias")] public string Alias { get; set; } @@ -199,11 +208,16 @@ namespace Umbraco.Core.Migrations.Upgrade.V_8_7_0 [JsonProperty("label")] public string Label { get; set; } + + [JsonProperty("editorSize")] + public string EditorSize { get; set; } } - [ConfigurationField("useInlineEditingAsDefault", "Inline editing mode", "boolean", Description = "Use the inline editor as the default block view")] + [JsonProperty("useInlineEditingAsDefault")] public bool UseInlineEditingAsDefault { get; set; } + [JsonProperty("maxPropertyWidth")] + public string MaxPropertyWidth { get; set; } } private class StackedContentConfiguration From 5bbf68e432daabedaa60017f6e42272e9102a84c Mon Sep 17 00:00:00 2001 From: Benjamin Carleski Date: Wed, 8 Apr 2020 08:49:11 -0700 Subject: [PATCH 115/377] Change declared converter type --- .../ValueConverters/BlockListPropertyValueConverter.cs | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/Umbraco.Web/PropertyEditors/ValueConverters/BlockListPropertyValueConverter.cs b/src/Umbraco.Web/PropertyEditors/ValueConverters/BlockListPropertyValueConverter.cs index d4e130cc0d..21105b531e 100644 --- a/src/Umbraco.Web/PropertyEditors/ValueConverters/BlockListPropertyValueConverter.cs +++ b/src/Umbraco.Web/PropertyEditors/ValueConverters/BlockListPropertyValueConverter.cs @@ -32,13 +32,7 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters => propertyType.EditorAlias.InvariantEquals(Constants.PropertyEditors.Aliases.BlockList); /// - public override Type GetPropertyValueType(IPublishedPropertyType propertyType) - { - var contentTypes = propertyType.DataType.ConfigurationAs().Blocks; - return contentTypes.Length == 1 - ? typeof(IEnumerable<>).MakeGenericType(ModelType.For(contentTypes[0].Alias)) - : typeof(IEnumerable); - } + public override Type GetPropertyValueType(IPublishedPropertyType propertyType) => typeof(BlockListModel); /// public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) From 133d30791f1c2502b6ad6379a0cfb7a0003bb36f Mon Sep 17 00:00:00 2001 From: Benjamin Carleski Date: Wed, 8 Apr 2020 14:27:50 -0700 Subject: [PATCH 116/377] Handle invalid data type references --- .../Upgrade/V_8_7_0/StackedContentToBlockList.cs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_7_0/StackedContentToBlockList.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_7_0/StackedContentToBlockList.cs index c75998a66d..1c27f1c48a 100644 --- a/src/Umbraco.Core/Migrations/Upgrade/V_8_7_0/StackedContentToBlockList.cs +++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_7_0/StackedContentToBlockList.cs @@ -109,9 +109,9 @@ namespace Umbraco.Core.Migrations.Upgrade.V_8_7_0 { Blocks = old.ContentTypes?.Select(t => new BlockListConfiguration.BlockConfiguration { - Alias = knownDocumentTypes[t.IcContentTypeGuid].Alias, + Alias = knownDocumentTypes.TryGetValue(t.IcContentTypeGuid, out var ct) ? ct.Alias : null, Label = t.NameTemplate - }).ToArray(), + }).Where(c => c.Alias != null).ToArray(), UseInlineEditingAsDefault = old.SingleItemMode == "1" || old.SingleItemMode == bool.TrueString }; @@ -247,9 +247,9 @@ namespace Umbraco.Core.Migrations.Upgrade.V_8_7_0 public void AddDataItem(JObject obj, Dictionary knownDocumentTypes) { - if (!Guid.TryParse(obj["key"].ToString(), out var key)) throw new ArgumentException("Could not find a valid key in the data item"); - if (!Guid.TryParse(obj["icContentTypeGuid"].ToString(), out var ctGuid)) throw new ArgumentException("Could not find a valid content type GUID in the data item"); - if (!knownDocumentTypes.TryGetValue(ctGuid, out var ct)) throw new ArgumentException($"Unknown content type GUID '{ctGuid}'"); + if (!Guid.TryParse(obj["key"].ToString(), out var key)) key = Guid.NewGuid(); + if (!Guid.TryParse(obj["icContentTypeGuid"].ToString(), out var ctGuid)) ctGuid = Guid.Empty; + if (!knownDocumentTypes.TryGetValue(ctGuid, out var ct)) ct = new KnownContentType { Alias = ctGuid.ToString() }; obj.Remove("key"); obj.Remove("icContentTypeGuid"); From e8a31ef7b61452f0f06b7152fd01118602f7c2e8 Mon Sep 17 00:00:00 2001 From: Benjamin Carleski Date: Mon, 13 Apr 2020 16:40:09 -0700 Subject: [PATCH 117/377] Remove code duplicated from PR #7957 --- .../Migrations/Upgrade/UmbracoPlan.cs | 1 - .../Upgrade/V_8_7_0/ColorPickerPreValues.cs | 106 ------------------ .../Upgrade/V_8_7_0/ConvertToElements.cs | 18 +-- src/Umbraco.Core/Umbraco.Core.csproj | 1 - 4 files changed, 2 insertions(+), 124 deletions(-) delete mode 100644 src/Umbraco.Core/Migrations/Upgrade/V_8_7_0/ColorPickerPreValues.cs diff --git a/src/Umbraco.Core/Migrations/Upgrade/UmbracoPlan.cs b/src/Umbraco.Core/Migrations/Upgrade/UmbracoPlan.cs index 1d30efa573..1d4cef45d4 100644 --- a/src/Umbraco.Core/Migrations/Upgrade/UmbracoPlan.cs +++ b/src/Umbraco.Core/Migrations/Upgrade/UmbracoPlan.cs @@ -197,7 +197,6 @@ namespace Umbraco.Core.Migrations.Upgrade // to 8.7.0... To("{DFA35FA2-BFBB-433F-84E5-BD75940CDDF6}"); - To("{711AC937-B11C-47AC-8D4A-5B8868A3C2C6}"); To("{DA434576-3DEF-46D7-942A-CE34D7F7FB8A}"); //FINAL } diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_7_0/ColorPickerPreValues.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_7_0/ColorPickerPreValues.cs deleted file mode 100644 index 6e959e86d7..0000000000 --- a/src/Umbraco.Core/Migrations/Upgrade/V_8_7_0/ColorPickerPreValues.cs +++ /dev/null @@ -1,106 +0,0 @@ -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using System; -using System.Linq; -using System.Text.RegularExpressions; -using Umbraco.Core.Persistence; -using Umbraco.Core.Persistence.Dtos; -using Umbraco.Core.PropertyEditors; - -namespace Umbraco.Core.Migrations.Upgrade.V_8_7_0 -{ - public class ColorPickerPreValues : MigrationBase - { - private static readonly Regex OldPreValuesPattern1 = new Regex("\\s*{(\\s*\"[0-9]+\"\\s*:\\s*\"[0-9a-fA-F]+\"\\s*,)*\\s*\"useLabel\"\\s*:\\s*\"[01]\"\\s*}\\s*", RegexOptions.Compiled); - private static readonly Regex OldPreValuesPattern2 = new Regex("\\s*{(\\s*\"[0-9]+\"\\s*:\\s*{\\s*\"value\"\\s*:\\s*\"[0-9a-fA-F]+\"\\s*(,\\s*\"label\"\\s*:\\s*\"[^\"]*\"\\s*)?(,\\s*\"sortOrder\"\\s*:\\s*[0-9]+\\s*)?}\\s*,)*\\s*\"useLabel\"\\s*:\\s*\"[01]\"\\s*}\\s*", RegexOptions.Compiled); - - public ColorPickerPreValues(IMigrationContext context) : base(context) - { - } - - public override void Migrate() - { - var sql = Sql() - .Select() - .From() - .Where(d => d.EditorAlias == Constants.PropertyEditors.Aliases.ColorPicker); - - var dtos = Database.Fetch(sql); - - foreach (var dto in dtos) - { - if (dto.Configuration.IsNullOrWhiteSpace()) continue; - - if (OldPreValuesPattern1.IsMatch(dto.Configuration)) ConvertPreValues(dto, ConvertStyle1); - else if (OldPreValuesPattern2.IsMatch(dto.Configuration)) ConvertPreValues(dto, ConvertStyle2); - else continue; - - Database.Update(dto); - } - } - - private void ConvertPreValues(DataTypeDto dto, Func converter) - { - var obj = JObject.Parse(dto.Configuration); - var config = new ColorPickerConfiguration(); - var id = 0; - - foreach (var prop in obj.Properties()) - { - if (prop.Name.ToLowerInvariant() == "uselabel") - { - config.UseLabel = prop.Value.ToString() == "1"; - } - else - { - id++; - config.Items.Add(new ValueListConfiguration.ValueListItem - { - Id = id, - Value = JsonConvert.SerializeObject(converter(id, prop.Value)) - }); - } - } - - dto.Configuration = JsonConvert.SerializeObject(config); - } - - private ItemValue ConvertStyle1(int index, JToken token) - { - var value = token.ToString(); - return new ItemValue - { - Color = value, - Label = value, - SortOrder = index - }; - } - - private ItemValue ConvertStyle2(int index, JToken token) - { - var obj = (JObject)token; - var value = obj["value"].ToString(); - var label = obj["label"]?.ToString(); - var order = obj["sortOrder"]?.ToString(); - - return new ItemValue - { - Color = value, - Label = label.IsNullOrWhiteSpace() ? value : label, - SortOrder = int.TryParse(order, out var o) ? o : index - }; - } - - private class ItemValue - { - [JsonProperty("value")] - public string Color { get; set; } - - [JsonProperty("label")] - public string Label { get; set; } - - [JsonProperty("sortOrder")] - public int SortOrder { get; set; } - } - } -} diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_7_0/ConvertToElements.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_7_0/ConvertToElements.cs index e42453a3fe..2bc017b643 100644 --- a/src/Umbraco.Core/Migrations/Upgrade/V_8_7_0/ConvertToElements.cs +++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_7_0/ConvertToElements.cs @@ -1,10 +1,7 @@ using Newtonsoft.Json; using Newtonsoft.Json.Linq; -using System; using System.Collections.Generic; using System.Linq; -using System.Text; -using System.Threading.Tasks; using Umbraco.Core.Persistence; using Umbraco.Core.Persistence.Dtos; @@ -24,7 +21,7 @@ namespace Umbraco.Core.Migrations.Upgrade.V_8_7_0 docTypes.ForEach(d => docTypeMap[d.Alias] = d.NodeId); // Find all Nested Content or Block List data types - var dataTypes = GetDataTypes(Constants.PropertyEditors.Aliases.NestedContent, Constants.PropertyEditors.Aliases.BlockList); + var dataTypes = GetDataTypes(Constants.PropertyEditors.Aliases.BlockList); // Find all document types listed in each var elementTypeIds = dataTypes.SelectMany(d => GetDocTypeIds(d.Configuration, docTypeMap)).ToList(); @@ -63,12 +60,7 @@ namespace Umbraco.Core.Migrations.Upgrade.V_8_7_0 if (configuration.IsNullOrWhiteSpace() || configuration[0] != '{') return Enumerable.Empty(); var obj = JObject.Parse(configuration); - if (obj["contentTypes"] is JArray ncArr) - { - var arr = ncArr.ToObject(); - return arr.Select(i => idMap.TryGetValue(i.Alias, out var id) ? id : 0).Where(i => i != 0); - } - else if (obj["blocks"] is JArray blArr) + if (obj["blocks"] is JArray blArr) { var arr = blArr.ToObject(); return arr.Select(i => idMap.TryGetValue(i.Alias, out var id) ? id : 0).Where(i => i != 0); @@ -77,12 +69,6 @@ namespace Umbraco.Core.Migrations.Upgrade.V_8_7_0 return Enumerable.Empty(); } - public class ContentType - { - [JsonProperty("ncAlias")] - public string Alias { get; set; } - } - public class BlockConfiguration { [JsonProperty("contentTypeAlias")] diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index 1ae5267b6a..c0d14a580e 100755 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -131,7 +131,6 @@ - From 3d820f36972462dae665fbe156e5dcdb85a29965 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Tue, 14 Apr 2020 17:08:35 +0200 Subject: [PATCH 118/377] use ElementModel for the ContentModel of an ElementType. So we can use ElementTypeModel for the ModelDefinition aka. the Type. --- .../common/services/blockeditor.service.js | 32 ++++++++++--------- .../blocklist/blocklist.component.js | 11 ++++--- 2 files changed, 23 insertions(+), 20 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 3524982a90..75f3efe322 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 @@ -9,9 +9,9 @@ * Simple mapping from property model content entry to editing model, * needs to stay simple to avoid deep watching. */ - function mapToElementTypeModel(elementTypeModel, contentModel) { + function mapToElementModel(elementModel, contentModel) { - var variant = elementTypeModel.variants[0]; + var variant = elementModel.variants[0]; for (var t = 0; t < variant.tabs.length; t++) { var tab = variant.tabs[t]; @@ -26,12 +26,12 @@ } /** - * Simple mapping from elementTypeModel to property model content entry, + * Simple mapping from elementModel to property model content entry, * needs to stay simple to avoid deep watching. */ - function mapToPropertyModel(elementTypeModel, contentModel) { + function mapToPropertyModel(elementModel, contentModel) { - var variant = elementTypeModel.variants[0]; + var variant = elementModel.variants[0]; for (var t = 0; t < variant.tabs.length; t++) { var tab = variant.tabs[t]; @@ -50,12 +50,12 @@ } /** - * Map property values from an ElementTypeModel to another ElementTypeModel. + * Map property values from an ElementModel to another ElementModel. * Used to tricker watchers for synchronization. - * @param {Object} fromModel ElementTypeModel to recive property values from. - * @param {Object} toModel ElementTypeModel to recive property values from. + * @param {Object} fromModel ElementModel to recive property values from. + * @param {Object} toModel ElementModel to recive property values from. */ - function mapElementTypeValues(fromModel, toModel) { + function mapElementValues(fromModel, toModel) { if (!fromModel || !fromModel.variants) { toModel.variants = null; return; @@ -244,7 +244,7 @@ if(scaffold) { blocks.push({ blockConfigModel: blockConfiguration, - elementTypeModel: scaffold + elementTypeModel: scaffold.documentType }); } }); @@ -283,8 +283,10 @@ var blockModel = {}; blockModel.key = String.CreateGuid().replace(/-/g, ""); blockModel.config = angular.copy(blockConfiguration); - blockModel.labelInterpolator = $interpolate(blockModel.config.label); - + if (blockModel.config.label) { + blockModel.labelInterpolator = $interpolate(blockModel.config.label); + } + var contentScaffold = this.getScaffoldFor(blockConfiguration.contentTypeAlias); if(contentScaffold === null) { return null; @@ -294,7 +296,7 @@ blockModel.content = angular.copy(contentScaffold); blockModel.content.udi = udi; - mapToElementTypeModel(blockModel.content, contentModel); + mapToElementModel(blockModel.content, contentModel); blockModel.contentModel = contentModel; blockModel.layoutModel = layoutEntry; @@ -311,7 +313,7 @@ layoutEntry.settings = layoutEntry.settings || { key: String.CreateGuid(), contentTypeAlias: blockConfiguration.settingsElementTypeAlias }; if (!layoutEntry.settings.key) { layoutEntry.settings.key = String.CreateGuid(); } if (!layoutEntry.settings.contentTypeAlias) { layoutEntry.settings.contentTypeAlias = blockConfiguration.settingsElementTypeAlias; } - mapToElementTypeModel(blockModel.settings, layoutEntry.settings); + mapToElementModel(blockModel.settings, layoutEntry.settings); } else { layoutEntry.settings = null; } @@ -458,7 +460,7 @@ createModelObject: function(propertyModelValue, propertyEditorAlias, blockConfigurations, propertyScope) { return new BlockEditorModelObject(propertyModelValue, propertyEditorAlias, blockConfigurations, propertyScope); }, - mapElementTypeValues: mapElementTypeValues, + mapElementValues: mapElementValues, getBlockLabel: getBlockLabel } } diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.js index d175e42241..21af10b792 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.js @@ -202,12 +202,12 @@ view: "views/common/infiniteeditors/blockeditor/blockeditor.html", size: blockModel.config.editorSize || "medium", submit: function(blockEditorModel) { - // To ensure syncronization gets tricked we transfer + // To ensure syncronization gets tricked we transfer each property. if (blockEditorModel.content !== null) { - blockEditorService.mapElementTypeValues(blockEditorModel.content, blockModel.content) + blockEditorService.mapElementValues(blockEditorModel.content, blockModel.content) } if (blockModel.config.settingsElementTypeAlias !== null) { - blockEditorService.mapElementTypeValues(blockEditorModel.settings, blockModel.settings) + blockEditorService.mapElementValues(blockEditorModel.settings, blockModel.settings) } editorService.close(); }, @@ -253,8 +253,9 @@ }, submit: function(blockPickerModel) { var added = false; - if (model && model.selectedItem) { - added = addNewBlock(createIndex, model.selectedItem.alias); + if (blockPickerModel && blockPickerModel.selectedItem) { + console.log(blockPickerModel.selectedItem) + added = addNewBlock(createIndex, blockPickerModel.selectedItem.blockConfigModel.contentTypeAlias); } blockPickerModel.close(); if (added && vm.model.config.useInlineEditingAsDefault !== true && vm.blocks.length > createIndex) { From 82294e3f2a80956a07a2fe6d5a28cc823de5e115 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Tue, 14 Apr 2020 17:09:15 +0200 Subject: [PATCH 119/377] do still show itempicker for BlockConfiguration even though there is no ElementTypes to pick. This enables the option to create a new ElementType to be used. --- .../blocklist.blockconfiguration.controller.js | 14 -------------- .../prevalue/blocklist.blockconfiguration.html | 2 +- 2 files changed, 1 insertion(+), 15 deletions(-) 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 a058f465e0..494ef35137 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 @@ -20,17 +20,8 @@ var vm = this; - vm.enableAddBlock = true; vm.openBlock = null; - function evaluateStatus() { - - if (!vm.elementTypes) return;// cancel if elementTypes isnt loaded jet. - - vm.enableAddBlock = vm.getAvailableElementTypes().length > 0; - - } - function onInit() { if (!$scope.model.value) { @@ -44,7 +35,6 @@ function loadElementTypes() { return elementTypeResource.getAll().then(function (elementTypes) { vm.elementTypes = elementTypes; - evaluateStatus(); }); } @@ -197,10 +187,6 @@ onInit(); - $scope.$watchCollection('model.value', function(newVal, oldVal) { - evaluateStatus(); - }); - } angular.module("umbraco").controller("Umbraco.PropertyEditors.BlockList.BlockConfigurationController", BlockConfigurationController); diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/blocklist.blockconfiguration.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/blocklist.blockconfiguration.html index 06e9714b77..bb5326e769 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/blocklist.blockconfiguration.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/blocklist.blockconfiguration.html @@ -23,7 +23,7 @@
    -
    From d6782fd752eb3853439245befedaee6b30a6c671 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Tue, 14 Apr 2020 17:09:41 +0200 Subject: [PATCH 120/377] use the right wrapper, for correct spacing --- .../blockpicker/blockpicker.html | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/blockpicker/blockpicker.html b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/blockpicker/blockpicker.html index 3eb0425751..35f7f38d97 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/blockpicker/blockpicker.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/blockpicker/blockpicker.html @@ -11,15 +11,16 @@
    +
    +
    -
    - - - + + +
    From dbd3de14a4e14bdca762b5e3c7968680d311c265 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Tue, 14 Apr 2020 17:09:48 +0200 Subject: [PATCH 121/377] parse item --- .../infiniteeditors/blockpicker/blockpicker.controller.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/blockpicker/blockpicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/blockpicker/blockpicker.controller.js index 5d02010485..d5525f8dd9 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/blockpicker/blockpicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/blockpicker/blockpicker.controller.js @@ -6,7 +6,7 @@ angular.module("umbraco") vm.model = $scope.model; - vm.selectItem = function() { + vm.selectItem = function(item) { vm.model.selectedItem = item; vm.model.submit($scope.model); } From 69950cfe3612096f2a685cd7f4a55df0181cc97a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Wed, 15 Apr 2020 14:37:44 +0200 Subject: [PATCH 122/377] correct fallback for label --- .../src/common/services/blockeditor.service.js | 4 ++-- 1 file changed, 2 insertions(+), 2 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 75f3efe322..67def3c75a 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 @@ -91,7 +91,7 @@ // We are just using the contentModel, since its a key/value object that is live synced. (if we need to add additional vars, we could make a shallow copy and apply those.) return blockModel.labelInterpolator(blockModel.contentModel); } - return blockModel.contentTypeName; + return blockModel.content.contentTypeName; } @@ -286,7 +286,7 @@ if (blockModel.config.label) { blockModel.labelInterpolator = $interpolate(blockModel.config.label); } - + var contentScaffold = this.getScaffoldFor(blockConfiguration.contentTypeAlias); if(contentScaffold === null) { return null; From d81dcc153768b5a6edde64438d76e58aca1876ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Wed, 15 Apr 2020 14:38:05 +0200 Subject: [PATCH 123/377] removed unused callback --- .../views/common/infiniteeditors/blockeditor/blockeditor.html | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/blockeditor/blockeditor.html b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/blockeditor/blockeditor.html index 301db23abd..85ce9298c6 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/blockeditor/blockeditor.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/blockeditor/blockeditor.html @@ -9,8 +9,7 @@ navigation="vm.tabs" hide-alias="true" hide-icon="true" - hide-description="true" - on-select-navigation-item="vm.selectTab"> + hide-description="true">
    From 061be643c84fd9568aea86d776e5617dbce68412 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Wed, 15 Apr 2020 14:38:25 +0200 Subject: [PATCH 124/377] paste feature for block-picker --- .../blockpicker/blockpicker.controller.js | 29 ++++++++++ .../blockpicker/blockpicker.html | 43 ++++++++++++++- .../blocklist/blocklist.component.js | 55 +++++++++++-------- src/Umbraco.Web.UI/Umbraco/config/lang/da.xml | 1 + src/Umbraco.Web.UI/Umbraco/config/lang/en.xml | 1 + .../Umbraco/config/lang/en_us.xml | 1 + 6 files changed, 103 insertions(+), 27 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/blockpicker/blockpicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/blockpicker/blockpicker.controller.js index d5525f8dd9..4957e13fad 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/blockpicker/blockpicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/blockpicker/blockpicker.controller.js @@ -4,6 +4,35 @@ angular.module("umbraco") function ($scope) { var vm = this; + vm.navigation = [ + { + "alias": "empty", + "name": "Create empty", + "icon": "icon-add", + "active": true, + "view": "" + }, + { + "alias": "clipboard", + "name": "Clipboard", + "icon": "icon-paste-in", + "view": "" + } + ]; + + vm.activeTab = vm.navigation[0]; + vm.onNavigationChanged = function(tab) { + vm.activeTab.active = false; + vm.activeTab = tab; + vm.activeTab.active = true; + } + + vm.clickClearClipboard = function() { + vm.onNavigationChanged(vm.navigation[0]); + vm.model.clipboardItems = [];// This dialog is not connected via the clipboardService events, so we need to update manually. + vm.model.clickClearClipboard(); + } + vm.model = $scope.model; vm.selectItem = function(item) { diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/blockpicker/blockpicker.html b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/blockpicker/blockpicker.html index 35f7f38d97..5206c7e66d 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/blockpicker/blockpicker.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/blockpicker/blockpicker.html @@ -4,6 +4,8 @@
    -
    + +
    + + + +
    + + + +
    +
    + +
    +
    + + +
    +
    + ng-repeat="block in vm.model.clipboardItems" + ng-click="vm.model.clickPasteItem(block)">
    diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.js index 21af10b792..69bfb1a71a 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.js @@ -241,20 +241,19 @@ clickPasteItem: function(item) { if (item.type === "elementTypeArray") { var indexIncrementor = 0; - item.data.forEach(function (entry) { + item.pasteData.forEach(function (entry) { if (requestPasteFromClipboard(createIndex + indexIncrementor, entry)) { indexIncrementor++; } }); } else { - requestPasteFromClipboard(createIndex, item.data); + requestPasteFromClipboard(createIndex, item.pasteData); } blockPickerModel.close(); }, submit: function(blockPickerModel) { var added = false; if (blockPickerModel && blockPickerModel.selectedItem) { - console.log(blockPickerModel.selectedItem) added = addNewBlock(createIndex, blockPickerModel.selectedItem.blockConfigModel.contentTypeAlias); } blockPickerModel.close(); @@ -267,36 +266,44 @@ } }; - blockPickerModel.pasteItems = []; + blockPickerModel.clickClearClipboard = function ($event) { + clipboardService.clearEntriesOfType("elementType", vm.availableContentTypes); + clipboardService.clearEntriesOfType("elementTypeArray", vm.availableContentTypes); + }; + + blockPickerModel.clipboardItems = []; var singleEntriesForPaste = clipboardService.retriveEntriesOfType("elementType", vm.availableContentTypes); singleEntriesForPaste.forEach(function (entry) { - blockPickerModel.pasteItems.push({ - type: "elementType", - name: entry.label, - data: entry.data, - icon: entry.icon - }); + console.log("paste Entry: ", entry) + blockPickerModel.clipboardItems.push( + { + type: "elementType", + pasteData: entry.data, + blockConfigModel: modelObject.getScaffoldFor(entry.alias), + elementTypeModel: { + name: entry.label, + icon: entry.icon + } + } + ); }); var arrayEntriesForPaste = clipboardService.retriveEntriesOfType("elementTypeArray", vm.availableContentTypes); arrayEntriesForPaste.forEach(function (entry) { - blockPickerModel.pasteItems.push({ - type: "elementTypeArray", - name: entry.label, - data: entry.data, - icon: entry.icon - }); + blockPickerModel.clipboardItems.push( + { + type: "elementTypeArray", + pasteData: entry.data, + blockConfigModel: {}, // no block configuration for paste items of elementTypeArray. + elementTypeModel: { + name: entry.label, + icon: entry.icon + } + } + ); }); - blockPickerModel.clickClearPaste = function ($event) { - $event.stopPropagation(); - $event.preventDefault(); - clipboardService.clearEntriesOfType("elementType", vm.availableContentTypes); - clipboardService.clearEntriesOfType("elementTypeArray", vm.availableContentTypes); - vm.blockTypePicker.pasteItems = [];// This dialog is not connected via the clipboardService events, so we need to update manually. - }; - // open block picker overlay editorService.open(blockPickerModel); diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/da.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/da.xml index 4c74c45d88..f0625ad767 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/da.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/da.xml @@ -1791,6 +1791,7 @@ Mange hilsner fra Umbraco robotten Kopier %0% %0% fra %1% Fjern alle elementer + Ryd udklipsholder Åben egenskabshandlinger diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml index a1de35c266..acd63dea4f 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml @@ -2279,6 +2279,7 @@ To manage your website, simply open the Umbraco back office and start adding con Copy %0% %0% from %1% Remove all items + Clear clipboard Open Property Actions 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 cb00bae3d1..d370de60bf 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml @@ -2290,6 +2290,7 @@ To manage your website, simply open the Umbraco back office and start adding con Copy %0% %0% from %1% Remove all items + Clear clipboard Open Property Actions From e2f60db9ebe36c54d7827a68e372f140f53da9fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Wed, 15 Apr 2020 14:58:13 +0200 Subject: [PATCH 125/377] localize block-picker tabs --- .../blockpicker/blockpicker.controller.js | 43 +++++++++++-------- src/Umbraco.Web.UI/Umbraco/config/lang/en.xml | 2 + .../Umbraco/config/lang/en_us.xml | 2 + 3 files changed, 30 insertions(+), 17 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/blockpicker/blockpicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/blockpicker/blockpicker.controller.js index 4957e13fad..1deb010073 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/blockpicker/blockpicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/blockpicker/blockpicker.controller.js @@ -1,26 +1,35 @@ //used for the media picker dialog angular.module("umbraco") .controller("Umbraco.Editors.BlockPickerController", - function ($scope) { + function ($scope, localizationService) { var vm = this; - vm.navigation = [ - { - "alias": "empty", - "name": "Create empty", - "icon": "icon-add", - "active": true, - "view": "" - }, - { - "alias": "clipboard", - "name": "Clipboard", - "icon": "icon-paste-in", - "view": "" - } - ]; - vm.activeTab = vm.navigation[0]; + vm.navigation = []; + + + localizationService.localizeMany(["blockEditor_tabCreateEmpty", "blockEditor_tabClipboard"]).then( + function (data) { + + vm.navigation = [{ + "alias": "empty", + "name": data[0], + "icon": "icon-add", + "active": true, + "view": "" + }, + { + "alias": "clipboard", + "name": data[1], + "icon": "icon-paste-in", + "view": "" + }]; + + vm.activeTab = vm.navigation[0]; + } + ); + + vm.onNavigationChanged = function(tab) { vm.activeTab.active = false; vm.activeTab = tab; diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml index acd63dea4f..d3e410e39c 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml @@ -2422,5 +2422,7 @@ To manage your website, simply open the Umbraco back office and start adding con Thumbnail Add thumbnail + Create empty + Clipboard 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 d370de60bf..5c833e84a5 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml @@ -2433,5 +2433,7 @@ To manage your website, simply open the Umbraco back office and start adding con Thumbnail Add thumbnail + Create empty + Clipboard From 3647cc366a70a952fbbb695261599b26b069b440 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Wed, 15 Apr 2020 15:07:32 +0200 Subject: [PATCH 126/377] Slightly change for shadow on block-picker item hover --- .../blockeditor/blockcard/blockcard.component.less | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blockeditor/blockcard/blockcard.component.less b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blockeditor/blockcard/blockcard.component.less index 703c1bca26..d269a87b48 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blockeditor/blockcard/blockcard.component.less +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blockeditor/blockcard/blockcard.component.less @@ -11,8 +11,14 @@ umb-block-card { box-shadow: 0 1px 2px rgba(0,0,0,.2); overflow: hidden; + transition: box-shadow 120ms; + cursor: pointer; + &:hover { + box-shadow: 0 1px 3px rgba(@ui-action-type-hover, .5); + } + &.--isOpen { &::after { content: ""; From 9fa34764ac6c662d781aefc22eefcb54646884f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Wed, 15 Apr 2020 15:18:48 +0200 Subject: [PATCH 127/377] Localization of BlockEditor Settings Tab --- .../blockeditor/blockeditor.controller.js | 20 +++++++++++-------- src/Umbraco.Web.UI/Umbraco/config/lang/en.xml | 1 + 2 files changed, 13 insertions(+), 8 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 c5e3776e91..8db36220bd 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 @@ -32,16 +32,20 @@ angular.module("umbraco") } if (vm.settings && vm.settings.variants) { - var settingsTab = { - "name": "Settings", - "alias": "settings", - "icon": "icon-settings", - "view": "views/common/infiniteeditors/elementeditor/elementeditor.settings.html" - }; - vm.tabs.push(settingsTab); + localizationService.localize("blockEditor_tabBlockSettings").then( + function (settingsName) { + var settingsTab = { + "name": settingsName, + "alias": "settings", + "icon": "icon-settings", + "view": "views/common/infiniteeditors/elementeditor/elementeditor.settings.html" + }; + vm.tabs.push(settingsTab); + } + ); } - // activate frst app: + // activate first app: if (vm.tabs.length > 0) { vm.tabs[0].active = true; } diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml index d3e410e39c..1902ec4dbf 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml @@ -2424,5 +2424,6 @@ To manage your website, simply open the Umbraco back office and start adding con Add thumbnail Create empty Clipboard + Settings From e9a2b92658ad12675609892f1504d4dd0c14f34d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Wed, 15 Apr 2020 15:20:45 +0200 Subject: [PATCH 128/377] localizationService --- .../infiniteeditors/blockeditor/blockeditor.controller.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 8db36220bd..81d9bb754d 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 @@ -1,7 +1,7 @@ //used for the media picker dialog angular.module("umbraco") .controller("Umbraco.Editors.BlockEditorController", - function ($scope) { + function ($scope, localizationService) { var vm = this; vm.content = $scope.model.content; From a19ebaa8d02c9101269e065f24b4f52a98117853 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Wed, 15 Apr 2020 15:51:55 +0200 Subject: [PATCH 129/377] only filter when more than 8 items available --- .../src/views/propertyeditors/blocklist/blocklist.component.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.js index 69bfb1a71a..185b22d764 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.js @@ -238,6 +238,7 @@ orderBy: "$index", view: "views/common/infiniteeditors/blockpicker/blockpicker.html", size: (amountOfAvailableTypes > 8 ? "medium" : "small"), + filter: (amountOfAvailableTypes > 8), clickPasteItem: function(item) { if (item.type === "elementTypeArray") { var indexIncrementor = 0; From 5236dbff7536fc8120f6b3440efb2cf00fd05616 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Wed, 15 Apr 2020 15:58:26 +0200 Subject: [PATCH 130/377] Add multiple blocks if hold down CTRL or SuperKey --- .../blockpicker/blockpicker.controller.js | 4 ++-- .../blockpicker/blockpicker.html | 4 ++-- .../blocklist/blocklist.component.js | 17 +++++++++++------ 3 files changed, 15 insertions(+), 10 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/blockpicker/blockpicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/blockpicker/blockpicker.controller.js index 1deb010073..77be5b741a 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/blockpicker/blockpicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/blockpicker/blockpicker.controller.js @@ -44,9 +44,9 @@ angular.module("umbraco") vm.model = $scope.model; - vm.selectItem = function(item) { + vm.selectItem = function(item, $event) { vm.model.selectedItem = item; - vm.model.submit($scope.model); + vm.model.submit($scope.model, $event); } vm.close = function() { diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/blockpicker/blockpicker.html b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/blockpicker/blockpicker.html index 5206c7e66d..196eec4cdb 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/blockpicker/blockpicker.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/blockpicker/blockpicker.html @@ -33,7 +33,7 @@ ng-repeat="block in vm.model.availableItems | filter:searchTerm" block-config-model="block.blockConfigModel" element-type-model="block.elementTypeModel" - ng-click="vm.selectItem(block)"> + ng-click="vm.selectItem(block, $event)">
    @@ -55,7 +55,7 @@ block-config-model="block.blockConfigModel" element-type-model="block.elementTypeModel" ng-repeat="block in vm.model.clipboardItems" - ng-click="vm.model.clickPasteItem(block)"> + ng-click="vm.model.clickPasteItem(block, $event)">
    diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.js index 185b22d764..efc0e49d21 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.js @@ -239,7 +239,7 @@ view: "views/common/infiniteeditors/blockpicker/blockpicker.html", size: (amountOfAvailableTypes > 8 ? "medium" : "small"), filter: (amountOfAvailableTypes > 8), - clickPasteItem: function(item) { + clickPasteItem: function(item, mouseEvent) { if (item.type === "elementTypeArray") { var indexIncrementor = 0; item.pasteData.forEach(function (entry) { @@ -250,16 +250,21 @@ } else { requestPasteFromClipboard(createIndex, item.pasteData); } - blockPickerModel.close(); + if(!(mouseEvent.ctrlKey || mouseEvent.metaKey)) { + blockPickerModel.close(); + } }, - submit: function(blockPickerModel) { + submit: function(blockPickerModel, mouseEvent) { var added = false; if (blockPickerModel && blockPickerModel.selectedItem) { added = addNewBlock(createIndex, blockPickerModel.selectedItem.blockConfigModel.contentTypeAlias); } - blockPickerModel.close(); - if (added && vm.model.config.useInlineEditingAsDefault !== true && vm.blocks.length > createIndex) { - editBlock(vm.blocks[createIndex]); + + if(!(mouseEvent.ctrlKey || mouseEvent.metaKey)) { + blockPickerModel.close(); + if (added && vm.model.config.useInlineEditingAsDefault !== true && vm.blocks.length > createIndex) { + editBlock(vm.blocks[createIndex]); + } } }, close: function() { From efcce3d25d4efa582773495bd0718114e2983042 Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 17 Apr 2020 10:43:18 +1000 Subject: [PATCH 131/377] adds notes --- .../Migrations/Upgrade/V_8_7_0/ConvertToElements.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_7_0/ConvertToElements.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_7_0/ConvertToElements.cs index 2bc017b643..fbcd55ec92 100644 --- a/src/Umbraco.Core/Migrations/Upgrade/V_8_7_0/ConvertToElements.cs +++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_7_0/ConvertToElements.cs @@ -36,6 +36,8 @@ namespace Umbraco.Core.Migrations.Upgrade.V_8_7_0 elementTypeIds = elementTypeIds.Union(parentElementTypeIds).ToList(); // Convert all those document types to element type + // TODO: We need to wait on an update from @benjaminc to make this 'safe' + // see https://github.com/umbraco/Umbraco-CMS/pull/7910#discussion_r409927495 foreach (var docType in docTypes) { if (!elementTypeIds.Contains(docType.NodeId)) continue; From dcce1f4a2e97dc241aaa6427aa645e5d24a6093f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Wed, 22 Apr 2020 15:18:25 +0200 Subject: [PATCH 132/377] ability to add a scoped stylesheet for block view --- .../imageblock/imageblock.editor.html | 7 ++- .../inlineblock/inlineblock.editor.html | 4 +- .../labelblock/labelblock.editor.html | 2 +- .../textareablock/textareablock.editor.html | 3 -- .../blocklist/blocklist.block.component.js | 36 ++++++++++++++ .../blocklist/blocklist.component.html | 7 ++- .../blocklist/blocklist.component.js | 22 +++++++-- .../blocklist.scopedblock.component.js | 44 +++++++++++++++++ ...t.blockconfiguration.overlay.controller.js | 49 ++++++++++++++++--- .../blocklist.blockconfiguration.overlay.html | 21 ++++++++ .../PropertyEditors/BlockListConfiguration.cs | 3 ++ 11 files changed, 175 insertions(+), 23 deletions(-) create mode 100644 src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.block.component.js create mode 100644 src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.scopedblock.component.js diff --git a/src/Umbraco.Web.UI.Client/src/views/blockelements/imageblock/imageblock.editor.html b/src/Umbraco.Web.UI.Client/src/views/blockelements/imageblock/imageblock.editor.html index 32db64b16b..7f09aab525 100644 --- a/src/Umbraco.Web.UI.Client/src/views/blockelements/imageblock/imageblock.editor.html +++ b/src/Umbraco.Web.UI.Client/src/views/blockelements/imageblock/imageblock.editor.html @@ -1,9 +1,8 @@ diff --git a/src/Umbraco.Web.UI.Client/src/views/blockelements/inlineblock/inlineblock.editor.html b/src/Umbraco.Web.UI.Client/src/views/blockelements/inlineblock/inlineblock.editor.html index 028a10d434..d61d343a64 100644 --- a/src/Umbraco.Web.UI.Client/src/views/blockelements/inlineblock/inlineblock.editor.html +++ b/src/Umbraco.Web.UI.Client/src/views/blockelements/inlineblock/inlineblock.editor.html @@ -1,5 +1,5 @@ -
    - diff --git a/src/Umbraco.Web.UI.Client/src/views/blockelements/textareablock/textareablock.editor.html b/src/Umbraco.Web.UI.Client/src/views/blockelements/textareablock/textareablock.editor.html index ca49a3f5b8..06f7cf49d3 100644 --- a/src/Umbraco.Web.UI.Client/src/views/blockelements/textareablock/textareablock.editor.html +++ b/src/Umbraco.Web.UI.Client/src/views/blockelements/textareablock/textareablock.editor.html @@ -6,9 +6,6 @@ diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.block.component.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.block.component.js new file mode 100644 index 0000000000..72229e773f --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.block.component.js @@ -0,0 +1,36 @@ +(function () { + "use strict"; + + + /** + * @ngdoc component + * @name Umbraco.umbBlockListBlockContent + * @function + * + * @description + * The component for a style-inheriting block of the block list property editor. + */ + angular + .module("umbraco") + .component("umbBlockListBlockContent", { + template: '
    ', + controller: BlockListBlockContentController, + controllerAs: "vm", + bindings: { + view: "@", + block: "=", + api: "=" + } + } + ); + + function BlockListBlockContentController($scope) { + var vm = this; + vm.$onInit = function() { + $scope.block = vm.block; + $scope.api = vm.api; + }; + } + + +})(); diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.html index 360201c27d..9a14f1148b 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.html @@ -18,8 +18,11 @@
    -
    -
    + + + + +
    + +
    +
    + +
    +
    + + +
    + +
    +
    + +
    +
    +
    +
    diff --git a/src/Umbraco.Web/PropertyEditors/BlockListConfiguration.cs b/src/Umbraco.Web/PropertyEditors/BlockListConfiguration.cs index fba80bd57d..5fc016f694 100644 --- a/src/Umbraco.Web/PropertyEditors/BlockListConfiguration.cs +++ b/src/Umbraco.Web/PropertyEditors/BlockListConfiguration.cs @@ -47,6 +47,9 @@ namespace Umbraco.Web.PropertyEditors [JsonProperty("view")] public string View { get; set; } + [JsonProperty("stylesheet")] + public string Stylesheet { get; set; } + [JsonProperty("label")] public string Label { get; set; } From acc90011f6a033326fb72ec6ffa2d711e0dd4643 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Thu, 23 Apr 2020 07:37:17 +0200 Subject: [PATCH 133/377] make scoped block draggable + style adjustments --- .../views/blockelements/inlineblock/inlineblock.editor.less | 1 + .../views/blockelements/labelblock/labelblock.editor.less | 5 +++-- .../views/propertyeditors/blocklist/blocklist.component.html | 4 ++-- .../views/propertyeditors/blocklist/blocklist.component.less | 2 +- 4 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/blockelements/inlineblock/inlineblock.editor.less b/src/Umbraco.Web.UI.Client/src/views/blockelements/inlineblock/inlineblock.editor.less index 2702ca163a..c1e2c72de3 100644 --- a/src/Umbraco.Web.UI.Client/src/views/blockelements/inlineblock/inlineblock.editor.less +++ b/src/Umbraco.Web.UI.Client/src/views/blockelements/inlineblock/inlineblock.editor.less @@ -1,5 +1,6 @@ .blockelement-inlineblock-editor { + display: block; margin-bottom: 4px; margin-top: 4px; border: 1px solid @gray-9; diff --git a/src/Umbraco.Web.UI.Client/src/views/blockelements/labelblock/labelblock.editor.less b/src/Umbraco.Web.UI.Client/src/views/blockelements/labelblock/labelblock.editor.less index 5ce53aece4..ce5883df6a 100644 --- a/src/Umbraco.Web.UI.Client/src/views/blockelements/labelblock/labelblock.editor.less +++ b/src/Umbraco.Web.UI.Client/src/views/blockelements/labelblock/labelblock.editor.less @@ -1,5 +1,8 @@ .blockelement-labelblock-editor { + display: block; + margin-bottom: 4px; + margin-top: 4px; width: 100%; min-height: 48px; border: 1px solid @gray-9; @@ -11,8 +14,6 @@ text-align: left; padding-left: 20px; padding-bottom: 2px; - margin-bottom: 2px; - margin-top: 2px; user-select: none; diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.html index 9a14f1148b..86a1f549af 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.html @@ -19,9 +19,9 @@
    - + - +
    diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.less b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.less index d118e6356c..35eece038e 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.less +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.less @@ -71,7 +71,7 @@ opacity: 0; outline: none; height: 12px; - margin-top: -7px; + margin-top: -9px; padding-top: 6px; margin-bottom: -6px; transition: opacity 240ms; From 31db52fe1be89a916a8be9f5aaed2afe7b96ffaa Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Thu, 23 Apr 2020 10:22:55 +0200 Subject: [PATCH 134/377] provide index for custom view --- .../propertyeditors/blocklist/blocklist.block.component.js | 5 +++-- .../views/propertyeditors/blocklist/blocklist.component.html | 4 ++-- .../blocklist/blocklist.scopedblock.component.js | 5 +++-- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.block.component.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.block.component.js index 72229e773f..0385460864 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.block.component.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.block.component.js @@ -15,11 +15,12 @@ .component("umbBlockListBlockContent", { template: '
    ', controller: BlockListBlockContentController, - controllerAs: "vm", + controllerAs: "model", bindings: { view: "@", block: "=", - api: "=" + api: "=", + index: "<" } } ); diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.html index 86a1f549af..070ee4839b 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.html @@ -19,9 +19,9 @@
    - + - +
    diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.scopedblock.component.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.scopedblock.component.js index b4f815a7ce..0498db32c3 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.scopedblock.component.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.scopedblock.component.js @@ -14,12 +14,13 @@ .module("umbraco") .component("umbBlockListScopedBlockContent", { controller: BlockListScopedBlockContentController, - controllerAs: "vm", + controllerAs: "model", bindings: { stylesheet: "@", view: "@", block: "=", - api: "=" + api: "=", + index: "<" } } ); From 04a77aa7c0a2ef9ef7d43407e8c590510e3a4384 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Thu, 23 Apr 2020 10:23:49 +0200 Subject: [PATCH 135/377] rename contentModel to data + rename layoutModel to layout --- .../common/services/blockeditor.service.js | 54 +++++++++---------- .../blocklist/blocklist.component.js | 8 +-- .../services/block-editor-service.spec.js | 6 +-- 3 files changed, 34 insertions(+), 34 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 67def3c75a..c6897e63a2 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 @@ -9,7 +9,7 @@ * Simple mapping from property model content entry to editing model, * needs to stay simple to avoid deep watching. */ - function mapToElementModel(elementModel, contentModel) { + function mapToElementModel(elementModel, dataModel) { var variant = elementModel.variants[0]; @@ -18,8 +18,8 @@ for (var p = 0; p < tab.properties.length; p++) { var prop = tab.properties[p]; - if (contentModel[prop.alias]) { - prop.value = contentModel[prop.alias]; + if (dataModel[prop.alias]) { + prop.value = dataModel[prop.alias]; } } } @@ -29,7 +29,7 @@ * Simple mapping from elementModel to property model content entry, * needs to stay simple to avoid deep watching. */ - function mapToPropertyModel(elementModel, contentModel) { + function mapToPropertyModel(elementModel, dataModel) { var variant = elementModel.variants[0]; @@ -39,14 +39,14 @@ for (var p = 0; p < tab.properties.length; p++) { var prop = tab.properties[p]; if (prop.value) { - contentModel[prop.alias] = prop.value; + dataModel[prop.alias] = prop.value; } } } } - function mapValueToPropertyModel(value, alias, contentModel) { - contentModel[alias] = value; + function mapValueToPropertyModel(value, alias, dataModel) { + dataModel[alias] = value; } /** @@ -88,8 +88,8 @@ function getBlockLabel(blockModel) { if(blockModel.labelInterpolator) { - // We are just using the contentModel, since its a key/value object that is live synced. (if we need to add additional vars, we could make a shallow copy and apply those.) - return blockModel.labelInterpolator(blockModel.contentModel); + // We are just using the data model, since its a key/value object that is live synced. (if we need to add additional vars, we could make a shallow copy and apply those.) + return blockModel.labelInterpolator(blockModel.data); } return blockModel.content.contentTypeName; } @@ -105,7 +105,7 @@ // Start watching each property value. var variant = model.variants[0]; var field = forSettings ? "settings" : "content"; - var watcherCreator = forSettings ? createSettingsModelPropWatcher : createContentModelPropWatcher; + var watcherCreator = forSettings ? createSettingsModelPropWatcher : createDataModelPropWatcher; for (var t = 0; t < variant.tabs.length; t++) { var tab = variant.tabs[t]; for (var p = 0; p < tab.properties.length; p++) { @@ -123,10 +123,10 @@ /** * Used to create a scoped watcher for a content property on a blockModel. */ - function createContentModelPropWatcher(blockModel, prop) { + function createDataModelPropWatcher(blockModel, prop) { return function() { // sync data: - blockModel.contentModel[prop.alias] = prop.value; + blockModel.data[prop.alias] = prop.value; // regenerate label. // TODO: could use a debounce. @@ -140,7 +140,7 @@ function createSettingsModelPropWatcher(blockModel, prop) { return function() { // sync data: - blockModel.layoutModel.settings[prop.alias] = prop.value; + blockModel.layout.settings[prop.alias] = prop.value; } } @@ -265,14 +265,14 @@ var udi = layoutEntry.udi; - var contentModel = this._getDataByUdi(udi); + var dataModel = this._getDataByUdi(udi); - if (contentModel === null) { + if (dataModel === null) { console.error("Couldnt find content model of "+udi) return null; } - var blockConfiguration = this.getBlockConfiguration(contentModel.contentTypeAlias); + var blockConfiguration = this.getBlockConfiguration(dataModel.contentTypeAlias); if (blockConfiguration === null) { console.error("The block entry of "+udi+" is not begin initialized cause its contentTypeAlias is not allowed for this PropertyEditor") @@ -296,10 +296,10 @@ blockModel.content = angular.copy(contentScaffold); blockModel.content.udi = udi; - mapToElementModel(blockModel.content, contentModel); + mapToElementModel(blockModel.content, dataModel); - blockModel.contentModel = contentModel; - blockModel.layoutModel = layoutEntry; + blockModel.data = dataModel; + blockModel.layout = layoutEntry; blockModel.watchers = []; if (blockConfiguration.settingsElementTypeAlias) { @@ -353,10 +353,10 @@ var udi = blockModel.content.key; - mapToPropertyModel(blockModel.content, blockModel.contentModel); + mapToPropertyModel(blockModel.content, blockModel.data); // TODO: implement settings, sync settings to layout entry. - // mapToPropertyModel(blockModel.settings, blockModel.layoutModel.settings) + // mapToPropertyModel(blockModel.settings, blockModel.layout.settings) }, @@ -398,23 +398,23 @@ * Insert data from ElementType Model * @return {Object} Layout entry object, to be inserted at a decired location in the layout object. */ - createFromElementType: function(elementTypeContentModel) { + createFromElementType: function(elementTypeDataModel) { - elementTypeContentModel = angular.copy(elementTypeContentModel); + elementTypeDataModel = angular.copy(elementTypeDataModel); - var contentTypeAlias = elementTypeContentModel.contentTypeAlias; + var contentTypeAlias = elementTypeDataModel.contentTypeAlias; var layoutEntry = this.create(contentTypeAlias); if(layoutEntry === null) { return null; } - var contentModel = this._getDataByUdi(layoutEntry.udi); - if(contentModel === null) { + var dataModel = this._getDataByUdi(layoutEntry.udi); + if(dataModel === null) { return null; } - mapToPropertyModel(elementTypeContentModel, contentModel); + mapToPropertyModel(elementTypeDataModel, dataModel); return layoutEntry; diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.js index c302856807..4115523e73 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.js @@ -200,17 +200,17 @@ function editBlock(blockModel, hideContent) { // make a clone to avoid editing model directly. - var blockContentModelClone = angular.copy(blockModel.content); + var blockContentClone = angular.copy(blockModel.content); var blockSettingsModelClone = null; if (blockModel.config.settingsElementTypeAlias) { - blockSettingsModelClone = angular.copy(blockModel.settings); + blockSettingsClone = angular.copy(blockModel.settings); } var blockEditorModel = { - content: blockContentModelClone, + content: blockContentClone, hideContent: hideContent, - settings: blockSettingsModelClone, + settings: blockSettingsClone, title: blockModel.label, view: "views/common/infiniteeditors/blockeditor/blockeditor.html", size: blockModel.config.editorSize || "medium", diff --git a/src/Umbraco.Web.UI.Client/test/unit/common/services/block-editor-service.spec.js b/src/Umbraco.Web.UI.Client/test/unit/common/services/block-editor-service.spec.js index 6abf85c9b6..094fd33558 100644 --- a/src/Umbraco.Web.UI.Client/test/unit/common/services/block-editor-service.spec.js +++ b/src/Umbraco.Web.UI.Client/test/unit/common/services/block-editor-service.spec.js @@ -110,7 +110,7 @@ var blockModel = modelObject.getBlockModel(layout[0]); expect(blockModel).not.toBeUndefined(); - expect(blockModel.contentModel.udi).toBe(propertyModelMock.data[0].udi); + expect(blockModel.data.udi).toBe(propertyModelMock.data[0].udi); expect(blockModel.content.variants[0].tabs[0].properties[0].value).toBe(propertyModelMock.data[0].testproperty); done(); @@ -135,8 +135,8 @@ $rootScope.$digest();// invoke angularJS Store. - expect(blockModel.contentModel).toBe(propertyModel.data[0]); - expect(blockModel.contentModel.testproperty).toBe("anotherTestValue"); + expect(blockModel.data).toBe(propertyModel.data[0]); + expect(blockModel.data.testproperty).toBe("anotherTestValue"); expect(propertyModel.data[0].testproperty).toBe("anotherTestValue"); // From fc8fc468842abee57c3c5f9f0d0fa1ceb511097c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Thu, 23 Apr 2020 11:01:10 +0200 Subject: [PATCH 136/377] clean up --- .../views/propertyeditors/blocklist/blocklist.component.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.js index 4115523e73..1cb0a0c2d6 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.js @@ -26,7 +26,7 @@ } }); - function BlockListController($scope, $interpolate, editorService, clipboardService, localizationService, overlayService, blockEditorService) { + function BlockListController($scope, editorService, clipboardService, localizationService, overlayService, blockEditorService) { var unsubscribe = []; var modelObject; @@ -201,7 +201,7 @@ // make a clone to avoid editing model directly. var blockContentClone = angular.copy(blockModel.content); - var blockSettingsModelClone = null; + var blockSettingsClone = null; if (blockModel.config.settingsElementTypeAlias) { blockSettingsClone = angular.copy(blockModel.settings); From 37e37a14cec966c511c78551115b36fb01818c9d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Mon, 27 Apr 2020 12:34:15 +0200 Subject: [PATCH 137/377] more localization --- ...blocklist.blockconfiguration.controller.js | 52 +++--- ...t.blockconfiguration.overlay.controller.js | 171 ++++++++++-------- src/Umbraco.Web.UI/Umbraco/config/lang/en.xml | 8 + .../Umbraco/config/lang/en_us.xml | 9 + 4 files changed, 139 insertions(+), 101 deletions(-) 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 494ef35137..29ea90c0f3 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 @@ -92,32 +92,36 @@ var availableItems = vm.getAvailableElementTypes() - var elemTypeSelectorOverlay = { - view: "itempicker", - title: "no title jet", - availableItems: availableItems, - selectedItems: selectedItems, - createNewItem: { - action: function() { - overlayService.close(); - vm.createElementTypeAndAdd(vm.addBlockFromElementTypeAlias); + localizationService.localizeMany(["blockEditor_headlineCreateBlock", "blockEditor_labelcreateNewElementType"]).then(function(localized) { + + var elemTypeSelectorOverlay = { + view: "itempicker", + title: localizedlocalized[0], + availableItems: availableItems, + selectedItems: selectedItems, + createNewItem: { + action: function() { + overlayService.close(); + vm.createElementTypeAndAdd(vm.addBlockFromElementTypeAlias); + }, + icon: "icon-add", + name: localizedlocalized[1] }, - icon: "icon-add", - name: "Create new" - }, - position: "target", - event: $event, - size: availableItems.length < 7 ? "small" : "medium", - submit: function (overlay) { - vm.addBlockFromElementTypeAlias(overlay.selectedItem.alias); - overlayService.close(); - }, - close: function () { - overlayService.close(); - } - }; + position: "target", + event: $event, + size: availableItems.length < 7 ? "small" : "medium", + submit: function (overlay) { + vm.addBlockFromElementTypeAlias(overlay.selectedItem.alias); + overlayService.close(); + }, + close: function () { + overlayService.close(); + } + }; - overlayService.open(elemTypeSelectorOverlay); + overlayService.open(elemTypeSelectorOverlay); + + }); }; vm.createElementTypeAndAdd = function(callback) { diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/blocklist.blockconfiguration.overlay.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/blocklist.blockconfiguration.overlay.controller.js index 055993f164..0eb8597cc1 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/blocklist.blockconfiguration.overlay.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/blocklist.blockconfiguration.overlay.controller.js @@ -46,33 +46,37 @@ vm.addSettingsForBlock = function ($event, block) { - var elemTypeSelectorOverlay = { - view: "itempicker", - title: "Pick settings (missing translation)", - availableItems: vm.elementTypes, - position: "target", - event: $event, - size: vm.elementTypes.length < 7 ? "small" : "medium", - createNewItem: { - action: function() { - overlayService.close(); - vm.createElementTypeAndAdd((alias) => { - vm.applySettingsToBlock(block, alias); - }); - }, - icon: "icon-add", - name: "Create new" - }, - submit: function (overlay) { - vm.applySettingsToBlock(block, overlay.selectedItem.alias); - overlayService.close(); - }, - close: function () { - overlayService.close(); - } - }; + localizationService.localizeMany(["blockEditor_headlineAddSettingsElementType", "blockEditor_labelcreateNewElementType"]).then(function(localized) { - overlayService.open(elemTypeSelectorOverlay); + var elemTypeSelectorOverlay = { + view: "itempicker", + title: localized[0], + availableItems: vm.elementTypes, + position: "target", + event: $event, + size: vm.elementTypes.length < 7 ? "small" : "medium", + createNewItem: { + action: function() { + overlayService.close(); + vm.createElementTypeAndAdd((alias) => { + vm.applySettingsToBlock(block, alias); + }); + }, + icon: "icon-add", + name: localized[1] + }, + submit: function (overlay) { + vm.applySettingsToBlock(block, overlay.selectedItem.alias); + overlayService.close(); + }, + close: function () { + overlayService.close(); + } + }; + + overlayService.open(elemTypeSelectorOverlay); + + }); }; vm.applySettingsToBlock = function(block, alias) { block.settingsElementTypeAlias = alias; @@ -102,23 +106,27 @@ vm.addViewForBlock = function(block) { - const filePicker = { - title: "Select view (TODO need translation)", - section: "settings", - treeAlias: "files", - entityType: "file", - isDialog: true, - select: function (node) { - console.log(node) - const filepath = decodeURIComponent(node.id.replace(/\+/g, " ")); - block.view = filepath; - editorService.close(); - }, - close: function () { - editorService.close(); - } - }; - editorService.treePicker(filePicker); + localizationService.localize("blockEditor_headlineSelectView").then(function(localizedTitle) { + + const filePicker = { + title: localizedTitle, + section: "settings", + treeAlias: "files", + entityType: "file", + isDialog: true, + select: function (node) { + console.log(node) + const filepath = decodeURIComponent(node.id.replace(/\+/g, " ")); + block.view = filepath; + editorService.close(); + }, + close: function () { + editorService.close(); + } + }; + editorService.treePicker(filePicker); + + }); } vm.requestRemoveViewForBlock = function(block) { localizationService.localizeMany(["general_remove", "defaultdialogs_confirmremoveusageof"]).then(function (data) { @@ -142,22 +150,26 @@ vm.addStylesheetForBlock = function(block) { - const filePicker = { - title: "Select Stylesheet (TODO need translation)", - section: "settings", - treeAlias: "files", - entityType: "file", - isDialog: true, - select: function (node) { - const filepath = decodeURIComponent(node.id.replace(/\+/g, " ")); - block.stylesheet = filepath; - editorService.close(); - }, - close: function () { - editorService.close(); - } - }; - editorService.treePicker(filePicker); + localizationService.localize("blockEditor_headlineAddCustomStylesheet").then(function(localizedTitle) { + + const filePicker = { + title: localizedTitle, + section: "settings", + treeAlias: "files", + entityType: "file", + isDialog: true, + select: function (node) { + const filepath = decodeURIComponent(node.id.replace(/\+/g, " ")); + block.stylesheet = filepath; + editorService.close(); + }, + close: function () { + editorService.close(); + } + }; + editorService.treePicker(filePicker); + + }); } vm.requestRemoveStylesheetForBlock = function(block) { localizationService.localizeMany(["general_remove", "defaultdialogs_confirmremoveusageof"]).then(function (data) { @@ -181,24 +193,29 @@ vm.addThumbnailForBlock = function(block) { - const thumbnailPicker = { - title: "Select thumbnail (TODO need translation)", - section: "settings", - treeAlias: "files", - entityType: "file", - isDialog: true, - filter: function (i) { - return !(i.name.indexOf(".jpg") !== -1 || i.name.indexOf(".jpeg") !== -1 || i.name.indexOf(".png") !== -1 || i.name.indexOf(".svg") !== -1 || i.name.indexOf(".webp") !== -1 || i.name.indexOf(".gif") !== -1); - }, - select: function (file) { - block.thumbnail = file.name; - editorService.close(); - }, - close: function () { - editorService.close(); - } - }; - editorService.treePicker(thumbnailPicker); + + localizationService.localize("blockEditor_headlineAddThumbnail").then(function(localizedTitle) { + + const thumbnailPicker = { + title: localizedTitle, + section: "settings", + treeAlias: "files", + entityType: "file", + isDialog: true, + filter: function (i) { + return !(i.name.indexOf(".jpg") !== -1 || i.name.indexOf(".jpeg") !== -1 || i.name.indexOf(".png") !== -1 || i.name.indexOf(".svg") !== -1 || i.name.indexOf(".webp") !== -1 || i.name.indexOf(".gif") !== -1); + }, + select: function (file) { + block.thumbnail = file.name; + editorService.close(); + }, + close: function () { + editorService.close(); + } + }; + editorService.treePicker(thumbnailPicker); + + }); } vm.removeThumbnailForBlock = function(entry) { entry.thumbnail = null; diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml index 1902ec4dbf..e617f9b07f 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml @@ -2404,6 +2404,14 @@ To manage your website, simply open the Umbraco back office and start adding con Create forms using an intuitive drag and drop interface. From simple contact forms that sends e-mails to advanced questionaires that integrate with CRM systems. Your clients will love it! + Create new block + Attach a settings section + Select view + Select stylesheet + Choose thumbnail + Create new + Custom stylesheet + Add stylesheet Block apperance Data models Showcase 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 5c833e84a5..df3cd8c4fe 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml @@ -2415,6 +2415,14 @@ To manage your website, simply open the Umbraco back office and start adding con Create forms using an intuitive drag and drop interface. From simple contact forms that sends e-mails to advanced questionaires that integrate with CRM systems. Your clients will love it! + Create new block + Attach a settings section + Select view + Select stylesheet + Choose thumbnail + Create new + Custom stylesheet + Add stylesheet Block apperance Data models Showcase @@ -2435,5 +2443,6 @@ To manage your website, simply open the Umbraco back office and start adding con Add thumbnail Create empty Clipboard + Settings From 223e8b122e62cb9d9df4307d2f122f28a03c9a97 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Mon, 27 Apr 2020 12:34:54 +0200 Subject: [PATCH 138/377] openSettings option for block editor --- .../blockeditor/blockeditor.controller.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 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 81d9bb754d..9fb32339f3 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 @@ -23,12 +23,14 @@ angular.module("umbraco") if($scope.model.hideContent) { apps.splice(apps.indexOf(contentApp), 1); + } else if ($scope.model.openSettings !== true) { + contentApp.active = true; } // remove info app: var infoAppIndex = apps.findIndex(entry => entry.alias === "umbInfo"); apps.splice(infoAppIndex, 1); - + } if (vm.settings && vm.settings.variants) { @@ -41,15 +43,13 @@ angular.module("umbraco") "view": "views/common/infiniteeditors/elementeditor/elementeditor.settings.html" }; vm.tabs.push(settingsTab); + if ($scope.model.openSettings) { + settingsTab.active = true; + } } ); } - // activate first app: - if (vm.tabs.length > 0) { - vm.tabs[0].active = true; - } - vm.submitAndClose = function () { if ($scope.model && $scope.model.submit) { $scope.model.submit($scope.model); From 5ef98ada8c0cbcb52954c2eeea2f2432773725e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Mon, 27 Apr 2020 12:36:25 +0200 Subject: [PATCH 139/377] minor changes for a better developer experience --- .../infiniteeditors/blockpicker/blockpicker.html | 1 + .../blocklist/blocklist.block.component.js | 10 +++++----- .../blocklist/blocklist.component.html | 12 ++++++------ .../propertyeditors/blocklist/blocklist.component.js | 10 ++++++++-- .../blocklist/blocklist.component.less | 1 + .../blocklist/blocklist.scopedblock.component.js | 12 ++++++------ 6 files changed, 27 insertions(+), 19 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/blockpicker/blockpicker.html b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/blockpicker/blockpicker.html index 196eec4cdb..fb7e946ee7 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/blockpicker/blockpicker.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/blockpicker/blockpicker.html @@ -30,6 +30,7 @@
    ', + template: '
    ', controller: BlockListBlockContentController, controllerAs: "model", bindings: { @@ -26,10 +26,10 @@ ); function BlockListBlockContentController($scope) { - var vm = this; - vm.$onInit = function() { - $scope.block = vm.block; - $scope.api = vm.api; + var model = this; + model.$onInit = function() { + $scope.block = model.block; + $scope.api = model.api; }; } diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.html index 070ee4839b..4167667af0 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.html @@ -19,25 +19,25 @@
    - + - +
    - - - diff --git a/src/Umbraco.Web.UI.Client/src/views/blockelements/imageblock/imageblock.editor.less b/src/Umbraco.Web.UI.Client/src/views/blockelements/imageblock/imageblock.editor.less deleted file mode 100644 index 2ea03bd703..0000000000 --- a/src/Umbraco.Web.UI.Client/src/views/blockelements/imageblock/imageblock.editor.less +++ /dev/null @@ -1,15 +0,0 @@ -.blockelement-imageblock-editor { - - width: 100%; - min-height: 42px; - padding-bottom: 10px; - padding-top: 10px; - - img { - width: 100%; - max-width: 500px; - border: none; - resize: none; - border-radius: @baseBorderRadius; - } -} diff --git a/src/Umbraco.Web.UI.Client/src/views/blockelements/textareablock/textareablock.editor.controller.js b/src/Umbraco.Web.UI.Client/src/views/blockelements/textareablock/textareablock.editor.controller.js deleted file mode 100644 index 1b074a0cb6..0000000000 --- a/src/Umbraco.Web.UI.Client/src/views/blockelements/textareablock/textareablock.editor.controller.js +++ /dev/null @@ -1,19 +0,0 @@ -//used for the media picker dialog -angular.module("umbraco") -.controller("Umbraco.Editors.TextAreaBlockElementEditorController", - function ($scope) { - - var vm = this; - - vm.firstProperty = $scope.block.content.variants[0].tabs[0].properties[0]; - /* - vm.onBlur = function() { - if (vm.firstProperty.value === null || vm.firstProperty.value === "") { - $scope.blockApi.deleteBlock($scope.block); - } - } - */ - // TODO: if text is empty and user hits backspace, then remove this block. - } - -); diff --git a/src/Umbraco.Web.UI.Client/src/views/blockelements/textareablock/textareablock.editor.html b/src/Umbraco.Web.UI.Client/src/views/blockelements/textareablock/textareablock.editor.html deleted file mode 100644 index 06f7cf49d3..0000000000 --- a/src/Umbraco.Web.UI.Client/src/views/blockelements/textareablock/textareablock.editor.html +++ /dev/null @@ -1,12 +0,0 @@ - -
    - - - - - -
    diff --git a/src/Umbraco.Web.UI.Client/src/views/blockelements/textareablock/textareablock.editor.less b/src/Umbraco.Web.UI.Client/src/views/blockelements/textareablock/textareablock.editor.less deleted file mode 100644 index fe11d0cd0c..0000000000 --- a/src/Umbraco.Web.UI.Client/src/views/blockelements/textareablock/textareablock.editor.less +++ /dev/null @@ -1,27 +0,0 @@ -.blockelement-textareablock-editor { - - width: 100%; - padding-bottom: 10px; - padding-top: 10px; - - padding-left: 24px; - padding-right: 24px; - - min-height: 64px; - box-sizing: border-box; - - textarea { - display: block; - width: 100%; - max-width: 640px; - margin-left: auto; - margin-right: auto; - border: none; - resize: none; - overflow: auto; - - font-size: 18px; - font-family: Georgia,Cambria,"Times New Roman",Times,serif; - line-height: 1.25; - } -} diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.js index b7d6cb5315..a2218ceb82 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.js @@ -139,7 +139,7 @@ if (block === null) return null; // Lets apply fallback views, and make the view available directly on the blockModel. - block.view = (block.config.view ? "/" + block.config.view : (inlineEditing ? "views/blockelements/inlineblock/inlineblock.editor.html" : "views/blockelements/labelblock/labelblock.editor.html")); + block.view = (block.config.view ? "/" + block.config.view : (inlineEditing ? "views/propertyeditors/blocklist/blocklistentryeditors/inlineblock/inlineblock.editor.html" : "views/propertyeditors/blocklist/blocklistentryeditors/labelblock/labelblock.editor.html")); block.showSettings = block.config.settingsElementTypeAlias != null; diff --git a/src/Umbraco.Web.UI.Client/src/views/blockelements/inlineblock/inlineblock.editor.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklistentryeditors/inlineblock/inlineblock.editor.controller.js similarity index 100% rename from src/Umbraco.Web.UI.Client/src/views/blockelements/inlineblock/inlineblock.editor.controller.js rename to src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklistentryeditors/inlineblock/inlineblock.editor.controller.js diff --git a/src/Umbraco.Web.UI.Client/src/views/blockelements/inlineblock/inlineblock.editor.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklistentryeditors/inlineblock/inlineblock.editor.html similarity index 100% rename from src/Umbraco.Web.UI.Client/src/views/blockelements/inlineblock/inlineblock.editor.html rename to src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklistentryeditors/inlineblock/inlineblock.editor.html diff --git a/src/Umbraco.Web.UI.Client/src/views/blockelements/inlineblock/inlineblock.editor.less b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklistentryeditors/inlineblock/inlineblock.editor.less similarity index 100% rename from src/Umbraco.Web.UI.Client/src/views/blockelements/inlineblock/inlineblock.editor.less rename to src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklistentryeditors/inlineblock/inlineblock.editor.less diff --git a/src/Umbraco.Web.UI.Client/src/views/blockelements/labelblock/labelblock.editor.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklistentryeditors/labelblock/labelblock.editor.html similarity index 100% rename from src/Umbraco.Web.UI.Client/src/views/blockelements/labelblock/labelblock.editor.html rename to src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklistentryeditors/labelblock/labelblock.editor.html diff --git a/src/Umbraco.Web.UI.Client/src/views/blockelements/labelblock/labelblock.editor.less b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklistentryeditors/labelblock/labelblock.editor.less similarity index 96% rename from src/Umbraco.Web.UI.Client/src/views/blockelements/labelblock/labelblock.editor.less rename to src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklistentryeditors/labelblock/labelblock.editor.less index ce5883df6a..974662b416 100644 --- a/src/Umbraco.Web.UI.Client/src/views/blockelements/labelblock/labelblock.editor.less +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklistentryeditors/labelblock/labelblock.editor.less @@ -21,6 +21,7 @@ i { font-size: 22px; + margin-right: 5px; display: inline-block; vertical-align: middle; } From b6dfee3a91b72399915ce4c3799bcf51dc6f2112 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Fri, 29 May 2020 10:31:30 +0200 Subject: [PATCH 150/377] limit labelinterpretator to only runs ones pr. edit. and lets make sure to have a label thought we dont have any properties. --- .../src/common/services/blockeditor.service.js | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 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 c6897e63a2..6f12db6c1e 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 @@ -87,7 +87,7 @@ function getBlockLabel(blockModel) { - if(blockModel.labelInterpolator) { + if(blockModel.labelInterpolator !== undefined) { // We are just using the data model, since its a key/value object that is live synced. (if we need to add additional vars, we could make a shallow copy and apply those.) return blockModel.labelInterpolator(blockModel.data); } @@ -118,6 +118,10 @@ blockModel.watchers.push(isolatedScope.$watch("blockModels._" + blockModel.key + "." + field + ".variants[0].tabs[" + t + "].properties[" + p + "].value", watcherCreator(blockModel, prop))); } } + if (blockModel.watchers.length === 0) { + // If no watcher where created, it means we have no properties to watch. This means that nothing will activate our generate the label, since its only triggered by watchers. + blockModel.updateLabel(); + } } /** @@ -128,9 +132,7 @@ // sync data: blockModel.data[prop.alias] = prop.value; - // regenerate label. - // TODO: could use a debounce. - blockModel.label = getBlockLabel(blockModel); + blockModel.updateLabel(); } } @@ -283,9 +285,12 @@ var blockModel = {}; blockModel.key = String.CreateGuid().replace(/-/g, ""); blockModel.config = angular.copy(blockConfiguration); - if (blockModel.config.label) { + if (blockModel.config.label && blockModel.config.label !== "") { blockModel.labelInterpolator = $interpolate(blockModel.config.label); } + blockModel.updateLabel = _.debounce(function() { + this.label = getBlockLabel(this); + }, 100); var contentScaffold = this.getScaffoldFor(blockConfiguration.contentTypeAlias); if(contentScaffold === null) { From cb9f6144465031a59d27356f622305bcfbb9cc51 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Fri, 29 May 2020 10:59:33 +0200 Subject: [PATCH 151/377] fixed inline views gulp watcher --- .../gulp/tasks/watchTask.js | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/gulp/tasks/watchTask.js b/src/Umbraco.Web.UI.Client/gulp/tasks/watchTask.js index a94314abd6..810146d7c8 100644 --- a/src/Umbraco.Web.UI.Client/gulp/tasks/watchTask.js +++ b/src/Umbraco.Web.UI.Client/gulp/tasks/watchTask.js @@ -1,7 +1,7 @@ 'use strict'; const config = require('../config'); -const {watch, parallel, dest, src} = require('gulp'); +const {watch, series, parallel, dest, src} = require('gulp'); var _ = require('lodash'); var MergeStream = require('merge-stream'); @@ -33,16 +33,16 @@ function watchTask(cb) { var viewWatcher; _.forEach(config.sources.views, function (group) { if(group.watch !== false) { - viewWatcher = watch(group.files, { ignoreInitial: true, interval: watchInterval }); - viewWatcher.on('change', function(path, stats) { + viewWatcher = watch(group.files, { ignoreInitial: true, interval: watchInterval }, function() { console.log("copying " + group.files + " to " + config.root + config.targets.views + group.folder); - return MergeStream( - src(group.files) - .pipe( dest(config.root + config.targets.views + group.folder) ) - , js() - ); + return parallel( + function MoveViewsAndRegenerateJS() { + return src(group.files).pipe( dest(config.root + config.targets.views + group.folder) ); + }, + js + )(); }); } From 1126a2d6992c4dc185d347ea43c51524c61e96bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Fri, 29 May 2020 11:00:28 +0200 Subject: [PATCH 152/377] changed vm to a better controller instance name --- .../src/common/services/blockeditor.service.js | 2 +- .../propertyeditors/blocklist/blocklist.component.html | 6 +++--- 2 files changed, 4 insertions(+), 4 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 6f12db6c1e..eaf2fc79fc 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 @@ -270,7 +270,7 @@ var dataModel = this._getDataByUdi(udi); if (dataModel === null) { - console.error("Couldnt find content model of "+udi) + console.error("Couldnt find content model of " + udi) return null; } diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.html index 4167667af0..8d2714382f 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.html @@ -11,10 +11,10 @@ type="button" class="btn-reset umb-block-list__block--create-button" ng-click="vm.showCreateDialog($index, $event)" - ng-controller="Umbraco.PropertyEditors.BlockListPropertyEditor.CreateButtonController as vm" - ng-mousemove="vm.onMouseMove($event)" + ng-controller="Umbraco.PropertyEditors.BlockListPropertyEditor.CreateButtonController as inlineCreateButtonCtrl" + ng-mousemove="inlineCreateButtonCtrl.onMouseMove($event)" > -
    +
    +
    +
    From ade450253363b2019551ed244f9f0ea5eb7dafdb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Fri, 29 May 2020 11:23:02 +0200 Subject: [PATCH 153/377] make watch for views work again. --- src/Umbraco.Web.UI.Client/gulp/config.js | 12 +++++++++--- src/Umbraco.Web.UI.Client/gulp/modes.js | 12 +++++++++++- src/Umbraco.Web.UI.Client/gulp/tasks/test.js | 17 +++++++++++++++-- .../gulp/tasks/watchTask.js | 19 ++++++++++++------- .../gulp/util/processJs.js | 4 +++- src/Umbraco.Web.UI.Client/gulpfile.js | 10 +++++----- 6 files changed, 55 insertions(+), 19 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/gulp/config.js b/src/Umbraco.Web.UI.Client/gulp/config.js index 39dc9bb2a4..3c32832ba0 100755 --- a/src/Umbraco.Web.UI.Client/gulp/config.js +++ b/src/Umbraco.Web.UI.Client/gulp/config.js @@ -3,10 +3,16 @@ module.exports = { compile: { build: { - sourcemaps: false + sourcemaps: false, + embedtemplates: true }, dev: { - sourcemaps: true + sourcemaps: true, + embedtemplates: true + }, + test: { + sourcemaps: false, + embedtemplates: true } }, sources: { @@ -17,7 +23,7 @@ module.exports = { installer: { files: "./src/less/installer.less", watch: "./src/less/**/*.less", out: "installer.css" }, nonodes: { files: "./src/less/pages/nonodes.less", watch: "./src/less/**/*.less", out: "nonodes.style.min.css"}, preview: { files: "./src/less/canvas-designer.less", watch: "./src/less/**/*.less", out: "canvasdesigner.css" }, - umbraco: { files: "./src/less/belle.less", watch: "./src/less/**/*.less", out: "umbraco.css" }, + umbraco: { files: "./src/less/belle.less", watch: "./src/**/*.less", out: "umbraco.css" }, rteContent: { files: "./src/less/rte-content.less", watch: "./src/less/**/*.less", out: "rte-content.css" } }, diff --git a/src/Umbraco.Web.UI.Client/gulp/modes.js b/src/Umbraco.Web.UI.Client/gulp/modes.js index dc2947f2cc..21609cdcf8 100644 --- a/src/Umbraco.Web.UI.Client/gulp/modes.js +++ b/src/Umbraco.Web.UI.Client/gulp/modes.js @@ -10,4 +10,14 @@ function setDevelopmentMode(cb) { return cb(); }; -module.exports = { setDevelopmentMode: setDevelopmentMode }; +function setTestMode(cb) { + + config.compile.current = config.compile.test; + + return cb(); +}; + +module.exports = { + setDevelopmentMode: setDevelopmentMode, + setTestMode: setTestMode + }; diff --git a/src/Umbraco.Web.UI.Client/gulp/tasks/test.js b/src/Umbraco.Web.UI.Client/gulp/tasks/test.js index 1e8d074f7e..255fe17435 100644 --- a/src/Umbraco.Web.UI.Client/gulp/tasks/test.js +++ b/src/Umbraco.Web.UI.Client/gulp/tasks/test.js @@ -6,11 +6,24 @@ var karmaServer = require('karma').Server; * Build tests **************************/ - // Karma test +// Karma test function testUnit() { + return new karmaServer({ + configFile: __dirname + "/../../test/config/karma.conf.js" + }) + .start(); +}; + +// Run karma test server +function runUnitTestServer() { + return new karmaServer({ configFile: __dirname + "/../../test/config/karma.conf.js", + autoWatch: true, + port: 9999, + singleRun: false, + browsers: ['ChromeDebugging'], keepalive: true }) .start(); @@ -24,4 +37,4 @@ function testE2e() { .start(); }; -module.exports = { testUnit: testUnit, testE2e: testE2e }; +module.exports = { testUnit: testUnit, testE2e: testE2e, runUnitTestServer: runUnitTestServer }; diff --git a/src/Umbraco.Web.UI.Client/gulp/tasks/watchTask.js b/src/Umbraco.Web.UI.Client/gulp/tasks/watchTask.js index 24a6e65540..810146d7c8 100644 --- a/src/Umbraco.Web.UI.Client/gulp/tasks/watchTask.js +++ b/src/Umbraco.Web.UI.Client/gulp/tasks/watchTask.js @@ -1,7 +1,7 @@ 'use strict'; const config = require('../config'); -const {watch, parallel, dest, src} = require('gulp'); +const {watch, series, parallel, dest, src} = require('gulp'); var _ = require('lodash'); var MergeStream = require('merge-stream'); @@ -9,9 +9,7 @@ var MergeStream = require('merge-stream'); var processJs = require('../util/processJs'); var processLess = require('../util/processLess'); -//const { less } = require('./less'); -//const { views } = require('./views'); - +var {js} = require('./js'); function watchTask(cb) { @@ -35,10 +33,17 @@ function watchTask(cb) { var viewWatcher; _.forEach(config.sources.views, function (group) { if(group.watch !== false) { - viewWatcher = watch(group.files, { ignoreInitial: true, interval: watchInterval }); - viewWatcher.on('change', function(path, stats) { + viewWatcher = watch(group.files, { ignoreInitial: true, interval: watchInterval }, function() { + console.log("copying " + group.files + " to " + config.root + config.targets.views + group.folder); - src(group.files).pipe( dest(config.root + config.targets.views + group.folder) ); + + return parallel( + function MoveViewsAndRegenerateJS() { + return src(group.files).pipe( dest(config.root + config.targets.views + group.folder) ); + }, + js + )(); + }); } }); diff --git a/src/Umbraco.Web.UI.Client/gulp/util/processJs.js b/src/Umbraco.Web.UI.Client/gulp/util/processJs.js index e3e393b661..67dd6dd420 100644 --- a/src/Umbraco.Web.UI.Client/gulp/util/processJs.js +++ b/src/Umbraco.Web.UI.Client/gulp/util/processJs.js @@ -25,7 +25,9 @@ module.exports = function (files, out) { .pipe(sort()); //in production, embed the templates - task = task.pipe(embedTemplates({ basePath: "./src/", minimize: { loose: true } })) + if(config.compile.current.embedtemplates === true) { + task = task.pipe(embedTemplates({ basePath: "./src/", minimize: { loose: true } })); + } task = task.pipe(concat(out)) .pipe(wrap('(function(){\n%= body %\n})();')) diff --git a/src/Umbraco.Web.UI.Client/gulpfile.js b/src/Umbraco.Web.UI.Client/gulpfile.js index 705c54bf04..542d45c479 100644 --- a/src/Umbraco.Web.UI.Client/gulpfile.js +++ b/src/Umbraco.Web.UI.Client/gulpfile.js @@ -13,11 +13,11 @@ const { src, dest, series, parallel, lastRun } = require('gulp'); const config = require('./gulp/config'); -const { setDevelopmentMode } = require('./gulp/modes'); +const { setDevelopmentMode, setTestMode } = require('./gulp/modes'); const { dependencies } = require('./gulp/tasks/dependencies'); const { js } = require('./gulp/tasks/js'); const { less } = require('./gulp/tasks/less'); -const { testE2e, testUnit } = require('./gulp/tasks/test'); +const { testE2e, testUnit, runUnitTestServer } = require('./gulp/tasks/test'); const { views } = require('./gulp/tasks/views'); const { watchTask } = require('./gulp/tasks/watchTask'); @@ -31,6 +31,6 @@ exports.build = series(parallel(dependencies, js, less, views), testUnit); exports.dev = series(setDevelopmentMode, parallel(dependencies, js, less, views), watchTask); exports.watch = series(watchTask); // -exports.runTests = series(js, testUnit); -exports.testUnit = series(testUnit); -exports.testE2e = series(testE2e); +exports.runTests = series(setTestMode, parallel(js, testUnit)); +exports.runUnit = series(setTestMode, parallel(js, runUnitTestServer), watchTask); +exports.testE2e = series(setTestMode, parallel(testE2e)); From 48ad161d512c0f7f017437fc0ada711afe817538 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Fri, 29 May 2020 11:31:10 +0200 Subject: [PATCH 154/377] able to re run watch --- src/Umbraco.Web.UI.Client/gulp/tasks/watchTask.js | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/gulp/tasks/watchTask.js b/src/Umbraco.Web.UI.Client/gulp/tasks/watchTask.js index 810146d7c8..f8e2570ff9 100644 --- a/src/Umbraco.Web.UI.Client/gulp/tasks/watchTask.js +++ b/src/Umbraco.Web.UI.Client/gulp/tasks/watchTask.js @@ -33,18 +33,14 @@ function watchTask(cb) { var viewWatcher; _.forEach(config.sources.views, function (group) { if(group.watch !== false) { - viewWatcher = watch(group.files, { ignoreInitial: true, interval: watchInterval }, function() { - - console.log("copying " + group.files + " to " + config.root + config.targets.views + group.folder); - - return parallel( + viewWatcher = watch(group.files, { ignoreInitial: true, interval: watchInterval }, + parallel( function MoveViewsAndRegenerateJS() { return src(group.files).pipe( dest(config.root + config.targets.views + group.folder) ); }, js - )(); - - }); + ) + ); } }); From 1fe7de0b5f78d3d2d7843a02f520fe65b21a2952 Mon Sep 17 00:00:00 2001 From: Warren Buckley Date: Fri, 29 May 2020 15:18:46 +0000 Subject: [PATCH 155/377] Merge pull request #7166 from umbraco/v8/feature/7133-do-not-paste-keys-in-nested-content Cherry picked from SHA 49c438b55f002b02e4555bb2622c73bf1ca51239 NC keys: Do not copy keys for Nested Content (Fix for #7133) # Conflicts: # src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.controller.js --- .../src/common/services/clipboard.service.js | 64 +++++++-- .../nestedcontent/nestedcontent.controller.js | 76 ++++++++++- .../test/config/app.unit.js | 4 +- .../Compose/NestedContentPropertyComponent.cs | 126 ++++++++++++++++++ .../Compose/NestedContentPropertyComposer.cs | 9 ++ src/Umbraco.Web/Umbraco.Web.csproj | 2 + 6 files changed, 262 insertions(+), 19 deletions(-) create mode 100644 src/Umbraco.Web/Compose/NestedContentPropertyComponent.cs create mode 100644 src/Umbraco.Web/Compose/NestedContentPropertyComposer.cs 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 c3a1ba6432..083b4e86b7 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 @@ -12,6 +12,9 @@ */ function clipboardService(notificationsService, eventsService, localStorageService, iconHelper) { + + var clearPropertyResolvers = []; + var STORAGE_KEY = "umbClipboardService"; @@ -53,13 +56,32 @@ function clipboardService(notificationsService, eventsService, localStorageServi return false; } - var prepareEntryForStorage = function(entryData) { - var shallowCloneData = Object.assign({}, entryData);// Notice only a shallow copy, since we dont need to deep copy. (that will happen when storing the data) - delete shallowCloneData.key; - delete shallowCloneData.$$hashKey; - - return shallowCloneData; + function clearPropertyForStorage(prop) { + + for (var i=0; i prepareEntryForStorage(data)); + var copiedDatas = datas.map(data => prepareEntryForStorage(data, firstLevelClearupMethod)); // remove previous copies of this entry: storage.entries = storage.entries.filter( 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 19547c38e4..5ebb34bb7a 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 @@ -1,6 +1,58 @@ (function () { 'use strict'; + /** + * When performing a copy, we do copy the ElementType Data Model, but each inner Nested Content property is still stored as the Nested Content Model, aka. each property is just storing its value. To handle this we need to ensure we handle both scenarios. + */ + + + angular.module('umbraco').run(['clipboardService', function (clipboardService) { + + function clearNestedContentPropertiesForStorage(prop, propClearingMethod) { + + // if prop.editor is "Umbraco.NestedContent" + if ((typeof prop === 'object' && prop.editor === "Umbraco.NestedContent")) { + + var value = prop.value; + for (var i = 0; i < value.length; i++) { + var obj = value[i]; + + // remove the key + delete obj.key; + + // Loop through all inner properties: + for (var k in obj) { + propClearingMethod(obj[k]); + } + } + } + } + + clipboardService.registrerClearPropertyResolver(clearNestedContentPropertiesForStorage) + + + 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 ((Array.isArray(prop) && prop.length > 0 && prop[0].ncContentTypeAlias !== undefined)) { + + for (var i = 0; i < prop.length; i++) { + var obj = prop[i]; + + // remove the key + delete obj.key; + + // Loop through all inner properties: + for (var k in obj) { + propClearingMethod(obj[k]); + } + } + } + } + + clipboardService.registrerClearPropertyResolver(clearInnerNestedContentPropertiesForStorage) + }]); + angular .module('umbraco') .component('nestedContentPropertyEditor', { @@ -13,7 +65,7 @@ } }); - function NestedContentController($scope, $interpolate, $filter, $timeout, contentResource, localizationService, iconHelper, clipboardService, eventsService, overlayService, $routeParams, editorState) { + function NestedContentController($scope, $interpolate, $filter, $timeout, contentResource, localizationService, iconHelper, clipboardService, eventsService, overlayService) { var vm = this; var model = $scope.$parent.$parent.model; @@ -76,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); + clipboardService.copyArray("elementTypeArray", aliases, vm.nodes, data, "icon-thumbnail-list", model.id, clearNodeForCopy); }); } @@ -208,7 +260,8 @@ }); }); - vm.overlayMenu.title = vm.overlayMenu.pasteItems.length > 0 ? labels.grid_addElement : labels.content_createEmpty; + vm.overlayMenu.title = labels.grid_addElement; + vm.overlayMenu.hideHeader = vm.overlayMenu.pasteItems.length > 0; vm.overlayMenu.clickClearPaste = function ($event) { $event.stopPropagation(); @@ -216,6 +269,7 @@ clipboardService.clearEntriesOfType("elementType", contentTypeAliases); clipboardService.clearEntriesOfType("elementTypeArray", contentTypeAliases); vm.overlayMenu.pasteItems = [];// This dialog is not connected via the clipboardService events, so we need to update manually. + vm.overlayMenu.hideHeader = false; }; if (vm.overlayMenu.availableItems.length === 1 && vm.overlayMenu.pasteItems.length === 0) { @@ -383,6 +437,11 @@ }); } + function clearNodeForCopy(clonedData) { + delete clonedData.key; + delete clonedData.$$hashKey; + } + vm.showCopy = clipboardService.isSupported(); vm.showPaste = false; @@ -390,7 +449,7 @@ syncCurrentNode(); - clipboardService.copy("elementType", node.contentTypeAlias, node); + clipboardService.copy("elementType", node.contentTypeAlias, node, null, null, null, clearNodeForCopy); $event.stopPropagation(); } @@ -488,10 +547,12 @@ } // Enforce min items if we only have one scaffold type + var modelWasChanged = false; if (vm.nodes.length < vm.minItems && vm.scaffolds.length === 1) { for (var i = vm.nodes.length; i < model.config.minItems; i++) { addNode(vm.scaffolds[0].contentTypeAlias); } + modelWasChanged = true; } // If there is only one item, set it as current node @@ -503,6 +564,9 @@ vm.inited = true; + if (modelWasChanged) { + updateModel(); + } updatePropertyActionStates(); checkAbilityToPasteContent(); } @@ -585,8 +649,8 @@ } function updatePropertyActionStates() { - copyAllEntriesAction.isDisabled = !model.value || model.value.length === 0; - removeAllEntriesAction.isDisabled = !model.value || model.value.length === 0; + copyAllEntriesAction.isDisabled = !model.value || !model.value.length; + removeAllEntriesAction.isDisabled = copyAllEntriesAction.isDisabled; } diff --git a/src/Umbraco.Web.UI.Client/test/config/app.unit.js b/src/Umbraco.Web.UI.Client/test/config/app.unit.js index 1f49d237e6..9e265215dd 100644 --- a/src/Umbraco.Web.UI.Client/test/config/app.unit.js +++ b/src/Umbraco.Web.UI.Client/test/config/app.unit.js @@ -13,8 +13,8 @@ var app = angular.module('umbraco', [ 'ngSanitize', //'ngMessages', - 'tmh.dynamicLocale' + 'tmh.dynamicLocale', //'ngFileUpload', - //'LocalStorageModule', + 'LocalStorageModule' //'chart.js' ]); diff --git a/src/Umbraco.Web/Compose/NestedContentPropertyComponent.cs b/src/Umbraco.Web/Compose/NestedContentPropertyComponent.cs new file mode 100644 index 0000000000..5794a2734e --- /dev/null +++ b/src/Umbraco.Web/Compose/NestedContentPropertyComponent.cs @@ -0,0 +1,126 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Newtonsoft.Json.Linq; +using Umbraco.Core; +using Umbraco.Core.Composing; +using Umbraco.Core.Events; +using Umbraco.Core.Models; +using Umbraco.Core.Services; +using Umbraco.Core.Services.Implement; +using Umbraco.Web.PropertyEditors; + +namespace Umbraco.Web.Compose +{ + public class NestedContentPropertyComponent : IComponent + { + public void Initialize() + { + ContentService.Copying += ContentService_Copying; + ContentService.Saving += ContentService_Saving; + } + + 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); + } + + 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); + } + } + + 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) + { + // used so we can test nicely + if (createGuid == null) + createGuid = () => Guid.NewGuid(); + + if (string.IsNullOrWhiteSpace(rawJson) || !rawJson.DetectIsJson()) + return rawJson; + + // Parse JSON + var complexEditorValue = JToken.Parse(rawJson); + + UpdateNestedContentKeysRecursively(complexEditorValue, onlyMissingKeys, createGuid); + + return complexEditorValue.ToString(); + } + + private void UpdateNestedContentKeysRecursively(JToken json, bool onlyMissingKeys, Func createGuid) + { + // check if this is NC + var isNestedContent = json.SelectTokens($"$..['{NestedContentPropertyEditor.ContentTypeAliasPropertyKey}']", false).Any(); + + // select all values (flatten) + var allProperties = json.SelectTokens("$..*").OfType().Select(x => x.Parent as JProperty).WhereNotNull().ToList(); + foreach (var prop in allProperties) + { + if (prop.Name == NestedContentPropertyEditor.ContentTypeAliasPropertyKey) + { + // 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 + prop.Parent["key"] = createGuid().ToString(); + } + } + else if (!isNestedContent || prop.Name != "key") + { + // this is an arbitrary property that could contain a nested complex editor + var propVal = prop.Value?.ToString(); + // check if this might contain a nested NC + if (!propVal.IsNullOrWhiteSpace() && propVal.DetectIsJson() && propVal.InvariantContains(NestedContentPropertyEditor.ContentTypeAliasPropertyKey)) + { + // recurse + var parsed = JToken.Parse(propVal); + UpdateNestedContentKeysRecursively(parsed, onlyMissingKeys, createGuid); + // set the value to the updated one + prop.Value = parsed.ToString(); + } + } + } + } + + } +} diff --git a/src/Umbraco.Web/Compose/NestedContentPropertyComposer.cs b/src/Umbraco.Web/Compose/NestedContentPropertyComposer.cs new file mode 100644 index 0000000000..4c9d9dee1c --- /dev/null +++ b/src/Umbraco.Web/Compose/NestedContentPropertyComposer.cs @@ -0,0 +1,9 @@ +using Umbraco.Core; +using Umbraco.Core.Composing; + +namespace Umbraco.Web.Compose +{ + [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 5173972f42..ed457b1364 100755 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -129,6 +129,7 @@ + @@ -233,6 +234,7 @@ + From 31c9afac7614c963d9f0d5839a8f56c32cf48401 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Tue, 2 Jun 2020 09:38:00 +0200 Subject: [PATCH 156/377] make js up to date --- .../src/common/services/blockeditor.service.js | 18 +++++++++--------- .../blocklist/blocklist.component.js | 4 ++-- .../blocklist.blockconfiguration.controller.js | 2 +- 3 files changed, 12 insertions(+), 12 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 eaf2fc79fc..8adf90af74 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 @@ -284,13 +284,14 @@ var blockModel = {}; blockModel.key = String.CreateGuid().replace(/-/g, ""); - blockModel.config = angular.copy(blockConfiguration); + blockModel.config = Utilities.copy(blockConfiguration); if (blockModel.config.label && blockModel.config.label !== "") { blockModel.labelInterpolator = $interpolate(blockModel.config.label); } - blockModel.updateLabel = _.debounce(function() { + blockModel.__scope = this.isolatedScope; + blockModel.updateLabel = _.debounce(function () {this.__scope.$evalAsync(function() { this.label = getBlockLabel(this); - }, 100); + }.bind(this))}.bind(blockModel), 10); var contentScaffold = this.getScaffoldFor(blockConfiguration.contentTypeAlias); if(contentScaffold === null) { @@ -298,7 +299,7 @@ } // make basics from scaffold - blockModel.content = angular.copy(contentScaffold); + blockModel.content = Utilities.copy(contentScaffold); blockModel.content.udi = udi; mapToElementModel(blockModel.content, dataModel); @@ -314,7 +315,7 @@ } // make basics from scaffold - blockModel.settings = angular.copy(settingsScaffold); + blockModel.settings = Utilities.copy(settingsScaffold); layoutEntry.settings = layoutEntry.settings || { key: String.CreateGuid(), contentTypeAlias: blockConfiguration.settingsElementTypeAlias }; if (!layoutEntry.settings.key) { layoutEntry.settings.key = String.CreateGuid(); } if (!layoutEntry.settings.contentTypeAlias) { layoutEntry.settings.contentTypeAlias = blockConfiguration.settingsElementTypeAlias; } @@ -340,9 +341,7 @@ destroyBlockModel: function(blockModel) { // remove property value watchers: - for (const w of blockModel.watchers) { - w(); - } + blockModel.watchers.forEach(w => { w(); }); // remove model from isolatedScope. delete this.isolatedScope.blockModels[blockModel.key]; @@ -405,7 +404,7 @@ */ createFromElementType: function(elementTypeDataModel) { - elementTypeDataModel = angular.copy(elementTypeDataModel); + elementTypeDataModel = Utilities.copy(elementTypeDataModel); var contentTypeAlias = elementTypeDataModel.contentTypeAlias; @@ -457,6 +456,7 @@ delete this.blockConfigurations; delete this.scaffolds; delete this.watchers; + this.isolatedScope.$destroy(); delete this.isolatedScope; } } diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.js index a2218ceb82..68302f2f31 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.js @@ -203,11 +203,11 @@ function editBlock(blockModel, openSettings) { // make a clone to avoid editing model directly. - var blockContentClone = angular.copy(blockModel.content); + var blockContentClone = Utilities.copy(blockModel.content); var blockSettingsClone = null; if (blockModel.config.settingsElementTypeAlias) { - blockSettingsClone = angular.copy(blockModel.settings); + blockSettingsClone = Utilities.copy(blockModel.settings); } var hideContent = (openSettings === true && inlineEditing === true); 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 d24f266dca..4d41336251 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 @@ -162,7 +162,7 @@ localizationService.localize("blockEditor_blockConfigurationOverlayTitle", [vm.getElementTypeByAlias(block.contentTypeAlias).name]).then(function (data) { - var clonedBlockData = angular.copy(block); + var clonedBlockData = Utilities.copy(block); vm.openBlock = block; var overlayModel = { From f157fe56f1026822c4253ad35fdf011447551d57 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Tue, 2 Jun 2020 10:04:39 +0200 Subject: [PATCH 157/377] fix white background of image-picker --- src/Umbraco.Web.UI.Client/src/less/property-editors.less | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/Umbraco.Web.UI.Client/src/less/property-editors.less b/src/Umbraco.Web.UI.Client/src/less/property-editors.less index b5870b8dce..83bad8cacb 100644 --- a/src/Umbraco.Web.UI.Client/src/less/property-editors.less +++ b/src/Umbraco.Web.UI.Client/src/less/property-editors.less @@ -320,6 +320,11 @@ box-shadow:none !important; } +.umb-sortable-thumbnails-container { + display: flex; + flex-wrap: wrap; + background-color: @white; +} .umb-sortable-thumbnails { list-style-type: none; From 61c0e42e5758313c9afe6cbd7af771fdcaf418f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Tue, 2 Jun 2020 10:05:00 +0200 Subject: [PATCH 158/377] media-picker container class --- .../src/views/propertyeditors/mediapicker/mediapicker.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker/mediapicker.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker/mediapicker.html index c4dba4d373..59e6fcd21c 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker/mediapicker.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker/mediapicker.html @@ -3,7 +3,7 @@

    -
    +
    • From 825e3eff9727614efaa0d687a2318e98b65d7f70 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Tue, 2 Jun 2020 10:05:08 +0200 Subject: [PATCH 159/377] loading indication --- .../views/propertyeditors/blocklist/blocklist.component.html | 4 +++- .../views/propertyeditors/blocklist/blocklist.component.js | 3 +++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.html index 8d2714382f..6a2c012e25 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.html @@ -1,9 +1,10 @@
      +
      -
      +
      @@ -50,6 +51,7 @@
      @@ -124,11 +124,11 @@
      -
      - {{ settingsPreview = vm.getElementTypeByAlias(vm.block.settingsElementTypeAlias); "" }} +
      + {{ settingsPreview = vm.getElementTypeByKey(vm.block.settingsElementTypeKey); "" }}
      -
      -
      - -
      @@ -246,86 +226,3 @@
      - - - diff --git a/src/Umbraco.Web.UI.Client/test/unit/common/services/block-editor-service.spec.js b/src/Umbraco.Web.UI.Client/test/unit/common/services/block-editor-service.spec.js index 17e3eda8be..92ebce981d 100644 --- a/src/Umbraco.Web.UI.Client/test/unit/common/services/block-editor-service.spec.js +++ b/src/Umbraco.Web.UI.Client/test/unit/common/services/block-editor-service.spec.js @@ -26,7 +26,7 @@ })); - var blockConfigurationMock = {contentTypeAlias: "testAlias", label:"Test label", settingsElementTypeAlias: null, view: "testview.html"}; + var blockConfigurationMock = {contentTypeKey: "testKey", label:"Test label", settingsElementTypeKey: null, view: "testview.html"}; var propertyModelMock = { layout: { @@ -39,7 +39,7 @@ data: [ { udi: 1234, - contentTypeAlias: "testAlias", + contentTypeKey: "testKey", testproperty: "myTestValue" } ] @@ -64,7 +64,7 @@ it('getBlockConfiguration provide the requested block configurtion', function () { var modelObject = blockEditorService.createModelObject({}, "Umbraco.TestBlockEditor", [blockConfigurationMock], $scope); - expect(modelObject.getBlockConfiguration(blockConfigurationMock.contentTypeAlias).label).toBe(blockConfigurationMock.label); + expect(modelObject.getBlockConfiguration(blockConfigurationMock.contentTypeKey).label).toBe(blockConfigurationMock.label); }); it('loadScaffolding provides data for itemPicker', function (done) { @@ -73,7 +73,7 @@ modelObject.loadScaffolding().then(() => { var itemPickerOptions = modelObject.getAvailableBlocksForBlockPicker(); expect(itemPickerOptions.length).toBe(1); - expect(itemPickerOptions[0].blockConfigModel.contentTypeAlias).toBe(blockConfigurationMock.contentTypeAlias); + expect(itemPickerOptions[0].blockConfigModel.contentTypeKey).toBe(blockConfigurationMock.contentTypeKey); done(); }); diff --git a/src/Umbraco.Web/Editors/ContentController.cs b/src/Umbraco.Web/Editors/ContentController.cs index 565eebf80e..42e7ff9f5a 100644 --- a/src/Umbraco.Web/Editors/ContentController.cs +++ b/src/Umbraco.Web/Editors/ContentController.cs @@ -369,6 +369,38 @@ namespace Umbraco.Web.Editors return mapped; } + + /// + /// Gets an empty content item for the document type. + /// + /// + /// + [OutgoingEditorModelEvent] + public ContentItemDisplay GetEmptyByKey(string contentTypeKey, int parentId) + { + + Guid.TryParse(contentTypeKey, out Guid contentTypeGuid); + + var contentType = Services.ContentTypeService.Get(contentTypeGuid); + if (contentType == null) + { + throw new HttpResponseException(HttpStatusCode.NotFound); + } + + var emptyContent = Services.ContentService.Create("", parentId, contentType.Alias, Security.GetUserId().ResultOr(0)); + var mapped = MapToDisplay(emptyContent); + // translate the content type name if applicable + mapped.ContentTypeName = Services.TextService.UmbracoDictionaryTranslate(mapped.ContentTypeName); + // if your user type doesn't have access to the Settings section it would not get this property mapped + if (mapped.DocumentType != null) + mapped.DocumentType.Name = Services.TextService.UmbracoDictionaryTranslate(mapped.DocumentType.Name); + + //remove the listview app if it exists + mapped.ContentApps = mapped.ContentApps.Where(x => x.Alias != "umbListView").ToList(); + + return mapped; + } + [OutgoingEditorModelEvent] public ContentItemDisplay GetEmpty(int blueprintId, int parentId) { @@ -821,7 +853,7 @@ namespace Umbraco.Web.Editors /// /// /// For invariant, the variants collection count will be 1 and this will check if that invariant item has the critical values for persistence (i.e. Name) - /// + /// /// For variant, each variant will be checked for critical data for persistence and if it's not there then it's flags will be reset and it will not /// be persisted. However, we also need to deal with the case where all variants don't pass this check and then there is nothing to save. This also deals /// with removing the Name validation keys based on data annotations validation for items that haven't been marked to be saved. @@ -908,8 +940,8 @@ namespace Umbraco.Web.Editors var savedWithoutErrors = contentItem.Variants .Where(x => x.Save && !variantErrors.Contains((x.Culture, x.Segment))) - .Select(x => (culture: x.Culture, segment: x.Segment)); - + .Select(x => (culture: x.Culture, segment: x.Segment)); + foreach (var (culture, segment) in savedWithoutErrors) { var variantName = GetVariantName(culture, segment); @@ -1258,7 +1290,7 @@ namespace Umbraco.Web.Editors //Now check if there are validation errors on each variant. //If validation errors are detected on a variant and it's state is set to 'publish', then we //need to change it to 'save'. - //It is a requirement that this is performed AFTER ValidatePublishingMandatoryLanguages. + //It is a requirement that this is performed AFTER ValidatePublishingMandatoryLanguages. foreach (var variant in contentItem.Variants) { if (variantErrors.Contains((variant.Culture, variant.Segment))) @@ -1389,7 +1421,7 @@ namespace Umbraco.Web.Editors /// Segment to assign the error to /// /// - /// The culture used in the localization message, null by default which means will be used. + /// The culture used in the localization message, null by default which means will be used. /// private void AddVariantValidationError(string culture, string segment, string localizationKey, string cultureToken = null) { @@ -1402,7 +1434,7 @@ namespace Umbraco.Web.Editors } /// - /// Creates the human readable variant name based on culture and segment + /// Creates the human readable variant name based on culture and segment /// /// Culture /// Segment diff --git a/src/Umbraco.Web/PropertyEditors/BlockEditorPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/BlockEditorPropertyEditor.cs index f8105c5651..a98a85922b 100644 --- a/src/Umbraco.Web/PropertyEditors/BlockEditorPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/BlockEditorPropertyEditor.cs @@ -20,7 +20,7 @@ namespace Umbraco.Web.PropertyEditors /// public abstract class BlockEditorPropertyEditor : DataEditor { - public const string ContentTypeAliasPropertyKey = "contentTypeAlias"; + public const string ContentTypeKeyPropertyKey = "contentTypeKey"; public const string UdiPropertyKey = "udi"; private readonly IBlockEditorDataHelper _dataHelper; private readonly Lazy _propertyEditors; @@ -166,13 +166,13 @@ namespace Umbraco.Web.PropertyEditors public BlockEditorValues(IBlockEditorDataHelper dataHelper, IContentTypeService contentTypeService) { _dataHelper = dataHelper; - _contentTypes = new Lazy>(() => contentTypeService.GetAll().ToDictionary(c => c.Alias)); + _contentTypes = new Lazy>(() => contentTypeService.GetAll().ToDictionary(c => c.Key)); } private IContentType GetElementType(JObject item) { - var contentTypeAlias = item[ContentTypeAliasPropertyKey]?.ToObject() ?? string.Empty; - _contentTypes.Value.TryGetValue(contentTypeAlias, out var contentType); + var contentTypeKey = item[ContentTypeKeyPropertyKey]?.ToObject() ?? string.Empty; + _contentTypes.Value.TryGetValue(contentTypeKey, out var contentType); return contentType; } @@ -270,6 +270,6 @@ namespace Umbraco.Web.PropertyEditors } #endregion - private static bool IsSystemPropertyKey(string propertyKey) => ContentTypeAliasPropertyKey == propertyKey || UdiPropertyKey == propertyKey; + private static bool IsSystemPropertyKey(string propertyKey) => ContentTypeKeyPropertyKey == propertyKey || UdiPropertyKey == propertyKey; } } diff --git a/src/Umbraco.Web/PropertyEditors/BlockListConfiguration.cs b/src/Umbraco.Web/PropertyEditors/BlockListConfiguration.cs index 5fc016f694..71e74e05fe 100644 --- a/src/Umbraco.Web/PropertyEditors/BlockListConfiguration.cs +++ b/src/Umbraco.Web/PropertyEditors/BlockListConfiguration.cs @@ -38,11 +38,11 @@ namespace Umbraco.Web.PropertyEditors [JsonProperty("thumbnail")] public string Thumbnail { get; set; } - [JsonProperty("contentTypeAlias")] - public string Alias { get; set; } + [JsonProperty("contentTypeKey")] + public string Key { get; set; } - [JsonProperty("settingsElementTypeAlias")] - public string SettingsElementTypeAlias { get; set; } + [JsonProperty("settingsElementTypeKey")] + public string SettingsElementTypeKey { get; set; } [JsonProperty("view")] public string View { get; set; } diff --git a/src/Umbraco.Web/PropertyEditors/ValueConverters/BlockEditorConverter.cs b/src/Umbraco.Web/PropertyEditors/ValueConverters/BlockEditorConverter.cs index c3c963ba6e..3ca70cbd22 100644 --- a/src/Umbraco.Web/PropertyEditors/ValueConverters/BlockEditorConverter.cs +++ b/src/Umbraco.Web/PropertyEditors/ValueConverters/BlockEditorConverter.cs @@ -20,15 +20,16 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters } public IPublishedElement ConvertToElement( - JObject sourceObject, string contentTypeAliasPropertyKey, + JObject sourceObject, string contentTypeKeyPropertyKey, PropertyCacheLevel referenceCacheLevel, bool preview) { - var elementTypeAlias = sourceObject[contentTypeAliasPropertyKey]?.ToObject(); - if (string.IsNullOrEmpty(elementTypeAlias)) + var elementTypeKey = sourceObject[contentTypeKeyPropertyKey]?.ToObject(); + if (string.IsNullOrEmpty(elementTypeKey)) return null; // only convert element types - content types will cause an exception when PublishedModelFactory creates the model - var publishedContentType = _publishedSnapshotAccessor.PublishedSnapshot.Content.GetContentType(elementTypeAlias); + // TODO: make this work with keys. + var publishedContentType = _publishedSnapshotAccessor.PublishedSnapshot.Content.GetContentType(elementTypeKey); if (publishedContentType == null || publishedContentType.IsElement == false) return null; diff --git a/src/Umbraco.Web/PropertyEditors/ValueConverters/BlockListPropertyValueConverter.cs b/src/Umbraco.Web/PropertyEditors/ValueConverters/BlockListPropertyValueConverter.cs index 21105b531e..6adee9deed 100644 --- a/src/Umbraco.Web/PropertyEditors/ValueConverters/BlockListPropertyValueConverter.cs +++ b/src/Umbraco.Web/PropertyEditors/ValueConverters/BlockListPropertyValueConverter.cs @@ -53,9 +53,10 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters { var configuration = propertyType.DataType.ConfigurationAs(); var contentTypes = configuration.Blocks; - var contentTypeMap = contentTypes.ToDictionary(x => x.Alias); + var contentTypeMap = contentTypes.ToDictionary(x => x.Key); var elements = (contentTypes.Length == 1 - ? (IList)_publishedModelFactory.CreateModelList(contentTypes[0].Alias) + // TODO: make this work with key + ? (IList)_publishedModelFactory.CreateModelList(contentTypes[0].Key) : new List()) .ToDictionary(x => x.Key, x => x); @@ -80,7 +81,7 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters // parse the data elements foreach (var data in jsonData.Cast()) { - var element = _blockConverter.ConvertToElement(data, BlockEditorPropertyEditor.ContentTypeAliasPropertyKey, referenceCacheLevel, preview); + var element = _blockConverter.ConvertToElement(data, BlockEditorPropertyEditor.contentTypeKeyPropertyKey, referenceCacheLevel, preview); if (element == null) continue; elements[element.Key] = element; } @@ -92,7 +93,7 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters { var settingsJson = blockListLayout["settings"] as JObject; // the result of this can be null, that's ok - var element = settingsJson != null ? _blockConverter.ConvertToElement(settingsJson, BlockEditorPropertyEditor.ContentTypeAliasPropertyKey, referenceCacheLevel, preview) : null; + var element = settingsJson != null ? _blockConverter.ConvertToElement(settingsJson, BlockEditorPropertyEditor.contentTypeKeyPropertyKey, referenceCacheLevel, preview) : null; if (!Udi.TryParse(blockListLayout.Value("udi"), out var udi) || !(udi is GuidUdi guidUdi)) continue; @@ -101,11 +102,11 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters if (!elements.TryGetValue(guidUdi.Guid, out var data)) continue; - if (!contentTypeMap.TryGetValue(data.ContentType.Alias, out var blockConfig)) + if (!contentTypeMap.TryGetValue(data.ContentType.Key, out var blockConfig)) continue; // this can happen if they have a settings type, save content, remove the settings type, and display the front-end page before saving the content again - if (element != null && string.IsNullOrWhiteSpace(blockConfig.SettingsElementTypeAlias)) + if (element != null && string.IsNullOrWhiteSpace(blockConfig.SettingsElementTypeKey)) element = null; var layoutRef = new BlockListLayoutReference(udi, data, element); diff --git a/src/Umbraco.Web/PropertyEditors/ValueConverters/NestedContentManyValueConverter.cs b/src/Umbraco.Web/PropertyEditors/ValueConverters/NestedContentManyValueConverter.cs index b961048851..72556f9d8b 100644 --- a/src/Umbraco.Web/PropertyEditors/ValueConverters/NestedContentManyValueConverter.cs +++ b/src/Umbraco.Web/PropertyEditors/ValueConverters/NestedContentManyValueConverter.cs @@ -60,7 +60,7 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters var configuration = propertyType.DataType.ConfigurationAs(); var contentTypes = configuration.ContentTypes; var elements = contentTypes.Length == 1 - ? PublishedModelFactory.CreateModelList(contentTypes[0].Alias) + ? PublishedModelFactory.CreateModelList(contentTypes[0].Key) : new List(); var value = (string)inter; From 0b71c10fdc7c5d4b4a0f962524a0d5d71952103a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Thu, 4 Jun 2020 11:16:18 +0200 Subject: [PATCH 164/377] revert change --- .../ValueConverters/NestedContentManyValueConverter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web/PropertyEditors/ValueConverters/NestedContentManyValueConverter.cs b/src/Umbraco.Web/PropertyEditors/ValueConverters/NestedContentManyValueConverter.cs index 72556f9d8b..b961048851 100644 --- a/src/Umbraco.Web/PropertyEditors/ValueConverters/NestedContentManyValueConverter.cs +++ b/src/Umbraco.Web/PropertyEditors/ValueConverters/NestedContentManyValueConverter.cs @@ -60,7 +60,7 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters var configuration = propertyType.DataType.ConfigurationAs(); var contentTypes = configuration.ContentTypes; var elements = contentTypes.Length == 1 - ? PublishedModelFactory.CreateModelList(contentTypes[0].Key) + ? PublishedModelFactory.CreateModelList(contentTypes[0].Alias) : new List(); var value = (string)inter; From 7077b5111ecddde6e87fd55c2633e6b03e3605f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Thu, 4 Jun 2020 11:19:57 +0200 Subject: [PATCH 165/377] add todo --- .../ValueConverters/BlockListPropertyValueConverter.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/Umbraco.Web/PropertyEditors/ValueConverters/BlockListPropertyValueConverter.cs b/src/Umbraco.Web/PropertyEditors/ValueConverters/BlockListPropertyValueConverter.cs index 6adee9deed..907c44c8f3 100644 --- a/src/Umbraco.Web/PropertyEditors/ValueConverters/BlockListPropertyValueConverter.cs +++ b/src/Umbraco.Web/PropertyEditors/ValueConverters/BlockListPropertyValueConverter.cs @@ -81,7 +81,7 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters // parse the data elements foreach (var data in jsonData.Cast()) { - var element = _blockConverter.ConvertToElement(data, BlockEditorPropertyEditor.contentTypeKeyPropertyKey, referenceCacheLevel, preview); + var element = _blockConverter.ConvertToElement(data, BlockEditorPropertyEditor.ContentTypeKeyPropertyKey, referenceCacheLevel, preview); if (element == null) continue; elements[element.Key] = element; } @@ -93,7 +93,7 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters { var settingsJson = blockListLayout["settings"] as JObject; // the result of this can be null, that's ok - var element = settingsJson != null ? _blockConverter.ConvertToElement(settingsJson, BlockEditorPropertyEditor.contentTypeKeyPropertyKey, referenceCacheLevel, preview) : null; + var element = settingsJson != null ? _blockConverter.ConvertToElement(settingsJson, BlockEditorPropertyEditor.ContentTypeKeyPropertyKey, referenceCacheLevel, preview) : null; if (!Udi.TryParse(blockListLayout.Value("udi"), out var udi) || !(udi is GuidUdi guidUdi)) continue; @@ -102,7 +102,8 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters if (!elements.TryGetValue(guidUdi.Guid, out var data)) continue; - if (!contentTypeMap.TryGetValue(data.ContentType.Key, out var blockConfig)) + // TODO: make this work with key, not Alias. + if (!contentTypeMap.TryGetValue(data.ContentType.Alias, out var blockConfig)) continue; // this can happen if they have a settings type, save content, remove the settings type, and display the front-end page before saving the content again From 4ffc69b7f57470f62bcd9917780f5111eaa40ee4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Thu, 4 Jun 2020 11:32:44 +0200 Subject: [PATCH 166/377] use Guid for Key --- .../PropertyEditors/BlockEditorPropertyEditor.cs | 6 +++--- .../ValueConverters/BlockListPropertyValueConverter.cs | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Umbraco.Web/PropertyEditors/BlockEditorPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/BlockEditorPropertyEditor.cs index a98a85922b..b32a91c058 100644 --- a/src/Umbraco.Web/PropertyEditors/BlockEditorPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/BlockEditorPropertyEditor.cs @@ -161,17 +161,17 @@ namespace Umbraco.Web.PropertyEditors internal class BlockEditorValues { private readonly IBlockEditorDataHelper _dataHelper; - private readonly Lazy> _contentTypes; + private readonly Lazy> _contentTypes; public BlockEditorValues(IBlockEditorDataHelper dataHelper, IContentTypeService contentTypeService) { _dataHelper = dataHelper; - _contentTypes = new Lazy>(() => contentTypeService.GetAll().ToDictionary(c => c.Key)); + _contentTypes = new Lazy>(() => contentTypeService.GetAll().ToDictionary(c => c.Key)); } private IContentType GetElementType(JObject item) { - var contentTypeKey = item[ContentTypeKeyPropertyKey]?.ToObject() ?? string.Empty; + Guid contentTypeKey = item[ContentTypeKeyPropertyKey]?.ToObject() ?? Guid.Empty; _contentTypes.Value.TryGetValue(contentTypeKey, out var contentType); return contentType; } diff --git a/src/Umbraco.Web/PropertyEditors/ValueConverters/BlockListPropertyValueConverter.cs b/src/Umbraco.Web/PropertyEditors/ValueConverters/BlockListPropertyValueConverter.cs index 907c44c8f3..7bd410f55a 100644 --- a/src/Umbraco.Web/PropertyEditors/ValueConverters/BlockListPropertyValueConverter.cs +++ b/src/Umbraco.Web/PropertyEditors/ValueConverters/BlockListPropertyValueConverter.cs @@ -102,8 +102,8 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters if (!elements.TryGetValue(guidUdi.Guid, out var data)) continue; - // TODO: make this work with key, not Alias. - if (!contentTypeMap.TryGetValue(data.ContentType.Alias, out var blockConfig)) + // Make this work with Key, not alias, since contentTypeMap is now a dctionary with contentTypeKey as the dictionary key. + if (!contentTypeMap.TryGetValue(data.ContentType.Key, out var blockConfig)) continue; // this can happen if they have a settings type, save content, remove the settings type, and display the front-end page before saving the content again From c9b67d12d45fd8609da80cd018c71b743114e569 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Thu, 4 Jun 2020 14:06:19 +0200 Subject: [PATCH 167/377] use key --- .../Upgrade/V_8_7_0/StackedContentToBlockList.cs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_7_0/StackedContentToBlockList.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_7_0/StackedContentToBlockList.cs index de5cb229f6..5e1396a392 100644 --- a/src/Umbraco.Core/Migrations/Upgrade/V_8_7_0/StackedContentToBlockList.cs +++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_7_0/StackedContentToBlockList.cs @@ -109,9 +109,9 @@ namespace Umbraco.Core.Migrations.Upgrade.V_8_7_0 { Blocks = old.ContentTypes?.Select(t => new BlockListConfiguration.BlockConfiguration { - Alias = knownDocumentTypes.TryGetValue(t.IcContentTypeGuid, out var ct) ? ct.Alias : null, + Key = knownDocumentTypes.TryGetValue(t.IcContentTypeGuid, out var ct) ? ct.Key : Guid.Empty, Label = t.NameTemplate - }).Where(c => c.Alias != null).ToArray(), + }).Where(c => c.Key != null).ToArray(), UseInlineEditingAsDefault = old.SingleItemMode == "1" || old.SingleItemMode == bool.TrueString }; @@ -198,7 +198,7 @@ namespace Umbraco.Core.Migrations.Upgrade.V_8_7_0 public string Thumbnail { get; set; } [JsonProperty("contentTypeKey")] - public string Key { get; set; } + public Guid Key { get; set; } [JsonProperty("settingsElementTypeKey")] public string settingsElementTypeKey { get; set; } @@ -249,7 +249,7 @@ namespace Umbraco.Core.Migrations.Upgrade.V_8_7_0 { if (!Guid.TryParse(obj["key"].ToString(), out var key)) key = Guid.NewGuid(); if (!Guid.TryParse(obj["icContentTypeGuid"].ToString(), out var ctGuid)) ctGuid = Guid.Empty; - if (!knownDocumentTypes.TryGetValue(ctGuid, out var ct)) ct = new KnownContentType { Alias = ctGuid.ToString() }; + if (!knownDocumentTypes.TryGetValue(ctGuid, out var ct)) ct = new KnownContentType { Key = ctGuid }; obj.Remove("key"); obj.Remove("icContentTypeGuid"); @@ -292,6 +292,7 @@ namespace Umbraco.Core.Migrations.Upgrade.V_8_7_0 private class KnownContentType { public string Alias { get; set; } + public Guid Key { get; set; } public string[] StringToRawProperties { get; set; } } } From 32a30aad97eb374c406a8aeac8d57704e4e8d46a Mon Sep 17 00:00:00 2001 From: Claus Date: Tue, 9 Jun 2020 14:29:56 +0200 Subject: [PATCH 168/377] Fix glitch in umb-checkbox background --- .../src/less/components/umb-form-check.less | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-form-check.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-form-check.less index 76a4df0056..f24fa05ce3 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-form-check.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-form-check.less @@ -37,6 +37,7 @@ } &:checked ~ .umb-form-check__state .umb-form-check__check { border-color: @ui-option-type; + background-color: @ui-option-type; } &:checked:hover ~ .umb-form-check__state .umb-form-check__check { &::before { From a5adb322f1431bc1b6ed736b71100a704787ab0d Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 10 Jun 2020 16:12:00 +1000 Subject: [PATCH 169/377] Updates the caching layer to handle GUID keys for content types while preserving backwards compat, fixes unit tests, removes the strongly typed lists for the block editor value since it's unecessary --- .../PublishedContent/IPublishedContentType.cs | 16 ++- .../IPublishedContentTypeFactory.cs | 1 + .../IPublishedModelFactory.cs | 1 + .../PublishedContentExtensionsForModels.cs | 1 + .../PublishedContent/PublishedContentType.cs | 35 +++++- .../PublishedContentTypeExtensions.cs | 24 ++++ .../PublishedContentTypeFactory.cs | 8 +- src/Umbraco.Core/Umbraco.Core.csproj | 1 + .../PublishedMediaCacheTests.cs | 6 +- .../PublishedContentCache.cs | 14 +-- .../PublishedMediaCache.cs | 14 +-- .../BlockListPropertyValueConverterTests.cs | 47 ++++---- .../PropertyEditorValueConverterTests.cs | 2 +- .../Published/ConvertersTests.cs | 42 +++---- .../Published/NestedContentTests.cs | 6 +- .../Published/PropertyCacheLevelTests.cs | 6 +- .../PublishedContentDataTableTests.cs | 4 +- .../PublishedContentLanguageVariantTests.cs | 4 +- .../PublishedContentMoreTests.cs | 7 +- .../PublishedContentTestBase.cs | 3 +- .../PublishedContent/PublishedContentTests.cs | 16 +-- .../PublishedContent/PublishedRouterTests.cs | 2 +- .../SolidPublishedSnapshot.cs | 31 +++--- .../Routing/ContentFinderByAliasTests.cs | 5 +- .../ContentFinderByAliasWithDomainsTests.cs | 5 +- .../Routing/MediaUrlProviderTests.cs | 4 +- .../Routing/RenderRouteHandlerTests.cs | 2 +- src/Umbraco.Tests/Routing/UrlProviderTests.cs | 6 +- .../Templates/HtmlImageSourceParserTests.cs | 2 +- .../Templates/HtmlLocalLinkParserTests.cs | 4 +- src/Umbraco.Tests/TestHelpers/BaseWebTest.cs | 2 +- .../Testing/TestingTests/MockTests.cs | 2 +- .../PropertyEditors/BlockListConfiguration.cs | 3 +- .../ValueConverters/BlockEditorConverter.cs | 12 +- .../BlockListPropertyValueConverter.cs | 17 ++- .../PublishedCache/IPublishedCache.cs | 10 ++ .../PublishedCache/IPublishedContentCache.cs | 5 + .../PublishedCache/IPublishedMediaCache.cs | 5 + .../PublishedCache/NuCache/ContentCache.cs | 14 +-- .../PublishedCache/NuCache/ContentStore.cs | 81 +++++++++----- .../PublishedCache/NuCache/MediaCache.cs | 14 +-- .../PublishedCache/PublishedCacheBase.cs | 4 +- .../PublishedContentTypeCache.cs | 105 ++++++++++++------ src/umbraco.sln | 23 +++- 44 files changed, 392 insertions(+), 224 deletions(-) create mode 100644 src/Umbraco.Core/Models/PublishedContent/PublishedContentTypeExtensions.cs diff --git a/src/Umbraco.Core/Models/PublishedContent/IPublishedContentType.cs b/src/Umbraco.Core/Models/PublishedContent/IPublishedContentType.cs index ab6920377c..cfc789324a 100644 --- a/src/Umbraco.Core/Models/PublishedContent/IPublishedContentType.cs +++ b/src/Umbraco.Core/Models/PublishedContent/IPublishedContentType.cs @@ -1,7 +1,21 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; namespace Umbraco.Core.Models.PublishedContent { + /// + /// Represents an type. + /// + /// Instances implementing the interface should be + /// immutable, ie if the content type changes, then a new instance needs to be created. + public interface IPublishedContentType2 : IPublishedContentType + { + /// + /// Gets the unique key for the content type. + /// + Guid Key { get; } + } + /// /// Represents an type. /// diff --git a/src/Umbraco.Core/Models/PublishedContent/IPublishedContentTypeFactory.cs b/src/Umbraco.Core/Models/PublishedContent/IPublishedContentTypeFactory.cs index 89009ac7b8..1a29f970d9 100644 --- a/src/Umbraco.Core/Models/PublishedContent/IPublishedContentTypeFactory.cs +++ b/src/Umbraco.Core/Models/PublishedContent/IPublishedContentTypeFactory.cs @@ -1,5 +1,6 @@ namespace Umbraco. Core.Models.PublishedContent { + /// /// Creates published content types. /// diff --git a/src/Umbraco.Core/Models/PublishedContent/IPublishedModelFactory.cs b/src/Umbraco.Core/Models/PublishedContent/IPublishedModelFactory.cs index ae4caf352e..60fa0fe603 100644 --- a/src/Umbraco.Core/Models/PublishedContent/IPublishedModelFactory.cs +++ b/src/Umbraco.Core/Models/PublishedContent/IPublishedModelFactory.cs @@ -3,6 +3,7 @@ using System.Collections; namespace Umbraco.Core.Models.PublishedContent { + /// /// Provides the published model creation service. /// diff --git a/src/Umbraco.Core/Models/PublishedContent/PublishedContentExtensionsForModels.cs b/src/Umbraco.Core/Models/PublishedContent/PublishedContentExtensionsForModels.cs index bfc65b70d6..033396e4a1 100644 --- a/src/Umbraco.Core/Models/PublishedContent/PublishedContentExtensionsForModels.cs +++ b/src/Umbraco.Core/Models/PublishedContent/PublishedContentExtensionsForModels.cs @@ -3,6 +3,7 @@ using Umbraco.Core.Composing; namespace Umbraco.Core.Models.PublishedContent { + /// /// Provides strongly typed published content models services. /// diff --git a/src/Umbraco.Core/Models/PublishedContent/PublishedContentType.cs b/src/Umbraco.Core/Models/PublishedContent/PublishedContentType.cs index 458b63ade3..7aa9b0dfd9 100644 --- a/src/Umbraco.Core/Models/PublishedContent/PublishedContentType.cs +++ b/src/Umbraco.Core/Models/PublishedContent/PublishedContentType.cs @@ -9,7 +9,7 @@ namespace Umbraco.Core.Models.PublishedContent /// /// Instances of the class are immutable, ie /// if the content type changes, then a new class needs to be created. - public class PublishedContentType : IPublishedContentType + public class PublishedContentType : IPublishedContentType2 { private readonly IPublishedPropertyType[] _propertyTypes; @@ -20,7 +20,7 @@ namespace Umbraco.Core.Models.PublishedContent /// Initializes a new instance of the class with a content type. /// public PublishedContentType(IContentTypeComposition contentType, IPublishedContentTypeFactory factory) - : this(contentType.Id, contentType.Alias, contentType.GetItemType(), contentType.CompositionAliases(), contentType.Variations, contentType.IsElement) + : this(contentType.Key, contentType.Id, contentType.Alias, contentType.GetItemType(), contentType.CompositionAliases(), contentType.Variations, contentType.IsElement) { var propertyTypes = contentType.CompositionPropertyTypes .Select(x => factory.CreatePropertyType(this, x)) @@ -40,8 +40,20 @@ namespace Umbraco.Core.Models.PublishedContent /// /// Values are assumed to be consistent and are not checked. /// + public PublishedContentType(Guid key, int id, string alias, PublishedItemType itemType, IEnumerable compositionAliases, IEnumerable propertyTypes, ContentVariation variations, bool isElement = false) + : this(key, id, alias, itemType, compositionAliases, variations, isElement) + { + var propertyTypesA = propertyTypes.ToArray(); + foreach (var propertyType in propertyTypesA) + propertyType.ContentType = this; + _propertyTypes = propertyTypesA; + + InitializeIndexes(); + } + + [Obsolete("Use the overload specifying a key instead")] public PublishedContentType(int id, string alias, PublishedItemType itemType, IEnumerable compositionAliases, IEnumerable propertyTypes, ContentVariation variations, bool isElement = false) - : this (id, alias, itemType, compositionAliases, variations, isElement) + : this (Guid.Empty, id, alias, itemType, compositionAliases, variations, isElement) { var propertyTypesA = propertyTypes.ToArray(); foreach (var propertyType in propertyTypesA) @@ -57,16 +69,26 @@ namespace Umbraco.Core.Models.PublishedContent /// /// Values are assumed to be consistent and are not checked. /// + public PublishedContentType(Guid key, int id, string alias, PublishedItemType itemType, IEnumerable compositionAliases, Func> propertyTypes, ContentVariation variations, bool isElement = false) + : this(key, id, alias, itemType, compositionAliases, variations, isElement) + { + _propertyTypes = propertyTypes(this).ToArray(); + + InitializeIndexes(); + } + + [Obsolete("Use the overload specifying a key instead")] public PublishedContentType(int id, string alias, PublishedItemType itemType, IEnumerable compositionAliases, Func> propertyTypes, ContentVariation variations, bool isElement = false) - : this(id, alias, itemType, compositionAliases, variations, isElement) + : this(Guid.Empty, id, alias, itemType, compositionAliases, variations, isElement) { _propertyTypes = propertyTypes(this).ToArray(); InitializeIndexes(); } - private PublishedContentType(int id, string alias, PublishedItemType itemType, IEnumerable compositionAliases, ContentVariation variations, bool isElement) + private PublishedContentType(Guid key, int id, string alias, PublishedItemType itemType, IEnumerable compositionAliases, ContentVariation variations, bool isElement) { + Key = key; Id = id; Alias = alias; ItemType = itemType; @@ -116,6 +138,9 @@ namespace Umbraco.Core.Models.PublishedContent #region Content type + /// + public Guid Key { get; } + /// public int Id { get; } diff --git a/src/Umbraco.Core/Models/PublishedContent/PublishedContentTypeExtensions.cs b/src/Umbraco.Core/Models/PublishedContent/PublishedContentTypeExtensions.cs new file mode 100644 index 0000000000..feab33c1d6 --- /dev/null +++ b/src/Umbraco.Core/Models/PublishedContent/PublishedContentTypeExtensions.cs @@ -0,0 +1,24 @@ +using System; + +namespace Umbraco.Core.Models.PublishedContent +{ + public static class PublishedContentTypeExtensions + { + /// + /// Get the GUID key from an + /// + /// + /// + /// + public static bool TryGetKey(this IPublishedContentType publishedContentType, out Guid key) + { + if (publishedContentType is IPublishedContentType2 contentTypeWithKey) + { + key = contentTypeWithKey.Key; + return true; + } + key = Guid.Empty; + return false; + } + } +} diff --git a/src/Umbraco.Core/Models/PublishedContent/PublishedContentTypeFactory.cs b/src/Umbraco.Core/Models/PublishedContent/PublishedContentTypeFactory.cs index 34094508c3..c1548d3c3d 100644 --- a/src/Umbraco.Core/Models/PublishedContent/PublishedContentTypeFactory.cs +++ b/src/Umbraco.Core/Models/PublishedContent/PublishedContentTypeFactory.cs @@ -35,18 +35,18 @@ namespace Umbraco.Core.Models.PublishedContent /// This method is for tests and is not intended to be used directly from application code. /// /// Values are assumed to be consisted and are not checked. - internal IPublishedContentType CreateContentType(int id, string alias, Func> propertyTypes, ContentVariation variations = ContentVariation.Nothing, bool isElement = false) + internal IPublishedContentType CreateContentType(Guid key, int id, string alias, Func> propertyTypes, ContentVariation variations = ContentVariation.Nothing, bool isElement = false) { - return new PublishedContentType(id, alias, PublishedItemType.Content, Enumerable.Empty(), propertyTypes, variations, isElement); + return new PublishedContentType(key, id, alias, PublishedItemType.Content, Enumerable.Empty(), propertyTypes, variations, isElement); } /// /// This method is for tests and is not intended to be used directly from application code. /// /// Values are assumed to be consisted and are not checked. - internal IPublishedContentType CreateContentType(int id, string alias, IEnumerable compositionAliases, Func> propertyTypes, ContentVariation variations = ContentVariation.Nothing, bool isElement = false) + internal IPublishedContentType CreateContentType(Guid key, int id, string alias, IEnumerable compositionAliases, Func> propertyTypes, ContentVariation variations = ContentVariation.Nothing, bool isElement = false) { - return new PublishedContentType(id, alias, PublishedItemType.Content, compositionAliases, propertyTypes, variations, isElement); + return new PublishedContentType(key, id, alias, PublishedItemType.Content, compositionAliases, propertyTypes, variations, isElement); } /// diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index 809074073b..408abed4e0 100755 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -139,6 +139,7 @@ + diff --git a/src/Umbraco.Tests/Cache/PublishedCache/PublishedMediaCacheTests.cs b/src/Umbraco.Tests/Cache/PublishedCache/PublishedMediaCacheTests.cs index f3d9f895ef..2346740ffb 100644 --- a/src/Umbraco.Tests/Cache/PublishedCache/PublishedMediaCacheTests.cs +++ b/src/Umbraco.Tests/Cache/PublishedCache/PublishedMediaCacheTests.cs @@ -42,9 +42,9 @@ namespace Umbraco.Tests.Cache.PublishedCache protected override void Initialize() { base.Initialize(); - var type = new AutoPublishedContentType(22, "myType", new PublishedPropertyType[] { }); - var image = new AutoPublishedContentType(23, "Image", new PublishedPropertyType[] { }); - var testMediaType = new AutoPublishedContentType(24, "TestMediaType", new PublishedPropertyType[] { }); + var type = new AutoPublishedContentType(Guid.NewGuid(), 22, "myType", new PublishedPropertyType[] { }); + var image = new AutoPublishedContentType(Guid.NewGuid(), 23, "Image", new PublishedPropertyType[] { }); + var testMediaType = new AutoPublishedContentType(Guid.NewGuid(), 24, "TestMediaType", new PublishedPropertyType[] { }); _mediaTypes = new Dictionary { { type.Alias, type }, diff --git a/src/Umbraco.Tests/LegacyXmlPublishedCache/PublishedContentCache.cs b/src/Umbraco.Tests/LegacyXmlPublishedCache/PublishedContentCache.cs index 8ce6b10983..48f0e7b27e 100644 --- a/src/Umbraco.Tests/LegacyXmlPublishedCache/PublishedContentCache.cs +++ b/src/Umbraco.Tests/LegacyXmlPublishedCache/PublishedContentCache.cs @@ -14,7 +14,7 @@ using Umbraco.Web.Routing; namespace Umbraco.Tests.LegacyXmlPublishedCache { - internal class PublishedContentCache : PublishedCacheBase, IPublishedContentCache + internal class PublishedContentCache : PublishedCacheBase, IPublishedContentCache2 { private readonly IAppCache _appCache; private readonly IGlobalSettings _globalSettings; @@ -532,15 +532,11 @@ namespace Umbraco.Tests.LegacyXmlPublishedCache #region Content types - public override IPublishedContentType GetContentType(int id) - { - return _contentTypeCache.Get(PublishedItemType.Content, id); - } + public override IPublishedContentType GetContentType(int id) => _contentTypeCache.Get(PublishedItemType.Content, id); - public override IPublishedContentType GetContentType(string alias) - { - return _contentTypeCache.Get(PublishedItemType.Content, alias); - } + public override IPublishedContentType GetContentType(string alias) => _contentTypeCache.Get(PublishedItemType.Content, alias); + + public override IPublishedContentType GetContentType(Guid key) => _contentTypeCache.Get(PublishedItemType.Content, key); #endregion } diff --git a/src/Umbraco.Tests/LegacyXmlPublishedCache/PublishedMediaCache.cs b/src/Umbraco.Tests/LegacyXmlPublishedCache/PublishedMediaCache.cs index 999d7f040d..56033e6b0a 100644 --- a/src/Umbraco.Tests/LegacyXmlPublishedCache/PublishedMediaCache.cs +++ b/src/Umbraco.Tests/LegacyXmlPublishedCache/PublishedMediaCache.cs @@ -28,7 +28,7 @@ namespace Umbraco.Tests.LegacyXmlPublishedCache /// /// NOTE: In the future if we want to properly cache all media this class can be extended or replaced when these classes/interfaces are exposed publicly. /// - internal class PublishedMediaCache : PublishedCacheBase, IPublishedMediaCache + internal class PublishedMediaCache : PublishedCacheBase, IPublishedMediaCache2 { private readonly IMediaService _mediaService; private readonly IUserService _userService; @@ -612,15 +612,11 @@ namespace Umbraco.Tests.LegacyXmlPublishedCache #region Content types - public override IPublishedContentType GetContentType(int id) - { - return _contentTypeCache.Get(PublishedItemType.Media, id); - } + public override IPublishedContentType GetContentType(int id) => _contentTypeCache.Get(PublishedItemType.Media, id); - public override IPublishedContentType GetContentType(string alias) - { - return _contentTypeCache.Get(PublishedItemType.Media, alias); - } + public override IPublishedContentType GetContentType(string alias) => _contentTypeCache.Get(PublishedItemType.Media, alias); + + public override IPublishedContentType GetContentType(Guid key) => _contentTypeCache.Get(PublishedItemType.Media, key); public override IEnumerable GetByContentType(IPublishedContentType contentType) { diff --git a/src/Umbraco.Tests/PropertyEditors/BlockListPropertyValueConverterTests.cs b/src/Umbraco.Tests/PropertyEditors/BlockListPropertyValueConverterTests.cs index 6a7ec33a5a..655db4e337 100644 --- a/src/Umbraco.Tests/PropertyEditors/BlockListPropertyValueConverterTests.cs +++ b/src/Umbraco.Tests/PropertyEditors/BlockListPropertyValueConverterTests.cs @@ -18,21 +18,28 @@ namespace Umbraco.Tests.PropertyEditors [TestFixture] public class BlockListPropertyValueConverterTests { + private readonly Guid Key1 = Guid.NewGuid(); + private readonly Guid Key2 = Guid.NewGuid(); + private readonly string Alias1 = "Test1"; + private readonly string Alias2 = "Test2"; + /// /// Setup mocks for IPublishedSnapshotAccessor /// /// private IPublishedSnapshotAccessor GetPublishedSnapshotAccessor() { - var test1ContentType = Mock.Of(x => + var test1ContentType = Mock.Of(x => x.IsElement == true - && x.Alias == "Test1"); - var test2ContentType = Mock.Of(x => + && x.Key == Key1 + && x.Alias == Alias1); + var test2ContentType = Mock.Of(x => x.IsElement == true - && x.Alias == "Test2"); - var contentCache = new Mock(); - contentCache.Setup(x => x.GetContentType("Test1")).Returns(test1ContentType); - contentCache.Setup(x => x.GetContentType("Test2")).Returns(test2ContentType); + && x.Key == Key2 + && x.Alias == Alias2); + var contentCache = new Mock(); + contentCache.Setup(x => x.GetContentType(Key1)).Returns(test1ContentType); + contentCache.Setup(x => x.GetContentType(Key2)).Returns(test2ContentType); var publishedSnapshot = Mock.Of(x => x.Content == contentCache.Object); var publishedSnapshotAccessor = Mock.Of(x => x.PublishedSnapshot == publishedSnapshot); return publishedSnapshotAccessor; @@ -44,7 +51,6 @@ namespace Umbraco.Tests.PropertyEditors var publishedModelFactory = new NoopPublishedModelFactory(); var editor = new BlockListPropertyValueConverter( Mock.Of(), - publishedModelFactory, new BlockEditorConverter(publishedSnapshotAccessor, publishedModelFactory)); return editor; } @@ -54,11 +60,11 @@ namespace Umbraco.Tests.PropertyEditors Blocks = new[] { new BlockListConfiguration.BlockConfiguration { - Alias = "Test1" + Key = Key1 }, new BlockListConfiguration.BlockConfiguration { - Alias = "Test2" + Key = Key2 } } }; @@ -68,7 +74,7 @@ namespace Umbraco.Tests.PropertyEditors Blocks = new[] { new BlockListConfiguration.BlockConfiguration { - Alias = "Test1" + Key = Key1 } } }; @@ -101,7 +107,8 @@ namespace Umbraco.Tests.PropertyEditors var valueType = editor.GetPropertyValueType(propType); - Assert.AreEqual(typeof(IEnumerable), valueType); + // the result is always block list model + Assert.AreEqual(typeof(BlockListModel), valueType); } [Test] @@ -115,10 +122,8 @@ namespace Umbraco.Tests.PropertyEditors var valueType = editor.GetPropertyValueType(propType); - var modelType = typeof(IEnumerable<>).MakeGenericType(ModelType.For(config.Blocks[0].Alias)); - - // we can't compare the exact match of types because ModelType.For generates a new/different type even if the same alias is used - Assert.AreEqual(modelType.FullName, valueType.FullName); + // the result is always block list model + Assert.AreEqual(typeof(BlockListModel), valueType); } [Test] @@ -225,7 +230,7 @@ data: []}"; }, data: [ { - 'contentTypeAlias': 'home', + 'contentTypeKey': '" + Key1 + @"', 'key': '1304E1DD-0000-4396-84FE-8A399231CB3D' } ] @@ -258,7 +263,7 @@ data: []}"; }, data: [ { - 'contentTypeAlias': 'Test1', + 'contentTypeKey': '" + Key1 + @"', 'udi': 'umb://element/1304E1DDAC87439684FE8A399231CB3D' } ] @@ -300,15 +305,15 @@ data: []}"; }, data: [ { - 'contentTypeAlias': 'Test1', + 'contentTypeKey': '" + Key1 + @"', 'udi': 'umb://element/1304E1DDAC87439684FE8A399231CB3D' }, { - 'contentTypeAlias': 'Test2', + 'contentTypeKey': '" + Key2 + @"', 'udi': 'umb://element/E05A034704424AB3A520E048E6197E79' }, { - 'contentTypeAlias': 'Test2', + 'contentTypeKey': '" + Key2 + @"', 'udi': 'umb://element/0A4A416E547D464FABCC6F345C17809A' } ] diff --git a/src/Umbraco.Tests/PropertyEditors/PropertyEditorValueConverterTests.cs b/src/Umbraco.Tests/PropertyEditors/PropertyEditorValueConverterTests.cs index 43c1a83d33..a9e3e8b9db 100644 --- a/src/Umbraco.Tests/PropertyEditors/PropertyEditorValueConverterTests.cs +++ b/src/Umbraco.Tests/PropertyEditors/PropertyEditorValueConverterTests.cs @@ -93,7 +93,7 @@ namespace Umbraco.Tests.PropertyEditors }))); var publishedPropType = new PublishedPropertyType( - new PublishedContentType(1234, "test", PublishedItemType.Content, Enumerable.Empty(), Enumerable.Empty(), ContentVariation.Nothing), + new PublishedContentType(Guid.NewGuid(), 1234, "test", PublishedItemType.Content, Enumerable.Empty(), Enumerable.Empty(), ContentVariation.Nothing), new PropertyType("test", ValueStorageType.Nvarchar) { DataTypeId = 123 }, new PropertyValueConverterCollection(Enumerable.Empty()), Mock.Of(), mockPublishedContentTypeFactory.Object); diff --git a/src/Umbraco.Tests/Published/ConvertersTests.cs b/src/Umbraco.Tests/Published/ConvertersTests.cs index 671129848c..3c60f4ddd0 100644 --- a/src/Umbraco.Tests/Published/ConvertersTests.cs +++ b/src/Umbraco.Tests/Published/ConvertersTests.cs @@ -40,7 +40,7 @@ namespace Umbraco.Tests.Published yield return contentTypeFactory.CreatePropertyType(contentType, "prop1", 1); } - var elementType1 = contentTypeFactory.CreateContentType(1000, "element1", CreatePropertyTypes); + var elementType1 = contentTypeFactory.CreateContentType(Guid.NewGuid(), 1000, "element1", CreatePropertyTypes); var element1 = new PublishedElement(elementType1, Guid.NewGuid(), new Dictionary { { "prop1", "1234" } }, false); @@ -74,7 +74,7 @@ namespace Umbraco.Tests.Published => propertyType.EditorAlias.InvariantEquals("Umbraco.Void"); public Type GetPropertyValueType(IPublishedPropertyType propertyType) - => typeof (int); + => typeof(int); public PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) => PropertyCacheLevel.Element; @@ -83,10 +83,10 @@ namespace Umbraco.Tests.Published => int.TryParse(source as string, out int i) ? i : 0; public object ConvertIntermediateToObject(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object inter, bool preview) - => (int) inter; + => (int)inter; public object ConvertIntermediateToXPath(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object inter, bool preview) - => ((int) inter).ToString(); + => ((int)inter).ToString(); } #endregion @@ -120,11 +120,11 @@ namespace Umbraco.Tests.Published yield return contentTypeFactory.CreatePropertyType(contentType, "prop1", 1); } - var elementType1 = contentTypeFactory.CreateContentType(1000, "element1", CreatePropertyTypes); + var elementType1 = contentTypeFactory.CreateContentType(Guid.NewGuid(), 1000, "element1", CreatePropertyTypes); var element1 = new PublishedElement(elementType1, Guid.NewGuid(), new Dictionary { { "prop1", "1234" } }, false); - var cntType1 = contentTypeFactory.CreateContentType(1001, "cnt1", t => Enumerable.Empty()); + var cntType1 = contentTypeFactory.CreateContentType(Guid.NewGuid(), 1001, "cnt1", t => Enumerable.Empty()); var cnt1 = new SolidPublishedContent(cntType1) { Id = 1234 }; cacheContent[cnt1.Id] = cnt1; @@ -143,7 +143,7 @@ namespace Umbraco.Tests.Published } public bool? IsValue(object value, PropertyValueLevel level) - => value != null && (!(value is string) || string.IsNullOrWhiteSpace((string) value) == false); + => value != null && (!(value is string) || string.IsNullOrWhiteSpace((string)value) == false); public bool IsConverter(IPublishedPropertyType propertyType) => propertyType.EditorAlias.InvariantEquals("Umbraco.Void"); @@ -162,10 +162,10 @@ namespace Umbraco.Tests.Published => int.TryParse(source as string, out int i) ? i : -1; public object ConvertIntermediateToObject(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object inter, bool preview) - => _publishedSnapshotAccessor.PublishedSnapshot.Content.GetById((int) inter); + => _publishedSnapshotAccessor.PublishedSnapshot.Content.GetById((int)inter); public object ConvertIntermediateToXPath(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object inter, bool preview) - => ((int) inter).ToString(); + => ((int)inter).ToString(); } #endregion @@ -215,10 +215,10 @@ namespace Umbraco.Tests.Published yield return contentTypeFactory.CreatePropertyType(contentType, "prop" + i, i); } - var elementType1 = contentTypeFactory.CreateContentType(1000, "element1", t => CreatePropertyTypes(t, 1)); - var elementType2 = contentTypeFactory.CreateContentType(1001, "element2", t => CreatePropertyTypes(t, 2)); - var contentType1 = contentTypeFactory.CreateContentType(1002, "content1", t => CreatePropertyTypes(t, 1)); - var contentType2 = contentTypeFactory.CreateContentType(1003, "content2", t => CreatePropertyTypes(t, 2)); + var elementType1 = contentTypeFactory.CreateContentType(Guid.NewGuid(), 1000, "element1", t => CreatePropertyTypes(t, 1)); + var elementType2 = contentTypeFactory.CreateContentType(Guid.NewGuid(), 1001, "element2", t => CreatePropertyTypes(t, 2)); + var contentType1 = contentTypeFactory.CreateContentType(Guid.NewGuid(), 1002, "content1", t => CreatePropertyTypes(t, 1)); + var contentType2 = contentTypeFactory.CreateContentType(Guid.NewGuid(), 1003, "content2", t => CreatePropertyTypes(t, 2)); var element1 = new PublishedElement(elementType1, Guid.NewGuid(), new Dictionary { { "prop1", "val1" } }, false); var element2 = new PublishedElement(elementType2, Guid.NewGuid(), new Dictionary { { "prop2", "1003" } }, false); @@ -239,22 +239,22 @@ namespace Umbraco.Tests.Published // can get the actual property Clr type // ie ModelType gets properly mapped by IPublishedContentModelFactory // must test ModelClrType with special equals 'cos they are not ref-equals - Assert.IsTrue(ModelType.Equals(typeof (IEnumerable<>).MakeGenericType(ModelType.For("content1")), contentType2.GetPropertyType("prop2").ModelClrType)); - Assert.AreEqual(typeof (IEnumerable), contentType2.GetPropertyType("prop2").ClrType); + Assert.IsTrue(ModelType.Equals(typeof(IEnumerable<>).MakeGenericType(ModelType.For("content1")), contentType2.GetPropertyType("prop2").ModelClrType)); + Assert.AreEqual(typeof(IEnumerable), contentType2.GetPropertyType("prop2").ClrType); // can create a model for an element var model1 = factory.CreateModel(element1); Assert.IsInstanceOf(model1); - Assert.AreEqual("val1", ((PublishedSnapshotTestObjects.TestElementModel1) model1).Prop1); + Assert.AreEqual("val1", ((PublishedSnapshotTestObjects.TestElementModel1)model1).Prop1); // can create a model for a published content var model2 = factory.CreateModel(element2); Assert.IsInstanceOf(model2); - var mmodel2 = (PublishedSnapshotTestObjects.TestElementModel2) model2; + var mmodel2 = (PublishedSnapshotTestObjects.TestElementModel2)model2; // and get direct property Assert.IsInstanceOf(model2.Value("prop2")); - Assert.AreEqual(1, ((PublishedSnapshotTestObjects.TestContentModel1[]) model2.Value("prop2")).Length); + Assert.AreEqual(1, ((PublishedSnapshotTestObjects.TestContentModel1[])model2.Value("prop2")).Length); // and get model property Assert.IsInstanceOf>(mmodel2.Prop2); @@ -271,7 +271,7 @@ namespace Umbraco.Tests.Published => propertyType.EditorAlias == "Umbraco.Void"; public override Type GetPropertyValueType(IPublishedPropertyType propertyType) - => typeof (string); + => typeof(string); public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) => PropertyCacheLevel.Element; @@ -290,7 +290,7 @@ namespace Umbraco.Tests.Published => propertyType.EditorAlias == "Umbraco.Void.2"; public override Type GetPropertyValueType(IPublishedPropertyType propertyType) - => typeof (IEnumerable<>).MakeGenericType(ModelType.For("content1")); + => typeof(IEnumerable<>).MakeGenericType(ModelType.For("content1")); public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) => PropertyCacheLevel.Elements; @@ -303,7 +303,7 @@ namespace Umbraco.Tests.Published public override object ConvertIntermediateToObject(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object inter, bool preview) { - return ((int[]) inter).Select(x => (PublishedSnapshotTestObjects.TestContentModel1) _publishedSnapshotAccessor.PublishedSnapshot.Content.GetById(x)).ToArray(); + return ((int[])inter).Select(x => (PublishedSnapshotTestObjects.TestContentModel1)_publishedSnapshotAccessor.PublishedSnapshot.Content.GetById(x)).ToArray(); } } diff --git a/src/Umbraco.Tests/Published/NestedContentTests.cs b/src/Umbraco.Tests/Published/NestedContentTests.cs index a102b9f93e..70da652a9a 100644 --- a/src/Umbraco.Tests/Published/NestedContentTests.cs +++ b/src/Umbraco.Tests/Published/NestedContentTests.cs @@ -144,9 +144,9 @@ namespace Umbraco.Tests.Published yield return factory.CreatePropertyType(contentType, "propertyN1", 3); } - var contentType1 = factory.CreateContentType(1, "content1", CreatePropertyTypes1); - var contentType2 = factory.CreateContentType(2, "content2", CreatePropertyTypes2); - var contentTypeN1 = factory.CreateContentType(2, "contentN1", CreatePropertyTypesN1, isElement: true); + var contentType1 = factory.CreateContentType(Guid.NewGuid(), 1, "content1", CreatePropertyTypes1); + var contentType2 = factory.CreateContentType(Guid.NewGuid(), 2, "content2", CreatePropertyTypes2); + var contentTypeN1 = factory.CreateContentType(Guid.NewGuid(), 2, "contentN1", CreatePropertyTypesN1, isElement: true); // mocked content cache returns content types contentCache diff --git a/src/Umbraco.Tests/Published/PropertyCacheLevelTests.cs b/src/Umbraco.Tests/Published/PropertyCacheLevelTests.cs index 9db539d142..a795ca433e 100644 --- a/src/Umbraco.Tests/Published/PropertyCacheLevelTests.cs +++ b/src/Umbraco.Tests/Published/PropertyCacheLevelTests.cs @@ -41,7 +41,7 @@ namespace Umbraco.Tests.Published yield return publishedContentTypeFactory.CreatePropertyType(contentType, "prop1", 1); } - var setType1 = publishedContentTypeFactory.CreateContentType(1000, "set1", CreatePropertyTypes); + var setType1 = publishedContentTypeFactory.CreateContentType(Guid.NewGuid(), 1000, "set1", CreatePropertyTypes); // PublishedElementPropertyBase.GetCacheLevels: // @@ -122,7 +122,7 @@ namespace Umbraco.Tests.Published yield return publishedContentTypeFactory.CreatePropertyType(contentType, "prop1", 1); } - var setType1 = publishedContentTypeFactory.CreateContentType(1000, "set1", CreatePropertyTypes); + var setType1 = publishedContentTypeFactory.CreateContentType(Guid.NewGuid(), 1000, "set1", CreatePropertyTypes); var elementsCache = new FastDictionaryAppCache(); var snapshotCache = new FastDictionaryAppCache(); @@ -199,7 +199,7 @@ namespace Umbraco.Tests.Published yield return publishedContentTypeFactory.CreatePropertyType(contentType, "prop1", 1); } - var setType1 = publishedContentTypeFactory.CreateContentType(1000, "set1", CreatePropertyTypes); + var setType1 = publishedContentTypeFactory.CreateContentType(Guid.NewGuid(), 1000, "set1", CreatePropertyTypes); Assert.Throws(() => { diff --git a/src/Umbraco.Tests/PublishedContent/PublishedContentDataTableTests.cs b/src/Umbraco.Tests/PublishedContent/PublishedContentDataTableTests.cs index cc455b8e5d..7277f75be2 100644 --- a/src/Umbraco.Tests/PublishedContent/PublishedContentDataTableTests.cs +++ b/src/Umbraco.Tests/PublishedContent/PublishedContentDataTableTests.cs @@ -98,7 +98,7 @@ namespace Umbraco.Tests.PublishedContent var doc = GetContent(true, 1); //change a doc type alias var c = (SolidPublishedContent)doc.Children.ElementAt(0); - c.ContentType = new PublishedContentType(22, "DontMatch", PublishedItemType.Content, Enumerable.Empty(), Enumerable.Empty(), ContentVariation.Nothing); + c.ContentType = new PublishedContentType(Guid.NewGuid(), 22, "DontMatch", PublishedItemType.Content, Enumerable.Empty(), Enumerable.Empty(), ContentVariation.Nothing); var dt = doc.ChildrenAsTable(Current.Services, "Child"); @@ -129,7 +129,7 @@ namespace Umbraco.Tests.PublishedContent var factory = new PublishedContentTypeFactory(Mock.Of(), new PropertyValueConverterCollection(Array.Empty()), dataTypeService); var contentTypeAlias = createChildren ? "Parent" : "Child"; - var contentType = new PublishedContentType(22, contentTypeAlias, PublishedItemType.Content, Enumerable.Empty(), Enumerable.Empty(), ContentVariation.Nothing); + var contentType = new PublishedContentType(Guid.NewGuid(), 22, contentTypeAlias, PublishedItemType.Content, Enumerable.Empty(), Enumerable.Empty(), ContentVariation.Nothing); var d = new SolidPublishedContent(contentType) { CreateDate = DateTime.Now, diff --git a/src/Umbraco.Tests/PublishedContent/PublishedContentLanguageVariantTests.cs b/src/Umbraco.Tests/PublishedContent/PublishedContentLanguageVariantTests.cs index 62447742ff..636f8502ed 100644 --- a/src/Umbraco.Tests/PublishedContent/PublishedContentLanguageVariantTests.cs +++ b/src/Umbraco.Tests/PublishedContent/PublishedContentLanguageVariantTests.cs @@ -73,14 +73,14 @@ namespace Umbraco.Tests.PublishedContent yield return factory.CreatePropertyType(contentType, "noprop", 1, variations: ContentVariation.Culture); } - var contentType1 = factory.CreateContentType(1, "ContentType1", Enumerable.Empty(), CreatePropertyTypes1); + var contentType1 = factory.CreateContentType(Guid.NewGuid(), 1, "ContentType1", Enumerable.Empty(), CreatePropertyTypes1); IEnumerable CreatePropertyTypes2(IPublishedContentType contentType) { yield return factory.CreatePropertyType(contentType, "prop3", 1, variations: ContentVariation.Culture); } - var contentType2 = factory.CreateContentType(2, "contentType2", Enumerable.Empty(), CreatePropertyTypes2); + var contentType2 = factory.CreateContentType(Guid.NewGuid(), 2, "contentType2", Enumerable.Empty(), CreatePropertyTypes2); var prop1 = new SolidPublishedPropertyWithLanguageVariants { diff --git a/src/Umbraco.Tests/PublishedContent/PublishedContentMoreTests.cs b/src/Umbraco.Tests/PublishedContent/PublishedContentMoreTests.cs index 440474ae74..e2a1721d26 100644 --- a/src/Umbraco.Tests/PublishedContent/PublishedContentMoreTests.cs +++ b/src/Umbraco.Tests/PublishedContent/PublishedContentMoreTests.cs @@ -7,6 +7,7 @@ using Umbraco.Web; using Umbraco.Core; using Umbraco.Tests.Testing; using Umbraco.Web.Composing; +using System; namespace Umbraco.Tests.PublishedContent { @@ -21,9 +22,9 @@ namespace Umbraco.Tests.PublishedContent yield return factory.CreatePropertyType(contentType, "prop1", 1); } - var contentType1 = factory.CreateContentType(1, "ContentType1", Enumerable.Empty(), CreatePropertyTypes); - var contentType2 = factory.CreateContentType(2, "ContentType2", Enumerable.Empty(), CreatePropertyTypes); - var contentType2Sub = factory.CreateContentType(3, "ContentType2Sub", Enumerable.Empty(), CreatePropertyTypes); + var contentType1 = factory.CreateContentType(Guid.NewGuid(), 1, "ContentType1", Enumerable.Empty(), CreatePropertyTypes); + var contentType2 = factory.CreateContentType(Guid.NewGuid(), 2, "ContentType2", Enumerable.Empty(), CreatePropertyTypes); + var contentType2Sub = factory.CreateContentType(Guid.NewGuid(), 3, "ContentType2Sub", Enumerable.Empty(), CreatePropertyTypes); var content = new SolidPublishedContent(contentType1) { diff --git a/src/Umbraco.Tests/PublishedContent/PublishedContentTestBase.cs b/src/Umbraco.Tests/PublishedContent/PublishedContentTestBase.cs index 59791fc645..7e0d0f332e 100644 --- a/src/Umbraco.Tests/PublishedContent/PublishedContentTestBase.cs +++ b/src/Umbraco.Tests/PublishedContent/PublishedContentTestBase.cs @@ -13,6 +13,7 @@ using Umbraco.Core.Services; using Umbraco.Web; using Umbraco.Web.Templates; using Umbraco.Web.Models; +using System; namespace Umbraco.Tests.PublishedContent { @@ -56,7 +57,7 @@ namespace Umbraco.Tests.PublishedContent yield return publishedContentTypeFactory.CreatePropertyType(contentType, "content", 1); } - var type = new AutoPublishedContentType(0, "anything", CreatePropertyTypes); + var type = new AutoPublishedContentType(Guid.NewGuid(), 0, "anything", CreatePropertyTypes); ContentTypesCache.GetPublishedContentTypeByAlias = alias => type; var umbracoContext = GetUmbracoContext("/test"); diff --git a/src/Umbraco.Tests/PublishedContent/PublishedContentTests.cs b/src/Umbraco.Tests/PublishedContent/PublishedContentTests.cs index 998fc92380..cafda161f4 100644 --- a/src/Umbraco.Tests/PublishedContent/PublishedContentTests.cs +++ b/src/Umbraco.Tests/PublishedContent/PublishedContentTests.cs @@ -83,8 +83,8 @@ namespace Umbraco.Tests.PublishedContent } var compositionAliases = new[] { "MyCompositionAlias" }; - var anythingType = new AutoPublishedContentType(0, "anything", compositionAliases, CreatePropertyTypes); - var homeType = new AutoPublishedContentType(0, "home", compositionAliases, CreatePropertyTypes); + var anythingType = new AutoPublishedContentType(Guid.NewGuid(), 0, "anything", compositionAliases, CreatePropertyTypes); + var homeType = new AutoPublishedContentType(Guid.NewGuid(), 0, "home", compositionAliases, CreatePropertyTypes); ContentTypesCache.GetPublishedContentTypeByAlias = alias => alias.InvariantEquals("home") ? homeType : anythingType; } @@ -398,8 +398,8 @@ namespace Umbraco.Tests.PublishedContent [Test] public void Children_GroupBy_DocumentTypeAlias() { - var home = new AutoPublishedContentType(22, "Home", new PublishedPropertyType[] { }); - var custom = new AutoPublishedContentType(23, "CustomDocument", new PublishedPropertyType[] { }); + var home = new AutoPublishedContentType(Guid.NewGuid(), 22, "Home", new PublishedPropertyType[] { }); + var custom = new AutoPublishedContentType(Guid.NewGuid(), 23, "CustomDocument", new PublishedPropertyType[] { }); var contentTypes = new Dictionary { { home.Alias, home }, @@ -419,8 +419,8 @@ namespace Umbraco.Tests.PublishedContent [Test] public void Children_Where_DocumentTypeAlias() { - var home = new AutoPublishedContentType(22, "Home", new PublishedPropertyType[] { }); - var custom = new AutoPublishedContentType(23, "CustomDocument", new PublishedPropertyType[] { }); + var home = new AutoPublishedContentType(Guid.NewGuid(), 22, "Home", new PublishedPropertyType[] { }); + var custom = new AutoPublishedContentType(Guid.NewGuid(), 23, "CustomDocument", new PublishedPropertyType[] { }); var contentTypes = new Dictionary { { home.Alias, home }, @@ -903,7 +903,7 @@ namespace Umbraco.Tests.PublishedContent yield return factory.CreatePropertyType(contentType, "detached", 1003); } - var ct = factory.CreateContentType(0, "alias", CreatePropertyTypes); + var ct = factory.CreateContentType(Guid.NewGuid(), 0, "alias", CreatePropertyTypes); var pt = ct.GetPropertyType("detached"); var prop = new PublishedElementPropertyBase(pt, null, false, PropertyCacheLevel.None, 5548); Assert.IsInstanceOf(prop.GetValue()); @@ -935,7 +935,7 @@ namespace Umbraco.Tests.PublishedContent var guid = Guid.NewGuid(); - var ct = factory.CreateContentType(0, "alias", CreatePropertyTypes); + var ct = factory.CreateContentType(Guid.NewGuid(), 0, "alias", CreatePropertyTypes); var c = new ImageWithLegendModel(ct, guid, new Dictionary { diff --git a/src/Umbraco.Tests/PublishedContent/PublishedRouterTests.cs b/src/Umbraco.Tests/PublishedContent/PublishedRouterTests.cs index d8dbabb569..4a93fadbdf 100644 --- a/src/Umbraco.Tests/PublishedContent/PublishedRouterTests.cs +++ b/src/Umbraco.Tests/PublishedContent/PublishedRouterTests.cs @@ -66,7 +66,7 @@ namespace Umbraco.Tests.PublishedContent pc.Setup(content => content.Path).Returns("-1,1"); pc.Setup(content => content.Parent).Returns(() => null); pc.Setup(content => content.Properties).Returns(new Collection()); - pc.Setup(content => content.ContentType).Returns(new PublishedContentType(22, "anything", PublishedItemType.Content, Enumerable.Empty(), Enumerable.Empty(), ContentVariation.Nothing)); + pc.Setup(content => content.ContentType).Returns(new PublishedContentType(Guid.NewGuid(), 22, "anything", PublishedItemType.Content, Enumerable.Empty(), Enumerable.Empty(), ContentVariation.Nothing)); return pc; } } diff --git a/src/Umbraco.Tests/PublishedContent/SolidPublishedSnapshot.cs b/src/Umbraco.Tests/PublishedContent/SolidPublishedSnapshot.cs index a7b6d3d18a..4a0af69999 100644 --- a/src/Umbraco.Tests/PublishedContent/SolidPublishedSnapshot.cs +++ b/src/Umbraco.Tests/PublishedContent/SolidPublishedSnapshot.cs @@ -44,7 +44,7 @@ namespace Umbraco.Tests.PublishedContent { } } - class SolidPublishedContentCache : PublishedCacheBase, IPublishedContentCache, IPublishedMediaCache + class SolidPublishedContentCache : PublishedCacheBase, IPublishedContentCache2, IPublishedMediaCache2 { private readonly Dictionary _content = new Dictionary(); @@ -150,6 +150,11 @@ namespace Umbraco.Tests.PublishedContent throw new NotImplementedException(); } + public override IPublishedContentType GetContentType(Guid key) + { + throw new NotImplementedException(); + } + public override IEnumerable GetByContentType(IPublishedContentType contentType) { throw new NotImplementedException(); @@ -378,7 +383,7 @@ namespace Umbraco.Tests.PublishedContent #endregion } - class PublishedContentStrong1 : PublishedContentModel + internal class PublishedContentStrong1 : PublishedContentModel { public PublishedContentStrong1(IPublishedContent content) : base(content) @@ -387,7 +392,7 @@ namespace Umbraco.Tests.PublishedContent public int StrongValue => this.Value("strongValue"); } - class PublishedContentStrong1Sub : PublishedContentStrong1 + internal class PublishedContentStrong1Sub : PublishedContentStrong1 { public PublishedContentStrong1Sub(IPublishedContent content) : base(content) @@ -396,7 +401,7 @@ namespace Umbraco.Tests.PublishedContent public int AnotherValue => this.Value("anotherValue"); } - class PublishedContentStrong2 : PublishedContentModel + internal class PublishedContentStrong2 : PublishedContentModel { public PublishedContentStrong2(IPublishedContent content) : base(content) @@ -405,7 +410,7 @@ namespace Umbraco.Tests.PublishedContent public int StrongValue => this.Value("strongValue"); } - class AutoPublishedContentType : PublishedContentType + internal class AutoPublishedContentType : PublishedContentType { private static readonly IPublishedPropertyType Default; @@ -418,20 +423,20 @@ namespace Umbraco.Tests.PublishedContent Default = factory.CreatePropertyType("*", 666); } - public AutoPublishedContentType(int id, string alias, IEnumerable propertyTypes) - : base(id, alias, PublishedItemType.Content, Enumerable.Empty(), propertyTypes, ContentVariation.Nothing) + public AutoPublishedContentType(Guid key, int id, string alias, IEnumerable propertyTypes) + : base(key, id, alias, PublishedItemType.Content, Enumerable.Empty(), propertyTypes, ContentVariation.Nothing) { } - public AutoPublishedContentType(int id, string alias, Func> propertyTypes) - : base(id, alias, PublishedItemType.Content, Enumerable.Empty(), propertyTypes, ContentVariation.Nothing) + public AutoPublishedContentType(Guid key, int id, string alias, Func> propertyTypes) + : base(key, id, alias, PublishedItemType.Content, Enumerable.Empty(), propertyTypes, ContentVariation.Nothing) { } - public AutoPublishedContentType(int id, string alias, IEnumerable compositionAliases, IEnumerable propertyTypes) - : base(id, alias, PublishedItemType.Content, compositionAliases, propertyTypes, ContentVariation.Nothing) + public AutoPublishedContentType(Guid key, int id, string alias, IEnumerable compositionAliases, IEnumerable propertyTypes) + : base(key, id, alias, PublishedItemType.Content, compositionAliases, propertyTypes, ContentVariation.Nothing) { } - public AutoPublishedContentType(int id, string alias, IEnumerable compositionAliases, Func> propertyTypes) - : base(id, alias, PublishedItemType.Content, compositionAliases, propertyTypes, ContentVariation.Nothing) + public AutoPublishedContentType(Guid key, int id, string alias, IEnumerable compositionAliases, Func> propertyTypes) + : base(key, id, alias, PublishedItemType.Content, compositionAliases, propertyTypes, ContentVariation.Nothing) { } public override IPublishedPropertyType GetPropertyType(string alias) diff --git a/src/Umbraco.Tests/Routing/ContentFinderByAliasTests.cs b/src/Umbraco.Tests/Routing/ContentFinderByAliasTests.cs index 2aa01916fb..25d3eda081 100644 --- a/src/Umbraco.Tests/Routing/ContentFinderByAliasTests.cs +++ b/src/Umbraco.Tests/Routing/ContentFinderByAliasTests.cs @@ -1,4 +1,5 @@ -using System.Linq; +using System; +using System.Linq; using Moq; using NUnit.Framework; using Umbraco.Core; @@ -27,7 +28,7 @@ namespace Umbraco.Tests.Routing Mock.Of(), Mock.Of()), }; - _publishedContentType = new PublishedContentType(0, "Doc", PublishedItemType.Content, Enumerable.Empty(), properties, ContentVariation.Nothing); + _publishedContentType = new PublishedContentType(Guid.NewGuid(), 0, "Doc", PublishedItemType.Content, Enumerable.Empty(), properties, ContentVariation.Nothing); } protected override PublishedContentType GetPublishedContentTypeByAlias(string alias) diff --git a/src/Umbraco.Tests/Routing/ContentFinderByAliasWithDomainsTests.cs b/src/Umbraco.Tests/Routing/ContentFinderByAliasWithDomainsTests.cs index 0c1f89f430..18fe268a9a 100644 --- a/src/Umbraco.Tests/Routing/ContentFinderByAliasWithDomainsTests.cs +++ b/src/Umbraco.Tests/Routing/ContentFinderByAliasWithDomainsTests.cs @@ -1,4 +1,5 @@ -using System.Linq; +using System; +using System.Linq; using Moq; using NUnit.Framework; using Umbraco.Core; @@ -25,7 +26,7 @@ namespace Umbraco.Tests.Routing Mock.Of(), Mock.Of()), }; - _publishedContentType = new PublishedContentType(0, "Doc", PublishedItemType.Content, Enumerable.Empty(), properties, ContentVariation.Nothing); + _publishedContentType = new PublishedContentType(Guid.NewGuid(), 0, "Doc", PublishedItemType.Content, Enumerable.Empty(), properties, ContentVariation.Nothing); } protected override PublishedContentType GetPublishedContentTypeByAlias(string alias) diff --git a/src/Umbraco.Tests/Routing/MediaUrlProviderTests.cs b/src/Umbraco.Tests/Routing/MediaUrlProviderTests.cs index 2f960d498d..3a1ff36a0a 100644 --- a/src/Umbraco.Tests/Routing/MediaUrlProviderTests.cs +++ b/src/Umbraco.Tests/Routing/MediaUrlProviderTests.cs @@ -139,7 +139,7 @@ namespace Umbraco.Tests.Routing property.SetSourceValue("en", enMediaUrl, true); property.SetSourceValue("da", daMediaUrl); - var contentType = new PublishedContentType(666, "alias", PublishedItemType.Content, Enumerable.Empty(), new [] { umbracoFilePropertyType }, ContentVariation.Culture); + var contentType = new PublishedContentType(Guid.NewGuid(), 666, "alias", PublishedItemType.Content, Enumerable.Empty(), new [] { umbracoFilePropertyType }, ContentVariation.Culture); var publishedContent = new SolidPublishedContent(contentType) {Properties = new[] {property}}; var resolvedUrl = umbracoContext.UrlProvider.GetMediaUrl(publishedContent, UrlMode.Auto, "da"); @@ -150,7 +150,7 @@ namespace Umbraco.Tests.Routing { var umbracoFilePropertyType = CreatePropertyType(propertyEditorAlias, dataTypeConfiguration, ContentVariation.Nothing); - var contentType = new PublishedContentType(666, "alias", PublishedItemType.Content, Enumerable.Empty(), + var contentType = new PublishedContentType(Guid.NewGuid(), 666, "alias", PublishedItemType.Content, Enumerable.Empty(), new[] {umbracoFilePropertyType}, ContentVariation.Nothing); return new SolidPublishedContent(contentType) diff --git a/src/Umbraco.Tests/Routing/RenderRouteHandlerTests.cs b/src/Umbraco.Tests/Routing/RenderRouteHandlerTests.cs index 135172460d..786eebea9f 100644 --- a/src/Umbraco.Tests/Routing/RenderRouteHandlerTests.cs +++ b/src/Umbraco.Tests/Routing/RenderRouteHandlerTests.cs @@ -140,7 +140,7 @@ namespace Umbraco.Tests.Routing frequest.TemplateModel = template; var umbracoContextAccessor = new TestUmbracoContextAccessor(umbracoContext); - var type = new AutoPublishedContentType(22, "CustomDocument", new PublishedPropertyType[] { }); + var type = new AutoPublishedContentType(Guid.NewGuid(), 22, "CustomDocument", new PublishedPropertyType[] { }); ContentTypesCache.GetPublishedContentTypeByAlias = alias => type; var handler = new RenderRouteHandler(umbracoContext, new TestControllerFactory(umbracoContextAccessor, Mock.Of(), context => diff --git a/src/Umbraco.Tests/Routing/UrlProviderTests.cs b/src/Umbraco.Tests/Routing/UrlProviderTests.cs index 02aa95cd2e..8043e25661 100644 --- a/src/Umbraco.Tests/Routing/UrlProviderTests.cs +++ b/src/Umbraco.Tests/Routing/UrlProviderTests.cs @@ -158,7 +158,7 @@ namespace Umbraco.Tests.Routing var umbracoSettings = Current.Configs.Settings(); - var contentType = new PublishedContentType(666, "alias", PublishedItemType.Content, Enumerable.Empty(), Enumerable.Empty(), ContentVariation.Culture); + var contentType = new PublishedContentType(Guid.NewGuid(), 666, "alias", PublishedItemType.Content, Enumerable.Empty(), Enumerable.Empty(), ContentVariation.Culture); var publishedContent = new SolidPublishedContent(contentType) { Id = 1234 }; var publishedContentCache = new Mock(); @@ -203,7 +203,7 @@ namespace Umbraco.Tests.Routing var umbracoSettings = Current.Configs.Settings(); - var contentType = new PublishedContentType(666, "alias", PublishedItemType.Content, Enumerable.Empty(), Enumerable.Empty(), ContentVariation.Culture); + var contentType = new PublishedContentType(Guid.NewGuid(), 666, "alias", PublishedItemType.Content, Enumerable.Empty(), Enumerable.Empty(), ContentVariation.Culture); var publishedContent = new SolidPublishedContent(contentType) { Id = 1234 }; var publishedContentCache = new Mock(); @@ -257,7 +257,7 @@ namespace Umbraco.Tests.Routing var umbracoSettings = Current.Configs.Settings(); - var contentType = new PublishedContentType(666, "alias", PublishedItemType.Content, Enumerable.Empty(), Enumerable.Empty(), ContentVariation.Culture); + var contentType = new PublishedContentType(Guid.NewGuid(), 666, "alias", PublishedItemType.Content, Enumerable.Empty(), Enumerable.Empty(), ContentVariation.Culture); var publishedContent = new SolidPublishedContent(contentType) { Id = 1234 }; var publishedContentCache = new Mock(); diff --git a/src/Umbraco.Tests/Templates/HtmlImageSourceParserTests.cs b/src/Umbraco.Tests/Templates/HtmlImageSourceParserTests.cs index bce9bd4155..6c40e2842d 100644 --- a/src/Umbraco.Tests/Templates/HtmlImageSourceParserTests.cs +++ b/src/Umbraco.Tests/Templates/HtmlImageSourceParserTests.cs @@ -65,7 +65,7 @@ namespace Umbraco.Tests.Templates { //setup a mock url provider which we'll use for testing - var mediaType = new PublishedContentType(777, "image", PublishedItemType.Media, Enumerable.Empty(), Enumerable.Empty(), ContentVariation.Nothing); + var mediaType = new PublishedContentType(Guid.NewGuid(), 777, "image", PublishedItemType.Media, Enumerable.Empty(), Enumerable.Empty(), ContentVariation.Nothing); var media = new Mock(); media.Setup(x => x.ContentType).Returns(mediaType); var mediaUrlProvider = new Mock(); diff --git a/src/Umbraco.Tests/Templates/HtmlLocalLinkParserTests.cs b/src/Umbraco.Tests/Templates/HtmlLocalLinkParserTests.cs index 7cd96a32ed..861a7e4db6 100644 --- a/src/Umbraco.Tests/Templates/HtmlLocalLinkParserTests.cs +++ b/src/Umbraco.Tests/Templates/HtmlLocalLinkParserTests.cs @@ -54,12 +54,12 @@ namespace Umbraco.Tests.Templates contentUrlProvider .Setup(x => x.GetUrl(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())) .Returns(UrlInfo.Url("/my-test-url")); - var contentType = new PublishedContentType(666, "alias", PublishedItemType.Content, Enumerable.Empty(), Enumerable.Empty(), ContentVariation.Nothing); + var contentType = new PublishedContentType(Guid.NewGuid(), 666, "alias", PublishedItemType.Content, Enumerable.Empty(), Enumerable.Empty(), ContentVariation.Nothing); var publishedContent = new Mock(); publishedContent.Setup(x => x.Id).Returns(1234); publishedContent.Setup(x => x.ContentType).Returns(contentType); - var mediaType = new PublishedContentType(777, "image", PublishedItemType.Media, Enumerable.Empty(), Enumerable.Empty(), ContentVariation.Nothing); + var mediaType = new PublishedContentType(Guid.NewGuid(), 777, "image", PublishedItemType.Media, Enumerable.Empty(), Enumerable.Empty(), ContentVariation.Nothing); var media = new Mock(); media.Setup(x => x.ContentType).Returns(mediaType); var mediaUrlProvider = new Mock(); diff --git a/src/Umbraco.Tests/TestHelpers/BaseWebTest.cs b/src/Umbraco.Tests/TestHelpers/BaseWebTest.cs index f7e3744600..7d8cedc9c6 100644 --- a/src/Umbraco.Tests/TestHelpers/BaseWebTest.cs +++ b/src/Umbraco.Tests/TestHelpers/BaseWebTest.cs @@ -42,7 +42,7 @@ namespace Umbraco.Tests.TestHelpers new DataType(new VoidEditor(Mock.Of())) { Id = 1 }); var factory = new PublishedContentTypeFactory(Mock.Of(), new PropertyValueConverterCollection(Array.Empty()), dataTypeService); - var type = new AutoPublishedContentType(0, "anything", new PublishedPropertyType[] { }); + var type = new AutoPublishedContentType(Guid.NewGuid(), 0, "anything", new PublishedPropertyType[] { }); ContentTypesCache.GetPublishedContentTypeByAlias = alias => GetPublishedContentTypeByAlias(alias) ?? type; } diff --git a/src/Umbraco.Tests/Testing/TestingTests/MockTests.cs b/src/Umbraco.Tests/Testing/TestingTests/MockTests.cs index ee91427a63..f53b0bfff0 100644 --- a/src/Umbraco.Tests/Testing/TestingTests/MockTests.cs +++ b/src/Umbraco.Tests/Testing/TestingTests/MockTests.cs @@ -86,7 +86,7 @@ namespace Umbraco.Tests.Testing.TestingTests var theUrlProvider = new UrlProvider(umbracoContext, new [] { urlProvider }, Enumerable.Empty(), umbracoContext.VariationContextAccessor); - var contentType = new PublishedContentType(666, "alias", PublishedItemType.Content, Enumerable.Empty(), Enumerable.Empty(), ContentVariation.Nothing); + var contentType = new PublishedContentType(Guid.NewGuid(), 666, "alias", PublishedItemType.Content, Enumerable.Empty(), Enumerable.Empty(), ContentVariation.Nothing); var publishedContent = Mock.Of(); Mock.Get(publishedContent).Setup(x => x.ContentType).Returns(contentType); diff --git a/src/Umbraco.Web/PropertyEditors/BlockListConfiguration.cs b/src/Umbraco.Web/PropertyEditors/BlockListConfiguration.cs index 71e74e05fe..7b0f98903c 100644 --- a/src/Umbraco.Web/PropertyEditors/BlockListConfiguration.cs +++ b/src/Umbraco.Web/PropertyEditors/BlockListConfiguration.cs @@ -1,4 +1,5 @@ using Newtonsoft.Json; +using System; using Umbraco.Core.PropertyEditors; namespace Umbraco.Web.PropertyEditors @@ -39,7 +40,7 @@ namespace Umbraco.Web.PropertyEditors public string Thumbnail { get; set; } [JsonProperty("contentTypeKey")] - public string Key { get; set; } + public Guid Key { get; set; } [JsonProperty("settingsElementTypeKey")] public string SettingsElementTypeKey { get; set; } diff --git a/src/Umbraco.Web/PropertyEditors/ValueConverters/BlockEditorConverter.cs b/src/Umbraco.Web/PropertyEditors/ValueConverters/BlockEditorConverter.cs index 3ca70cbd22..917462e2f2 100644 --- a/src/Umbraco.Web/PropertyEditors/ValueConverters/BlockEditorConverter.cs +++ b/src/Umbraco.Web/PropertyEditors/ValueConverters/BlockEditorConverter.cs @@ -23,13 +23,17 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters JObject sourceObject, string contentTypeKeyPropertyKey, PropertyCacheLevel referenceCacheLevel, bool preview) { - var elementTypeKey = sourceObject[contentTypeKeyPropertyKey]?.ToObject(); - if (string.IsNullOrEmpty(elementTypeKey)) + var elementTypeKey = sourceObject[contentTypeKeyPropertyKey]?.ToObject(); + if (!elementTypeKey.HasValue) return null; + // hack! we need to cast, we have n ochoice beacuse we cannot make breaking changes. + var publishedContentCache = _publishedSnapshotAccessor.PublishedSnapshot.Content as IPublishedContentCache2; + if (publishedContentCache == null) + throw new InvalidOperationException("The published content cache is not " + typeof(IPublishedContentCache2)); + // only convert element types - content types will cause an exception when PublishedModelFactory creates the model - // TODO: make this work with keys. - var publishedContentType = _publishedSnapshotAccessor.PublishedSnapshot.Content.GetContentType(elementTypeKey); + var publishedContentType = publishedContentCache.GetContentType(elementTypeKey.Value); if (publishedContentType == null || publishedContentType.IsElement == false) return null; diff --git a/src/Umbraco.Web/PropertyEditors/ValueConverters/BlockListPropertyValueConverter.cs b/src/Umbraco.Web/PropertyEditors/ValueConverters/BlockListPropertyValueConverter.cs index 7bd410f55a..a833efa2e5 100644 --- a/src/Umbraco.Web/PropertyEditors/ValueConverters/BlockListPropertyValueConverter.cs +++ b/src/Umbraco.Web/PropertyEditors/ValueConverters/BlockListPropertyValueConverter.cs @@ -17,13 +17,11 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters public class BlockListPropertyValueConverter : PropertyValueConverterBase { private readonly IProfilingLogger _proflog; - private readonly IPublishedModelFactory _publishedModelFactory; private readonly BlockEditorConverter _blockConverter; - public BlockListPropertyValueConverter(IProfilingLogger proflog, IPublishedModelFactory publishedModelFactory, BlockEditorConverter blockConverter) + public BlockListPropertyValueConverter(IProfilingLogger proflog, BlockEditorConverter blockConverter) { _proflog = proflog; - _publishedModelFactory = publishedModelFactory; _blockConverter = blockConverter; } @@ -54,11 +52,8 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters var configuration = propertyType.DataType.ConfigurationAs(); var contentTypes = configuration.Blocks; var contentTypeMap = contentTypes.ToDictionary(x => x.Key); - var elements = (contentTypes.Length == 1 - // TODO: make this work with key - ? (IList)_publishedModelFactory.CreateModelList(contentTypes[0].Key) - : new List()) - .ToDictionary(x => x.Key, x => x); + + var elements = new Dictionary(); var layout = new List(); var model = new BlockListModel(elements.Values, layout); @@ -102,8 +97,10 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters if (!elements.TryGetValue(guidUdi.Guid, out var data)) continue; - // Make this work with Key, not alias, since contentTypeMap is now a dctionary with contentTypeKey as the dictionary key. - if (!contentTypeMap.TryGetValue(data.ContentType.Key, out var blockConfig)) + if (!data.ContentType.TryGetKey(out var contentTypeKey)) + throw new InvalidOperationException("The content type was not of type " + typeof(IPublishedContentType2)); + + if (!contentTypeMap.TryGetValue(contentTypeKey, out var blockConfig)) continue; // this can happen if they have a settings type, save content, remove the settings type, and display the front-end page before saving the content again diff --git a/src/Umbraco.Web/PublishedCache/IPublishedCache.cs b/src/Umbraco.Web/PublishedCache/IPublishedCache.cs index 0370088f77..4760082908 100644 --- a/src/Umbraco.Web/PublishedCache/IPublishedCache.cs +++ b/src/Umbraco.Web/PublishedCache/IPublishedCache.cs @@ -7,6 +7,16 @@ using Umbraco.Core.Xml; namespace Umbraco.Web.PublishedCache { + public interface IPublishedCache2 : IPublishedCache + { + /// + /// Gets a content type identified by its alias. + /// + /// The content type key. + /// The content type, or null. + IPublishedContentType GetContentType(Guid key); + } + /// /// Provides access to cached contents. /// diff --git a/src/Umbraco.Web/PublishedCache/IPublishedContentCache.cs b/src/Umbraco.Web/PublishedCache/IPublishedContentCache.cs index b4a6e3d1e0..8175285c3a 100644 --- a/src/Umbraco.Web/PublishedCache/IPublishedContentCache.cs +++ b/src/Umbraco.Web/PublishedCache/IPublishedContentCache.cs @@ -4,6 +4,11 @@ using Umbraco.Core.Models.PublishedContent; namespace Umbraco.Web.PublishedCache { + public interface IPublishedContentCache2 : IPublishedContentCache, IPublishedCache2 + { + // NOTE: this is here purely to avoid API breaking changes + } + public interface IPublishedContentCache : IPublishedCache { /// diff --git a/src/Umbraco.Web/PublishedCache/IPublishedMediaCache.cs b/src/Umbraco.Web/PublishedCache/IPublishedMediaCache.cs index 0b461882b7..702b4fe49d 100644 --- a/src/Umbraco.Web/PublishedCache/IPublishedMediaCache.cs +++ b/src/Umbraco.Web/PublishedCache/IPublishedMediaCache.cs @@ -1,5 +1,10 @@ namespace Umbraco.Web.PublishedCache { + public interface IPublishedMediaCache2 : IPublishedMediaCache, IPublishedCache2 + { + // NOTE: this is here purely to avoid API breaking changes + } + public interface IPublishedMediaCache : IPublishedCache { } } diff --git a/src/Umbraco.Web/PublishedCache/NuCache/ContentCache.cs b/src/Umbraco.Web/PublishedCache/NuCache/ContentCache.cs index 24c6a7018b..8e6e517aea 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/ContentCache.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/ContentCache.cs @@ -13,7 +13,7 @@ using Umbraco.Web.PublishedCache.NuCache.Navigable; namespace Umbraco.Web.PublishedCache.NuCache { - internal class ContentCache : PublishedCacheBase, IPublishedContentCache, INavigableData, IDisposable + internal class ContentCache : PublishedCacheBase, IPublishedContentCache2, INavigableData, IDisposable { private readonly ContentStore.Snapshot _snapshot; private readonly IAppCache _snapshotCache; @@ -384,15 +384,11 @@ namespace Umbraco.Web.PublishedCache.NuCache #region Content types - public override IPublishedContentType GetContentType(int id) - { - return _snapshot.GetContentType(id); - } + public override IPublishedContentType GetContentType(int id) => _snapshot.GetContentType(id); - public override IPublishedContentType GetContentType(string alias) - { - return _snapshot.GetContentType(alias); - } + public override IPublishedContentType GetContentType(string alias) => _snapshot.GetContentType(alias); + + public override IPublishedContentType GetContentType(Guid key) => _snapshot.GetContentType(key); #endregion diff --git a/src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs b/src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs index a3f918c92c..b39b38ca32 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs @@ -37,9 +37,14 @@ namespace Umbraco.Web.PublishedCache.NuCache private readonly IVariationContextAccessor _variationContextAccessor; private readonly ConcurrentDictionary> _contentNodes; private LinkedNode _root; - private readonly ConcurrentDictionary> _contentTypesById; + + // We must keep separate dictionaries for by id and by alias because we track these in snapshot/layers + // and it is possible that the alias of a content type can be different for the same id in another layer + // whereas the GUID -> INT cross reference can never be different + private readonly ConcurrentDictionary> _contentTypesById; private readonly ConcurrentDictionary> _contentTypesByAlias; - private readonly ConcurrentDictionary _xmap; + private readonly ConcurrentDictionary _contentTypeKeyToIdMap; + private readonly ConcurrentDictionary _contentKeyToIdMap; private readonly ILogger _logger; private BPlusTree _localDb; @@ -73,7 +78,8 @@ namespace Umbraco.Web.PublishedCache.NuCache _root = new LinkedNode(new ContentNode(), 0); _contentTypesById = new ConcurrentDictionary>(); _contentTypesByAlias = new ConcurrentDictionary>(StringComparer.InvariantCultureIgnoreCase); - _xmap = new ConcurrentDictionary(); + _contentTypeKeyToIdMap = new ConcurrentDictionary(); + _contentKeyToIdMap = new ConcurrentDictionary(); _genObjs = new ConcurrentQueue(); _genObj = null; // no initial gen exists @@ -136,7 +142,7 @@ namespace Umbraco.Web.PublishedCache.NuCache Monitor.Enter(_wlocko, ref lockInfo.Taken); - lock(_rlocko) + lock (_rlocko) { // see SnapDictionary try { } @@ -152,7 +158,7 @@ namespace Umbraco.Web.PublishedCache.NuCache _nextGen = true; } } - } + } } private void Release(WriteLockInfo lockInfo, bool commit = true) @@ -291,8 +297,7 @@ namespace Umbraco.Web.PublishedCache.NuCache foreach (var type in types) { - SetValueLocked(_contentTypesById, type.Id, type); - SetValueLocked(_contentTypesByAlias, type.Alias, type); + SetContentTypeLocked(type); } } @@ -318,8 +323,7 @@ namespace Umbraco.Web.PublishedCache.NuCache foreach (var type in index.Values) { - SetValueLocked(_contentTypesById, type.Id, type); - SetValueLocked(_contentTypesByAlias, type.Alias, type); + SetContentTypeLocked(type); } foreach (var link in _contentNodes.Values) @@ -354,8 +358,7 @@ namespace Umbraco.Web.PublishedCache.NuCache // set all new content types foreach (var type in types) { - SetValueLocked(_contentTypesById, type.Id, type); - SetValueLocked(_contentTypesByAlias, type.Alias, type); + SetContentTypeLocked(type); } // beware! at that point the cache is inconsistent, @@ -419,8 +422,7 @@ namespace Umbraco.Web.PublishedCache.NuCache // perform update of refreshed content types foreach (var type in refreshedTypesA) { - SetValueLocked(_contentTypesById, type.Id, type); - SetValueLocked(_contentTypesByAlias, type.Alias, type); + SetContentTypeLocked(type); } // perform update of content with refreshed content type - from the kits @@ -638,7 +640,7 @@ namespace Umbraco.Web.PublishedCache.NuCache kit.Node.PreviousSiblingContentId = existing.PreviousSiblingContentId; } - _xmap[kit.Node.Uid] = kit.Node.Id; + _contentKeyToIdMap[kit.Node.Uid] = kit.Node.Id; return true; } @@ -734,7 +736,7 @@ namespace Umbraco.Web.PublishedCache.NuCache // this node becomes the previous node previousNode = thisNode; - _xmap[kit.Node.Uid] = kit.Node.Id; + _contentKeyToIdMap[kit.Node.Uid] = kit.Node.Id; } return ok; @@ -757,7 +759,7 @@ namespace Umbraco.Web.PublishedCache.NuCache EnsureLocked(); var ok = true; - + ClearLocked(_contentNodes); ClearRootLocked(); @@ -778,7 +780,7 @@ namespace Umbraco.Web.PublishedCache.NuCache if (_localDb != null) RegisterChange(kit.Node.Id, kit); AddTreeNodeLocked(kit.Node, parent); - _xmap[kit.Node.Uid] = kit.Node.Id; + _contentKeyToIdMap[kit.Node.Uid] = kit.Node.Id; } return ok; @@ -807,7 +809,7 @@ namespace Umbraco.Web.PublishedCache.NuCache EnsureLocked(); var ok = true; - + // get existing _contentNodes.TryGetValue(rootContentId, out var link); var existing = link?.Value; @@ -833,7 +835,7 @@ namespace Umbraco.Web.PublishedCache.NuCache if (_localDb != null) RegisterChange(kit.Node.Id, kit); AddTreeNodeLocked(kit.Node, parent); - _xmap[kit.Node.Uid] = kit.Node.Id; + _contentKeyToIdMap[kit.Node.Uid] = kit.Node.Id; } return ok; @@ -885,11 +887,11 @@ namespace Umbraco.Web.PublishedCache.NuCache // This should never be null, all code that calls this method is null checking but we've seen // issues of null ref exceptions in issue reports so we'll double check here if (content == null) throw new ArgumentNullException(nameof(content)); - + SetValueLocked(_contentNodes, content.Id, null); if (_localDb != null) RegisterChange(content.Id, ContentNodeKit.Null); - _xmap.TryRemove(content.Uid, out _); + _contentKeyToIdMap.TryRemove(content.Uid, out _); var id = content.FirstChildContentId; while (id > 0) @@ -913,10 +915,10 @@ namespace Umbraco.Web.PublishedCache.NuCache { if (_contentNodes.TryGetValue(id, out var link)) { - link = GetLinkedNodeGen(link, gen); + link = GetLinkedNodeGen(link, gen); if (link != null && link.Value != null) return link; - } + } throw new PanicException($"failed to get {description} with id={id}"); } @@ -929,13 +931,13 @@ namespace Umbraco.Web.PublishedCache.NuCache { if (content.ParentContentId < 0) { - var root = GetLinkedNodeGen(_root, gen); + var root = GetLinkedNodeGen(_root, gen); return root; } if (_contentNodes.TryGetValue(content.ParentContentId, out var link)) link = GetLinkedNodeGen(link, gen); - + return link; } @@ -1154,6 +1156,15 @@ namespace Umbraco.Web.PublishedCache.NuCache } } + private void SetContentTypeLocked(IPublishedContentType type) + { + SetValueLocked(_contentTypesById, type.Id, type); + SetValueLocked(_contentTypesByAlias, type.Alias, type); + // ensure the key/id map is accurate + if (type.TryGetKey(out var key)) + _contentTypeKeyToIdMap[key] = type.Id; + } + // set a node (just the node, not the tree) private void SetValueLocked(ConcurrentDictionary> dict, TKey key, TValue value) where TValue : class @@ -1211,14 +1222,14 @@ namespace Umbraco.Web.PublishedCache.NuCache public ContentNode Get(Guid uid, long gen) { - return _xmap.TryGetValue(uid, out var id) + return _contentKeyToIdMap.TryGetValue(uid, out var id) ? GetValue(_contentNodes, id, gen) : null; } public IEnumerable GetAtRoot(long gen) { - var root = GetLinkedNodeGen(_root, gen); + var root = GetLinkedNodeGen(_root, gen); if (root == null) yield break; @@ -1274,13 +1285,20 @@ namespace Umbraco.Web.PublishedCache.NuCache return GetValue(_contentTypesByAlias, alias, gen); } + public IPublishedContentType GetContentType(Guid key, long gen) + { + if (!_contentTypeKeyToIdMap.TryGetValue(key, out var id)) + return null; + return GetContentType(id, gen); + } + #endregion #region Snapshots public Snapshot CreateSnapshot() { - lock(_rlocko) + lock (_rlocko) { // if no next generation is required, and we already have one, // use it and create a new snapshot @@ -1606,6 +1624,13 @@ namespace Umbraco.Web.PublishedCache.NuCache return _store.GetContentType(alias, _gen); } + public IPublishedContentType GetContentType(Guid key) + { + if (_gen < 0) + throw new ObjectDisposedException("snapshot" /*+ " (" + _thisCount + ")"*/); + return _store.GetContentType(key, _gen); + } + // this code is here just so you don't try to implement it // the only way we can iterate over "all" without locking the entire cache forever // is by shallow cloning the cache, which is quite expensive, so we should probably not do it, diff --git a/src/Umbraco.Web/PublishedCache/NuCache/MediaCache.cs b/src/Umbraco.Web/PublishedCache/NuCache/MediaCache.cs index 182086ed7f..a466460ede 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/MediaCache.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/MediaCache.cs @@ -11,7 +11,7 @@ using Umbraco.Web.PublishedCache.NuCache.Navigable; namespace Umbraco.Web.PublishedCache.NuCache { - internal class MediaCache : PublishedCacheBase, IPublishedMediaCache, INavigableData, IDisposable + internal class MediaCache : PublishedCacheBase, IPublishedMediaCache2, INavigableData, IDisposable { private readonly ContentStore.Snapshot _snapshot; private readonly IVariationContextAccessor _variationContextAccessor; @@ -155,15 +155,11 @@ namespace Umbraco.Web.PublishedCache.NuCache #region Content types - public override IPublishedContentType GetContentType(int id) - { - return _snapshot.GetContentType(id); - } + public override IPublishedContentType GetContentType(int id) => _snapshot.GetContentType(id); - public override IPublishedContentType GetContentType(string alias) - { - return _snapshot.GetContentType(alias); - } + public override IPublishedContentType GetContentType(string alias) => _snapshot.GetContentType(alias); + + public override IPublishedContentType GetContentType(Guid key) => _snapshot.GetContentType(key); #endregion diff --git a/src/Umbraco.Web/PublishedCache/PublishedCacheBase.cs b/src/Umbraco.Web/PublishedCache/PublishedCacheBase.cs index 1f637663e5..1b4a9bb92a 100644 --- a/src/Umbraco.Web/PublishedCache/PublishedCacheBase.cs +++ b/src/Umbraco.Web/PublishedCache/PublishedCacheBase.cs @@ -8,7 +8,7 @@ using Umbraco.Core.Xml; namespace Umbraco.Web.PublishedCache { - abstract class PublishedCacheBase : IPublishedCache + internal abstract class PublishedCacheBase : IPublishedCache2 { public bool PreviewDefault { get; } @@ -89,8 +89,8 @@ namespace Umbraco.Web.PublishedCache } public abstract IPublishedContentType GetContentType(int id); - public abstract IPublishedContentType GetContentType(string alias); + public abstract IPublishedContentType GetContentType(Guid key); public virtual IEnumerable GetByContentType(IPublishedContentType contentType) { diff --git a/src/Umbraco.Web/PublishedCache/PublishedContentTypeCache.cs b/src/Umbraco.Web/PublishedCache/PublishedContentTypeCache.cs index e453471bb8..8eb50b0588 100644 --- a/src/Umbraco.Web/PublishedCache/PublishedContentTypeCache.cs +++ b/src/Umbraco.Web/PublishedCache/PublishedContentTypeCache.cs @@ -15,8 +15,10 @@ namespace Umbraco.Web.PublishedCache /// This cache is not snapshotted, so it refreshes any time things change. public class PublishedContentTypeCache { + // NOTE: These are not concurrent dictionaries because all access is done within a lock private readonly Dictionary _typesByAlias = new Dictionary(); private readonly Dictionary _typesById = new Dictionary(); + private readonly Dictionary _keyToIdMap = new Dictionary(); private readonly IContentTypeService _contentTypeService; private readonly IMediaTypeService _mediaTypeService; private readonly IMemberTypeService _memberTypeService; @@ -130,6 +132,42 @@ namespace Umbraco.Web.PublishedCache } } + /// + /// Gets a published content type. + /// + /// An item type. + /// An key. + /// The published content type corresponding to the item key. + public IPublishedContentType Get(PublishedItemType itemType, Guid key) + { + try + { + _lock.EnterUpgradeableReadLock(); + + if (_keyToIdMap.TryGetValue(key, out var id)) + return Get(itemType, id); + + var type = CreatePublishedContentType(itemType, key); + + try + { + _lock.EnterWriteLock(); + _keyToIdMap[key] = type.Id; + return _typesByAlias[GetAliasKey(type)] = _typesById[type.Id] = type; + } + finally + { + if (_lock.IsWriteLockHeld) + _lock.ExitWriteLock(); + } + } + finally + { + if (_lock.IsUpgradeableReadLockHeld) + _lock.ExitUpgradeableReadLock(); + } + } + /// /// Gets a published content type. /// @@ -152,7 +190,8 @@ namespace Umbraco.Web.PublishedCache try { _lock.EnterWriteLock(); - + if (type.TryGetKey(out var key)) + _keyToIdMap[key] = type.Id; return _typesByAlias[aliasKey] = _typesById[type.Id] = type; } finally @@ -188,7 +227,8 @@ namespace Umbraco.Web.PublishedCache try { _lock.EnterWriteLock(); - + if (type.TryGetKey(out var key)) + _keyToIdMap[key] = type.Id; return _typesByAlias[GetAliasKey(type)] = _typesById[type.Id] = type; } finally @@ -204,27 +244,32 @@ namespace Umbraco.Web.PublishedCache } } + private IPublishedContentType CreatePublishedContentType(PublishedItemType itemType, Guid key) + { + IContentTypeComposition contentType = itemType switch + { + PublishedItemType.Content => _contentTypeService.Get(key), + PublishedItemType.Media => _mediaTypeService.Get(key), + PublishedItemType.Member => _memberTypeService.Get(key), + _ => throw new ArgumentOutOfRangeException(nameof(itemType)), + }; + if (contentType == null) + throw new Exception($"ContentTypeService failed to find a {itemType.ToString().ToLower()} type with key \"{key}\"."); + + return _publishedContentTypeFactory.CreateContentType(contentType); + } + private IPublishedContentType CreatePublishedContentType(PublishedItemType itemType, string alias) { if (GetPublishedContentTypeByAlias != null) return GetPublishedContentTypeByAlias(alias); - - IContentTypeComposition contentType; - switch (itemType) + IContentTypeComposition contentType = itemType switch { - case PublishedItemType.Content: - contentType = _contentTypeService.Get(alias); - break; - case PublishedItemType.Media: - contentType = _mediaTypeService.Get(alias); - break; - case PublishedItemType.Member: - contentType = _memberTypeService.Get(alias); - break; - default: - throw new ArgumentOutOfRangeException(nameof(itemType)); - } - + PublishedItemType.Content => _contentTypeService.Get(alias), + PublishedItemType.Media => _mediaTypeService.Get(alias), + PublishedItemType.Member => _memberTypeService.Get(alias), + _ => throw new ArgumentOutOfRangeException(nameof(itemType)), + }; if (contentType == null) throw new Exception($"ContentTypeService failed to find a {itemType.ToString().ToLower()} type with alias \"{alias}\"."); @@ -235,23 +280,13 @@ namespace Umbraco.Web.PublishedCache { if (GetPublishedContentTypeById != null) return GetPublishedContentTypeById(id); - - IContentTypeComposition contentType; - switch (itemType) + IContentTypeComposition contentType = itemType switch { - case PublishedItemType.Content: - contentType = _contentTypeService.Get(id); - break; - case PublishedItemType.Media: - contentType = _mediaTypeService.Get(id); - break; - case PublishedItemType.Member: - contentType = _memberTypeService.Get(id); - break; - default: - throw new ArgumentOutOfRangeException(nameof(itemType)); - } - + PublishedItemType.Content => _contentTypeService.Get(id), + PublishedItemType.Media => _mediaTypeService.Get(id), + PublishedItemType.Member => _memberTypeService.Get(id), + _ => throw new ArgumentOutOfRangeException(nameof(itemType)), + }; if (contentType == null) throw new Exception($"ContentTypeService failed to find a {itemType.ToString().ToLower()} type with id {id}."); @@ -259,6 +294,7 @@ namespace Umbraco.Web.PublishedCache } // for unit tests - changing the callback must reset the cache obviously + // TODO: Why does this even exist? For testing you'd pass in a mocked service to get by id private Func _getPublishedContentTypeByAlias; internal Func GetPublishedContentTypeByAlias { @@ -282,6 +318,7 @@ namespace Umbraco.Web.PublishedCache } // for unit tests - changing the callback must reset the cache obviously + // TODO: Why does this even exist? For testing you'd pass in a mocked service to get by id private Func _getPublishedContentTypeById; internal Func GetPublishedContentTypeById { diff --git a/src/umbraco.sln b/src/umbraco.sln index 63fb856b5d..78ff0ef12d 100644 --- a/src/umbraco.sln +++ b/src/umbraco.sln @@ -58,8 +58,24 @@ Project("{E24C65DC-7377-472B-9ABA-BC803B73C61A}") = "Umbraco.Web.UI.Client", "ht StartServerOnDebug = "false" EndProjectSection EndProject -Project("{E24C65DC-7377-472B-9ABA-BC803B73C61A}") = "Umbraco.Tests.AcceptanceTest\", "Umbraco.Tests.AcceptanceTest\", "{9E4C8A12-FBE0-4673-8CE2-DF99D5D57817}" +Project("{E24C65DC-7377-472B-9ABA-BC803B73C61A}") = "Umbraco.Tests.AcceptanceTest", "Umbraco.Tests.AcceptanceTest\", "{9E4C8A12-FBE0-4673-8CE2-DF99D5D57817}" ProjectSection(WebsiteProperties) = preProject + TargetFrameworkMoniker = ".NETFramework,Version%3Dv4.0" + Debug.AspNetCompiler.VirtualPath = "/localhost_62926" + Debug.AspNetCompiler.PhysicalPath = "Umbraco.Tests.AcceptanceTest\" + Debug.AspNetCompiler.TargetPath = "PrecompiledWeb\localhost_62926\" + Debug.AspNetCompiler.Updateable = "true" + Debug.AspNetCompiler.ForceOverwrite = "true" + Debug.AspNetCompiler.FixedNames = "false" + Debug.AspNetCompiler.Debug = "True" + Release.AspNetCompiler.VirtualPath = "/localhost_62926" + Release.AspNetCompiler.PhysicalPath = "Umbraco.Tests.AcceptanceTest\" + Release.AspNetCompiler.TargetPath = "PrecompiledWeb\localhost_62926\" + Release.AspNetCompiler.Updateable = "true" + Release.AspNetCompiler.ForceOverwrite = "true" + Release.AspNetCompiler.FixedNames = "false" + Release.AspNetCompiler.Debug = "False" + VWDPort = "62926" SlnRelativePath = "Umbraco.Tests.AcceptanceTest\" EndProjectSection EndProject @@ -123,6 +139,9 @@ Global {4C4C194C-B5E4-4991-8F87-4373E24CC19F}.Release|Any CPU.Build.0 = Release|Any CPU {3819A550-DCEC-4153-91B4-8BA9F7F0B9B4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {3819A550-DCEC-4153-91B4-8BA9F7F0B9B4}.Release|Any CPU.ActiveCfg = Debug|Any CPU + {9E4C8A12-FBE0-4673-8CE2-DF99D5D57817}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9E4C8A12-FBE0-4673-8CE2-DF99D5D57817}.Release|Any CPU.ActiveCfg = Debug|Any CPU + {9E4C8A12-FBE0-4673-8CE2-DF99D5D57817}.Release|Any CPU.Build.0 = Debug|Any CPU {651E1350-91B6-44B7-BD60-7207006D7003}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {651E1350-91B6-44B7-BD60-7207006D7003}.Debug|Any CPU.Build.0 = Debug|Any CPU {651E1350-91B6-44B7-BD60-7207006D7003}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -157,6 +176,7 @@ Global EndGlobalSection GlobalSection(NestedProjects) = preSolution {227C3B55-80E5-4E7E-A802-BE16C5128B9D} = {2849E9D4-3B4E-40A3-A309-F3CB4F0E125F} + {9E4C8A12-FBE0-4673-8CE2-DF99D5D57817} = {B5BD12C1-A454-435E-8A46-FF4A364C0382} {5D3B8245-ADA6-453F-A008-50ED04BFE770} = {B5BD12C1-A454-435E-8A46-FF4A364C0382} {E3F9F378-AFE1-40A5-90BD-82833375DBFE} = {227C3B55-80E5-4E7E-A802-BE16C5128B9D} {5B03EF4E-E0AC-4905-861B-8C3EC1A0D458} = {227C3B55-80E5-4E7E-A802-BE16C5128B9D} @@ -164,7 +184,6 @@ Global {3A33ADC9-C6C0-4DB1-A613-A9AF0210DF3D} = {B5BD12C1-A454-435E-8A46-FF4A364C0382} {C7311C00-2184-409B-B506-52A5FAEA8736} = {FD962632-184C-4005-A5F3-E705D92FC645} {FB5676ED-7A69-492C-B802-E7B24144C0FC} = {B5BD12C1-A454-435E-8A46-FF4A364C0382} - {9E4C8A12-FBE0-4673-8CE2-DF99D5D57817} = {B5BD12C1-A454-435E-8A46-FF4A364C0382} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {7A0F2E34-D2AF-4DAB-86A0-7D7764B3D0EC} From 1522260111d473792eac7ae57eb4bf697429ba55 Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 10 Jun 2020 18:09:54 +1000 Subject: [PATCH 170/377] Reverts the nested content changes, fixes up the GetEmptyByKey --- .../V_8_7_0/StackedContentToBlockList.cs | 26 +++++++------- .../Published/NestedContentTests.cs | 20 +++++------ .../src/common/resources/content.resource.js | 14 ++++---- src/Umbraco.Web/Editors/ContentController.cs | 25 +++++--------- .../NestedContentManyValueConverter.cs | 10 +++--- .../NestedContentSingleValueConverter.cs | 8 ++--- .../NestedContentValueConverterBase.cs | 34 ++++++++++++++++--- 7 files changed, 76 insertions(+), 61 deletions(-) diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_7_0/StackedContentToBlockList.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_7_0/StackedContentToBlockList.cs index 5e1396a392..0e8533a549 100644 --- a/src/Umbraco.Core/Migrations/Upgrade/V_8_7_0/StackedContentToBlockList.cs +++ b/src/Umbraco.Core/Migrations/Upgrade/V_8_7_0/StackedContentToBlockList.cs @@ -73,11 +73,7 @@ namespace Umbraco.Core.Migrations.Upgrade.V_8_7_0 var propLk = props.ToLookup(p => p.ContentTypeId, p => p.Alias); var knownMap = new Dictionary(types.Count); - types.ForEach(t => knownMap[t.NodeDto.UniqueId] = new KnownContentType - { - Alias = t.Alias, - StringToRawProperties = propLk[t.NodeId].Union(joinLk[t.NodeId].SelectMany(r => propLk[r])).ToArray() - }); + types.ForEach(t => knownMap[t.NodeDto.UniqueId] = new KnownContentType(t.Alias, t.NodeDto.UniqueId, propLk[t.NodeId].Union(joinLk[t.NodeId].SelectMany(r => propLk[r])).ToArray())); return knownMap; } @@ -249,15 +245,14 @@ namespace Umbraco.Core.Migrations.Upgrade.V_8_7_0 { if (!Guid.TryParse(obj["key"].ToString(), out var key)) key = Guid.NewGuid(); if (!Guid.TryParse(obj["icContentTypeGuid"].ToString(), out var ctGuid)) ctGuid = Guid.Empty; - if (!knownDocumentTypes.TryGetValue(ctGuid, out var ct)) ct = new KnownContentType { Key = ctGuid }; + if (!knownDocumentTypes.TryGetValue(ctGuid, out var ct)) ct = new KnownContentType(null, ctGuid, null); obj.Remove("key"); obj.Remove("icContentTypeGuid"); var udi = new GuidUdi(Constants.UdiEntityType.Element, key).ToString(); - obj["udi"] = udi; - // TODO: retrive the key for the content type. - //obj["contentTypeAlias"] = ct.Alias; + obj["udi"] = udi; + obj["contentTypeKey"] = ct.Key; if (ct.StringToRawProperties != null && ct.StringToRawProperties.Length > 0) { @@ -291,9 +286,16 @@ namespace Umbraco.Core.Migrations.Upgrade.V_8_7_0 private class KnownContentType { - public string Alias { get; set; } - public Guid Key { get; set; } - public string[] StringToRawProperties { get; set; } + public KnownContentType(string alias, Guid key, string[] stringToRawProperties) + { + Alias = alias ?? throw new ArgumentNullException(nameof(alias)); + Key = key; + StringToRawProperties = stringToRawProperties ?? throw new ArgumentNullException(nameof(stringToRawProperties)); + } + + public string Alias { get; } + public Guid Key { get; } + public string[] StringToRawProperties { get; } } } } diff --git a/src/Umbraco.Tests/Published/NestedContentTests.cs b/src/Umbraco.Tests/Published/NestedContentTests.cs index 70da652a9a..7f499d479b 100644 --- a/src/Umbraco.Tests/Published/NestedContentTests.cs +++ b/src/Umbraco.Tests/Published/NestedContentTests.cs @@ -100,8 +100,8 @@ namespace Umbraco.Tests.Published .Returns((string alias) => { return alias == "contentN1" - ? (IList) new List() - : (IList) new List(); + ? (IList)new List() + : (IList)new List(); }); var contentCache = new Mock(); @@ -119,12 +119,10 @@ namespace Umbraco.Tests.Published .Setup(x => x.PublishedSnapshot) .Returns(publishedSnapshot.Object); - var blockEditorConverter = new BlockEditorConverter(publishedSnapshotAccessor.Object, publishedModelFactory.Object); - var converters = new PropertyValueConverterCollection(new IPropertyValueConverter[] { - new NestedContentSingleValueConverter(blockEditorConverter, publishedModelFactory.Object, proflog), - new NestedContentManyValueConverter(blockEditorConverter, publishedModelFactory.Object, proflog), + new NestedContentSingleValueConverter(publishedSnapshotAccessor.Object, publishedModelFactory.Object, proflog), + new NestedContentManyValueConverter(publishedSnapshotAccessor.Object, publishedModelFactory.Object, proflog), }); var factory = new PublishedContentTypeFactory(publishedModelFactory.Object, converters, dataTypeService); @@ -166,7 +164,7 @@ namespace Umbraco.Tests.Published (var contentType1, _) = CreateContentTypes(); // nested single converter returns the proper value clr type TestModel, and cache level - Assert.AreEqual(typeof (TestElementModel), contentType1.GetPropertyType("property1").ClrType); + Assert.AreEqual(typeof(TestElementModel), contentType1.GetPropertyType("property1").ClrType); Assert.AreEqual(PropertyCacheLevel.Element, contentType1.GetPropertyType("property1").CacheLevel); var key = Guid.NewGuid(); @@ -174,7 +172,7 @@ namespace Umbraco.Tests.Published var content = new SolidPublishedContent(contentType1) { Key = key, - Properties = new [] + Properties = new[] { new TestPublishedProperty(contentType1.GetPropertyType("property1"), $@"[ {{ ""key"": ""{keyA}"", ""propertyN1"": ""foo"", ""ncContentTypeAlias"": ""contentN1"" }} @@ -185,7 +183,7 @@ namespace Umbraco.Tests.Published // nested single converter returns proper TestModel value Assert.IsInstanceOf(value); - var valueM = (TestElementModel) value; + var valueM = (TestElementModel)value; Assert.AreEqual("foo", valueM.PropValue); Assert.AreEqual(keyA, valueM.Key); } @@ -196,7 +194,7 @@ namespace Umbraco.Tests.Published (_, var contentType2) = CreateContentTypes(); // nested many converter returns the proper value clr type IEnumerable, and cache level - Assert.AreEqual(typeof (IEnumerable), contentType2.GetPropertyType("property2").ClrType); + Assert.AreEqual(typeof(IEnumerable), contentType2.GetPropertyType("property2").ClrType); Assert.AreEqual(PropertyCacheLevel.Element, contentType2.GetPropertyType("property2").CacheLevel); var key = Guid.NewGuid(); @@ -218,7 +216,7 @@ namespace Umbraco.Tests.Published // nested many converter returns proper IEnumerable value Assert.IsInstanceOf>(value); Assert.IsInstanceOf>(value); - var valueM = ((IEnumerable) value).ToArray(); + var valueM = ((IEnumerable)value).ToArray(); Assert.AreEqual("foo", valueM[0].PropValue); Assert.AreEqual(keyA, valueM[0].Key); Assert.AreEqual("bar", valueM[1].PropValue); diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/content.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/content.resource.js index da21a1e0a2..169da14d46 100644 --- a/src/Umbraco.Web.UI.Client/src/common/resources/content.resource.js +++ b/src/Umbraco.Web.UI.Client/src/common/resources/content.resource.js @@ -388,7 +388,7 @@ function contentResource($q, $http, umbDataFormatter, umbRequestHelper) { umbRequestHelper.getApiUrl( "contentApiBaseUrl", "GetBlueprintById", - [{ id: id }])), + { id: id })), 'Failed to retrieve data for content id ' + id) .then(function (result) { return $q.when(umbDataFormatter.formatContentGetData(result)); @@ -401,7 +401,7 @@ function contentResource($q, $http, umbDataFormatter, umbRequestHelper) { umbRequestHelper.getApiUrl( "contentApiBaseUrl", "GetNotificationOptions", - [{ contentId: id }])), + { contentId: id })), 'Failed to retrieve data for content id ' + id); }, @@ -502,7 +502,7 @@ function contentResource($q, $http, umbDataFormatter, umbRequestHelper) { umbRequestHelper.getApiUrl( "contentApiBaseUrl", "GetEmpty", - [{ contentTypeAlias: alias }, { parentId: parentId }])), + { contentTypeAlias: alias, parentId: parentId })), 'Failed to retrieve data for empty content item type ' + alias) .then(function (result) { return $q.when(umbDataFormatter.formatContentGetData(result)); @@ -510,7 +510,7 @@ function contentResource($q, $http, umbDataFormatter, umbRequestHelper) { }, /** * @ngdoc method - * @name umbraco.resources.contentResource#getScaffoldByID + * @name umbraco.resources.contentResource#getScaffoldByKey * @methodOf umbraco.resources.contentResource * * @description @@ -523,7 +523,7 @@ function contentResource($q, $http, umbDataFormatter, umbRequestHelper) { * * ##usage *
      -         * contentResource.getScaffoldById(1234, '...')
      +         * contentResource.getScaffoldByKey(1234, '...')
                *    .then(function(scaffold) {
                *        var myDoc = scaffold;
                 *        myDoc.name = "My new document";
      @@ -547,7 +547,7 @@ function contentResource($q, $http, umbDataFormatter, umbRequestHelper) {
                           umbRequestHelper.getApiUrl(
                               "contentApiBaseUrl",
                               "GetEmptyByKey",
      -                        [{ contentTypeKey: contentTypeKey }, { parentId: parentId }])),
      +                        { contentTypeKey: contentTypeKey, parentId: parentId })),
                       'Failed to retrieve data for empty content item id ' + contentTypeId)
                       .then(function (result) {
                           return $q.when(umbDataFormatter.formatContentGetData(result));
      @@ -561,7 +561,7 @@ function contentResource($q, $http, umbDataFormatter, umbRequestHelper) {
                           umbRequestHelper.getApiUrl(
                               "contentApiBaseUrl",
                               "GetEmpty",
      -                        [{ blueprintId: blueprintId }, { parentId: parentId }])),
      +                        { blueprintId: blueprintId, parentId: parentId })),
                       'Failed to retrieve blueprint for id ' + blueprintId)
                       .then(function (result) {
                           return $q.when(umbDataFormatter.formatContentGetData(result));
      diff --git a/src/Umbraco.Web/Editors/ContentController.cs b/src/Umbraco.Web/Editors/ContentController.cs
      index 42e7ff9f5a..b004d49ab0 100644
      --- a/src/Umbraco.Web/Editors/ContentController.cs
      +++ b/src/Umbraco.Web/Editors/ContentController.cs
      @@ -355,18 +355,7 @@ namespace Umbraco.Web.Editors
                       throw new HttpResponseException(HttpStatusCode.NotFound);
                   }
       
      -            var emptyContent = Services.ContentService.Create("", parentId, contentType.Alias, Security.GetUserId().ResultOr(0));
      -            var mapped = MapToDisplay(emptyContent);
      -            // translate the content type name if applicable
      -            mapped.ContentTypeName = Services.TextService.UmbracoDictionaryTranslate(mapped.ContentTypeName);
      -            // if your user type doesn't have access to the Settings section it would not get this property mapped
      -            if (mapped.DocumentType != null)
      -                mapped.DocumentType.Name = Services.TextService.UmbracoDictionaryTranslate(mapped.DocumentType.Name);
      -
      -            //remove the listview app if it exists
      -            mapped.ContentApps = mapped.ContentApps.Where(x => x.Alias != "umbListView").ToList();
      -
      -            return mapped;
      +            return GetEmpty(contentType, parentId);
               }
       
       
      @@ -376,17 +365,19 @@ namespace Umbraco.Web.Editors
               /// 
               /// 
               [OutgoingEditorModelEvent]
      -        public ContentItemDisplay GetEmptyByKey(string contentTypeKey, int parentId)
      +        public ContentItemDisplay GetEmptyByKey(Guid contentTypeKey, int parentId)
               {
      -
      -            Guid.TryParse(contentTypeKey, out Guid contentTypeGuid);
      -
      -            var contentType = Services.ContentTypeService.Get(contentTypeGuid);
      +            var contentType = Services.ContentTypeService.Get(contentTypeKey);
                   if (contentType == null)
                   {
                       throw new HttpResponseException(HttpStatusCode.NotFound);
                   }
       
      +            return GetEmpty(contentType, parentId);
      +        }
      +
      +        private ContentItemDisplay GetEmpty(IContentType contentType, int parentId)
      +        {
                   var emptyContent = Services.ContentService.Create("", parentId, contentType.Alias, Security.GetUserId().ResultOr(0));
                   var mapped = MapToDisplay(emptyContent);
                   // translate the content type name if applicable
      diff --git a/src/Umbraco.Web/PropertyEditors/ValueConverters/NestedContentManyValueConverter.cs b/src/Umbraco.Web/PropertyEditors/ValueConverters/NestedContentManyValueConverter.cs
      index b961048851..11b924552e 100644
      --- a/src/Umbraco.Web/PropertyEditors/ValueConverters/NestedContentManyValueConverter.cs
      +++ b/src/Umbraco.Web/PropertyEditors/ValueConverters/NestedContentManyValueConverter.cs
      @@ -23,8 +23,8 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters
               /// 
               /// Initializes a new instance of the  class.
               /// 
      -        public NestedContentManyValueConverter(BlockEditorConverter blockEditorConverter, IPublishedModelFactory publishedModelFactory, IProfilingLogger proflog)
      -            : base(blockEditorConverter, publishedModelFactory)
      +        public NestedContentManyValueConverter(IPublishedSnapshotAccessor publishedSnapshotAccessor, IPublishedModelFactory publishedModelFactory, IProfilingLogger proflog)
      +            : base(publishedSnapshotAccessor, publishedModelFactory)
               {
                   _proflog = proflog;
               }
      @@ -38,8 +38,8 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters
               {
                   var contentTypes = propertyType.DataType.ConfigurationAs().ContentTypes;
                   return contentTypes.Length == 1
      -                ? typeof (IEnumerable<>).MakeGenericType(ModelType.For(contentTypes[0].Alias))
      -                : typeof (IEnumerable);
      +                ? typeof(IEnumerable<>).MakeGenericType(ModelType.For(contentTypes[0].Alias))
      +                : typeof(IEnumerable);
               }
       
               /// 
      @@ -71,7 +71,7 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters
       
                       foreach (var sourceObject in objects)
                       {
      -                    var element = BlockEditorConverter.ConvertToElement(sourceObject, NestedContentPropertyEditor.ContentTypeAliasPropertyKey, referenceCacheLevel, preview);
      +                    var element = ConvertToElement(sourceObject, referenceCacheLevel, preview);
                           if (element != null)
                               elements.Add(element);
                       }
      diff --git a/src/Umbraco.Web/PropertyEditors/ValueConverters/NestedContentSingleValueConverter.cs b/src/Umbraco.Web/PropertyEditors/ValueConverters/NestedContentSingleValueConverter.cs
      index b3a2a9294d..c9859c9770 100644
      --- a/src/Umbraco.Web/PropertyEditors/ValueConverters/NestedContentSingleValueConverter.cs
      +++ b/src/Umbraco.Web/PropertyEditors/ValueConverters/NestedContentSingleValueConverter.cs
      @@ -22,8 +22,8 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters
               /// 
               /// Initializes a new instance of the  class.
               /// 
      -        public NestedContentSingleValueConverter(BlockEditorConverter blockEditorConverter, IPublishedModelFactory publishedModelFactory, IProfilingLogger proflog)
      -            : base(blockEditorConverter, publishedModelFactory)
      +        public NestedContentSingleValueConverter(IPublishedSnapshotAccessor publishedSnapshotAccessor, IPublishedModelFactory publishedModelFactory, IProfilingLogger proflog)
      +            : base(publishedSnapshotAccessor, publishedModelFactory)
               {
                   _proflog = proflog;
               }
      @@ -56,7 +56,7 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters
               {
                   using (_proflog.DebugDuration($"ConvertPropertyToNestedContent ({propertyType.DataType.Id})"))
                   {
      -                var value = (string) inter;
      +                var value = (string)inter;
                       if (string.IsNullOrWhiteSpace(value)) return null;
       
                       var objects = JsonConvert.DeserializeObject>(value);
      @@ -65,7 +65,7 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters
                       if (objects.Count > 1)
                           throw new InvalidOperationException();
       
      -                return BlockEditorConverter.ConvertToElement(objects[0], NestedContentPropertyEditor.ContentTypeAliasPropertyKey, referenceCacheLevel, preview);
      +                return ConvertToElement(objects[0], referenceCacheLevel, preview);
                   }
               }
           }
      diff --git a/src/Umbraco.Web/PropertyEditors/ValueConverters/NestedContentValueConverterBase.cs b/src/Umbraco.Web/PropertyEditors/ValueConverters/NestedContentValueConverterBase.cs
      index 4295daf5fe..7c18d8ebca 100644
      --- a/src/Umbraco.Web/PropertyEditors/ValueConverters/NestedContentValueConverterBase.cs
      +++ b/src/Umbraco.Web/PropertyEditors/ValueConverters/NestedContentValueConverterBase.cs
      @@ -1,19 +1,23 @@
      -using Umbraco.Core;
      +using System;
      +using System.Collections.Generic;
      +using Newtonsoft.Json.Linq;
      +using Umbraco.Core;
       using Umbraco.Core.Models.PublishedContent;
       using Umbraco.Core.PropertyEditors;
      +using Umbraco.Web.PublishedCache;
       
       namespace Umbraco.Web.PropertyEditors.ValueConverters
       {
           public abstract class NestedContentValueConverterBase : PropertyValueConverterBase
           {
      +        private readonly IPublishedSnapshotAccessor _publishedSnapshotAccessor;
       
      -        protected NestedContentValueConverterBase(BlockEditorConverter blockEditorConverter, IPublishedModelFactory publishedModelFactory)
      +        protected NestedContentValueConverterBase(IPublishedSnapshotAccessor publishedSnapshotAccessor, IPublishedModelFactory publishedModelFactory)
               {
      -            BlockEditorConverter = blockEditorConverter;
      +            _publishedSnapshotAccessor = publishedSnapshotAccessor;
                   PublishedModelFactory = publishedModelFactory;
               }
       
      -        protected BlockEditorConverter BlockEditorConverter { get; }
               protected IPublishedModelFactory PublishedModelFactory { get; }
       
               public static bool IsNested(IPublishedPropertyType publishedProperty)
      @@ -35,6 +39,26 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters
                   return IsNested(publishedProperty) && !IsNestedSingle(publishedProperty);
               }
       
      -        
      +        protected IPublishedElement ConvertToElement(JObject sourceObject, PropertyCacheLevel referenceCacheLevel, bool preview)
      +        {
      +            var elementTypeAlias = sourceObject[NestedContentPropertyEditor.ContentTypeAliasPropertyKey]?.ToObject();
      +            if (string.IsNullOrEmpty(elementTypeAlias))
      +                return null;
      +
      +            // only convert element types - content types will cause an exception when PublishedModelFactory creates the model
      +            var publishedContentType = _publishedSnapshotAccessor.PublishedSnapshot.Content.GetContentType(elementTypeAlias);
      +            if (publishedContentType == null || publishedContentType.IsElement == false)
      +                return null;
      +
      +            var propertyValues = sourceObject.ToObject>();
      +
      +            if (!propertyValues.TryGetValue("key", out var keyo)
      +                || !Guid.TryParse(keyo.ToString(), out var key))
      +                key = Guid.Empty;
      +
      +            IPublishedElement element = new PublishedElement(publishedContentType, key, propertyValues, preview, referenceCacheLevel, _publishedSnapshotAccessor);
      +            element = PublishedModelFactory.CreateModel(element);
      +            return element;
      +        }
           }
       }
      
      From 12755e5db623ffcb6798bc6c281770c7afbb3516 Mon Sep 17 00:00:00 2001
      From: Shannon 
      Date: Thu, 11 Jun 2020 17:16:43 +1000
      Subject: [PATCH 171/377] Returns ContentTypeKey from the server, updates js to
       use this everywhere and fix all js tests.
      
      ---
       .../src/common/mocks/resources/_utils.js                  | 1 +
       .../src/common/resources/content.resource.js              | 2 +-
       .../src/common/services/blockeditor.service.js            | 6 +++---
       .../unit/common/services/block-editor-service.spec.js     | 8 ++++----
       .../Models/ContentEditing/ContentItemDisplay.cs           | 3 +++
       src/Umbraco.Web/Models/Mapping/ContentMapDefinition.cs    | 1 +
       6 files changed, 13 insertions(+), 8 deletions(-)
      
      diff --git a/src/Umbraco.Web.UI.Client/src/common/mocks/resources/_utils.js b/src/Umbraco.Web.UI.Client/src/common/mocks/resources/_utils.js
      index 1eb259747e..619e1b77c3 100644
      --- a/src/Umbraco.Web.UI.Client/src/common/mocks/resources/_utils.js
      +++ b/src/Umbraco.Web.UI.Client/src/common/mocks/resources/_utils.js
      @@ -296,6 +296,7 @@ angular.module('umbraco.mocks').
                           path: "-1,1234,2455",
                           allowedActions: ["U", "H", "A"],
                           contentTypeAlias: "testAlias", 
      +                    contentTypeKey: "7C5B74D1-E2F9-45A3-AE4B-FC7A829BF8AB", 
                           variants: [
                               {
                                   name: "",
      diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/content.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/content.resource.js
      index 169da14d46..4ca85f44e6 100644
      --- a/src/Umbraco.Web.UI.Client/src/common/resources/content.resource.js
      +++ b/src/Umbraco.Web.UI.Client/src/common/resources/content.resource.js
      @@ -548,7 +548,7 @@ function contentResource($q, $http, umbDataFormatter, umbRequestHelper) {
                               "contentApiBaseUrl",
                               "GetEmptyByKey",
                               { contentTypeKey: contentTypeKey, parentId: parentId })),
      -                'Failed to retrieve data for empty content item id ' + contentTypeId)
      +                'Failed to retrieve data for empty content item id ' + contentTypeKey)
                       .then(function (result) {
                           return $q.when(umbDataFormatter.formatContentGetData(result));
                       });
      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 4e2296170b..bc36859df5 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
      @@ -300,7 +300,7 @@
                    * @return {Array} array of strings representing alias.
                    */
                   getAvailableAliasesForBlockContent: function() {
      -                return this.blockConfigurations.map(blockConfiguration => getScaffoldFor(blockConfiguration.contentTypeKey).contentTypeAlias);
      +                return this.blockConfigurations.map(blockConfiguration => getScaffoldFor(blockConfiguration.contentTypeKey).contentTypeKey);
                   },
       
                   getAvailableBlocksForBlockPicker: function() {
      @@ -359,7 +359,7 @@
                           this.label = getBlockLabel(this);
                       }.bind(this))}.bind(blockModel), 10);
       
      -                var contentScaffold = this.getScaffoldFor(blockConfiguration.contentTypeAlias);
      +                var contentScaffold = this.getScaffoldFor(blockConfiguration.contentTypeKey);
                       if(contentScaffold === null) {
                           return null;
                       }
      @@ -384,7 +384,7 @@
                           blockModel.settings = Utilities.copy(settingsScaffold);
                           layoutEntry.settings = layoutEntry.settings || {};
                           if (!layoutEntry.settings.key) { layoutEntry.settings.key = String.CreateGuid(); }
      -                    if (!layoutEntry.settings.contentTypeAlias) { layoutEntry.settings.contentTypeKey = blockConfiguration.settingsElementTypeKey; }
      +                    if (!layoutEntry.settings.contentTypeKey) { layoutEntry.settings.contentTypeKey = blockConfiguration.settingsElementTypeKey; }
                           mapToElementModel(blockModel.settings, layoutEntry.settings);
                       } else {
                           layoutEntry.settings = null;
      diff --git a/src/Umbraco.Web.UI.Client/test/unit/common/services/block-editor-service.spec.js b/src/Umbraco.Web.UI.Client/test/unit/common/services/block-editor-service.spec.js
      index 92ebce981d..6924946cff 100644
      --- a/src/Umbraco.Web.UI.Client/test/unit/common/services/block-editor-service.spec.js
      +++ b/src/Umbraco.Web.UI.Client/test/unit/common/services/block-editor-service.spec.js
      @@ -15,7 +15,7 @@
               $scope = $rootScope.$new();
       
               contentResource = $injector.get("contentResource");
      -        spyOn(contentResource, "getScaffold").and.callFake(
      +        spyOn(contentResource, "getScaffoldByKey").and.callFake(
                   function () {
                       return Promise.resolve(mocksUtils.getMockVariantContent(1234))
                   }
      @@ -26,7 +26,7 @@
           }));
       
       
      -    var blockConfigurationMock = {contentTypeKey: "testKey", label:"Test label", settingsElementTypeKey: null, view: "testview.html"};
      +    var blockConfigurationMock = { contentTypeKey: "7C5B74D1-E2F9-45A3-AE4B-FC7A829BF8AB", label:"Test label", settingsElementTypeKey: null, view: "testview.html"};
       
           var propertyModelMock = {
               layout: {
      @@ -39,7 +39,7 @@
               data: [
                   {
                       udi: 1234,
      -                contentTypeKey: "testKey",
      +                contentTypeKey: "7C5B74D1-E2F9-45A3-AE4B-FC7A829BF8AB",
                       testproperty: "myTestValue"
                   }
               ]
      @@ -66,7 +66,7 @@
                   
                   expect(modelObject.getBlockConfiguration(blockConfigurationMock.contentTypeKey).label).toBe(blockConfigurationMock.label);
               });
      -        
      +
               it('loadScaffolding provides data for itemPicker', function (done) {
                   var modelObject = blockEditorService.createModelObject({}, "Umbraco.TestBlockEditor", [blockConfigurationMock], $scope);
                   
      diff --git a/src/Umbraco.Web/Models/ContentEditing/ContentItemDisplay.cs b/src/Umbraco.Web/Models/ContentEditing/ContentItemDisplay.cs
      index b6a90a93c3..1ef9207628 100644
      --- a/src/Umbraco.Web/Models/ContentEditing/ContentItemDisplay.cs
      +++ b/src/Umbraco.Web/Models/ContentEditing/ContentItemDisplay.cs
      @@ -111,6 +111,9 @@ namespace Umbraco.Web.Models.ContentEditing
               [DataMember(Name = "contentTypeId")]
               public int ContentTypeId { get; set; }
       
      +        [DataMember(Name = "contentTypeKey")]
      +        public Guid ContentTypeKey { get; set; }
      +
               [DataMember(Name = "contentTypeAlias", IsRequired = true)]
               [Required(AllowEmptyStrings = false)]
               public string ContentTypeAlias { get; set; }
      diff --git a/src/Umbraco.Web/Models/Mapping/ContentMapDefinition.cs b/src/Umbraco.Web/Models/Mapping/ContentMapDefinition.cs
      index 865057ba24..50ed4684d9 100644
      --- a/src/Umbraco.Web/Models/Mapping/ContentMapDefinition.cs
      +++ b/src/Umbraco.Web/Models/Mapping/ContentMapDefinition.cs
      @@ -76,6 +76,7 @@ namespace Umbraco.Web.Models.Mapping
                   target.AllowedTemplates = GetAllowedTemplates(source);
                   target.ContentApps = _commonMapper.GetContentApps(source);
                   target.ContentTypeId = source.ContentType.Id;
      +            target.ContentTypeKey = source.ContentType.Key;
                   target.ContentTypeAlias = source.ContentType.Alias;
                   target.ContentTypeName = _localizedTextService.UmbracoDictionaryTranslate(source.ContentType.Name);
                   target.DocumentType = _commonMapper.GetContentType(source, context);
      
      From ba43a4348386eca09078c3927352f053b5a21525 Mon Sep 17 00:00:00 2001
      From: Shannon 
      Date: Thu, 11 Jun 2020 18:08:36 +1000
      Subject: [PATCH 172/377] Allows key in SimpleContentType
      
      ---
       src/Umbraco.Core/Models/SimpleContentType.cs | 8 ++++++--
       1 file changed, 6 insertions(+), 2 deletions(-)
      
      diff --git a/src/Umbraco.Core/Models/SimpleContentType.cs b/src/Umbraco.Core/Models/SimpleContentType.cs
      index 5c81017ec8..45b9d0ecfc 100644
      --- a/src/Umbraco.Core/Models/SimpleContentType.cs
      +++ b/src/Umbraco.Core/Models/SimpleContentType.cs
      @@ -37,6 +37,7 @@ namespace Umbraco.Core.Models
                   if (contentType == null) throw new ArgumentNullException(nameof(contentType));
       
                   Id = contentType.Id;
      +            Key = contentType.Key;
                   Alias = contentType.Alias;
                   Variations = contentType.Variations;
                   Icon = contentType.Icon;
      @@ -51,6 +52,8 @@ namespace Umbraco.Core.Models
       
               public int Id { get; }
       
      +        public Guid Key { get; }
      +
               /// 
               public ITemplate DefaultTemplate { get;  }
       
      @@ -109,13 +112,14 @@ namespace Umbraco.Core.Models
               string ITreeEntity.Name { get => this.Name; set => throw new NotImplementedException(); }
               int IEntity.Id { get => this.Id; set => throw new NotImplementedException(); }
               bool IEntity.HasIdentity => this.Id != default;
      +        Guid IEntity.Key { get => this.Key; set => throw new NotImplementedException(); }
      +
               int ITreeEntity.CreatorId { get => throw new NotImplementedException(); set => throw new NotImplementedException(); }
               int ITreeEntity.ParentId { get => throw new NotImplementedException(); set => throw new NotImplementedException(); }
               int ITreeEntity.Level { get => throw new NotImplementedException(); set => throw new NotImplementedException(); }
               string ITreeEntity.Path { get => throw new NotImplementedException(); set => throw new NotImplementedException(); }
               int ITreeEntity.SortOrder { get => throw new NotImplementedException(); set => throw new NotImplementedException(); }
      -        bool ITreeEntity.Trashed => throw new NotImplementedException();
      -        Guid IEntity.Key { get => throw new NotImplementedException(); set => throw new NotImplementedException(); }
      +        bool ITreeEntity.Trashed => throw new NotImplementedException();        
               DateTime IEntity.CreateDate { get => throw new NotImplementedException(); set => throw new NotImplementedException(); }
               DateTime IEntity.UpdateDate { get => throw new NotImplementedException(); set => throw new NotImplementedException(); }
               DateTime? IEntity.DeleteDate { get => throw new NotImplementedException(); set => throw new NotImplementedException(); }
      
      From 03d1b7445ea5328f4f0117b1ac9ffa4d30cd9512 Mon Sep 17 00:00:00 2001
      From: Claus 
      Date: Thu, 11 Jun 2020 15:08:17 +0200
      Subject: [PATCH 173/377] Fix radio button checked appearance
      
      (cherry picked from commit ae993519ce7fffa0303cc08c86c6f0e963453dde)
      ---
       .../src/less/components/umb-form-check.less                     | 2 ++
       1 file changed, 2 insertions(+)
      
      diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-form-check.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-form-check.less
      index f24fa05ce3..ce259c876d 100644
      --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-form-check.less
      +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-form-check.less
      @@ -37,6 +37,8 @@
               }
               &:checked ~ .umb-form-check__state .umb-form-check__check {
                   border-color: @ui-option-type;
      +        }
      +        &[type='checkbox']:checked ~ .umb-form-check__state .umb-form-check__check {
                   background-color: @ui-option-type;
               }
               &:checked:hover ~ .umb-form-check__state .umb-form-check__check {
      
      From 963feadf51832e1888c156569ce7b851fef4fc92 Mon Sep 17 00:00:00 2001
      From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= 
      Date: Thu, 11 Jun 2020 20:30:34 +0200
      Subject: [PATCH 174/377] correct for the new spelling
      
      ---
       .../src/views/propertyeditors/blocklist/blocklist.component.js  | 2 +-
       1 file changed, 1 insertion(+), 1 deletion(-)
      
      diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.js
      index aa20407622..7f8a8e18f1 100644
      --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.js
      +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.js
      @@ -127,7 +127,7 @@
                       }
                   });
       
      -            vm.availableContentTypesAliases = modelObject.getAvailableAliasForBlockContent();
      +            vm.availableContentTypesAliases = modelObject.getAvailableAliasesForBlockContent();
                   vm.availableBlockTypes = modelObject.getAvailableBlocksForBlockPicker();
       
                   vm.loading = false;
      
      From 67c7c1674f7ca712635306b4db392ee5f0a0a56f Mon Sep 17 00:00:00 2001
      From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= 
      Date: Thu, 11 Jun 2020 20:31:18 +0200
      Subject: [PATCH 175/377] appended this. since the method is a non-static class
       method.
      
      ---
       .../src/common/services/blockeditor.service.js                  | 2 +-
       1 file changed, 1 insertion(+), 1 deletion(-)
      
      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 bc36859df5..cfd70d48d6 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
      @@ -300,7 +300,7 @@
                    * @return {Array} array of strings representing alias.
                    */
                   getAvailableAliasesForBlockContent: function() {
      -                return this.blockConfigurations.map(blockConfiguration => getScaffoldFor(blockConfiguration.contentTypeKey).contentTypeKey);
      +                return this.blockConfigurations.map(blockConfiguration => this.getScaffoldFor(blockConfiguration.contentTypeKey).contentTypeKey);
                   },
       
                   getAvailableBlocksForBlockPicker: function() {
      
      From d9e683ed29592bf367b031278d8f1f5653a57c53 Mon Sep 17 00:00:00 2001
      From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= 
      Date: Thu, 11 Jun 2020 20:48:15 +0200
      Subject: [PATCH 176/377] only add background-image if value isnt null
      
      ---
       .../blockeditor/blockcard/blockcard.component.html              | 2 +-
       1 file changed, 1 insertion(+), 1 deletion(-)
      
      diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blockeditor/blockcard/blockcard.component.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blockeditor/blockcard/blockcard.component.html
      index c66879e864..4ab2c18fa7 100644
      --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blockeditor/blockcard/blockcard.component.html
      +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blockeditor/blockcard/blockcard.component.html
      @@ -1,5 +1,5 @@
       
      -    
      +
      From 7aea02bcb9bf52705589effca94a0b04ee24cc32 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Fri, 12 Jun 2020 11:31:20 +0200 Subject: [PATCH 177/377] simplifyed limits validation --- .../blocklist/blocklist.component.js | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.js index 7f8a8e18f1..484f475f64 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.js @@ -489,18 +489,13 @@ // validate limits: if (vm.propertyForm) { - if (vm.validationLimit.min !== null && vm.blocks.length < vm.validationLimit.min) { - vm.propertyForm.minCount.$setValidity("minCount", false); - } - else { - vm.propertyForm.minCount.$setValidity("minCount", true); - } - if (vm.validationLimit.max !== null && vm.blocks.length > vm.validationLimit.max) { - vm.propertyForm.maxCount.$setValidity("maxCount", false); - } - else { - vm.propertyForm.maxCount.$setValidity("maxCount", true); - } + + var isMinRequirementGood = vm.validationLimit.min === null || vm.blocks.length >= vm.validationLimit.min; + vm.propertyForm.minCount.$setValidity("minCount", isMinRequirementGood); + + var isMaxRequirementGood = vm.validationLimit.max === null || vm.blocks.length <= vm.validationLimit.max; + vm.propertyForm.maxCount.$setValidity("maxCount", isMaxRequirementGood); + } } From e8ccfecf1a7a8d7d18185694e2b48d9da3114cb7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Fri, 12 Jun 2020 12:04:01 +0200 Subject: [PATCH 178/377] clean up --- .../src/common/services/blockeditor.service.js | 4 ---- 1 file changed, 4 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 cfd70d48d6..523ee92ce4 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 @@ -111,10 +111,6 @@ } } - function mapValueToPropertyModel(value, alias, dataModel) { - dataModel[alias] = value; - } - /** * Map property values from an ElementModel to another ElementModel. * Used to tricker watchers for synchronization. From c809c5ae5786694faeae53d8264e1332fe282275 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Fri, 12 Jun 2020 12:21:27 +0200 Subject: [PATCH 179/377] no need to execute a digest. --- .../src/views/propertyeditors/blocklist/blocklist.component.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.js index 484f475f64..8d581065b5 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.js @@ -131,8 +131,6 @@ vm.availableBlockTypes = modelObject.getAvailableBlocksForBlockPicker(); vm.loading = false; - - $scope.$evalAsync(); } From 80e12d9af36ae7ec194ff0776c21643343767d8e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Fri, 12 Jun 2020 12:21:50 +0200 Subject: [PATCH 180/377] define the full model for new configurations --- .../prevalue/blocklist.blockconfiguration.controller.js | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) 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 2c5b229a02..65135a13a1 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 @@ -146,9 +146,14 @@ var entry = { "contentTypeKey": key, - "view": null, + "settingsElementTypeKey": null, "labelTemplate": "", - "settingsElementTypeKey": null + "view": null, + "stylesheet": null, + "editorSize": "small", + "iconColor": null, + "backgroundColor": null, + "thumbnail": null }; $scope.model.value.push(entry); From 0b9ad918c3b07ae53144d21ee676e9aca60ddf03 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Fri, 12 Jun 2020 13:12:21 +0200 Subject: [PATCH 181/377] removed setDataFromBlockModel and general clean up and added documentation --- .../common/services/blockeditor.service.js | 105 ++++++++++++------ .../blocklist/blocklist.component.js | 11 +- 2 files changed, 76 insertions(+), 40 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 523ee92ce4..1efc09b3be 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 @@ -147,7 +147,11 @@ } } - + + /** + * Generate label for Block, uses either the labelInterpolator or falls back to the contentTypeName. + * @param {Object} blockModel BlockModel to recive data values from. + */ function getBlockLabel(blockModel) { if(blockModel.labelInterpolator !== undefined) { // We are just using the data model, since its a key/value object that is live synced. (if we need to add additional vars, we could make a shallow copy and apply those.) @@ -263,10 +267,20 @@ BlockEditorModelObject.prototype = { + /** + * Get block configuration object for a given contentTypeKey. + * @param {string} key contentTypeKey to recive the configuration model for. + * @returns {Object | null} Configuration model for the that specific block. Or ´null´ if the contentTypeKey isnt available in the current block configurations. + */ getBlockConfiguration: function(key) { return this.blockConfigurations.find(bc => bc.contentTypeKey === key); }, + /** + * Load the scaffolding models for the given configuration, these are needed to provide usefull models for each block. + * @param {Object} blockModel BlockModel to recive data values from. + * @returns {Promise} A Promise object which resolves when all scaffold models are loaded. + */ loadScaffolding: function() { var tasks = []; @@ -299,6 +313,11 @@ return this.blockConfigurations.map(blockConfiguration => this.getScaffoldFor(blockConfiguration.contentTypeKey).contentTypeKey); }, + /** + * Retrive a list of available blocks, the list containing object with the confirugation model(blockConfigModel) and the element type model(elementTypeModel). + * The purpose of this data is to provide it for the Block Picker. + * @return {Array} array of objects representing available blocks, each object containing properties blockConfigModel and elementTypeModel. + */ getAvailableBlocksForBlockPicker: function() { var blocks = []; @@ -316,14 +335,30 @@ return blocks; }, + /** + * Get scaffold model for a given contentTypeKey. + * @param {string} key contentTypeKey to recive the scaffold model for. + * @returns {Object | null} Scaffold model for the that content type. Or null if the scaffolding model dosnt exist in this context. + */ getScaffoldFor: function(contentTypeKey) { return this.scaffolds.find(o => o.contentTypeKey === contentTypeKey); }, /** * Retrieve editor friendly model of a block. - * @param {Object} layoutEntry the layout entry to build the block model from. - * @return {Object} Scaffolded Block Content object. + * BlockModel is a class instance which setups live syncronization of content and settings models back to the data of your property editor model. + * The returned object, named ´BlockModel´, contains several usefull models to make editing of this block happen. + * The ´BlockModel´ contains the following properties: + * - key {string}: runtime generated key, usefull for tracking of this object + * - content {Object}: Content model, the content type model for content merged with the content data of this block. + * - settings {Object}: Settings model, the content type model for settings merged with the settings data of this block. + * - config {Object}: A deep copy of the block configuration model. + * - label {string}: The label for this block. + * - updateLabel {Method}: Method to trigger an update of the label for this block. + * - data {Object}: A reference to the data object from your property editor model. + * - layout {Object}: A refernce to the layout entry from your property editor model. + * @param {Object} layoutEntry the layout entry object to build the block model from. + * @return {Object | null} The BlockModel for the given layout entry. Or null if data or configuration wasnt found for this block. */ getBlockModel: function(layoutEntry) { @@ -344,6 +379,11 @@ return null; } + var contentScaffold = this.getScaffoldFor(blockConfiguration.contentTypeKey); + if(contentScaffold === null) { + return null; + } + var blockModel = {}; blockModel.key = String.CreateGuid().replace(/-/g, ""); blockModel.config = Utilities.copy(blockConfiguration); @@ -355,11 +395,6 @@ this.label = getBlockLabel(this); }.bind(this))}.bind(blockModel), 10); - var contentScaffold = this.getScaffoldFor(blockConfiguration.contentTypeKey); - if(contentScaffold === null) { - return null; - } - // make basics from scaffold blockModel.content = Utilities.copy(contentScaffold); blockModel.content.udi = udi; @@ -386,6 +421,15 @@ layoutEntry.settings = null; } + blockModel.transferDataTo = function(otherBlockModel) { + if (this.content !== null) { + blockEditorService.mapElementValues(this.content, otherBlockModel.content); + } + if (this.config.settingsElementTypeKey !== null) { + blockEditorService.mapElementValues(this.settings, otherBlockModel.settings); + } + } + // Add blockModel to our isolated scope to enable watching its values: this.isolatedScope.blockModels["_"+blockModel.key] = blockModel; addWatchers(blockModel, this.isolatedScope); @@ -395,11 +439,20 @@ }, + /** + * Removes the data and destroys the Block Model. + * Notive this method does not remove the block from your layout, this will need to be handlede by the Property Editor since this services donst know about your layout structure. + * @param {Object} blockModel The BlockModel to be removed and destroyed. + */ removeDataAndDestroyModel: function (blockModel) { this.destroyBlockModel(blockModel); this.removeDataByUdi(blockModel.content.udi); }, + /** + * Destroys the Block Model, but all data is kept. + * @param {Object} blockModel The BlockModel to be destroyed. + */ destroyBlockModel: function(blockModel) { // remove property value watchers: @@ -410,25 +463,9 @@ }, - /** - * Retrieve block model of a layout entry - * @return {Object} Scaffolded Block Content object. - */ - setDataFromBlockModel: function(blockModel) { - - var udi = blockModel.content.key; - - mapToPropertyModel(blockModel.content, blockModel.data); - - // TODO: implement settings, sync settings to layout entry. - // mapToPropertyModel(blockModel.settings, blockModel.layout.settings) - - }, - - /** - * Retrieve the layout object for this specific property editor. - * @return {Object} Layout object. + * Retrieve the layout object from this specific property editor model. + * @return {Object} Layout object, structure depends on the model of your property editor. */ getLayout: function() { if (!this.value.layout[this.propertyEditorAlias]) { @@ -438,9 +475,9 @@ }, /** - * Create a empty layout entry - * @param {Object} blockConfiguration - * @return {Object} Layout entry object, to be inserted at a decired location in the layout object. + * Create a empty layout entry, notice the layout entry is not added to the property editors model layout object, since the layout sturcture depends on the property editor. + * @param {string} contentTypeKey, the contentTypeKey of the block you wish to create, if contentTypeKey is not avaiable in the block configuration then ´null´ will be returned. + * @return {Object | null} Layout entry object, to be inserted at a decired location in the layout object. Or null if contentTypeKey is unavaiaible. */ create: function(contentTypeKey) { @@ -462,7 +499,7 @@ /** * Insert data from ElementType Model - * @return {Object} Layout entry object, to be inserted at a decired location in the layout object. + * @return {Object | null} Layout entry object, to be inserted at a decired location in the layout object. Or ´null´ if the given ElementType isnt supported by the block configuration. */ createFromElementType: function(elementTypeDataModel) { @@ -524,11 +561,13 @@ } return { + /** + * Create a new Block Editor Model Object, used to deal with editing of the Block Editor Model. + * @return {BlockEditorModelObject} A instance of the BlockEditorModelObject class. + */ createModelObject: function(propertyModelValue, propertyEditorAlias, blockConfigurations, propertyScope) { return new BlockEditorModelObject(propertyModelValue, propertyEditorAlias, blockConfigurations, propertyScope); - }, - mapElementValues: mapElementValues, - getBlockLabel: getBlockLabel + } } } diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.js index 8d581065b5..251cc8bcbb 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.js @@ -223,13 +223,10 @@ view: "views/common/infiniteeditors/blockeditor/blockeditor.html", size: blockModel.config.editorSize || "medium", submit: function(blockEditorModel) { - // To ensure syncronization gets tricked we transfer each property. - if (blockEditorModel.content !== null) { - blockEditorService.mapElementValues(blockEditorModel.content, blockModel.content) - } - if (blockModel.config.settingsElementTypeKey !== null) { - blockEditorService.mapElementValues(blockEditorModel.settings, blockModel.settings) - } + + // Now lets syncronize by transfer data back to our blockModel. + blockEditorModel.transferDataTo(blockModel); + editorService.close(); }, close: function() { From 6af8a532087376107576f68ff696f1fc81a993b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Fri, 12 Jun 2020 13:12:42 +0200 Subject: [PATCH 182/377] default size should be medium --- .../prevalue/blocklist.blockconfiguration.controller.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 65135a13a1..5490d1aa08 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 @@ -150,7 +150,7 @@ "labelTemplate": "", "view": null, "stylesheet": null, - "editorSize": "small", + "editorSize": "medium", "iconColor": null, "backgroundColor": null, "thumbnail": null From 5ad47aa6410374aa1480db7617c6537bdec3f807 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Fri, 12 Jun 2020 13:35:05 +0200 Subject: [PATCH 183/377] use retriveValuesFrom method to transfer data. --- .../src/common/services/blockeditor.service.js | 6 +++--- .../views/propertyeditors/blocklist/blocklist.component.js | 7 +++---- 2 files changed, 6 insertions(+), 7 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 1efc09b3be..f36490fd1c 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 @@ -421,12 +421,12 @@ layoutEntry.settings = null; } - blockModel.transferDataTo = function(otherBlockModel) { + blockModel.retriveValuesFrom = function(content, settings) { if (this.content !== null) { - blockEditorService.mapElementValues(this.content, otherBlockModel.content); + mapElementValues(content, this.content); } if (this.config.settingsElementTypeKey !== null) { - blockEditorService.mapElementValues(this.settings, otherBlockModel.settings); + mapElementValues(settings, this.settings); } } diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.js index 251cc8bcbb..c9ca360797 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.js @@ -223,10 +223,9 @@ view: "views/common/infiniteeditors/blockeditor/blockeditor.html", size: blockModel.config.editorSize || "medium", submit: function(blockEditorModel) { - - // Now lets syncronize by transfer data back to our blockModel. - blockEditorModel.transferDataTo(blockModel); - + + blockModel.retriveValuesFrom(blockEditorModel.content, blockEditorModel.settings) + editorService.close(); }, close: function() { From 86c1abdbf7f6baec30b962b552d7857ed52b9842 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Fri, 12 Jun 2020 13:54:47 +0200 Subject: [PATCH 184/377] ability to disable an navigation item --- .../src/less/components/umb-editor-navigation-item.less | 6 ++++++ .../views/components/editor/umb-editor-navigation-item.html | 1 + 2 files changed, 7 insertions(+) diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-editor-navigation-item.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-editor-navigation-item.less index f3c41dbc33..cb673e3c6f 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-editor-navigation-item.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-editor-navigation-item.less @@ -20,12 +20,18 @@ justify-content: center; height: calc(~'@{editorHeaderHeight}'- ~'1px'); // need to offset the 1px border-bottom on .umb-editor-header - avoids overflowing top of the container color: @ui-active-type; + user-select: none; &:hover { color: @ui-active-type-hover !important; text-decoration: none; } + &:disabled { + pointer-events: none; + color: @gray-6; + } + &::before { content: ""; position: absolute; diff --git a/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editor-navigation-item.html b/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editor-navigation-item.html index d743907d07..484e0175c5 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editor-navigation-item.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editor-navigation-item.html @@ -4,6 +4,7 @@ hotkey="{{::vm.hotkey}}" hotkey-when-hidden="true" ng-class="{'is-active': vm.item.active, '-has-error': vm.item.hasError}" + ng-disabled="vm.item.disabled" class="umb-sub-views-nav-item__action umb-outline umb-outline--thin"> {{ vm.item.name }} From 80dfea874384f0d104f3faa23aaa6b20f88994d9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Fri, 12 Jun 2020 13:55:21 +0200 Subject: [PATCH 185/377] createElementTypeAndCallback working again for settings. --- ...blocklist.blockconfiguration.controller.js | 5 ++-- ...t.blockconfiguration.overlay.controller.js | 24 ++++++++++++++++--- 2 files changed, 24 insertions(+), 5 deletions(-) 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 5490d1aa08..3141f0418a 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 @@ -102,7 +102,7 @@ createNewItem: { action: function() { overlayService.close(); - vm.createElementTypeAndAdd(vm.addBlockFromElementTypeKey); + vm.createElementTypeAndCallback(vm.addBlockFromElementTypeKey); }, icon: "icon-add", name: localized[1] @@ -124,7 +124,7 @@ }); }; - vm.createElementTypeAndAdd = function(callback) { + vm.createElementTypeAndCallback = function(callback) { const editor = { create: true, infiniteMode: true, @@ -176,6 +176,7 @@ view: "views/propertyeditors/blocklist/prevalue/blocklist.blockconfiguration.overlay.html", size: "small", submit: function(overlayModel) { + loadElementTypes()// lets load elementType again, to ensure we are up to date. TransferProperties(overlayModel.block, block);// transfer properties back to block object. (Doing this cause we dont know if block object is added to model jet, therefor we cant use index or replace the object.) overlayModel.close(); }, diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/blocklist.blockconfiguration.overlay.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/blocklist.blockconfiguration.overlay.controller.js index 5e7d43c6f4..b17ee35100 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/blocklist.blockconfiguration.overlay.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/blocklist.blockconfiguration.overlay.controller.js @@ -44,6 +44,24 @@ editorService.documentTypeEditor(editor); } + vm.createElementTypeAndCallback = function(callback) { + const editor = { + create: true, + infiniteMode: true, + isElement: true, + submit: function (model) { + loadElementTypes().then( function () { + callback(model.documentTypeKey); + }); + editorService.close(); + }, + close: function () { + editorService.close(); + } + }; + editorService.documentTypeEditor(editor); + } + vm.addSettingsForBlock = function ($event, block) { localizationService.localizeMany(["blockEditor_headlineAddSettingsElementType", "blockEditor_labelcreateNewElementType"]).then(function(localized) { @@ -58,15 +76,15 @@ createNewItem: { action: function() { overlayService.close(); - vm.createElementTypeAndAdd((alias) => { - vm.applySettingsToBlock(block, alias); + vm.createElementTypeAndCallback((key) => { + vm.applySettingsToBlock(block, key); }); }, icon: "icon-add", name: localized[1] }, submit: function (overlay) { - vm.applySettingsToBlock(block, overlay.selectedItem.alias); + vm.applySettingsToBlock(block, overlay.selectedItem.key); overlayService.close(); }, close: function () { From cbca5bd9e6d75eae9f402ec8bb9e131cce035f73 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Fri, 12 Jun 2020 13:56:14 +0200 Subject: [PATCH 186/377] still have the ability to retrive a scaffold model by alias, since we are still using alias in clipboard service. --- .../common/services/blockeditor.service.js | 19 ++++++++++++++----- .../blocklist/blocklist.component.js | 6 ++++-- 2 files changed, 18 insertions(+), 7 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 f36490fd1c..8ef6bfa214 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 @@ -310,7 +310,7 @@ * @return {Array} array of strings representing alias. */ getAvailableAliasesForBlockContent: function() { - return this.blockConfigurations.map(blockConfiguration => this.getScaffoldFor(blockConfiguration.contentTypeKey).contentTypeKey); + return this.blockConfigurations.map(blockConfiguration => this.getScaffoldFromKey(blockConfiguration.contentTypeKey).contentTypeKey); }, /** @@ -323,7 +323,7 @@ var blocks = []; this.blockConfigurations.forEach(blockConfiguration => { - var scaffold = this.getScaffoldFor(blockConfiguration.contentTypeKey); + var scaffold = this.getScaffoldFromKey(blockConfiguration.contentTypeKey); if(scaffold) { blocks.push({ blockConfigModel: blockConfiguration, @@ -340,10 +340,19 @@ * @param {string} key contentTypeKey to recive the scaffold model for. * @returns {Object | null} Scaffold model for the that content type. Or null if the scaffolding model dosnt exist in this context. */ - getScaffoldFor: function(contentTypeKey) { + getScaffoldFromKey: function(contentTypeKey) { return this.scaffolds.find(o => o.contentTypeKey === contentTypeKey); }, + /** + * Get scaffold model for a given contentTypeAlias, used by clipboardService. + * @param {string} alias contentTypeAlias to recive the scaffold model for. + * @returns {Object | null} Scaffold model for the that content type. Or null if the scaffolding model dosnt exist in this context. + */ + getScaffoldFromAlias: function(contentTypeAlias) { + return this.scaffolds.find(o => o.contentTypeAlias === contentTypeAlias); + }, + /** * Retrieve editor friendly model of a block. * BlockModel is a class instance which setups live syncronization of content and settings models back to the data of your property editor model. @@ -379,7 +388,7 @@ return null; } - var contentScaffold = this.getScaffoldFor(blockConfiguration.contentTypeKey); + var contentScaffold = this.getScaffoldFromKey(blockConfiguration.contentTypeKey); if(contentScaffold === null) { return null; } @@ -406,7 +415,7 @@ blockModel.watchers = []; if (blockConfiguration.settingsElementTypeKey) { - var settingsScaffold = this.getScaffoldFor(blockConfiguration.settingsElementTypeKey); + var settingsScaffold = this.getScaffoldFromKey(blockConfiguration.settingsElementTypeKey); if (settingsScaffold === null) { return null; } diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.js index c9ca360797..9de525e3ef 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.js @@ -132,6 +132,8 @@ vm.loading = false; + $scope.$evalAsync(); + } @@ -225,7 +227,7 @@ submit: function(blockEditorModel) { blockModel.retriveValuesFrom(blockEditorModel.content, blockEditorModel.settings) - + editorService.close(); }, close: function() { @@ -302,7 +304,7 @@ { type: "elementType", pasteData: entry.data, - blockConfigModel: modelObject.getScaffoldFor(entry.key), + blockConfigModel: modelObject.getScaffoldFromAlias(entry.alias), elementTypeModel: { name: entry.label, icon: entry.icon From 5870426af967c3eef7be6a626abdcc357fe0b2b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Fri, 12 Jun 2020 13:56:35 +0200 Subject: [PATCH 187/377] disable clipboard tab if no available paste options --- .../infiniteeditors/blockpicker/blockpicker.controller.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/blockpicker/blockpicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/blockpicker/blockpicker.controller.js index 77be5b741a..49e32fa5f9 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/blockpicker/blockpicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/blockpicker/blockpicker.controller.js @@ -22,7 +22,8 @@ angular.module("umbraco") "alias": "clipboard", "name": data[1], "icon": "icon-paste-in", - "view": "" + "view": "", + "disabled": vm.model.clipboardItems.length === 0 }]; vm.activeTab = vm.navigation[0]; From 60fc66797e96fce8f5f0303dafe63672fb8b714c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Fri, 12 Jun 2020 13:58:58 +0200 Subject: [PATCH 188/377] ups, should stay as alias --- .../src/common/services/blockeditor.service.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 8ef6bfa214..c698bc315c 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 @@ -310,7 +310,7 @@ * @return {Array} array of strings representing alias. */ getAvailableAliasesForBlockContent: function() { - return this.blockConfigurations.map(blockConfiguration => this.getScaffoldFromKey(blockConfiguration.contentTypeKey).contentTypeKey); + return this.blockConfigurations.map(blockConfiguration => this.getScaffoldFromKey(blockConfiguration.contentTypeKey).contentTypeAlias); }, /** From 6d8d79531d202b3bef7f198226390619cff4dd64 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Fri, 12 Jun 2020 14:01:13 +0200 Subject: [PATCH 189/377] disable clipboard when empty --- .../common/infiniteeditors/blockpicker/blockpicker.controller.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/blockpicker/blockpicker.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/blockpicker/blockpicker.controller.js index 49e32fa5f9..581460954a 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/blockpicker/blockpicker.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/blockpicker/blockpicker.controller.js @@ -39,6 +39,7 @@ angular.module("umbraco") vm.clickClearClipboard = function() { vm.onNavigationChanged(vm.navigation[0]); + vm.navigation[1].disabled = true;// disabled ws determined when creating the navigation, so we need to update it here. vm.model.clipboardItems = [];// This dialog is not connected via the clipboardService events, so we need to update manually. vm.model.clickClearClipboard(); } From e1f3eb6f0023533ec28f71792560bd6044cc7d78 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Fri, 12 Jun 2020 14:12:13 +0200 Subject: [PATCH 190/377] use property alias for content add button --- .../src/views/propertyeditors/blocklist/blocklist.component.html | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.html index 6a2c012e25..e3afba8cab 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.html @@ -52,6 +52,7 @@ diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklistentryeditors/labelblock/labelblock.editor.less b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklistentryeditors/labelblock/labelblock.editor.less index 974662b416..d01e53ef9c 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklistentryeditors/labelblock/labelblock.editor.less +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklistentryeditors/labelblock/labelblock.editor.less @@ -1,5 +1,6 @@ .blockelement-labelblock-editor { + position: relative; display: block; margin-bottom: 4px; margin-top: 4px; @@ -34,4 +35,10 @@ color: @ui-action-discreet-type-hover; border-color: @gray-8; } + + &.--active { + color: @ui-active-type; + border-color: @ui-active; + background-color: @ui-active; + } } From 6aa21d8c8325f2e88225c3cd5e0f5eec06cc71ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Mon, 15 Jun 2020 10:29:09 +0200 Subject: [PATCH 193/377] rename entry to block --- .../blocklist.blockconfiguration.overlay.controller.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/blocklist.blockconfiguration.overlay.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/blocklist.blockconfiguration.overlay.controller.js index b17ee35100..d722a17c1a 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/blocklist.blockconfiguration.overlay.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/blocklist.blockconfiguration.overlay.controller.js @@ -103,7 +103,7 @@ vm.requestRemoveSettingsForBlock = function(block) { localizationService.localizeMany(["general_remove", "defaultdialogs_confirmremoveusageof"]).then(function (data) { - var settingsElementType = vm.getElementTypeByAlias(entry.settingsElementTypeKey); + var settingsElementType = vm.getElementTypeByKey(block.settingsElementTypeKey); overlayService.confirmRemove({ title: data[0], @@ -112,14 +112,14 @@ overlayService.close(); }, submit: function () { - vm.removeSettingsForEntry(entry); + vm.removeSettingsForBlock(block); overlayService.close(); } }); }); }; - vm.removeSettingsForEntry = function(entry) { - entry.settingsElementTypeKey = null; + vm.removeSettingsForBlock = function(block) { + block.settingsElementTypeKey = null; }; From ed9d4d66620e73820186259493b22a86cd710751 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Mon, 15 Jun 2020 10:58:05 +0200 Subject: [PATCH 194/377] appended ' and added space in Element Type --- src/Umbraco.Web.UI/Umbraco/config/lang/en.xml | 4 ++-- src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml index d51ffa5851..fe260cf562 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml @@ -621,7 +621,7 @@ %0% is a mandatory field %0% at %1% is not in a correct format %0% is not in a correct format - Property %0% uses editor %1% which is not supported in ElementTypes. + Property '%0%' uses editor '%1%' which is not supported in Element Types. Received an error from the server @@ -2260,7 +2260,7 @@ To manage your website, simply open the Umbraco back office and start adding con Create Edit Name - Add new row + Add new row View more options 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 dcdd724a1f..75748d7fb7 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml @@ -644,7 +644,7 @@ Please place cursor at the left of the two cells you wish to merge You cannot split a cell that hasn't been merged. This property is invalid - Property %0% uses editor %1% which is not supported in ElementTypes. + Property '%0%' uses editor '%1%' which is not supported in Element Types. Options From 9944cce4d9c1c6a4db9adde4d13fc60447ef1909 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Mon, 15 Jun 2020 12:12:09 +0200 Subject: [PATCH 195/377] use background color for hover to corospond with active state --- .../blocklistentryeditors/labelblock/labelblock.editor.less | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklistentryeditors/labelblock/labelblock.editor.less b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklistentryeditors/labelblock/labelblock.editor.less index d01e53ef9c..0ebcfaea53 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklistentryeditors/labelblock/labelblock.editor.less +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklistentryeditors/labelblock/labelblock.editor.less @@ -33,7 +33,7 @@ &:hover { color: @ui-action-discreet-type-hover; - border-color: @gray-8; + background-color: @ui-action-hover; } &.--active { From 27b539b8c80b0edc7e200ccddec7cb03503dfe09 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Mon, 15 Jun 2020 12:12:32 +0200 Subject: [PATCH 196/377] make nested content unsupported --- .../src/common/services/blockeditor.service.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 c698bc315c..1a44190163 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 @@ -219,7 +219,8 @@ var notSupportedProperties = [ "Umbraco.Tags", "Umbraco.UploadField", - "Umbraco.ImageCropper" + "Umbraco.ImageCropper", + "Umbraco.NestedContent" ]; function replaceUnsupportedProperties(scaffold) { scaffold.variants.forEach((variant) => { From 32e3ebb6fbb8441ec7fb48232fc5e74e4eed853a Mon Sep 17 00:00:00 2001 From: Shannon Date: Mon, 15 Jun 2020 23:05:32 +1000 Subject: [PATCH 197/377] Centralizes logic to validate complex editors, starts transitioning to new JSON error messages for complex editors, starts adding tests --- .../Models/Blocks/IBlockEditorDataHelper.cs | 4 + .../Services/PropertyValidationService.cs | 62 ++++- src/Umbraco.Tests/Models/VariationTests.cs | 10 +- .../Services/ContentServiceTests.cs | 2 +- .../PropertyValidationServiceTests.cs | 2 +- .../Entities/MockedContentTypes.cs | 8 +- src/Umbraco.Tests/Umbraco.Tests.csproj | 3 +- .../Validation/ContentModelValidatorTests.cs | 254 ++++++++++++++++++ .../ModelStateExtensionsTests.cs | 2 +- .../Editors/Binders/BlueprintItemBinder.cs | 4 +- .../Editors/Binders/ContentItemBinder.cs | 25 +- .../Binders/ContentModelBinderHelper.cs | 6 +- .../Editors/Binders/MediaItemBinder.cs | 6 +- .../Editors/Binders/MemberBinder.cs | 6 +- .../Editors/Filters/ContentModelValidator.cs | 57 ++-- .../Filters/ContentSaveModelValidator.cs | 30 ++- src/Umbraco.Web/ModelStateExtensions.cs | 26 +- .../Models/ContentEditing/ContentItemSave.cs | 2 +- .../Mapping/ContentPropertyBasicMapper.cs | 3 + .../BlockEditorPropertyEditor.cs | 63 +---- .../BlockListPropertyEditor.cs | 1 + .../PropertyEditors/ComplexEditorValidator.cs | 73 +++++ .../NestedContentPropertyEditor.cs | 79 ++---- .../Validation/NestedValidationResults.cs | 22 ++ .../Validation/PropertyValidationResult.cs | 27 ++ .../Validation/ValidationResultConverter.cs | 71 +++++ src/Umbraco.Web/Umbraco.Web.csproj | 4 + 27 files changed, 659 insertions(+), 193 deletions(-) create mode 100644 src/Umbraco.Tests/Web/Validation/ContentModelValidatorTests.cs rename src/Umbraco.Tests/Web/{ => Validation}/ModelStateExtensionsTests.cs (99%) create mode 100644 src/Umbraco.Web/PropertyEditors/ComplexEditorValidator.cs create mode 100644 src/Umbraco.Web/PropertyEditors/Validation/NestedValidationResults.cs create mode 100644 src/Umbraco.Web/PropertyEditors/Validation/PropertyValidationResult.cs create mode 100644 src/Umbraco.Web/PropertyEditors/Validation/ValidationResultConverter.cs diff --git a/src/Umbraco.Core/Models/Blocks/IBlockEditorDataHelper.cs b/src/Umbraco.Core/Models/Blocks/IBlockEditorDataHelper.cs index 32f8431e65..35bcaa49ab 100644 --- a/src/Umbraco.Core/Models/Blocks/IBlockEditorDataHelper.cs +++ b/src/Umbraco.Core/Models/Blocks/IBlockEditorDataHelper.cs @@ -3,8 +3,12 @@ using System.Collections.Generic; namespace Umbraco.Core.Models.Blocks { + // TODO: Rename this, we don't want to use the name "Helper" + // TODO: What is this? This requires code docs + // TODO: This is not used publicly at all - therefore it probably shouldn't be public public interface IBlockEditorDataHelper { + // TODO: Does this abstraction need a reference to JObject? Maybe it does but ideally it doesn't IEnumerable GetBlockReferences(JObject layout); bool IsEditorSpecificPropertyKey(string propertyKey); } diff --git a/src/Umbraco.Core/Services/PropertyValidationService.cs b/src/Umbraco.Core/Services/PropertyValidationService.cs index a037a83920..adadb12ef6 100644 --- a/src/Umbraco.Core/Services/PropertyValidationService.cs +++ b/src/Umbraco.Core/Services/PropertyValidationService.cs @@ -1,9 +1,7 @@ using System; using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; using System.Linq; -using System.Text; -using System.Threading.Tasks; -using Umbraco.Core.Collections; using Umbraco.Core.Composing; using Umbraco.Core.Models; using Umbraco.Core.PropertyEditors; @@ -15,19 +13,73 @@ namespace Umbraco.Core.Services { private readonly PropertyEditorCollection _propertyEditors; private readonly IDataTypeService _dataTypeService; + private readonly ILocalizedTextService _textService; - public PropertyValidationService(PropertyEditorCollection propertyEditors, IDataTypeService dataTypeService) + public PropertyValidationService(PropertyEditorCollection propertyEditors, IDataTypeService dataTypeService, ILocalizedTextService textService) { _propertyEditors = propertyEditors; _dataTypeService = dataTypeService; + _textService = textService; } //TODO: Remove this method in favor of the overload specifying all dependencies public PropertyValidationService() - : this(Current.PropertyEditors, Current.Services.DataTypeService) + : this(Current.PropertyEditors, Current.Services.DataTypeService, Current.Services.TextService) { } + public IEnumerable ValidatePropertyValue( + PropertyType propertyType, + object postedValue) + { + if (propertyType is null) throw new ArgumentNullException(nameof(propertyType)); + var dataType = _dataTypeService.GetDataType(propertyType.DataTypeId); + if (dataType == null) throw new InvalidOperationException("No data type found by id " + propertyType.DataTypeId); + + var editor = _propertyEditors[propertyType.PropertyEditorAlias]; + if (editor == null) throw new InvalidOperationException("No property editor found by alias " + propertyType.PropertyEditorAlias); ; + + return ValidatePropertyValue(_textService, editor, dataType, postedValue, propertyType.Mandatory, propertyType.ValidationRegExp, propertyType.MandatoryMessage, propertyType.ValidationRegExpMessage); + } + + internal static IEnumerable ValidatePropertyValue( + ILocalizedTextService textService, + IDataEditor editor, + IDataType dataType, + object postedValue, + bool isRequired, + string validationRegExp, + string isRequiredMessage, + string validationRegExpMessage) + { + // Retrieve default messages used for required and regex validatation. We'll replace these + // if set with custom ones if they've been provided for a given property. + var requiredDefaultMessages = new[] + { + textService.Localize("validation", "invalidNull"), + textService.Localize("validation", "invalidEmpty") + }; + var formatDefaultMessages = new[] + { + textService.Localize("validation", "invalidPattern"), + }; + + var valueEditor = editor.GetValueEditor(dataType.Configuration); + foreach (var validationResult in valueEditor.Validate(postedValue, isRequired, validationRegExp)) + { + // If we've got custom error messages, we'll replace the default ones that will have been applied in the call to Validate(). + if (isRequired && !string.IsNullOrWhiteSpace(isRequiredMessage) && requiredDefaultMessages.Contains(validationResult.ErrorMessage, StringComparer.OrdinalIgnoreCase)) + { + validationResult.ErrorMessage = isRequiredMessage; + } + if (!string.IsNullOrWhiteSpace(validationRegExp) && !string.IsNullOrWhiteSpace(validationRegExpMessage) && formatDefaultMessages.Contains(validationResult.ErrorMessage, StringComparer.OrdinalIgnoreCase)) + { + validationResult.ErrorMessage = validationRegExpMessage; + } + yield return validationResult; + } + } + /// /// Validates the content item's properties pass validation rules /// diff --git a/src/Umbraco.Tests/Models/VariationTests.cs b/src/Umbraco.Tests/Models/VariationTests.cs index ab5f726894..b87ff499b6 100644 --- a/src/Umbraco.Tests/Models/VariationTests.cs +++ b/src/Umbraco.Tests/Models/VariationTests.cs @@ -442,7 +442,10 @@ namespace Umbraco.Tests.Models [Test] public void ContentPublishValuesWithMixedPropertyTypeVariations() { - var propertyValidationService = new PropertyValidationService(Current.Factory.GetInstance(), Current.Factory.GetInstance().DataTypeService); + var propertyValidationService = new PropertyValidationService( + Current.Factory.GetInstance(), + Current.Factory.GetInstance().DataTypeService, + Current.Factory.GetInstance().TextService); const string langFr = "fr-FR"; // content type varies by Culture @@ -574,7 +577,10 @@ namespace Umbraco.Tests.Models prop.SetValue("a"); Assert.AreEqual("a", prop.GetValue()); Assert.IsNull(prop.GetValue(published: true)); - var propertyValidationService = new PropertyValidationService(Current.Factory.GetInstance(), Current.Factory.GetInstance().DataTypeService); + var propertyValidationService = new PropertyValidationService( + Current.Factory.GetInstance(), + Current.Factory.GetInstance().DataTypeService, + Current.Factory.GetInstance().TextService); Assert.IsTrue(propertyValidationService.IsPropertyValid(prop)); diff --git a/src/Umbraco.Tests/Services/ContentServiceTests.cs b/src/Umbraco.Tests/Services/ContentServiceTests.cs index 553d1b97ba..041dabe7d2 100644 --- a/src/Umbraco.Tests/Services/ContentServiceTests.cs +++ b/src/Umbraco.Tests/Services/ContentServiceTests.cs @@ -1197,7 +1197,7 @@ namespace Umbraco.Tests.Services Assert.IsFalse(content.HasIdentity); // content cannot publish values because they are invalid - var propertyValidationService = new PropertyValidationService(Factory.GetInstance(), ServiceContext.DataTypeService); + var propertyValidationService = new PropertyValidationService(Factory.GetInstance(), ServiceContext.DataTypeService, ServiceContext.TextService); var isValid = propertyValidationService.IsPropertyDataValid(content, out var invalidProperties, CultureImpact.Invariant); Assert.IsFalse(isValid); Assert.IsNotEmpty(invalidProperties); diff --git a/src/Umbraco.Tests/Services/PropertyValidationServiceTests.cs b/src/Umbraco.Tests/Services/PropertyValidationServiceTests.cs index e1e19918ce..28bdf59373 100644 --- a/src/Umbraco.Tests/Services/PropertyValidationServiceTests.cs +++ b/src/Umbraco.Tests/Services/PropertyValidationServiceTests.cs @@ -36,7 +36,7 @@ namespace Umbraco.Tests.Services var propEditors = new PropertyEditorCollection(new DataEditorCollection(new[] { dataEditor })); - validationService = new PropertyValidationService(propEditors, dataTypeService.Object); + validationService = new PropertyValidationService(propEditors, dataTypeService.Object, Mock.Of()); } [Test] diff --git a/src/Umbraco.Tests/TestHelpers/Entities/MockedContentTypes.cs b/src/Umbraco.Tests/TestHelpers/Entities/MockedContentTypes.cs index c55467431d..b4cd4ab05e 100644 --- a/src/Umbraco.Tests/TestHelpers/Entities/MockedContentTypes.cs +++ b/src/Umbraco.Tests/TestHelpers/Entities/MockedContentTypes.cs @@ -41,12 +41,12 @@ namespace Umbraco.Tests.TestHelpers.Entities }; var contentCollection = new PropertyTypeCollection(true); - contentCollection.Add(new PropertyType("test", ValueStorageType.Ntext) { Alias = "title", Name = "Title", Description = "", Mandatory = false, SortOrder = 1, DataTypeId = -88 }); - contentCollection.Add(new PropertyType("test", ValueStorageType.Ntext) { Alias = "bodyText", Name = "Body Text", Description = "", Mandatory = false, SortOrder = 2, DataTypeId = -87 }); + contentCollection.Add(new PropertyType("test", ValueStorageType.Ntext) { Alias = "title", Name = "Title", Description = "", Mandatory = false, SortOrder = 1, DataTypeId = Constants.DataTypes.Textbox }); + contentCollection.Add(new PropertyType("test", ValueStorageType.Ntext) { Alias = "bodyText", Name = "Body Text", Description = "", Mandatory = false, SortOrder = 2, DataTypeId = Constants.DataTypes.RichtextEditor }); var metaCollection = new PropertyTypeCollection(true); - metaCollection.Add(new PropertyType("test", ValueStorageType.Ntext) { Alias = "keywords", Name = "Meta Keywords", Description = "", Mandatory = false, SortOrder = 1, DataTypeId = -88 }); - metaCollection.Add(new PropertyType("test", ValueStorageType.Ntext) { Alias = "description", Name = "Meta Description", Description = "", Mandatory = false, SortOrder = 2, DataTypeId = -89 }); + metaCollection.Add(new PropertyType("test", ValueStorageType.Ntext) { Alias = "keywords", Name = "Meta Keywords", Description = "", Mandatory = false, SortOrder = 1, DataTypeId = Constants.DataTypes.Textbox }); + metaCollection.Add(new PropertyType("test", ValueStorageType.Ntext) { Alias = "description", Name = "Meta Description", Description = "", Mandatory = false, SortOrder = 2, DataTypeId = Constants.DataTypes.Textarea }); contentType.PropertyGroups.Add(new PropertyGroup(contentCollection) { Name = "Content", SortOrder = 1 }); contentType.PropertyGroups.Add(new PropertyGroup(metaCollection) { Name = "Meta", SortOrder = 2 }); diff --git a/src/Umbraco.Tests/Umbraco.Tests.csproj b/src/Umbraco.Tests/Umbraco.Tests.csproj index 08ff3167b5..cfc4686853 100644 --- a/src/Umbraco.Tests/Umbraco.Tests.csproj +++ b/src/Umbraco.Tests/Umbraco.Tests.csproj @@ -266,7 +266,7 @@ - + @@ -513,6 +513,7 @@ + diff --git a/src/Umbraco.Tests/Web/Validation/ContentModelValidatorTests.cs b/src/Umbraco.Tests/Web/Validation/ContentModelValidatorTests.cs new file mode 100644 index 0000000000..cbc4152c17 --- /dev/null +++ b/src/Umbraco.Tests/Web/Validation/ContentModelValidatorTests.cs @@ -0,0 +1,254 @@ +using Moq; +using NUnit.Framework; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Umbraco.Core.Services; +using Umbraco.Core.Logging; +using Umbraco.Web; +using Umbraco.Web.Editors.Filters; +using Umbraco.Tests.TestHelpers.Entities; +using Umbraco.Core.Models; +using Umbraco.Web.Models.ContentEditing; +using Umbraco.Web.Editors.Binders; +using Umbraco.Core; +using Umbraco.Tests.Testing; +using Umbraco.Core.Mapping; +using Umbraco.Core.PropertyEditors; +using Umbraco.Core.Composing; +using System.Web.Http.ModelBinding; +using Umbraco.Web.PropertyEditors; +using System.ComponentModel.DataAnnotations; +using Umbraco.Tests.TestHelpers; +using System.Globalization; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace Umbraco.Tests.Web.Validation +{ + [UmbracoTest(Mapper = true, WithApplication = true, Logger = UmbracoTestOptions.Logger.Console)] + [TestFixture] + public class ContentModelValidatorTests : UmbracoTestBase + { + private const int ComplexDataTypeId = 9999; + private const string ContentTypeAlias = "textPage"; + private ContentType _contentType; + + public override void SetUp() + { + base.SetUp(); + + _contentType = MockedContentTypes.CreateTextPageContentType(ContentTypeAlias); + // add complex editor + _contentType.AddPropertyType( + new PropertyType("complexTest", ValueStorageType.Ntext) { Alias = "complex", Name = "Meta Keywords", Description = "", Mandatory = false, SortOrder = 1, DataTypeId = ComplexDataTypeId }, + "Content"); + + // make them all validate with a regex rule that will not pass + foreach (var prop in _contentType.PropertyTypes) + { + prop.ValidationRegExp = "^donotmatch$"; + prop.ValidationRegExpMessage = "Does not match!"; + } + } + + protected override void Compose() + { + base.Compose(); + + var complexEditorConfig = new NestedContentConfiguration + { + ContentTypes = new[] + { + new NestedContentConfiguration.ContentType { Alias = "feature" } + } + }; + var dataTypeService = new Mock(); + dataTypeService.Setup(x => x.GetDataType(It.IsAny())) + .Returns((int id) => id == ComplexDataTypeId + ? Mock.Of(x => x.Configuration == complexEditorConfig) + : Mock.Of()); + + var contentTypeService = new Mock(); + contentTypeService.Setup(x => x.GetAll(It.IsAny())) + .Returns(() => new List + { + _contentType + }); + + var textService = new Mock(); + textService.Setup(x => x.Localize("validation/invalidPattern", It.IsAny(), It.IsAny>())).Returns(() => "invalidPattern"); + textService.Setup(x => x.Localize("validation/invalidNull", It.IsAny(), It.IsAny>())).Returns("invalidNull"); + textService.Setup(x => x.Localize("validation/invalidEmpty", It.IsAny(), It.IsAny>())).Returns("invalidEmpty"); + + Composition.RegisterUnique(x => Mock.Of(x => x.GetDataType(It.IsAny()) == Mock.Of())); + Composition.RegisterUnique(x => dataTypeService.Object); + Composition.RegisterUnique(x => contentTypeService.Object); + Composition.RegisterUnique(x => textService.Object); + + Composition.WithCollectionBuilder() + .Add() + .Add(); + } + + [Test] + public void Test() + { + var validator = new ContentSaveModelValidator( + Factory.GetInstance(), + Mock.Of(), + Factory.GetInstance()); + + var content = MockedContent.CreateTextpageContent(_contentType, "test", -1); + + const string complexValue = @"[{ + ""key"": ""c8df5136-d606-41f0-9134-dea6ae0c2fd9"", + ""name"": ""Hello world"", + ""ncContentTypeAlias"": """ + ContentTypeAlias + @""", + ""title"": ""Hello world"" + }, { + ""key"": ""f916104a-4082-48b2-a515-5c4bf2230f38"", + ""name"": ""Hello worldsss ddf"", + ""ncContentTypeAlias"": """ + ContentTypeAlias + @""", + ""title"": ""Hello worldsss ddf"" + } +]"; + content.SetValue("complex", complexValue); + + // map the persisted properties to a model representing properties to save + //var saveProperties = content.Properties.Select(x => Mapper.Map(x)).ToList(); + var saveProperties = content.Properties.Select(x => + { + return new ContentPropertyBasic + { + Alias = x.Alias, + Id = x.Id, + Value = x.GetValue() + }; + }).ToList(); + + var saveVariants = new List + { + new ContentVariantSave + { + Culture = string.Empty, + Segment = string.Empty, + Name = content.Name, + Save = true, + Properties = saveProperties + } + }; + + var save = new ContentItemSave + { + Id = content.Id, + Action = ContentSaveAction.Save, + ContentTypeAlias = _contentType.Alias, + ParentId = -1, + PersistedContent = content, + TemplateAlias = null, + Variants = saveVariants + }; + + // This will map the ContentItemSave.Variants.PropertyCollectionDto and then map the values in the saved model + // back onto the persisted IContent model. + ContentItemBinder.BindModel(save, content); + + var modelState = new ModelStateDictionary(); + var isValid = validator.ValidatePropertiesData(save, saveVariants[0], saveVariants[0].PropertyCollectionDto, modelState); + + // list results for debugging + foreach (var state in modelState) + { + Console.WriteLine(state.Key); + foreach (var error in state.Value.Errors) + { + Console.WriteLine("\t" + error.ErrorMessage); + } + } + + // assert + + Assert.IsFalse(isValid); + Assert.AreEqual(5, modelState.Keys.Count); + const string complexPropertyKey = "_Properties.complex.invariant.null"; + Assert.IsTrue(modelState.Keys.Contains(complexPropertyKey)); + foreach(var state in modelState.Where(x => x.Key != complexPropertyKey)) + { + foreach (var error in state.Value.Errors) + { + Assert.IsTrue(error.ErrorMessage.DetectIsJson()); + var json = JsonConvert.DeserializeObject(error.ErrorMessage); + Assert.IsNotEmpty(json["errorMessage"].Value()); + Assert.AreEqual(1, json["memberNames"].Value().Count); + } + } + var complexEditorErrors = modelState.Single(x => x.Key == complexPropertyKey).Value.Errors; + Assert.AreEqual(3, complexEditorErrors.Count); + var nestedError = complexEditorErrors.Single(x => x.ErrorMessage.Contains("nestedValidation")); + var jsonNestedError = JsonConvert.DeserializeObject(nestedError.ErrorMessage); + Assert.AreEqual(JTokenType.Array, jsonNestedError["nestedValidation"].Type); + var nestedValidation = (JArray)jsonNestedError["nestedValidation"]; + Assert.AreEqual(2, nestedValidation.Count); // there are 2 because there are 2 nested content rows + foreach(var rowErrors in nestedValidation) + { + var elementTypeErrors = (JArray)rowErrors; // this is an array of errors for the nested content row (element type) + Assert.AreEqual(2, elementTypeErrors.Count); + foreach(var elementTypeErr in elementTypeErrors) + { + Assert.IsNotEmpty(elementTypeErr["errorMessage"].Value()); + Assert.AreEqual(1, elementTypeErr["memberNames"].Value().Count); + } + } + } + + [HideFromTypeFinder] + [DataEditor("complexTest", "test", "test")] + public class ComplexTestEditor : NestedContentPropertyEditor + { + public ComplexTestEditor(ILogger logger, Lazy propertyEditors, IDataTypeService dataTypeService, IContentTypeService contentTypeService) : base(logger, propertyEditors, dataTypeService, contentTypeService) + { + } + + protected override IDataValueEditor CreateValueEditor() + { + var editor = base.CreateValueEditor(); + editor.Validators.Add(new NeverValidateValidator()); + return editor; + } + } + + [HideFromTypeFinder] + [DataEditor("test", "test", "test")] // This alias aligns with the prop editor alias for all properties created from MockedContentTypes.CreateTextPageContentType + public class TestEditor : DataEditor + { + public TestEditor(ILogger logger) + : base(logger) + { + } + + protected override IDataValueEditor CreateValueEditor() => new TestValueEditor(Attribute); + + private class TestValueEditor : DataValueEditor + { + public TestValueEditor(DataEditorAttribute attribute) + : base(attribute) + { + Validators.Add(new NeverValidateValidator()); + } + + } + } + + public class NeverValidateValidator : IValueValidator + { + public IEnumerable Validate(object value, string valueType, object dataTypeConfiguration) + { + yield return new ValidationResult("WRONG!", new[] { "innerFieldId" }); + } + } + + } +} diff --git a/src/Umbraco.Tests/Web/ModelStateExtensionsTests.cs b/src/Umbraco.Tests/Web/Validation/ModelStateExtensionsTests.cs similarity index 99% rename from src/Umbraco.Tests/Web/ModelStateExtensionsTests.cs rename to src/Umbraco.Tests/Web/Validation/ModelStateExtensionsTests.cs index 7b25e60b5a..0355705378 100644 --- a/src/Umbraco.Tests/Web/ModelStateExtensionsTests.cs +++ b/src/Umbraco.Tests/Web/Validation/ModelStateExtensionsTests.cs @@ -6,7 +6,7 @@ using NUnit.Framework; using Umbraco.Core.Services; using Umbraco.Web; -namespace Umbraco.Tests.Web +namespace Umbraco.Tests.Web.Validation { [TestFixture] public class ModelStateExtensionsTests diff --git a/src/Umbraco.Web/Editors/Binders/BlueprintItemBinder.cs b/src/Umbraco.Web/Editors/Binders/BlueprintItemBinder.cs index eb4d482b14..bd86d74f5d 100644 --- a/src/Umbraco.Web/Editors/Binders/BlueprintItemBinder.cs +++ b/src/Umbraco.Web/Editors/Binders/BlueprintItemBinder.cs @@ -11,8 +11,8 @@ namespace Umbraco.Web.Editors.Binders { } - public BlueprintItemBinder(ILogger logger, ServiceContext services, IUmbracoContextAccessor umbracoContextAccessor) - : base(logger, services, umbracoContextAccessor) + public BlueprintItemBinder(ServiceContext services) + : base(services) { } diff --git a/src/Umbraco.Web/Editors/Binders/ContentItemBinder.cs b/src/Umbraco.Web/Editors/Binders/ContentItemBinder.cs index a6cba6ce41..609974bef7 100644 --- a/src/Umbraco.Web/Editors/Binders/ContentItemBinder.cs +++ b/src/Umbraco.Web/Editors/Binders/ContentItemBinder.cs @@ -17,16 +17,13 @@ namespace Umbraco.Web.Editors.Binders ///
      internal class ContentItemBinder : IModelBinder { - private readonly ContentModelBinderHelper _modelBinderHelper; - - public ContentItemBinder() : this(Current.Logger, Current.Services, Current.UmbracoContextAccessor) + public ContentItemBinder() : this(Current.Services) { } - public ContentItemBinder(ILogger logger, ServiceContext services, IUmbracoContextAccessor umbracoContextAccessor) + public ContentItemBinder(ServiceContext services) { Services = services; - _modelBinderHelper = new ContentModelBinderHelper(); } protected ServiceContext Services { get; } @@ -39,10 +36,20 @@ namespace Umbraco.Web.Editors.Binders /// public bool BindModel(HttpActionContext actionContext, ModelBindingContext bindingContext) { - var model = _modelBinderHelper.BindModelFromMultipartRequest(actionContext, bindingContext); + var model = ContentModelBinderHelper.BindModelFromMultipartRequest(actionContext, bindingContext); if (model == null) return false; - model.PersistedContent = ContentControllerBase.IsCreatingAction(model.Action) ? CreateNew(model) : GetExisting(model); + BindModel(model, ContentControllerBase.IsCreatingAction(model.Action) ? CreateNew(model) : GetExisting(model)); + + return true; + } + + internal static void BindModel(ContentItemSave model, IContent persistedContent) + { + if (model is null) throw new ArgumentNullException(nameof(model)); + if (persistedContent is null) throw new ArgumentNullException(nameof(persistedContent)); + + model.PersistedContent = persistedContent; //create the dto from the persisted model if (model.PersistedContent != null) @@ -60,11 +67,9 @@ namespace Umbraco.Web.Editors.Binders }); //now map all of the saved values to the dto - _modelBinderHelper.MapPropertyValuesFromSaved(variant, variant.PropertyCollectionDto); + ContentModelBinderHelper.MapPropertyValuesFromSaved(variant, variant.PropertyCollectionDto); } } - - return true; } protected virtual IContent GetExisting(ContentItemSave model) diff --git a/src/Umbraco.Web/Editors/Binders/ContentModelBinderHelper.cs b/src/Umbraco.Web/Editors/Binders/ContentModelBinderHelper.cs index b962de74ac..a017ae5afb 100644 --- a/src/Umbraco.Web/Editors/Binders/ContentModelBinderHelper.cs +++ b/src/Umbraco.Web/Editors/Binders/ContentModelBinderHelper.cs @@ -15,9 +15,9 @@ namespace Umbraco.Web.Editors.Binders /// /// Helper methods to bind media/member models /// - internal class ContentModelBinderHelper + internal static class ContentModelBinderHelper { - public TModelSave BindModelFromMultipartRequest(HttpActionContext actionContext, ModelBindingContext bindingContext) + public static TModelSave BindModelFromMultipartRequest(HttpActionContext actionContext, ModelBindingContext bindingContext) where TModelSave : IHaveUploadedFiles { var result = actionContext.ReadAsMultipart(SystemDirectories.TempFileUploads); @@ -86,7 +86,7 @@ namespace Umbraco.Web.Editors.Binders /// /// /// - public void MapPropertyValuesFromSaved(IContentProperties saveModel, ContentPropertyCollectionDto dto) + public static void MapPropertyValuesFromSaved(IContentProperties saveModel, ContentPropertyCollectionDto dto) { //NOTE: Don't convert this to linq, this is much quicker foreach (var p in saveModel.Properties) diff --git a/src/Umbraco.Web/Editors/Binders/MediaItemBinder.cs b/src/Umbraco.Web/Editors/Binders/MediaItemBinder.cs index 1084aa16ea..bfd5c853d5 100644 --- a/src/Umbraco.Web/Editors/Binders/MediaItemBinder.cs +++ b/src/Umbraco.Web/Editors/Binders/MediaItemBinder.cs @@ -13,7 +13,6 @@ namespace Umbraco.Web.Editors.Binders /// internal class MediaItemBinder : IModelBinder { - private readonly ContentModelBinderHelper _modelBinderHelper; private readonly ServiceContext _services; public MediaItemBinder() : this(Current.Services) @@ -23,7 +22,6 @@ namespace Umbraco.Web.Editors.Binders public MediaItemBinder(ServiceContext services) { _services = services; - _modelBinderHelper = new ContentModelBinderHelper(); } /// @@ -34,7 +32,7 @@ namespace Umbraco.Web.Editors.Binders /// public bool BindModel(HttpActionContext actionContext, ModelBindingContext bindingContext) { - var model = _modelBinderHelper.BindModelFromMultipartRequest(actionContext, bindingContext); + var model = ContentModelBinderHelper.BindModelFromMultipartRequest(actionContext, bindingContext); if (model == null) return false; model.PersistedContent = ContentControllerBase.IsCreatingAction(model.Action) ? CreateNew(model) : GetExisting(model); @@ -44,7 +42,7 @@ namespace Umbraco.Web.Editors.Binders { model.PropertyCollectionDto = Current.Mapper.Map(model.PersistedContent); //now map all of the saved values to the dto - _modelBinderHelper.MapPropertyValuesFromSaved(model, model.PropertyCollectionDto); + ContentModelBinderHelper.MapPropertyValuesFromSaved(model, model.PropertyCollectionDto); } model.Name = model.Name.Trim(); diff --git a/src/Umbraco.Web/Editors/Binders/MemberBinder.cs b/src/Umbraco.Web/Editors/Binders/MemberBinder.cs index 60b4f85c21..bd51d268ee 100644 --- a/src/Umbraco.Web/Editors/Binders/MemberBinder.cs +++ b/src/Umbraco.Web/Editors/Binders/MemberBinder.cs @@ -20,7 +20,6 @@ namespace Umbraco.Web.Editors.Binders /// internal class MemberBinder : IModelBinder { - private readonly ContentModelBinderHelper _modelBinderHelper; private readonly ServiceContext _services; public MemberBinder() : this(Current.Services) @@ -30,7 +29,6 @@ namespace Umbraco.Web.Editors.Binders public MemberBinder(ServiceContext services) { _services = services; - _modelBinderHelper = new ContentModelBinderHelper(); } /// @@ -41,7 +39,7 @@ namespace Umbraco.Web.Editors.Binders /// public bool BindModel(HttpActionContext actionContext, ModelBindingContext bindingContext) { - var model = _modelBinderHelper.BindModelFromMultipartRequest(actionContext, bindingContext); + var model = ContentModelBinderHelper.BindModelFromMultipartRequest(actionContext, bindingContext); if (model == null) return false; model.PersistedContent = ContentControllerBase.IsCreatingAction(model.Action) ? CreateNew(model) : GetExisting(model); @@ -51,7 +49,7 @@ namespace Umbraco.Web.Editors.Binders { model.PropertyCollectionDto = Current.Mapper.Map(model.PersistedContent); //now map all of the saved values to the dto - _modelBinderHelper.MapPropertyValuesFromSaved(model, model.PropertyCollectionDto); + ContentModelBinderHelper.MapPropertyValuesFromSaved(model, model.PropertyCollectionDto); } model.Name = model.Name.Trim(); diff --git a/src/Umbraco.Web/Editors/Filters/ContentModelValidator.cs b/src/Umbraco.Web/Editors/Filters/ContentModelValidator.cs index 810c2d1bea..9de27248a6 100644 --- a/src/Umbraco.Web/Editors/Filters/ContentModelValidator.cs +++ b/src/Umbraco.Web/Editors/Filters/ContentModelValidator.cs @@ -1,15 +1,19 @@ -using System; +using Newtonsoft.Json; +using System; using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; using System.Linq; using System.Net; using System.Net.Http; using System.Web.Http.Controllers; using System.Web.Http.ModelBinding; +using Umbraco.Core; using Umbraco.Core.Logging; using Umbraco.Core.Models; using Umbraco.Core.PropertyEditors; using Umbraco.Core.Services; using Umbraco.Web.Models.ContentEditing; +using Umbraco.Web.PropertyEditors.Validation; namespace Umbraco.Web.Editors.Filters { @@ -125,18 +129,6 @@ namespace Umbraco.Web.Editors.Filters { var properties = modelWithProperties.Properties.ToDictionary(x => x.Alias, x => x); - // Retrieve default messages used for required and regex validatation. We'll replace these - // if set with custom ones if they've been provided for a given property. - var requiredDefaultMessages = new[] - { - _textService.Localize("validation", "invalidNull"), - _textService.Localize("validation", "invalidEmpty") - }; - var formatDefaultMessages = new[] - { - _textService.Localize("validation", "invalidPattern"), - }; - foreach (var p in dto.Properties) { var editor = p.PropertyEditor; @@ -156,7 +148,7 @@ namespace Umbraco.Web.Editors.Filters var postedValue = postedProp.Value; - ValidatePropertyValue(model, modelWithProperties, editor, p, postedValue, modelState, requiredDefaultMessages, formatDefaultMessages); + ValidatePropertyValue(model, modelWithProperties, editor, p, postedValue, modelState); } @@ -180,26 +172,29 @@ namespace Umbraco.Web.Editors.Filters IDataEditor editor, ContentPropertyDto property, object postedValue, - ModelStateDictionary modelState, - string[] requiredDefaultMessages, - string[] formatDefaultMessages) + ModelStateDictionary modelState) { - var valueEditor = editor.GetValueEditor(property.DataType.Configuration); - foreach (var r in valueEditor.Validate(postedValue, property.IsRequired, property.ValidationRegExp)) + if (property is null) throw new ArgumentNullException(nameof(property)); + if (property.DataType is null) throw new InvalidOperationException($"{nameof(property)}.{nameof(property.DataType)} cannot be null"); + + foreach (var validationResult in PropertyValidationService.ValidatePropertyValue( + _textService, editor, property.DataType, postedValue, property.IsRequired, + property.ValidationRegExp, property.IsRequiredMessage, property.ValidationRegExpMessage)) { - // If we've got custom error messages, we'll replace the default ones that will have been applied in the call to Validate(). - if (property.IsRequired && !string.IsNullOrWhiteSpace(property.IsRequiredMessage) && requiredDefaultMessages.Contains(r.ErrorMessage, StringComparer.OrdinalIgnoreCase)) - { - r.ErrorMessage = property.IsRequiredMessage; - } - - if (!string.IsNullOrWhiteSpace(property.ValidationRegExp) && !string.IsNullOrWhiteSpace(property.ValidationRegExpMessage) && formatDefaultMessages.Contains(r.ErrorMessage, StringComparer.OrdinalIgnoreCase)) - { - r.ErrorMessage = property.ValidationRegExpMessage; - } - - modelState.AddPropertyError(r, property.Alias, property.Culture, property.Segment); + AddPropertyError(model, modelWithProperties, editor, property, validationResult, modelState); } } + + protected virtual void AddPropertyError( + TModelSave model, + TModelWithProperties modelWithProperties, + IDataEditor editor, + ContentPropertyDto property, + ValidationResult validationResult, + ModelStateDictionary modelState) + { + modelState.AddPropertyError(validationResult, property.Alias, property.Culture, property.Segment); + } + } } diff --git a/src/Umbraco.Web/Editors/Filters/ContentSaveModelValidator.cs b/src/Umbraco.Web/Editors/Filters/ContentSaveModelValidator.cs index 39bd6ab0f4..f9d7203d12 100644 --- a/src/Umbraco.Web/Editors/Filters/ContentSaveModelValidator.cs +++ b/src/Umbraco.Web/Editors/Filters/ContentSaveModelValidator.cs @@ -1,5 +1,8 @@ -using Umbraco.Core.Logging; +using System.ComponentModel.DataAnnotations; +using System.Web.Http.ModelBinding; +using Umbraco.Core.Logging; using Umbraco.Core.Models; +using Umbraco.Core.PropertyEditors; using Umbraco.Core.Services; using Umbraco.Web.Models.ContentEditing; @@ -13,5 +16,30 @@ namespace Umbraco.Web.Editors.Filters public ContentSaveModelValidator(ILogger logger, IUmbracoContextAccessor umbracoContextAccessor, ILocalizedTextService textService) : base(logger, umbracoContextAccessor, textService) { } + + protected override void AddPropertyError(ContentItemSave model, ContentVariantSave modelWithProperties, IDataEditor editor, ContentPropertyDto property, ValidationResult validationResult, ModelStateDictionary modelState) + { + // Original idea: See if we can build up the JSON + JSON Path + // SD: I'm just keeping these notes here for later just to remind myself that we might want to take into account the tab number in validation + // which we might be able to get in the PropertyValidationService anyways? + + // Create a JSON + JSON Path key, see https://gist.github.com/Shazwazza/ad9fcbdb0fdacff1179a9eed88393aa6 + + //var json = new PropertyError + //{ + // Culture = property.Culture, + // Segment = property.Segment + //}; + + // TODO: Hrm, we can't get the tab index without a reference to the content type itself! the IContent doesn't contain a reference to groups/indexes + // BUT! I think it contains a reference to the group alias so we could use JSON Path for a group alias instead of index like: + // .tabs[?(@.alias=='Content')] + //var tabIndex = ?? + + //var jsonPath = "$.variants[0].tabs[0].properties[?(@.alias=='title')].value[0]"; + + base.AddPropertyError(model, modelWithProperties, editor, property, validationResult, modelState); + } + } } diff --git a/src/Umbraco.Web/ModelStateExtensions.cs b/src/Umbraco.Web/ModelStateExtensions.cs index 10b1b5dadd..718094e6b5 100644 --- a/src/Umbraco.Web/ModelStateExtensions.cs +++ b/src/Umbraco.Web/ModelStateExtensions.cs @@ -1,4 +1,5 @@ -using System; +using Newtonsoft.Json; +using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Linq; @@ -6,6 +7,7 @@ using System.Web.Mvc; using Umbraco.Core; using Umbraco.Core.Models; using Umbraco.Core.Services; +using Umbraco.Web.PropertyEditors.Validation; namespace Umbraco.Web { @@ -51,13 +53,26 @@ namespace Umbraco.Web internal static void AddPropertyError(this System.Web.Http.ModelBinding.ModelStateDictionary modelState, ValidationResult result, string propertyAlias, string culture = "", string segment = "") { - if (culture == null) - culture = ""; - modelState.AddValidationError(result, "_Properties", propertyAlias, + + var propValidationResult = new PropertyValidationResult(result); + + var keyParts = new[] + { + "_Properties", + propertyAlias, //if the culture is null, we'll add the term 'invariant' as part of the key culture.IsNullOrWhiteSpace() ? "invariant" : culture, // if the segment is null, we'll add the term 'null' as part of the key - segment.IsNullOrWhiteSpace() ? "null" : segment); + segment.IsNullOrWhiteSpace() ? "null" : segment + }; + + var key = string.Join(".", keyParts); + + modelState.AddModelError( + key, + JsonConvert.SerializeObject( + propValidationResult, + new ValidationResultConverter())); } /// @@ -236,5 +251,6 @@ namespace Umbraco.Web }; } + } } diff --git a/src/Umbraco.Web/Models/ContentEditing/ContentItemSave.cs b/src/Umbraco.Web/Models/ContentEditing/ContentItemSave.cs index 4dbbb1385a..aace4645dd 100644 --- a/src/Umbraco.Web/Models/ContentEditing/ContentItemSave.cs +++ b/src/Umbraco.Web/Models/ContentEditing/ContentItemSave.cs @@ -14,7 +14,7 @@ namespace Umbraco.Web.Models.ContentEditing [DataContract(Name = "content", Namespace = "")] public class ContentItemSave : IContentSave { - protected ContentItemSave() + public ContentItemSave() { UploadedFiles = new List(); Variants = new List(); diff --git a/src/Umbraco.Web/Models/Mapping/ContentPropertyBasicMapper.cs b/src/Umbraco.Web/Models/Mapping/ContentPropertyBasicMapper.cs index cf1bc3c253..2e035430df 100644 --- a/src/Umbraco.Web/Models/Mapping/ContentPropertyBasicMapper.cs +++ b/src/Umbraco.Web/Models/Mapping/ContentPropertyBasicMapper.cs @@ -44,6 +44,9 @@ namespace Umbraco.Web.Models.Mapping property.PropertyType.PropertyEditorAlias); editor = _propertyEditors[Constants.PropertyEditors.Aliases.Label]; + + if (editor == null) + throw new InvalidOperationException($"Could not resolve the property editor {Constants.PropertyEditors.Aliases.Label}"); } dest.Id = property.Id; diff --git a/src/Umbraco.Web/PropertyEditors/BlockEditorPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/BlockEditorPropertyEditor.cs index b32a91c058..db28b55ee8 100644 --- a/src/Umbraco.Web/PropertyEditors/BlockEditorPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/BlockEditorPropertyEditor.cs @@ -86,75 +86,24 @@ namespace Umbraco.Web.PropertyEditors } } - internal class BlockEditorValidator : IValueValidator + internal class BlockEditorValidator : ComplexEditorValidator { - private readonly PropertyEditorCollection _propertyEditors; - private readonly IDataTypeService _dataTypeService; private readonly BlockEditorValues _blockEditorValues; - public BlockEditorValidator(PropertyEditorCollection propertyEditors, IDataTypeService dataTypeService, BlockEditorValues blockEditorValues) + public BlockEditorValidator(BlockEditorValues blockEditorValues, PropertyEditorCollection propertyEditors, IDataTypeService dataTypeService, ILocalizedTextService textService) : base(propertyEditors, dataTypeService, textService) { - _propertyEditors = propertyEditors; - _dataTypeService = dataTypeService; _blockEditorValues = blockEditorValues; } - public IEnumerable Validate(object rawValue, string valueType, object dataTypeConfiguration) + protected override IEnumerable GetElementsFromValue(object value) { - var validationResults = new List(); - - foreach (var row in _blockEditorValues.GetPropertyValues(rawValue, out _)) + foreach (var row in _blockEditorValues.GetPropertyValues(value, out _)) { if (row.PropType == null) continue; - var config = _dataTypeService.GetDataType(row.PropType.DataTypeId).Configuration; - var propertyEditor = _propertyEditors[row.PropType.PropertyEditorAlias]; - if (propertyEditor == null) continue; - - foreach (var validator in propertyEditor.GetValueEditor().Validators) - { - foreach (var result in validator.Validate(row.JsonRowValue[row.PropKey], propertyEditor.GetValueEditor().ValueType, config)) - { - result.ErrorMessage = "Item " + (row.RowIndex + 1) + " '" + row.PropType.Name + "' " + result.ErrorMessage; - validationResults.Add(result); - } - } - - // Check mandatory - if (row.PropType.Mandatory) - { - if (row.JsonRowValue[row.PropKey] == null) - { - var message = string.IsNullOrWhiteSpace(row.PropType.MandatoryMessage) - ? $"'{row.PropType.Name}' cannot be null" - : row.PropType.MandatoryMessage; - validationResults.Add(new ValidationResult($"Item {(row.RowIndex + 1)}: {message}", new[] { row.PropKey })); - } - else if (row.JsonRowValue[row.PropKey].ToString().IsNullOrWhiteSpace() || (row.JsonRowValue[row.PropKey].Type == JTokenType.Array && !row.JsonRowValue[row.PropKey].HasValues)) - { - var message = string.IsNullOrWhiteSpace(row.PropType.MandatoryMessage) - ? $"'{row.PropType.Name}' cannot be empty" - : row.PropType.MandatoryMessage; - validationResults.Add(new ValidationResult($"Item {(row.RowIndex + 1)}: {message}", new[] { row.PropKey })); - } - } - - // Check regex - if (!row.PropType.ValidationRegExp.IsNullOrWhiteSpace() - && row.JsonRowValue[row.PropKey] != null && !row.JsonRowValue[row.PropKey].ToString().IsNullOrWhiteSpace()) - { - var regex = new Regex(row.PropType.ValidationRegExp); - if (!regex.IsMatch(row.JsonRowValue[row.PropKey].ToString())) - { - var message = string.IsNullOrWhiteSpace(row.PropType.ValidationRegExpMessage) - ? $"'{row.PropType.Name}' is invalid, it does not match the correct pattern" - : row.PropType.ValidationRegExpMessage; - validationResults.Add(new ValidationResult($"Item {(row.RowIndex + 1)}: {message}", new[] { row.PropKey })); - } - } + var val = row.JsonRowValue[row.PropKey]; + yield return new ElementTypeValidationModel(val, row.PropType); } - - return validationResults; } } diff --git a/src/Umbraco.Web/PropertyEditors/BlockListPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/BlockListPropertyEditor.cs index 3f8288b0ac..8a639e2932 100644 --- a/src/Umbraco.Web/PropertyEditors/BlockListPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/BlockListPropertyEditor.cs @@ -34,6 +34,7 @@ namespace Umbraco.Web.PropertyEditors #region IBlockEditorDataHelper + // TODO: Rename this we don't want to use the name "Helper" private class DataHelper : IBlockEditorDataHelper { public IEnumerable GetBlockReferences(JObject layout) diff --git a/src/Umbraco.Web/PropertyEditors/ComplexEditorValidator.cs b/src/Umbraco.Web/PropertyEditors/ComplexEditorValidator.cs new file mode 100644 index 0000000000..4e9dceed0d --- /dev/null +++ b/src/Umbraco.Web/PropertyEditors/ComplexEditorValidator.cs @@ -0,0 +1,73 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using Umbraco.Core; +using Umbraco.Core.Models; +using Umbraco.Core.PropertyEditors; +using Umbraco.Core.Services; +using Umbraco.Web.PropertyEditors.Validation; + +namespace Umbraco.Web.PropertyEditors +{ + /// + /// Used to validate complex editors that contain nested editors + /// + public abstract class ComplexEditorValidator : IValueValidator + { + private readonly PropertyValidationService _propertyValidationService; + + public ComplexEditorValidator(PropertyEditorCollection propertyEditors, IDataTypeService dataTypeService, ILocalizedTextService textService) + { + _propertyValidationService = new PropertyValidationService(propertyEditors, dataTypeService, textService); + } + + public IEnumerable Validate(object value, string valueType, object dataTypeConfiguration) + { + var elements = GetElementsFromValue(value); + var rowResults = GetNestedValidationResults(elements).ToList(); + + return rowResults.Count > 0 + ? new NestedValidationResults(rowResults).Yield() + : Enumerable.Empty(); + } + + protected abstract IEnumerable GetElementsFromValue(object value); + + /// + /// Return a nested validation result per row + /// + /// + /// + protected IEnumerable GetNestedValidationResults(IEnumerable elements) + { + foreach (var row in elements) + { + var nestedValidation = new List(); + + foreach(var validationResult in _propertyValidationService.ValidatePropertyValue(row.PropertyType, row.PostedValue)) + { + nestedValidation.Add(validationResult); + } + + if (nestedValidation.Count > 0) + { + yield return new NestedValidationResults(nestedValidation); + } + } + } + + public class ElementTypeValidationModel + { + public ElementTypeValidationModel(object postedValue, PropertyType propertyType) + { + PostedValue = postedValue ?? throw new ArgumentNullException(nameof(postedValue)); + PropertyType = propertyType ?? throw new ArgumentNullException(nameof(propertyType)); + } + + public object PostedValue { get; } + public PropertyType PropertyType { get; } + + } + } +} diff --git a/src/Umbraco.Web/PropertyEditors/NestedContentPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/NestedContentPropertyEditor.cs index 564630c574..61a48835de 100644 --- a/src/Umbraco.Web/PropertyEditors/NestedContentPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/NestedContentPropertyEditor.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Linq; using System.Text.RegularExpressions; +using Microsoft.CodeAnalysis.CSharp.Syntax; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using Umbraco.Core; @@ -13,9 +14,11 @@ using Umbraco.Core.Models; using Umbraco.Core.Models.Editors; using Umbraco.Core.PropertyEditors; using Umbraco.Core.Services; +using Umbraco.Web.PropertyEditors.Validation; namespace Umbraco.Web.PropertyEditors { + /// /// Represents a nested content property editor. /// @@ -31,14 +34,20 @@ namespace Umbraco.Web.PropertyEditors private readonly Lazy _propertyEditors; private readonly IDataTypeService _dataTypeService; private readonly IContentTypeService _contentTypeService; + private readonly ILocalizedTextService _localizedTextService; internal const string ContentTypeAliasPropertyKey = "ncContentTypeAlias"; + [Obsolete("Use the constructor specifying all parameters instead")] public NestedContentPropertyEditor(ILogger logger, Lazy propertyEditors, IDataTypeService dataTypeService, IContentTypeService contentTypeService) + : this(logger, propertyEditors, dataTypeService, contentTypeService, Current.Services.TextService) { } + + public NestedContentPropertyEditor(ILogger logger, Lazy propertyEditors, IDataTypeService dataTypeService, IContentTypeService contentTypeService, ILocalizedTextService localizedTextService) : base (logger) { _propertyEditors = propertyEditors; _dataTypeService = dataTypeService; _contentTypeService = contentTypeService; + _localizedTextService = localizedTextService; } // has to be lazy else circular dep in ctor @@ -52,7 +61,7 @@ namespace Umbraco.Web.PropertyEditors #region Value Editor - protected override IDataValueEditor CreateValueEditor() => new NestedContentPropertyValueEditor(Attribute, PropertyEditors, _dataTypeService, _contentTypeService); + protected override IDataValueEditor CreateValueEditor() => new NestedContentPropertyValueEditor(Attribute, PropertyEditors, _dataTypeService, _contentTypeService, _localizedTextService); internal class NestedContentPropertyValueEditor : DataValueEditor, IDataValueReference { @@ -60,13 +69,13 @@ namespace Umbraco.Web.PropertyEditors private readonly IDataTypeService _dataTypeService; private readonly NestedContentValues _nestedContentValues; - public NestedContentPropertyValueEditor(DataEditorAttribute attribute, PropertyEditorCollection propertyEditors, IDataTypeService dataTypeService, IContentTypeService contentTypeService) + public NestedContentPropertyValueEditor(DataEditorAttribute attribute, PropertyEditorCollection propertyEditors, IDataTypeService dataTypeService, IContentTypeService contentTypeService, ILocalizedTextService textService) : base(attribute) { _propertyEditors = propertyEditors; _dataTypeService = dataTypeService; _nestedContentValues = new NestedContentValues(contentTypeService); - Validators.Add(new NestedContentValidator(propertyEditors, dataTypeService, _nestedContentValues)); + Validators.Add(new NestedContentValidator(_nestedContentValues, propertyEditors, dataTypeService, textService)); } /// @@ -255,75 +264,25 @@ namespace Umbraco.Web.PropertyEditors } } - internal class NestedContentValidator : IValueValidator + internal class NestedContentValidator : ComplexEditorValidator { - private readonly PropertyEditorCollection _propertyEditors; - private readonly IDataTypeService _dataTypeService; private readonly NestedContentValues _nestedContentValues; - public NestedContentValidator(PropertyEditorCollection propertyEditors, IDataTypeService dataTypeService, NestedContentValues nestedContentValues) + public NestedContentValidator(NestedContentValues nestedContentValues, PropertyEditorCollection propertyEditors, IDataTypeService dataTypeService, ILocalizedTextService textService) + : base(propertyEditors, dataTypeService, textService) { - _propertyEditors = propertyEditors; - _dataTypeService = dataTypeService; _nestedContentValues = nestedContentValues; } - public IEnumerable Validate(object rawValue, string valueType, object dataTypeConfiguration) + protected override IEnumerable GetElementsFromValue(object value) { - var validationResults = new List(); - - foreach(var row in _nestedContentValues.GetPropertyValues(rawValue, out _)) + foreach (var row in _nestedContentValues.GetPropertyValues(value, out _)) { if (row.PropType == null) continue; - var config = _dataTypeService.GetDataType(row.PropType.DataTypeId).Configuration; - var propertyEditor = _propertyEditors[row.PropType.PropertyEditorAlias]; - if (propertyEditor == null) continue; - - foreach (var validator in propertyEditor.GetValueEditor().Validators) - { - foreach (var result in validator.Validate(row.JsonRowValue[row.PropKey], propertyEditor.GetValueEditor().ValueType, config)) - { - result.ErrorMessage = "Item " + (row.RowIndex + 1) + " '" + row.PropType.Name + "' " + result.ErrorMessage; - validationResults.Add(result); - } - } - - // Check mandatory - if (row.PropType.Mandatory) - { - if (row.JsonRowValue[row.PropKey] == null) - { - var message = string.IsNullOrWhiteSpace(row.PropType.MandatoryMessage) - ? $"'{row.PropType.Name}' cannot be null" - : row.PropType.MandatoryMessage; - validationResults.Add(new ValidationResult($"Item {(row.RowIndex + 1)}: {message}", new[] { row.PropKey })); - } - else if (row.JsonRowValue[row.PropKey].ToString().IsNullOrWhiteSpace() || (row.JsonRowValue[row.PropKey].Type == JTokenType.Array && !row.JsonRowValue[row.PropKey].HasValues)) - { - var message = string.IsNullOrWhiteSpace(row.PropType.MandatoryMessage) - ? $"'{row.PropType.Name}' cannot be empty" - : row.PropType.MandatoryMessage; - validationResults.Add(new ValidationResult($"Item {(row.RowIndex + 1)}: {message}", new[] { row.PropKey })); - } - } - - // Check regex - if (!row.PropType.ValidationRegExp.IsNullOrWhiteSpace() - && row.JsonRowValue[row.PropKey] != null && !row.JsonRowValue[row.PropKey].ToString().IsNullOrWhiteSpace()) - { - var regex = new Regex(row.PropType.ValidationRegExp); - if (!regex.IsMatch(row.JsonRowValue[row.PropKey].ToString())) - { - var message = string.IsNullOrWhiteSpace(row.PropType.ValidationRegExpMessage) - ? $"'{row.PropType.Name}' is invalid, it does not match the correct pattern" - : row.PropType.ValidationRegExpMessage; - validationResults.Add(new ValidationResult($"Item {(row.RowIndex + 1)}: {message}", new[] { row.PropKey })); - } - } + var val = row.JsonRowValue[row.PropKey]; + yield return new ElementTypeValidationModel(val, row.PropType); } - - return validationResults; } } diff --git a/src/Umbraco.Web/PropertyEditors/Validation/NestedValidationResults.cs b/src/Umbraco.Web/PropertyEditors/Validation/NestedValidationResults.cs new file mode 100644 index 0000000000..86462fe333 --- /dev/null +++ b/src/Umbraco.Web/PropertyEditors/Validation/NestedValidationResults.cs @@ -0,0 +1,22 @@ +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; + +namespace Umbraco.Web.PropertyEditors.Validation +{ + /// + /// Custom that contains a list of nested validation results + /// + /// + /// For example, each represents validation results for a row in Nested Content + /// + public class NestedValidationResults : ValidationResult + { + public NestedValidationResults(IEnumerable nested) + : base(string.Empty) + { + ValidationResults = new List(nested); + } + + public IList ValidationResults { get; } + } +} diff --git a/src/Umbraco.Web/PropertyEditors/Validation/PropertyValidationResult.cs b/src/Umbraco.Web/PropertyEditors/Validation/PropertyValidationResult.cs new file mode 100644 index 0000000000..f2c92e441e --- /dev/null +++ b/src/Umbraco.Web/PropertyEditors/Validation/PropertyValidationResult.cs @@ -0,0 +1,27 @@ +using System.ComponentModel.DataAnnotations; + +namespace Umbraco.Web.PropertyEditors.Validation +{ + /// + /// Custom for content properties + /// + /// + /// This clones the original result and then ensures the nested result if it's the correct type + /// + public class PropertyValidationResult : ValidationResult + { + public PropertyValidationResult(ValidationResult nested) + : base(nested.ErrorMessage, nested.MemberNames) + { + NestedResuls = nested as NestedValidationResults; + } + + /// + /// Nested validation results for the content property + /// + /// + /// There can be nested results for complex editors that contain other editors + /// + public NestedValidationResults NestedResuls { get; } + } +} diff --git a/src/Umbraco.Web/PropertyEditors/Validation/ValidationResultConverter.cs b/src/Umbraco.Web/PropertyEditors/Validation/ValidationResultConverter.cs new file mode 100644 index 0000000000..a03528628b --- /dev/null +++ b/src/Umbraco.Web/PropertyEditors/Validation/ValidationResultConverter.cs @@ -0,0 +1,71 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using Newtonsoft.Json.Serialization; +using System; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Umbraco.Core; + +namespace Umbraco.Web.PropertyEditors.Validation +{ + /// + /// Custom json converter for and + /// + internal class ValidationResultConverter : JsonConverter + { + public override bool CanConvert(Type objectType) => typeof(ValidationResult).IsAssignableFrom(objectType); + + public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) + { + throw new NotImplementedException(); + } + + public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) + { + var camelCaseSerializer = new JsonSerializer + { + ContractResolver = new CamelCasePropertyNamesContractResolver(), + DefaultValueHandling = DefaultValueHandling.Ignore + }; + foreach (var c in serializer.Converters) + camelCaseSerializer.Converters.Add(c); + + var validationResult = (ValidationResult)value; + + if (validationResult is NestedValidationResults nestedResult) + { + if (nestedResult.ValidationResults.Count > 0) + { + var validationItems = JToken.FromObject(nestedResult.ValidationResults, camelCaseSerializer); + validationItems.WriteTo(writer); + } + } + else + { + var jo = new JObject(); + + if (!validationResult.ErrorMessage.IsNullOrWhiteSpace()) + { + jo.Add("errorMessage", JToken.FromObject(validationResult.ErrorMessage, camelCaseSerializer)); + } + + if (validationResult.MemberNames.Any()) + { + jo.Add("memberNames", JToken.FromObject(validationResult.MemberNames, camelCaseSerializer)); + } + + if (validationResult is PropertyValidationResult propertyValidationResult) + { + if (propertyValidationResult.NestedResuls?.ValidationResults.Count > 0) + { + jo.Add("nestedValidation", JToken.FromObject(propertyValidationResult.NestedResuls, camelCaseSerializer)); + } + } + + jo.WriteTo(writer); + } + } + } +} diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index 1fb37322dd..f1791908f9 100644 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -245,8 +245,12 @@ + + + + From 19a114ac2b09fa1ec0468ea46e1dbdbbb558e428 Mon Sep 17 00:00:00 2001 From: Shannon Date: Mon, 15 Jun 2020 23:22:29 +1000 Subject: [PATCH 198/377] fix build --- .../Models/Blocks/IBlockEditorDataHelper.cs | 1 + .../PropertyEditors/BlockEditorPropertyEditor.cs | 10 ++++++---- .../PropertyEditors/BlockListPropertyEditor.cs | 4 ++-- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/src/Umbraco.Core/Models/Blocks/IBlockEditorDataHelper.cs b/src/Umbraco.Core/Models/Blocks/IBlockEditorDataHelper.cs index 35bcaa49ab..3def72bf9c 100644 --- a/src/Umbraco.Core/Models/Blocks/IBlockEditorDataHelper.cs +++ b/src/Umbraco.Core/Models/Blocks/IBlockEditorDataHelper.cs @@ -6,6 +6,7 @@ namespace Umbraco.Core.Models.Blocks // TODO: Rename this, we don't want to use the name "Helper" // TODO: What is this? This requires code docs // TODO: This is not used publicly at all - therefore it probably shouldn't be public + // TODO: These could easily be abstract methods on the underlying BlockEditorPropertyEditor instead public interface IBlockEditorDataHelper { // TODO: Does this abstraction need a reference to JObject? Maybe it does but ideally it doesn't diff --git a/src/Umbraco.Web/PropertyEditors/BlockEditorPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/BlockEditorPropertyEditor.cs index db28b55ee8..c2f8be359f 100644 --- a/src/Umbraco.Web/PropertyEditors/BlockEditorPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/BlockEditorPropertyEditor.cs @@ -23,14 +23,16 @@ namespace Umbraco.Web.PropertyEditors public const string ContentTypeKeyPropertyKey = "contentTypeKey"; public const string UdiPropertyKey = "udi"; private readonly IBlockEditorDataHelper _dataHelper; + private readonly ILocalizedTextService _localizedTextService; private readonly Lazy _propertyEditors; private readonly IDataTypeService _dataTypeService; private readonly IContentTypeService _contentTypeService; - public BlockEditorPropertyEditor(ILogger logger, Lazy propertyEditors, IDataTypeService dataTypeService, IContentTypeService contentTypeService, IBlockEditorDataHelper dataHelper) + public BlockEditorPropertyEditor(ILogger logger, Lazy propertyEditors, IDataTypeService dataTypeService, IContentTypeService contentTypeService, IBlockEditorDataHelper dataHelper, ILocalizedTextService localizedTextService) : base(logger) { _dataHelper = dataHelper; + _localizedTextService = localizedTextService; _propertyEditors = propertyEditors; _dataTypeService = dataTypeService; _contentTypeService = contentTypeService; @@ -41,7 +43,7 @@ namespace Umbraco.Web.PropertyEditors #region Value Editor - protected override IDataValueEditor CreateValueEditor() => new BlockEditorPropertyValueEditor(Attribute, _dataHelper, PropertyEditors, _dataTypeService, _contentTypeService); + protected override IDataValueEditor CreateValueEditor() => new BlockEditorPropertyValueEditor(Attribute, _dataHelper, PropertyEditors, _dataTypeService, _contentTypeService, _localizedTextService); internal class BlockEditorPropertyValueEditor : DataValueEditor, IDataValueReference { @@ -50,14 +52,14 @@ namespace Umbraco.Web.PropertyEditors private readonly IDataTypeService _dataTypeService; private readonly BlockEditorValues _blockEditorValues; - public BlockEditorPropertyValueEditor(DataEditorAttribute attribute, IBlockEditorDataHelper dataHelper, PropertyEditorCollection propertyEditors, IDataTypeService dataTypeService, IContentTypeService contentTypeService) + public BlockEditorPropertyValueEditor(DataEditorAttribute attribute, IBlockEditorDataHelper dataHelper, PropertyEditorCollection propertyEditors, IDataTypeService dataTypeService, IContentTypeService contentTypeService, ILocalizedTextService textService) : base(attribute) { _dataHelper = dataHelper; _propertyEditors = propertyEditors; _dataTypeService = dataTypeService; _blockEditorValues = new BlockEditorValues(dataHelper, contentTypeService); - Validators.Add(new BlockEditorValidator(propertyEditors, dataTypeService, _blockEditorValues)); + Validators.Add(new BlockEditorValidator(_blockEditorValues, propertyEditors, dataTypeService, textService)); } public IEnumerable GetReferences(object value) diff --git a/src/Umbraco.Web/PropertyEditors/BlockListPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/BlockListPropertyEditor.cs index 8a639e2932..d4c5e88c68 100644 --- a/src/Umbraco.Web/PropertyEditors/BlockListPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/BlockListPropertyEditor.cs @@ -22,8 +22,8 @@ namespace Umbraco.Web.PropertyEditors Icon = "icon-thumbnail-list")] public class BlockListPropertyEditor : BlockEditorPropertyEditor { - public BlockListPropertyEditor(ILogger logger, Lazy propertyEditors, IDataTypeService dataTypeService, IContentTypeService contentTypeService) - : base(logger, propertyEditors, dataTypeService, contentTypeService, new DataHelper()) + public BlockListPropertyEditor(ILogger logger, Lazy propertyEditors, IDataTypeService dataTypeService, IContentTypeService contentTypeService, ILocalizedTextService localizedTextService) + : base(logger, propertyEditors, dataTypeService, contentTypeService, new DataHelper(), localizedTextService) { } #region Pre Value Editor From 48d47060115d44c0cf2493f2e606c3e75c0e8fd3 Mon Sep 17 00:00:00 2001 From: Shannon Date: Tue, 16 Jun 2020 00:08:33 +1000 Subject: [PATCH 199/377] notes --- src/Umbraco.Tests/Web/Validation/ContentModelValidatorTests.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Umbraco.Tests/Web/Validation/ContentModelValidatorTests.cs b/src/Umbraco.Tests/Web/Validation/ContentModelValidatorTests.cs index cbc4152c17..782091ee80 100644 --- a/src/Umbraco.Tests/Web/Validation/ContentModelValidatorTests.cs +++ b/src/Umbraco.Tests/Web/Validation/ContentModelValidatorTests.cs @@ -103,6 +103,8 @@ namespace Umbraco.Tests.Web.Validation var content = MockedContent.CreateTextpageContent(_contentType, "test", -1); + // TODO: Ok now test with a 3rd/4th level complex nested editor + const string complexValue = @"[{ ""key"": ""c8df5136-d606-41f0-9134-dea6ae0c2fd9"", ""name"": ""Hello world"", From bfa07377b44aaf7043d5f3c47e1793fc744decb8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Mon, 15 Jun 2020 22:27:56 +0200 Subject: [PATCH 200/377] Moving BlockEditorModelObject into its own file for Documentation purpose. Same goes for renaming BlockModel to BlockObject. and a lot of documentation written. --- .../common/services/blockeditor.service.js | 570 +------------- .../blockeditormodelobject.service.js | 724 ++++++++++++++++++ .../blocklist/blocklist.component.js | 51 +- .../services/block-editor-service.spec.js | 50 +- 4 files changed, 794 insertions(+), 601 deletions(-) create mode 100644 src/Umbraco.Web.UI.Client/src/common/services/blockeditormodelobject.service.js 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 1a44190163..36e378a612 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 @@ -1,8 +1,10 @@ /** - @ngdoc service + * @ngdoc service * @name umbraco.services.blockEditorService * * @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. * @@ -13,571 +15,37 @@ * *
        *     modelObject = blockEditorService.createModelObject(vm.model.value, vm.model.editor, vm.model.config.blocks, $scope);
      - *     modelObject.loadScaffolding().then(onLoaded);
      + *     modelObject.load().then(onLoaded);
        * 
      * - * ####Use the Model Object to retrive Content Models for the blocks you want to edit. Content Models contains all the data about the properties of that content and as well handles syncroniztion so your data is always up-to-date. - * - *
      - *     // Store a reference to the layout model, because we need to maintain this model.
      - *     var layout = modelObject.getLayout();
        * 
      - *     // maps layout entries to editor friendly models aka. BlockModels.
      - *     layout.forEach(entry => {
      - *         var block = modelObject.getBlockModel(entry);
      - *         // If this entry was not supported by our property-editor it would return 'null'.
      - *         if(block !== null) {
      - *             // store this block in an array we use in our view.
      - *             vm.myViewModelOfBlocks.push(block);
      - *         }
      - *     });
      - *
      - * 
      - * - * ####Use the Model Object to retrive Content Models for the blocks you want to edit. Content Models contains all the data about the properties of that content and as well handles syncroniztion so your data is always up-to-date. - * - *
      - * function addNewBlock(index, contentTypeKey) {
      - * 
      - *     // Create layout entry. (not added to property model jet.)
      - *     var layoutEntry = modelObject.create(contentTypeKey);
      - *     if (layoutEntry === null) {
      - *         return false;
      - *     }
      - * 
      - *     // make block model
      - *     var blockModel = getBlockModel(layoutEntry);
      - *     if (blockModel === null) {
      - *         return false;
      - *     }
      - *     
      - *     // If we reach this line, we are good to add the layoutEntry and blockModel to layout model and view model.
      - *     // add layout entry at the decired location in layout, depending on your layout structure.
      - *     layout.splice(index, 0, layoutEntry);
      - * 
      - *     // apply block model at decired location in our model used for the view.
      - *     vm.myViewModelOfBlocks.splice(index, 0, blockModel);
      - *     
      - *     // at this stage we know everything went well.
      - *     return true;
      - * }
      - * 
      + * See {@link umbraco.services.BlockEditorModelObject BlockEditorModelObject} for more samples. * */ (function () { 'use strict'; - function blockEditorService($interpolate, udiService, contentResource) { - + function blockEditorService(blockEditorModelObject) { /** - * Simple mapping from property model content entry to editing model, - * needs to stay simple to avoid deep watching. + * @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 + * @return {BlockEditorModelObject} A instance of the BlockEditorModelObject class. */ - function mapToElementModel(elementModel, dataModel) { - - var variant = elementModel.variants[0]; - - for (var t = 0; t < variant.tabs.length; t++) { - var tab = variant.tabs[t]; - - for (var p = 0; p < tab.properties.length; p++) { - var prop = tab.properties[p]; - if (dataModel[prop.alias]) { - prop.value = dataModel[prop.alias]; - } - } - } - } - - /** - * Simple mapping from elementModel to property model content entry, - * needs to stay simple to avoid deep watching. - */ - function mapToPropertyModel(elementModel, dataModel) { - - var variant = elementModel.variants[0]; - - for (var t = 0; t < variant.tabs.length; t++) { - var tab = variant.tabs[t]; - - for (var p = 0; p < tab.properties.length; p++) { - var prop = tab.properties[p]; - if (prop.value) { - dataModel[prop.alias] = prop.value; - } - } - } - } - - /** - * Map property values from an ElementModel to another ElementModel. - * Used to tricker watchers for synchronization. - * @param {Object} fromModel ElementModel to recive property values from. - * @param {Object} toModel ElementModel to recive property values from. - */ - function mapElementValues(fromModel, toModel) { - if (!fromModel || !fromModel.variants) { - toModel.variants = null; - return; - } - if (!fromModel.variants.length) { - toModel.variants = []; - return; - } - - var fromVariant = fromModel.variants[0]; - if (!fromVariant) { - toModel.variants = [null]; - return; - } - - var toVariant = toModel.variants[0]; - - for (var t = 0; t < fromVariant.tabs.length; t++) { - var fromTab = fromVariant.tabs[t]; - var toTab = toVariant.tabs[t]; - - for (var p = 0; p < fromTab.properties.length; p++) { - var fromProp = fromTab.properties[p]; - var toProp = toTab.properties[p]; - toProp.value = fromProp.value; - } - } - } - - - /** - * Generate label for Block, uses either the labelInterpolator or falls back to the contentTypeName. - * @param {Object} blockModel BlockModel to recive data values from. - */ - function getBlockLabel(blockModel) { - if(blockModel.labelInterpolator !== undefined) { - // We are just using the data model, since its a key/value object that is live synced. (if we need to add additional vars, we could make a shallow copy and apply those.) - return blockModel.labelInterpolator(blockModel.data); - } - return blockModel.content.contentTypeName; - } - - - /** - * Used to add watchers on all properties in a content or settings model - */ - function addWatchers(blockModel, isolatedScope, forSettings) { - var model = forSettings ? blockModel.settings : blockModel.content; - if (!model || !model.variants || !model.variants.length) { return; } - - // Start watching each property value. - var variant = model.variants[0]; - var field = forSettings ? "settings" : "content"; - var watcherCreator = forSettings ? createSettingsModelPropWatcher : createDataModelPropWatcher; - for (var t = 0; t < variant.tabs.length; t++) { - var tab = variant.tabs[t]; - for (var p = 0; p < tab.properties.length; p++) { - var prop = tab.properties[p]; - - // Watch value of property since this is the only value we want to keep synced. - // Do notice that it is not performing a deep watch, meaning that we are only watching primatives and changes directly to the object of property-value. - // But we like to sync non-primative values as well! Yes, and this does happen, just not through this code, but through the nature of JavaScript. - // Non-primative values act as references to the same data and are therefor synced. - blockModel.watchers.push(isolatedScope.$watch("blockModels._" + blockModel.key + "." + field + ".variants[0].tabs[" + t + "].properties[" + p + "].value", watcherCreator(blockModel, prop))); - } - } - if (blockModel.watchers.length === 0) { - // If no watcher where created, it means we have no properties to watch. This means that nothing will activate our generate the label, since its only triggered by watchers. - blockModel.updateLabel(); - } - } - - /** - * Used to create a scoped watcher for a content property on a blockModel. - */ - function createDataModelPropWatcher(blockModel, prop) { - return function() { - // sync data: - blockModel.data[prop.alias] = prop.value; - - blockModel.updateLabel(); - } - } - - /** - * Used to create a scoped watcher for a settings property on a blockModel. - */ - function createSettingsModelPropWatcher(blockModel, prop) { - return function() { - // sync data: - blockModel.layout.settings[prop.alias] = prop.value; - } - } - - - /** - * Used to highlight unsupported properties for the user, changes unsupported properties into a unsupported-property. - */ - var notSupportedProperties = [ - "Umbraco.Tags", - "Umbraco.UploadField", - "Umbraco.ImageCropper", - "Umbraco.NestedContent" - ]; - function replaceUnsupportedProperties(scaffold) { - scaffold.variants.forEach((variant) => { - variant.tabs.forEach((tab) => { - tab.properties.forEach((property) => { - if (notSupportedProperties.indexOf(property.editor) !== -1) { - property.view = "notsupported"; - } - }); - }); - }); - return scaffold; - } - - - /** - * @ngdoc factory - * @name umbraco.factory.BlockEditorModelObject - * @description A model object used to handle Block Editor data. - **/ - function BlockEditorModelObject(propertyModelValue, propertyEditorAlias, blockConfigurations, propertyScope) { - - if (!propertyModelValue) { - throw new Error("propertyModelValue cannot be undefined, to ensure we keep the binding to the angular model we need minimum an empty object."); - } - - // ensure basic part of data-structure is in place: - this.value = propertyModelValue; - this.value.layout = this.value.layout || {}; - this.value.data = this.value.data || []; - - this.propertyEditorAlias = propertyEditorAlias; - this.blockConfigurations = blockConfigurations; - - this.scaffolds = []; - - this.watchers = []; - - this.isolatedScope = propertyScope.$new(true); - this.isolatedScope.blockModels = {}; - - this.isolatedScope.$on("$destroy", this.onDestroyed.bind(this)); - - }; - - BlockEditorModelObject.prototype = { - - /** - * Get block configuration object for a given contentTypeKey. - * @param {string} key contentTypeKey to recive the configuration model for. - * @returns {Object | null} Configuration model for the that specific block. Or ´null´ if the contentTypeKey isnt available in the current block configurations. - */ - getBlockConfiguration: function(key) { - return this.blockConfigurations.find(bc => bc.contentTypeKey === key); - }, - - /** - * Load the scaffolding models for the given configuration, these are needed to provide usefull models for each block. - * @param {Object} blockModel BlockModel to recive data values from. - * @returns {Promise} A Promise object which resolves when all scaffold models are loaded. - */ - loadScaffolding: function() { - var tasks = []; - - var scaffoldKeys = []; - - this.blockConfigurations.forEach(blockConfiguration => { - scaffoldKeys.push(blockConfiguration.contentTypeKey); - if (blockConfiguration.settingsElementTypeKey != null) { - scaffoldKeys.push(blockConfiguration.settingsElementTypeKey); - } - }); - - // removing dublicates. - scaffoldKeys = scaffoldKeys.filter((value, index, self) => self.indexOf(value) === index); - - scaffoldKeys.forEach((contentTypeKey => { - tasks.push(contentResource.getScaffoldByKey(-20, contentTypeKey).then(scaffold => { - this.scaffolds.push(replaceUnsupportedProperties(scaffold)); - })); - })); - - return Promise.all(tasks); - }, - - /** - * Retrive a list of aliases that are available for content of blocks in this property editor, does not contain aliases of block settings. - * @return {Array} array of strings representing alias. - */ - getAvailableAliasesForBlockContent: function() { - return this.blockConfigurations.map(blockConfiguration => this.getScaffoldFromKey(blockConfiguration.contentTypeKey).contentTypeAlias); - }, - - /** - * Retrive a list of available blocks, the list containing object with the confirugation model(blockConfigModel) and the element type model(elementTypeModel). - * The purpose of this data is to provide it for the Block Picker. - * @return {Array} array of objects representing available blocks, each object containing properties blockConfigModel and elementTypeModel. - */ - getAvailableBlocksForBlockPicker: function() { - - var blocks = []; - - this.blockConfigurations.forEach(blockConfiguration => { - var scaffold = this.getScaffoldFromKey(blockConfiguration.contentTypeKey); - if(scaffold) { - blocks.push({ - blockConfigModel: blockConfiguration, - elementTypeModel: scaffold.documentType - }); - } - }); - - return blocks; - }, - - /** - * Get scaffold model for a given contentTypeKey. - * @param {string} key contentTypeKey to recive the scaffold model for. - * @returns {Object | null} Scaffold model for the that content type. Or null if the scaffolding model dosnt exist in this context. - */ - getScaffoldFromKey: function(contentTypeKey) { - return this.scaffolds.find(o => o.contentTypeKey === contentTypeKey); - }, - - /** - * Get scaffold model for a given contentTypeAlias, used by clipboardService. - * @param {string} alias contentTypeAlias to recive the scaffold model for. - * @returns {Object | null} Scaffold model for the that content type. Or null if the scaffolding model dosnt exist in this context. - */ - getScaffoldFromAlias: function(contentTypeAlias) { - return this.scaffolds.find(o => o.contentTypeAlias === contentTypeAlias); - }, - - /** - * Retrieve editor friendly model of a block. - * BlockModel is a class instance which setups live syncronization of content and settings models back to the data of your property editor model. - * The returned object, named ´BlockModel´, contains several usefull models to make editing of this block happen. - * The ´BlockModel´ contains the following properties: - * - key {string}: runtime generated key, usefull for tracking of this object - * - content {Object}: Content model, the content type model for content merged with the content data of this block. - * - settings {Object}: Settings model, the content type model for settings merged with the settings data of this block. - * - config {Object}: A deep copy of the block configuration model. - * - label {string}: The label for this block. - * - updateLabel {Method}: Method to trigger an update of the label for this block. - * - data {Object}: A reference to the data object from your property editor model. - * - layout {Object}: A refernce to the layout entry from your property editor model. - * @param {Object} layoutEntry the layout entry object to build the block model from. - * @return {Object | null} The BlockModel for the given layout entry. Or null if data or configuration wasnt found for this block. - */ - getBlockModel: function(layoutEntry) { - - var udi = layoutEntry.udi; - - var dataModel = this._getDataByUdi(udi); - - if (dataModel === null) { - console.error("Couldnt find content model of " + udi) - return null; - } - - var blockConfiguration = this.getBlockConfiguration(dataModel.contentTypeKey); - - if (blockConfiguration === null) { - console.error("The block entry of "+udi+" is not begin initialized cause its contentTypeKey is not allowed for this PropertyEditor") - // This is not an allowed block type, therefor we return null; - return null; - } - - var contentScaffold = this.getScaffoldFromKey(blockConfiguration.contentTypeKey); - if(contentScaffold === null) { - return null; - } - - var blockModel = {}; - blockModel.key = String.CreateGuid().replace(/-/g, ""); - blockModel.config = Utilities.copy(blockConfiguration); - if (blockModel.config.label && blockModel.config.label !== "") { - blockModel.labelInterpolator = $interpolate(blockModel.config.label); - } - blockModel.__scope = this.isolatedScope; - blockModel.updateLabel = _.debounce(function () {this.__scope.$evalAsync(function() { - this.label = getBlockLabel(this); - }.bind(this))}.bind(blockModel), 10); - - // make basics from scaffold - blockModel.content = Utilities.copy(contentScaffold); - blockModel.content.udi = udi; - - mapToElementModel(blockModel.content, dataModel); - - blockModel.data = dataModel; - blockModel.layout = layoutEntry; - blockModel.watchers = []; - - if (blockConfiguration.settingsElementTypeKey) { - var settingsScaffold = this.getScaffoldFromKey(blockConfiguration.settingsElementTypeKey); - if (settingsScaffold === null) { - return null; - } - - // make basics from scaffold - blockModel.settings = Utilities.copy(settingsScaffold); - layoutEntry.settings = layoutEntry.settings || {}; - if (!layoutEntry.settings.key) { layoutEntry.settings.key = String.CreateGuid(); } - if (!layoutEntry.settings.contentTypeKey) { layoutEntry.settings.contentTypeKey = blockConfiguration.settingsElementTypeKey; } - mapToElementModel(blockModel.settings, layoutEntry.settings); - } else { - layoutEntry.settings = null; - } - - blockModel.retriveValuesFrom = function(content, settings) { - if (this.content !== null) { - mapElementValues(content, this.content); - } - if (this.config.settingsElementTypeKey !== null) { - mapElementValues(settings, this.settings); - } - } - - // Add blockModel to our isolated scope to enable watching its values: - this.isolatedScope.blockModels["_"+blockModel.key] = blockModel; - addWatchers(blockModel, this.isolatedScope); - addWatchers(blockModel, this.isolatedScope, true); - - return blockModel; - - }, - - /** - * Removes the data and destroys the Block Model. - * Notive this method does not remove the block from your layout, this will need to be handlede by the Property Editor since this services donst know about your layout structure. - * @param {Object} blockModel The BlockModel to be removed and destroyed. - */ - removeDataAndDestroyModel: function (blockModel) { - this.destroyBlockModel(blockModel); - this.removeDataByUdi(blockModel.content.udi); - }, - - /** - * Destroys the Block Model, but all data is kept. - * @param {Object} blockModel The BlockModel to be destroyed. - */ - destroyBlockModel: function(blockModel) { - - // remove property value watchers: - blockModel.watchers.forEach(w => { w(); }); - - // remove model from isolatedScope. - delete this.isolatedScope.blockModels[blockModel.key]; - - }, - - /** - * Retrieve the layout object from this specific property editor model. - * @return {Object} Layout object, structure depends on the model of your property editor. - */ - getLayout: function() { - if (!this.value.layout[this.propertyEditorAlias]) { - this.value.layout[this.propertyEditorAlias] = []; - } - return this.value.layout[this.propertyEditorAlias]; - }, - - /** - * Create a empty layout entry, notice the layout entry is not added to the property editors model layout object, since the layout sturcture depends on the property editor. - * @param {string} contentTypeKey, the contentTypeKey of the block you wish to create, if contentTypeKey is not avaiable in the block configuration then ´null´ will be returned. - * @return {Object | null} Layout entry object, to be inserted at a decired location in the layout object. Or null if contentTypeKey is unavaiaible. - */ - create: function(contentTypeKey) { - - var blockConfiguration = this.getBlockConfiguration(contentTypeKey); - if(blockConfiguration === null) { - return null; - } - - var entry = { - udi: this._createDataEntry(contentTypeKey) - } - - if (blockConfiguration.settingsElementTypeKey != null) { - entry.settings = { key: String.CreateGuid(), contentTypeKey: blockConfiguration.settingsElementTypeKey }; - } - - return entry; - }, - - /** - * Insert data from ElementType Model - * @return {Object | null} Layout entry object, to be inserted at a decired location in the layout object. Or ´null´ if the given ElementType isnt supported by the block configuration. - */ - createFromElementType: function(elementTypeDataModel) { - - elementTypeDataModel = Utilities.copy(elementTypeDataModel); - - var contentTypeKey = elementTypeDataModel.contentTypeKey; - - var layoutEntry = this.create(contentTypeKey); - if(layoutEntry === null) { - return null; - } - - var dataModel = this._getDataByUdi(layoutEntry.udi); - if(dataModel === null) { - return null; - } - - mapToPropertyModel(elementTypeDataModel, dataModel); - - return layoutEntry; - - }, - - // private - _createDataEntry: function(elementTypeKey) { - var content = { - contentTypeKey: elementTypeKey, - udi: udiService.create("element") - }; - this.value.data.push(content); - return content.udi; - }, - // private - _getDataByUdi: function(udi) { - return this.value.data.find(entry => entry.udi === udi) || null; - }, - - removeDataByUdi: function(udi) { - const index = this.value.data.findIndex(o => o.udi === udi); - if (index !== -1) { - this.value.data.splice(index, 1); - } - }, - - onDestroyed: function() { - - for (const key in this.isolatedScope.blockModels) { - this.destroyBlockModel(this.isolatedScope.blockModels[key]); - } - - delete this.value; - delete this.propertyEditorAlias; - delete this.blockConfigurations; - delete this.scaffolds; - delete this.watchers; - this.isolatedScope.$destroy(); - delete this.isolatedScope; - } + function createModelObject(propertyModelValue, propertyEditorAlias, blockConfigurations, propertyScope) { + return new blockEditorModelObject(propertyModelValue, propertyEditorAlias, blockConfigurations, propertyScope); } return { - /** - * Create a new Block Editor Model Object, used to deal with editing of the Block Editor Model. - * @return {BlockEditorModelObject} A instance of the BlockEditorModelObject class. - */ - createModelObject: function(propertyModelValue, propertyEditorAlias, blockConfigurations, propertyScope) { - return new BlockEditorModelObject(propertyModelValue, propertyEditorAlias, blockConfigurations, propertyScope); - } + createModelObject: createModelObject } } 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 new file mode 100644 index 0000000000..18e0c9322f --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/common/services/blockeditormodelobject.service.js @@ -0,0 +1,724 @@ +/** + * @ngdoc service + * @name umbraco.services.BlockEditorModelObject + * + * @description + * Added in Umbraco 8.7. Model Object for dealing with data of Block Editors. + * + * Block Editor Model Object provides the basic features for editing data of a block editor.
      + * Use the Block Editor Service to instantiate the Model Object.
      + * See {@link umbraco.services.blockEditorService blockEditorService} + * + * ## Basic knowledge for understanding how to work with Block Editor data. + * There is a few things we need to understand before we can use the Model Object(BlockEditorModelObject). + * The data structure of a Block Editor contains two main properties 'layout' and 'data'. + * - The 'layout' property is the origin of the data, this object defines the blocks of this property including the the order and layout of those. + * - The 'data' property is the data of your blocks and is managed by the Model Object therefor it can be ignored for most use. + * + * To get a better understanding of what this means as a user of the Model Object, we need to look at some simple usages of the Model Object: + * + * ## Maintain and work with the Layout of a Block Editor. + * The 'layout' of a Block Editor can be of any structure. Therefor the Model Object(BlockEditorModelObject) cannot maintain this data. + * Since the origin of blocks is in the 'layout' the Model Object only can serve as a helper to maintain and create data. + * Therefor the Property Editor code will be using the 'layout' as origin, using the Model Object help managing speicfic parts.
      + * To give an unstanding of what that means please read the following documentation of how to create a block. + * + * ## The basic setup for a Block Editor. + * ####Instantiate a Model Object and load dependencies. And provide the basic structure for the 'layout' property when reciving the reference to it: + * + *
      + * 
      + *     // Define variables for layout and modelObject as you will be using these through our your property-editor.
      + *     var layout;
      + *     var modelObject;
      + *     
      + *     // When we are ready we can instantiate the Model Object can load the dependencies of it.
      + *     vm.$onInit = function() {
      + *         modelObject = blockEditorService.createModelObject(vm.model.value, vm.model.editor, vm.model.config.blocks, $scope);
      + *         modelObject.load().then(onLoaded);
      + *     }
      + * 
      + *     function onLoaded() {
      + *
      + *          // Define the default layout, this is used when there is no data jet stored for this property.
      + *          var defaultLayout = [];
      + * 
      + *          // We store a reference to layout as we have to maintain this.
      + *          layout = modelObject.getLayout(defaultLayout);
      + * 
      + *      }
      + * 
      + * + * ## Create a Block. + * Use the Model Object to create a Block and append the returned layout-entry to the 'layout'. + * + * #### In the following example we will create a new block and append it at the decidered location in the 'layout' object: + * + *
      + *     // continuing from previous example.
      + *     
      + *     // Creates a block and returns a layout entry. The layout entry is not part of layout jet as its not managed by the Model Object.
      + *     var layoutEntry = modelObject.create(contentTypeKey);
      + *     if (layoutEntry === null) {
      + *         // The creation was not successful, therefore exit and without appending anything to our 'layout' object.
      + *         return false;
      + *     }
      + * 
      + *     // If we reach this line, we are good to add the layoutEntry to layout model.
      + *     // In this example our layout is an array and we would like to append the new block as the last entry.
      + *     layout.push(layoutEntry);
      + *     
      + * 
      + * + * ## Working with Blocks + * + * The layout-entries does not provide much value when it comes to displaying or editing Blocks. + * Our Model Object provides the option to get a Block Object for a given Block. Retrived by parsing the layout-entry of the block we would like. + * The Block Object provides data of interest, the most important of these properties are: Block configuration, A label and the Block content in the Element Type Data Model format, this Content-model is very usefull to make UI for editing the Content of a Block. + * + * #### This example uses the Model Object to retrive a Block Object for outputting its label in the console.
      + * + *
      + *     // We store blocks in the layout
      + *     var layout = modelObject.getLayout([]);
      + * 
      + *     if (layout.length > 0) {
      + * 
      + *         // Get first entry of from the layout, which is an array in this sample.
      + *         var firstLayoutEntry = layout[0];
      + * 
      + *         // Create a Block Object for that entry.
      + *         var block = modelObject.getBlockObject(firstLayoutEntry);
      + * 
      + *         // Check if the Block Object creation went well. (If a block isnt supported by the configuration of the Property Editor)
      + *         if(block !== null) {
      + *             console.log(block.label);
      + *         }
      + * 
      + *     }
      + * 
      + * + * #### This similar example uses the Block Object for settings a value on the first property in the Blocks Content.
      + * + *
      + *     // We store blocks in the layout
      + *     var layout = modelObject.getLayout([]);
      + * 
      + *     if (layout.length > 0) {
      + * 
      + *         // Get first entry of from the layout, which is an array in this sample.
      + *         var firstLayoutEntry = layout[0];
      + * 
      + *         // Create a Block Object for that entry.
      + *         var block = modelObject.getBlockObject(firstLayoutEntry);
      + * 
      + *         // Check if the Block Object creation went well. (If a block isnt supported by the configuration of the Property Editor)
      + *         if(block !== null) {
      + *             block.content.variants[0].tabs[0].properties[0].value = "Hello world";// This value will automaticly be synced to the Property Editors Data Model.
      + *         }
      + * 
      + *     }
      + * 
      + * + * See getBlockObject method for more information on the properties avaiable on a Block Object. + * + * ## Remove a Block + * + * Removing a Block and destroying the data of it is done by calling one method of the Model Object, but we have remember that we need to maintain the 'layout' object and this case is a great example of how thats done. + * You will find that your code will very much be based on working with Block Objects and therefor removal of a Block is be done by refering a Block Object. + * + * #### This example shows how to remove the first Block of our imaginary Block Editor and removing the block from our layout. + * + *
      + *     var layout = modelObject.getLayout([]);
      + *     if (layout.length > 0) {
      + * 
      + *         // Get first entry of from the layout, which is an array in this sample.
      + *         var firstLayoutEntry = layout[0];
      + * 
      + *         // Create a Block Object for that entry.
      + *         var block = modelObject.getBlockObject(firstLayoutEntry);
      + * 
      + *         // Check if the Block Object creation went well. (If a block isnt supported by the configuration of the Property Editor)
      + *         if(block !== null) {
      + *             modelObject.removeDataAndDestroyModel(block);// Removing the data of our block and destroying the Block Object for performance reasons.
      + *             
      + *             // We need to maintain the 'layout' object, so therefor its up to our code to remove the block from the 'layout' object.
      + *             const index = array.indexOf(5);
      + *             if (index > -1) {
      + *                 layout.splice(index, 1);
      + *             }
      + *         }
      + * 
      + *     }
      + *
      + * 
      + * + * + * ## Manage a Render Model for Displaying Blocks in the Property Editor + * + * For Rendering a Block in our AngularJS view + * + *
      + *     // TODO to be done.
      + * 
      + * + * + */ +(function () { + 'use strict'; + + + function blockEditorModelObjectFactory($interpolate, udiService, contentResource) { + + + /** + * Simple mapping from property model content entry to editing model, + * needs to stay simple to avoid deep watching. + */ + function mapToElementModel(elementModel, dataModel) { + + var variant = elementModel.variants[0]; + + for (var t = 0; t < variant.tabs.length; t++) { + var tab = variant.tabs[t]; + + for (var p = 0; p < tab.properties.length; p++) { + var prop = tab.properties[p]; + if (dataModel[prop.alias]) { + prop.value = dataModel[prop.alias]; + } + } + } + } + + /** + * Simple mapping from elementModel to property model content entry, + * needs to stay simple to avoid deep watching. + */ + function mapToPropertyModel(elementModel, dataModel) { + + var variant = elementModel.variants[0]; + + for (var t = 0; t < variant.tabs.length; t++) { + var tab = variant.tabs[t]; + + for (var p = 0; p < tab.properties.length; p++) { + var prop = tab.properties[p]; + if (prop.value) { + dataModel[prop.alias] = prop.value; + } + } + } + } + + /** + * Map property values from an ElementModel to another ElementModel. + * Used to tricker watchers for synchronization. + * @param {Object} fromModel ElementModel to recive property values from. + * @param {Object} toModel ElementModel to recive property values from. + */ + function mapElementValues(fromModel, toModel) { + if (!fromModel || !fromModel.variants) { + toModel.variants = null; + return; + } + if (!fromModel.variants.length) { + toModel.variants = []; + return; + } + + var fromVariant = fromModel.variants[0]; + if (!fromVariant) { + toModel.variants = [null]; + return; + } + + var toVariant = toModel.variants[0]; + + for (var t = 0; t < fromVariant.tabs.length; t++) { + var fromTab = fromVariant.tabs[t]; + var toTab = toVariant.tabs[t]; + + for (var p = 0; p < fromTab.properties.length; p++) { + var fromProp = fromTab.properties[p]; + var toProp = toTab.properties[p]; + toProp.value = fromProp.value; + } + } + } + + + /** + * Generate label for Block, uses either the labelInterpolator or falls back to the contentTypeName. + * @param {Object} blockObject BlockObject to recive data values from. + */ + function getBlockLabel(blockObject) { + if(blockObject.labelInterpolator !== undefined) { + // We are just using the data model, since its a key/value object that is live synced. (if we need to add additional vars, we could make a shallow copy and apply those.) + return blockObject.labelInterpolator(blockObject.data); + } + return blockObject.content.contentTypeName; + } + + + /** + * Used to add watchers on all properties in a content or settings model + */ + function addWatchers(blockObject, isolatedScope, forSettings) { + var model = forSettings ? blockObject.settings : blockObject.content; + if (!model || !model.variants || !model.variants.length) { return; } + + // Start watching each property value. + var variant = model.variants[0]; + var field = forSettings ? "settings" : "content"; + var watcherCreator = forSettings ? createSettingsModelPropWatcher : createDataModelPropWatcher; + for (var t = 0; t < variant.tabs.length; t++) { + var tab = variant.tabs[t]; + for (var p = 0; p < tab.properties.length; p++) { + var prop = tab.properties[p]; + + // Watch value of property since this is the only value we want to keep synced. + // Do notice that it is not performing a deep watch, meaning that we are only watching primatives and changes directly to the object of property-value. + // But we like to sync non-primative values as well! Yes, and this does happen, just not through this code, but through the nature of JavaScript. + // Non-primative values act as references to the same data and are therefor synced. + blockObject.watchers.push(isolatedScope.$watch("blockObjects._" + blockObject.key + "." + field + ".variants[0].tabs[" + t + "].properties[" + p + "].value", watcherCreator(blockObject, prop))); + } + } + if (blockObject.watchers.length === 0) { + // If no watcher where created, it means we have no properties to watch. This means that nothing will activate our generate the label, since its only triggered by watchers. + blockObject.updateLabel(); + } + } + + /** + * Used to create a scoped watcher for a content property on a blockObject. + */ + function createDataModelPropWatcher(blockObject, prop) { + return function() { + // sync data: + blockObject.data[prop.alias] = prop.value; + + blockObject.updateLabel(); + } + } + + /** + * Used to create a scoped watcher for a settings property on a blockObject. + */ + function createSettingsModelPropWatcher(blockObject, prop) { + return function() { + // sync data: + blockObject.layout.settings[prop.alias] = prop.value; + } + } + + + /** + * Used to highlight unsupported properties for the user, changes unsupported properties into a unsupported-property. + */ + var notSupportedProperties = [ + "Umbraco.Tags", + "Umbraco.UploadField", + "Umbraco.ImageCropper", + "Umbraco.NestedContent" + ]; + function replaceUnsupportedProperties(scaffold) { + scaffold.variants.forEach((variant) => { + variant.tabs.forEach((tab) => { + tab.properties.forEach((property) => { + if (notSupportedProperties.indexOf(property.editor) !== -1) { + property.view = "notsupported"; + } + }); + }); + }); + return scaffold; + } + + /** + * @ngdoc method + * @name constructor + * @methodOf umbraco.services.BlockEditorModelObject + * @description Constructor of the model object used to handle Block Editor data. + * @param {object} propertyModelValue data object of the property editor, usually model.value. + * @param {string} propertyEditorAlias alias of the property. + * @param {object} blockConfigurations block configurations. + * @param {angular-scope} propertyScope The local angularJS scope. + * @returns {BlockEditorModelObject} A instance of BlockEditorModelObject. + */ + function BlockEditorModelObject(propertyModelValue, propertyEditorAlias, blockConfigurations, propertyScope) { + + if (!propertyModelValue) { + throw new Error("propertyModelValue cannot be undefined, to ensure we keep the binding to the angular model we need minimum an empty object."); + } + + // ensure basic part of data-structure is in place: + this.value = propertyModelValue; + this.value.layout = this.value.layout || {}; + this.value.data = this.value.data || []; + + this.propertyEditorAlias = propertyEditorAlias; + this.blockConfigurations = blockConfigurations; + + this.scaffolds = []; + + this.watchers = []; + + this.isolatedScope = propertyScope.$new(true); + this.isolatedScope.blockObjects = {}; + + this.isolatedScope.$on("$destroy", this.onDestroyed.bind(this)); + + }; + + BlockEditorModelObject.prototype = { + + /** + * @ngdoc function + * @name getBlockConfiguration + * @methodOf umbraco.services.BlockEditorModelObject + * @description Get block configuration object for a given contentTypeKey. + * @param {string} key contentTypeKey to recive the configuration model for. + * @returns {Object | null} Configuration model for the that specific block. Or ´null´ if the contentTypeKey isnt available in the current block configurations. + */ + getBlockConfiguration: function(key) { + return this.blockConfigurations.find(bc => bc.contentTypeKey === key) || null; + }, + + /** + * @ngdoc function + * @name load + * @methodOf umbraco.services.BlockEditorModelObject + * @description Load the scaffolding models for the given configuration, these are needed to provide usefull models for each block. + * @param {Object} blockObject BlockObject to recive data values from. + * @returns {Promise} A Promise object which resolves when all scaffold models are loaded. + */ + load: function() { + var tasks = []; + + var scaffoldKeys = []; + + this.blockConfigurations.forEach(blockConfiguration => { + scaffoldKeys.push(blockConfiguration.contentTypeKey); + if (blockConfiguration.settingsElementTypeKey != null) { + scaffoldKeys.push(blockConfiguration.settingsElementTypeKey); + } + }); + + // removing dublicates. + scaffoldKeys = scaffoldKeys.filter((value, index, self) => self.indexOf(value) === index); + + scaffoldKeys.forEach((contentTypeKey => { + tasks.push(contentResource.getScaffoldByKey(-20, contentTypeKey).then(scaffold => { + this.scaffolds.push(replaceUnsupportedProperties(scaffold)); + })); + })); + + return Promise.all(tasks); + }, + + /** + * @ngdoc function + * @name getAvailableAliasesForBlockContent + * @methodOf umbraco.services.BlockEditorModelObject + * @description Retrive a list of aliases that are available for content of blocks in this property editor, does not contain aliases of block settings. + * @return {Array} array of strings representing alias. + */ + getAvailableAliasesForBlockContent: function() { + return this.blockConfigurations.map(blockConfiguration => this.getScaffoldFromKey(blockConfiguration.contentTypeKey).contentTypeAlias); + }, + + /** + * @ngdoc function + * @name getAvailableBlocksForBlockPicker + * @methodOf umbraco.services.BlockEditorModelObject + * @description Retrive a list of available blocks, the list containing object with the confirugation model(blockConfigModel) and the element type model(elementTypeModel). + * The purpose of this data is to provide it for the Block Picker. + * @return {Array} array of objects representing available blocks, each object containing properties blockConfigModel and elementTypeModel. + */ + getAvailableBlocksForBlockPicker: function() { + + var blocks = []; + + this.blockConfigurations.forEach(blockConfiguration => { + var scaffold = this.getScaffoldFromKey(blockConfiguration.contentTypeKey); + if(scaffold) { + blocks.push({ + blockConfigModel: blockConfiguration, + elementTypeModel: scaffold.documentType + }); + } + }); + + return blocks; + }, + + /** + * @ngdoc function + * @name getScaffoldFromKey + * @methodOf umbraco.services.BlockEditorModelObject + * @description Get scaffold model for a given contentTypeKey. + * @param {string} key contentTypeKey to recive the scaffold model for. + * @returns {Object | null} Scaffold model for the that content type. Or null if the scaffolding model dosnt exist in this context. + */ + getScaffoldFromKey: function(contentTypeKey) { + return this.scaffolds.find(o => o.contentTypeKey === contentTypeKey); + }, + + /** + * @ngdoc function + * @name getScaffoldFromAlias + * @methodOf umbraco.services.BlockEditorModelObject + * @description Get scaffold model for a given contentTypeAlias, used by clipboardService. + * @param {string} alias contentTypeAlias to recive the scaffold model for. + * @returns {Object | null} Scaffold model for the that content type. Or null if the scaffolding model dosnt exist in this context. + */ + getScaffoldFromAlias: function(contentTypeAlias) { + return this.scaffolds.find(o => o.contentTypeAlias === contentTypeAlias); + }, + + /** + * @ngdoc function + * @name getBlockObject + * @methodOf umbraco.services.BlockEditorModelObject + * @description Retrieve editor friendly model of a block. + * BlockObject is a class instance which setups live syncronization of content and settings models back to the data of your property editor model. + * The returned object, named ´BlockObject´, contains several usefull models to make editing of this block happen. + * The ´BlockObject´ contains the following properties: + * - key {string}: runtime generated key, usefull for tracking of this object + * - content {Object}: Content model, the content type model for content merged with the content data of this block. + * - settings {Object}: Settings model, the content type model for settings merged with the settings data of this block. + * - config {Object}: A deep copy of the block configuration model. + * - label {string}: The label for this block. + * - updateLabel {Method}: Method to trigger an update of the label for this block. + * - data {Object}: A reference to the data object from your property editor model. + * - layout {Object}: A refernce to the layout entry from your property editor model. + * @param {Object} layoutEntry the layout entry object to build the block model from. + * @return {Object | null} The BlockObject for the given layout entry. Or null if data or configuration wasnt found for this block. + */ + getBlockObject: function(layoutEntry) { + + var udi = layoutEntry.udi; + + var dataModel = this._getDataByUdi(udi); + + if (dataModel === null) { + console.error("Couldnt find content model of " + udi) + return null; + } + + var blockConfiguration = this.getBlockConfiguration(dataModel.contentTypeKey); + + if (blockConfiguration === null) { + console.error("The block entry of "+udi+" is not begin initialized cause its contentTypeKey is not allowed for this PropertyEditor") + // This is not an allowed block type, therefor we return null; + return null; + } + + var contentScaffold = this.getScaffoldFromKey(blockConfiguration.contentTypeKey); + if(contentScaffold === null) { + return null; + } + + var blockObject = {}; + blockObject.key = String.CreateGuid().replace(/-/g, ""); + blockObject.config = Utilities.copy(blockConfiguration); + if (blockObject.config.label && blockObject.config.label !== "") { + blockObject.labelInterpolator = $interpolate(blockObject.config.label); + } + blockObject.__scope = this.isolatedScope; + blockObject.updateLabel = _.debounce(function () {this.__scope.$evalAsync(function() { + this.label = getBlockLabel(this); + }.bind(this))}.bind(blockObject), 10); + + // make basics from scaffold + blockObject.content = Utilities.copy(contentScaffold); + blockObject.content.udi = udi; + + mapToElementModel(blockObject.content, dataModel); + + blockObject.data = dataModel; + blockObject.layout = layoutEntry; + blockObject.watchers = []; + + if (blockConfiguration.settingsElementTypeKey) { + var settingsScaffold = this.getScaffoldFromKey(blockConfiguration.settingsElementTypeKey); + if (settingsScaffold === null) { + return null; + } + + // make basics from scaffold + blockObject.settings = Utilities.copy(settingsScaffold); + layoutEntry.settings = layoutEntry.settings || {}; + if (!layoutEntry.settings.key) { layoutEntry.settings.key = String.CreateGuid(); } + if (!layoutEntry.settings.contentTypeKey) { layoutEntry.settings.contentTypeKey = blockConfiguration.settingsElementTypeKey; } + mapToElementModel(blockObject.settings, layoutEntry.settings); + } else { + layoutEntry.settings = null; + } + + blockObject.retriveValuesFrom = function(content, settings) { + if (this.content !== null) { + mapElementValues(content, this.content); + } + if (this.config.settingsElementTypeKey !== null) { + mapElementValues(settings, this.settings); + } + } + + // first time instant update of label. + blockObject.label = getBlockLabel(blockObject); + + // Add blockObject to our isolated scope to enable watching its values: + this.isolatedScope.blockObjects["_"+blockObject.key] = blockObject; + addWatchers(blockObject, this.isolatedScope); + addWatchers(blockObject, this.isolatedScope, true); + + return blockObject; + + }, + + /** + * @ngdoc function + * @name removeDataAndDestroyModel + * @methodOf umbraco.services.BlockEditorModelObject + * @description Removes the data and destroys the Block Model. + * Notive this method does not remove the block from your layout, this will need to be handlede by the Property Editor since this services donst know about your layout structure. + * @param {Object} blockObject The BlockObject to be removed and destroyed. + */ + removeDataAndDestroyModel: function (blockObject) { + this.destroyBlockObject(blockObject); + this.removeDataByUdi(blockObject.content.udi); + }, + + /** + * @ngdoc function + * @name destroyBlockObject + * @methodOf umbraco.services.BlockEditorModelObject + * @description Destroys the Block Model, but all data is kept. + * @param {Object} blockObject The BlockObject to be destroyed. + */ + destroyBlockObject: function(blockObject) { + + // remove property value watchers: + blockObject.watchers.forEach(w => { w(); }); + + // remove model from isolatedScope. + delete this.isolatedScope.blockObjects[blockObject.key]; + + }, + + /** + * @ngdoc function + * @name getLayout + * @methodOf umbraco.services.BlockEditorModelObject + * @description Retrieve the layout object from this specific property editor model. + * @param {object} DefaultStructure if no data exist the layout of your poerty editor will be set to this object. + * @return {Object} Layout object, structure depends on the model of your property editor. + */ + getLayout: function(defaultStructure) { + if (!this.value.layout[this.propertyEditorAlias]) { + this.value.layout[this.propertyEditorAlias] = defaultStructure; + } + return this.value.layout[this.propertyEditorAlias]; + }, + + /** + * @ngdoc function + * @name create + * @methodOf umbraco.services.BlockEditorModelObject + * @description Create a empty layout entry, notice the layout entry is not added to the property editors model layout object, since the layout sturcture depends on the property editor. + * @param {string} contentTypeKey the contentTypeKey of the block you wish to create, if contentTypeKey is not avaiable in the block configuration then ´null´ will be returned. + * @return {Object | null} Layout entry object, to be inserted at a decired location in the layout object. Or null if contentTypeKey is unavaiaible. + */ + create: function(contentTypeKey) { + + var blockConfiguration = this.getBlockConfiguration(contentTypeKey); + if(blockConfiguration === null) { + return null; + } + + var entry = { + udi: this._createDataEntry(contentTypeKey) + } + + if (blockConfiguration.settingsElementTypeKey != null) { + entry.settings = { key: String.CreateGuid(), contentTypeKey: blockConfiguration.settingsElementTypeKey }; + } + + return entry; + }, + + /** + * @ngdoc function + * @name createFromElementType + * @methodOf umbraco.services.BlockEditorModelObject + * @description Insert data from ElementType Model + * @return {Object | null} Layout entry object, to be inserted at a decired location in the layout object. Or ´null´ if the given ElementType isnt supported by the block configuration. + */ + createFromElementType: function(elementTypeDataModel) { + + elementTypeDataModel = Utilities.copy(elementTypeDataModel); + + var contentTypeKey = elementTypeDataModel.contentTypeKey; + + var layoutEntry = this.create(contentTypeKey); + if(layoutEntry === null) { + return null; + } + + var dataModel = this._getDataByUdi(layoutEntry.udi); + if(dataModel === null) { + return null; + } + + mapToPropertyModel(elementTypeDataModel, dataModel); + + return layoutEntry; + + }, + + // private + _createDataEntry: function(elementTypeKey) { + var content = { + contentTypeKey: elementTypeKey, + udi: udiService.create("element") + }; + this.value.data.push(content); + return content.udi; + }, + // private + _getDataByUdi: function(udi) { + return this.value.data.find(entry => entry.udi === udi) || null; + }, + + removeDataByUdi: function(udi) { + const index = this.value.data.findIndex(o => o.udi === udi); + if (index !== -1) { + this.value.data.splice(index, 1); + } + }, + + onDestroyed: function() { + + for (const key in this.isolatedScope.blockObjects) { + this.destroyBlockObject(this.isolatedScope.blockObjects[key]); + } + + delete this.value; + delete this.propertyEditorAlias; + delete this.blockConfigurations; + delete this.scaffolds; + delete this.watchers; + this.isolatedScope.$destroy(); + delete this.isolatedScope; + } + } + + return BlockEditorModelObject; + } + + angular.module('umbraco.services').service('blockEditorModelObject', blockEditorModelObjectFactory); + +})(); diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.js index 8b37001ae8..da62d10c87 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.js @@ -85,7 +85,7 @@ // Create Model Object, to manage our data for this Block Editor. modelObject = blockEditorService.createModelObject(vm.model.value, vm.model.editor, vm.model.config.blocks, $scope); - modelObject.loadScaffolding().then(onLoaded); + modelObject.load().then(onLoaded); copyAllBlocksAction = { labelKey: "clipboard_labelForCopyAllEntries", @@ -124,11 +124,12 @@ function onLoaded() { // Store a reference to the layout model, because we need to maintain this model. - layout = modelObject.getLayout(); + layout = modelObject.getLayout([]); - // maps layout entries to editor friendly models aka. BlockModels. + // maps layout entries to editor friendly models aka. blockObjects. layout.forEach(entry => { - var block = getBlockModel(entry); + var block = getBlockObject(entry); + // If this entry was not supported by our property-editor it would return 'null'. if(block !== null) { vm.blocks.push(block); @@ -145,12 +146,12 @@ } - function getBlockModel(entry) { - var block = modelObject.getBlockModel(entry); + function getBlockObject(entry) { + var block = modelObject.getBlockObject(entry); if (block === null) return null; - // Lets apply fallback views, and make the view available directly on the blockModel. + // Lets apply fallback views, and make the view available directly on the blockObject. block.view = (block.config.view ? "/" + block.config.view : (inlineEditing ? "views/propertyeditors/blocklist/blocklistentryeditors/inlineblock/inlineblock.editor.html" : "views/propertyeditors/blocklist/blocklistentryeditors/labelblock/labelblock.editor.html")); block.showSettings = block.config.settingsElementTypeKey != null; @@ -168,21 +169,21 @@ } // make block model - var blockModel = getBlockModel(layoutEntry); - if (blockModel === null) { + var blockObject = getBlockObject(layoutEntry); + if (blockObject === null) { return false; } - // If we reach this line, we are good to add the layoutEntry and blockModel to our models. + // If we reach this line, we are good to add the layoutEntry and blockObject to our models. // add layout entry at the decired location in layout. layout.splice(index, 0, layoutEntry); // apply block model at decired location in blocks. - vm.blocks.splice(index, 0, blockModel); + vm.blocks.splice(index, 0, blockObject); // lets move focus to this new block. - vm.setBlockFocus(blockModel); + vm.setBlockFocus(blockObject); return true; @@ -212,16 +213,16 @@ vm.blocks.forEach(deleteBlock); } - function editBlock(blockModel, openSettings) { + function editBlock(blockObject, openSettings) { - blockModel.active = true; + blockObject.active = true; // make a clone to avoid editing model directly. - var blockContentClone = Utilities.copy(blockModel.content); + var blockContentClone = Utilities.copy(blockObject.content); var blockSettingsClone = null; - if (blockModel.config.settingsElementTypeKey) { - blockSettingsClone = Utilities.copy(blockModel.settings); + if (blockObject.config.settingsElementTypeKey) { + blockSettingsClone = Utilities.copy(blockObject.settings); } var hideContent = (openSettings === true && inlineEditing === true); @@ -231,17 +232,17 @@ hideContent: hideContent, openSettings: openSettings === true, settings: blockSettingsClone, - title: blockModel.label, + title: blockObject.label, view: "views/common/infiniteeditors/blockeditor/blockeditor.html", - size: blockModel.config.editorSize || "medium", + size: blockObject.config.editorSize || "medium", submit: function(blockEditorModel) { - blockModel.retriveValuesFrom(blockEditorModel.content, blockEditorModel.settings) + blockObject.retriveValuesFrom(blockEditorModel.content, blockEditorModel.settings) blockEditorModel.close(); }, close: function() { - blockModel.active = false; + blockObject.active = false; editorService.close(); } }; @@ -387,8 +388,8 @@ } // make block model - var blockModel = getBlockModel(layoutEntry); - if (blockModel === null) { + var blockObject = getBlockObject(layoutEntry); + if (blockObject === null) { return false; } @@ -396,9 +397,9 @@ layout.splice(index, 0, layoutEntry); // insert block model at the decired location in blocks. - vm.blocks.splice(index, 0, blockModel); + vm.blocks.splice(index, 0, blockObject); - vm.currentBlockInFocus = blockModel; + vm.currentBlockInFocus = blockObject; return true; diff --git a/src/Umbraco.Web.UI.Client/test/unit/common/services/block-editor-service.spec.js b/src/Umbraco.Web.UI.Client/test/unit/common/services/block-editor-service.spec.js index 6924946cff..39d28311cc 100644 --- a/src/Umbraco.Web.UI.Client/test/unit/common/services/block-editor-service.spec.js +++ b/src/Umbraco.Web.UI.Client/test/unit/common/services/block-editor-service.spec.js @@ -58,7 +58,7 @@ var modelObject = blockEditorService.createModelObject({}, "Umbraco.TestBlockEditor", [], $scope); expect(modelObject).not.toBeUndefined(); - expect(modelObject.loadScaffolding).not.toBeUndefined(); + expect(modelObject.load).not.toBeUndefined(); }); it('getBlockConfiguration provide the requested block configurtion', function () { @@ -67,10 +67,10 @@ expect(modelObject.getBlockConfiguration(blockConfigurationMock.contentTypeKey).label).toBe(blockConfigurationMock.label); }); - it('loadScaffolding provides data for itemPicker', function (done) { + it('load provides data for itemPicker', function (done) { var modelObject = blockEditorService.createModelObject({}, "Umbraco.TestBlockEditor", [blockConfigurationMock], $scope); - modelObject.loadScaffolding().then(() => { + modelObject.load().then(() => { var itemPickerOptions = modelObject.getAvailableBlocksForBlockPicker(); expect(itemPickerOptions.length).toBe(1); expect(itemPickerOptions[0].blockConfigModel.contentTypeKey).toBe(blockConfigurationMock.contentTypeKey); @@ -84,7 +84,7 @@ var modelObject = blockEditorService.createModelObject(propertyModelMock, "Umbraco.TestBlockEditor", [blockConfigurationMock], $scope); - modelObject.loadScaffolding().then(() => { + modelObject.load().then(() => { var layout = modelObject.getLayout(); @@ -98,20 +98,20 @@ }); - it('getBlockModel has values', function (done) { + it('getBlockObject has values', function (done) { var modelObject = blockEditorService.createModelObject(propertyModelMock, "Umbraco.TestBlockEditor", [blockConfigurationMock], $scope); - modelObject.loadScaffolding().then(() => { + modelObject.load().then(() => { var layout = modelObject.getLayout(); - var blockModel = modelObject.getBlockModel(layout[0]); + var blockObject = modelObject.getBlockObject(layout[0]); - expect(blockModel).not.toBeUndefined(); - expect(blockModel.data.udi).toBe(propertyModelMock.data[0].udi); - expect(blockModel.content.variants[0].tabs[0].properties[0].value).toBe(propertyModelMock.data[0].testproperty); + expect(blockObject).not.toBeUndefined(); + expect(blockObject.data.udi).toBe(propertyModelMock.data[0].udi); + expect(blockObject.content.variants[0].tabs[0].properties[0].value).toBe(propertyModelMock.data[0].testproperty); done(); }); @@ -119,24 +119,24 @@ }); - it('getBlockModel syncs primative values', function (done) { + it('getBlockObject syncs primative values', function (done) { var propertyModel = angular.copy(propertyModelMock); var modelObject = blockEditorService.createModelObject(propertyModel, "Umbraco.TestBlockEditor", [blockConfigurationMock], $scope); - modelObject.loadScaffolding().then(() => { + modelObject.load().then(() => { var layout = modelObject.getLayout(); - var blockModel = modelObject.getBlockModel(layout[0]); + var blockObject = modelObject.getBlockObject(layout[0]); - blockModel.content.variants[0].tabs[0].properties[0].value = "anotherTestValue"; + blockObject.content.variants[0].tabs[0].properties[0].value = "anotherTestValue"; $rootScope.$digest();// invoke angularJS Store. - expect(blockModel.data).toBe(propertyModel.data[0]); - expect(blockModel.data.testproperty).toBe("anotherTestValue"); + expect(blockObject.data).toBe(propertyModel.data[0]); + expect(blockObject.data.testproperty).toBe("anotherTestValue"); expect(propertyModel.data[0].testproperty).toBe("anotherTestValue"); // @@ -147,7 +147,7 @@ }); - it('getBlockModel syncs values of object', function (done) { + it('getBlockObject syncs values of object', function (done) { var propertyModel = angular.copy(propertyModelMock); @@ -157,14 +157,14 @@ var modelObject = blockEditorService.createModelObject(propertyModel, "Umbraco.TestBlockEditor", [blockConfigurationMock], $scope); - modelObject.loadScaffolding().then(() => { + modelObject.load().then(() => { var layout = modelObject.getLayout(); - var blockModel = modelObject.getBlockModel(layout[0]); + var blockObject = modelObject.getBlockObject(layout[0]); - blockModel.content.variants[0].tabs[0].properties[0].value.list[0] = "AA"; - blockModel.content.variants[0].tabs[0].properties[0].value.list.push("D"); + blockObject.content.variants[0].tabs[0].properties[0].value.list[0] = "AA"; + blockObject.content.variants[0].tabs[0].properties[0].value.list.push("D"); $rootScope.$digest();// invoke angularJS Store. @@ -182,7 +182,7 @@ var modelObject = blockEditorService.createModelObject(propertyModel, "Umbraco.TestBlockEditor", [blockConfigurationMock], $scope); - modelObject.loadScaffolding().then(() => { + modelObject.load().then(() => { var layout = modelObject.getLayout(); @@ -203,17 +203,17 @@ var modelObject = blockEditorService.createModelObject(propertyModel, "Umbraco.TestBlockEditor", [blockConfigurationMock], $scope); - modelObject.loadScaffolding().then(() => { + modelObject.load().then(() => { var layout = modelObject.getLayout(); - var blockModel = modelObject.getBlockModel(layout[0]); + var blockObject = modelObject.getBlockObject(layout[0]); // remove from layout; layout.splice(0, 1); // remove from data; - modelObject.removeDataAndDestroyModel(blockModel); + modelObject.removeDataAndDestroyModel(blockObject); expect(propertyModel.data.length).toBe(0); expect(propertyModel.data[0]).toBeUndefined(); From d2b329cdb5fb3f0dfc3a754498eaec987ebdd679 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Tue, 16 Jun 2020 09:36:19 +0200 Subject: [PATCH 201/377] fix links in js docs --- .../common/services/blockeditor.service.js | 8 +-- .../blockeditormodelobject.service.js | 56 +++++++++---------- 2 files changed, 32 insertions(+), 32 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 36e378a612..87ab274f13 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 @@ -19,7 +19,7 @@ * * * - * See {@link umbraco.services.BlockEditorModelObject BlockEditorModelObject} for more samples. + * See {@link umbraco.services.blockEditorModelObject BlockEditorModelObject} for more samples. * */ (function () { @@ -35,10 +35,10 @@ * * @description * Create a new Block Editor Model Object. - * See {@link umbraco.services.blockEditorModelObject BlockEditorModelObject} + * See {@link umbraco.services.blockEditorModelObject blockEditorModelObject} * - * @see umbraco.services.BlockEditorModelObject - * @return {BlockEditorModelObject} A instance of the BlockEditorModelObject class. + * @see umbraco.services.blockEditorModelObject + * @return {blockEditorModelObject} A instance of the BlockEditorModelObject class. */ function createModelObject(propertyModelValue, propertyEditorAlias, blockConfigurations, propertyScope) { return new blockEditorModelObject(propertyModelValue, propertyEditorAlias, blockConfigurations, propertyScope); 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 18e0c9322f..c3aaf1a5f5 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 @@ -1,6 +1,6 @@ /** * @ngdoc service - * @name umbraco.services.BlockEditorModelObject + * @name umbraco.services.blockEditorModelObject * * @description * Added in Umbraco 8.7. Model Object for dealing with data of Block Editors. @@ -120,7 +120,7 @@ * } * * - * See getBlockObject method for more information on the properties avaiable on a Block Object. + * See {@link umbraco.services.blockEditorModelObject#methods_getBlockObject getBlockObject} method for more information on the properties avaiable on a Block Object. * * ## Remove a Block * @@ -339,7 +339,7 @@ /** * @ngdoc method * @name constructor - * @methodOf umbraco.services.BlockEditorModelObject + * @methodOf umbraco.services.blockEditorModelObject * @description Constructor of the model object used to handle Block Editor data. * @param {object} propertyModelValue data object of the property editor, usually model.value. * @param {string} propertyEditorAlias alias of the property. @@ -375,9 +375,9 @@ BlockEditorModelObject.prototype = { /** - * @ngdoc function + * @ngdoc method * @name getBlockConfiguration - * @methodOf umbraco.services.BlockEditorModelObject + * @methodOf umbraco.services.blockEditorModelObject * @description Get block configuration object for a given contentTypeKey. * @param {string} key contentTypeKey to recive the configuration model for. * @returns {Object | null} Configuration model for the that specific block. Or ´null´ if the contentTypeKey isnt available in the current block configurations. @@ -387,9 +387,9 @@ }, /** - * @ngdoc function + * @ngdoc method * @name load - * @methodOf umbraco.services.BlockEditorModelObject + * @methodOf umbraco.services.blockEditorModelObject * @description Load the scaffolding models for the given configuration, these are needed to provide usefull models for each block. * @param {Object} blockObject BlockObject to recive data values from. * @returns {Promise} A Promise object which resolves when all scaffold models are loaded. @@ -419,9 +419,9 @@ }, /** - * @ngdoc function + * @ngdoc method * @name getAvailableAliasesForBlockContent - * @methodOf umbraco.services.BlockEditorModelObject + * @methodOf umbraco.services.blockEditorModelObject * @description Retrive a list of aliases that are available for content of blocks in this property editor, does not contain aliases of block settings. * @return {Array} array of strings representing alias. */ @@ -430,9 +430,9 @@ }, /** - * @ngdoc function + * @ngdoc method * @name getAvailableBlocksForBlockPicker - * @methodOf umbraco.services.BlockEditorModelObject + * @methodOf umbraco.services.blockEditorModelObject * @description Retrive a list of available blocks, the list containing object with the confirugation model(blockConfigModel) and the element type model(elementTypeModel). * The purpose of this data is to provide it for the Block Picker. * @return {Array} array of objects representing available blocks, each object containing properties blockConfigModel and elementTypeModel. @@ -455,9 +455,9 @@ }, /** - * @ngdoc function + * @ngdoc method * @name getScaffoldFromKey - * @methodOf umbraco.services.BlockEditorModelObject + * @methodOf umbraco.services.blockEditorModelObject * @description Get scaffold model for a given contentTypeKey. * @param {string} key contentTypeKey to recive the scaffold model for. * @returns {Object | null} Scaffold model for the that content type. Or null if the scaffolding model dosnt exist in this context. @@ -467,9 +467,9 @@ }, /** - * @ngdoc function + * @ngdoc method * @name getScaffoldFromAlias - * @methodOf umbraco.services.BlockEditorModelObject + * @methodOf umbraco.services.blockEditorModelObject * @description Get scaffold model for a given contentTypeAlias, used by clipboardService. * @param {string} alias contentTypeAlias to recive the scaffold model for. * @returns {Object | null} Scaffold model for the that content type. Or null if the scaffolding model dosnt exist in this context. @@ -479,9 +479,9 @@ }, /** - * @ngdoc function + * @ngdoc method * @name getBlockObject - * @methodOf umbraco.services.BlockEditorModelObject + * @methodOf umbraco.services.blockEditorModelObject * @description Retrieve editor friendly model of a block. * BlockObject is a class instance which setups live syncronization of content and settings models back to the data of your property editor model. * The returned object, named ´BlockObject´, contains several usefull models to make editing of this block happen. @@ -580,9 +580,9 @@ }, /** - * @ngdoc function + * @ngdoc method * @name removeDataAndDestroyModel - * @methodOf umbraco.services.BlockEditorModelObject + * @methodOf umbraco.services.blockEditorModelObject * @description Removes the data and destroys the Block Model. * Notive this method does not remove the block from your layout, this will need to be handlede by the Property Editor since this services donst know about your layout structure. * @param {Object} blockObject The BlockObject to be removed and destroyed. @@ -593,9 +593,9 @@ }, /** - * @ngdoc function + * @ngdoc method * @name destroyBlockObject - * @methodOf umbraco.services.BlockEditorModelObject + * @methodOf umbraco.services.blockEditorModelObject * @description Destroys the Block Model, but all data is kept. * @param {Object} blockObject The BlockObject to be destroyed. */ @@ -610,11 +610,11 @@ }, /** - * @ngdoc function + * @ngdoc method * @name getLayout - * @methodOf umbraco.services.BlockEditorModelObject + * @methodOf umbraco.services.blockEditorModelObject * @description Retrieve the layout object from this specific property editor model. - * @param {object} DefaultStructure if no data exist the layout of your poerty editor will be set to this object. + * @param {object} defaultStructure if no data exist the layout of your poerty editor will be set to this object. * @return {Object} Layout object, structure depends on the model of your property editor. */ getLayout: function(defaultStructure) { @@ -625,9 +625,9 @@ }, /** - * @ngdoc function + * @ngdoc method * @name create - * @methodOf umbraco.services.BlockEditorModelObject + * @methodOf umbraco.services.blockEditorModelObject * @description Create a empty layout entry, notice the layout entry is not added to the property editors model layout object, since the layout sturcture depends on the property editor. * @param {string} contentTypeKey the contentTypeKey of the block you wish to create, if contentTypeKey is not avaiable in the block configuration then ´null´ will be returned. * @return {Object | null} Layout entry object, to be inserted at a decired location in the layout object. Or null if contentTypeKey is unavaiaible. @@ -651,9 +651,9 @@ }, /** - * @ngdoc function + * @ngdoc method * @name createFromElementType - * @methodOf umbraco.services.BlockEditorModelObject + * @methodOf umbraco.services.blockEditorModelObject * @description Insert data from ElementType Model * @return {Object | null} Layout entry object, to be inserted at a decired location in the layout object. Or ´null´ if the given ElementType isnt supported by the block configuration. */ From 8c428622d13b7ac49397d92916ea663608bba72f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Tue, 16 Jun 2020 14:36:14 +0200 Subject: [PATCH 202/377] added a blocklistentryeditor for unsupported blocks --- src/Umbraco.Web.UI.Client/src/less/belle.less | 1 + .../src/less/variables.less | 1 + .../unsupportedblock.editor.html | 4 +++ .../unsupportedblock.editor.less | 36 +++++++++++++++++++ 4 files changed, 42 insertions(+) create mode 100644 src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklistentryeditors/unsupportedblock/unsupportedblock.editor.html create mode 100644 src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklistentryeditors/unsupportedblock/unsupportedblock.editor.less diff --git a/src/Umbraco.Web.UI.Client/src/less/belle.less b/src/Umbraco.Web.UI.Client/src/less/belle.less index 777ed762ad..445d87fbb9 100644 --- a/src/Umbraco.Web.UI.Client/src/less/belle.less +++ b/src/Umbraco.Web.UI.Client/src/less/belle.less @@ -206,6 +206,7 @@ @import "../views/propertyeditors/blocklist/prevalue/blocklist.blockconfiguration.less"; @import "../views/propertyeditors/blocklist/prevalue/blocklist.blockconfiguration.overlay.less"; @import "../views/propertyeditors/notsupported/notsupported.less"; +@import "../views/propertyeditors/blocklist/blocklistentryeditors/unsupportedblock/unsupportedblock.editor.less"; @import "../views/propertyeditors/blocklist/blocklistentryeditors/labelblock/labelblock.editor.less"; @import "../views/propertyeditors/blocklist/blocklistentryeditors/inlineblock/inlineblock.editor.less"; diff --git a/src/Umbraco.Web.UI.Client/src/less/variables.less b/src/Umbraco.Web.UI.Client/src/less/variables.less index 26ec1d795a..2f627f3ab3 100644 --- a/src/Umbraco.Web.UI.Client/src/less/variables.less +++ b/src/Umbraco.Web.UI.Client/src/less/variables.less @@ -199,6 +199,7 @@ @ui-btn-positive-type: @white; @ui-btn-negative: @red; +@ui-btn-negative-type: @white; @ui-btn-negative-hover: @red; @ui-icon: @blueNight; 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 new file mode 100644 index 0000000000..bdd7341528 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklistentryeditors/unsupportedblock/unsupportedblock.editor.html @@ -0,0 +1,4 @@ + diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklistentryeditors/unsupportedblock/unsupportedblock.editor.less b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklistentryeditors/unsupportedblock/unsupportedblock.editor.less new file mode 100644 index 0000000000..214a8399fd --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklistentryeditors/unsupportedblock/unsupportedblock.editor.less @@ -0,0 +1,36 @@ +.blockelement-unsupportedblock-editor { + + position: relative; + display: block; + margin-bottom: 4px; + margin-top: 4px; + width: 100%; + min-height: 48px; + border: 1px solid @gray-9; + border-radius: @baseBorderRadius; + + cursor: pointer; + color: @ui-action-discreet-type; + + text-align: left; + padding-left: 20px; + padding-bottom: 2px; + + user-select: none; + + transition: border-color 120ms; + + background-color: @ui-btn-negative; + color: @ui-btn-negative-type; + + i { + font-size: 22px; + margin-right: 5px; + display: inline-block; + vertical-align: middle; + } + span { + display: inline-block; + vertical-align: middle; + } +} From 87059a9f95fb19b8453ab17583a13040f739597b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Tue, 16 Jun 2020 14:37:19 +0200 Subject: [PATCH 203/377] ability to retrive the $scope from UmbVariantContentEditors, needed for Block Editor to get a scope existing across variants and splitview. --- .../content/umbvariantcontenteditors.directive.js | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/content/umbvariantcontenteditors.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/content/umbvariantcontenteditors.directive.js index a188a83d83..25e51bb5f1 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/content/umbvariantcontenteditors.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/content/umbvariantcontenteditors.directive.js @@ -37,6 +37,8 @@ vm.selectApp = selectApp; vm.selectAppAnchor = selectAppAnchor; + vm.getScope = getScope;// used by property editors to get a scope that is the root of split view, content apps etc. + //Used to track how many content views there are (for split view there will be 2, it could support more in theory) vm.editors = []; @@ -243,6 +245,10 @@ } } + function getScope() { + return $scope; + } + } angular.module('umbraco.directives').component('umbVariantContentEditors', umbVariantContentEditors); From 53968202a01832e436c5bca73b9cb7292be97ad5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Tue, 16 Jun 2020 14:38:49 +0200 Subject: [PATCH 204/377] Appending the block objects to layout, to share it across variants and in split-view. --- .../blockeditormodelobject.service.js | 78 ++++++++-- .../blocklist/blocklist.component.html | 28 ++-- .../blocklist/blocklist.component.js | 138 ++++++++---------- 3 files changed, 143 insertions(+), 101 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 c3aaf1a5f5..955e026da5 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 @@ -28,13 +28,20 @@ * *
        * 
      + *     // We must get a scope that exists in all the lifetime of this data. Across variants and split-view.
      + *     var scopeOfExistence = $scope;
      + *     // Setup your component to require umbVariantContentEditors and use the method getScope to retrive a shared scope for multiple editors of this content.
      + *     if(vm.umbVariantContentEditors && vm.umbVariantContentEditors.getScope) {
      + *         scopeOfExistence = vm.umbVariantContentEditors.getScope();
      + *     }
      + * 
        *     // Define variables for layout and modelObject as you will be using these through our your property-editor.
        *     var layout;
        *     var modelObject;
        *     
        *     // When we are ready we can instantiate the Model Object can load the dependencies of it.
        *     vm.$onInit = function() {
      - *         modelObject = blockEditorService.createModelObject(vm.model.value, vm.model.editor, vm.model.config.blocks, $scope);
      + *         modelObject = blockEditorService.createModelObject(vm.model.value, vm.model.editor, vm.model.config.blocks, scopeOfExistence);
        *         modelObject.load().then(onLoaded);
        *     }
        * 
      @@ -171,13 +178,14 @@
       
           function blockEditorModelObjectFactory($interpolate, udiService, contentResource) {
       
      -
               /**
                * Simple mapping from property model content entry to editing model,
                * needs to stay simple to avoid deep watching.
                */
               function mapToElementModel(elementModel, dataModel) {
       
      +            if (!elementModel || !elementModel.variants || !elementModel.variants.length) { return; }
      +
                   var variant = elementModel.variants[0];
                   
                   for (var t = 0; t < variant.tabs.length; t++) {
      @@ -190,6 +198,7 @@
                           }
                       }
                   }
      +            
               }
       
               /**
      @@ -198,6 +207,8 @@
                */
               function mapToPropertyModel(elementModel, dataModel) {
                   
      +            if (!elementModel || !elementModel.variants || !elementModel.variants.length) { return; }
      +
                   var variant = elementModel.variants[0];
                   
                   for (var t = 0; t < variant.tabs.length; t++) {
      @@ -210,6 +221,7 @@
                           }
                       }
                   }
      +            
               }
       
               /**
      @@ -272,7 +284,7 @@
                   // Start watching each property value.
                   var variant = model.variants[0];
                   var field = forSettings ? "settings" : "content";
      -            var watcherCreator = forSettings ? createSettingsModelPropWatcher : createDataModelPropWatcher;
      +            var watcherCreator = forSettings ? createSettingsModelPropWatcher : createContentModelPropWatcher;
                   for (var t = 0; t < variant.tabs.length; t++) {
                       var tab = variant.tabs[t];
                       for (var p = 0; p < tab.properties.length; p++) {
      @@ -283,6 +295,13 @@
                           // But we like to sync non-primative values as well! Yes, and this does happen, just not through this code, but through the nature of JavaScript. 
                           // Non-primative values act as references to the same data and are therefor synced.
                           blockObject.watchers.push(isolatedScope.$watch("blockObjects._" + blockObject.key + "." + field + ".variants[0].tabs[" + t + "].properties[" + p + "].value", watcherCreator(blockObject, prop)));
      +                    
      +                    // We also like to watch our data model to be able to capture changes coming from other places.
      +                    if (forSettings === true) {
      +                        blockObject.watchers.push(isolatedScope.$watch("blockObjects._" + blockObject.key + "." + "layout.settings" + "." + prop.alias, createLayoutSettingsModelWatcher(blockObject, prop)));
      +                    } else {
      +                        blockObject.watchers.push(isolatedScope.$watch("blockObjects._" + blockObject.key + "." + "data" + "." + prop.alias, createDataModelWatcher(blockObject, prop)));
      +                    }
                       }
                   }
                   if (blockObject.watchers.length === 0) {
      @@ -291,10 +310,31 @@
                   }
               }
       
      +        /**
      +         * Used to create a prop watcher for the data in the property editor data model.
      +         */
      +        function createDataModelWatcher(blockObject, prop)  {
      +            return function() {
      +                // sync data:
      +                prop.value = blockObject.data[prop.alias];
      +
      +                blockObject.updateLabel();
      +            }
      +        }
      +        /**
      +         * Used to create a prop watcher for the settings in the property editor data model.
      +         */
      +        function createLayoutSettingsModelWatcher(blockObject, prop)  {
      +            return function() {
      +                // sync data:
      +                prop.value = blockObject.layout.settings[prop.alias];
      +            }
      +        }
      +
               /**
                * Used to create a scoped watcher for a content property on a blockObject.
                */
      -        function createDataModelPropWatcher(blockObject, prop)  {
      +        function createContentModelPropWatcher(blockObject, prop)  {
                   return function() {
                       // sync data:
                       blockObject.data[prop.alias] = prop.value;
      @@ -482,8 +522,9 @@
                    * @ngdoc method
                    * @name getBlockObject
                    * @methodOf umbraco.services.blockEditorModelObject
      -             * @description Retrieve editor friendly model of a block.
      -             * BlockObject is a class instance which setups live syncronization of content and settings models back to the data of your property editor model.
      +             * @description Retrieve a Block Object for the given layout entry.
      +             * The Block Object offers the nesecary data to display and edit a block.
      +             * The Block Object setups live syncronization of content and settings models back to the data of your Property Editor model.
                    * The returned object, named ´BlockObject´, contains several usefull models to make editing of this block happen.
                    * The ´BlockObject´ contains the following properties:
                    * - key {string}: runtime generated key, usefull for tracking of this object
      @@ -509,19 +550,32 @@
                       }
       
                       var blockConfiguration = this.getBlockConfiguration(dataModel.contentTypeKey);
      +                var contentScaffold;
       
                       if (blockConfiguration === null) {
      -                    console.error("The block entry of "+udi+" is not begin initialized cause its contentTypeKey is not allowed for this PropertyEditor")
      -                    // This is not an allowed block type, therefor we return null;
      -                    return null;
      +                    console.error("The block entry of "+udi+" is not begin initialized cause its contentTypeKey is not allowed for this PropertyEditor");
      +                } else {
      +                    var contentScaffold = this.getScaffoldFromKey(blockConfiguration.contentTypeKey);
      +                    if(contentScaffold === null) {
      +                        console.error("The block entry of "+udi+" is not begin initialized cause its Element Type was not loaded.");
      +                    }
                       }
       
      -                var contentScaffold = this.getScaffoldFromKey(blockConfiguration.contentTypeKey);
      -                if(contentScaffold === null) {
      -                    return null;
      +                if (blockConfiguration === null || contentScaffold === null) {
      +
      +                    blockConfiguration = {
      +                        label: "Unsupported Block ("+udi+")",
      +                        unsupported: true
      +                    };
      +                    contentScaffold = {};
      +                    
                       }
       
                       var blockObject = {};
      +                // Set an angularJS cloneNode method, to avoid this object begin cloned.
      +                blockObject.cloneNode = function() {
      +                    return null;// angularJS accept this as a cloned value as long as the 
      +                }
                       blockObject.key = String.CreateGuid().replace(/-/g, "");
                       blockObject.config = Utilities.copy(blockConfiguration);
                       if (blockObject.config.label && blockObject.config.label !== "") {
      diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.html
      index e3afba8cab..64ae99eeb5 100644
      --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.html
      +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.html
      @@ -4,9 +4,9 @@
       
           
      -
      +
      -
      +
      -
      +
      - + - +
      - - - - - + +
      - Minimum %0% entries, needs %1% more. + Minimum %0% entries, needs %1% more.
      -
      +
      - Maximum %0% entries, %1% too many. + Maximum %0% entries, %1% too many.
      diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.js index da62d10c87..d57b3a9012 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.js @@ -22,7 +22,8 @@ }, require: { umbProperty: "?^umbProperty", - umbVariantContent: '?^^umbVariantContent' + umbVariantContent: '?^^umbVariantContent', + umbVariantContentEditors: '?^^umbVariantContentEditors' } }); @@ -49,10 +50,9 @@ vm.currentBlockInFocus = block; block.focus = true; } - vm.showCopy = clipboardService.isSupported(); + vm.supportCopy = clipboardService.isSupported(); - var layout = [];// The layout object specific to this Block Editor, will be a direct reference from Property Model. - vm.blocks = [];// Runtime list of block models, needs to be synced to property model on form submit. + vm.layout = [];// The layout object specific to this Block Editor, will be a direct reference from Property Model. vm.availableBlockTypes = [];// Available block entries of this property editor. var labels = {}; @@ -82,9 +82,14 @@ if(typeof vm.model.value !== 'object' || vm.model.value === null) {// testing if we have null or undefined value or if the value is set to another type than Object. vm.model.value = {}; } + + var scopeOfExistence = $scope; + if(vm.umbVariantContentEditors && vm.umbVariantContentEditors.getScope) { + scopeOfExistence = vm.umbVariantContentEditors.getScope(); + } // Create Model Object, to manage our data for this Block Editor. - modelObject = blockEditorService.createModelObject(vm.model.value, vm.model.editor, vm.model.config.blocks, $scope); + modelObject = blockEditorService.createModelObject(vm.model.value, vm.model.editor, vm.model.config.blocks, scopeOfExistence); modelObject.load().then(onLoaded); copyAllBlocksAction = { @@ -124,15 +129,20 @@ function onLoaded() { // Store a reference to the layout model, because we need to maintain this model. - layout = modelObject.getLayout([]); + vm.layout = modelObject.getLayout([]); - // maps layout entries to editor friendly models aka. blockObjects. - layout.forEach(entry => { - var block = getBlockObject(entry); - - // If this entry was not supported by our property-editor it would return 'null'. - if(block !== null) { - vm.blocks.push(block); + // Append the blockObjects to our layout. + vm.layout.forEach(entry => { + if (entry.$block === undefined || entry.$block === null) { + console.log("We are creating a BlockObject for", entry.udi); + var block = getBlockObject(entry); + + // If this entry was not supported by our property-editor it would return 'null'. + if(block !== null) { + entry.$block = block; + } else { + entry.$block = blockEditorService.UNSUPPORTED_BLOCKOBJECT; + } } }); @@ -145,16 +155,25 @@ } + function getDefaultViewForBlock(block) { + + if (block.config.unsupported === true) + return "views/propertyeditors/blocklist/blocklistentryeditors/unsupportedblock/unsupportedblock.editor.html"; + + if (inlineEditing === true) + return "views/propertyeditors/blocklist/blocklistentryeditors/inlineblock/inlineblock.editor.html"; + return "views/propertyeditors/blocklist/blocklistentryeditors/labelblock/labelblock.editor.html"; + } function getBlockObject(entry) { var block = modelObject.getBlockObject(entry); if (block === null) return null; - // Lets apply fallback views, and make the view available directly on the blockObject. - block.view = (block.config.view ? "/" + block.config.view : (inlineEditing ? "views/propertyeditors/blocklist/blocklistentryeditors/inlineblock/inlineblock.editor.html" : "views/propertyeditors/blocklist/blocklistentryeditors/labelblock/labelblock.editor.html")); + block.view = (block.config.view ? "/" + block.config.view : getDefaultViewForBlock(block)); block.showSettings = block.config.settingsElementTypeKey != null; + block.showCopy = vm.supportCopy && block.config.contentTypeKey != null;// if we have content, otherwise it dosnt make sense to copy. return block; } @@ -176,11 +195,11 @@ // If we reach this line, we are good to add the layoutEntry and blockObject to our models. - // add layout entry at the decired location in layout. - layout.splice(index, 0, layoutEntry); + // Add the Block Object to our layout entry. + layoutEntry.$block = blockObject; - // apply block model at decired location in blocks. - vm.blocks.splice(index, 0, blockObject); + // add layout entry at the decired location in layout. + vm.layout.splice(index, 0, layoutEntry); // lets move focus to this new block. vm.setBlockFocus(blockObject); @@ -193,24 +212,19 @@ function deleteBlock(block) { - var index = vm.blocks.indexOf(block); - if(index !== -1) { - - var layoutIndex = layout.findIndex(entry => entry.udi === block.content.udi); - if(layoutIndex !== -1) { - layout.splice(index, 1); - } else { - throw new Error("Could not find layout entry of block with udi: "+block.content.udi) - } - - vm.blocks.splice(index, 1); - - modelObject.removeDataAndDestroyModel(block); + var layoutIndex = vm.layout.findIndex(entry => entry.udi === block.content.udi); + if(layoutIndex === -1) { + throw new Error("Could not find layout entry of block with udi: "+block.content.udi) } + vm.layout.splice(layoutIndex, 1); + modelObject.removeDataAndDestroyModel(block); + } function deleteAllBlocks() { - vm.blocks.forEach(deleteBlock); + vm.layout.forEach(entry => { + deleteBlock(entry.$block); + }); } function editBlock(blockObject, openSettings) { @@ -293,15 +307,15 @@ if(!(mouseEvent.ctrlKey || mouseEvent.metaKey)) { editorService.close(); - if (added && vm.model.config.useInlineEditingAsDefault !== true && vm.blocks.length > createIndex) { - editBlock(vm.blocks[createIndex]); + if (added && vm.model.config.useInlineEditingAsDefault !== true && vm.layout.length > createIndex) { + editBlock(vm.layout[createIndex].$block); } } }, close: function() { // if opned by a inline creator button(index less than length), we want to move the focus away, to hide line-creator. - if (createIndex < vm.blocks.length) { - vm.setBlockFocus(vm.blocks[Math.max(createIndex-1, 0)]); + if (createIndex < vm.layout.length) { + vm.setBlockFocus(vm.layout[Math.max(createIndex-1, 0)].$block); } editorService.close(); @@ -357,7 +371,7 @@ var requestCopyAllBlocks = function() { // list aliases - var aliases = vm.blocks.map(block => block.content.contentTypeAlias); + var aliases = vm.layout.map(entry => entry.$block.content.contentTypeAlias); // remove dublicates aliases = aliases.filter((item, index) => aliases.indexOf(item) === index); @@ -366,8 +380,9 @@ if(vm.umbVariantContent) { contentNodeName = vm.umbVariantContent.editor.content.name; } + // TODO: check if we are in an overlay and then lets get the Label of this block. - var elementTypesToCopy = vm.blocks.map(block => block.content); + var elementTypesToCopy = vm.layout.map(entry => entry.$block.content); localizationService.localize("clipboard_labelForArrayOfItemsFrom", [vm.model.label, contentNodeName]).then(function(localizedLabel) { clipboardService.copyArray("elementTypeArray", aliases, elementTypesToCopy, localizedLabel, "icon-thumbnail-list", vm.model.id); @@ -392,13 +407,13 @@ if (blockObject === null) { return false; } + + // set the BlockObject on our layout entry. + layoutEntry.$block = blockObject; // insert layout entry at the decired location in layout. - layout.splice(index, 0, layoutEntry); + vm.layout.splice(index, 0, layoutEntry); - // insert block model at the decired location in blocks. - vm.blocks.splice(index, 0, blockObject); - vm.currentBlockInFocus = blockObject; return true; @@ -452,11 +467,6 @@ openSettingsForBlock: openSettingsForBlock } - - - var runtimeSortVars = {}; - - vm.sorting = false; vm.sortableOptions = { axis: "y", cursor: "grabbing", @@ -466,47 +476,25 @@ distance: 5, tolerance: "pointer", scroll: true, - start: function (ev, ui) { - runtimeSortVars.moveFromIndex = ui.item.index(); - $scope.$evalAsync(function () { - vm.sorting = true; - }); - }, update: function (ev, ui) { setDirty(); }, - stop: function (ev, ui) { - - // Lets update the layout part of the property model to match the update. - var moveFromIndex = runtimeSortVars.moveFromIndex; - var moveToIndex = ui.item.index(); - - if (moveToIndex !== -1 && moveFromIndex !== moveToIndex) { - var movedEntry = layout[moveFromIndex]; - layout.splice(moveFromIndex, 1); - layout.splice(moveToIndex, 0, movedEntry); - } - - $scope.$evalAsync(function () { - vm.sorting = false; - }); - } }; function onAmountOfBlocksChanged() { // enable/disable property actions - copyAllBlocksAction.isDisabled = vm.blocks.length === 0; - deleteAllBlocksAction.isDisabled = vm.blocks.length === 0; + copyAllBlocksAction.isDisabled = vm.layout.length === 0; + deleteAllBlocksAction.isDisabled = vm.layout.length === 0; // validate limits: if (vm.propertyForm) { - var isMinRequirementGood = vm.validationLimit.min === null || vm.blocks.length >= vm.validationLimit.min; + var isMinRequirementGood = vm.validationLimit.min === null || vm.layout.length >= vm.validationLimit.min; vm.propertyForm.minCount.$setValidity("minCount", isMinRequirementGood); - var isMaxRequirementGood = vm.validationLimit.max === null || vm.blocks.length <= vm.validationLimit.max; + var isMaxRequirementGood = vm.validationLimit.max === null || vm.layout.length <= vm.validationLimit.max; vm.propertyForm.maxCount.$setValidity("maxCount", isMaxRequirementGood); } @@ -515,7 +503,7 @@ - unsubscribe.push($scope.$watch(() => vm.blocks.length, onAmountOfBlocksChanged)); + unsubscribe.push($scope.$watch(() => vm.layout.length, onAmountOfBlocksChanged)); $scope.$on("$destroy", function () { for (const subscription of unsubscribe) { From addd56f30d48a20df65ffdb267ebee946e4f73a8 Mon Sep 17 00:00:00 2001 From: Warren Buckley Date: Tue, 9 Jun 2020 06:55:06 +0000 Subject: [PATCH 205/377] Adds skipStepIfVisible property to JSON Tour Steps to allow skipping if DOM item is present (#8235) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Niels Lyngsø # Conflicts: # src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbtour.directive.js --- .../application/umbtour.directive.js | 37 ++++++++++++++----- .../BackOfficeTours/getting-started.json | 19 ++++------ src/Umbraco.Web/Models/BackOfficeTourStep.cs | 4 +- 3 files changed, 38 insertions(+), 22 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbtour.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbtour.directive.js index 287962b6d3..7cd84224f4 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbtour.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbtour.directive.js @@ -5,14 +5,14 @@ @scope @description -Added in Umbraco 7.8. The tour component is a global component and is already added to the umbraco markup. -In the Umbraco UI the tours live in the "Help drawer" which opens when you click the Help-icon in the bottom left corner of Umbraco. -You can easily add you own tours to the Help-drawer or show and start tours from +Added in Umbraco 7.8. The tour component is a global component and is already added to the umbraco markup. +In the Umbraco UI the tours live in the "Help drawer" which opens when you click the Help-icon in the bottom left corner of Umbraco. +You can easily add you own tours to the Help-drawer or show and start tours from anywhere in the Umbraco backoffice. To see a real world example of a custom tour implementation, install The Starter Kit in Umbraco 7.8

      Extending the help drawer with custom tours

      -The easiet way to add new tours to Umbraco is through the Help-drawer. All it requires is a my-tour.json file. -Place the file in App_Plugins/{MyPackage}/backoffice/tours/{my-tour}.json and it will automatically be +The easiet way to add new tours to Umbraco is through the Help-drawer. All it requires is a my-tour.json file. +Place the file in App_Plugins/{MyPackage}/backoffice/tours/{my-tour}.json and it will automatically be picked up by Umbraco and shown in the Help-drawer.

      The tour object

      @@ -26,7 +26,7 @@ The tour object consist of two parts - The overall tour configuration and a list "groupOrder": 200 // Control the order of tour groups "allowDisable": // Adds a "Don't" show this tour again"-button to the intro step "culture" : // From v7.11+. Specifies the culture of the tour (eg. en-US), if set the tour will only be shown to users with this culture set on their profile. If omitted or left empty the tour will be visible to all users - "requiredSections":["content", "media", "mySection"] // Sections that the tour will access while running, if the user does not have access to the required tour sections, the tour will not load. + "requiredSections":["content", "media", "mySection"] // Sections that the tour will access while running, if the user does not have access to the required tour sections, the tour will not load. "steps": [] // tour steps - see next example }
      @@ -43,11 +43,12 @@ The tour object consist of two parts - The overall tour configuration and a list "backdropOpacity": 0.4 // the backdrop opacity "view": "" // add a custom view "customProperties" : {} // add any custom properties needed for the custom view + "skipStepIfVisible": ".dashboard div [data-element='my-tour-button']" // if we can find this DOM element on the page then we will skip this step }

      Adding tours to other parts of the Umbraco backoffice

      -It is also possible to add a list of custom tours to other parts of the Umbraco backoffice, +It is also possible to add a list of custom tours to other parts of the Umbraco backoffice, as an example on a Dashboard in a Custom section. You can then use the {@link umbraco.services.tourService tourService} to start and stop tours but you don't have to register them as part of the tour service.

      Using the tour service

      @@ -86,7 +87,8 @@ as an example on a Dashboard in a Custom section. You can then use the {@link um "element": "[data-element='my-tour-button']", "title": "Click the button", "content": "Click the button", - "event": "click" + "event": "click", + "skipStepIfVisible": "[data-element='my-other-tour-button']" } ] }; @@ -257,9 +259,26 @@ In the following example you see how to run some custom logic before a step goes // make sure we don't go too far if(scope.model.currentStepIndex !== scope.model.steps.length) { + + var upcomingStep = scope.model.steps[scope.model.currentStepIndex]; + + // If the currentStep JSON object has 'skipStepIfVisible' + // It's a DOM selector - if we find it then we ship over this step + if(upcomingStep.skipStepIfVisible) { + let tryFindDomEl = document.querySelector(upcomingStep.element); + if(tryFindDomEl) { + // check if element is visible: + if( tryFindDomEl.offsetWidth || tryFindDomEl.offsetHeight || tryFindDomEl.getClientRects().length ) { + // if it was visible then we skip the step. + nextStep(); + } + } + } + startStep(); - // tour completed - final step + // tour completed - final step } else { + // tour completed - final step scope.loadingStep = true; waitForPendingRerequests().then(function(){ diff --git a/src/Umbraco.Web.UI/config/BackOfficeTours/getting-started.json b/src/Umbraco.Web.UI/config/BackOfficeTours/getting-started.json index 7b3f2a2184..bba88b1b08 100644 --- a/src/Umbraco.Web.UI/config/BackOfficeTours/getting-started.json +++ b/src/Umbraco.Web.UI/config/BackOfficeTours/getting-started.json @@ -184,27 +184,21 @@ "event": "click" }, { - "element": "[data-element~='editor-data-type-picker']", + "element": "[ng-controller*='Umbraco.Editors.DataTypePickerController'] [data-element='editor-data-type-picker']", "elementPreventClick": true, "title": "Editor picker", - "content": "

      In the editor picker dialog we can pick one of the many built-in editors.

      You can choose from preconfigured data types (Reuse) or create a new configuration (Available editors).

      " + "content": "

      In the editor picker dialog we can pick one of the many built-in editors.

      " }, { - "element": "[data-element~='editor-data-type-picker'] [data-element='editor-Textarea']", + "element": "[data-element~='editor-data-type-picker'] [data-element='datatype-Textarea']", "title": "Select editor", "content": "Select the Textarea editor. This will add a textarea to the Welcome Text property.", "event": "click" }, { - "element": "[data-element~='editor-data-type-settings']", - "elementPreventClick": true, + "element": "[data-element='editor-data-type-picker'] [data-element='datatypeconfig-Textarea'] > a", "title": "Editor settings", - "content": "Each property editor can have individual settings. For the textarea editor you can set a character limit but in this case it is not needed." - }, - { - "element": "[data-element~='editor-data-type-settings'] [data-element='button-submit']", - "title": "Save editor", - "content": "Click Submit to save the changes.", + "content": "Each property editor can have individual settings. For the textarea editor you can set a character limit but in this case it is not needed.", "event": "click" }, { @@ -313,7 +307,8 @@ "content": "

      To see all our templates click the small triangle to the left of the templates node.

      ", "event": "click", "eventElement": "#tree [data-element='tree-item-templates'] [data-element='tree-item-expand']", - "view": "templatetree" + "view": "templatetree", + "skipStepIfVisible": "#tree [data-element='tree-item-templates'] > div > button[data-element=tree-item-expand].icon-navigation-down" }, { "element": "#tree [data-element='tree-item-templates'] [data-element='tree-item-Home Page']", diff --git a/src/Umbraco.Web/Models/BackOfficeTourStep.cs b/src/Umbraco.Web/Models/BackOfficeTourStep.cs index a64bf15b7f..c21b09523d 100644 --- a/src/Umbraco.Web/Models/BackOfficeTourStep.cs +++ b/src/Umbraco.Web/Models/BackOfficeTourStep.cs @@ -29,5 +29,7 @@ namespace Umbraco.Web.Models public string EventElement { get; set; } [DataMember(Name = "customProperties")] public JObject CustomProperties { get; set; } + [DataMember(Name = "skipStepIfVisible")] + public string SkipStepIfVisible { get; set; } } -} \ No newline at end of file +} From 6cfb710925c09e12163595e24788ca71f99d2d1d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Wed, 17 Jun 2020 10:06:39 +0200 Subject: [PATCH 206/377] removed trailing comma --- .../src/views/propertyeditors/blocklist/blocklist.component.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.js index d57b3a9012..4d170009d5 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.js @@ -478,7 +478,7 @@ scroll: true, update: function (ev, ui) { setDirty(); - }, + } }; From 6a8ceb7af9c8e2d9ae116d8a21271756cd43cf09 Mon Sep 17 00:00:00 2001 From: Warren Buckley Date: Wed, 17 Jun 2020 09:07:17 +0100 Subject: [PATCH 207/377] Set Version to 8.6.3 .\build SetUmbracoVersion 8.6.3 --- src/SolutionInfo.cs | 4 ++-- src/Umbraco.Web.UI/Umbraco.Web.UI.csproj | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/SolutionInfo.cs b/src/SolutionInfo.cs index 07be28a87c..9294203699 100644 --- a/src/SolutionInfo.cs +++ b/src/SolutionInfo.cs @@ -18,5 +18,5 @@ using System.Resources; [assembly: AssemblyVersion("8.0.0")] // these are FYI and changed automatically -[assembly: AssemblyFileVersion("8.6.2")] -[assembly: AssemblyInformationalVersion("8.6.2")] +[assembly: AssemblyFileVersion("8.6.3")] +[assembly: AssemblyInformationalVersion("8.6.3")] diff --git a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj index 59a37bb0e0..e8ee470b53 100644 --- a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj +++ b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj @@ -345,9 +345,9 @@ False True - 8620 + 8630 / - http://localhost:8620 + http://localhost:8630 False False From 0121e3fde3fe8420d2fb946d8e2bd904c47d7678 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Wed, 17 Jun 2020 11:53:55 +0200 Subject: [PATCH 208/377] Unsupported block --- .../blockeditormodelobject.service.js | 2 +- .../unsupportedblock.editor.html | 17 +++-- .../unsupportedblock.editor.less | 65 ++++++++++++------- 3 files changed, 57 insertions(+), 27 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 955e026da5..889dd40fab 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 @@ -564,7 +564,7 @@ if (blockConfiguration === null || contentScaffold === null) { blockConfiguration = { - label: "Unsupported Block ("+udi+")", + label: "Unsupported Block", unsupported: true }; contentScaffold = {}; 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 bdd7341528..61488ac859 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 @@ -1,4 +1,13 @@ - +
      +
      + + {{block.label}} +
      +
      + This Block 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 on Our +
      Block data:
      +
      
      +    
      +
      diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklistentryeditors/unsupportedblock/unsupportedblock.editor.less b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklistentryeditors/unsupportedblock/unsupportedblock.editor.less index 214a8399fd..672a6d3ff4 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklistentryeditors/unsupportedblock/unsupportedblock.editor.less +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklistentryeditors/unsupportedblock/unsupportedblock.editor.less @@ -5,32 +5,53 @@ margin-bottom: 4px; margin-top: 4px; width: 100%; - min-height: 48px; border: 1px solid @gray-9; border-radius: @baseBorderRadius; - cursor: pointer; - color: @ui-action-discreet-type; + > .__header { - text-align: left; - padding-left: 20px; - padding-bottom: 2px; - - user-select: none; - - transition: border-color 120ms; - - background-color: @ui-btn-negative; - color: @ui-btn-negative-type; - - i { - font-size: 22px; - margin-right: 5px; - display: inline-block; - vertical-align: middle; + display: flex; + align-items: center; + + padding-left: 20px; + padding-bottom: 2px; + min-height: 48px; + border-bottom: 1px solid @gray-9; + + background-color: @ui-btn-negative; + color: @ui-btn-negative-type; + + i { + font-size: 22px; + margin-right: 5px; + display: inline-block; + vertical-align: middle; + } + span { + display: inline-block; + vertical-align: middle; + } } - span { - display: inline-block; - vertical-align: middle; + + > .__body { + + padding: 20px; + + background-color: @red-washed; + + a { + text-decoration: underline; + color: @ui-action-type; + &:hover { + color:@ui-action-type-hover; + } + } + + pre { + border: none; + padding: 0; + background-color: transparent; + } + } } From 950d64ff17505e57341839393b5843c53bd4d83a Mon Sep 17 00:00:00 2001 From: Warren Buckley Date: Wed, 17 Jun 2020 11:49:20 +0100 Subject: [PATCH 209/377] Revert changes made to tour from 8.7 due to UX changes --- .../config/BackOfficeTours/getting-started.json | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/src/Umbraco.Web.UI/config/BackOfficeTours/getting-started.json b/src/Umbraco.Web.UI/config/BackOfficeTours/getting-started.json index bba88b1b08..aa76e42b09 100644 --- a/src/Umbraco.Web.UI/config/BackOfficeTours/getting-started.json +++ b/src/Umbraco.Web.UI/config/BackOfficeTours/getting-started.json @@ -184,21 +184,27 @@ "event": "click" }, { - "element": "[ng-controller*='Umbraco.Editors.DataTypePickerController'] [data-element='editor-data-type-picker']", + "element": "[data-element~='editor-data-type-picker']", "elementPreventClick": true, "title": "Editor picker", - "content": "

      In the editor picker dialog we can pick one of the many built-in editors.

      " + "content": "

      In the editor picker dialog we can pick one of the many built-in editors.

      You can choose from preconfigured data types (Reuse) or create a new configuration (Available editors).

      " }, { - "element": "[data-element~='editor-data-type-picker'] [data-element='datatype-Textarea']", + "element": "[data-element~='editor-data-type-picker'] [data-element='editor-Textarea']", "title": "Select editor", "content": "Select the Textarea editor. This will add a textarea to the Welcome Text property.", "event": "click" }, { - "element": "[data-element='editor-data-type-picker'] [data-element='datatypeconfig-Textarea'] > a", + "element": "[data-element~='editor-data-type-settings']", + "elementPreventClick": true, "title": "Editor settings", - "content": "Each property editor can have individual settings. For the textarea editor you can set a character limit but in this case it is not needed.", + "content": "Each property editor can have individual settings. For the textarea editor you can set a character limit but in this case it is not needed." + }, + { + "element": "[data-element~='editor-data-type-settings'] [data-element='button-submit']", + "title": "Save editor", + "content": "Click Submit to save the changes.", "event": "click" }, { From ac394619fcdd0907f91fcbcf264f23ba98a82998 Mon Sep 17 00:00:00 2001 From: Warren Buckley Date: Wed, 17 Jun 2020 12:46:11 +0100 Subject: [PATCH 210/377] Fix SkipStep in tour directive was looking at wrong DOM selector it needs to be skipStepIfVisible --- .../directives/components/application/umbtour.directive.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbtour.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbtour.directive.js index 7cd84224f4..3e729e3ffe 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbtour.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbtour.directive.js @@ -264,13 +264,14 @@ In the following example you see how to run some custom logic before a step goes // If the currentStep JSON object has 'skipStepIfVisible' // It's a DOM selector - if we find it then we ship over this step - if(upcomingStep.skipStepIfVisible) { - let tryFindDomEl = document.querySelector(upcomingStep.element); - if(tryFindDomEl) { + if (upcomingStep.skipStepIfVisible) { + let tryFindDomEl = document.querySelector(upcomingStep.skipStepIfVisible); + if (tryFindDomEl) { // check if element is visible: if( tryFindDomEl.offsetWidth || tryFindDomEl.offsetHeight || tryFindDomEl.getClientRects().length ) { // if it was visible then we skip the step. nextStep(); + return; } } } From f7167542539a8c32d91edd6aa32a19e8dc63f08c Mon Sep 17 00:00:00 2001 From: Warren Buckley Date: Wed, 17 Jun 2020 12:50:31 +0100 Subject: [PATCH 211/377] Remove duplicate comment --- .../directives/components/application/umbtour.directive.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbtour.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbtour.directive.js index 3e729e3ffe..1f779b09c9 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbtour.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/application/umbtour.directive.js @@ -277,7 +277,6 @@ In the following example you see how to run some custom logic before a step goes } startStep(); - // tour completed - final step } else { // tour completed - final step scope.loadingStep = true; From e6e6d94b7835680dc66f382d4d803f718760cac4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Wed, 17 Jun 2020 15:31:46 +0200 Subject: [PATCH 212/377] Dont copy unsupported blocks --- .../views/propertyeditors/blocklist/blocklist.component.js | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.js index 4d170009d5..ab97382571 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.js @@ -134,7 +134,6 @@ // Append the blockObjects to our layout. vm.layout.forEach(entry => { if (entry.$block === undefined || entry.$block === null) { - console.log("We are creating a BlockObject for", entry.udi); var block = getBlockObject(entry); // If this entry was not supported by our property-editor it would return 'null'. @@ -369,9 +368,11 @@ } var requestCopyAllBlocks = function() { + + var elementTypesToCopy = vm.layout.filter(entry => entry.$block.config.unsupported !== true).map(entry => entry.$block.content); // list aliases - var aliases = vm.layout.map(entry => entry.$block.content.contentTypeAlias); + var aliases = elementTypesToCopy.map(content => content.contentTypeAlias); // remove dublicates aliases = aliases.filter((item, index) => aliases.indexOf(item) === index); @@ -382,8 +383,6 @@ } // TODO: check if we are in an overlay and then lets get the Label of this block. - var elementTypesToCopy = vm.layout.map(entry => entry.$block.content); - localizationService.localize("clipboard_labelForArrayOfItemsFrom", [vm.model.label, contentNodeName]).then(function(localizedLabel) { clipboardService.copyArray("elementTypeArray", aliases, elementTypesToCopy, localizedLabel, "icon-thumbnail-list", vm.model.id); }); From e0ae0c084be9b16255955852d27fb416b51057df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Wed, 17 Jun 2020 15:31:59 +0200 Subject: [PATCH 213/377] use grey color for unsupported block --- .../unsupportedblock/unsupportedblock.editor.less | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklistentryeditors/unsupportedblock/unsupportedblock.editor.less b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklistentryeditors/unsupportedblock/unsupportedblock.editor.less index 672a6d3ff4..aff741e278 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklistentryeditors/unsupportedblock/unsupportedblock.editor.less +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklistentryeditors/unsupportedblock/unsupportedblock.editor.less @@ -2,6 +2,7 @@ position: relative; display: block; + box-sizing: border-box; margin-bottom: 4px; margin-top: 4px; width: 100%; @@ -18,8 +19,8 @@ min-height: 48px; border-bottom: 1px solid @gray-9; - background-color: @ui-btn-negative; - color: @ui-btn-negative-type; + background-color: @gray-11; + color: @gray-6; i { font-size: 22px; @@ -37,7 +38,7 @@ padding: 20px; - background-color: @red-washed; + background-color: @gray-11; a { text-decoration: underline; From 73f066ec92c92698fe07800946082ce52e92e772 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Thu, 18 Jun 2020 09:00:26 +0200 Subject: [PATCH 214/377] text correction --- .../unsupportedblock/unsupportedblock.editor.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 61488ac859..84d5dd17b7 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 @@ -6,7 +6,7 @@
      This Block 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 on Our + Learn about this circumstance
      Block data:
      
           
      From 685d1047fab63d855b9fc34952459d244ae3802a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Thu, 18 Jun 2020 09:00:39 +0200 Subject: [PATCH 215/377] we dont have this fallback anymore --- .../src/views/propertyeditors/blocklist/blocklist.component.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.js index ab97382571..38e2ab2043 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.js @@ -139,8 +139,6 @@ // If this entry was not supported by our property-editor it would return 'null'. if(block !== null) { entry.$block = block; - } else { - entry.$block = blockEditorService.UNSUPPORTED_BLOCKOBJECT; } } }); From 1aca60e9c71a6a0f85984a65977374a5db4cfa96 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Thu, 18 Jun 2020 09:00:47 +0200 Subject: [PATCH 216/377] sort properties --- .../PropertyEditors/BlockListConfiguration.cs | 27 ++++++++++--------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/src/Umbraco.Web/PropertyEditors/BlockListConfiguration.cs b/src/Umbraco.Web/PropertyEditors/BlockListConfiguration.cs index 7b0f98903c..fdc31a5ada 100644 --- a/src/Umbraco.Web/PropertyEditors/BlockListConfiguration.cs +++ b/src/Umbraco.Web/PropertyEditors/BlockListConfiguration.cs @@ -11,22 +11,10 @@ namespace Umbraco.Web.PropertyEditors public class BlockListConfiguration { + [ConfigurationField("blocks", "Available Blocks", "views/propertyeditors/blocklist/prevalue/blocklist.blockconfiguration.html", Description = "Define the available blocks.")] public BlockConfiguration[] Blocks { get; set; } - - [ConfigurationField("validationLimit", "Amount", "numberrange", Description = "Set a required range of blocks")] - public NumberRange ValidationLimit { get; set; } = new NumberRange(); - - public class NumberRange - { - [JsonProperty("min")] - public int? Min { get; set; } - - [JsonProperty("max")] - public int? Max { get; set; } - } - public class BlockConfiguration { @@ -58,11 +46,24 @@ namespace Umbraco.Web.PropertyEditors public string EditorSize { get; set; } } + [ConfigurationField("validationLimit", "Amount", "numberrange", Description = "Set a required range of blocks")] + public NumberRange ValidationLimit { get; set; } = new NumberRange(); + + public class NumberRange + { + [JsonProperty("min")] + public int? Min { get; set; } + + [JsonProperty("max")] + public int? Max { get; set; } + } + [ConfigurationField("useInlineEditingAsDefault", "Inline editing mode", "boolean", Description = "Use the inline editor as the default block view.")] public bool UseInlineEditingAsDefault { get; set; } [ConfigurationField("maxPropertyWidth", "Property editor width", "textstring", Description = "optional css overwrite, example: 800px or 100%")] public string MaxPropertyWidth { get; set; } + } } From c99798ac1b9e236cd56a4375f41ea7738eb30a0c Mon Sep 17 00:00:00 2001 From: Warren Buckley Date: Wed, 10 Jun 2020 14:33:32 +0100 Subject: [PATCH 217/377] Allows unpublished content to render a macro HTML in the RTE (cherry picked from commit 3680f22321c77b8732fbc4d94c26e399d19e6cdd) --- src/Umbraco.Web/Editors/MacroRenderingController.cs | 2 +- src/Umbraco.Web/IUmbracoComponentRenderer.cs | 13 +++++++++++++ src/Umbraco.Web/UmbracoComponentRenderer.cs | 9 +++++++++ 3 files changed, 23 insertions(+), 1 deletion(-) diff --git a/src/Umbraco.Web/Editors/MacroRenderingController.cs b/src/Umbraco.Web/Editors/MacroRenderingController.cs index 7aabff6822..33a58328b9 100644 --- a/src/Umbraco.Web/Editors/MacroRenderingController.cs +++ b/src/Umbraco.Web/Editors/MacroRenderingController.cs @@ -142,7 +142,7 @@ namespace Umbraco.Web.Editors //need to create a specific content result formatted as HTML since this controller has been configured //with only json formatters. result.Content = new StringContent( - _componentRenderer.RenderMacro(pageId, m.Alias, macroParams).ToString(), + _componentRenderer.RenderMacroForContent(publishedContent, m.Alias, macroParams).ToString(), Encoding.UTF8, "text/html"); diff --git a/src/Umbraco.Web/IUmbracoComponentRenderer.cs b/src/Umbraco.Web/IUmbracoComponentRenderer.cs index 4dc9036e6b..f05afe3f6b 100644 --- a/src/Umbraco.Web/IUmbracoComponentRenderer.cs +++ b/src/Umbraco.Web/IUmbracoComponentRenderer.cs @@ -42,5 +42,18 @@ namespace Umbraco.Web /// The parameters. /// IHtmlString RenderMacro(int contentId, string alias, IDictionary parameters); + + /// + /// Renders the macro with the specified alias, passing in the specified parameters. + /// + /// An IPublishedContent to use for the context for the macro rendering + /// The alias. + /// The parameters. + /// A raw HTML string of the macro output + /// + /// Currently only used when the node is unpublished and unable to get the contentId item from the + /// content cache as its unpublished. This deals with taking in a preview/draft version of the content node + /// + IHtmlString RenderMacroForContent(IPublishedContent content, string alias, IDictionary parameters); } } diff --git a/src/Umbraco.Web/UmbracoComponentRenderer.cs b/src/Umbraco.Web/UmbracoComponentRenderer.cs index 0373c73724..aa4cbefe7b 100644 --- a/src/Umbraco.Web/UmbracoComponentRenderer.cs +++ b/src/Umbraco.Web/UmbracoComponentRenderer.cs @@ -102,6 +102,15 @@ namespace Umbraco.Web return RenderMacro(content, alias, parameters); } + + public IHtmlString RenderMacroForContent(IPublishedContent content, string alias, IDictionary parameters) + { + if(content == null) + throw new InvalidOperationException("Cannot render a macro, IPublishedContent is null"); + + return RenderMacro(content, alias, parameters); + } + /// /// Renders the macro with the specified alias, passing in the specified parameters. /// From 9f566f8d70598975a96169fa39ddac0364318f8f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Thu, 18 Jun 2020 09:20:56 +0200 Subject: [PATCH 218/377] Text change --- .../prevalue/blocklist.blockconfiguration.overlay.html | 4 ++-- src/Umbraco.Web.UI/Umbraco/config/lang/en.xml | 4 ++-- src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/blocklist.blockconfiguration.overlay.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/blocklist.blockconfiguration.overlay.html index 4aaf01fa3b..8313dcf15e 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/blocklist.blockconfiguration.overlay.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/blocklist.blockconfiguration.overlay.html @@ -18,7 +18,7 @@
      - Block appearance + Editor appearance
      @@ -148,7 +148,7 @@
      - Showcase + Catalogue appearance
      diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml index fe260cf562..aab0b2247d 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml @@ -2442,9 +2442,9 @@ To manage your website, simply open the Umbraco back office and start adding con Create new Custom stylesheet Add stylesheet - Block apperance + Editor apperance Data models - Showcase + Catalogue appearance Background color Icon color Content model 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 75748d7fb7..ee5d616b3a 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml @@ -2461,9 +2461,9 @@ To manage your website, simply open the Umbraco back office and start adding con Create new Custom stylesheet Add stylesheet - Block apperance + Editor apperance Data models - Showcase + Catalogue appearance Background color Icon color Content model From 932c6a6cc5f37a42585da619054eb7e0ec7d7a21 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Thu, 18 Jun 2020 12:21:04 +0200 Subject: [PATCH 219/377] css correction --- .../blockeditor/blockcard/blockcard.component.less | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blockeditor/blockcard/blockcard.component.less b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blockeditor/blockcard/blockcard.component.less index 1937cf4866..b1260e198b 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blockeditor/blockcard/blockcard.component.less +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blockeditor/blockcard/blockcard.component.less @@ -4,8 +4,6 @@ umb-block-card { display: inline-block; width: 100%; height: auto; - margin-right: 20px; - margin-bottom: 20px; background-color: white; border-radius: @doubleBorderRadius; box-shadow: 0 1px 2px rgba(0,0,0,.2); From f71daf72d965330368441ead2abb8b7b2cb44dbd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Thu, 18 Jun 2020 12:31:14 +0200 Subject: [PATCH 220/377] use active to determine if an inline editor is expanded. To enable the inline editor to be expanded on creation. --- .../propertyeditors/blocklist/blocklist.component.html | 2 +- .../views/propertyeditors/blocklist/blocklist.component.js | 6 +++++- .../propertyeditors/blocklist/blocklist.component.less | 2 +- .../inlineblock/inlineblock.editor.controller.js | 2 +- .../inlineblock/inlineblock.editor.html | 2 +- .../inlineblock/inlineblock.editor.less | 4 ++-- 6 files changed, 11 insertions(+), 7 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.html index 64ae99eeb5..ea3537b52a 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.html @@ -18,7 +18,7 @@
      +
      -
      +
      diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.js index 38e2ab2043..533afe079c 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.js @@ -228,6 +228,10 @@ blockObject.active = true; + if (inlineEditing === true && openSettings !== true) { + return; + } + // make a clone to avoid editing model directly. var blockContentClone = Utilities.copy(blockObject.content); var blockSettingsClone = null; @@ -304,7 +308,7 @@ if(!(mouseEvent.ctrlKey || mouseEvent.metaKey)) { editorService.close(); - if (added && vm.model.config.useInlineEditingAsDefault !== true && vm.layout.length > createIndex) { + if (added && vm.layout.length > createIndex) { editBlock(vm.layout[createIndex].$block); } } diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.less b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.less index d3bfc110c1..81af1fcf54 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.less +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.less @@ -24,7 +24,7 @@ &:hover, &:focus, &:focus-within, - &.--open { + &.--active { > .umb-block-list__block--actions { opacity: 1; diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklistentryeditors/inlineblock/inlineblock.editor.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklistentryeditors/inlineblock/inlineblock.editor.controller.js index 4dba0158ae..ead82f72d5 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklistentryeditors/inlineblock/inlineblock.editor.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklistentryeditors/inlineblock/inlineblock.editor.controller.js @@ -6,7 +6,7 @@ const bc = this; bc.openBlock = function(block) { - block.isOpen = !block.isOpen; + block.active = !block.active; } } diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklistentryeditors/inlineblock/inlineblock.editor.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklistentryeditors/inlineblock/inlineblock.editor.html index 3ad7d58409..60b3542d6c 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklistentryeditors/inlineblock/inlineblock.editor.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklistentryeditors/inlineblock/inlineblock.editor.html @@ -4,7 +4,7 @@ {{block.label}} -
      +
      diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklistentryeditors/inlineblock/inlineblock.editor.less b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklistentryeditors/inlineblock/inlineblock.editor.less index c1e2c72de3..47aa5b62ca 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklistentryeditors/inlineblock/inlineblock.editor.less +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklistentryeditors/inlineblock/inlineblock.editor.less @@ -7,7 +7,7 @@ border-radius: @baseBorderRadius; transition: border-color 120ms; - .umb-block-list__block:not(.--open) &:hover { + .umb-block-list__block:not(.--active) &:hover { border-color: @gray-8; } @@ -44,7 +44,7 @@ } } - .umb-block-list__block.--open & { + .umb-block-list__block.--active & { border-color: @gray-8; box-shadow: 0 0 2px 0px rgba(0, 0, 0, 0.05); > button { From edf84ca7d7a3c61e11dd776b34b70383d10c74a7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Fri, 19 Jun 2020 09:15:04 +0200 Subject: [PATCH 221/377] using udi instead of key. --- .../blockeditormodelobject.service.js | 45 +++++++++---------- .../blocklist/blocklist.component.html | 2 +- 2 files changed, 23 insertions(+), 24 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 889dd40fab..e623c93efd 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 @@ -294,17 +294,17 @@ // Do notice that it is not performing a deep watch, meaning that we are only watching primatives and changes directly to the object of property-value. // But we like to sync non-primative values as well! Yes, and this does happen, just not through this code, but through the nature of JavaScript. // Non-primative values act as references to the same data and are therefor synced. - blockObject.watchers.push(isolatedScope.$watch("blockObjects._" + blockObject.key + "." + field + ".variants[0].tabs[" + t + "].properties[" + p + "].value", watcherCreator(blockObject, prop))); + blockObject.__watchers.push(isolatedScope.$watch("blockObjects._" + blockObject.key + "." + field + ".variants[0].tabs[" + t + "].properties[" + p + "].value", watcherCreator(blockObject, prop))); // We also like to watch our data model to be able to capture changes coming from other places. if (forSettings === true) { - blockObject.watchers.push(isolatedScope.$watch("blockObjects._" + blockObject.key + "." + "layout.settings" + "." + prop.alias, createLayoutSettingsModelWatcher(blockObject, prop))); + blockObject.__watchers.push(isolatedScope.$watch("blockObjects._" + blockObject.key + "." + "layout.settings" + "." + prop.alias, createLayoutSettingsModelWatcher(blockObject, prop))); } else { - blockObject.watchers.push(isolatedScope.$watch("blockObjects._" + blockObject.key + "." + "data" + "." + prop.alias, createDataModelWatcher(blockObject, prop))); + blockObject.__watchers.push(isolatedScope.$watch("blockObjects._" + blockObject.key + "." + "data" + "." + prop.alias, createDataModelWatcher(blockObject, prop))); } } } - if (blockObject.watchers.length === 0) { + if (blockObject.__watchers.length === 0) { // If no watcher where created, it means we have no properties to watch. This means that nothing will activate our generate the label, since its only triggered by watchers. blockObject.updateLabel(); } @@ -527,13 +527,13 @@ * The Block Object setups live syncronization of content and settings models back to the data of your Property Editor model. * The returned object, named ´BlockObject´, contains several usefull models to make editing of this block happen. * The ´BlockObject´ contains the following properties: - * - key {string}: runtime generated key, usefull for tracking of this object - * - content {Object}: Content model, the content type model for content merged with the content data of this block. - * - settings {Object}: Settings model, the content type model for settings merged with the settings data of this block. - * - config {Object}: A deep copy of the block configuration model. + * - content {Object}: Content model, the content data in a ElementType model. + * - settings {Object}: Settings model, the settings data in a ElementType model. + * - config {Object}: A local deep copy of the block configuration model. * - label {string}: The label for this block. * - updateLabel {Method}: Method to trigger an update of the label for this block. - * - data {Object}: A reference to the data object from your property editor model. + * - data {Object}: A reference to the content data object from your property editor model. + * - settingsData {Object}: A reference to the settings data object from your property editor model. * - layout {Object}: A refernce to the layout entry from your property editor model. * @param {Object} layoutEntry the layout entry object to build the block model from. * @return {Object | null} The BlockObject for the given layout entry. Or null if data or configuration wasnt found for this block. @@ -576,7 +576,6 @@ blockObject.cloneNode = function() { return null;// angularJS accept this as a cloned value as long as the } - blockObject.key = String.CreateGuid().replace(/-/g, ""); blockObject.config = Utilities.copy(blockConfiguration); if (blockObject.config.label && blockObject.config.label !== "") { blockObject.labelInterpolator = $interpolate(blockObject.config.label); @@ -594,22 +593,22 @@ blockObject.data = dataModel; blockObject.layout = layoutEntry; - blockObject.watchers = []; + blockObject.__watchers = []; + layoutEntry.settings = null; if (blockConfiguration.settingsElementTypeKey) { var settingsScaffold = this.getScaffoldFromKey(blockConfiguration.settingsElementTypeKey); - if (settingsScaffold === null) { - return null; - } + if (settingsScaffold !== null) { + + blockObject.settingsData = layoutEntry.settings; - // make basics from scaffold - blockObject.settings = Utilities.copy(settingsScaffold); - layoutEntry.settings = layoutEntry.settings || {}; - if (!layoutEntry.settings.key) { layoutEntry.settings.key = String.CreateGuid(); } - if (!layoutEntry.settings.contentTypeKey) { layoutEntry.settings.contentTypeKey = blockConfiguration.settingsElementTypeKey; } - mapToElementModel(blockObject.settings, layoutEntry.settings); - } else { - layoutEntry.settings = null; + // make basics from scaffold + blockObject.settings = Utilities.copy(settingsScaffold); + layoutEntry.settings = layoutEntry.settings || {}; + if (!layoutEntry.settings.key) { layoutEntry.settings.key = String.CreateGuid(); } + if (!layoutEntry.settings.contentTypeKey) { layoutEntry.settings.contentTypeKey = blockConfiguration.settingsElementTypeKey; } + mapToElementModel(blockObject.settings, layoutEntry.settings); + } } blockObject.retriveValuesFrom = function(content, settings) { @@ -656,7 +655,7 @@ destroyBlockObject: function(blockObject) { // remove property value watchers: - blockObject.watchers.forEach(w => { w(); }); + blockObject.__watchers.forEach(w => { w(); }); // remove model from isolatedScope. delete this.isolatedScope.blockObjects[blockObject.key]; diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.html index ea3537b52a..5c6ab259ac 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.html @@ -6,7 +6,7 @@
      -
      +
      /// /// - protected IEnumerable GetNestedValidationResults(IEnumerable elements) + protected IEnumerable GetNestedValidationResults(IEnumerable elements) { foreach (var row in elements) { - var nestedValidation = new List(); + var elementTypeValidationResult = new ElementTypeValidationResult(row.ElementTypeAlias); foreach (var prop in row.PropertyTypeValidation) { + var propValidationResult = new PropertyTypeValidationResult(prop.PropertyType.Alias); + foreach (var validationResult in _propertyValidationService.ValidatePropertyValue(prop.PropertyType, prop.PostedValue)) { - nestedValidation.Add(validationResult); + // add the result to the property results + propValidationResult.ValidationResults.Add(validationResult); } + + // add the property results to the element type results + elementTypeValidationResult.ValidationResults.Add(propValidationResult); } - if (nestedValidation.Count > 0) + if (elementTypeValidationResult.ValidationResults.Count > 0) { - yield return new ValidationResultCollection(nestedValidation.ToArray()); + yield return elementTypeValidationResult; } } } public class PropertyTypeValidationModel { - public PropertyTypeValidationModel(object postedValue, PropertyType propertyType) + public PropertyTypeValidationModel(PropertyType propertyType, object postedValue) { PostedValue = postedValue ?? throw new ArgumentNullException(nameof(postedValue)); PropertyType = propertyType ?? throw new ArgumentNullException(nameof(propertyType)); @@ -90,9 +96,17 @@ namespace Umbraco.Web.PropertyEditors public class ElementTypeValidationModel { + public ElementTypeValidationModel(string elementTypeAlias) + { + ElementTypeAlias = elementTypeAlias; + } + private List _list = new List(); + public IEnumerable PropertyTypeValidation => _list; + public string ElementTypeAlias { get; } + public void AddPropertyTypeValidation(PropertyTypeValidationModel propValidation) => _list.Add(propValidation); } } diff --git a/src/Umbraco.Web/PropertyEditors/NestedContentPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/NestedContentPropertyEditor.cs index 5bcb5cec0f..4f5616e3d7 100644 --- a/src/Umbraco.Web/PropertyEditors/NestedContentPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/NestedContentPropertyEditor.cs @@ -269,10 +269,11 @@ namespace Umbraco.Web.PropertyEditors { foreach (var row in _nestedContentValues.GetPropertyValues(value)) { - var elementValidation = new ElementTypeValidationModel(); + var elementValidation = new ElementTypeValidationModel(row.ContentTypeAlias); foreach (var prop in row.PropertyValues) { - elementValidation.AddPropertyTypeValidation(new PropertyTypeValidationModel(prop.Value.Value, prop.Value.PropertyType)); + elementValidation.AddPropertyTypeValidation( + new PropertyTypeValidationModel(prop.Value.PropertyType, prop.Value.Value)); } yield return elementValidation; } diff --git a/src/Umbraco.Web/PropertyEditors/Validation/NestedValidationResults.cs b/src/Umbraco.Web/PropertyEditors/Validation/NestedValidationResults.cs index 9ae11c1c38..26a47602db 100644 --- a/src/Umbraco.Web/PropertyEditors/Validation/NestedValidationResults.cs +++ b/src/Umbraco.Web/PropertyEditors/Validation/NestedValidationResults.cs @@ -3,15 +3,28 @@ using System.ComponentModel.DataAnnotations; namespace Umbraco.Web.PropertyEditors.Validation { - public class ValidationResultCollection : ValidationResult + public class PropertyTypeValidationResult : ValidationResult { - public ValidationResultCollection(params ValidationResult[] nested) + public PropertyTypeValidationResult(string propertyTypeAlias) : base(string.Empty) { - ValidationResults = new List(nested); + PropertyTypeAlias = propertyTypeAlias; } - public IList ValidationResults { get; } + public IList ValidationResults { get; } = new List(); + public string PropertyTypeAlias { get; } + } + + public class ElementTypeValidationResult : ValidationResult + { + public ElementTypeValidationResult(string elementTypeAlias) + : base(string.Empty) + { + ElementTypeAlias = elementTypeAlias; + } + + public IList ValidationResults { get; } = new List(); + public string ElementTypeAlias { get; } } /// @@ -27,11 +40,6 @@ namespace Umbraco.Web.PropertyEditors.Validation { } - public void AddElementTypeValidationResults(ValidationResultCollection resultCollection) - { - ValidationResults.Add(resultCollection); - } - - public IList ValidationResults { get; } = new List(); + public IList ValidationResults { get; } = new List(); } } diff --git a/src/Umbraco.Web/PropertyEditors/Validation/ValidationResultConverter.cs b/src/Umbraco.Web/PropertyEditors/Validation/ValidationResultConverter.cs index 2be90a6bcc..cb5e71b060 100644 --- a/src/Umbraco.Web/PropertyEditors/Validation/ValidationResultConverter.cs +++ b/src/Umbraco.Web/PropertyEditors/Validation/ValidationResultConverter.cs @@ -42,16 +42,25 @@ namespace Umbraco.Web.PropertyEditors.Validation jo.Add("nestedValidation", obj); jo.WriteTo(writer); } - else if (validationResult is ValidationResultCollection resultCollection && resultCollection.ValidationResults.Count > 0) + else if (validationResult is ElementTypeValidationResult elementTypeValidationResult && elementTypeValidationResult.ValidationResults.Count > 0) { - var ja = new JArray(); - foreach (var result in resultCollection.ValidationResults) - { - // recurse to write out the ValidationResult - var obj = JObject.FromObject(result, camelCaseSerializer); - ja.Add(obj); + var joElementType = new JObject(); + var joPropertyType = new JObject(); + // loop over property validations + foreach (var propTypeResult in elementTypeValidationResult.ValidationResults) + { + var ja = new JArray(); + foreach (var result in propTypeResult.ValidationResults) + { + // recurse to get the validation result object and add to the array + var obj = JObject.FromObject(result, camelCaseSerializer); + ja.Add(obj); + } + // create a dictionary entry + joPropertyType.Add(propTypeResult.PropertyTypeAlias, ja); } - ja.WriteTo(writer); + joElementType.Add(elementTypeValidationResult.ElementTypeAlias, joPropertyType); + joElementType.WriteTo(writer); } else { From 42b15a958d0cff9c3f6d82eb919adc191fa93af6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Tue, 23 Jun 2020 13:29:18 +0200 Subject: [PATCH 227/377] call formSubmit before property editor are removed from DOM. Both for overlay-editing and inline-editing. Fire an postFormSubmitting event after formSubmitting event to let editors know that data could have been updated. --- .../components/umbnestedcontent.directive.js | 3 ++ .../common/services/blockeditor.service.js | 9 +++- .../blockeditormodelobject.service.js | 50 ++++++++++++++++--- .../src/common/services/formhelper.service.js | 3 ++ .../blockeditor/blockeditor.controller.js | 6 ++- .../blocklist/blocklist.component.js | 7 +-- .../inlineblock.editor.controller.js | 11 +++- 7 files changed, 71 insertions(+), 18 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbnestedcontent.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbnestedcontent.directive.js index 72dba3ca2f..42035579d5 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbnestedcontent.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbnestedcontent.directive.js @@ -49,6 +49,9 @@ } }); + + // Some property editors need to performe an action after all property editors have reacted to the formSubmitting. + $scope.$broadcast("postFormSubmitting", { scope: $scope }); } }); 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 87ab274f13..ffb1971169 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 @@ -38,10 +38,15 @@ * 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. + * @param {object} blockConfigurations block configurations. + * @param {angular-scope} scopeOfExistance A local angularJS scope that exists as long as the data exists. + * @param {angular-scope} propertyEditorScope A local angularJS scope that represents the property editors scope. * @return {blockEditorModelObject} A instance of the BlockEditorModelObject class. */ - function createModelObject(propertyModelValue, propertyEditorAlias, blockConfigurations, propertyScope) { - return new blockEditorModelObject(propertyModelValue, propertyEditorAlias, blockConfigurations, propertyScope); + function createModelObject(propertyModelValue, propertyEditorAlias, blockConfigurations, scopeOfExistance, propertyEditorScope) { + return new blockEditorModelObject(propertyModelValue, propertyEditorAlias, blockConfigurations, scopeOfExistance, propertyEditorScope); } return { 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 0a02398de1..d083cf3999 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 @@ -360,8 +360,7 @@ var notSupportedProperties = [ "Umbraco.Tags", "Umbraco.UploadField", - "Umbraco.ImageCropper", - "Umbraco.NestedContent" + "Umbraco.ImageCropper" ]; function replaceUnsupportedProperties(scaffold) { scaffold.variants.forEach((variant) => { @@ -384,15 +383,18 @@ * @param {object} propertyModelValue data object of the property editor, usually model.value. * @param {string} propertyEditorAlias alias of the property. * @param {object} blockConfigurations block configurations. - * @param {angular-scope} propertyScope The local angularJS scope. + * @param {angular-scope} scopeOfExistance A local angularJS scope that exists as long as the data exists. + * @param {angular-scope} propertyEditorScope A local angularJS scope that represents the property editors scope. * @returns {BlockEditorModelObject} A instance of BlockEditorModelObject. */ - function BlockEditorModelObject(propertyModelValue, propertyEditorAlias, blockConfigurations, propertyScope) { + function BlockEditorModelObject(propertyModelValue, propertyEditorAlias, blockConfigurations, scopeOfExistance, propertyEditorScope) { if (!propertyModelValue) { throw new Error("propertyModelValue cannot be undefined, to ensure we keep the binding to the angular model we need minimum an empty object."); } + this.__watchers = []; + // ensure basic part of data-structure is in place: this.value = propertyModelValue; this.value.layout = this.value.layout || {}; @@ -403,10 +405,12 @@ this.scaffolds = []; - this.isolatedScope = propertyScope.$new(true); + this.isolatedScope = scopeOfExistance.$new(true); this.isolatedScope.blockObjects = {}; - this.isolatedScope.$on("$destroy", this.onDestroyed.bind(this)); + this.__watchers.push(this.isolatedScope.$on("$destroy", this.onDestroyed.bind(this))); + + this.__watchers.push(propertyEditorScope.$on("postFormSubmitting", this.sync.bind(this))); }; @@ -620,11 +624,21 @@ } } + + blockObject.sync = function() { + if (this.content !== null) { + mapToPropertyModel(this.content, this.data); + } + if (this.config.settingsElementTypeKey !== null) { + mapToPropertyModel(this.settings, this.layout.settings); + } + } + // first time instant update of label. blockObject.label = getBlockLabel(blockObject); // Add blockObject to our isolated scope to enable watching its values: - this.isolatedScope.blockObjects["_"+blockObject.key] = blockObject; + this.isolatedScope.blockObjects["_" + blockObject.key] = blockObject; addWatchers(blockObject, this.isolatedScope); addWatchers(blockObject, this.isolatedScope, true); @@ -656,9 +670,13 @@ // remove property value watchers: blockObject.__watchers.forEach(w => { w(); }); + + // help carbage collector: + delete blockObject.layout; + delete blockObject.data; // remove model from isolatedScope. - delete this.isolatedScope.blockObjects[blockObject.key]; + delete this.isolatedScope.blockObjects["_" + blockObject.key]; }, @@ -732,6 +750,20 @@ }, + + + /** + * @ngdoc method + * @name sync + * @methodOf umbraco.services.blockEditorModelObject + * @description Force immidiate update of the blockobject models to the property model. + */ + sync: function() { + for (const key in this.isolatedScope.blockObjects) { + this.isolatedScope.blockObjects[key].sync(); + } + }, + // private _createDataEntry: function(elementTypeKey) { var content = { @@ -755,10 +787,12 @@ onDestroyed: function() { + this.__watchers.forEach(w => { w(); }); for (const key in this.isolatedScope.blockObjects) { this.destroyBlockObject(this.isolatedScope.blockObjects[key]); } + delete this.__watchers; delete this.value; delete this.propertyEditorAlias; delete this.blockConfigurations; diff --git a/src/Umbraco.Web.UI.Client/src/common/services/formhelper.service.js b/src/Umbraco.Web.UI.Client/src/common/services/formhelper.service.js index 90fbd76ec9..9c789d0bfb 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/formhelper.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/formhelper.service.js @@ -44,6 +44,9 @@ function formHelper(angularHelper, serverValidationManager, notificationsService //the first thing any form must do is broadcast the formSubmitting event args.scope.$broadcast("formSubmitting", { scope: args.scope, action: args.action }); + // Some property editors need to performe an action after all property editors have reacted to the formSubmitting. + args.scope.$broadcast("postFormSubmitting", { scope: args.scope, action: args.action }); + //then check if the form is valid if (!args.skipValidation) { if (currentForm.$invalid) { 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 f8667778af..08ffb4a9ae 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 @@ -1,7 +1,7 @@ //used for the media picker dialog angular.module("umbraco") .controller("Umbraco.Editors.BlockEditorController", - function ($scope, localizationService) { + function ($scope, localizationService, formHelper) { var vm = this; vm.content = $scope.model.content; @@ -61,7 +61,9 @@ angular.module("umbraco") vm.submitAndClose = function () { if ($scope.model && $scope.model.submit) { - $scope.model.submit($scope.model); + if (formHelper.submitForm({ scope: $scope })) { + $scope.model.submit($scope.model); + } } } diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.js index 654cd31f74..87559c0e79 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.js @@ -90,7 +90,7 @@ } // Create Model Object, to manage our data for this Block Editor. - modelObject = blockEditorService.createModelObject(vm.model.value, vm.model.editor, vm.model.config.blocks, scopeOfExistence); + modelObject = blockEditorService.createModelObject(vm.model.value, vm.model.editor, vm.model.config.blocks, scopeOfExistence, $scope); modelObject.load().then(onLoaded); copyAllBlocksAction = { @@ -518,11 +518,8 @@ } } - - - - unsubscribe.push($scope.$watch(() => vm.layout.length, onAmountOfBlocksChanged)); + $scope.$on("$destroy", function () { for (const subscription of unsubscribe) { diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklistentryeditors/inlineblock/inlineblock.editor.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklistentryeditors/inlineblock/inlineblock.editor.controller.js index ead82f72d5..29f8b1320c 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklistentryeditors/inlineblock/inlineblock.editor.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklistentryeditors/inlineblock/inlineblock.editor.controller.js @@ -1,11 +1,20 @@ (function () { 'use strict'; - function InlineBlockEditor() { + function InlineBlockEditor($scope) { const bc = this; bc.openBlock = function(block) { + + // if we are closing: + if (block.active === true) { + // boardcast the formSubmitting event to trigger syncronization or none-live property-editors + $scope.$broadcast("formSubmitting", { scope: $scope }); + // Some property editors need to performe an action after all property editors have reacted to the formSubmitting. + $scope.$broadcast("postFormSubmitting", { scope: $scope }); + } + block.active = !block.active; } From 2db5496dd6ccb233d40fbdc6b9e45feb5bb1cdf7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Tue, 23 Jun 2020 14:41:20 +0200 Subject: [PATCH 228/377] make sure settings object exists --- .../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 d083cf3999..1e5bfde801 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 @@ -599,10 +599,11 @@ blockObject.layout = layoutEntry; blockObject.__watchers = []; - layoutEntry.settings = null; if (blockConfiguration.settingsElementTypeKey) { var settingsScaffold = this.getScaffoldFromKey(blockConfiguration.settingsElementTypeKey); if (settingsScaffold !== null) { + + layoutEntry.settings = layoutEntry.settings || {}; blockObject.settingsData = layoutEntry.settings; From 01199ce88b0eb41a6f594a9077475a73954f6af4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Tue, 23 Jun 2020 14:41:43 +0200 Subject: [PATCH 229/377] only set active to false if it was false before opening the editor. --- .../views/propertyeditors/blocklist/blocklist.component.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.js index 87559c0e79..542cc3c482 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.js @@ -227,6 +227,7 @@ function editBlock(blockObject, openSettings) { + var wasNotActiveBefore = blockObject.active !== true; blockObject.active = true; if (inlineEditing === true && openSettings !== true) { @@ -267,7 +268,9 @@ blockObject.retriveValuesFrom(blockContentClone, blockSettingsClone); } - blockObject.active = false; + if (wasNotActiveBefore === true) { + blockObject.active = false; + } editorService.close(); } }; From eeea6410098ec72298b9244eb0da8074bd6c63a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Tue, 23 Jun 2020 14:56:01 +0200 Subject: [PATCH 230/377] update test with new scope parameter --- .../services/block-editor-service.spec.js | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/test/unit/common/services/block-editor-service.spec.js b/src/Umbraco.Web.UI.Client/test/unit/common/services/block-editor-service.spec.js index 39d28311cc..b69551d67f 100644 --- a/src/Umbraco.Web.UI.Client/test/unit/common/services/block-editor-service.spec.js +++ b/src/Umbraco.Web.UI.Client/test/unit/common/services/block-editor-service.spec.js @@ -49,26 +49,26 @@ it('fail if no model value', function () { function createWithNoModelValue() { - blockEditorService.createModelObject(null, "Umbraco.TestBlockEditor", [], $scope); + blockEditorService.createModelObject(null, "Umbraco.TestBlockEditor", [], $scope, $scope); } expect(createWithNoModelValue).toThrow(); }); it('return a object, with methods', function () { - var modelObject = blockEditorService.createModelObject({}, "Umbraco.TestBlockEditor", [], $scope); + var modelObject = blockEditorService.createModelObject({}, "Umbraco.TestBlockEditor", [], $scope, $scope); expect(modelObject).not.toBeUndefined(); expect(modelObject.load).not.toBeUndefined(); }); it('getBlockConfiguration provide the requested block configurtion', function () { - var modelObject = blockEditorService.createModelObject({}, "Umbraco.TestBlockEditor", [blockConfigurationMock], $scope); + var modelObject = blockEditorService.createModelObject({}, "Umbraco.TestBlockEditor", [blockConfigurationMock], $scope, $scope); expect(modelObject.getBlockConfiguration(blockConfigurationMock.contentTypeKey).label).toBe(blockConfigurationMock.label); }); it('load provides data for itemPicker', function (done) { - var modelObject = blockEditorService.createModelObject({}, "Umbraco.TestBlockEditor", [blockConfigurationMock], $scope); + var modelObject = blockEditorService.createModelObject({}, "Umbraco.TestBlockEditor", [blockConfigurationMock], $scope, $scope); modelObject.load().then(() => { var itemPickerOptions = modelObject.getAvailableBlocksForBlockPicker(); @@ -82,7 +82,7 @@ it('getLayoutEntry has values', function (done) { - var modelObject = blockEditorService.createModelObject(propertyModelMock, "Umbraco.TestBlockEditor", [blockConfigurationMock], $scope); + var modelObject = blockEditorService.createModelObject(propertyModelMock, "Umbraco.TestBlockEditor", [blockConfigurationMock], $scope, $scope); modelObject.load().then(() => { @@ -101,7 +101,7 @@ it('getBlockObject has values', function (done) { - var modelObject = blockEditorService.createModelObject(propertyModelMock, "Umbraco.TestBlockEditor", [blockConfigurationMock], $scope); + var modelObject = blockEditorService.createModelObject(propertyModelMock, "Umbraco.TestBlockEditor", [blockConfigurationMock], $scope, $scope); modelObject.load().then(() => { @@ -123,7 +123,7 @@ var propertyModel = angular.copy(propertyModelMock); - var modelObject = blockEditorService.createModelObject(propertyModel, "Umbraco.TestBlockEditor", [blockConfigurationMock], $scope); + var modelObject = blockEditorService.createModelObject(propertyModel, "Umbraco.TestBlockEditor", [blockConfigurationMock], $scope, $scope); modelObject.load().then(() => { @@ -155,7 +155,7 @@ propertyModel.data[0].testproperty = complexValue; - var modelObject = blockEditorService.createModelObject(propertyModel, "Umbraco.TestBlockEditor", [blockConfigurationMock], $scope); + var modelObject = blockEditorService.createModelObject(propertyModel, "Umbraco.TestBlockEditor", [blockConfigurationMock], $scope, $scope); modelObject.load().then(() => { @@ -180,7 +180,7 @@ var propertyModel = angular.copy(propertyModelMock); - var modelObject = blockEditorService.createModelObject(propertyModel, "Umbraco.TestBlockEditor", [blockConfigurationMock], $scope); + var modelObject = blockEditorService.createModelObject(propertyModel, "Umbraco.TestBlockEditor", [blockConfigurationMock], $scope, $scope); modelObject.load().then(() => { @@ -201,7 +201,7 @@ var propertyModel = angular.copy(propertyModelMock); - var modelObject = blockEditorService.createModelObject(propertyModel, "Umbraco.TestBlockEditor", [blockConfigurationMock], $scope); + var modelObject = blockEditorService.createModelObject(propertyModel, "Umbraco.TestBlockEditor", [blockConfigurationMock], $scope, $scope); modelObject.load().then(() => { From 67a9a3f807582fd215b46deea58117f68f833f3d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Tue, 23 Jun 2020 20:42:14 +0200 Subject: [PATCH 231/377] move destroy responsibility to blockObject --- .../blockeditormodelobject.service.js | 23 +++++++++++-------- 1 file changed, 14 insertions(+), 9 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 1e5bfde801..6b48bbdc37 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 @@ -643,6 +643,19 @@ addWatchers(blockObject, this.isolatedScope); addWatchers(blockObject, this.isolatedScope, true); + blockObject.destroy = function() { + // remove property value watchers: + this.__watchers.forEach(w => { w(); }); + + // help carbage collector: + delete this.layout; + delete this.data; + + // remove model from isolatedScope. + delete this.__scope.blockObjects["_" + this.key]; + delete this.__scope; + } + return blockObject; }, @@ -669,15 +682,7 @@ */ destroyBlockObject: function(blockObject) { - // remove property value watchers: - blockObject.__watchers.forEach(w => { w(); }); - - // help carbage collector: - delete blockObject.layout; - delete blockObject.data; - - // remove model from isolatedScope. - delete this.isolatedScope.blockObjects["_" + blockObject.key]; + blockObject.destroy(); }, From 58ee8da948c28b0160f0c0c26fa46b82781e5c60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Tue, 23 Jun 2020 20:42:28 +0200 Subject: [PATCH 232/377] rename onDestroy to destroy --- .../common/services/blockeditormodelobject.service.js | 11 +++++++---- 1 file changed, 7 insertions(+), 4 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 6b48bbdc37..80bd7e077e 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 @@ -408,7 +408,7 @@ this.isolatedScope = scopeOfExistance.$new(true); this.isolatedScope.blockObjects = {}; - this.__watchers.push(this.isolatedScope.$on("$destroy", this.onDestroyed.bind(this))); + this.__watchers.push(this.isolatedScope.$on("$destroy", this.destroy.bind(this))); this.__watchers.push(propertyEditorScope.$on("postFormSubmitting", this.sync.bind(this))); @@ -646,6 +646,7 @@ blockObject.destroy = function() { // remove property value watchers: this.__watchers.forEach(w => { w(); }); + delete this.__watchers; // help carbage collector: delete this.layout; @@ -654,6 +655,9 @@ // remove model from isolatedScope. delete this.__scope.blockObjects["_" + this.key]; delete this.__scope; + + // removes this method, making it unposible to destroy again. + delete this.destroy; } return blockObject; @@ -681,9 +685,7 @@ * @param {Object} blockObject The BlockObject to be destroyed. */ destroyBlockObject: function(blockObject) { - blockObject.destroy(); - }, /** @@ -791,7 +793,7 @@ } }, - onDestroyed: function() { + destroy: function() { this.__watchers.forEach(w => { w(); }); for (const key in this.isolatedScope.blockObjects) { @@ -805,6 +807,7 @@ delete this.scaffolds; this.isolatedScope.$destroy(); delete this.isolatedScope; + delete this.destroy; } } From 90b4d89c1a3434fcc863d290b61ccac35be09812 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Tue, 23 Jun 2020 20:46:15 +0200 Subject: [PATCH 233/377] added some JS-Docs --- .../services/blockeditormodelobject.service.js | 16 +++++++++++++++- 1 file changed, 15 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 80bd7e077e..4bb5f90556 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 @@ -669,7 +669,7 @@ * @name removeDataAndDestroyModel * @methodOf umbraco.services.blockEditorModelObject * @description Removes the data and destroys the Block Model. - * Notive this method does not remove the block from your layout, this will need to be handlede by the Property Editor since this services donst know about your layout structure. + * Notice this method does not remove the block from your layout, this will need to be handlede by the Property Editor since this services donst know about your layout structure. * @param {Object} blockObject The BlockObject to be removed and destroyed. */ removeDataAndDestroyModel: function (blockObject) { @@ -786,6 +786,14 @@ return this.value.data.find(entry => entry.udi === udi) || null; }, + /** + * @ngdoc method + * @name removeDataByUdi + * @methodOf umbraco.services.blockEditorModelObject + * @description Removes the data of a given UDI. + * Notice this method does not remove the block from your layout, this will need to be handlede by the Property Editor since this services donst know about your layout structure. + * @param {string} udi The UDI of the data to be removed. + */ removeDataByUdi: function(udi) { const index = this.value.data.findIndex(o => o.udi === udi); if (index !== -1) { @@ -793,6 +801,12 @@ } }, + /** + * @ngdoc method + * @name destroy + * @methodOf umbraco.services.blockEditorModelObject + * @description Notice you should not need to destroy the BlockEditorModelObject since it will automaticly be destroyed when the scope of existance gets destroyed. + */ destroy: function() { this.__watchers.forEach(w => { w(); }); From 29aaabcc6e050c0b4874b1ed27459c79d49176b9 Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 24 Jun 2020 10:20:25 +1000 Subject: [PATCH 234/377] Gets the c# side of things working for complex validation for complex editors --- .../Validation/ContentModelValidatorTests.cs | 31 ++++++------- src/Umbraco.Web/ModelStateExtensions.cs | 2 +- .../PropertyEditors/ComplexEditorValidator.cs | 10 ++--- ...omplexEditorElementTypeValidationResult.cs | 20 +++++++++ ...mplexEditorPropertyTypeValidationResult.cs | 20 +++++++++ .../ComplexEditorValidationResult.cs | 22 +++++++++ ....cs => ContentPropertyValidationResult.cs} | 8 ++-- .../Validation/NestedValidationResults.cs | 45 ------------------- .../Validation/ValidationResultConverter.cs | 18 ++++---- src/Umbraco.Web/Umbraco.Web.csproj | 6 ++- 10 files changed, 102 insertions(+), 80 deletions(-) create mode 100644 src/Umbraco.Web/PropertyEditors/Validation/ComplexEditorElementTypeValidationResult.cs create mode 100644 src/Umbraco.Web/PropertyEditors/Validation/ComplexEditorPropertyTypeValidationResult.cs create mode 100644 src/Umbraco.Web/PropertyEditors/Validation/ComplexEditorValidationResult.cs rename src/Umbraco.Web/PropertyEditors/Validation/{PropertyValidationResult.cs => ContentPropertyValidationResult.cs} (69%) delete mode 100644 src/Umbraco.Web/PropertyEditors/Validation/NestedValidationResults.cs diff --git a/src/Umbraco.Tests/Web/Validation/ContentModelValidatorTests.cs b/src/Umbraco.Tests/Web/Validation/ContentModelValidatorTests.cs index 29a8f9ebb7..8c52c7e918 100644 --- a/src/Umbraco.Tests/Web/Validation/ContentModelValidatorTests.cs +++ b/src/Umbraco.Tests/Web/Validation/ContentModelValidatorTests.cs @@ -97,17 +97,17 @@ namespace Umbraco.Tests.Web.Validation [Test] public void TestSerializer() { - var nestedLevel2 = new NestedValidationResults(); - var elementTypeResult2 = new ElementTypeValidationResult("type2"); - var propertyTypeResult2 = new PropertyTypeValidationResult("prop2"); + var nestedLevel2 = new ComplexEditorValidationResult(); + var elementTypeResult2 = new ComplexEditorElementTypeValidationResult("type2"); + var propertyTypeResult2 = new ComplexEditorPropertyTypeValidationResult("prop2"); propertyTypeResult2.ValidationResults.Add(new ValidationResult("error2-1", new[] { "level2" })); propertyTypeResult2.ValidationResults.Add(new ValidationResult("error2-2", new[] { "level2" })); elementTypeResult2.ValidationResults.Add(propertyTypeResult2); nestedLevel2.ValidationResults.Add(elementTypeResult2); - var nestedLevel1 = new NestedValidationResults(); - var elementTypeResult1 = new ElementTypeValidationResult("type1"); - var propertyTypeResult1 = new PropertyTypeValidationResult("prop1"); + var nestedLevel1 = new ComplexEditorValidationResult(); + var elementTypeResult1 = new ComplexEditorElementTypeValidationResult("type1"); + var propertyTypeResult1 = new ComplexEditorPropertyTypeValidationResult("prop1"); propertyTypeResult1.ValidationResults.Add(new ValidationResult("error1-1", new[] { "level1" })); propertyTypeResult1.ValidationResults.Add(nestedLevel2); // This is a nested result within the level 1 elementTypeResult1.ValidationResults.Add(propertyTypeResult1); @@ -115,6 +115,11 @@ namespace Umbraco.Tests.Web.Validation var serialized = JsonConvert.SerializeObject(nestedLevel1, Formatting.Indented, new ValidationResultConverter()); Console.WriteLine(serialized); + + var jsonNestedError = JsonConvert.DeserializeObject(serialized); + Assert.AreEqual(JTokenType.Array, jsonNestedError["nestedValidation"].Type); + var nestedValidation = (JArray)jsonNestedError["nestedValidation"]; + AssertNestedValidation(nestedValidation); } [Test] @@ -226,12 +231,12 @@ namespace Umbraco.Tests.Web.Validation var jsonNestedError = JsonConvert.DeserializeObject(nestedError.ErrorMessage); Assert.AreEqual(JTokenType.Array, jsonNestedError["nestedValidation"].Type); var nestedValidation = (JArray)jsonNestedError["nestedValidation"]; - AssertNestedValidation(nestedValidation, 2); // there are 2 because there are 2 nested content rows + AssertNestedValidation(nestedValidation); } - private void AssertNestedValidation(JArray nestedValidation, int rows) + private void AssertNestedValidation(JArray nestedValidation) { - Assert.AreEqual(rows, nestedValidation.Count); + Assert.Greater(nestedValidation.Count, 0); foreach (var rowErrors in nestedValidation) { Assert.AreEqual(JTokenType.Object, rowErrors.Type); @@ -239,7 +244,7 @@ namespace Umbraco.Tests.Web.Validation Assert.AreEqual(1, elementTypeErrors.Count); // there is 1 element type in error foreach (var elementTypeAliasToErrors in elementTypeErrors) { - Assert.AreEqual("textPage", elementTypeAliasToErrors.Key); + Assert.IsNotEmpty(elementTypeAliasToErrors.Key); var propErrors = (JObject)elementTypeAliasToErrors.Value; foreach (var propAliasToErrors in propErrors) @@ -253,19 +258,15 @@ namespace Umbraco.Tests.Web.Validation if (nested != null) { // recurse - AssertNestedValidation((JArray)nested, 1); // we know this is 1 row + AssertNestedValidation((JArray)nested); continue; } Assert.IsNotEmpty(propError["errorMessage"].Value()); Assert.AreEqual(1, propError["memberNames"].Value().Count); } - } - } - - } } diff --git a/src/Umbraco.Web/ModelStateExtensions.cs b/src/Umbraco.Web/ModelStateExtensions.cs index 718094e6b5..8002c32e61 100644 --- a/src/Umbraco.Web/ModelStateExtensions.cs +++ b/src/Umbraco.Web/ModelStateExtensions.cs @@ -54,7 +54,7 @@ namespace Umbraco.Web ValidationResult result, string propertyAlias, string culture = "", string segment = "") { - var propValidationResult = new PropertyValidationResult(result); + var propValidationResult = new ContentPropertyValidationResult(result); var keyParts = new[] { diff --git a/src/Umbraco.Web/PropertyEditors/ComplexEditorValidator.cs b/src/Umbraco.Web/PropertyEditors/ComplexEditorValidator.cs index 16a3357cbb..b65c872e68 100644 --- a/src/Umbraco.Web/PropertyEditors/ComplexEditorValidator.cs +++ b/src/Umbraco.Web/PropertyEditors/ComplexEditorValidator.cs @@ -23,7 +23,7 @@ namespace Umbraco.Web.PropertyEditors } /// - /// Return a single for all sub nested validation results in the complex editor + /// Return a single for all sub nested validation results in the complex editor /// /// /// @@ -36,7 +36,7 @@ namespace Umbraco.Web.PropertyEditors if (rowResults.Count > 0) { - var result = new NestedValidationResults(); + var result = new ComplexEditorValidationResult(); foreach(var rowResult in rowResults) { result.ValidationResults.Add(rowResult); @@ -55,15 +55,15 @@ namespace Umbraco.Web.PropertyEditors /// /// /// - protected IEnumerable GetNestedValidationResults(IEnumerable elements) + protected IEnumerable GetNestedValidationResults(IEnumerable elements) { foreach (var row in elements) { - var elementTypeValidationResult = new ElementTypeValidationResult(row.ElementTypeAlias); + var elementTypeValidationResult = new ComplexEditorElementTypeValidationResult(row.ElementTypeAlias); foreach (var prop in row.PropertyTypeValidation) { - var propValidationResult = new PropertyTypeValidationResult(prop.PropertyType.Alias); + var propValidationResult = new ComplexEditorPropertyTypeValidationResult(prop.PropertyType.Alias); foreach (var validationResult in _propertyValidationService.ValidatePropertyValue(prop.PropertyType, prop.PostedValue)) { diff --git a/src/Umbraco.Web/PropertyEditors/Validation/ComplexEditorElementTypeValidationResult.cs b/src/Umbraco.Web/PropertyEditors/Validation/ComplexEditorElementTypeValidationResult.cs new file mode 100644 index 0000000000..d43c3ed13d --- /dev/null +++ b/src/Umbraco.Web/PropertyEditors/Validation/ComplexEditorElementTypeValidationResult.cs @@ -0,0 +1,20 @@ +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; + +namespace Umbraco.Web.PropertyEditors.Validation +{ + /// + /// A collection of for an element type within complex editor represented by an Element Type + /// + public class ComplexEditorElementTypeValidationResult : ValidationResult + { + public ComplexEditorElementTypeValidationResult(string elementTypeAlias) + : base(string.Empty) + { + ElementTypeAlias = elementTypeAlias; + } + + public IList ValidationResults { get; } = new List(); + public string ElementTypeAlias { get; } + } +} diff --git a/src/Umbraco.Web/PropertyEditors/Validation/ComplexEditorPropertyTypeValidationResult.cs b/src/Umbraco.Web/PropertyEditors/Validation/ComplexEditorPropertyTypeValidationResult.cs new file mode 100644 index 0000000000..20ba3a27d3 --- /dev/null +++ b/src/Umbraco.Web/PropertyEditors/Validation/ComplexEditorPropertyTypeValidationResult.cs @@ -0,0 +1,20 @@ +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; + +namespace Umbraco.Web.PropertyEditors.Validation +{ + /// + /// A collection of for a property type within a complex editor represented by an Element Type + /// + public class ComplexEditorPropertyTypeValidationResult : ValidationResult + { + public ComplexEditorPropertyTypeValidationResult(string propertyTypeAlias) + : base(string.Empty) + { + PropertyTypeAlias = propertyTypeAlias; + } + + public IList ValidationResults { get; } = new List(); + public string PropertyTypeAlias { get; } + } +} diff --git a/src/Umbraco.Web/PropertyEditors/Validation/ComplexEditorValidationResult.cs b/src/Umbraco.Web/PropertyEditors/Validation/ComplexEditorValidationResult.cs new file mode 100644 index 0000000000..eb1efbc64f --- /dev/null +++ b/src/Umbraco.Web/PropertyEditors/Validation/ComplexEditorValidationResult.cs @@ -0,0 +1,22 @@ +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; + +namespace Umbraco.Web.PropertyEditors.Validation +{ + + /// + /// A collection of for a complex editor represented by an Element Type + /// + /// + /// For example, each represents validation results for a row in Nested Content + /// + public class ComplexEditorValidationResult : ValidationResult + { + public ComplexEditorValidationResult() + : base(string.Empty) + { + } + + public IList ValidationResults { get; } = new List(); + } +} diff --git a/src/Umbraco.Web/PropertyEditors/Validation/PropertyValidationResult.cs b/src/Umbraco.Web/PropertyEditors/Validation/ContentPropertyValidationResult.cs similarity index 69% rename from src/Umbraco.Web/PropertyEditors/Validation/PropertyValidationResult.cs rename to src/Umbraco.Web/PropertyEditors/Validation/ContentPropertyValidationResult.cs index f2c92e441e..9c80f6b8e7 100644 --- a/src/Umbraco.Web/PropertyEditors/Validation/PropertyValidationResult.cs +++ b/src/Umbraco.Web/PropertyEditors/Validation/ContentPropertyValidationResult.cs @@ -8,12 +8,12 @@ namespace Umbraco.Web.PropertyEditors.Validation /// /// This clones the original result and then ensures the nested result if it's the correct type /// - public class PropertyValidationResult : ValidationResult + public class ContentPropertyValidationResult : ValidationResult { - public PropertyValidationResult(ValidationResult nested) + public ContentPropertyValidationResult(ValidationResult nested) : base(nested.ErrorMessage, nested.MemberNames) { - NestedResuls = nested as NestedValidationResults; + ComplexEditorResults = nested as ComplexEditorValidationResult; } /// @@ -22,6 +22,6 @@ namespace Umbraco.Web.PropertyEditors.Validation /// /// There can be nested results for complex editors that contain other editors /// - public NestedValidationResults NestedResuls { get; } + public ComplexEditorValidationResult ComplexEditorResults { get; } } } diff --git a/src/Umbraco.Web/PropertyEditors/Validation/NestedValidationResults.cs b/src/Umbraco.Web/PropertyEditors/Validation/NestedValidationResults.cs deleted file mode 100644 index 26a47602db..0000000000 --- a/src/Umbraco.Web/PropertyEditors/Validation/NestedValidationResults.cs +++ /dev/null @@ -1,45 +0,0 @@ -using System.Collections.Generic; -using System.ComponentModel.DataAnnotations; - -namespace Umbraco.Web.PropertyEditors.Validation -{ - public class PropertyTypeValidationResult : ValidationResult - { - public PropertyTypeValidationResult(string propertyTypeAlias) - : base(string.Empty) - { - PropertyTypeAlias = propertyTypeAlias; - } - - public IList ValidationResults { get; } = new List(); - public string PropertyTypeAlias { get; } - } - - public class ElementTypeValidationResult : ValidationResult - { - public ElementTypeValidationResult(string elementTypeAlias) - : base(string.Empty) - { - ElementTypeAlias = elementTypeAlias; - } - - public IList ValidationResults { get; } = new List(); - public string ElementTypeAlias { get; } - } - - /// - /// Custom that contains a list of nested validation results - /// - /// - /// For example, each represents validation results for a row in Nested Content - /// - public class NestedValidationResults : ValidationResult - { - public NestedValidationResults() - : base(string.Empty) - { - } - - public IList ValidationResults { get; } = new List(); - } -} diff --git a/src/Umbraco.Web/PropertyEditors/Validation/ValidationResultConverter.cs b/src/Umbraco.Web/PropertyEditors/Validation/ValidationResultConverter.cs index cb5e71b060..6769d0bd08 100644 --- a/src/Umbraco.Web/PropertyEditors/Validation/ValidationResultConverter.cs +++ b/src/Umbraco.Web/PropertyEditors/Validation/ValidationResultConverter.cs @@ -4,15 +4,17 @@ using Newtonsoft.Json.Serialization; using System; using System.ComponentModel.DataAnnotations; using System.Linq; -using System.Text; -using System.Threading.Tasks; using Umbraco.Core; namespace Umbraco.Web.PropertyEditors.Validation { /// - /// Custom json converter for and + /// Custom json converter for and /// + /// + /// This converter is specifically used to convert validation results for content in order to be able to have nested + /// validation results for complex editors. + /// internal class ValidationResultConverter : JsonConverter { public override bool CanConvert(Type objectType) => typeof(ValidationResult).IsAssignableFrom(objectType); @@ -34,7 +36,7 @@ namespace Umbraco.Web.PropertyEditors.Validation var validationResult = (ValidationResult)value; - if (validationResult is NestedValidationResults nestedResult && nestedResult.ValidationResults.Count > 0) + if (validationResult is ComplexEditorValidationResult nestedResult && nestedResult.ValidationResults.Count > 0) { var jo = new JObject(); // recurse to write out an array of ValidationResultCollection @@ -42,7 +44,7 @@ namespace Umbraco.Web.PropertyEditors.Validation jo.Add("nestedValidation", obj); jo.WriteTo(writer); } - else if (validationResult is ElementTypeValidationResult elementTypeValidationResult && elementTypeValidationResult.ValidationResults.Count > 0) + else if (validationResult is ComplexEditorElementTypeValidationResult elementTypeValidationResult && elementTypeValidationResult.ValidationResults.Count > 0) { var joElementType = new JObject(); var joPropertyType = new JObject(); @@ -65,11 +67,11 @@ namespace Umbraco.Web.PropertyEditors.Validation else { - if (validationResult is PropertyValidationResult propertyValidationResult - && propertyValidationResult.NestedResuls?.ValidationResults.Count > 0) + if (validationResult is ContentPropertyValidationResult propertyValidationResult + && propertyValidationResult.ComplexEditorResults?.ValidationResults.Count > 0) { // recurse to write out the NestedValidationResults - var obj = JToken.FromObject(propertyValidationResult.NestedResuls, camelCaseSerializer); + var obj = JToken.FromObject(propertyValidationResult.ComplexEditorResults, camelCaseSerializer); obj.WriteTo(writer); } diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index f1791908f9..f9aed27684 100644 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -248,8 +248,10 @@ - - + + + + From b99c170f8694ecf86ab1414165815a4209fc823c Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 24 Jun 2020 11:51:48 +1000 Subject: [PATCH 235/377] adding umbNestedProperty to build up a json path for a targeted nested property type. --- .../property/umbnestedproperty.component.js | 49 ++++++++++ .../property/umbproperty.directive.js | 4 +- .../components/umbnestedcontent.directive.js | 3 +- .../validation/valserver.directive.js | 36 ++++++- .../src/common/services/formhelper.service.js | 6 ++ .../services/servervalidationmgr.service.js | 93 +++++++++++++------ .../blocklist/blocklist.component.html | 11 ++- .../blocklist/blocklist.component.js | 4 - .../nestedcontent/nestedcontent.controller.js | 6 ++ .../nestedcontent/nestedcontent.editor.html | 21 +++-- .../nestedcontent.propertyeditor.html | 2 +- 11 files changed, 188 insertions(+), 47 deletions(-) create mode 100644 src/Umbraco.Web.UI.Client/src/common/directives/components/property/umbnestedproperty.component.js diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/property/umbnestedproperty.component.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/property/umbnestedproperty.component.js new file mode 100644 index 0000000000..239259b3fa --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/property/umbnestedproperty.component.js @@ -0,0 +1,49 @@ +(function () { + "use strict"; + + + /** + * @ngdoc component + * @name Umbraco.umbBlockListBlockContent + * @function + * + * @description + * The component for a style-inheriting block of the block list property editor. + */ + angular + .module("umbraco") + .component("umbNestedProperty", { + transclude: true, + template: '
      ', + controller: NestedPropertyController, + controllerAs: 'vm', + bindings: { + propertyTypeAlias: "@", + elementTypeIndex: "@" + }, + require: { + umbNestedProperty: "?^^umbNestedProperty" + } + }); + + function NestedPropertyController($scope) { + var vm = this; + vm.$onInit = function () { + + }; + + // returns a jsonpath for where this property is located in a hierarchy + // this will call into all hierarchical parents + vm.getValidationPath = function () { + + var path = vm.umbNestedProperty ? vm.umbNestedProperty.getValidationPath() : "$"; + if (vm.propertyTypeAlias && vm.elementTypeIndex) { + path += ".[nestedValidation].[" + vm.elementTypeIndex + "].[" + vm.propertyTypeAlias + "]"; + return path; + } + return null; + } + } + + +})(); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/property/umbproperty.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/property/umbproperty.directive.js index ad62bcd3db..d83b066a89 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/property/umbproperty.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/property/umbproperty.directive.js @@ -15,7 +15,7 @@ angular.module("umbraco.directives") restrict: 'E', replace: true, templateUrl: 'views/components/property/umb-property.html', - link: function (scope) { + link: function (scope, element, attr, ctrls) { scope.controlLabelTitle = null; if(Umbraco.Sys.ServerVariables.isDebuggingEnabled) { @@ -43,6 +43,8 @@ angular.module("umbraco.directives") $scope.propertyActions = actions; }; + + } }; }); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbnestedcontent.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbnestedcontent.directive.js index 72dba3ca2f..8a19bcc400 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbnestedcontent.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbnestedcontent.directive.js @@ -63,7 +63,8 @@ templateUrl: Umbraco.Sys.ServerVariables.umbracoSettings.umbracoPath + "/views/propertyeditors/nestedcontent/nestedcontent.editor.html", scope: { ngModel: '=', - tabAlias: '=' + tabAlias: '=', + itemIndex: '@' }, link: link }; diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/validation/valserver.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/validation/valserver.directive.js index 3fa9220f7b..cea4cbfde6 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/validation/valserver.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/validation/valserver.directive.js @@ -7,7 +7,7 @@ **/ function valServer(serverValidationManager) { return { - require: ['ngModel', '?^^umbProperty', '?^^umbVariantContent'], + require: ['ngModel', '?^^umbProperty', '?^^umbVariantContent', '?^^umbNestedProperty'], restrict: "A", scope: {}, link: function (scope, element, attr, ctrls) { @@ -21,6 +21,7 @@ function valServer(serverValidationManager) { // optional reference to the varaint-content-controller, needed to avoid validation when the field is invariant on non-default languages. var umbVariantCtrl = ctrls.length > 2 ? ctrls[2] : null; + var umbNestedPropertyCtrl = ctrls.length > 3 ? ctrls[3] : null; var currentProperty = umbPropCtrl.property; var currentCulture = currentProperty.culture; @@ -75,7 +76,10 @@ function valServer(serverValidationManager) { if (modelCtrl.$invalid) { modelCtrl.$setValidity('valServer', true); + //clear the server validation entry + // TODO: We'll need to handle this differently since this will need to target the actual 'fieldName' or validation + // path if there is one serverValidationManager.removePropertyError(currentProperty.alias, currentCulture, fieldName, currentSegment); stopWatch(); } @@ -105,9 +109,35 @@ function valServer(serverValidationManager) { stopWatch(); } } - unsubscribe.push(serverValidationManager.subscribe(currentProperty.alias, + + // TODO: If this is a property/field within a complex editor which means it could be a nested/nested/nested property/field + // we need to figure out a way to get it's "Path" (or jsonpath) which can be represented by something like: + // $.[nestedValidation].[0].[prop1].[nestedValidation].[0].[prop2] + // Or ... if we have names instead of indexes (which is seems like we do) + // $.nestedValidation.[type1].[prop1].[nestedValidation].[type2].[prop2] + // This would mean: + // - the first row/item in a complex editor + // - within the property 'prop1' + // - the first row/item in a complex editor + // - within the property 'prop2' + // So how can we figure out this path? The only way is really by looking up our current hierarchy of items + // TODO: OK, so we thought we had it with umb-property being able to know the content type BUT this doesn't work + // because the validation results could have a many rows for the same content type, we need to have the index available + // so the firest example above works much better. + // ... OK ... looks like we have an index to work with, but we'll need to update the block editor to support this too. + + var propertyValidationPath = umbNestedPropertyCtrl ? umbNestedPropertyCtrl.getValidationPath() : null; + + unsubscribe.push(serverValidationManager.subscribe( + currentProperty.alias, currentCulture, - fieldName, + // use the propertyValidationPath for the fieldName value if there is one since if there is one it means it's a complex + // editor and as such the 'fieldName' will be empty. The serverValidationManager knows how to handle the jsonpath + // string as the fieldName. + // TODO: This isn't quite true! If there is a fieldName specified, then it will need to be added to the + // validation path. We should pass in the fieldName to umbNestedPropertyCtrl.getValidationPath(); since this could very well be targeting a specific field + + propertyValidationPath ? propertyValidationPath : fieldName, serverValidationManagerCallback, currentSegment) ); diff --git a/src/Umbraco.Web.UI.Client/src/common/services/formhelper.service.js b/src/Umbraco.Web.UI.Client/src/common/services/formhelper.service.js index 90fbd76ec9..68e2e8a78e 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/formhelper.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/formhelper.service.js @@ -162,6 +162,12 @@ function formHelper(angularHelper, serverValidationManager, notificationsService //There will always be at least 4 parts for content properties since all model errors for properties are prefixed with "_Properties" //If it is not prefixed with "_Properties" that means the error is for a field of the object directly. + // TODO: This 4 part dot notation isn't ideal and instead it would probably be nicer to have a json structure as the key (which could be converted + // to base64 if we cannot do that since it's a 'key'). That way the key can be flexible and 'future proof' since I'm sure something in the future + // will change for this. Another idea is to just have a single key for one property type and have the model error a json structure that handles + // everything. This would probably be the 'nicest' way but would require quite a lot of work. We are part way there with how we are doing + // validation for complex editors. + // Example: "_Properties.headerImage.en-US.mySegment.myField" // * it's for a property since it has a _Properties prefix // * it's for the headerImage property type diff --git a/src/Umbraco.Web.UI.Client/src/common/services/servervalidationmgr.service.js b/src/Umbraco.Web.UI.Client/src/common/services/servervalidationmgr.service.js index 718e44d66e..04b4d2ad9a 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/servervalidationmgr.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/servervalidationmgr.service.js @@ -36,7 +36,6 @@ function serverValidationManager($timeout) { }); } - function getPropertyErrors(self, propertyAlias, culture, segment, fieldName) { if (!Utilities.isString(propertyAlias)) { throw "propertyAlias must be a string"; @@ -98,7 +97,30 @@ function serverValidationManager($timeout) { } } } - + + function parseComplexEditorError(errorMsg) { + var json = JSON.parse(errorMsg); + + var nestedValidation = json["nestedValidation"]; + if (!nestedValidation) { + throw "Invalid JSON structure for complex property, missing 'nestedValidation'"; + } + + // each key represents an element type, the key is it's alias + var keys = Object.keys(nestedValidation); + + // TODO: Could we use an individual instance of serverValidationManager for each element type? It could/should work the way + // it does today since it currently manages all callbacks for all simple properties on a content item based on a content type. + // Hrmmm... only thing is then how to dispose/cleanup of these instances? + + // TODO: ... actually, because we are registering a JSONPath into the 'fieldName' for when complex editors subscribe, perhaps + // the only thing we need to do is build up all of the different JSONPath's and their errors here based on this object and then + // execute callbacks for each? So I think we need to make a function recursively return all possible keys! ... we can even have tests + // for that :) + + + } + return { /** @@ -173,7 +195,14 @@ function serverValidationManager($timeout) { if (!segment) { segment = null; } - + + // TODO: Check if the fieldName is a jsonpath, we will know this if it starts with $. + // in which case we need to handle this a little differently. + if (fieldName && fieldName.startsWith("$.")) { + // TODO: Or... Do we even need to deal with it differently? Maybe with some luck + // we can just store that path and use it. Lets see how this goes. + } + if (propertyAlias === null) { callbacks.push({ propertyAlias: null, @@ -370,6 +399,10 @@ function serverValidationManager($timeout) { * Adds an error message for the content property */ addPropertyError: function (propertyAlias, culture, fieldName, errorMsg, segment) { + + // TODO: We need to handle the errorMsg in a special way to check if this is a json structure. If it is we know we are dealing with + // a complex editor and in which case we'll need to adjust how everything works. + if (!propertyAlias) { return; } @@ -383,31 +416,39 @@ function serverValidationManager($timeout) { segment = null; } - //only add the item if it doesn't exist - if (!this.hasPropertyError(propertyAlias, culture, fieldName, segment)) { - this.items.push({ - propertyAlias: propertyAlias, - culture: culture, - segment: segment, - fieldName: fieldName, - errorMsg: errorMsg - }); - } - - //find all errors for this item - var errorsForCallback = getPropertyErrors(this, propertyAlias, culture, segment, fieldName); - //we should now call all of the call backs registered for this error - var cbs = this.getPropertyCallbacks(propertyAlias, culture, fieldName, segment); - //call each callback for this error - for (var cb in cbs) { - executeCallback(this, errorsForCallback, cbs[cb].callback, culture, segment); + // if the error message is json it's a complex editor validation response that we need to parse + if (errorMsg.startsWith("{")) { + parseComplexEditorError(errorMsg); } + else { + + //only add the item if it doesn't exist + if (!this.hasPropertyError(propertyAlias, culture, fieldName, segment)) { + this.items.push({ + propertyAlias: propertyAlias, + culture: culture, + segment: segment, + fieldName: fieldName, + errorMsg: errorMsg + }); + } + + //find all errors for this item + var errorsForCallback = getPropertyErrors(this, propertyAlias, culture, segment, fieldName); + //we should now call all of the call backs registered for this error + var cbs = this.getPropertyCallbacks(propertyAlias, culture, fieldName, segment); + //call each callback for this error + for (var cb in cbs) { + executeCallback(this, errorsForCallback, cbs[cb].callback, culture, segment); + } + + //execute variant specific callbacks here too when a propery error is added + var variantCbs = this.getVariantCallbacks(culture, segment); + //call each callback for this error + for (var cb in variantCbs) { + executeCallback(this, errorsForCallback, variantCbs[cb].callback, culture, segment); + } - //execute variant specific callbacks here too when a propery error is added - var variantCbs = this.getVariantCallbacks(culture, segment); - //call each callback for this error - for (var cb in variantCbs) { - executeCallback(this, errorsForCallback, variantCbs[cb].callback, culture, segment); } }, diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.html index 6a2c012e25..fd6620060e 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.html @@ -20,10 +20,13 @@
      - - - - + + + + + +
      - +
      From 6f1ea3eb45838bb536b1befe9af0d02982f5c7b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Wed, 24 Jun 2020 11:10:40 +0200 Subject: [PATCH 236/377] correction jsDocs --- .../propertyeditors/blocklist/blocklist.block.component.js | 6 ++---- .../views/propertyeditors/blocklist/blocklist.component.js | 2 +- .../blocklist/blocklist.scopedblock.component.js | 4 +--- 3 files changed, 4 insertions(+), 8 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.block.component.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.block.component.js index fb47ffdaab..ab2590e249 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.block.component.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.block.component.js @@ -1,15 +1,13 @@ (function () { "use strict"; - /** * @ngdoc component - * @name Umbraco.umbBlockListBlockContent - * @function - * + * @name umbraco.directives.directive:umbBlockListBlockContent * @description * The component for a style-inheriting block of the block list property editor. */ + angular .module("umbraco") .component("umbBlockListBlockContent", { diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.js index 542cc3c482..54d664b54e 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.js @@ -4,7 +4,7 @@ /** * @ngdoc component - * @name Umbraco.Editors.BlockList.blockListPropertyEditor + * @name umbraco.directives.directive:blockListPropertyEditor * @function * * @description diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.scopedblock.component.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.scopedblock.component.js index b778abc35b..f0a387f1e8 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.scopedblock.component.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.scopedblock.component.js @@ -3,9 +3,7 @@ /** * @ngdoc component - * @name Umbraco.umbBlockListScopedBlockContent - * @function - * + * @name umbraco.directives.directive:umbBlockListScopedBlockContent * @description * The component for a style-scoped block of the block list property editor. */ From c53ea9382f50216a581aefa754598f5b8adc6dee Mon Sep 17 00:00:00 2001 From: Warren Buckley Date: Wed, 24 Jun 2020 11:21:24 +0100 Subject: [PATCH 237/377] Update ElementType Resource to not use hardcoded URL but to use the Umbraco.Sys.ServerVariables.umbracoUrls instead --- .../src/common/resources/elementtype.resource.js | 11 ++--------- src/Umbraco.Web/Editors/BackOfficeServerVariables.cs | 4 ++++ src/Umbraco.Web/Editors/ElementTypeController.cs | 4 ++-- 3 files changed, 8 insertions(+), 11 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/elementtype.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/elementtype.resource.js index a235bcda3e..acc0a94485 100644 --- a/src/Umbraco.Web.UI.Client/src/common/resources/elementtype.resource.js +++ b/src/Umbraco.Web.UI.Client/src/common/resources/elementtype.resource.js @@ -8,21 +8,14 @@ function elementTypeResource($q, $http, umbRequestHelper) { return { getAll: function () { - - var url = Umbraco.Sys.ServerVariables.umbracoSettings.umbracoPath + "/backoffice/UmbracoApi/ElementType/GetAll"; - return umbRequestHelper.resourcePromise( - $http.get(url), - 'Failed to retrieve element types' - ); - /* return umbRequestHelper.resourcePromise( $http.get( umbRequestHelper.getApiUrl( "elementTypeApiBaseUrl", "GetAll")), - "Failed to retrieve data"); - */ + "Failed to retrieve element types"); + } }; diff --git a/src/Umbraco.Web/Editors/BackOfficeServerVariables.cs b/src/Umbraco.Web/Editors/BackOfficeServerVariables.cs index 53865830d8..9e02b629f9 100644 --- a/src/Umbraco.Web/Editors/BackOfficeServerVariables.cs +++ b/src/Umbraco.Web/Editors/BackOfficeServerVariables.cs @@ -318,6 +318,10 @@ namespace Umbraco.Web.Editors "imageUrlGeneratorApiBaseUrl", _urlHelper.GetUmbracoApiServiceBaseUrl( controller => controller.GetCropUrl(null, null, null, null, null)) }, + { + "elementTypeApiBaseUrl", _urlHelper.GetUmbracoApiServiceBaseUrl( + controller => controller.GetAll()) + }, } }, { diff --git a/src/Umbraco.Web/Editors/ElementTypeController.cs b/src/Umbraco.Web/Editors/ElementTypeController.cs index 64dfbbb4ab..e49a5eb214 100644 --- a/src/Umbraco.Web/Editors/ElementTypeController.cs +++ b/src/Umbraco.Web/Editors/ElementTypeController.cs @@ -1,7 +1,7 @@ using System.Collections.Generic; using System.Linq; +using System.Web.Http; using Umbraco.Core.Services; -using Umbraco.Web.Editors; using Umbraco.Web.Mvc; namespace Umbraco.Web.Editors @@ -9,7 +9,7 @@ namespace Umbraco.Web.Editors [PluginController("UmbracoApi")] public class ElementTypeController : UmbracoAuthorizedJsonController { - [System.Web.Http.HttpGet] + [HttpGet] public IEnumerable GetAll() { return Services.ContentTypeService From 79bf359fa0d3979756fd8fc346f0e8f6d756ab15 Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 26 Jun 2020 00:13:21 +1000 Subject: [PATCH 238/377] Gets property json paths working in complex editors, next lets see if we can make it work for validation. --- .../components/editor/umbeditors.directive.js | 120 +++++++++++++----- .../property/umbnestedproperty.component.js | 36 ++++-- .../components/umbnestedcontent.directive.js | 2 +- .../common/services/angularhelper.service.js | 17 +++ .../blockeditor/blockeditor.controller.js | 32 ++--- .../elementEditor.content.component.html | 28 ++-- .../elementEditor.content.component.js | 7 +- .../elementeditor/elementeditor.content.html | 2 +- .../elementeditor/elementeditor.settings.html | 2 +- .../views/components/editor/umb-editors.html | 43 ++++--- .../blockcard/blockcard.component.js | 2 + .../blocklist/blocklist.block.component.js | 16 ++- .../blocklist/blocklist.component.html | 62 ++++----- .../blocklist/blocklist.component.js | 22 ++-- .../blocklist.scopedblock.component.js | 18 ++- .../inlineblock/inlineblock.editor.html | 3 +- .../labelblock/labelblock.editor.html | 5 +- .../nestedcontent/nestedcontent.editor.html | 2 +- .../nestedcontent.propertyeditor.html | 2 +- 19 files changed, 279 insertions(+), 142 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbeditors.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbeditors.directive.js index 9bdef41225..21f0af6c8f 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbeditors.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbeditors.directive.js @@ -7,94 +7,94 @@ var evts = []; var allowedNumberOfVisibleEditors = 3; - + scope.editors = []; - + function addEditor(editor) { - + editor.inFront = true; editor.moveRight = true; editor.level = 0; editor.styleIndex = 0; - + // push the new editor to the dom scope.editors.push(editor); - + $timeout(() => { editor.moveRight = false; }) - + editor.animating = true; setTimeout(revealEditorContent.bind(this, editor), 400); - + updateEditors(); } - + function removeEditor(editor) { - + editor.moveRight = true; - + editor.animating = true; setTimeout(removeEditorFromDOM.bind(this, editor), 400); - + updateEditors(-1); - + } - + function revealEditorContent(editor) { - + editor.animating = false; - + scope.$digest(); - + } - + function removeEditorFromDOM(editor) { - + // push the new editor to the dom var index = scope.editors.indexOf(editor); if (index !== -1) { scope.editors.splice(index, 1); } - + updateEditors(); - + scope.$digest(); - + } - + /** update layer positions. With ability to offset positions, needed for when an item is moving out, then we dont want it to influence positions */ function updateEditors(offset) { - + offset = offset || 0;// fallback value. - + var len = scope.editors.length; var calcLen = len + offset; var ceiling = Math.min(calcLen, allowedNumberOfVisibleEditors); - var origin = Math.max(calcLen-1, 0)-ceiling; + var origin = Math.max(calcLen - 1, 0) - ceiling; var i = 0; - while(i= ceiling; i++; } } - + evts.push(eventsService.on("appState.editors.open", function (name, args) { addEditor(args.editor); })); evts.push(eventsService.on("appState.editors.close", function (name, args) { // remove the closed editor - if(args && args.editor) { + if (args && args.editor) { removeEditor(args.editor); } // close all editors - if(args && !args.editor && args.editors.length === 0) { + if (args && !args.editor && args.editors.length === 0) { scope.editors = []; } })); @@ -119,6 +119,64 @@ } + // This directive allows for us to run a custom $compile for the view within the repeater which allows + // us to maintain a $scope hierarchy with the rendered view based on the $scope that initiated the + // infinite editing. The retain the $scope hiearchy a special $parentScope property is passed in to the model. + function EditorRepeaterDirective($http, $templateCache, $compile) { + function link(scope, el, attr, ctrl) { + + var editor = scope && scope.$parent ? scope.$parent.model : null; + if (!editor) { + return; + } + + var unsubscribe = []; + + //if a custom parent scope is defined then we need to manually compile the view + if (editor.$parentScope) { + var element = el.find(".scoped-view"); + $http.get(editor.view, { cache: $templateCache }) + .then(function (response) { + var templateScope = editor.$parentScope.$new(); + + unsubscribe.push(function () { + templateScope.$destroy(); + }); + + // NOTE: the 'model' name here directly affects the naming convention used in infinite editors, this why you access the model + // like $scope.model.If this is changed, everything breaks.This is because we are entirely reliant upon ng - include and inheriting $scopes. + // by default without a $parentScope used for infinite editing the 'model' propety will be set because the view creates the scopes in + // ng-repeat by ng-repeat="model in editors" + templateScope.model = editor; + + element.html(response.data); + element.show(); + $compile(element.contents())(templateScope); + }); + } + + scope.$on('$destroy', function () { + for (var i = 0; i < unsubscribe.length; i++) { + unsubscribe[i](); + } + }); + } + + var directive = { + restrict: 'E', + replace: true, + transclude: true, + scope: { + editors: "=" + }, + template: "
      ", + link: link + }; + + return directive; + } + angular.module('umbraco.directives').directive('umbEditors', EditorsDirective); + angular.module('umbraco.directives').directive('umbEditorRepeater', EditorRepeaterDirective); })(); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/property/umbnestedproperty.component.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/property/umbnestedproperty.component.js index 239259b3fa..2679ce92a0 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/property/umbnestedproperty.component.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/property/umbnestedproperty.component.js @@ -4,44 +4,54 @@ /** * @ngdoc component - * @name Umbraco.umbBlockListBlockContent + * @name umbraco.umbNestedProperty * @function * * @description - * The component for a style-inheriting block of the block list property editor. + * Used to have nested property editors within complex editors in order to generate jsonpath for them to be used in validation */ angular .module("umbraco") .component("umbNestedProperty", { transclude: true, - template: '
      ', + template: '
      {{vm.getValidationPath()}}
      ', controller: NestedPropertyController, controllerAs: 'vm', bindings: { propertyTypeAlias: "@", - elementTypeIndex: "@" + elementTypeIndex: "<", + findInScopeChain: "<" // TODO: Use/enable this }, require: { - umbNestedProperty: "?^^umbNestedProperty" + umbNestedProperty: "?^^" } }); - function NestedPropertyController($scope) { + function NestedPropertyController($scope, angularHelper) { var vm = this; vm.$onInit = function () { - + if (!vm.propertyTypeAlias) { + throw "no propertyTypeAlias specified for umbNestedProperty"; + } }; + vm.$postLink = function () { + // if directive inheritance (DOM) doesn't find one, then check scope inheritance + if (!vm.umbNestedProperty/* && findInScopeChain*/) { + var found = angularHelper.traverseScopeChain($scope, s => s.vm && s.vm.constructor.name == "NestedPropertyController"); + if (found) { + vm.umbNestedProperty = found.vm; + } + } + } + // returns a jsonpath for where this property is located in a hierarchy // this will call into all hierarchical parents vm.getValidationPath = function () { - var path = vm.umbNestedProperty ? vm.umbNestedProperty.getValidationPath() : "$"; - if (vm.propertyTypeAlias && vm.elementTypeIndex) { - path += ".[nestedValidation].[" + vm.elementTypeIndex + "].[" + vm.propertyTypeAlias + "]"; - return path; - } - return null; + var path = vm.umbNestedProperty ? vm.umbNestedProperty.getValidationPath() : "$"; + path += ".[nestedValidation].[" + vm.elementTypeIndex + "].[" + vm.propertyTypeAlias + "]"; + return path; } } diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbnestedcontent.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbnestedcontent.directive.js index b135d9cdc4..b632471a69 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbnestedcontent.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbnestedcontent.directive.js @@ -67,7 +67,7 @@ scope: { ngModel: '=', tabAlias: '=', - itemIndex: '@' + itemIndex: '=' }, link: link }; diff --git a/src/Umbraco.Web.UI.Client/src/common/services/angularhelper.service.js b/src/Umbraco.Web.UI.Client/src/common/services/angularhelper.service.js index f2ff711ac9..325c6255a5 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/angularhelper.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/angularhelper.service.js @@ -9,6 +9,23 @@ function angularHelper($q) { return { + /** + * Will traverse up the $scope chain to all ancestors until the predicate matches for the current scope or until it's at the root. + * @param {any} scope + * @param {any} predicate + */ + traverseScopeChain: function (scope, predicate) { + var s = scope.$parent; + while (s) { + var result = predicate(s); + if (result === true) { + return s; + } + s = s.$parent; + } + return null; + }, + /** * Method used to re-run the $parsers for a given ngModel * @param {} scope 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 08ffb4a9ae..48522d22ff 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 @@ -4,21 +4,19 @@ angular.module("umbraco") function ($scope, localizationService, formHelper) { var vm = this; + // TODO: Why are we assigning content/setting separately when we already have vm.model? vm.content = $scope.model.content; - vm.settings = $scope.model.settings; - + vm.settings = $scope.model.settings; + vm.model = $scope.model; + vm.tabs = []; localizationService.localizeMany([ - $scope.model.liveEditing ? "prompt_discardChanges" : "general_close", - $scope.model.liveEditing ? "buttons_confirmActionConfirm" : "buttons_submitChanges" + vm.model.liveEditing ? "prompt_discardChanges" : "general_close", + vm.model.liveEditing ? "buttons_confirmActionConfirm" : "buttons_submitChanges" ]).then(function (data) { vm.closeLabel = data[0]; vm.submitLabel = data[1]; }); - vm.model = $scope.model; - - vm.tabs = []; - if (vm.content && vm.content.variants) { var apps = vm.content.apps; @@ -27,11 +25,12 @@ angular.module("umbraco") // replace view of content app. var contentApp = apps.find(entry => entry.alias === "umbContent"); - if(contentApp) { + if (contentApp) { + // TODO: This is strange, why does this render a view from somewhere else and this is the only place where that view is used? contentApp.view = "views/common/infiniteeditors/elementeditor/elementeditor.content.html"; - if($scope.model.hideContent) { + if(vm.model.hideContent) { apps.splice(apps.indexOf(contentApp), 1); - } else if ($scope.model.openSettings !== true) { + } else if (vm.model.openSettings !== true) { contentApp.active = true; } } @@ -49,10 +48,11 @@ angular.module("umbraco") "name": settingsName, "alias": "settings", "icon": "icon-settings", + // TODO: This is strange, why does this render a view from somewhere else and this is the only place where that view is used? "view": "views/common/infiniteeditors/elementeditor/elementeditor.settings.html" }; vm.tabs.push(settingsTab); - if ($scope.model.openSettings) { + if (vm.model.openSettings) { settingsTab.active = true; } } @@ -60,17 +60,17 @@ angular.module("umbraco") } vm.submitAndClose = function () { - if ($scope.model && $scope.model.submit) { + if (vm.model && vm.model.submit) { if (formHelper.submitForm({ scope: $scope })) { - $scope.model.submit($scope.model); + vm.model.submit(vm.model); } } } vm.close = function() { - if ($scope.model && $scope.model.close) { + if (vm.model && vm.model.close) { // TODO: If content has changed, we should notify user. - $scope.model.close($scope.model); + vm.model.close(vm.model); } } diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/elementeditor/elementEditor.content.component.html b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/elementeditor/elementEditor.content.component.html index 5308173c72..aa1a8c5afa 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/elementeditor/elementEditor.content.component.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/elementeditor/elementEditor.content.component.html @@ -9,19 +9,23 @@
      - -
      - - -
      + + + + +
      + + +
      + +
      diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/elementeditor/elementEditor.content.component.js b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/elementeditor/elementEditor.content.component.js index 5056576ca3..c4456a584b 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/elementeditor/elementEditor.content.component.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/elementeditor/elementEditor.content.component.js @@ -1,6 +1,8 @@ (function () { 'use strict'; + // TODO: Add docs - this component is used to render a content item based on an Element Type as a nested editor + angular .module('umbraco.directives') .component('umbElementEditorContent', { @@ -8,7 +10,10 @@ controller: ElementEditorContentComponentController, controllerAs: 'vm', bindings: { - model: '=' + model: '=', + // As this component is used for creating nested editors based on an element type, we need to know the index of this nested + // editor so that validation works. For example, if this is used in the block editor, this is the index of the block being rendered. + itemIndex: '<' } }); diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/elementeditor/elementeditor.content.html b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/elementeditor/elementeditor.content.html index eb8c72c579..3e2aec72a3 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/elementeditor/elementeditor.content.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/elementeditor/elementeditor.content.html @@ -1,3 +1,3 @@
      - +
      diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/elementeditor/elementeditor.settings.html b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/elementeditor/elementeditor.settings.html index df69e2e648..63222a5cab 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/elementeditor/elementeditor.settings.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/elementeditor/elementeditor.settings.html @@ -1 +1 @@ - + diff --git a/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editors.html b/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editors.html index 2f1286b090..9bb5a6161d 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editors.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/editor/umb-editors.html @@ -1,26 +1,29 @@
      -
      + +
      -
      + +
      +
      -
      + +
      diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blockeditor/blockcard/blockcard.component.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blockeditor/blockcard/blockcard.component.js index 8f1cb00c6d..f07c4f1529 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blockeditor/blockcard/blockcard.component.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blockeditor/blockcard/blockcard.component.js @@ -1,6 +1,8 @@ (function () { "use strict"; + // TODO: Does this belong in the property editors folder? + angular .module("umbraco") .component("umbBlockCard", { diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.block.component.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.block.component.js index fb47ffdaab..af4fcf438c 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.block.component.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.block.component.js @@ -19,7 +19,7 @@ bindings: { view: "@", block: "=", - api: "=", + api: "=", // Should this be a one way bind? index: "<" } } @@ -27,10 +27,22 @@ function BlockListBlockContentController($scope) { var model = this; - model.$onInit = function() { + model.$onInit = function () { + // Ugh, due to the way we work with angularjs and property editors not being components and needing to use ng-include, + // it means we need to expose things directly on the $scope so they can use them. + // It also means we need to watch for changes and upate the $scope values. + $scope.block = model.block; $scope.api = model.api; + $scope.index = model.index; }; + model.$onChanges = function (changes) { + if (changes.index) { + $scope.index = changes.index.currentValue; + } + + // TODO: Wouldn't we need to watch for any changes to model.block/api here too? + } } diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.html index 3c255e023a..ac2e9eb376 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.html @@ -8,31 +8,36 @@
      -
      - - - - + - + - -
      -
      - @@ -82,12 +85,11 @@
      - + diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.js index 0ae5527da9..33684e9c03 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.component.js @@ -131,6 +131,7 @@ // Append the blockObjects to our layout. vm.layout.forEach(entry => { if (entry.$block === undefined || entry.$block === null) { + var block = getBlockObject(entry); // If this entry was not supported by our property-editor it would return 'null'. @@ -221,7 +222,12 @@ }); } - function editBlock(blockObject, openSettings) { + function editBlock(blockObject, openSettings, blockIndex) { + + // this must be set + if (blockIndex === undefined) { + throw "blockIndex was not specified on call to editBlock"; + } var wasNotActiveBefore = blockObject.active !== true; blockObject.active = true; @@ -239,12 +245,14 @@ } var hideContent = (openSettings === true && inlineEditing === true); - + var blockEditorModel = { + $parentScope: $scope, // pass in a $parentScope, this maintains the scope inheritance in infinite editing hideContent: hideContent, openSettings: openSettings === true, liveEditing: liveEditing, title: blockObject.label, + index: blockIndex, view: "views/common/infiniteeditors/blockeditor/blockeditor.html", size: blockObject.config.editorSize || "medium", submit: function(blockEditorModel) { @@ -326,7 +334,7 @@ if(!(mouseEvent.ctrlKey || mouseEvent.metaKey)) { editorService.close(); if (added && vm.layout.length > createIndex) { - editBlock(vm.layout[createIndex].$block); + editBlock(vm.layout[createIndex].$block, false, createIndex); } } }, @@ -471,18 +479,16 @@ }); } - function openSettingsForBlock(block) { - editBlock(block, true); + function openSettingsForBlock(block, blockIndex) { + editBlock(block, true, blockIndex); } - - vm.blockEditorApi = { editBlock: editBlock, requestCopyBlock: requestCopyBlock, requestDeleteBlock: requestDeleteBlock, deleteBlock: deleteBlock, - openSettingsForBlock: openSettingsForBlock + openSettingsForBlock: openSettingsForBlock } vm.sortableOptions = { diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.scopedblock.component.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.scopedblock.component.js index b778abc35b..4678b5741f 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.scopedblock.component.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.scopedblock.component.js @@ -19,7 +19,7 @@ stylesheet: "@", view: "@", block: "=", - api: "=", + api: "=", // Should this be a one way bind? index: "<" } } @@ -27,9 +27,15 @@ function BlockListScopedBlockContentController($compile, $element, $scope) { var model = this; - model.$onInit = function() { + model.$onInit = function () { + // Ugh, due to the way we work with angularjs and property editors not being components and needing to use ng-include, + // it means we need to expose things directly on the $scope so they can use them. + // It also means we need to watch for changes and upate the $scope values. + $scope.block = model.block; $scope.api = model.api; + $scope.index = model.index; + var shadowRoot = $element[0].attachShadow({mode:'open'}); shadowRoot.innerHTML = ` +
      + `; + $compile(shadowRoot)($scope); + } + } + + +})(); 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 new file mode 100644 index 0000000000..880334a7b3 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/umbBlockListPropertyEditor.component.js @@ -0,0 +1,534 @@ +(function () { + "use strict"; + + + /** + * @ngdoc directive + * @name umbraco.directives.directive:umbBlockListPropertyEditor + * @function + * + * @description + * The component for the block list property editor. + */ + angular + .module("umbraco") + .component("umbBlockListPropertyEditor", { + templateUrl: "views/propertyeditors/blocklist/umb-block-list-property-editor.html", + controller: BlockListController, + controllerAs: "vm", + bindings: { + model: "=", + propertyForm: "=" + }, + require: { + umbProperty: "?^umbProperty", + umbVariantContent: '?^^umbVariantContent', + umbVariantContentEditors: '?^^umbVariantContentEditors', + umbElementEditorContent: '?^^umbElementEditorContent' + } + }); + + function BlockListController($scope, editorService, clipboardService, localizationService, overlayService, blockEditorService) { + + var unsubscribe = []; + var modelObject; + + // Property actions: + var copyAllBlocksAction; + var deleteAllBlocksAction; + + var inlineEditing = false; + var liveEditing = true; + + var vm = this; + + vm.loading = true; + vm.currentBlockInFocus = null; + vm.setBlockFocus = function(block) { + if(vm.currentBlockInFocus !== null) { + vm.currentBlockInFocus.focus = false; + } + vm.currentBlockInFocus = block; + block.focus = true; + } + vm.supportCopy = clipboardService.isSupported(); + + vm.layout = [];// The layout object specific to this Block Editor, will be a direct reference from Property Model. + vm.availableBlockTypes = [];// Available block entries of this property editor. + + var labels = {}; + vm.labels = labels; + localizationService.localizeMany(["grid_addElement", "content_createEmpty"]).then(function (data) { + labels.grid_addElement = data[0]; + labels.content_createEmpty = data[1]; + }); + + + + + + vm.$onInit = function() { + + inlineEditing = vm.model.config.useInlineEditingAsDefault; + liveEditing = vm.model.config.useLiveEditing; + + vm.validationLimit = vm.model.config.validationLimit; + + vm.listWrapperStyles = {}; + + if (vm.model.config.maxPropertyWidth) { + vm.listWrapperStyles['max-width'] = vm.model.config.maxPropertyWidth; + } + + // We need to ensure that the property model value is an object, this is needed for modelObject to recive a reference and keep that updated. + if(typeof vm.model.value !== 'object' || vm.model.value === null) {// testing if we have null or undefined value or if the value is set to another type than Object. + vm.model.value = {}; + } + + var scopeOfExistence = $scope; + if(vm.umbVariantContentEditors && vm.umbVariantContentEditors.getScope) { + scopeOfExistence = vm.umbVariantContentEditors.getScope(); + } + + // Create Model Object, to manage our data for this Block Editor. + modelObject = blockEditorService.createModelObject(vm.model.value, vm.model.editor, vm.model.config.blocks, scopeOfExistence, $scope); + modelObject.load().then(onLoaded); + + copyAllBlocksAction = { + labelKey: "clipboard_labelForCopyAllEntries", + labelTokens: [vm.model.label], + icon: "documents", + method: requestCopyAllBlocks, + isDisabled: true + } + deleteAllBlocksAction = { + labelKey: 'clipboard_labelForRemoveAllEntries', + labelTokens: [], + icon: 'trash', + method: requestDeleteAllBlocks, + isDisabled: true + } + + var propertyActions = [ + copyAllBlocksAction, + deleteAllBlocksAction + ]; + + if (vm.umbProperty) { + vm.umbProperty.setPropertyActions(propertyActions); + } + }; + + + + + function setDirty() { + if (vm.propertyForm) { + vm.propertyForm.$setDirty(); + } + } + + function onLoaded() { + + // Store a reference to the layout model, because we need to maintain this model. + vm.layout = modelObject.getLayout([]); + + // Append the blockObjects to our layout. + vm.layout.forEach(entry => { + if (entry.$block === undefined || entry.$block === null) { + var block = getBlockObject(entry); + + // If this entry was not supported by our property-editor it would return 'null'. + if(block !== null) { + entry.$block = block; + } + } + }); + + vm.availableContentTypesAliases = modelObject.getAvailableAliasesForBlockContent(); + vm.availableBlockTypes = modelObject.getAvailableBlocksForBlockPicker(); + + vm.loading = false; + + $scope.$evalAsync(); + + } + + function getDefaultViewForBlock(block) { + + if (block.config.unsupported === true) + return "views/propertyeditors/blocklist/blocklistentryeditors/unsupportedblock/unsupportedblock.editor.html"; + + if (inlineEditing === true) + return "views/propertyeditors/blocklist/blocklistentryeditors/inlineblock/inlineblock.editor.html"; + return "views/propertyeditors/blocklist/blocklistentryeditors/labelblock/labelblock.editor.html"; + } + + function getBlockObject(entry) { + var block = modelObject.getBlockObject(entry); + + if (block === null) return null; + + block.view = (block.config.view ? "/" + block.config.view : getDefaultViewForBlock(block)); + + block.showSettings = block.config.settingsElementTypeKey != null; + block.showCopy = vm.supportCopy && block.config.contentTypeKey != null;// if we have content, otherwise it dosnt make sense to copy. + + return block; + } + + + function addNewBlock(index, contentTypeKey) { + + // Create layout entry. (not added to property model jet.) + var layoutEntry = modelObject.create(contentTypeKey); + if (layoutEntry === null) { + return false; + } + + // make block model + var blockObject = getBlockObject(layoutEntry); + if (blockObject === null) { + return false; + } + + // If we reach this line, we are good to add the layoutEntry and blockObject to our models. + + // Add the Block Object to our layout entry. + layoutEntry.$block = blockObject; + + // add layout entry at the decired location in layout. + vm.layout.splice(index, 0, layoutEntry); + + // lets move focus to this new block. + vm.setBlockFocus(blockObject); + + return true; + + } + + + + function deleteBlock(block) { + + var layoutIndex = vm.layout.findIndex(entry => entry.udi === block.content.udi); + if(layoutIndex === -1) { + throw new Error("Could not find layout entry of block with udi: "+block.content.udi) + } + vm.layout.splice(layoutIndex, 1); + modelObject.removeDataAndDestroyModel(block); + + } + + function deleteAllBlocks() { + vm.layout.forEach(entry => { + deleteBlock(entry.$block); + }); + } + + function editBlock(blockObject, openSettings) { + + var wasNotActiveBefore = blockObject.active !== true; + blockObject.active = true; + + if (inlineEditing === true && openSettings !== true) { + return; + } + + // make a clone to avoid editing model directly. + var blockContentClone = Utilities.copy(blockObject.content); + var blockSettingsClone = null; + + if (blockObject.config.settingsElementTypeKey) { + blockSettingsClone = Utilities.copy(blockObject.settings); + } + + var hideContent = (openSettings === true && inlineEditing === true); + + var blockEditorModel = { + hideContent: hideContent, + openSettings: openSettings === true, + liveEditing: liveEditing, + title: blockObject.label, + view: "views/common/infiniteeditors/blockeditor/blockeditor.html", + size: blockObject.config.editorSize || "medium", + submit: function(blockEditorModel) { + + if (liveEditing === false) { + // transfer values when submitting in none-liveediting mode. + blockObject.retriveValuesFrom(blockEditorModel.content, blockEditorModel.settings); + } + + blockObject.active = false; + editorService.close(); + }, + close: function() { + + if (liveEditing === true) { + // revert values when closing in liveediting mode. + blockObject.retriveValuesFrom(blockContentClone, blockSettingsClone); + } + + if (wasNotActiveBefore === true) { + blockObject.active = false; + } + editorService.close(); + } + }; + + if (liveEditing === true) { + blockEditorModel.content = blockObject.content; + blockEditorModel.settings = blockObject.settings; + } else { + blockEditorModel.content = blockContentClone; + blockEditorModel.settings = blockSettingsClone; + } + + // open property settings editor + editorService.open(blockEditorModel); + } + + vm.showCreateDialog = showCreateDialog; + function showCreateDialog(createIndex, $event) { + + if (vm.blockTypePicker) { + return; + } + + if (vm.availableBlockTypes.length === 0) { + return; + } + + var amountOfAvailableTypes = vm.availableBlockTypes.length; + var blockPickerModel = { + availableItems: vm.availableBlockTypes, + title: vm.labels.grid_addElement, + orderBy: "$index", + view: "views/common/infiniteeditors/blockpicker/blockpicker.html", + size: (amountOfAvailableTypes > 8 ? "medium" : "small"), + filter: (amountOfAvailableTypes > 8), + clickPasteItem: function(item, mouseEvent) { + if (item.type === "elementTypeArray") { + var indexIncrementor = 0; + item.pasteData.forEach(function (entry) { + if (requestPasteFromClipboard(createIndex + indexIncrementor, entry)) { + indexIncrementor++; + } + }); + } else { + requestPasteFromClipboard(createIndex, item.pasteData); + } + if(!(mouseEvent.ctrlKey || mouseEvent.metaKey)) { + blockPickerModel.close(); + } + }, + submit: function(blockPickerModel, mouseEvent) { + var added = false; + if (blockPickerModel && blockPickerModel.selectedItem) { + added = addNewBlock(createIndex, blockPickerModel.selectedItem.blockConfigModel.contentTypeKey); + } + + if(!(mouseEvent.ctrlKey || mouseEvent.metaKey)) { + editorService.close(); + if (added && vm.layout.length > createIndex) { + editBlock(vm.layout[createIndex].$block); + } + } + }, + close: function() { + // if opned by a inline creator button(index less than length), we want to move the focus away, to hide line-creator. + if (createIndex < vm.layout.length) { + vm.setBlockFocus(vm.layout[Math.max(createIndex-1, 0)].$block); + } + + editorService.close(); + } + }; + + blockPickerModel.clickClearClipboard = function ($event) { + clipboardService.clearEntriesOfType("elementType", vm.availableContentTypesAliases); + clipboardService.clearEntriesOfType("elementTypeArray", vm.availableContentTypesAliases); + }; + + blockPickerModel.clipboardItems = []; + + var singleEntriesForPaste = clipboardService.retriveEntriesOfType("elementType", vm.availableContentTypesAliases); + singleEntriesForPaste.forEach(function (entry) { + blockPickerModel.clipboardItems.push( + { + type: "elementType", + pasteData: entry.data, + blockConfigModel: modelObject.getScaffoldFromAlias(entry.alias), + elementTypeModel: { + name: entry.label, + icon: entry.icon + } + } + ); + }); + + var arrayEntriesForPaste = clipboardService.retriveEntriesOfType("elementTypeArray", vm.availableContentTypesAliases); + arrayEntriesForPaste.forEach(function (entry) { + blockPickerModel.clipboardItems.push( + { + type: "elementTypeArray", + pasteData: entry.data, + blockConfigModel: {}, // no block configuration for paste items of elementTypeArray. + elementTypeModel: { + name: entry.label, + icon: entry.icon + } + } + ); + }); + + // open block picker overlay + editorService.open(blockPickerModel); + + }; + + var requestCopyAllBlocks = function() { + + var elementTypesToCopy = vm.layout.filter(entry => entry.$block.config.unsupported !== true).map(entry => entry.$block.content); + + // list aliases + var aliases = elementTypesToCopy.map(content => content.contentTypeAlias); + + // remove dublicates + aliases = aliases.filter((item, index) => aliases.indexOf(item) === index); + + var contentNodeName = ""; + if(vm.umbVariantContent) { + contentNodeName = vm.umbVariantContent.editor.content.name; + } else if (vm.umbElementEditorContent) { + contentNodeName = vm.umbElementEditorContent.model.documentType.name + } + + localizationService.localize("clipboard_labelForArrayOfItemsFrom", [vm.model.label, contentNodeName]).then(function(localizedLabel) { + clipboardService.copyArray("elementTypeArray", aliases, elementTypesToCopy, localizedLabel, "icon-thumbnail-list", vm.model.id); + }); + } + function copyBlock(block) { + clipboardService.copy("elementType", block.content.contentTypeAlias, block.content, block.label); + } + function requestPasteFromClipboard(index, pasteEntry) { + + if (pasteEntry === undefined) { + return false; + } + + var layoutEntry = modelObject.createFromElementType(pasteEntry); + if (layoutEntry === null) { + return false; + } + + // make block model + var blockObject = getBlockObject(layoutEntry); + if (blockObject === null) { + return false; + } + + // set the BlockObject on our layout entry. + layoutEntry.$block = blockObject; + + // insert layout entry at the decired location in layout. + vm.layout.splice(index, 0, layoutEntry); + + vm.currentBlockInFocus = blockObject; + + return true; + + } + function requestDeleteBlock(block) { + localizationService.localizeMany(["general_delete", "blockEditor_confirmDeleteBlockMessage", "contentTypeEditor_yesDelete"]).then(function (data) { + const overlay = { + title: data[0], + content: localizationService.tokenReplace(data[1], [block.label]), + submitButtonLabel: data[2], + close: function () { + overlayService.close(); + }, + submit: function () { + deleteBlock(block); + overlayService.close(); + } + }; + + overlayService.confirmDelete(overlay); + }); + } + function requestDeleteAllBlocks() { + localizationService.localizeMany(["content_nestedContentDeleteAllItems", "general_delete"]).then(function (data) { + overlayService.confirmDelete({ + title: data[1], + content: data[0], + close: function () { + overlayService.close(); + }, + submit: function () { + deleteAllBlocks(); + overlayService.close(); + } + }); + }); + } + + function openSettingsForBlock(block) { + editBlock(block, true); + } + + + + vm.blockEditorApi = { + editBlock: editBlock, + copyBlock: copyBlock, + requestDeleteBlock: requestDeleteBlock, + deleteBlock: deleteBlock, + openSettingsForBlock: openSettingsForBlock + } + + vm.sortableOptions = { + axis: "y", + cursor: "grabbing", + handle: ".blockelement__draggable-element", + cancel: "input,textarea,select,option", + classes: ".blockelement--dragging", + distance: 5, + tolerance: "pointer", + scroll: true, + update: function (ev, ui) { + setDirty(); + } + }; + + + function onAmountOfBlocksChanged() { + + // enable/disable property actions + copyAllBlocksAction.isDisabled = vm.layout.length === 0; + deleteAllBlocksAction.isDisabled = vm.layout.length === 0; + + // validate limits: + if (vm.propertyForm) { + + var isMinRequirementGood = vm.validationLimit.min === null || vm.layout.length >= vm.validationLimit.min; + vm.propertyForm.minCount.$setValidity("minCount", isMinRequirementGood); + + var isMaxRequirementGood = vm.validationLimit.max === null || vm.layout.length <= vm.validationLimit.max; + vm.propertyForm.maxCount.$setValidity("maxCount", isMaxRequirementGood); + + } + } + unsubscribe.push($scope.$watch(() => vm.layout.length, onAmountOfBlocksChanged)); + + + $scope.$on("$destroy", function () { + for (const subscription of unsubscribe) { + subscription(); + } + }); + + + } + +})(); diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/umbBlockListPropertyEditor.createButton.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/umbBlockListPropertyEditor.createButton.controller.js new file mode 100644 index 0000000000..98d4f4ea3a --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/umbBlockListPropertyEditor.createButton.controller.js @@ -0,0 +1,18 @@ +(function () { + "use strict"; + + angular + .module("umbraco") + .controller("Umbraco.PropertyEditors.BlockListPropertyEditor.CreateButtonController", + function Controller($scope) { + + var vm = this; + vm.plusPosX = 0; + + vm.onMouseMove = function($event) { + vm.plusPosX = $event.offsetX; + } + + }); + +})(); diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker/mediapicker.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker/mediapicker.html index c4dba4d373..59e6fcd21c 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker/mediapicker.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/mediapicker/mediapicker.html @@ -3,7 +3,7 @@

      -
      +
      • 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 fad68acbee..59aaec662b 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 @@ -480,7 +480,8 @@ var notSupported = [ "Umbraco.Tags", "Umbraco.UploadField", - "Umbraco.ImageCropper" + "Umbraco.ImageCropper", + "Umbraco.BlockList" ]; // Initialize diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/notsupported/notsupported.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/notsupported/notsupported.html new file mode 100644 index 0000000000..a2fbb0e907 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/notsupported/notsupported.html @@ -0,0 +1,3 @@ +
        + +
        diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/notsupported/notsupported.less b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/notsupported/notsupported.less new file mode 100644 index 0000000000..5eaec3f67b --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/notsupported/notsupported.less @@ -0,0 +1,7 @@ +.umb-property-editor.umb-property-editor--notsupported { + background-color: @red; + color: white; + padding: 5px 10px; + width: auto; + border-radius: @baseBorderRadius * 2; +} diff --git a/src/Umbraco.Web.UI.Client/test/unit/common/filters/truncate-filters.spec.js b/src/Umbraco.Web.UI.Client/test/unit/common/filters/truncate-filters.spec.js index 1e9ea2ea46..64a984b499 100644 --- a/src/Umbraco.Web.UI.Client/test/unit/common/filters/truncate-filters.spec.js +++ b/src/Umbraco.Web.UI.Client/test/unit/common/filters/truncate-filters.spec.js @@ -53,7 +53,6 @@ testCases.forEach(function(test){ it('Expects \'' + test.input + '\' to be truncated as \''+ test.expectedResult + '\', when noOfChars=' + test.noOfChars + ', and appendDots=' + test.appendDots, function() { - console.log($truncate(test.input, test.noOfChars, test.appendDots)); expect($truncate(test.input, test.noOfChars, test.appendDots)).toBe(test.expectedResult); }); }); diff --git a/src/Umbraco.Web.UI.Client/test/unit/common/services/block-editor-service.spec.js b/src/Umbraco.Web.UI.Client/test/unit/common/services/block-editor-service.spec.js new file mode 100644 index 0000000000..b0e84a793e --- /dev/null +++ b/src/Umbraco.Web.UI.Client/test/unit/common/services/block-editor-service.spec.js @@ -0,0 +1,231 @@ +describe('blockEditorService tests', function () { + + var blockEditorService, contentResource, $rootScope, $scope; + + beforeEach(module('umbraco.services')); + beforeEach(module('umbraco.resources')); + beforeEach(module('umbraco.mocks')); + beforeEach(module('umbraco')); + + beforeEach(inject(function ($injector, mocksUtils, _$rootScope_) { + + mocksUtils.disableAuth(); + + $rootScope = _$rootScope_; + $scope = $rootScope.$new(); + + contentResource = $injector.get("contentResource"); + spyOn(contentResource, "getScaffoldByKey").and.callFake( + function () { + return Promise.resolve(mocksUtils.getMockVariantContent(1234)) + } + ); + + blockEditorService = $injector.get('blockEditorService'); + + })); + + + var blockConfigurationMock = { contentTypeKey: "7C5B74D1-E2F9-45A3-AE4B-FC7A829BF8AB", label:"Test label", settingsElementTypeKey: null, view: "testview.html"}; + + var propertyModelMock = { + layout: { + "Umbraco.TestBlockEditor": [ + { + udi: 1234 + } + ] + }, + data: [ + { + udi: 1234, + contentTypeKey: "7C5B74D1-E2F9-45A3-AE4B-FC7A829BF8AB", + testproperty: "myTestValue" + } + ] + }; + + describe('init blockEditorModelObject', function () { + + it('fail if no model value', function () { + function createWithNoModelValue() { + blockEditorService.createModelObject(null, "Umbraco.TestBlockEditor", [], $scope, $scope); + } + expect(createWithNoModelValue).toThrow(); + }); + + it('return a object, with methods', function () { + var modelObject = blockEditorService.createModelObject({}, "Umbraco.TestBlockEditor", [], $scope, $scope); + + expect(modelObject).not.toBeUndefined(); + expect(modelObject.load).not.toBeUndefined(); + }); + + it('getBlockConfiguration provide the requested block configurtion', function () { + var modelObject = blockEditorService.createModelObject({}, "Umbraco.TestBlockEditor", [blockConfigurationMock], $scope, $scope); + + expect(modelObject.getBlockConfiguration(blockConfigurationMock.contentTypeKey).label).toBe(blockConfigurationMock.label); + }); + + it('load provides data for itemPicker', function (done) { + var modelObject = blockEditorService.createModelObject({}, "Umbraco.TestBlockEditor", [blockConfigurationMock], $scope, $scope); + + modelObject.load().then(() => { + var itemPickerOptions = modelObject.getAvailableBlocksForBlockPicker(); + expect(itemPickerOptions.length).toBe(1); + expect(itemPickerOptions[0].blockConfigModel.contentTypeKey).toBe(blockConfigurationMock.contentTypeKey); + done(); + }); + + }); + + it('getLayoutEntry has values', function (done) { + + + var modelObject = blockEditorService.createModelObject(propertyModelMock, "Umbraco.TestBlockEditor", [blockConfigurationMock], $scope, $scope); + + modelObject.load().then(() => { + + var layout = modelObject.getLayout(); + + expect(layout).not.toBeUndefined(); + expect(layout.length).toBe(1); + expect(layout[0]).toBe(propertyModelMock.layout["Umbraco.TestBlockEditor"][0]); + expect(layout[0].udi).toBe(propertyModelMock.layout["Umbraco.TestBlockEditor"][0].udi); + + done(); + }); + + }); + + it('getBlockObject has values', function (done) { + + + var modelObject = blockEditorService.createModelObject(propertyModelMock, "Umbraco.TestBlockEditor", [blockConfigurationMock], $scope, $scope); + + modelObject.load().then(() => { + + var layout = modelObject.getLayout(); + + var blockObject = modelObject.getBlockObject(layout[0]); + + expect(blockObject).not.toBeUndefined(); + expect(blockObject.data.udi).toBe(propertyModelMock.data[0].udi); + expect(blockObject.content.variants[0].tabs[0].properties[0].value).toBe(propertyModelMock.data[0].testproperty); + + done(); + }); + + }); + + + it('getBlockObject syncs primative values', function (done) { + + var propertyModel = angular.copy(propertyModelMock); + + var modelObject = blockEditorService.createModelObject(propertyModel, "Umbraco.TestBlockEditor", [blockConfigurationMock], $scope, $scope); + + modelObject.load().then(() => { + + var layout = modelObject.getLayout(); + + var blockObject = modelObject.getBlockObject(layout[0]); + + blockObject.content.variants[0].tabs[0].properties[0].value = "anotherTestValue"; + + $rootScope.$digest();// invoke angularJS Store. + + expect(blockObject.data).toBe(propertyModel.data[0]); + expect(blockObject.data.testproperty).toBe("anotherTestValue"); + expect(propertyModel.data[0].testproperty).toBe("anotherTestValue"); + + // + + done(); + }); + + }); + + + it('getBlockObject syncs values of object', function (done) { + + var propertyModel = angular.copy(propertyModelMock); + + var complexValue = {"list": ["A", "B", "C"]}; + propertyModel.data[0].testproperty = complexValue; + + + var modelObject = blockEditorService.createModelObject(propertyModel, "Umbraco.TestBlockEditor", [blockConfigurationMock], $scope, $scope); + + modelObject.load().then(() => { + + var layout = modelObject.getLayout(); + + var blockObject = modelObject.getBlockObject(layout[0]); + + blockObject.content.variants[0].tabs[0].properties[0].value.list[0] = "AA"; + blockObject.content.variants[0].tabs[0].properties[0].value.list.push("D"); + + $rootScope.$digest();// invoke angularJS Store. + + expect(propertyModel.data[0].testproperty.list[0]).toBe("AA"); + expect(propertyModel.data[0].testproperty.list.length).toBe(4); + + done(); + }); + + }); + + it('layout is referencing layout of propertyModel', function (done) { + + var propertyModel = angular.copy(propertyModelMock); + + var modelObject = blockEditorService.createModelObject(propertyModel, "Umbraco.TestBlockEditor", [blockConfigurationMock], $scope, $scope); + + modelObject.load().then(() => { + + var layout = modelObject.getLayout(); + + // remove from layout; + layout.splice(0, 1); + + expect(propertyModel.layout["Umbraco.TestBlockEditor"].length).toBe(0); + expect(propertyModel.layout["Umbraco.TestBlockEditor"][0]).toBeUndefined(); + + done(); + }); + + }); + + it('removeDataAndDestroyModel removes data', function (done) { + + var propertyModel = angular.copy(propertyModelMock); + + var modelObject = blockEditorService.createModelObject(propertyModel, "Umbraco.TestBlockEditor", [blockConfigurationMock], $scope, $scope); + + modelObject.load().then(() => { + + var layout = modelObject.getLayout(); + + var blockObject = modelObject.getBlockObject(layout[0]); + + // remove from layout; + layout.splice(0, 1); + + // remove from data; + modelObject.removeDataAndDestroyModel(blockObject); + + expect(propertyModel.data.length).toBe(0); + expect(propertyModel.data[0]).toBeUndefined(); + expect(propertyModel.layout["Umbraco.TestBlockEditor"].length).toBe(0); + expect(propertyModel.layout["Umbraco.TestBlockEditor"][0]).toBeUndefined(); + + done(); + }); + + }); + + + }); + +}); diff --git a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj index 1d77505fc6..8a5f223bd5 100644 --- a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj +++ b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj @@ -285,6 +285,7 @@ + Web.Template.config diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/da.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/da.xml index 59ce327d4d..7df379aa81 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/da.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/da.xml @@ -17,6 +17,7 @@ Opret gruppe Slet Deaktivér + Edit settings Tøm papirkurv Aktivér Eksportér dokumenttype @@ -152,6 +153,8 @@ Fortryd Bekræft Flere publiseringsmuligheder + Indsæt + Indsæt og luk For @@ -607,6 +610,7 @@ Du skal stå til venstre for de 2 celler du ønsker at samle! Du kan ikke opdele en celle, som ikke allerede er delt. Denne egenskab er ugyldig + Feltet %0% bruger editor %1% som ikke er supporteret for ElementTyper. Om @@ -1810,8 +1814,41 @@ Mange hilsner fra Umbraco robotten Kopier %0% %0% fra %1% Fjern alle elementer + Ryd udklipsholder Åben egenskabshandlinger + + Opret ny blok + Tilføj en indstillings afsnit + Tilføj visning + Tilføj stylesheet + Vælg billede + Opret ny + Overskriv stylesheet + Tilføj stylesheet + Redigerings udseende + Data modeller + katalog udseende + Baggrunds farve + Ikon farve + Indholds model + Label + Speciel visning + Indstillings model + Rederings lagets størrelse + Tilføj speciel visning + Tilføj instillinger + Overskriv label form + %0%.]]> + Indhold der benytter sig af denne blok vil gå bort. + + Billede + Tilføj billede + Opret ny + Udklipsholder + Indstillinger + + diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml index 0150a5542d..eceb63c478 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml @@ -16,6 +16,7 @@ Create group Delete Disable + Edit settings Empty recycle bin Enable Export Document Type @@ -30,6 +31,7 @@ Unpublish Reload Republish entire site + Remove Rename Restore Set permissions for the page %0% @@ -154,6 +156,8 @@ Cancel Confirm More publishing options + Submit + Submit and close Viewing for @@ -427,7 +431,11 @@ Manage hostnames Close this window Are you sure you want to delete + Are you sure you want to delete %0% based on %1% Are you sure you want to disable + Are you sure you want to remove + %0%]]> + %0%]]> Are you sure? Are you sure? Cut @@ -613,6 +621,7 @@ %0% is a mandatory field %0% at %1% is not in a correct format %0% is not in a correct format + Property '%0%' uses editor '%1%' which is not supported in Element Types. Received an error from the server @@ -2302,6 +2311,7 @@ To manage your website, simply open the Umbraco back office and start adding con Copy %0% %0% from %1% Remove all items + Clear clipboard Open Property Actions @@ -2425,6 +2435,37 @@ To manage your website, simply open the Umbraco back office and start adding con Umbraco Forms Create forms using an intuitive drag and drop interface. From simple contact forms that sends e-mails to advanced questionaires that integrate with CRM systems. Your clients will love it! + + Create new block + Attach a settings section + Select view + Select stylesheet + Choose thumbnail + Create new + Custom stylesheet + Add stylesheet + Editor apperance + Data models + Catalogue appearance + Background color + Icon color + Content model + Label + Custom view + Settings model + Overlay editor size + Add custom view + Add settings + Overwrite label template + %0%.]]> + Content using this block will be lost. + + Thumbnail + Add thumbnail + Create empty + Clipboard + Settings + What are Content Templates? Content Templates are pre-defined content that can be selected when creating a new content node. 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 6700818ee0..93b73f25b4 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml @@ -17,6 +17,7 @@ Create group Delete Disable + Edit settings Empty recycle bin Enable Export Document Type @@ -31,6 +32,7 @@ Unpublish Reload Republish entire site + Remove Rename Restore Set permissions for the page %0% @@ -155,6 +157,8 @@ Cancel Confirm More publishing options + Submit + Submit and close Viewing for @@ -433,6 +437,9 @@ Close this window Are you sure you want to delete Are you sure you want to disable + Are you sure you want to remove + %0%]]> + %0%]]> Are you sure? Are you sure? Cut @@ -637,6 +644,7 @@ Please place cursor at the left of the two cells you wish to merge You cannot split a cell that hasn't been merged. This property is invalid + Property '%0%' uses editor '%1%' which is not supported in Element Types. Options @@ -2323,6 +2331,7 @@ To manage your website, simply open the Umbraco back office and start adding con Copy %0% %0% from %1% Remove all items + Clear clipboard Open Property Actions @@ -2446,6 +2455,37 @@ To manage your website, simply open the Umbraco back office and start adding con Umbraco Forms Create forms using an intuitive drag and drop interface. From simple contact forms that sends e-mails to advanced questionaires that integrate with CRM systems. Your clients will love it! + + Create new block + Attach a settings section + Select view + Select stylesheet + Choose thumbnail + Create new + Custom stylesheet + Add stylesheet + Editor apperance + Data models + Catalogue appearance + Background color + Icon color + Content model + Label + Custom view + Settings model + Overlay editor size + Add custom view + Add settings + Overwrite label template + %0%.]]> + Content using this block will be lost. + + Thumbnail + Add thumbnail + Create empty + Clipboard + Settings + What are Content Templates? Content Templates are pre-defined content that can be selected when creating a new content node. diff --git a/src/Umbraco.Web.UI/Views/Partials/BlockList/Default.cshtml b/src/Umbraco.Web.UI/Views/Partials/BlockList/Default.cshtml new file mode 100644 index 0000000000..8e5de940c5 --- /dev/null +++ b/src/Umbraco.Web.UI/Views/Partials/BlockList/Default.cshtml @@ -0,0 +1,20 @@ +@inherits UmbracoViewPage +@using Umbraco.Core.Models.Blocks +@{ + if (Model?.Layout == null || !Model.Layout.Any()) { return; } +} +
        + @foreach (var layout in Model.Layout) + { + if (layout?.Udi == null) { continue; } + var data = layout.Data; + try + { + @Html.Partial("BlockList/Components/" + data.ContentType.Alias, layout) + } + catch (Exception ex) + { + global::Umbraco.Core.Composing.Current.Logger.Error(typeof(BlockListModel), ex, "Could not display block list component for content type {0}", data?.ContentType?.Alias); + } + } +
        diff --git a/src/Umbraco.Web/BlockListTemplateExtensions.cs b/src/Umbraco.Web/BlockListTemplateExtensions.cs new file mode 100644 index 0000000000..1754eb4fc4 --- /dev/null +++ b/src/Umbraco.Web/BlockListTemplateExtensions.cs @@ -0,0 +1,44 @@ +using System; +using System.Linq; +using System.Web.Mvc; +using System.Web.Mvc.Html; +using Umbraco.Core.Models.Blocks; +using Umbraco.Core.Models.PublishedContent; + +namespace Umbraco.Web +{ + public static class BlockListTemplateExtensions + { + public const string DefaultFolder = "BlockList/"; + public const string DefaultTemplate = "Default"; + + public static MvcHtmlString GetBlockListHtml(this HtmlHelper html, BlockListModel model, string template = DefaultTemplate) + { + if (model?.Layout == null || !model.Layout.Any()) return new MvcHtmlString(string.Empty); + + var view = DefaultFolder + template; + 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 MvcHtmlString 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) + { + 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)); + + var prop = contentItem.GetProperty(propertyAlias); + if (prop == null) throw new InvalidOperationException("No property type found with alias " + propertyAlias); + + 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 MvcHtmlString 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); + } +} diff --git a/src/Umbraco.Web/Editors/BackOfficeServerVariables.cs b/src/Umbraco.Web/Editors/BackOfficeServerVariables.cs index 53865830d8..9e02b629f9 100644 --- a/src/Umbraco.Web/Editors/BackOfficeServerVariables.cs +++ b/src/Umbraco.Web/Editors/BackOfficeServerVariables.cs @@ -318,6 +318,10 @@ namespace Umbraco.Web.Editors "imageUrlGeneratorApiBaseUrl", _urlHelper.GetUmbracoApiServiceBaseUrl( controller => controller.GetCropUrl(null, null, null, null, null)) }, + { + "elementTypeApiBaseUrl", _urlHelper.GetUmbracoApiServiceBaseUrl( + controller => controller.GetAll()) + }, } }, { diff --git a/src/Umbraco.Web/Editors/ContentController.cs b/src/Umbraco.Web/Editors/ContentController.cs index 565eebf80e..b004d49ab0 100644 --- a/src/Umbraco.Web/Editors/ContentController.cs +++ b/src/Umbraco.Web/Editors/ContentController.cs @@ -355,6 +355,29 @@ namespace Umbraco.Web.Editors throw new HttpResponseException(HttpStatusCode.NotFound); } + return GetEmpty(contentType, parentId); + } + + + /// + /// Gets an empty content item for the document type. + /// + /// + /// + [OutgoingEditorModelEvent] + public ContentItemDisplay GetEmptyByKey(Guid contentTypeKey, int parentId) + { + var contentType = Services.ContentTypeService.Get(contentTypeKey); + if (contentType == null) + { + throw new HttpResponseException(HttpStatusCode.NotFound); + } + + return GetEmpty(contentType, parentId); + } + + private ContentItemDisplay GetEmpty(IContentType contentType, int parentId) + { var emptyContent = Services.ContentService.Create("", parentId, contentType.Alias, Security.GetUserId().ResultOr(0)); var mapped = MapToDisplay(emptyContent); // translate the content type name if applicable @@ -821,7 +844,7 @@ namespace Umbraco.Web.Editors /// /// /// For invariant, the variants collection count will be 1 and this will check if that invariant item has the critical values for persistence (i.e. Name) - /// + /// /// For variant, each variant will be checked for critical data for persistence and if it's not there then it's flags will be reset and it will not /// be persisted. However, we also need to deal with the case where all variants don't pass this check and then there is nothing to save. This also deals /// with removing the Name validation keys based on data annotations validation for items that haven't been marked to be saved. @@ -908,8 +931,8 @@ namespace Umbraco.Web.Editors var savedWithoutErrors = contentItem.Variants .Where(x => x.Save && !variantErrors.Contains((x.Culture, x.Segment))) - .Select(x => (culture: x.Culture, segment: x.Segment)); - + .Select(x => (culture: x.Culture, segment: x.Segment)); + foreach (var (culture, segment) in savedWithoutErrors) { var variantName = GetVariantName(culture, segment); @@ -1258,7 +1281,7 @@ namespace Umbraco.Web.Editors //Now check if there are validation errors on each variant. //If validation errors are detected on a variant and it's state is set to 'publish', then we //need to change it to 'save'. - //It is a requirement that this is performed AFTER ValidatePublishingMandatoryLanguages. + //It is a requirement that this is performed AFTER ValidatePublishingMandatoryLanguages. foreach (var variant in contentItem.Variants) { if (variantErrors.Contains((variant.Culture, variant.Segment))) @@ -1389,7 +1412,7 @@ namespace Umbraco.Web.Editors /// Segment to assign the error to /// /// - /// The culture used in the localization message, null by default which means will be used. + /// The culture used in the localization message, null by default which means will be used. /// private void AddVariantValidationError(string culture, string segment, string localizationKey, string cultureToken = null) { @@ -1402,7 +1425,7 @@ namespace Umbraco.Web.Editors } /// - /// Creates the human readable variant name based on culture and segment + /// Creates the human readable variant name based on culture and segment /// /// Culture /// Segment diff --git a/src/Umbraco.Web/Editors/ElementTypeController.cs b/src/Umbraco.Web/Editors/ElementTypeController.cs new file mode 100644 index 0000000000..e49a5eb214 --- /dev/null +++ b/src/Umbraco.Web/Editors/ElementTypeController.cs @@ -0,0 +1,29 @@ +using System.Collections.Generic; +using System.Linq; +using System.Web.Http; +using Umbraco.Core.Services; +using Umbraco.Web.Mvc; + +namespace Umbraco.Web.Editors +{ + [PluginController("UmbracoApi")] + public class ElementTypeController : UmbracoAuthorizedJsonController + { + [HttpGet] + public IEnumerable GetAll() + { + return Services.ContentTypeService + .GetAllElementTypes() + .OrderBy(x => x.SortOrder) + .Select(x => new + { + id = x.Id, + key = x.Key, + name = x.Name, + description = x.Description, + alias = x.Alias, + icon = x.Icon + }); + } + } +} diff --git a/src/Umbraco.Web/Models/ContentEditing/ContentItemDisplay.cs b/src/Umbraco.Web/Models/ContentEditing/ContentItemDisplay.cs index b6a90a93c3..1ef9207628 100644 --- a/src/Umbraco.Web/Models/ContentEditing/ContentItemDisplay.cs +++ b/src/Umbraco.Web/Models/ContentEditing/ContentItemDisplay.cs @@ -111,6 +111,9 @@ namespace Umbraco.Web.Models.ContentEditing [DataMember(Name = "contentTypeId")] public int ContentTypeId { get; set; } + [DataMember(Name = "contentTypeKey")] + public Guid ContentTypeKey { get; set; } + [DataMember(Name = "contentTypeAlias", IsRequired = true)] [Required(AllowEmptyStrings = false)] public string ContentTypeAlias { get; set; } diff --git a/src/Umbraco.Web/Models/Mapping/ContentMapDefinition.cs b/src/Umbraco.Web/Models/Mapping/ContentMapDefinition.cs index 865057ba24..50ed4684d9 100644 --- a/src/Umbraco.Web/Models/Mapping/ContentMapDefinition.cs +++ b/src/Umbraco.Web/Models/Mapping/ContentMapDefinition.cs @@ -76,6 +76,7 @@ namespace Umbraco.Web.Models.Mapping target.AllowedTemplates = GetAllowedTemplates(source); target.ContentApps = _commonMapper.GetContentApps(source); target.ContentTypeId = source.ContentType.Id; + target.ContentTypeKey = source.ContentType.Key; target.ContentTypeAlias = source.ContentType.Alias; target.ContentTypeName = _localizedTextService.UmbracoDictionaryTranslate(source.ContentType.Name); target.DocumentType = _commonMapper.GetContentType(source, context); diff --git a/src/Umbraco.Web/PropertyEditors/BlockEditorPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/BlockEditorPropertyEditor.cs new file mode 100644 index 0000000000..b32a91c058 --- /dev/null +++ b/src/Umbraco.Web/PropertyEditors/BlockEditorPropertyEditor.cs @@ -0,0 +1,275 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using System; +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Text.RegularExpressions; +using Umbraco.Core; +using Umbraco.Core.Logging; +using Umbraco.Core.Models; +using Umbraco.Core.Models.Blocks; +using Umbraco.Core.Models.Editors; +using Umbraco.Core.PropertyEditors; +using Umbraco.Core.Services; + +namespace Umbraco.Web.PropertyEditors +{ + /// + /// Abstract class for block editor based editors + /// + public abstract class BlockEditorPropertyEditor : DataEditor + { + public const string ContentTypeKeyPropertyKey = "contentTypeKey"; + public const string UdiPropertyKey = "udi"; + private readonly IBlockEditorDataHelper _dataHelper; + private readonly Lazy _propertyEditors; + private readonly IDataTypeService _dataTypeService; + private readonly IContentTypeService _contentTypeService; + + public BlockEditorPropertyEditor(ILogger logger, Lazy propertyEditors, IDataTypeService dataTypeService, IContentTypeService contentTypeService, IBlockEditorDataHelper dataHelper) + : base(logger) + { + _dataHelper = dataHelper; + _propertyEditors = propertyEditors; + _dataTypeService = dataTypeService; + _contentTypeService = contentTypeService; + } + + // has to be lazy else circular dep in ctor + private PropertyEditorCollection PropertyEditors => _propertyEditors.Value; + + #region Value Editor + + protected override IDataValueEditor CreateValueEditor() => new BlockEditorPropertyValueEditor(Attribute, _dataHelper, PropertyEditors, _dataTypeService, _contentTypeService); + + internal class BlockEditorPropertyValueEditor : DataValueEditor, IDataValueReference + { + private readonly IBlockEditorDataHelper _dataHelper; + private readonly PropertyEditorCollection _propertyEditors; + private readonly IDataTypeService _dataTypeService; + private readonly BlockEditorValues _blockEditorValues; + + public BlockEditorPropertyValueEditor(DataEditorAttribute attribute, IBlockEditorDataHelper dataHelper, PropertyEditorCollection propertyEditors, IDataTypeService dataTypeService, IContentTypeService contentTypeService) + : base(attribute) + { + _dataHelper = dataHelper; + _propertyEditors = propertyEditors; + _dataTypeService = dataTypeService; + _blockEditorValues = new BlockEditorValues(dataHelper, contentTypeService); + Validators.Add(new BlockEditorValidator(propertyEditors, dataTypeService, _blockEditorValues)); + } + + public IEnumerable GetReferences(object value) + { + var rawJson = value == null ? string.Empty : value is string str ? str : value.ToString(); + + var result = new List(); + + foreach (var row in _blockEditorValues.GetPropertyValues(rawJson, out _)) + { + if (row.PropType == null) continue; + + var propEditor = _propertyEditors[row.PropType.PropertyEditorAlias]; + + var valueEditor = propEditor?.GetValueEditor(); + if (!(valueEditor is IDataValueReference reference)) continue; + + var val = row.JsonRowValue[row.PropKey]?.ToString(); + + var refs = reference.GetReferences(val); + + result.AddRange(refs); + } + + return result; + } + } + + internal class BlockEditorValidator : IValueValidator + { + private readonly PropertyEditorCollection _propertyEditors; + private readonly IDataTypeService _dataTypeService; + private readonly BlockEditorValues _blockEditorValues; + + public BlockEditorValidator(PropertyEditorCollection propertyEditors, IDataTypeService dataTypeService, BlockEditorValues blockEditorValues) + { + _propertyEditors = propertyEditors; + _dataTypeService = dataTypeService; + _blockEditorValues = blockEditorValues; + } + + public IEnumerable Validate(object rawValue, string valueType, object dataTypeConfiguration) + { + var validationResults = new List(); + + foreach (var row in _blockEditorValues.GetPropertyValues(rawValue, out _)) + { + if (row.PropType == null) continue; + + var config = _dataTypeService.GetDataType(row.PropType.DataTypeId).Configuration; + var propertyEditor = _propertyEditors[row.PropType.PropertyEditorAlias]; + if (propertyEditor == null) continue; + + foreach (var validator in propertyEditor.GetValueEditor().Validators) + { + foreach (var result in validator.Validate(row.JsonRowValue[row.PropKey], propertyEditor.GetValueEditor().ValueType, config)) + { + result.ErrorMessage = "Item " + (row.RowIndex + 1) + " '" + row.PropType.Name + "' " + result.ErrorMessage; + validationResults.Add(result); + } + } + + // Check mandatory + if (row.PropType.Mandatory) + { + if (row.JsonRowValue[row.PropKey] == null) + { + var message = string.IsNullOrWhiteSpace(row.PropType.MandatoryMessage) + ? $"'{row.PropType.Name}' cannot be null" + : row.PropType.MandatoryMessage; + validationResults.Add(new ValidationResult($"Item {(row.RowIndex + 1)}: {message}", new[] { row.PropKey })); + } + else if (row.JsonRowValue[row.PropKey].ToString().IsNullOrWhiteSpace() || (row.JsonRowValue[row.PropKey].Type == JTokenType.Array && !row.JsonRowValue[row.PropKey].HasValues)) + { + var message = string.IsNullOrWhiteSpace(row.PropType.MandatoryMessage) + ? $"'{row.PropType.Name}' cannot be empty" + : row.PropType.MandatoryMessage; + validationResults.Add(new ValidationResult($"Item {(row.RowIndex + 1)}: {message}", new[] { row.PropKey })); + } + } + + // Check regex + if (!row.PropType.ValidationRegExp.IsNullOrWhiteSpace() + && row.JsonRowValue[row.PropKey] != null && !row.JsonRowValue[row.PropKey].ToString().IsNullOrWhiteSpace()) + { + var regex = new Regex(row.PropType.ValidationRegExp); + if (!regex.IsMatch(row.JsonRowValue[row.PropKey].ToString())) + { + var message = string.IsNullOrWhiteSpace(row.PropType.ValidationRegExpMessage) + ? $"'{row.PropType.Name}' is invalid, it does not match the correct pattern" + : row.PropType.ValidationRegExpMessage; + validationResults.Add(new ValidationResult($"Item {(row.RowIndex + 1)}: {message}", new[] { row.PropKey })); + } + } + } + + return validationResults; + } + } + + internal class BlockEditorValues + { + private readonly IBlockEditorDataHelper _dataHelper; + private readonly Lazy> _contentTypes; + + public BlockEditorValues(IBlockEditorDataHelper dataHelper, IContentTypeService contentTypeService) + { + _dataHelper = dataHelper; + _contentTypes = new Lazy>(() => contentTypeService.GetAll().ToDictionary(c => c.Key)); + } + + private IContentType GetElementType(JObject item) + { + Guid contentTypeKey = item[ContentTypeKeyPropertyKey]?.ToObject() ?? Guid.Empty; + _contentTypes.Value.TryGetValue(contentTypeKey, out var contentType); + return contentType; + } + + public IEnumerable GetPropertyValues(object propertyValue, out List deserialized) + { + var rowValues = new List(); + + deserialized = null; + + if (propertyValue == null || string.IsNullOrWhiteSpace(propertyValue.ToString())) + return Enumerable.Empty(); + + var data = JsonConvert.DeserializeObject(propertyValue.ToString()); + if (data?.Layout == null || data.Data == null || data.Data.Count == 0) + return Enumerable.Empty(); + + var blockRefs = _dataHelper.GetBlockReferences(data.Layout); + if (blockRefs == null) + return Enumerable.Empty(); + + var dataMap = new Dictionary(data.Data.Count); + data.Data.ForEach(d => + { + var udiObj = d?[UdiPropertyKey]; + if (Udi.TryParse(udiObj == null || udiObj.Type != JTokenType.String ? null : udiObj.ToString(), out var udi)) + dataMap[udi] = d; + }); + + deserialized = blockRefs.Select(r => dataMap.TryGetValue(r.Udi, out var block) ? block : null).Where(r => r != null).ToList(); + if (deserialized == null || deserialized.Count == 0) + return Enumerable.Empty(); + + var index = 0; + + foreach (var o in deserialized) + { + var propValues = o; + + var contentType = GetElementType(propValues); + if (contentType == null) + continue; + + var propertyTypes = contentType.CompositionPropertyTypes.ToDictionary(x => x.Alias, x => x); + var propAliases = propValues.Properties().Select(x => x.Name); + foreach (var propAlias in propAliases) + { + propertyTypes.TryGetValue(propAlias, out var propType); + rowValues.Add(new RowValue(propAlias, propType, propValues, index)); + } + index++; + } + + return rowValues; + } + + internal class RowValue + { + public RowValue(string propKey, PropertyType propType, JObject propValues, int index) + { + PropKey = propKey ?? throw new ArgumentNullException(nameof(propKey)); + PropType = propType; + JsonRowValue = propValues ?? throw new ArgumentNullException(nameof(propValues)); + RowIndex = index; + } + + /// + /// The current property key being iterated for the row value + /// + public string PropKey { get; } + + /// + /// The of the value (if any), this may be null + /// + public PropertyType PropType { get; } + + /// + /// The json values for the current row + /// + public JObject JsonRowValue { get; } + + /// + /// The Nested Content row index + /// + public int RowIndex { get; } + } + + private class BlockEditorData + { + [JsonProperty("layout")] + public JObject Layout { get; set; } + + [JsonProperty("data")] + public List Data { get; set; } + } + } + #endregion + + private static bool IsSystemPropertyKey(string propertyKey) => ContentTypeKeyPropertyKey == propertyKey || UdiPropertyKey == propertyKey; + } +} diff --git a/src/Umbraco.Web/PropertyEditors/BlockListConfiguration.cs b/src/Umbraco.Web/PropertyEditors/BlockListConfiguration.cs new file mode 100644 index 0000000000..4ff17c31f3 --- /dev/null +++ b/src/Umbraco.Web/PropertyEditors/BlockListConfiguration.cs @@ -0,0 +1,72 @@ +using Newtonsoft.Json; +using System; +using Umbraco.Core.PropertyEditors; + +namespace Umbraco.Web.PropertyEditors +{ + + /// + /// The configuration object for the Block List editor + /// + public class BlockListConfiguration + { + + + [ConfigurationField("blocks", "Available Blocks", "views/propertyeditors/blocklist/prevalue/blocklist.blockconfiguration.html", Description = "Define the available blocks.")] + public BlockConfiguration[] Blocks { get; set; } + + public class BlockConfiguration + { + + [JsonProperty("backgroundColor")] + public string BackgroundColor { get; set; } + + [JsonProperty("iconColor")] + public string IconColor { get; set; } + + [JsonProperty("thumbnail")] + public string Thumbnail { get; set; } + + [JsonProperty("contentTypeKey")] + public Guid Key { get; set; } + + [JsonProperty("settingsElementTypeKey")] + public string SettingsElementTypeKey { get; set; } + + [JsonProperty("view")] + public string View { get; set; } + + [JsonProperty("stylesheet")] + public string Stylesheet { get; set; } + + [JsonProperty("label")] + public string Label { get; set; } + + [JsonProperty("editorSize")] + public string EditorSize { get; set; } + } + + [ConfigurationField("validationLimit", "Amount", "numberrange", Description = "Set a required range of blocks")] + public NumberRange ValidationLimit { get; set; } = new NumberRange(); + + public class NumberRange + { + [JsonProperty("min")] + public int? Min { get; set; } + + [JsonProperty("max")] + public int? Max { get; set; } + } + + [ConfigurationField("useLiveEditing", "Live editing mode", "boolean", Description = "Live editing in editor overlays for live updated custom views.")] + public bool UseLiveEditing { get; set; } + + [ConfigurationField("useInlineEditingAsDefault", "Inline editing mode", "boolean", Description = "Use the inline editor as the default block view.")] + public bool UseInlineEditingAsDefault { get; set; } + + [ConfigurationField("maxPropertyWidth", "Property editor width", "textstring", Description = "optional css overwrite, example: 800px or 100%")] + public string MaxPropertyWidth { get; set; } + + + } +} diff --git a/src/Umbraco.Web/PropertyEditors/BlockListConfigurationEditor.cs b/src/Umbraco.Web/PropertyEditors/BlockListConfigurationEditor.cs new file mode 100644 index 0000000000..3a4e3eae9b --- /dev/null +++ b/src/Umbraco.Web/PropertyEditors/BlockListConfigurationEditor.cs @@ -0,0 +1,20 @@ +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using System.Linq; +using System.Text.RegularExpressions; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using Umbraco.Core; +using Umbraco.Core.PropertyEditors; + +namespace Umbraco.Web.PropertyEditors +{ + internal class BlockListConfigurationEditor : ConfigurationEditor + { + public BlockListConfigurationEditor() + { + + } + + } +} diff --git a/src/Umbraco.Web/PropertyEditors/BlockListPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/BlockListPropertyEditor.cs new file mode 100644 index 0000000000..3f8288b0ac --- /dev/null +++ b/src/Umbraco.Web/PropertyEditors/BlockListPropertyEditor.cs @@ -0,0 +1,66 @@ +using Newtonsoft.Json.Linq; +using System; +using System.Collections.Generic; +using System.Linq; +using Umbraco.Core; +using Umbraco.Core.Logging; +using Umbraco.Core.Models.Blocks; +using Umbraco.Core.PropertyEditors; +using Umbraco.Core.Services; + +namespace Umbraco.Web.PropertyEditors +{ + /// + /// Represents a block list property editor. + /// + [DataEditor( + Constants.PropertyEditors.Aliases.BlockList, + "Block List", + "blocklist", + ValueType = ValueTypes.Json, + Group = Constants.PropertyEditors.Groups.Lists, + Icon = "icon-thumbnail-list")] + public class BlockListPropertyEditor : BlockEditorPropertyEditor + { + public BlockListPropertyEditor(ILogger logger, Lazy propertyEditors, IDataTypeService dataTypeService, IContentTypeService contentTypeService) + : base(logger, propertyEditors, dataTypeService, contentTypeService, new DataHelper()) + { } + + #region Pre Value Editor + + protected override IConfigurationEditor CreateConfigurationEditor() => new BlockListConfigurationEditor(); + + #endregion + + #region IBlockEditorDataHelper + + private class DataHelper : IBlockEditorDataHelper + { + public IEnumerable GetBlockReferences(JObject layout) + { + if (!(layout?[Constants.PropertyEditors.Aliases.BlockList] is JArray blLayouts)) + yield break; + + foreach (var blLayout in blLayouts) + { + if (!(blLayout is JObject blockRef) || !(blockRef[UdiPropertyKey] is JValue udiRef) || udiRef.Type != JTokenType.String || !Udi.TryParse(udiRef.ToString(), out var udi)) continue; + yield return new SimpleRef(udi); + } + } + + public bool IsEditorSpecificPropertyKey(string propertyKey) => false; + + private class SimpleRef : IBlockReference + { + public SimpleRef(Udi udi) + { + Udi = udi; + } + + public Udi Udi { get; } + } + } + + #endregion + } +} diff --git a/src/Umbraco.Web/PropertyEditors/NestedContentConfiguration.cs b/src/Umbraco.Web/PropertyEditors/NestedContentConfiguration.cs index 0f53207462..89190883c8 100644 --- a/src/Umbraco.Web/PropertyEditors/NestedContentConfiguration.cs +++ b/src/Umbraco.Web/PropertyEditors/NestedContentConfiguration.cs @@ -3,6 +3,7 @@ using Umbraco.Core.PropertyEditors; namespace Umbraco.Web.PropertyEditors { + /// /// Represents the configuration for the nested content value editor. /// diff --git a/src/Umbraco.Web/PropertyEditors/ValueConverters/BlockEditorConverter.cs b/src/Umbraco.Web/PropertyEditors/ValueConverters/BlockEditorConverter.cs new file mode 100644 index 0000000000..917462e2f2 --- /dev/null +++ b/src/Umbraco.Web/PropertyEditors/ValueConverters/BlockEditorConverter.cs @@ -0,0 +1,59 @@ +using Newtonsoft.Json.Linq; +using System; +using System.Collections.Generic; +using Umbraco.Core; +using Umbraco.Core.Models.PublishedContent; +using Umbraco.Core.PropertyEditors; +using Umbraco.Web.PublishedCache; + +namespace Umbraco.Web.PropertyEditors.ValueConverters +{ + public sealed class BlockEditorConverter + { + private readonly IPublishedSnapshotAccessor _publishedSnapshotAccessor; + private readonly IPublishedModelFactory _publishedModelFactory; + + public BlockEditorConverter(IPublishedSnapshotAccessor publishedSnapshotAccessor, IPublishedModelFactory publishedModelFactory) + { + _publishedSnapshotAccessor = publishedSnapshotAccessor; + _publishedModelFactory = publishedModelFactory; + } + + public IPublishedElement ConvertToElement( + JObject sourceObject, string contentTypeKeyPropertyKey, + PropertyCacheLevel referenceCacheLevel, bool preview) + { + var elementTypeKey = sourceObject[contentTypeKeyPropertyKey]?.ToObject(); + if (!elementTypeKey.HasValue) + return null; + + // hack! we need to cast, we have n ochoice beacuse we cannot make breaking changes. + var publishedContentCache = _publishedSnapshotAccessor.PublishedSnapshot.Content as IPublishedContentCache2; + if (publishedContentCache == null) + throw new InvalidOperationException("The published content cache is not " + typeof(IPublishedContentCache2)); + + // only convert element types - content types will cause an exception when PublishedModelFactory creates the model + var publishedContentType = publishedContentCache.GetContentType(elementTypeKey.Value); + if (publishedContentType == null || publishedContentType.IsElement == false) + return null; + + var propertyValues = sourceObject.ToObject>(); + + if (!propertyValues.TryGetValue("key", out var keyo) || !Guid.TryParse(keyo.ToString(), out var key)) + { + if (propertyValues.TryGetValue("udi", out var udio) && udio is string udis && GuidUdi.TryParse(udis, out var udi)) + { + key = udi.Guid; + } + else + { + key = Guid.Empty; + } + } + + IPublishedElement element = new PublishedElement(publishedContentType, key, propertyValues, preview, referenceCacheLevel, _publishedSnapshotAccessor); + element = _publishedModelFactory.CreateModel(element); + return element; + } + } +} diff --git a/src/Umbraco.Web/PropertyEditors/ValueConverters/BlockListPropertyValueConverter.cs b/src/Umbraco.Web/PropertyEditors/ValueConverters/BlockListPropertyValueConverter.cs new file mode 100644 index 0000000000..a833efa2e5 --- /dev/null +++ b/src/Umbraco.Web/PropertyEditors/ValueConverters/BlockListPropertyValueConverter.cs @@ -0,0 +1,120 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using System; +using System.Collections.Generic; +using System.Linq; +using Umbraco.Core; +using Umbraco.Core.Logging; +using Umbraco.Core.Models.Blocks; +using Umbraco.Core.Models.PublishedContent; +using Umbraco.Core.PropertyEditors; +using Umbraco.Core.PropertyEditors.ValueConverters; + +namespace Umbraco.Web.PropertyEditors.ValueConverters +{ + + [DefaultPropertyValueConverter(typeof(JsonValueConverter))] + public class BlockListPropertyValueConverter : PropertyValueConverterBase + { + private readonly IProfilingLogger _proflog; + private readonly BlockEditorConverter _blockConverter; + + public BlockListPropertyValueConverter(IProfilingLogger proflog, BlockEditorConverter blockConverter) + { + _proflog = proflog; + _blockConverter = blockConverter; + } + + /// + public override bool IsConverter(IPublishedPropertyType propertyType) + => propertyType.EditorAlias.InvariantEquals(Constants.PropertyEditors.Aliases.BlockList); + + /// + public override Type GetPropertyValueType(IPublishedPropertyType propertyType) => typeof(BlockListModel); + + /// + public override PropertyCacheLevel GetPropertyCacheLevel(IPublishedPropertyType propertyType) + => PropertyCacheLevel.Element; + + /// + public override object ConvertSourceToIntermediate(IPublishedElement owner, IPublishedPropertyType propertyType, object source, bool preview) + { + return source?.ToString(); + } + + /// + public override object ConvertIntermediateToObject(IPublishedElement owner, IPublishedPropertyType propertyType, PropertyCacheLevel referenceCacheLevel, object inter, bool preview) + { + // NOTE: The intermediate object is just a json string, we don't actually convert from source -> intermediate since source is always just a json string + + using (_proflog.DebugDuration($"ConvertPropertyToBlockList ({propertyType.DataType.Id})")) + { + var configuration = propertyType.DataType.ConfigurationAs(); + var contentTypes = configuration.Blocks; + var contentTypeMap = contentTypes.ToDictionary(x => x.Key); + + var elements = new Dictionary(); + + var layout = new List(); + var model = new BlockListModel(elements.Values, layout); + + var value = (string)inter; + if (string.IsNullOrWhiteSpace(value)) return model; + + var objects = JsonConvert.DeserializeObject(value); + if (objects.Count == 0) return model; + + var jsonLayout = objects["layout"] as JObject; + if (jsonLayout == null) return model; + + var jsonData = objects["data"] as JArray; + if (jsonData == null) return model; + + var blockListLayouts = jsonLayout[Constants.PropertyEditors.Aliases.BlockList] as JArray; + if (blockListLayouts == null) return model; + + // parse the data elements + foreach (var data in jsonData.Cast()) + { + var element = _blockConverter.ConvertToElement(data, BlockEditorPropertyEditor.ContentTypeKeyPropertyKey, referenceCacheLevel, preview); + if (element == null) continue; + elements[element.Key] = element; + } + + // if there's no elements just return since if there's no data it doesn't matter what is stored in layout + if (elements.Count == 0) return model; + + foreach (var blockListLayout in blockListLayouts) + { + var settingsJson = blockListLayout["settings"] as JObject; + // the result of this can be null, that's ok + var element = settingsJson != null ? _blockConverter.ConvertToElement(settingsJson, BlockEditorPropertyEditor.ContentTypeKeyPropertyKey, referenceCacheLevel, preview) : null; + + if (!Udi.TryParse(blockListLayout.Value("udi"), out var udi) || !(udi is GuidUdi guidUdi)) + continue; + + // get the data reference + if (!elements.TryGetValue(guidUdi.Guid, out var data)) + continue; + + if (!data.ContentType.TryGetKey(out var contentTypeKey)) + throw new InvalidOperationException("The content type was not of type " + typeof(IPublishedContentType2)); + + if (!contentTypeMap.TryGetValue(contentTypeKey, out var blockConfig)) + continue; + + // this can happen if they have a settings type, save content, remove the settings type, and display the front-end page before saving the content again + if (element != null && string.IsNullOrWhiteSpace(blockConfig.SettingsElementTypeKey)) + element = null; + + var layoutRef = new BlockListLayoutReference(udi, data, element); + layout.Add(layoutRef); + } + + return model; + } + } + + + } +} diff --git a/src/Umbraco.Web/PropertyEditors/ValueConverters/NestedContentManyValueConverter.cs b/src/Umbraco.Web/PropertyEditors/ValueConverters/NestedContentManyValueConverter.cs index 4a25049695..11b924552e 100644 --- a/src/Umbraco.Web/PropertyEditors/ValueConverters/NestedContentManyValueConverter.cs +++ b/src/Umbraco.Web/PropertyEditors/ValueConverters/NestedContentManyValueConverter.cs @@ -38,8 +38,8 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters { var contentTypes = propertyType.DataType.ConfigurationAs().ContentTypes; return contentTypes.Length == 1 - ? typeof (IEnumerable<>).MakeGenericType(ModelType.For(contentTypes[0].Alias)) - : typeof (IEnumerable); + ? typeof(IEnumerable<>).MakeGenericType(ModelType.For(contentTypes[0].Alias)) + : typeof(IEnumerable); } /// diff --git a/src/Umbraco.Web/PropertyEditors/ValueConverters/NestedContentSingleValueConverter.cs b/src/Umbraco.Web/PropertyEditors/ValueConverters/NestedContentSingleValueConverter.cs index c9c99615f6..c9859c9770 100644 --- a/src/Umbraco.Web/PropertyEditors/ValueConverters/NestedContentSingleValueConverter.cs +++ b/src/Umbraco.Web/PropertyEditors/ValueConverters/NestedContentSingleValueConverter.cs @@ -56,7 +56,7 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters { using (_proflog.DebugDuration($"ConvertPropertyToNestedContent ({propertyType.DataType.Id})")) { - var value = (string) inter; + var value = (string)inter; if (string.IsNullOrWhiteSpace(value)) return null; var objects = JsonConvert.DeserializeObject>(value); diff --git a/src/Umbraco.Web/PublishedCache/IPublishedCache.cs b/src/Umbraco.Web/PublishedCache/IPublishedCache.cs index 0370088f77..4760082908 100644 --- a/src/Umbraco.Web/PublishedCache/IPublishedCache.cs +++ b/src/Umbraco.Web/PublishedCache/IPublishedCache.cs @@ -7,6 +7,16 @@ using Umbraco.Core.Xml; namespace Umbraco.Web.PublishedCache { + public interface IPublishedCache2 : IPublishedCache + { + /// + /// Gets a content type identified by its alias. + /// + /// The content type key. + /// The content type, or null. + IPublishedContentType GetContentType(Guid key); + } + /// /// Provides access to cached contents. /// diff --git a/src/Umbraco.Web/PublishedCache/IPublishedContentCache.cs b/src/Umbraco.Web/PublishedCache/IPublishedContentCache.cs index b4a6e3d1e0..8175285c3a 100644 --- a/src/Umbraco.Web/PublishedCache/IPublishedContentCache.cs +++ b/src/Umbraco.Web/PublishedCache/IPublishedContentCache.cs @@ -4,6 +4,11 @@ using Umbraco.Core.Models.PublishedContent; namespace Umbraco.Web.PublishedCache { + public interface IPublishedContentCache2 : IPublishedContentCache, IPublishedCache2 + { + // NOTE: this is here purely to avoid API breaking changes + } + public interface IPublishedContentCache : IPublishedCache { /// diff --git a/src/Umbraco.Web/PublishedCache/IPublishedMediaCache.cs b/src/Umbraco.Web/PublishedCache/IPublishedMediaCache.cs index 0b461882b7..702b4fe49d 100644 --- a/src/Umbraco.Web/PublishedCache/IPublishedMediaCache.cs +++ b/src/Umbraco.Web/PublishedCache/IPublishedMediaCache.cs @@ -1,5 +1,10 @@ namespace Umbraco.Web.PublishedCache { + public interface IPublishedMediaCache2 : IPublishedMediaCache, IPublishedCache2 + { + // NOTE: this is here purely to avoid API breaking changes + } + public interface IPublishedMediaCache : IPublishedCache { } } diff --git a/src/Umbraco.Web/PublishedCache/NuCache/ContentCache.cs b/src/Umbraco.Web/PublishedCache/NuCache/ContentCache.cs index 24c6a7018b..8e6e517aea 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/ContentCache.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/ContentCache.cs @@ -13,7 +13,7 @@ using Umbraco.Web.PublishedCache.NuCache.Navigable; namespace Umbraco.Web.PublishedCache.NuCache { - internal class ContentCache : PublishedCacheBase, IPublishedContentCache, INavigableData, IDisposable + internal class ContentCache : PublishedCacheBase, IPublishedContentCache2, INavigableData, IDisposable { private readonly ContentStore.Snapshot _snapshot; private readonly IAppCache _snapshotCache; @@ -384,15 +384,11 @@ namespace Umbraco.Web.PublishedCache.NuCache #region Content types - public override IPublishedContentType GetContentType(int id) - { - return _snapshot.GetContentType(id); - } + public override IPublishedContentType GetContentType(int id) => _snapshot.GetContentType(id); - public override IPublishedContentType GetContentType(string alias) - { - return _snapshot.GetContentType(alias); - } + public override IPublishedContentType GetContentType(string alias) => _snapshot.GetContentType(alias); + + public override IPublishedContentType GetContentType(Guid key) => _snapshot.GetContentType(key); #endregion diff --git a/src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs b/src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs index a3f918c92c..b39b38ca32 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs @@ -37,9 +37,14 @@ namespace Umbraco.Web.PublishedCache.NuCache private readonly IVariationContextAccessor _variationContextAccessor; private readonly ConcurrentDictionary> _contentNodes; private LinkedNode _root; - private readonly ConcurrentDictionary> _contentTypesById; + + // We must keep separate dictionaries for by id and by alias because we track these in snapshot/layers + // and it is possible that the alias of a content type can be different for the same id in another layer + // whereas the GUID -> INT cross reference can never be different + private readonly ConcurrentDictionary> _contentTypesById; private readonly ConcurrentDictionary> _contentTypesByAlias; - private readonly ConcurrentDictionary _xmap; + private readonly ConcurrentDictionary _contentTypeKeyToIdMap; + private readonly ConcurrentDictionary _contentKeyToIdMap; private readonly ILogger _logger; private BPlusTree _localDb; @@ -73,7 +78,8 @@ namespace Umbraco.Web.PublishedCache.NuCache _root = new LinkedNode(new ContentNode(), 0); _contentTypesById = new ConcurrentDictionary>(); _contentTypesByAlias = new ConcurrentDictionary>(StringComparer.InvariantCultureIgnoreCase); - _xmap = new ConcurrentDictionary(); + _contentTypeKeyToIdMap = new ConcurrentDictionary(); + _contentKeyToIdMap = new ConcurrentDictionary(); _genObjs = new ConcurrentQueue(); _genObj = null; // no initial gen exists @@ -136,7 +142,7 @@ namespace Umbraco.Web.PublishedCache.NuCache Monitor.Enter(_wlocko, ref lockInfo.Taken); - lock(_rlocko) + lock (_rlocko) { // see SnapDictionary try { } @@ -152,7 +158,7 @@ namespace Umbraco.Web.PublishedCache.NuCache _nextGen = true; } } - } + } } private void Release(WriteLockInfo lockInfo, bool commit = true) @@ -291,8 +297,7 @@ namespace Umbraco.Web.PublishedCache.NuCache foreach (var type in types) { - SetValueLocked(_contentTypesById, type.Id, type); - SetValueLocked(_contentTypesByAlias, type.Alias, type); + SetContentTypeLocked(type); } } @@ -318,8 +323,7 @@ namespace Umbraco.Web.PublishedCache.NuCache foreach (var type in index.Values) { - SetValueLocked(_contentTypesById, type.Id, type); - SetValueLocked(_contentTypesByAlias, type.Alias, type); + SetContentTypeLocked(type); } foreach (var link in _contentNodes.Values) @@ -354,8 +358,7 @@ namespace Umbraco.Web.PublishedCache.NuCache // set all new content types foreach (var type in types) { - SetValueLocked(_contentTypesById, type.Id, type); - SetValueLocked(_contentTypesByAlias, type.Alias, type); + SetContentTypeLocked(type); } // beware! at that point the cache is inconsistent, @@ -419,8 +422,7 @@ namespace Umbraco.Web.PublishedCache.NuCache // perform update of refreshed content types foreach (var type in refreshedTypesA) { - SetValueLocked(_contentTypesById, type.Id, type); - SetValueLocked(_contentTypesByAlias, type.Alias, type); + SetContentTypeLocked(type); } // perform update of content with refreshed content type - from the kits @@ -638,7 +640,7 @@ namespace Umbraco.Web.PublishedCache.NuCache kit.Node.PreviousSiblingContentId = existing.PreviousSiblingContentId; } - _xmap[kit.Node.Uid] = kit.Node.Id; + _contentKeyToIdMap[kit.Node.Uid] = kit.Node.Id; return true; } @@ -734,7 +736,7 @@ namespace Umbraco.Web.PublishedCache.NuCache // this node becomes the previous node previousNode = thisNode; - _xmap[kit.Node.Uid] = kit.Node.Id; + _contentKeyToIdMap[kit.Node.Uid] = kit.Node.Id; } return ok; @@ -757,7 +759,7 @@ namespace Umbraco.Web.PublishedCache.NuCache EnsureLocked(); var ok = true; - + ClearLocked(_contentNodes); ClearRootLocked(); @@ -778,7 +780,7 @@ namespace Umbraco.Web.PublishedCache.NuCache if (_localDb != null) RegisterChange(kit.Node.Id, kit); AddTreeNodeLocked(kit.Node, parent); - _xmap[kit.Node.Uid] = kit.Node.Id; + _contentKeyToIdMap[kit.Node.Uid] = kit.Node.Id; } return ok; @@ -807,7 +809,7 @@ namespace Umbraco.Web.PublishedCache.NuCache EnsureLocked(); var ok = true; - + // get existing _contentNodes.TryGetValue(rootContentId, out var link); var existing = link?.Value; @@ -833,7 +835,7 @@ namespace Umbraco.Web.PublishedCache.NuCache if (_localDb != null) RegisterChange(kit.Node.Id, kit); AddTreeNodeLocked(kit.Node, parent); - _xmap[kit.Node.Uid] = kit.Node.Id; + _contentKeyToIdMap[kit.Node.Uid] = kit.Node.Id; } return ok; @@ -885,11 +887,11 @@ namespace Umbraco.Web.PublishedCache.NuCache // This should never be null, all code that calls this method is null checking but we've seen // issues of null ref exceptions in issue reports so we'll double check here if (content == null) throw new ArgumentNullException(nameof(content)); - + SetValueLocked(_contentNodes, content.Id, null); if (_localDb != null) RegisterChange(content.Id, ContentNodeKit.Null); - _xmap.TryRemove(content.Uid, out _); + _contentKeyToIdMap.TryRemove(content.Uid, out _); var id = content.FirstChildContentId; while (id > 0) @@ -913,10 +915,10 @@ namespace Umbraco.Web.PublishedCache.NuCache { if (_contentNodes.TryGetValue(id, out var link)) { - link = GetLinkedNodeGen(link, gen); + link = GetLinkedNodeGen(link, gen); if (link != null && link.Value != null) return link; - } + } throw new PanicException($"failed to get {description} with id={id}"); } @@ -929,13 +931,13 @@ namespace Umbraco.Web.PublishedCache.NuCache { if (content.ParentContentId < 0) { - var root = GetLinkedNodeGen(_root, gen); + var root = GetLinkedNodeGen(_root, gen); return root; } if (_contentNodes.TryGetValue(content.ParentContentId, out var link)) link = GetLinkedNodeGen(link, gen); - + return link; } @@ -1154,6 +1156,15 @@ namespace Umbraco.Web.PublishedCache.NuCache } } + private void SetContentTypeLocked(IPublishedContentType type) + { + SetValueLocked(_contentTypesById, type.Id, type); + SetValueLocked(_contentTypesByAlias, type.Alias, type); + // ensure the key/id map is accurate + if (type.TryGetKey(out var key)) + _contentTypeKeyToIdMap[key] = type.Id; + } + // set a node (just the node, not the tree) private void SetValueLocked(ConcurrentDictionary> dict, TKey key, TValue value) where TValue : class @@ -1211,14 +1222,14 @@ namespace Umbraco.Web.PublishedCache.NuCache public ContentNode Get(Guid uid, long gen) { - return _xmap.TryGetValue(uid, out var id) + return _contentKeyToIdMap.TryGetValue(uid, out var id) ? GetValue(_contentNodes, id, gen) : null; } public IEnumerable GetAtRoot(long gen) { - var root = GetLinkedNodeGen(_root, gen); + var root = GetLinkedNodeGen(_root, gen); if (root == null) yield break; @@ -1274,13 +1285,20 @@ namespace Umbraco.Web.PublishedCache.NuCache return GetValue(_contentTypesByAlias, alias, gen); } + public IPublishedContentType GetContentType(Guid key, long gen) + { + if (!_contentTypeKeyToIdMap.TryGetValue(key, out var id)) + return null; + return GetContentType(id, gen); + } + #endregion #region Snapshots public Snapshot CreateSnapshot() { - lock(_rlocko) + lock (_rlocko) { // if no next generation is required, and we already have one, // use it and create a new snapshot @@ -1606,6 +1624,13 @@ namespace Umbraco.Web.PublishedCache.NuCache return _store.GetContentType(alias, _gen); } + public IPublishedContentType GetContentType(Guid key) + { + if (_gen < 0) + throw new ObjectDisposedException("snapshot" /*+ " (" + _thisCount + ")"*/); + return _store.GetContentType(key, _gen); + } + // this code is here just so you don't try to implement it // the only way we can iterate over "all" without locking the entire cache forever // is by shallow cloning the cache, which is quite expensive, so we should probably not do it, diff --git a/src/Umbraco.Web/PublishedCache/NuCache/MediaCache.cs b/src/Umbraco.Web/PublishedCache/NuCache/MediaCache.cs index 182086ed7f..a466460ede 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/MediaCache.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/MediaCache.cs @@ -11,7 +11,7 @@ using Umbraco.Web.PublishedCache.NuCache.Navigable; namespace Umbraco.Web.PublishedCache.NuCache { - internal class MediaCache : PublishedCacheBase, IPublishedMediaCache, INavigableData, IDisposable + internal class MediaCache : PublishedCacheBase, IPublishedMediaCache2, INavigableData, IDisposable { private readonly ContentStore.Snapshot _snapshot; private readonly IVariationContextAccessor _variationContextAccessor; @@ -155,15 +155,11 @@ namespace Umbraco.Web.PublishedCache.NuCache #region Content types - public override IPublishedContentType GetContentType(int id) - { - return _snapshot.GetContentType(id); - } + public override IPublishedContentType GetContentType(int id) => _snapshot.GetContentType(id); - public override IPublishedContentType GetContentType(string alias) - { - return _snapshot.GetContentType(alias); - } + public override IPublishedContentType GetContentType(string alias) => _snapshot.GetContentType(alias); + + public override IPublishedContentType GetContentType(Guid key) => _snapshot.GetContentType(key); #endregion diff --git a/src/Umbraco.Web/PublishedCache/PublishedCacheBase.cs b/src/Umbraco.Web/PublishedCache/PublishedCacheBase.cs index 1f637663e5..1b4a9bb92a 100644 --- a/src/Umbraco.Web/PublishedCache/PublishedCacheBase.cs +++ b/src/Umbraco.Web/PublishedCache/PublishedCacheBase.cs @@ -8,7 +8,7 @@ using Umbraco.Core.Xml; namespace Umbraco.Web.PublishedCache { - abstract class PublishedCacheBase : IPublishedCache + internal abstract class PublishedCacheBase : IPublishedCache2 { public bool PreviewDefault { get; } @@ -89,8 +89,8 @@ namespace Umbraco.Web.PublishedCache } public abstract IPublishedContentType GetContentType(int id); - public abstract IPublishedContentType GetContentType(string alias); + public abstract IPublishedContentType GetContentType(Guid key); public virtual IEnumerable GetByContentType(IPublishedContentType contentType) { diff --git a/src/Umbraco.Web/PublishedCache/PublishedContentTypeCache.cs b/src/Umbraco.Web/PublishedCache/PublishedContentTypeCache.cs index e453471bb8..8eb50b0588 100644 --- a/src/Umbraco.Web/PublishedCache/PublishedContentTypeCache.cs +++ b/src/Umbraco.Web/PublishedCache/PublishedContentTypeCache.cs @@ -15,8 +15,10 @@ namespace Umbraco.Web.PublishedCache /// This cache is not snapshotted, so it refreshes any time things change. public class PublishedContentTypeCache { + // NOTE: These are not concurrent dictionaries because all access is done within a lock private readonly Dictionary _typesByAlias = new Dictionary(); private readonly Dictionary _typesById = new Dictionary(); + private readonly Dictionary _keyToIdMap = new Dictionary(); private readonly IContentTypeService _contentTypeService; private readonly IMediaTypeService _mediaTypeService; private readonly IMemberTypeService _memberTypeService; @@ -130,6 +132,42 @@ namespace Umbraco.Web.PublishedCache } } + /// + /// Gets a published content type. + /// + /// An item type. + /// An key. + /// The published content type corresponding to the item key. + public IPublishedContentType Get(PublishedItemType itemType, Guid key) + { + try + { + _lock.EnterUpgradeableReadLock(); + + if (_keyToIdMap.TryGetValue(key, out var id)) + return Get(itemType, id); + + var type = CreatePublishedContentType(itemType, key); + + try + { + _lock.EnterWriteLock(); + _keyToIdMap[key] = type.Id; + return _typesByAlias[GetAliasKey(type)] = _typesById[type.Id] = type; + } + finally + { + if (_lock.IsWriteLockHeld) + _lock.ExitWriteLock(); + } + } + finally + { + if (_lock.IsUpgradeableReadLockHeld) + _lock.ExitUpgradeableReadLock(); + } + } + /// /// Gets a published content type. /// @@ -152,7 +190,8 @@ namespace Umbraco.Web.PublishedCache try { _lock.EnterWriteLock(); - + if (type.TryGetKey(out var key)) + _keyToIdMap[key] = type.Id; return _typesByAlias[aliasKey] = _typesById[type.Id] = type; } finally @@ -188,7 +227,8 @@ namespace Umbraco.Web.PublishedCache try { _lock.EnterWriteLock(); - + if (type.TryGetKey(out var key)) + _keyToIdMap[key] = type.Id; return _typesByAlias[GetAliasKey(type)] = _typesById[type.Id] = type; } finally @@ -204,27 +244,32 @@ namespace Umbraco.Web.PublishedCache } } + private IPublishedContentType CreatePublishedContentType(PublishedItemType itemType, Guid key) + { + IContentTypeComposition contentType = itemType switch + { + PublishedItemType.Content => _contentTypeService.Get(key), + PublishedItemType.Media => _mediaTypeService.Get(key), + PublishedItemType.Member => _memberTypeService.Get(key), + _ => throw new ArgumentOutOfRangeException(nameof(itemType)), + }; + if (contentType == null) + throw new Exception($"ContentTypeService failed to find a {itemType.ToString().ToLower()} type with key \"{key}\"."); + + return _publishedContentTypeFactory.CreateContentType(contentType); + } + private IPublishedContentType CreatePublishedContentType(PublishedItemType itemType, string alias) { if (GetPublishedContentTypeByAlias != null) return GetPublishedContentTypeByAlias(alias); - - IContentTypeComposition contentType; - switch (itemType) + IContentTypeComposition contentType = itemType switch { - case PublishedItemType.Content: - contentType = _contentTypeService.Get(alias); - break; - case PublishedItemType.Media: - contentType = _mediaTypeService.Get(alias); - break; - case PublishedItemType.Member: - contentType = _memberTypeService.Get(alias); - break; - default: - throw new ArgumentOutOfRangeException(nameof(itemType)); - } - + PublishedItemType.Content => _contentTypeService.Get(alias), + PublishedItemType.Media => _mediaTypeService.Get(alias), + PublishedItemType.Member => _memberTypeService.Get(alias), + _ => throw new ArgumentOutOfRangeException(nameof(itemType)), + }; if (contentType == null) throw new Exception($"ContentTypeService failed to find a {itemType.ToString().ToLower()} type with alias \"{alias}\"."); @@ -235,23 +280,13 @@ namespace Umbraco.Web.PublishedCache { if (GetPublishedContentTypeById != null) return GetPublishedContentTypeById(id); - - IContentTypeComposition contentType; - switch (itemType) + IContentTypeComposition contentType = itemType switch { - case PublishedItemType.Content: - contentType = _contentTypeService.Get(id); - break; - case PublishedItemType.Media: - contentType = _mediaTypeService.Get(id); - break; - case PublishedItemType.Member: - contentType = _memberTypeService.Get(id); - break; - default: - throw new ArgumentOutOfRangeException(nameof(itemType)); - } - + PublishedItemType.Content => _contentTypeService.Get(id), + PublishedItemType.Media => _mediaTypeService.Get(id), + PublishedItemType.Member => _memberTypeService.Get(id), + _ => throw new ArgumentOutOfRangeException(nameof(itemType)), + }; if (contentType == null) throw new Exception($"ContentTypeService failed to find a {itemType.ToString().ToLower()} type with id {id}."); @@ -259,6 +294,7 @@ namespace Umbraco.Web.PublishedCache } // for unit tests - changing the callback must reset the cache obviously + // TODO: Why does this even exist? For testing you'd pass in a mocked service to get by id private Func _getPublishedContentTypeByAlias; internal Func GetPublishedContentTypeByAlias { @@ -282,6 +318,7 @@ namespace Umbraco.Web.PublishedCache } // for unit tests - changing the callback must reset the cache obviously + // TODO: Why does this even exist? For testing you'd pass in a mocked service to get by id private Func _getPublishedContentTypeById; internal Func GetPublishedContentTypeById { diff --git a/src/Umbraco.Web/Runtime/WebInitialComposer.cs b/src/Umbraco.Web/Runtime/WebInitialComposer.cs index 60521e6d90..135d54560b 100644 --- a/src/Umbraco.Web/Runtime/WebInitialComposer.cs +++ b/src/Umbraco.Web/Runtime/WebInitialComposer.cs @@ -110,6 +110,7 @@ namespace Umbraco.Web.Runtime composition.RegisterUnique(); composition.RegisterUnique(); composition.RegisterUnique(); + composition.RegisterUnique(); // register the umbraco helper - this is Transient! very important! // also, if not level.Run, we cannot really use the helper (during upgrade...) diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj old mode 100755 new mode 100644 index b1cbff0fef..24a2c98fc8 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -121,6 +121,7 @@ + @@ -153,6 +154,7 @@ + @@ -242,9 +244,15 @@ + + + + + + diff --git a/src/umbraco.sln b/src/umbraco.sln index 63fb856b5d..78ff0ef12d 100644 --- a/src/umbraco.sln +++ b/src/umbraco.sln @@ -58,8 +58,24 @@ Project("{E24C65DC-7377-472B-9ABA-BC803B73C61A}") = "Umbraco.Web.UI.Client", "ht StartServerOnDebug = "false" EndProjectSection EndProject -Project("{E24C65DC-7377-472B-9ABA-BC803B73C61A}") = "Umbraco.Tests.AcceptanceTest\", "Umbraco.Tests.AcceptanceTest\", "{9E4C8A12-FBE0-4673-8CE2-DF99D5D57817}" +Project("{E24C65DC-7377-472B-9ABA-BC803B73C61A}") = "Umbraco.Tests.AcceptanceTest", "Umbraco.Tests.AcceptanceTest\", "{9E4C8A12-FBE0-4673-8CE2-DF99D5D57817}" ProjectSection(WebsiteProperties) = preProject + TargetFrameworkMoniker = ".NETFramework,Version%3Dv4.0" + Debug.AspNetCompiler.VirtualPath = "/localhost_62926" + Debug.AspNetCompiler.PhysicalPath = "Umbraco.Tests.AcceptanceTest\" + Debug.AspNetCompiler.TargetPath = "PrecompiledWeb\localhost_62926\" + Debug.AspNetCompiler.Updateable = "true" + Debug.AspNetCompiler.ForceOverwrite = "true" + Debug.AspNetCompiler.FixedNames = "false" + Debug.AspNetCompiler.Debug = "True" + Release.AspNetCompiler.VirtualPath = "/localhost_62926" + Release.AspNetCompiler.PhysicalPath = "Umbraco.Tests.AcceptanceTest\" + Release.AspNetCompiler.TargetPath = "PrecompiledWeb\localhost_62926\" + Release.AspNetCompiler.Updateable = "true" + Release.AspNetCompiler.ForceOverwrite = "true" + Release.AspNetCompiler.FixedNames = "false" + Release.AspNetCompiler.Debug = "False" + VWDPort = "62926" SlnRelativePath = "Umbraco.Tests.AcceptanceTest\" EndProjectSection EndProject @@ -123,6 +139,9 @@ Global {4C4C194C-B5E4-4991-8F87-4373E24CC19F}.Release|Any CPU.Build.0 = Release|Any CPU {3819A550-DCEC-4153-91B4-8BA9F7F0B9B4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {3819A550-DCEC-4153-91B4-8BA9F7F0B9B4}.Release|Any CPU.ActiveCfg = Debug|Any CPU + {9E4C8A12-FBE0-4673-8CE2-DF99D5D57817}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9E4C8A12-FBE0-4673-8CE2-DF99D5D57817}.Release|Any CPU.ActiveCfg = Debug|Any CPU + {9E4C8A12-FBE0-4673-8CE2-DF99D5D57817}.Release|Any CPU.Build.0 = Debug|Any CPU {651E1350-91B6-44B7-BD60-7207006D7003}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {651E1350-91B6-44B7-BD60-7207006D7003}.Debug|Any CPU.Build.0 = Debug|Any CPU {651E1350-91B6-44B7-BD60-7207006D7003}.Release|Any CPU.ActiveCfg = Release|Any CPU @@ -157,6 +176,7 @@ Global EndGlobalSection GlobalSection(NestedProjects) = preSolution {227C3B55-80E5-4E7E-A802-BE16C5128B9D} = {2849E9D4-3B4E-40A3-A309-F3CB4F0E125F} + {9E4C8A12-FBE0-4673-8CE2-DF99D5D57817} = {B5BD12C1-A454-435E-8A46-FF4A364C0382} {5D3B8245-ADA6-453F-A008-50ED04BFE770} = {B5BD12C1-A454-435E-8A46-FF4A364C0382} {E3F9F378-AFE1-40A5-90BD-82833375DBFE} = {227C3B55-80E5-4E7E-A802-BE16C5128B9D} {5B03EF4E-E0AC-4905-861B-8C3EC1A0D458} = {227C3B55-80E5-4E7E-A802-BE16C5128B9D} @@ -164,7 +184,6 @@ Global {3A33ADC9-C6C0-4DB1-A613-A9AF0210DF3D} = {B5BD12C1-A454-435E-8A46-FF4A364C0382} {C7311C00-2184-409B-B506-52A5FAEA8736} = {FD962632-184C-4005-A5F3-E705D92FC645} {FB5676ED-7A69-492C-B802-E7B24144C0FC} = {B5BD12C1-A454-435E-8A46-FF4A364C0382} - {9E4C8A12-FBE0-4673-8CE2-DF99D5D57817} = {B5BD12C1-A454-435E-8A46-FF4A364C0382} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {7A0F2E34-D2AF-4DAB-86A0-7D7764B3D0EC} From 38e8a93b28ac610adcef1e919ca52a19c4ea7f37 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Tue, 30 Jun 2020 13:36:17 +0200 Subject: [PATCH 276/377] parse on contentType key --- .../src/views/documenttypes/edit.controller.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Umbraco.Web.UI.Client/src/views/documenttypes/edit.controller.js b/src/Umbraco.Web.UI.Client/src/views/documenttypes/edit.controller.js index ad042842f5..8e80e89a40 100644 --- a/src/Umbraco.Web.UI.Client/src/views/documenttypes/edit.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/documenttypes/edit.controller.js @@ -359,6 +359,7 @@ if (infiniteMode && $scope.model.submit) { $scope.model.documentTypeAlias = vm.contentType.alias; + $scope.model.documentTypeKey = vm.contentType.key; $scope.model.submit($scope.model); } From 26fa38c5362c0ae9f0356f1a2a35c354d8715add Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Tue, 30 Jun 2020 15:45:20 +0200 Subject: [PATCH 277/377] use one-way binding --- .../blocklist/umb-block-list-scoped-block.component.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/umb-block-list-scoped-block.component.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/umb-block-list-scoped-block.component.js index 492423c132..8878db8ec0 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/umb-block-list-scoped-block.component.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/umb-block-list-scoped-block.component.js @@ -19,7 +19,7 @@ stylesheet: "@", view: "@", block: "=", - api: "=", + api: "<", index: "<" } } From e2b7043d3c7a89be0d4a9c096417939810f84929 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Tue, 30 Jun 2020 15:59:06 +0200 Subject: [PATCH 278/377] check for $block.data, to verify that the blockObject is not in a destroyed state. --- .../blocklist/umbBlockListPropertyEditor.component.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) 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 880334a7b3..9ed28cdee5 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 @@ -135,7 +135,8 @@ // Append the blockObjects to our layout. vm.layout.forEach(entry => { - if (entry.$block === undefined || entry.$block === null) { + // $block must have the data property to be a valid BlockObject, if not its concidered as a destroyed blockObject. + if (entry.$block === undefined || entry.$block === null || entry.$block.data === undefined) { var block = getBlockObject(entry); // If this entry was not supported by our property-editor it would return 'null'. From 2fc6ed10fb5634626310443cea87b5c75f188dc9 Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 1 Jul 2020 12:10:12 +1000 Subject: [PATCH 279/377] Gets inherited property validation working but will switch this to server side --- src/Umbraco.Core/Umbraco.Core.csproj | 2 +- .../validation/valpropertymsg.directive.js | 5 +- .../services/servervalidationmgr.service.js | 64 +++++++++++++------ 3 files changed, 50 insertions(+), 21 deletions(-) diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index 1015124a8b..ac3f6bd464 100755 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -137,7 +137,7 @@ - + diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/validation/valpropertymsg.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/validation/valpropertymsg.directive.js index 059a4c6853..27fa67695b 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/validation/valpropertymsg.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/validation/valpropertymsg.directive.js @@ -118,7 +118,8 @@ function valPropertyMsg(serverValidationManager, localizationService) { || (formCtrl.$invalid && Utilities.isArray(formCtrl.$error.valServer))) { scope.errorMsg = ""; formCtrl.$setValidity('valPropertyMsg', true); - } else if (showValidation && scope.errorMsg === "") { + } + else if (showValidation && scope.errorMsg === "") { formCtrl.$setValidity('valPropertyMsg', false); scope.errorMsg = getErrorMsg(); } @@ -229,7 +230,7 @@ function valPropertyMsg(serverValidationManager, localizationService) { hasError = !isValid; if (hasError) { //set the error message to the server message - scope.errorMsg = propertyErrors[0].errorMsg; + scope.errorMsg = propertyErrors[0].errorMsg ? propertyErrors[0].errorMsg : labels.propertyHasErrors; //flag that the current validator is invalid formCtrl.$setValidity('valPropertyMsg', false); startWatch(); diff --git a/src/Umbraco.Web.UI.Client/src/common/services/servervalidationmgr.service.js b/src/Umbraco.Web.UI.Client/src/common/services/servervalidationmgr.service.js index a945c71dff..02bccc8dd2 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/servervalidationmgr.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/servervalidationmgr.service.js @@ -123,25 +123,52 @@ function serverValidationManager($timeout, udiService) { * Returns a dictionary of id (of the block) and it's corresponding validation ModelState * @param {any} errorMsg */ - function parseComplexEditorError(errorMsg) { + function parseComplexEditorError(errorMsg, culture, segment) { + + //normalize culture to "invariant" + if (!culture) { + culture = "invariant"; + } + //normalize segment to "null" (as a string, in this case it's used for creating a key) + if (!segment) { + segment = "null"; + } var json = JSON.parse(errorMsg); var result = {}; function extractModelState(validation) { - if (validation.$id && validation.ModelState) { - result[validation.$id] = validation.ModelState; + if (validation.$id && validation.ModelState && Object.keys(validation.ModelState).length > 0) { + result[validation.$id] = validation.ModelState; } + else { + // we'll still add the id in the dictionary with an empty result, this indicates that this element + // has nested errors but no errors itself + result[validation.$id] = {}; + } + return result[validation.$id]; } function iterateErrorBlocks(blocks) { for (var i = 0; i < blocks.length; i++) { var validation = blocks[i]; - extractModelState(validation); + var modelState = extractModelState(validation); + var hasModelState = Object.keys(modelState).length > 0; var nested = _.omit(validation, "$id", "$elementTypeAlias", "ModelState"); for (const [key, value] of Object.entries(nested)) { + if (Array.isArray(value)) { + + // The key here is the property type alias of the nested validation. + // If the extracted ModelState is empty it indicates that this element + // has nested errors but no errors itself. In that case we need to manually populate the + // ModelState properties. + if (!hasModelState) { + var propertyKey = "_Properties." + key + "." + culture + "." + segment; + modelState[propertyKey] = null; + } + iterateErrorBlocks(value); // recurse } } @@ -262,9 +289,6 @@ function serverValidationManager($timeout, udiService) { */ function addPropertyError(propertyAlias, culture, fieldName, errorMsg, segment) { - // TODO: We need to handle the errorMsg in a special way to check if this is a json structure. If it is we know we are dealing with - // a complex editor and in which case we'll need to adjust how everything works. - if (!propertyAlias) { return; } @@ -277,17 +301,22 @@ function serverValidationManager($timeout, udiService) { if (!segment) { segment = null; } + //normalize errorMsg to empty + if (!errorMsg) { + errorMsg = ""; + } // if the error message is json it's a complex editor validation response that we need to parse if (errorMsg.startsWith("[")) { - var idsToErrors = parseComplexEditorError(errorMsg); + var idsToErrors = parseComplexEditorError(errorMsg, culture, segment); for (const [key, value] of Object.entries(idsToErrors)) { - addErrorsForModelState(value, udiService.build("element", key)); + const elementUdi = udiService.build("element", key); + addErrorsForModelState(value, elementUdi); } - // TODO: Make this the generic "Property has errors" but need to find the lang key for that - errorMsg = "Hello!"; + // We need to clear the error message else it will show up as a giant json block against the property + errorMsg = ""; } //only add the item if it doesn't exist @@ -372,7 +401,7 @@ function serverValidationManager($timeout, udiService) { * This wires up all of the server validation model state so that valServer and valServerField directives work */ function addErrorsForModelState(modelState, elementUdi) { - for (var e in modelState) { + for (const [key, value] of Object.entries(modelState)) { //This is where things get interesting.... // We need to support validation for all editor types such as both the content and content type editors. @@ -403,14 +432,15 @@ function serverValidationManager($timeout, udiService) { // * it's for the mySegment segment // * it's for the myField html field (optional) - var parts = e.split("."); + var parts = key.split("."); //Check if this is for content properties - specific to content/media/member editors because those are special // user defined properties with custom controls. if (parts.length > 1 && parts[0] === "_Properties") { // create the validation key, might just be the prop alias but if it's nested will be a unique udi - var propertyAlias = createPropertyValidationKey(parts[1], elementUdi); + // like umb://element/GUID/propertyAlias + var propertyValidationKey = createPropertyValidationKey(parts[1], elementUdi); var culture = null; if (parts.length > 2) { @@ -436,16 +466,14 @@ function serverValidationManager($timeout, udiService) { } // add a generic error for the property - addPropertyError(propertyAlias, culture, htmlFieldReference, modelState[e][0], segment); - + addPropertyError(propertyValidationKey, culture, htmlFieldReference, value && Array.isArray(value) && value.length > 0 ? value[0] : null, segment); } else { //Everthing else is just a 'Field'... the field name could contain any level of 'parts' though, for example: // Groups[0].Properties[2].Alias - addFieldError(e, modelState[e][0]); + addFieldError(key, value[0]); } - } } From 7c6cf3710b12b4944d55a8504a90dd107c41d1bb Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 1 Jul 2020 12:44:34 +1000 Subject: [PATCH 280/377] Gets the inherited property validation working from the server side. --- .../Validation/ContentModelValidatorTests.cs | 53 ++++++++++----- .../services/servervalidationmgr.service.js | 35 ++-------- .../server-validation-manager.spec.js | 67 ++++++++++++++++++- .../Validation/ValidationResultConverter.cs | 7 ++ 4 files changed, 112 insertions(+), 50 deletions(-) diff --git a/src/Umbraco.Tests/Web/Validation/ContentModelValidatorTests.cs b/src/Umbraco.Tests/Web/Validation/ContentModelValidatorTests.cs index f385fbcba6..fbb2ce9c80 100644 --- a/src/Umbraco.Tests/Web/Validation/ContentModelValidatorTests.cs +++ b/src/Umbraco.Tests/Web/Validation/ContentModelValidatorTests.cs @@ -130,43 +130,60 @@ namespace Umbraco.Tests.Web.Validation addressBookElementTypeResult2.ValidationResults.Add(bookNamePropertyTypeResult2); nestedLevel1.ValidationResults.Add(addressBookElementTypeResult2); - var serialized = JsonConvert.SerializeObject(nestedLevel1, Formatting.Indented, new ValidationResultConverter()); + // books is the outer most validation result and doesn't have it's own direct ValidationResult errors + var outerError = new ComplexEditorValidationResult(); + var id4 = Guid.NewGuid(); + var addressBookCollectionElementTypeResult = new ComplexEditorElementTypeValidationResult("addressBookCollection", id4); + var booksPropertyTypeResult= new ComplexEditorPropertyTypeValidationResult("books"); + booksPropertyTypeResult.AddValidationResult(nestedLevel1); // books is the outer most validation result + addressBookCollectionElementTypeResult.ValidationResults.Add(booksPropertyTypeResult); + outerError.ValidationResults.Add(addressBookCollectionElementTypeResult); + + var serialized = JsonConvert.SerializeObject(outerError, Formatting.Indented, new ValidationResultConverter()); Console.WriteLine(serialized); var jsonError = JsonConvert.DeserializeObject(serialized); Assert.IsNotNull(jsonError.SelectToken("$[0]")); - Assert.AreEqual(id2.ToString(), jsonError.SelectToken("$[0].$id").Value()); - Assert.AreEqual("addressBook", jsonError.SelectToken("$[0].$elementTypeAlias").Value()); - Assert.IsNotNull(jsonError.SelectToken("$[0].ModelState")); - var error1 = jsonError.SelectToken("$[0].ModelState['_Properties.addresses.invariant.null.counter']") as JArray; + Assert.AreEqual(id4.ToString(), jsonError.SelectToken("$[0].$id").Value()); + Assert.AreEqual("addressBookCollection", jsonError.SelectToken("$[0].$elementTypeAlias").Value()); + Assert.AreEqual(string.Empty, jsonError.SelectToken("$[0].ModelState['_Properties.books.invariant.null'][0]").Value()); + + var error0 = jsonError.SelectToken("$[0].books") as JArray; + Assert.IsNotNull(error0); + Assert.AreEqual(id2.ToString(), error0.SelectToken("$[0].$id").Value()); + Assert.AreEqual("addressBook", error0.SelectToken("$[0].$elementTypeAlias").Value()); + Assert.IsNotNull(error0.SelectToken("$[0].ModelState")); + Assert.AreEqual(string.Empty, error0.SelectToken("$[0].ModelState['_Properties.addresses.invariant.null'][0]").Value()); + var error1 = error0.SelectToken("$[0].ModelState['_Properties.addresses.invariant.null.counter']") as JArray; Assert.IsNotNull(error1); Assert.AreEqual(1, error1.Count); - var error2 = jsonError.SelectToken("$[0].ModelState['_Properties.bookName.invariant.null.book']") as JArray; + var error2 = error0.SelectToken("$[0].ModelState['_Properties.bookName.invariant.null.book']") as JArray; Assert.IsNotNull(error2); Assert.AreEqual(1, error2.Count); - Assert.AreEqual(id3.ToString(), jsonError.SelectToken("$[1].$id").Value()); - Assert.AreEqual("addressBook", jsonError.SelectToken("$[1].$elementTypeAlias").Value()); - Assert.IsNotNull(jsonError.SelectToken("$[1].ModelState")); - var error6 = jsonError.SelectToken("$[1].ModelState['_Properties.addresses.invariant.null.counter']") as JArray; + Assert.AreEqual(id3.ToString(), error0.SelectToken("$[1].$id").Value()); + Assert.AreEqual("addressBook", error0.SelectToken("$[1].$elementTypeAlias").Value()); + Assert.IsNotNull(error0.SelectToken("$[1].ModelState")); + Assert.AreEqual(string.Empty, error0.SelectToken("$[1].ModelState['_Properties.addresses.invariant.null'][0]").Value()); + var error6 = error0.SelectToken("$[1].ModelState['_Properties.addresses.invariant.null.counter']") as JArray; Assert.IsNotNull(error6); Assert.AreEqual(1, error6.Count); - var error7 = jsonError.SelectToken("$[1].ModelState['_Properties.bookName.invariant.null']") as JArray; + var error7 = error0.SelectToken("$[1].ModelState['_Properties.bookName.invariant.null']") as JArray; Assert.IsNotNull(error7); Assert.AreEqual(1, error7.Count); - Assert.IsNotNull(jsonError.SelectToken("$[0].addresses")); - Assert.AreEqual(id1.ToString(), jsonError.SelectToken("$[0].addresses[0].$id").Value()); - Assert.AreEqual("addressInfo", jsonError.SelectToken("$[0].addresses[0].$elementTypeAlias").Value()); - Assert.IsNotNull(jsonError.SelectToken("$[0].addresses[0].ModelState")); - var error3 = jsonError.SelectToken("$[0].addresses[0].ModelState['_Properties.city.invariant.null.country']") as JArray; + Assert.IsNotNull(error0.SelectToken("$[0].addresses")); + Assert.AreEqual(id1.ToString(), error0.SelectToken("$[0].addresses[0].$id").Value()); + Assert.AreEqual("addressInfo", error0.SelectToken("$[0].addresses[0].$elementTypeAlias").Value()); + Assert.IsNotNull(error0.SelectToken("$[0].addresses[0].ModelState")); + var error3 = error0.SelectToken("$[0].addresses[0].ModelState['_Properties.city.invariant.null.country']") as JArray; Assert.IsNotNull(error3); Assert.AreEqual(1, error3.Count); - var error4 = jsonError.SelectToken("$[0].addresses[0].ModelState['_Properties.city.invariant.null.capital']") as JArray; + var error4 = error0.SelectToken("$[0].addresses[0].ModelState['_Properties.city.invariant.null.capital']") as JArray; Assert.IsNotNull(error4); Assert.AreEqual(1, error4.Count); - var error5 = jsonError.SelectToken("$[0].addresses[0].ModelState['_Properties.city.invariant.null']") as JArray; + var error5 = error0.SelectToken("$[0].addresses[0].ModelState['_Properties.city.invariant.null']") as JArray; Assert.IsNotNull(error5); Assert.AreEqual(2, error5.Count); } diff --git a/src/Umbraco.Web.UI.Client/src/common/services/servervalidationmgr.service.js b/src/Umbraco.Web.UI.Client/src/common/services/servervalidationmgr.service.js index 02bccc8dd2..5e65f49794 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/servervalidationmgr.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/servervalidationmgr.service.js @@ -123,52 +123,25 @@ function serverValidationManager($timeout, udiService) { * Returns a dictionary of id (of the block) and it's corresponding validation ModelState * @param {any} errorMsg */ - function parseComplexEditorError(errorMsg, culture, segment) { - - //normalize culture to "invariant" - if (!culture) { - culture = "invariant"; - } - //normalize segment to "null" (as a string, in this case it's used for creating a key) - if (!segment) { - segment = "null"; - } + function parseComplexEditorError(errorMsg) { var json = JSON.parse(errorMsg); var result = {}; function extractModelState(validation) { - if (validation.$id && validation.ModelState && Object.keys(validation.ModelState).length > 0) { + if (validation.$id && validation.ModelState) { result[validation.$id] = validation.ModelState; } - else { - // we'll still add the id in the dictionary with an empty result, this indicates that this element - // has nested errors but no errors itself - result[validation.$id] = {}; - } - return result[validation.$id]; } function iterateErrorBlocks(blocks) { for (var i = 0; i < blocks.length; i++) { var validation = blocks[i]; - var modelState = extractModelState(validation); - var hasModelState = Object.keys(modelState).length > 0; + extractModelState(validation); var nested = _.omit(validation, "$id", "$elementTypeAlias", "ModelState"); for (const [key, value] of Object.entries(nested)) { - if (Array.isArray(value)) { - - // The key here is the property type alias of the nested validation. - // If the extracted ModelState is empty it indicates that this element - // has nested errors but no errors itself. In that case we need to manually populate the - // ModelState properties. - if (!hasModelState) { - var propertyKey = "_Properties." + key + "." + culture + "." + segment; - modelState[propertyKey] = null; - } - iterateErrorBlocks(value); // recurse } } @@ -309,7 +282,7 @@ function serverValidationManager($timeout, udiService) { // if the error message is json it's a complex editor validation response that we need to parse if (errorMsg.startsWith("[")) { - var idsToErrors = parseComplexEditorError(errorMsg, culture, segment); + var idsToErrors = parseComplexEditorError(errorMsg); for (const [key, value] of Object.entries(idsToErrors)) { const elementUdi = udiService.build("element", key); addErrorsForModelState(value, elementUdi); diff --git a/src/Umbraco.Web.UI.Client/test/unit/common/services/server-validation-manager.spec.js b/src/Umbraco.Web.UI.Client/test/unit/common/services/server-validation-manager.spec.js index be30f2b92d..009043cae7 100644 --- a/src/Umbraco.Web.UI.Client/test/unit/common/services/server-validation-manager.spec.js +++ b/src/Umbraco.Web.UI.Client/test/unit/common/services/server-validation-manager.spec.js @@ -316,7 +316,7 @@ describe('managing complex editor validation errors', function () { - it('create json paths for complex validation error', function () { + it('create dictionary of id to ModelState', function () { //arrange var complexValidationMsg = `[ @@ -377,6 +377,71 @@ }); + it('create dictionary of id to ModelState with inherited errors', function () { + + // arrange + // this root element doesn't have it's own attached errors, instead it has model state just + // showing that it has errors within it's nested properties. that ModelState is automatically + // added on the server side. + var complexValidationMsg = `[ + { + "$elementTypeAlias": "addressBook", + "$id": "34E3A26C-103D-4A05-AB9D-7E14032309C3", + "addresses": + [ + { + "$elementTypeAlias": "addressInfo", + "$id": "FBEAEE8F-4BC9-43EE-8B81-FCA8978850F1", + "ModelState": + { + "_Properties.city.invariant.null.country": [ + "City is not in Australia" + ], + "_Properties.city.invariant.null.capital": [ + "Not a capital city" + ] + } + }, + { + "$elementTypeAlias": "addressInfo", + "$id": "7170A4DD-2441-4B1B-A8D3-437D75C4CBC9", + "ModelState": + { + "_Properties.city.invariant.null.country": [ + "City is not in Australia" + ], + "_Properties.city.invariant.null.capital": [ + "Not a capital city" + ] + } + } + ], + "ModelState": + { + "_Properties.addresses.invariant.null": [ + "" + ] + } + } +]`; + + //act + var ids = serverValidationManager.parseComplexEditorError(complexValidationMsg); + + //assert + var keys = Object.keys(ids); + + expect(keys.length).toEqual(3); + expect(keys[0]).toEqual("34E3A26C-103D-4A05-AB9D-7E14032309C3"); + var item0ModelState = ids["34E3A26C-103D-4A05-AB9D-7E14032309C3"]; + expect(Object.keys(item0ModelState).length).toEqual(1); + expect(item0ModelState["_Properties.addresses.invariant.null"].length).toEqual(1); + expect(item0ModelState["_Properties.addresses.invariant.null"][0]).toEqual(""); + expect(keys[1]).toEqual("FBEAEE8F-4BC9-43EE-8B81-FCA8978850F1"); + expect(keys[2]).toEqual("7170A4DD-2441-4B1B-A8D3-437D75C4CBC9"); + + }); + }); describe('validation error subscriptions', function() { diff --git a/src/Umbraco.Web/PropertyEditors/Validation/ValidationResultConverter.cs b/src/Umbraco.Web/PropertyEditors/Validation/ValidationResultConverter.cs index af915569c0..40fc5cc36c 100644 --- a/src/Umbraco.Web/PropertyEditors/Validation/ValidationResultConverter.cs +++ b/src/Umbraco.Web/PropertyEditors/Validation/ValidationResultConverter.cs @@ -80,6 +80,13 @@ namespace Umbraco.Web.PropertyEditors.Validation // recurse to get the validation result object var obj = JToken.FromObject(complexResult, camelCaseSerializer); joElementType.Add(propTypeResult.PropertyTypeAlias, obj); + + // For any nested property error we add the model state as empty state for that nested property + // NOTE: Instead of the empty validation message we could put in the translated + // "errors/propertyHasErrors" message, however I think that leaves for less flexibility since it could/should be + // up to the front-end validator to show whatever message it wants (if any) for an error indicating a nested property error. + // Will leave blank. + modelState.AddPropertyValidationError(new ValidationResult(string.Empty), propTypeResult.PropertyTypeAlias); } } else From 086d7b6e23077bacb41d2ddce37a9ece9041ca8b Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 1 Jul 2020 13:00:56 +1000 Subject: [PATCH 281/377] ensures the udi structure works always for the validation key --- .../components/property/umbproperty.directive.js | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/property/umbproperty.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/property/umbproperty.directive.js index b9e607ecc9..8b3f51f0f9 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/property/umbproperty.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/property/umbproperty.directive.js @@ -5,6 +5,15 @@ **/ angular.module("umbraco.directives") .directive('umbProperty', function (userService, serverValidationManager, udiService) { + + // if only a guid is passed in, we'll ensure a correct udi structure + function ensureUdi(udi) { + if (udi && !udi.startsWith("umb://")) { + udi = udiService.build("element", udi); + } + return udi; + } + return { scope: { property: "=", @@ -29,9 +38,7 @@ angular.module("umbraco.directives") }); } - if (scope.elementUdi && !scope.elementUdi.startsWith("umb://")) { - scope.elementUdi = udiService.build("element", scope.elementUdi); - } + scope.elementUdi = ensureUdi(scope.elementUdi); }, //Define a controller for this directive to expose APIs to other directives @@ -56,6 +63,7 @@ angular.module("umbraco.directives") // the elementUdi will be empty when this is not a nested property var propAlias = $scope.propertyAlias ? $scope.propertyAlias : $scope.property.alias; + $scope.elementUdi = ensureUdi($scope.elementUdi); return serverValidationManager.createPropertyValidationKey(propAlias, $scope.elementUdi); } $scope.getValidationPath = self.getValidationPath; From a8a1bb2a5515e544f9b5d688dff82066c829c154 Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 1 Jul 2020 15:01:55 +1000 Subject: [PATCH 282/377] manually merge in benj PR --- .../Migrations/Upgrade/UmbracoPlan.cs | 6 +- .../Upgrade/V_8_7_0/ConvertToElements.cs | 80 ----- .../V_8_7_0/StackedContentToBlockList.cs | 301 ------------------ src/Umbraco.Core/Umbraco.Core.csproj | 2 - 4 files changed, 1 insertion(+), 388 deletions(-) delete mode 100644 src/Umbraco.Core/Migrations/Upgrade/V_8_7_0/ConvertToElements.cs delete mode 100644 src/Umbraco.Core/Migrations/Upgrade/V_8_7_0/StackedContentToBlockList.cs diff --git a/src/Umbraco.Core/Migrations/Upgrade/UmbracoPlan.cs b/src/Umbraco.Core/Migrations/Upgrade/UmbracoPlan.cs index 9bd1972ee4..03ba58d15e 100644 --- a/src/Umbraco.Core/Migrations/Upgrade/UmbracoPlan.cs +++ b/src/Umbraco.Core/Migrations/Upgrade/UmbracoPlan.cs @@ -7,7 +7,6 @@ using Umbraco.Core.Migrations.Upgrade.V_8_0_0; using Umbraco.Core.Migrations.Upgrade.V_8_0_1; using Umbraco.Core.Migrations.Upgrade.V_8_1_0; using Umbraco.Core.Migrations.Upgrade.V_8_6_0; -using Umbraco.Core.Migrations.Upgrade.V_8_7_0; namespace Umbraco.Core.Migrations.Upgrade { @@ -194,10 +193,7 @@ namespace Umbraco.Core.Migrations.Upgrade // to 8.7.0... To("{a78e3369-8ea3-40ec-ad3f-5f76929d2b20}"); - - // to 8.7.0... - To("{DFA35FA2-BFBB-433F-84E5-BD75940CDDF6}"); - To("{DA434576-3DEF-46D7-942A-CE34D7F7FB8A}"); + //FINAL } } diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_7_0/ConvertToElements.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_7_0/ConvertToElements.cs deleted file mode 100644 index fbcd55ec92..0000000000 --- a/src/Umbraco.Core/Migrations/Upgrade/V_8_7_0/ConvertToElements.cs +++ /dev/null @@ -1,80 +0,0 @@ -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using System.Collections.Generic; -using System.Linq; -using Umbraco.Core.Persistence; -using Umbraco.Core.Persistence.Dtos; - -namespace Umbraco.Core.Migrations.Upgrade.V_8_7_0 -{ - public class ConvertToElements : MigrationBase - { - public ConvertToElements(IMigrationContext context) : base(context) - { - } - - public override void Migrate() - { - // Get all document type IDs by alias - var docTypes = Database.Fetch(); - var docTypeMap = new Dictionary(docTypes.Count); - docTypes.ForEach(d => docTypeMap[d.Alias] = d.NodeId); - - // Find all Nested Content or Block List data types - var dataTypes = GetDataTypes(Constants.PropertyEditors.Aliases.BlockList); - - // Find all document types listed in each - var elementTypeIds = dataTypes.SelectMany(d => GetDocTypeIds(d.Configuration, docTypeMap)).ToList(); - - // Find all compositions those document types use - var parentElementTypeIds = Database.Fetch(Sql() - .Select() - .From() - .WhereIn(c => c.ChildId, elementTypeIds) - ).Select(c => c.ParentId); - - elementTypeIds = elementTypeIds.Union(parentElementTypeIds).ToList(); - - // Convert all those document types to element type - // TODO: We need to wait on an update from @benjaminc to make this 'safe' - // see https://github.com/umbraco/Umbraco-CMS/pull/7910#discussion_r409927495 - foreach (var docType in docTypes) - { - if (!elementTypeIds.Contains(docType.NodeId)) continue; - - docType.IsElement = true; - Database.Update(docType); - } - } - - private List GetDataTypes(params string[] aliases) - { - var sql = Sql() - .Select() - .From() - .WhereIn(d => d.EditorAlias, aliases); - - return Database.Fetch(sql); - } - - private IEnumerable GetDocTypeIds(string configuration, Dictionary idMap) - { - if (configuration.IsNullOrWhiteSpace() || configuration[0] != '{') return Enumerable.Empty(); - - var obj = JObject.Parse(configuration); - if (obj["blocks"] is JArray blArr) - { - var arr = blArr.ToObject(); - return arr.Select(i => idMap.TryGetValue(i.Alias, out var id) ? id : 0).Where(i => i != 0); - } - - return Enumerable.Empty(); - } - - public class BlockConfiguration - { - [JsonProperty("contentTypeAlias")] - public string Alias { get; set; } - } - } -} diff --git a/src/Umbraco.Core/Migrations/Upgrade/V_8_7_0/StackedContentToBlockList.cs b/src/Umbraco.Core/Migrations/Upgrade/V_8_7_0/StackedContentToBlockList.cs deleted file mode 100644 index 0e8533a549..0000000000 --- a/src/Umbraco.Core/Migrations/Upgrade/V_8_7_0/StackedContentToBlockList.cs +++ /dev/null @@ -1,301 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using Umbraco.Core.Logging; -using Umbraco.Core.Migrations.PostMigrations; -using Umbraco.Core.Models; -using Umbraco.Core.Persistence; -using Umbraco.Core.Persistence.Dtos; -using Umbraco.Core.PropertyEditors; - -namespace Umbraco.Core.Migrations.Upgrade.V_8_7_0 -{ - public class StackedContentToBlockList : MigrationBase - { - public StackedContentToBlockList(IMigrationContext context) : base(context) - { - } - - public override void Migrate() - { - // Convert all Stacked Content properties to Block List properties, both in the data types and in the property data - var refreshCache = Migrate(GetDataTypes("Our.Umbraco.StackedContent"), GetKnownDocumentTypes()); - - // if some data types have been updated directly in the database (editing DataTypeDto and/or PropertyDataDto), - // bypassing the services, then we need to rebuild the cache entirely, including the umbracoContentNu table - if (refreshCache) - Context.AddPostMigration(); - } - - private List GetDataTypes(string alias) - { - var sql = Sql() - .Select() - .From() - .Where(d => d.EditorAlias == alias); - - return Database.Fetch(sql); - } - - private Dictionary GetKnownDocumentTypes() - { - var sql = Sql() - .Select(r => r.Select(x => x.NodeDto)) - .From() - .InnerJoin() - .On(c => c.NodeId, n => n.NodeId); - - var types = Database.Fetch(sql); - var typeMap = new Dictionary(types.Count); - types.ForEach(t => typeMap[t.NodeId] = t); - - sql = Sql() - .Select() - .From(); - var joins = Database.Fetch(sql); - // Find all relationships between types, either inherited or composited - var joinLk = joins - .Union(types - .Where(t => typeMap.ContainsKey(t.NodeDto.ParentId)) - .Select(t => new ContentType2ContentTypeDto { ChildId = t.NodeId, ParentId = t.NodeDto.ParentId })) - .ToLookup(j => j.ChildId, j => j.ParentId); - - sql = Sql() - .Select(r => r.Select(x => x.DataTypeDto)) - .From() - .InnerJoin() - .On(c => c.DataTypeId, n => n.NodeId) - .WhereIn(d => d.EditorAlias, new[] { Constants.PropertyEditors.Aliases.NestedContent, Constants.PropertyEditors.Aliases.ColorPicker }); - var props = Database.Fetch(sql); - // Get all nested content and color picker property aliases by content type ID - var propLk = props.ToLookup(p => p.ContentTypeId, p => p.Alias); - - var knownMap = new Dictionary(types.Count); - types.ForEach(t => knownMap[t.NodeDto.UniqueId] = new KnownContentType(t.Alias, t.NodeDto.UniqueId, propLk[t.NodeId].Union(joinLk[t.NodeId].SelectMany(r => propLk[r])).ToArray())); - return knownMap; - } - - private bool Migrate(IEnumerable dataTypesToMigrate, Dictionary knownDocumentTypes) - { - var refreshCache = false; - - foreach (var dataType in dataTypesToMigrate) - { - if (!dataType.Configuration.IsNullOrWhiteSpace()) - { - var config = UpdateConfiguration(dataType, knownDocumentTypes); - - if (config.Blocks.Length > 0) UpdatePropertyData(dataType, config, knownDocumentTypes); - } - - UpdateDataType(dataType); - - refreshCache = true; - } - - return refreshCache; - } - - private BlockListConfiguration UpdateConfiguration(DataTypeDto dataType, Dictionary knownDocumentTypes) - { - var old = JsonConvert.DeserializeObject(dataType.Configuration); - var config = new BlockListConfiguration - { - Blocks = old.ContentTypes?.Select(t => new BlockListConfiguration.BlockConfiguration - { - Key = knownDocumentTypes.TryGetValue(t.IcContentTypeGuid, out var ct) ? ct.Key : Guid.Empty, - Label = t.NameTemplate - }).Where(c => c.Key != null).ToArray(), - UseInlineEditingAsDefault = old.SingleItemMode == "1" || old.SingleItemMode == bool.TrueString - }; - - if (int.TryParse(old.MaxItems, out var max) && max > 0) - { - config.ValidationLimit = new BlockListConfiguration.NumberRange { Max = max }; - } - - dataType.Configuration = ConfigurationEditor.ToDatabase(config); - - return config; - } - - private void UpdatePropertyData(DataTypeDto dataType, BlockListConfiguration config, Dictionary knownDocumentTypes) - { - // get property data dtos - var propertyDataDtos = Database.Fetch(Sql() - .Select() - .From() - .InnerJoin().On((pt, pd) => pt.Id == pd.PropertyTypeId) - .InnerJoin().On((dt, pt) => dt.NodeId == pt.DataTypeId) - .Where(x => x.DataTypeId == dataType.NodeId)); - - // update dtos - var updatedDtos = propertyDataDtos.Where(x => UpdatePropertyDataDto(x, config, knownDocumentTypes)); - - // persist changes - foreach (var propertyDataDto in updatedDtos) - Database.Update(propertyDataDto); - } - - - private bool UpdatePropertyDataDto(PropertyDataDto dto, BlockListConfiguration config, Dictionary knownDocumentTypes) - { - var model = new SimpleModel(); - - if (dto != null && !dto.TextValue.IsNullOrWhiteSpace() && dto.TextValue[0] == '[') - { - var scObjs = JsonConvert.DeserializeObject(dto.TextValue); - foreach (var obj in scObjs) model.AddDataItem(obj, knownDocumentTypes); - } - - dto.TextValue = JsonConvert.SerializeObject(model); - - return true; - } - - private void UpdateDataType(DataTypeDto dataType) - { - dataType.DbType = ValueStorageType.Ntext.ToString(); - dataType.EditorAlias = Constants.PropertyEditors.Aliases.BlockList; - - Database.Update(dataType); - } - - private class BlockListConfiguration - { - - [JsonProperty("blocks")] - public BlockConfiguration[] Blocks { get; set; } - - - [JsonProperty("validationLimit")] - public NumberRange ValidationLimit { get; set; } = new NumberRange(); - - public class NumberRange - { - [JsonProperty("min")] - public int? Min { get; set; } - - [JsonProperty("max")] - public int? Max { get; set; } - } - - public class BlockConfiguration - { - [JsonProperty("backgroundColor")] - public string BackgroundColor { get; set; } - - [JsonProperty("iconColor")] - public string IconColor { get; set; } - - [JsonProperty("thumbnail")] - public string Thumbnail { get; set; } - - [JsonProperty("contentTypeKey")] - public Guid Key { get; set; } - - [JsonProperty("settingsElementTypeKey")] - public string settingsElementTypeKey { get; set; } - - [JsonProperty("view")] - public string View { get; set; } - - [JsonProperty("label")] - public string Label { get; set; } - - [JsonProperty("editorSize")] - public string EditorSize { get; set; } - } - - [JsonProperty("useInlineEditingAsDefault")] - public bool UseInlineEditingAsDefault { get; set; } - - [JsonProperty("maxPropertyWidth")] - public string MaxPropertyWidth { get; set; } - } - - private class StackedContentConfiguration - { - - public class StackedContentType - { - public Guid IcContentTypeGuid { get; set; } - public string NameTemplate { get; set; } - } - - public StackedContentType[] ContentTypes { get; set; } - public string EnableCopy { get; set; } - public string EnableFilter { get; set; } - public string EnablePreview { get; set; } - public string HideLabel { get; set; } - public string MaxItems { get; set; } - public string SingleItemMode { get; set; } - } - - private class SimpleModel - { - [JsonProperty("layout")] - public SimpleLayout Layout { get; } = new SimpleLayout(); - [JsonProperty("data")] - public List Data { get; } = new List(); - - public void AddDataItem(JObject obj, Dictionary knownDocumentTypes) - { - if (!Guid.TryParse(obj["key"].ToString(), out var key)) key = Guid.NewGuid(); - if (!Guid.TryParse(obj["icContentTypeGuid"].ToString(), out var ctGuid)) ctGuid = Guid.Empty; - if (!knownDocumentTypes.TryGetValue(ctGuid, out var ct)) ct = new KnownContentType(null, ctGuid, null); - - obj.Remove("key"); - obj.Remove("icContentTypeGuid"); - - var udi = new GuidUdi(Constants.UdiEntityType.Element, key).ToString(); - obj["udi"] = udi; - obj["contentTypeKey"] = ct.Key; - - if (ct.StringToRawProperties != null && ct.StringToRawProperties.Length > 0) - { - // Nested content inside a stacked content item used to be stored as a deserialized string of the JSON array - // Now we store the content as the raw JSON array, so we need to convert from the string form to the array - foreach (var prop in ct.StringToRawProperties) - { - var val = obj[prop]; - var value = val?.ToString(); - if (val != null && val.Type == JTokenType.String && !value.IsNullOrWhiteSpace()) - obj[prop] = JsonConvert.DeserializeObject(value); - } - } - - Data.Add(obj); - Layout.Refs.Add(new SimpleLayout.SimpleLayoutRef { Udi = udi }); - } - - public class SimpleLayout - { - [JsonProperty(Constants.PropertyEditors.Aliases.BlockList)] - public List Refs { get; } = new List(); - - public class SimpleLayoutRef - { - [JsonProperty("udi")] - public string Udi { get; set; } - } - } - } - - private class KnownContentType - { - public KnownContentType(string alias, Guid key, string[] stringToRawProperties) - { - Alias = alias ?? throw new ArgumentNullException(nameof(alias)); - Key = key; - StringToRawProperties = stringToRawProperties ?? throw new ArgumentNullException(nameof(stringToRawProperties)); - } - - public string Alias { get; } - public Guid Key { get; } - public string[] StringToRawProperties { get; } - } - } -} diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index eb3b487dcd..a5c1141d8b 100755 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -132,8 +132,6 @@ - - From 5173a7b58969e7bed38d1486e3ac801e8c2754af Mon Sep 17 00:00:00 2001 From: Warren Buckley Date: Wed, 1 Jul 2020 08:07:30 +0100 Subject: [PATCH 283/377] Remove try/catch block as it will make it hard for people to see what the issue when using the Razor view when building sites --- .../Views/Partials/BlockList/Default.cshtml | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/src/Umbraco.Web.UI/Views/Partials/BlockList/Default.cshtml b/src/Umbraco.Web.UI/Views/Partials/BlockList/Default.cshtml index 8e5de940c5..19ae842759 100644 --- a/src/Umbraco.Web.UI/Views/Partials/BlockList/Default.cshtml +++ b/src/Umbraco.Web.UI/Views/Partials/BlockList/Default.cshtml @@ -8,13 +8,6 @@ { if (layout?.Udi == null) { continue; } var data = layout.Data; - try - { - @Html.Partial("BlockList/Components/" + data.ContentType.Alias, layout) - } - catch (Exception ex) - { - global::Umbraco.Core.Composing.Current.Logger.Error(typeof(BlockListModel), ex, "Could not display block list component for content type {0}", data?.ContentType?.Alias); - } + @Html.Partial("BlockList/Components/" + data.ContentType.Alias, layout) } From 5debb4a11ba05419d0c964bcc84e321c73f43f0f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Wed, 1 Jul 2020 10:28:02 +0200 Subject: [PATCH 284/377] option to hide content editor in overlay --- .../inlineblock.editor.controller.js | 6 +- .../labelblock/labelblock.editor.html | 2 +- .../blocklist.blockconfiguration.overlay.html | 22 + .../umbBlockListPropertyEditor.component.js | 14 +- src/Umbraco.Web.UI/Umbraco/config/lang/da.xml | 2 + src/Umbraco.Web.UI/Umbraco/config/lang/en.xml | 4968 +++++++++-------- .../Umbraco/config/lang/en_us.xml | 2 + .../PropertyEditors/BlockListConfiguration.cs | 3 + 8 files changed, 2530 insertions(+), 2489 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklistentryeditors/inlineblock/inlineblock.editor.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklistentryeditors/inlineblock/inlineblock.editor.controller.js index 29f8b1320c..c2c86ca824 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklistentryeditors/inlineblock/inlineblock.editor.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklistentryeditors/inlineblock/inlineblock.editor.controller.js @@ -13,9 +13,11 @@ $scope.$broadcast("formSubmitting", { scope: $scope }); // Some property editors need to performe an action after all property editors have reacted to the formSubmitting. $scope.$broadcast("postFormSubmitting", { scope: $scope }); + + block.active = false; + } else { + $scope.api.activateBlock(block); } - - block.active = !block.active; } } diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklistentryeditors/labelblock/labelblock.editor.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklistentryeditors/labelblock/labelblock.editor.html index 892c9e4be8..60fd3eb7dc 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklistentryeditors/labelblock/labelblock.editor.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklistentryeditors/labelblock/labelblock.editor.html @@ -1,4 +1,4 @@ - diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/blocklist.blockconfiguration.overlay.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/blocklist.blockconfiguration.overlay.html index ae5eb5543c..9675677c11 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/blocklist.blockconfiguration.overlay.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/blocklist.blockconfiguration.overlay.html @@ -196,6 +196,28 @@ +
        + +
        + Advanced +
        + +
        + + +
        +
        + +
        + +
        +
        +
        + +
        + +
        + 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 9ed28cdee5..346c79cff7 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 @@ -172,6 +172,7 @@ block.view = (block.config.view ? "/" + block.config.view : getDefaultViewForBlock(block)); + block.hideContentInOverlay = block.config.forceHideContentEditorInOverlay === true || inlineEditing === true; block.showSettings = block.config.settingsElementTypeKey != null; block.showCopy = vm.supportCopy && block.config.contentTypeKey != null;// if we have content, otherwise it dosnt make sense to copy. @@ -226,13 +227,19 @@ deleteBlock(entry.$block); }); } + + function activateBlock(blockObject) { + blockObject.active = true; + } function editBlock(blockObject, openSettings) { var wasNotActiveBefore = blockObject.active !== true; - blockObject.active = true; + activateBlock(blockObject); - if (inlineEditing === true && openSettings !== true) { + + if (inlineEditing === true && openSettings !== true) { // dont open the editor overlay if block has hidden its content editor in overlays and we are requesting to open content, not settings. + if (openSettings !== true && blockObject.hideContentInOverlay === true) { return; } @@ -247,7 +254,7 @@ var hideContent = (openSettings === true && inlineEditing === true); var blockEditorModel = { - hideContent: hideContent, + hideContent: blockObject.hideContentInOverlay, openSettings: openSettings === true, liveEditing: liveEditing, title: blockObject.label, @@ -481,6 +488,7 @@ vm.blockEditorApi = { + activateBlock: activateBlock, editBlock: editBlock, copyBlock: copyBlock, requestDeleteBlock: requestDeleteBlock, diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/da.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/da.xml index 7df379aa81..e3d456dea7 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/da.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/da.xml @@ -1849,6 +1849,8 @@ Mange hilsner fra Umbraco robotten Opret ny Udklipsholder Indstillinger + Avanceret + Skjuld indholds editoren diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml index eceb63c478..8355c36bd1 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml @@ -1,2486 +1,2488 @@ - - - - The Umbraco community - https://our.umbraco.com/documentation/Extending-Umbraco/Language-Files - - - Culture and Hostnames - Audit Trail - Browse Node - Change Document Type - Copy - Create - Export - Create Package - Create group - Delete - Disable - Edit settings - Empty recycle bin - Enable - Export Document Type - Import Document Type - Import Package - Edit in Canvas - Exit - Move - Notifications - Public access - Publish - Unpublish - Reload - Republish entire site - Remove - Rename - Restore - Set permissions for the page %0% - Choose where to copy - Choose where to move - to in the tree structure below - Choose where to copy the selected item(s) - Choose where to move the selected item(s) - was moved to - was copied to - was deleted - Permissions - Rollback - Send To Publish - Send To Translation - Set group - Sort - Translate - Update - Set permissions - Unlock - Create Content Template - Resend Invitation - - - Content - Administration - Structure - Other - - - Allow access to assign culture and hostnames - Allow access to view a node's history log - Allow access to view a node - Allow access to change document type for a node - Allow access to copy a node - Allow access to create nodes - Allow access to delete nodes - Allow access to move a node - Allow access to set and change public access for a node - Allow access to publish a node - Allow access to unpublish a node - Allow access to change permissions for a node - Allow access to roll back a node to a previous state - Allow access to send a node for approval before publishing - Allow access to send a node for translation - Allow access to change the sort order for nodes - Allow access to translate a node - Allow access to save a node - Allow access to create a Content Template - - - Content - Info - - - Permission denied. - Add new Domain - remove - Invalid node. - One or more domains have an invalid format. - Domain has already been assigned. - Language - Domain - New domain '%0%' has been created - Domain '%0%' is deleted - Domain '%0%' has already been assigned - Domain '%0%' has been updated - Edit Current Domains - - - Inherit - Culture - - or inherit culture from parent nodes. Will also apply
        - to the current node, unless a domain below applies too.]]> -
        - Domains - - - Clear selection - Select - Do something else - Bold - Cancel Paragraph Indent - Insert form field - Insert graphic headline - Edit Html - Indent Paragraph - Italic - Center - Justify Left - Justify Right - Insert Link - Insert local link (anchor) - Bullet List - Numeric List - Insert macro - Insert picture - Publish and close - Publish with descendants - Edit relations - Return to list - Save - Save and close - Save and publish - Save and schedule - Save and send for approval - Save list view - Schedule - Preview - Save and preview - Preview is disabled because there's no template assigned - Choose style - Show styles - Insert table - Save and generate models - Undo - Redo - Delete tag - Cancel - Confirm - More publishing options - Submit - Submit and close - - - Viewing for - Content deleted - Content unpublished - Content saved and Published - Content saved and published for languages: %0% - Content saved - Content saved for languages: %0% - Content moved - Content copied - Content rolled back - Content sent for publishing - Content sent for publishing for languages: %0% - Sort child items performed by user - %0% - Copy - Publish - Publish - Move - Save - Save - Delete - Unpublish - Rollback - Send To Publish - Send To Publish - Sort - Custom - History (all variants) - - - To change the document type for the selected content, first select from the list of valid types for this location. - Then confirm and/or amend the mapping of properties from the current type to the new, and click Save. - The content has been re-published. - Current Property - Current type - The document type cannot be changed, as there are no alternatives valid for this location. An alternative will be valid if it is allowed under the parent of the selected content item and that all existing child content items are allowed to be created under it. - Document Type Changed - Map Properties - Map to Property - New Template - New Type - none - Content - Select New Document Type - The document type of the selected content has been successfully changed to [new type] and the following properties mapped: - to - Could not complete property mapping as one or more properties have more than one mapping defined. - Only alternate types valid for the current location are displayed. - - - Failed to create a folder under parent with ID %0% - Failed to create a folder under parent with name %0% - The folder name cannot contain illegal characters. - Failed to delete item: %0% - - - Is Published - About this page - Alias - (how would you describe the picture over the phone) - Alternative Links - Click to edit this item - Created by - Original author - Updated by - Created - Date/time this document was created - Document Type - Editing - Remove at - This item has been changed after publication - This item is not published - Last published - There are no items to show - There are no items to show in the list. - No content has been added - No members have been added - Media Type - Link to media item(s) - Member Group - Role - Member Type - No changes have been made - No date chosen - Page title - This media item has no link - Properties - This document is published but is not visible because the parent '%0%' is unpublished - This culture is published but is not visible because it is unpublished on parent '%0%' - This document is published but is not in the cache - Could not get the url - This document is published but its url would collide with content %0% - This document is published but its url cannot be routed - Publish - Published - Published (pending changes) - Publication Status - %0% and all content items underneath and thereby making their content publicly available.]]> - - Publish at - Unpublish at - Clear Date - Set date - Sortorder is updated - To sort the nodes, simply drag the nodes or click one of the column headers. You can select multiple nodes by holding the "shift" or "control" key while selecting - Statistics - Title (optional) - Alternative text (optional) - Type - Unpublish - Unpublished - Last edited - Date/time this document was edited - Remove file(s) - Click here to remove the image from the media item - Click here to remove the file from the media item - Link to document - Member of group(s) - Not a member of group(s) - Child items - Target - This translates to the following time on the server: - What does this mean?]]> - Are you sure you want to delete this item? - Property %0% uses editor %1% which is not supported by Nested Content. - Are you sure you want to delete all items? - No content types are configured for this property. - Add element type - Select element type - Select the group whose properties should be displayed. If left blank, the first group on the element type will be used. - Enter an angular expression to evaluate against each item for its name. Use - to display the item index - Add another text box - Remove this text box - Content root - Include drafts and unpublished content items. - This value is hidden. If you need access to view this value please contact your website administrator. - This value is hidden. - What languages would you like to publish? All languages with content are saved! - What languages would you like to publish? - What languages would you like to save? - All languages with content are saved on creation! - What languages would you like to send for approval? - What languages would you like to schedule? - Select the languages to unpublish. Unpublishing a mandatory language will unpublish all languages. - Published Languages - Unpublished Languages - Unmodified Languages - These languages haven't been created - - All new variants will be saved. - Which variants you would like to publish? - Choose which variants to be saved. - Pick variants to send for approval. - Set scheduled publishing... - Select the variants to unpublish. Unpublishing a mandatory language will unpublish all variants. - The following variants is required for publishing to take place: - - We are not ready to Publish - Ready to publish? - Ready to Save? - Send for approval - Select the date and time to publish and/or unpublish the content item. - Create new - Paste from clipboard - This item is in the Recycle Bin - - - Create a new Content Template from '%0%' - Blank - Select a Content Template - Content Template created - A Content Template was created from '%0%' - Another Content Template with the same name already exists - A Content Template is predefined content that an editor can select to use as the basis for creating new content - - - Click to upload - or click here to choose files - You can drag files here to upload - Cannot upload this file, it does not have an approved file type - Max file size is - Media root - Failed to move media - Failed to copy media - Failed to create a folder under parent id %0% - Failed to rename the folder with id %0% - Drag and drop your file(s) into the area - - - Create a new member - All Members - Member groups have no additional properties for editing. - - - Where do you want to create the new %0% - Create an item under - Select the document type you want to make a content template for - Enter a folder name - Choose a type and a title - Document Types within the Settings section, by editing the Allowed child node types under Permissions.]]> - Document Types within the Settings section.]]> - The selected page in the content tree doesn't allow for any pages to be created below it. - Edit permissions for this document type - Create a new document type - Document Types within the Settings section, by changing the Allow as root option under Permissions.]]> - Media Types Types within the Settings section, by editing the Allowed child node types under Permissions.]]> - The selected media in the tree doesn't allow for any other media to be created below it. - Edit permissions for this media type - Document Type without a template - New folder - New data type - New JavaScript file - New empty partial view - New partial view macro - New partial view from snippet - New partial view macro from snippet - New partial view macro (without macro) - New style sheet file - New Rich Text Editor style sheet file - - - Browse your website - - Hide - If Umbraco isn't opening, you might need to allow popups from this site - has opened in a new window - Restart - Visit - Welcome - - - Stay - Discard changes - You have unsaved changes - Are you sure you want to navigate away from this page? - you have unsaved changes - Publishing will make the selected items visible on the site. - Unpublishing will remove the selected items and all their descendants from the site. - Unpublishing will remove this page and all its descendants from the site. - You have unsaved changes. Making changes to the Document Type will discard the changes. - - - Done - Deleted %0% item - Deleted %0% items - Deleted %0% out of %1% item - Deleted %0% out of %1% items - Published %0% item - Published %0% items - Published %0% out of %1% item - Published %0% out of %1% items - Unpublished %0% item - Unpublished %0% items - Unpublished %0% out of %1% item - Unpublished %0% out of %1% items - Moved %0% item - Moved %0% items - Moved %0% out of %1% item - Moved %0% out of %1% items - Copied %0% item - Copied %0% items - Copied %0% out of %1% item - Copied %0% out of %1% items - - - Link title - Link - Anchor / querystring - Name - Manage hostnames - Close this window - Are you sure you want to delete - Are you sure you want to delete %0% based on %1% - Are you sure you want to disable - Are you sure you want to remove - %0%]]> - %0%]]> - Are you sure? - Are you sure? - Cut - Edit Dictionary Item - Edit Language - Edit selected media - Insert local link - Insert character - Insert graphic headline - Insert picture - Insert link - Click to add a Macro - Insert table - This will delete the language - Changing the culture for a language may be an expensive operation and will result in the content cache and indexes being rebuilt - Last Edited - Link - Internal link: - When using local links, insert "#" in front of link - Open in new window? - Macro Settings - This macro does not contain any properties you can edit - Paste - Edit permissions for - Set permissions for - Set permissions for %0% for user group %1% - Select the users groups you want to set permissions for - The items in the recycle bin are now being deleted. Please do not close this window while this operation takes place - The recycle bin is now empty - When items are deleted from the recycle bin, they will be gone forever - regexlib.com's webservice is currently experiencing some problems, which we have no control over. We are very sorry for this inconvenience.]]> - Search for a regular expression to add validation to a form field. Example: 'email, 'zip-code' 'url' - Remove Macro - Required Field - Site is reindexed - The website cache has been refreshed. All publish content is now up to date. While all unpublished content is still unpublished - The website cache will be refreshed. All published content will be updated, while unpublished content will stay unpublished. - Number of columns - Number of rows - Click on the image to see full size - Pick item - View Cache Item - Relate to original - Include descendants - The friendliest community - Link to page - Opens the linked document in a new window or tab - Link to media - Select content start node - Select media - Select media type - Select icon - Select item - Select link - Select macro - Select content - Select content type - Select media start node - Select member - Select member group - Select member type - Select node - Select sections - Select users - No icons were found - There are no parameters for this macro - There are no macros available to insert - External login providers - Exception Details - Stacktrace - Inner Exception - Link your - Un-link your - account - Select editor - Select snippet - This will delete the node and all its languages. If you only want to delete one language, you should unpublish the node in that language instead. - - - There are no dictionary items. - - - %0%' below - ]]> - Culture Name - - Dictionary overview - - - Configured Searchers - Shows properties and tools for any configured Searcher (i.e. such as a multi-index searcher) - Field values - Health status - The health status of the index and if it can be read - Indexers - Index info - Lists the properties of the index - Manage Examine's indexes - Allows you to view the details of each index and provides some tools for managing the indexes - Rebuild index - - Depending on how much content there is in your site this could take a while.
        - It is not recommended to rebuild an index during times of high website traffic or when editors are editing content. - ]]> -
        - Searchers - Search the index and view the results - Tools - Tools to manage the index - fields - The index cannot be read and will need to be rebuilt - The process is taking longer than expected, check the umbraco log to see if there have been any errors during this operation - This index cannot be rebuilt because it has no assigned - IIndexPopulator - - - Enter your username - Enter your password - Confirm your password - Name the %0%... - Enter a name... - Enter an email... - Enter a username... - Label... - Enter a description... - Type to search... - Type to filter... - Type to add tags (press enter after each tag)... - Enter your email - Enter a message... - Your username is usually your email - #value or ?key=value - Enter alias... - Generating alias... - Create item - Create - Edit - Name - - - Create custom list view - Remove custom list view - A content type, media type or member type with this alias already exists - - - Renamed - Enter a new folder name here - %0% was renamed to %1% - - - Add prevalue - Database datatype - Property editor GUID - Property editor - Buttons - Enable advanced settings for - Enable context menu - Maximum default size of inserted images - Related stylesheets - Show label - Width and height - All property types & property data - using this data type will be deleted permanently, please confirm you want to delete these as well - Yes, delete - and all property types & property data using this data type - Select the folder to move - to in the tree structure below - was moved underneath - - - Your data has been saved, but before you can publish this page there are some errors you need to fix first: - The current membership provider does not support changing password (EnablePasswordRetrieval need to be true) - %0% already exists - There were errors: - There were errors: - The password should be a minimum of %0% characters long and contain at least %1% non-alpha numeric character(s) - %0% must be an integer - The %0% field in the %1% tab is mandatory - %0% is a mandatory field - %0% at %1% is not in a correct format - %0% is not in a correct format - Property '%0%' uses editor '%1%' which is not supported in Element Types. - - - Received an error from the server - The specified file type has been disallowed by the administrator - NOTE! Even though CodeMirror is enabled by configuration, it is disabled in Internet Explorer because it's not stable enough. - Please fill both alias and name on the new property type! - There is a problem with read/write access to a specific file or folder - Error loading Partial View script (file: %0%) - Please enter a title - Please choose a type - You're about to make the picture larger than the original size. Are you sure that you want to proceed? - Startnode deleted, please contact your administrator - Please mark content before changing style - No active styles available - Please place cursor at the left of the two cells you wish to merge - You cannot split a cell that hasn't been merged. - This property is invalid - - - About - Action - Actions - Add - Alias - All - Are you sure? - Back - Back to overview - Border - by - Cancel - Cell margin - Choose - Clear - Close - Close Window - Comment - Confirm - Constrain - Constrain proportions - Content - Continue - Copy - Create - Database - Date - Default - Delete - Deleted - Deleting... - Design - Dictionary - Dimensions - Down - Download - Edit - Edited - Elements - Email - Error - Field - Find - First - Focal point - General - Groups - Group - Height - Help - Hide - History - Icon - Id - Import - Include subfolders in search - Search only this folder - Info - Inner margin - Insert - Install - Invalid - Justify - Label - Language - Last - Layout - Links - Loading - Locked - Login - Log off - Logout - Macro - Mandatory - Message - Move - Name - New - Next - No - of - Off - OK - Open - Options - On - or - Order by - Password - Path - One moment please... - Previous - Properties - Rebuild - Email to receive form data - Recycle Bin - Your recycle bin is empty - Reload - Remaining - Remove - Rename - Renew - Required - Retrieve - Retry - Permissions - Scheduled Publishing - Search - Sorry, we can not find what you are looking for. - No items have been added - Server - Settings - Show - Show page on Send - Size - Sort - Status - Submit - Success - Type - Type to search... - under - Up - Update - Upgrade - Upload - Url - User - Username - Value - View - Welcome... - Width - Yes - Folder - Search results - Reorder - I am done reordering - Preview - Change password - to - List view - Saving... - current - Embed - selected - Other - Articles - Videos - Clear - Installing - - - Blue - - - Add group - Add property - Add editor - Add template - Add child node - Add child - Edit data type - Navigate sections - Shortcuts - show shortcuts - Toggle list view - Toggle allow as root - Comment/Uncomment lines - Remove line - Copy Lines Up - Copy Lines Down - Move Lines Up - Move Lines Down - General - Editor - Toggle allow culture variants - Toggle allow segmentation - - - Background colour - Bold - Text colour - Font - Text - - - Page - - - The installer cannot connect to the database. - Could not save the web.config file. Please modify the connection string manually. - Your database has been found and is identified as - Database configuration - - install button to install the Umbraco %0% database - ]]> - - Next to proceed.]]> - Database not found! Please check that the information in the "connection string" of the "web.config" file is correct.

        -

        To proceed, please edit the "web.config" file (using Visual Studio or your favourite text editor), scroll to the bottom, add the connection string for your database in the key named "UmbracoDbDSN" and save the file.

        -

        - Click the retry button when - done.
        - More information on editing web.config here.

        ]]>
        - - Please contact your ISP if necessary. - If you're installing on a local machine or server you might need information from your system administrator.]]> - - Press the upgrade button to upgrade your database to Umbraco %0%

        -

        - Don't worry - no content will be deleted and everything will continue working afterwards! -

        - ]]>
        - Press Next to - proceed. ]]> - next to continue the configuration wizard]]> - The Default users' password needs to be changed!]]> - The Default user has been disabled or has no access to Umbraco!

        No further actions needs to be taken. Click Next to proceed.]]> - The Default user's password has been successfully changed since the installation!

        No further actions needs to be taken. Click Next to proceed.]]> - The password is changed! - Get a great start, watch our introduction videos - By clicking the next button (or modifying the umbracoConfigurationStatus in web.config), you accept the license for this software as specified in the box below. Notice that this Umbraco distribution consists of two different licenses, the open source MIT license for the framework and the Umbraco freeware license that covers the UI. - Not installed yet. - Affected files and folders - More information on setting up permissions for Umbraco here - You need to grant ASP.NET modify permissions to the following files/folders - Your permission settings are almost perfect!

        - You can run Umbraco without problems, but you will not be able to install packages which are recommended to take full advantage of Umbraco.]]>
        - How to Resolve - Click here to read the text version - video tutorial on setting up folder permissions for Umbraco or read the text version.]]> - Your permission settings might be an issue! -

        - You can run Umbraco without problems, but you will not be able to create folders or install packages which are recommended to take full advantage of Umbraco.]]>
        - Your permission settings are not ready for Umbraco! -

        - In order to run Umbraco, you'll need to update your permission settings.]]>
        - Your permission settings are perfect!

        - You are ready to run Umbraco and install packages!]]>
        - Resolving folder issue - Follow this link for more information on problems with ASP.NET and creating folders - Setting up folder permissions - - I want to start from scratch - learn how) - You can still choose to install Runway later on. Please go to the Developer section and choose Packages. - ]]> - You've just set up a clean Umbraco platform. What do you want to do next? - Runway is installed - - This is our list of recommended modules, check off the ones you would like to install, or view the full list of modules - ]]> - Only recommended for experienced users - I want to start with a simple website - - "Runway" is a simple website providing some basic document types and templates. The installer can set up Runway for you automatically, - but you can easily edit, extend or remove it. It's not necessary and you can perfectly use Umbraco without it. However, - Runway offers an easy foundation based on best practices to get you started faster than ever. - If you choose to install Runway, you can optionally select basic building blocks called Runway Modules to enhance your Runway pages. -

        - - Included with Runway: Home page, Getting Started page, Installing Modules page.
        - Optional Modules: Top Navigation, Sitemap, Contact, Gallery. -
        - ]]>
        - What is Runway - Step 1/5 Accept license - Step 2/5: Database configuration - Step 3/5: Validating File Permissions - Step 4/5: Check Umbraco security - Step 5/5: Umbraco is ready to get you started - Thank you for choosing Umbraco - Browse your new site -You installed Runway, so why not see how your new website looks.]]> - Further help and information -Get help from our award winning community, browse the documentation or watch some free videos on how to build a simple site, how to use packages and a quick guide to the Umbraco terminology]]> - Umbraco %0% is installed and ready for use - /web.config file and update the AppSetting key UmbracoConfigurationStatus in the bottom to the value of '%0%'.]]> - started instantly by clicking the "Launch Umbraco" button below.
        If you are new to Umbraco, -you can find plenty of resources on our getting started pages.]]>
        - Launch Umbraco -To manage your website, simply open the Umbraco back office and start adding content, updating the templates and stylesheets or add new functionality]]> - Connection to database failed. - Umbraco Version 3 - Umbraco Version 4 - Watch - Umbraco %0% for a fresh install or upgrading from version 3.0. -

        - Press "next" to start the wizard.]]>
        - - - Culture Code - Culture Name - - - You've been idle and logout will automatically occur in - Renew now to save your work - - - Happy super Sunday - Happy manic Monday - Happy tubular Tuesday - Happy wonderful Wednesday - Happy thunderous Thursday - Happy funky Friday - Happy Caturday - Log in below - Sign in with - Session timed out - © 2001 - %0%
        Umbraco.com

        ]]>
        - Forgotten password? - An email will be sent to the address specified with a link to reset your password - An email with password reset instructions will be sent to the specified address if it matched our records - Show password - Hide password - Return to login form - Please provide a new password - Your Password has been updated - The link you have clicked on is invalid or has expired - Umbraco: Reset Password - - - - - - - - - - - -
        - - - - - -
        - -
        - -
        -
        - - - - - - -
        -
        -
        - - - - -
        - - - - -
        -

        - Password reset requested -

        -

        - Your username to login to the Umbraco back-office is: %0% -

        -

        - - - - - - -
        - - Click this link to reset your password - -
        -

        -

        If you cannot click on the link, copy and paste this URL into your browser window:

        - - - - -
        - - %1% - -
        -

        -
        -
        -


        -
        -
        - - - ]]>
        - - - Dashboard - Sections - Content - - - Choose page above... - %0% has been copied to %1% - Select where the document %0% should be copied to below - %0% has been moved to %1% - Select where the document %0% should be moved to below - has been selected as the root of your new content, click 'ok' below. - No node selected yet, please select a node in the list above before clicking 'ok' - The current node is not allowed under the chosen node because of its type - The current node cannot be moved to one of its subpages - The current node cannot exist at the root - The action isn't allowed since you have insufficient permissions on 1 or more child documents. - Relate copied items to original - - - %0%]]> - Notification settings saved for - - The following languages have been modified %0% - - - - - - - - - - - -
        - - - - - -
        - -
        - -
        -
        - - - - - - -
        -
        -
        - - - - -
        - - - - -
        -

        - Hi %0%, -

        -

        - This is an automated mail to inform you that the task '%1%' has been performed on the page '%2%' by the user '%3%' -

        - - - - - - -
        - -
        - EDIT
        -
        -

        -

        Update summary:

        - %6% -

        -

        - Have a nice day!

        - Cheers from the Umbraco robot -

        -
        -
        -


        -
        -
        - - - ]]>
        - The following languages have been modified:

        - %0% - ]]>
        - [%0%] Notification about %1% performed on %2% - Notifications - - - Actions - Created - Create package - - button and locating the package. Umbraco packages usually have a ".umb" or ".zip" extension. - ]]> - This will delete the package - Drop to upload - Include all child nodes - or click here to choose package file - Upload package - Install a local package by selecting it from your machine. Only install packages from sources you know and trust - Upload another package - Cancel and upload another package - I accept - terms of use - - Path to file - Absolute path to file (ie: /bin/umbraco.bin) - Installed - Installed packages - Install local - Finish - This package has no configuration view - No packages have been created yet - You don’t have any packages installed - 'Packages' icon in the top right of your screen]]> - Package Actions - Author URL - Package Content - Package Files - Icon URL - Install package - License - License URL - Package Properties - Search for packages - Results for - We couldn’t find anything for - Please try searching for another package or browse through the categories - Popular - New releases - has - karma points - Information - Owner - Contributors - Created - Current version - .NET version - Downloads - Likes - Compatibility - This package is compatible with the following versions of Umbraco, as reported by community members. Full compatability cannot be guaranteed for versions reported below 100% - External sources - Author - Documentation - Package meta data - Package name - Package doesn't contain any items -
        - You can safely remove this from the system by clicking "uninstall package" below.]]>
        - Package options - Package readme - Package repository - Confirm package uninstall - Package was uninstalled - The package was successfully uninstalled - Uninstall package - - Notice: any documents, media etc depending on the items you remove, will stop working, and could lead to system instability, - so uninstall with caution. If in doubt, contact the package author.]]> - Package version - Package already installed - This package cannot be installed, it requires a minimum Umbraco version of - Uninstalling... - Downloading... - Importing... - Installing... - Restarting, please wait... - All done, your browser will now refresh, please wait... - Please click 'Finish' to complete installation and reload the page. - Uploading package... - - - Paste with full formatting (Not recommended) - The text you're trying to paste contains special characters or formatting. This could be caused by copying text from Microsoft Word. Umbraco can remove special characters or formatting automatically, so the pasted content will be more suitable for the web. - Paste as raw text without any formatting at all - Paste, but remove formatting (Recommended) - - - Group based protection - If you want to grant access to all members of specific member groups - You need to create a member group before you can use group based authentication - Error Page - Used when people are logged on, but do not have access - %0%]]> - %0% is now protected]]> - %0%]]> - Login Page - Choose the page that contains the login form - Remove protection... - %0%?]]> - Select the pages that contain login form and error messages - %0%]]> - %0%]]> - Specific members protection - If you wish to grant access to specific members - - - - - - - - - Include unpublished subpages - Publishing in progress - please wait... - %0% out of %1% pages have been published... - %0% has been published - %0% and subpages have been published - Publish %0% and all its subpages - Publish to publish %0% and thereby making its content publicly available.

        - You can publish this page and all its subpages by checking Include unpublished subpages below. - ]]>
        - - - You have not configured any approved colours - - - You can only select items of type(s): %0% - You have picked a content item currently deleted or in the recycle bin - You have picked content items currently deleted or in the recycle bin - - - Deleted item - You have picked a media item currently deleted or in the recycle bin - You have picked media items currently deleted or in the recycle bin - Trashed - - - enter external link - choose internal page - Caption - Link - Open in new window - enter the display caption - Enter the link - - - Reset crop - Save crop - Add new crop - Done - Undo edits - User defined - - + + + + The Umbraco community + https://our.umbraco.com/documentation/Extending-Umbraco/Language-Files + + + Culture and Hostnames + Audit Trail + Browse Node + Change Document Type + Copy + Create + Export + Create Package + Create group + Delete + Disable + Edit settings + Empty recycle bin + Enable + Export Document Type + Import Document Type + Import Package + Edit in Canvas + Exit + Move + Notifications + Public access + Publish + Unpublish + Reload + Republish entire site + Remove + Rename + Restore + Set permissions for the page %0% + Choose where to copy + Choose where to move + to in the tree structure below + Choose where to copy the selected item(s) + Choose where to move the selected item(s) + was moved to + was copied to + was deleted + Permissions + Rollback + Send To Publish + Send To Translation + Set group + Sort + Translate + Update + Set permissions + Unlock + Create Content Template + Resend Invitation + + + Content + Administration + Structure + Other + + + Allow access to assign culture and hostnames + Allow access to view a node's history log + Allow access to view a node + Allow access to change document type for a node + Allow access to copy a node + Allow access to create nodes + Allow access to delete nodes + Allow access to move a node + Allow access to set and change public access for a node + Allow access to publish a node + Allow access to unpublish a node + Allow access to change permissions for a node + Allow access to roll back a node to a previous state + Allow access to send a node for approval before publishing + Allow access to send a node for translation + Allow access to change the sort order for nodes + Allow access to translate a node + Allow access to save a node + Allow access to create a Content Template + + + Content + Info + + + Permission denied. + Add new Domain + remove + Invalid node. + One or more domains have an invalid format. + Domain has already been assigned. + Language + Domain + New domain '%0%' has been created + Domain '%0%' is deleted + Domain '%0%' has already been assigned + Domain '%0%' has been updated + Edit Current Domains + + + Inherit + Culture + + or inherit culture from parent nodes. Will also apply
        + to the current node, unless a domain below applies too.]]> +
        + Domains + + + Clear selection + Select + Do something else + Bold + Cancel Paragraph Indent + Insert form field + Insert graphic headline + Edit Html + Indent Paragraph + Italic + Center + Justify Left + Justify Right + Insert Link + Insert local link (anchor) + Bullet List + Numeric List + Insert macro + Insert picture + Publish and close + Publish with descendants + Edit relations + Return to list + Save + Save and close + Save and publish + Save and schedule + Save and send for approval + Save list view + Schedule + Preview + Save and preview + Preview is disabled because there's no template assigned + Choose style + Show styles + Insert table + Save and generate models + Undo + Redo + Delete tag + Cancel + Confirm + More publishing options + Submit + Submit and close + + + Viewing for + Content deleted + Content unpublished + Content saved and Published + Content saved and published for languages: %0% + Content saved + Content saved for languages: %0% + Content moved + Content copied + Content rolled back + Content sent for publishing + Content sent for publishing for languages: %0% + Sort child items performed by user + %0% + Copy + Publish + Publish + Move + Save + Save + Delete + Unpublish + Rollback + Send To Publish + Send To Publish + Sort + Custom + History (all variants) + + + To change the document type for the selected content, first select from the list of valid types for this location. + Then confirm and/or amend the mapping of properties from the current type to the new, and click Save. + The content has been re-published. + Current Property + Current type + The document type cannot be changed, as there are no alternatives valid for this location. An alternative will be valid if it is allowed under the parent of the selected content item and that all existing child content items are allowed to be created under it. + Document Type Changed + Map Properties + Map to Property + New Template + New Type + none + Content + Select New Document Type + The document type of the selected content has been successfully changed to [new type] and the following properties mapped: + to + Could not complete property mapping as one or more properties have more than one mapping defined. + Only alternate types valid for the current location are displayed. + + + Failed to create a folder under parent with ID %0% + Failed to create a folder under parent with name %0% + The folder name cannot contain illegal characters. + Failed to delete item: %0% + + + Is Published + About this page + Alias + (how would you describe the picture over the phone) + Alternative Links + Click to edit this item + Created by + Original author + Updated by + Created + Date/time this document was created + Document Type + Editing + Remove at + This item has been changed after publication + This item is not published + Last published + There are no items to show + There are no items to show in the list. + No content has been added + No members have been added + Media Type + Link to media item(s) + Member Group + Role + Member Type + No changes have been made + No date chosen + Page title + This media item has no link + Properties + This document is published but is not visible because the parent '%0%' is unpublished + This culture is published but is not visible because it is unpublished on parent '%0%' + This document is published but is not in the cache + Could not get the url + This document is published but its url would collide with content %0% + This document is published but its url cannot be routed + Publish + Published + Published (pending changes) + Publication Status + %0% and all content items underneath and thereby making their content publicly available.]]> + + Publish at + Unpublish at + Clear Date + Set date + Sortorder is updated + To sort the nodes, simply drag the nodes or click one of the column headers. You can select multiple nodes by holding the "shift" or "control" key while selecting + Statistics + Title (optional) + Alternative text (optional) + Type + Unpublish + Unpublished + Last edited + Date/time this document was edited + Remove file(s) + Click here to remove the image from the media item + Click here to remove the file from the media item + Link to document + Member of group(s) + Not a member of group(s) + Child items + Target + This translates to the following time on the server: + What does this mean?]]> + Are you sure you want to delete this item? + Property %0% uses editor %1% which is not supported by Nested Content. + Are you sure you want to delete all items? + No content types are configured for this property. + Add element type + Select element type + Select the group whose properties should be displayed. If left blank, the first group on the element type will be used. + Enter an angular expression to evaluate against each item for its name. Use + to display the item index + Add another text box + Remove this text box + Content root + Include drafts and unpublished content items. + This value is hidden. If you need access to view this value please contact your website administrator. + This value is hidden. + What languages would you like to publish? All languages with content are saved! + What languages would you like to publish? + What languages would you like to save? + All languages with content are saved on creation! + What languages would you like to send for approval? + What languages would you like to schedule? + Select the languages to unpublish. Unpublishing a mandatory language will unpublish all languages. + Published Languages + Unpublished Languages + Unmodified Languages + These languages haven't been created + + All new variants will be saved. + Which variants you would like to publish? + Choose which variants to be saved. + Pick variants to send for approval. + Set scheduled publishing... + Select the variants to unpublish. Unpublishing a mandatory language will unpublish all variants. + The following variants is required for publishing to take place: + + We are not ready to Publish + Ready to publish? + Ready to Save? + Send for approval + Select the date and time to publish and/or unpublish the content item. + Create new + Paste from clipboard + This item is in the Recycle Bin + + + Create a new Content Template from '%0%' + Blank + Select a Content Template + Content Template created + A Content Template was created from '%0%' + Another Content Template with the same name already exists + A Content Template is predefined content that an editor can select to use as the basis for creating new content + + + Click to upload + or click here to choose files + You can drag files here to upload + Cannot upload this file, it does not have an approved file type + Max file size is + Media root + Failed to move media + Failed to copy media + Failed to create a folder under parent id %0% + Failed to rename the folder with id %0% + Drag and drop your file(s) into the area + + + Create a new member + All Members + Member groups have no additional properties for editing. + + + Where do you want to create the new %0% + Create an item under + Select the document type you want to make a content template for + Enter a folder name + Choose a type and a title + Document Types within the Settings section, by editing the Allowed child node types under Permissions.]]> + Document Types within the Settings section.]]> + The selected page in the content tree doesn't allow for any pages to be created below it. + Edit permissions for this document type + Create a new document type + Document Types within the Settings section, by changing the Allow as root option under Permissions.]]> + Media Types Types within the Settings section, by editing the Allowed child node types under Permissions.]]> + The selected media in the tree doesn't allow for any other media to be created below it. + Edit permissions for this media type + Document Type without a template + New folder + New data type + New JavaScript file + New empty partial view + New partial view macro + New partial view from snippet + New partial view macro from snippet + New partial view macro (without macro) + New style sheet file + New Rich Text Editor style sheet file + + + Browse your website + - Hide + If Umbraco isn't opening, you might need to allow popups from this site + has opened in a new window + Restart + Visit + Welcome + + + Stay + Discard changes + You have unsaved changes + Are you sure you want to navigate away from this page? - you have unsaved changes + Publishing will make the selected items visible on the site. + Unpublishing will remove the selected items and all their descendants from the site. + Unpublishing will remove this page and all its descendants from the site. + You have unsaved changes. Making changes to the Document Type will discard the changes. + + + Done + Deleted %0% item + Deleted %0% items + Deleted %0% out of %1% item + Deleted %0% out of %1% items + Published %0% item + Published %0% items + Published %0% out of %1% item + Published %0% out of %1% items + Unpublished %0% item + Unpublished %0% items + Unpublished %0% out of %1% item + Unpublished %0% out of %1% items + Moved %0% item + Moved %0% items + Moved %0% out of %1% item + Moved %0% out of %1% items + Copied %0% item + Copied %0% items + Copied %0% out of %1% item + Copied %0% out of %1% items + + + Link title + Link + Anchor / querystring + Name + Manage hostnames + Close this window + Are you sure you want to delete + Are you sure you want to delete %0% based on %1% + Are you sure you want to disable + Are you sure you want to remove + %0%]]> + %0%]]> + Are you sure? + Are you sure? + Cut + Edit Dictionary Item + Edit Language + Edit selected media + Insert local link + Insert character + Insert graphic headline + Insert picture + Insert link + Click to add a Macro + Insert table + This will delete the language + Changing the culture for a language may be an expensive operation and will result in the content cache and indexes being rebuilt + Last Edited + Link + Internal link: + When using local links, insert "#" in front of link + Open in new window? + Macro Settings + This macro does not contain any properties you can edit + Paste + Edit permissions for + Set permissions for + Set permissions for %0% for user group %1% + Select the users groups you want to set permissions for + The items in the recycle bin are now being deleted. Please do not close this window while this operation takes place + The recycle bin is now empty + When items are deleted from the recycle bin, they will be gone forever + regexlib.com's webservice is currently experiencing some problems, which we have no control over. We are very sorry for this inconvenience.]]> + Search for a regular expression to add validation to a form field. Example: 'email, 'zip-code' 'url' + Remove Macro + Required Field + Site is reindexed + The website cache has been refreshed. All publish content is now up to date. While all unpublished content is still unpublished + The website cache will be refreshed. All published content will be updated, while unpublished content will stay unpublished. + Number of columns + Number of rows + Click on the image to see full size + Pick item + View Cache Item + Relate to original + Include descendants + The friendliest community + Link to page + Opens the linked document in a new window or tab + Link to media + Select content start node + Select media + Select media type + Select icon + Select item + Select link + Select macro + Select content + Select content type + Select media start node + Select member + Select member group + Select member type + Select node + Select sections + Select users + No icons were found + There are no parameters for this macro + There are no macros available to insert + External login providers + Exception Details + Stacktrace + Inner Exception + Link your + Un-link your + account + Select editor + Select snippet + This will delete the node and all its languages. If you only want to delete one language, you should unpublish the node in that language instead. + + + There are no dictionary items. + + + %0%' below + ]]> + Culture Name + + Dictionary overview + + + Configured Searchers + Shows properties and tools for any configured Searcher (i.e. such as a multi-index searcher) + Field values + Health status + The health status of the index and if it can be read + Indexers + Index info + Lists the properties of the index + Manage Examine's indexes + Allows you to view the details of each index and provides some tools for managing the indexes + Rebuild index + + Depending on how much content there is in your site this could take a while.
        + It is not recommended to rebuild an index during times of high website traffic or when editors are editing content. + ]]> +
        + Searchers + Search the index and view the results + Tools + Tools to manage the index + fields + The index cannot be read and will need to be rebuilt + The process is taking longer than expected, check the umbraco log to see if there have been any errors during this operation + This index cannot be rebuilt because it has no assigned + IIndexPopulator + + + Enter your username + Enter your password + Confirm your password + Name the %0%... + Enter a name... + Enter an email... + Enter a username... + Label... + Enter a description... + Type to search... + Type to filter... + Type to add tags (press enter after each tag)... + Enter your email + Enter a message... + Your username is usually your email + #value or ?key=value + Enter alias... + Generating alias... + Create item + Create + Edit + Name + + + Create custom list view + Remove custom list view + A content type, media type or member type with this alias already exists + + + Renamed + Enter a new folder name here + %0% was renamed to %1% + + + Add prevalue + Database datatype + Property editor GUID + Property editor + Buttons + Enable advanced settings for + Enable context menu + Maximum default size of inserted images + Related stylesheets + Show label + Width and height + All property types & property data + using this data type will be deleted permanently, please confirm you want to delete these as well + Yes, delete + and all property types & property data using this data type + Select the folder to move + to in the tree structure below + was moved underneath + + + Your data has been saved, but before you can publish this page there are some errors you need to fix first: + The current membership provider does not support changing password (EnablePasswordRetrieval need to be true) + %0% already exists + There were errors: + There were errors: + The password should be a minimum of %0% characters long and contain at least %1% non-alpha numeric character(s) + %0% must be an integer + The %0% field in the %1% tab is mandatory + %0% is a mandatory field + %0% at %1% is not in a correct format + %0% is not in a correct format + Property '%0%' uses editor '%1%' which is not supported in Element Types. + + + Received an error from the server + The specified file type has been disallowed by the administrator + NOTE! Even though CodeMirror is enabled by configuration, it is disabled in Internet Explorer because it's not stable enough. + Please fill both alias and name on the new property type! + There is a problem with read/write access to a specific file or folder + Error loading Partial View script (file: %0%) + Please enter a title + Please choose a type + You're about to make the picture larger than the original size. Are you sure that you want to proceed? + Startnode deleted, please contact your administrator + Please mark content before changing style + No active styles available + Please place cursor at the left of the two cells you wish to merge + You cannot split a cell that hasn't been merged. + This property is invalid + + + About + Action + Actions + Add + Alias + All + Are you sure? + Back + Back to overview + Border + by + Cancel + Cell margin + Choose + Clear + Close + Close Window + Comment + Confirm + Constrain + Constrain proportions + Content + Continue + Copy + Create + Database + Date + Default + Delete + Deleted + Deleting... + Design + Dictionary + Dimensions + Down + Download + Edit + Edited + Elements + Email + Error + Field + Find + First + Focal point + General + Groups + Group + Height + Help + Hide + History + Icon + Id + Import + Include subfolders in search + Search only this folder + Info + Inner margin + Insert + Install + Invalid + Justify + Label + Language + Last + Layout + Links + Loading + Locked + Login + Log off + Logout + Macro + Mandatory + Message + Move + Name + New + Next + No + of + Off + OK + Open + Options + On + or + Order by + Password + Path + One moment please... + Previous + Properties + Rebuild + Email to receive form data + Recycle Bin + Your recycle bin is empty + Reload + Remaining + Remove + Rename + Renew + Required + Retrieve + Retry + Permissions + Scheduled Publishing + Search + Sorry, we can not find what you are looking for. + No items have been added + Server + Settings + Show + Show page on Send + Size + Sort + Status + Submit + Success + Type + Type to search... + under + Up + Update + Upgrade + Upload + Url + User + Username + Value + View + Welcome... + Width + Yes + Folder + Search results + Reorder + I am done reordering + Preview + Change password + to + List view + Saving... + current + Embed + selected + Other + Articles + Videos + Clear + Installing + + + Blue + + + Add group + Add property + Add editor + Add template + Add child node + Add child + Edit data type + Navigate sections + Shortcuts + show shortcuts + Toggle list view + Toggle allow as root + Comment/Uncomment lines + Remove line + Copy Lines Up + Copy Lines Down + Move Lines Up + Move Lines Down + General + Editor + Toggle allow culture variants + Toggle allow segmentation + + + Background colour + Bold + Text colour + Font + Text + + + Page + + + The installer cannot connect to the database. + Could not save the web.config file. Please modify the connection string manually. + Your database has been found and is identified as + Database configuration + + install button to install the Umbraco %0% database + ]]> + + Next to proceed.]]> + Database not found! Please check that the information in the "connection string" of the "web.config" file is correct.

        +

        To proceed, please edit the "web.config" file (using Visual Studio or your favourite text editor), scroll to the bottom, add the connection string for your database in the key named "UmbracoDbDSN" and save the file.

        +

        + Click the retry button when + done.
        + More information on editing web.config here.

        ]]>
        + + Please contact your ISP if necessary. + If you're installing on a local machine or server you might need information from your system administrator.]]> + + Press the upgrade button to upgrade your database to Umbraco %0%

        +

        + Don't worry - no content will be deleted and everything will continue working afterwards! +

        + ]]>
        + Press Next to + proceed. ]]> + next to continue the configuration wizard]]> + The Default users' password needs to be changed!]]> + The Default user has been disabled or has no access to Umbraco!

        No further actions needs to be taken. Click Next to proceed.]]> + The Default user's password has been successfully changed since the installation!

        No further actions needs to be taken. Click Next to proceed.]]> + The password is changed! + Get a great start, watch our introduction videos + By clicking the next button (or modifying the umbracoConfigurationStatus in web.config), you accept the license for this software as specified in the box below. Notice that this Umbraco distribution consists of two different licenses, the open source MIT license for the framework and the Umbraco freeware license that covers the UI. + Not installed yet. + Affected files and folders + More information on setting up permissions for Umbraco here + You need to grant ASP.NET modify permissions to the following files/folders + Your permission settings are almost perfect!

        + You can run Umbraco without problems, but you will not be able to install packages which are recommended to take full advantage of Umbraco.]]>
        + How to Resolve + Click here to read the text version + video tutorial on setting up folder permissions for Umbraco or read the text version.]]> + Your permission settings might be an issue! +

        + You can run Umbraco without problems, but you will not be able to create folders or install packages which are recommended to take full advantage of Umbraco.]]>
        + Your permission settings are not ready for Umbraco! +

        + In order to run Umbraco, you'll need to update your permission settings.]]>
        + Your permission settings are perfect!

        + You are ready to run Umbraco and install packages!]]>
        + Resolving folder issue + Follow this link for more information on problems with ASP.NET and creating folders + Setting up folder permissions + + I want to start from scratch + learn how) + You can still choose to install Runway later on. Please go to the Developer section and choose Packages. + ]]> + You've just set up a clean Umbraco platform. What do you want to do next? + Runway is installed + + This is our list of recommended modules, check off the ones you would like to install, or view the full list of modules + ]]> + Only recommended for experienced users + I want to start with a simple website + + "Runway" is a simple website providing some basic document types and templates. The installer can set up Runway for you automatically, + but you can easily edit, extend or remove it. It's not necessary and you can perfectly use Umbraco without it. However, + Runway offers an easy foundation based on best practices to get you started faster than ever. + If you choose to install Runway, you can optionally select basic building blocks called Runway Modules to enhance your Runway pages. +

        + + Included with Runway: Home page, Getting Started page, Installing Modules page.
        + Optional Modules: Top Navigation, Sitemap, Contact, Gallery. +
        + ]]>
        + What is Runway + Step 1/5 Accept license + Step 2/5: Database configuration + Step 3/5: Validating File Permissions + Step 4/5: Check Umbraco security + Step 5/5: Umbraco is ready to get you started + Thank you for choosing Umbraco + Browse your new site +You installed Runway, so why not see how your new website looks.]]> + Further help and information +Get help from our award winning community, browse the documentation or watch some free videos on how to build a simple site, how to use packages and a quick guide to the Umbraco terminology]]> + Umbraco %0% is installed and ready for use + /web.config file and update the AppSetting key UmbracoConfigurationStatus in the bottom to the value of '%0%'.]]> + started instantly by clicking the "Launch Umbraco" button below.
        If you are new to Umbraco, +you can find plenty of resources on our getting started pages.]]>
        + Launch Umbraco +To manage your website, simply open the Umbraco back office and start adding content, updating the templates and stylesheets or add new functionality]]> + Connection to database failed. + Umbraco Version 3 + Umbraco Version 4 + Watch + Umbraco %0% for a fresh install or upgrading from version 3.0. +

        + Press "next" to start the wizard.]]>
        + + + Culture Code + Culture Name + + + You've been idle and logout will automatically occur in + Renew now to save your work + + + Happy super Sunday + Happy manic Monday + Happy tubular Tuesday + Happy wonderful Wednesday + Happy thunderous Thursday + Happy funky Friday + Happy Caturday + Log in below + Sign in with + Session timed out + © 2001 - %0%
        Umbraco.com

        ]]>
        + Forgotten password? + An email will be sent to the address specified with a link to reset your password + An email with password reset instructions will be sent to the specified address if it matched our records + Show password + Hide password + Return to login form + Please provide a new password + Your Password has been updated + The link you have clicked on is invalid or has expired + Umbraco: Reset Password + + + + + + + + + + + +
        + + + + + +
        + +
        + +
        +
        + + + + + + +
        +
        +
        + + + + +
        + + + + +
        +

        + Password reset requested +

        +

        + Your username to login to the Umbraco back-office is: %0% +

        +

        + + + + + + +
        + + Click this link to reset your password + +
        +

        +

        If you cannot click on the link, copy and paste this URL into your browser window:

        + + + + +
        + + %1% + +
        +

        +
        +
        +


        +
        +
        + + + ]]>
        + + + Dashboard + Sections + Content + + + Choose page above... + %0% has been copied to %1% + Select where the document %0% should be copied to below + %0% has been moved to %1% + Select where the document %0% should be moved to below + has been selected as the root of your new content, click 'ok' below. + No node selected yet, please select a node in the list above before clicking 'ok' + The current node is not allowed under the chosen node because of its type + The current node cannot be moved to one of its subpages + The current node cannot exist at the root + The action isn't allowed since you have insufficient permissions on 1 or more child documents. + Relate copied items to original + + + %0%]]> + Notification settings saved for + + The following languages have been modified %0% + + + + + + + + + + + +
        + + + + + +
        + +
        + +
        +
        + + + + + + +
        +
        +
        + + + + +
        + + + + +
        +

        + Hi %0%, +

        +

        + This is an automated mail to inform you that the task '%1%' has been performed on the page '%2%' by the user '%3%' +

        + + + + + + +
        + +
        + EDIT
        +
        +

        +

        Update summary:

        + %6% +

        +

        + Have a nice day!

        + Cheers from the Umbraco robot +

        +
        +
        +


        +
        +
        + + + ]]>
        + The following languages have been modified:

        + %0% + ]]>
        + [%0%] Notification about %1% performed on %2% + Notifications + + + Actions + Created + Create package + + button and locating the package. Umbraco packages usually have a ".umb" or ".zip" extension. + ]]> + This will delete the package + Drop to upload + Include all child nodes + or click here to choose package file + Upload package + Install a local package by selecting it from your machine. Only install packages from sources you know and trust + Upload another package + Cancel and upload another package + I accept + terms of use + + Path to file + Absolute path to file (ie: /bin/umbraco.bin) + Installed + Installed packages + Install local + Finish + This package has no configuration view + No packages have been created yet + You don’t have any packages installed + 'Packages' icon in the top right of your screen]]> + Package Actions + Author URL + Package Content + Package Files + Icon URL + Install package + License + License URL + Package Properties + Search for packages + Results for + We couldn’t find anything for + Please try searching for another package or browse through the categories + Popular + New releases + has + karma points + Information + Owner + Contributors + Created + Current version + .NET version + Downloads + Likes + Compatibility + This package is compatible with the following versions of Umbraco, as reported by community members. Full compatability cannot be guaranteed for versions reported below 100% + External sources + Author + Documentation + Package meta data + Package name + Package doesn't contain any items +
        + You can safely remove this from the system by clicking "uninstall package" below.]]>
        + Package options + Package readme + Package repository + Confirm package uninstall + Package was uninstalled + The package was successfully uninstalled + Uninstall package + + Notice: any documents, media etc depending on the items you remove, will stop working, and could lead to system instability, + so uninstall with caution. If in doubt, contact the package author.]]> + Package version + Package already installed + This package cannot be installed, it requires a minimum Umbraco version of + Uninstalling... + Downloading... + Importing... + Installing... + Restarting, please wait... + All done, your browser will now refresh, please wait... + Please click 'Finish' to complete installation and reload the page. + Uploading package... + + + Paste with full formatting (Not recommended) + The text you're trying to paste contains special characters or formatting. This could be caused by copying text from Microsoft Word. Umbraco can remove special characters or formatting automatically, so the pasted content will be more suitable for the web. + Paste as raw text without any formatting at all + Paste, but remove formatting (Recommended) + + + Group based protection + If you want to grant access to all members of specific member groups + You need to create a member group before you can use group based authentication + Error Page + Used when people are logged on, but do not have access + %0%]]> + %0% is now protected]]> + %0%]]> + Login Page + Choose the page that contains the login form + Remove protection... + %0%?]]> + Select the pages that contain login form and error messages + %0%]]> + %0%]]> + Specific members protection + If you wish to grant access to specific members + + + + + + + + + Include unpublished subpages + Publishing in progress - please wait... + %0% out of %1% pages have been published... + %0% has been published + %0% and subpages have been published + Publish %0% and all its subpages + Publish to publish %0% and thereby making its content publicly available.

        + You can publish this page and all its subpages by checking Include unpublished subpages below. + ]]>
        + + + You have not configured any approved colours + + + You can only select items of type(s): %0% + You have picked a content item currently deleted or in the recycle bin + You have picked content items currently deleted or in the recycle bin + + + Deleted item + You have picked a media item currently deleted or in the recycle bin + You have picked media items currently deleted or in the recycle bin + Trashed + + + enter external link + choose internal page + Caption + Link + Open in new window + enter the display caption + Enter the link + + + Reset crop + Save crop + Add new crop + Done + Undo edits + User defined + + Changes Created - Current version - Red text will not be shown in the selected version. , green means added]]> - Document has been rolled back + Current version + Red text will not be shown in the selected version. , green means added]]> + Document has been rolled back Select a version to compare with the current version - This displays the selected version as HTML, if you wish to see the difference between 2 versions at the same time, use the diff view - Rollback to - Select version - View - - - Edit script file - - - Concierge - Content - Courier - Developer - Forms - Help - Umbraco Configuration Wizard - Media - Members - Newsletters - Packages - Settings - Statistics - Translation - Users - - - Tours - The best Umbraco video tutorials - Visit our.umbraco.com - Visit umbraco.tv - - - Default template - To import a document type, find the ".udt" file on your computer by clicking the "Browse" button and click "Import" (you'll be asked for confirmation on the next screen) - New Tab Title - Node type - Type - Stylesheet - Script - Tab - Tab Title - Tabs - Master Content Type enabled - This Content Type uses - as a Master Content Type. Tabs from Master Content Types are not shown and can only be edited on the Master Content Type itself - No properties defined on this tab. Click on the "add a new property" link at the top to create a new property. - Create matching template - Add icon - - - Sort order - Creation date - Sorting complete. - Drag the different items up or down below to set how they should be arranged. Or click the column headers to sort the entire collection of items - - - - Validation - Validation errors must be fixed before the item can be saved - Failed - Saved - Insufficient user permissions, could not complete the operation - Cancelled - Operation was cancelled by a 3rd party add-in - Publishing was cancelled by a 3rd party add-in - Property type already exists - Property type created - DataType: %1%]]> - Propertytype deleted - Document Type saved - Tab created - Tab deleted - Tab with id: %0% deleted - Stylesheet not saved - Stylesheet saved - Stylesheet saved without any errors - Datatype saved - Dictionary item saved - Publishing failed because the parent page isn't published - Content published - and visible on the website - Content saved - Remember to publish to make changes visible - Sent For Approval - Changes have been sent for approval - Media saved - Member group saved - Media saved without any errors - Member saved - Stylesheet Property Saved - Stylesheet saved - Template saved - Error saving user (check log) - User Saved - User type saved - User group saved - Cultures and hostnames saved - Error saving cultures and hostnames - File not saved - file could not be saved. Please check file permissions - File saved - File saved without any errors - Language saved - Media Type saved - Member Type saved - Member Group saved - Template not saved - Please make sure that you do not have 2 templates with the same alias - Template saved - Template saved without any errors! - Content unpublished - Partial view saved - Partial view saved without any errors! - Partial view not saved - An error occurred saving the file. - Permissions saved for - Deleted %0% user groups - %0% was deleted - Enabled %0% users - Disabled %0% users - %0% is now enabled - %0% is now disabled - User groups have been set - Unlocked %0% users - %0% is now unlocked - Member was exported to file - An error occurred while exporting the member - User %0% was deleted - Invite user - Invitation has been re-sent to %0% - Document type was exported to file - An error occurred while exporting the document type - - - Add style - Edit style - Rich text editor styles - Define the styles that should be available in the rich text editor for this stylesheet - Edit stylesheet - Edit stylesheet property - The name displayed in the editor style selector - Preview - How the text will look like in the rich text editor. - Selector - Uses CSS syntax, e.g. "h1" or ".redHeader" - Styles - The CSS that should be applied in the rich text editor, e.g. "color:red;" - Code - Editor - - - Failed to delete template with ID %0% - Edit template - Sections - Insert content area - Insert content area placeholder - Insert - Choose what to insert into your template - Dictionary item - A dictionary item is a placeholder for a translatable piece of text, which makes it easy to create designs for multilingual websites. - Macro - - A Macro is a configurable component which is great for - reusable parts of your design, where you need the option to provide parameters, - such as galleries, forms and lists. - - Value - Displays the value of a named field from the current page, with options to modify the value or fallback to alternative values. - Partial view - - A partial view is a separate template file which can be rendered inside another - template, it's great for reusing markup or for separating complex templates into separate files. - - Master template - No master - Render child template - @RenderBody() placeholder. - ]]> - Define a named section - @section { ... }. This can be rendered in a - specific area of the parent of this template, by using @RenderSection. - ]]> - Render a named section - @RenderSection(name) placeholder. - This renders an area of a child template which is wrapped in a corresponding @section [name]{ ... } definition. - ]]> - Section Name - Section is mandatory - - If mandatory, the child template must contain a @section definition, otherwise an error is shown. - - Query builder - items returned, in - copy to clipboard - I want - all content - content of type "%0%" - from - my website - where - and - is - is not - before - before (including selected date) - after - after (including selected date) - equals - does not equal - contains - does not contain - greater than - greater than or equal to - less than - less than or equal to - Id - Name - Created Date - Last Updated Date - order by - ascending - descending - Template - - - Image - Macro - Choose type of content - Choose a layout - Add a row - Add content - Drop content - Settings applied - This content is not allowed here - This content is allowed here - Click to embed - Click to insert image - Image caption... - Write here... - Grid Layouts - Layouts are the overall work area for the grid editor, usually you only need one or two different layouts - Add Grid Layout - Adjust the layout by setting column widths and adding additional sections - Row configurations - Rows are predefined cells arranged horizontally - Add row configuration - Adjust the row by setting cell widths and adding additional cells - Columns - Total combined number of columns in the grid layout - Settings - Configure what settings editors can change - Styles - Configure what styling editors can change - Allow all editors - Allow all row configurations - Maximum items - Leave blank or set to 0 for unlimited - Set as default - Choose extra - Choose default - are added - Warning - You are deleting the row configuration - - Deleting a row configuration name will result in loss of data for any existing content that is based on this configuration. - - - - Compositions - Group - You have not added any groups - Add group - Inherited from - Add property - Required label - Enable list view - Configures the content item to show a sortable and searchable list of its children, the children will not be shown in the tree - Allowed Templates - Choose which templates editors are allowed to use on content of this type - Allow as root - Allow editors to create content of this type in the root of the content tree. - Allowed child node types - Allow content of the specified types to be created underneath content of this type. - Choose child node - Inherit tabs and properties from an existing document type. New tabs will be added to the current document type or merged if a tab with an identical name exists. - This content type is used in a composition, and therefore cannot be composed itself. - There are no content types available to use as a composition. - Removing a composition will delete all the associated property data. Once you save the document type there's no way back. - Create new - Use existing - Editor settings - Configuration - Yes, delete - was moved underneath - was copied underneath - Select the folder to move - Select the folder to copy - to in the tree structure below - All Document types - All Documents - All media items - using this document type will be deleted permanently, please confirm you want to delete these as well. - using this media type will be deleted permanently, please confirm you want to delete these as well. - using this member type will be deleted permanently, please confirm you want to delete these as well - and all documents using this type - and all media items using this type - and all members using this type - Member can edit - Allow this property value to be edited by the member on their profile page - Is sensitive data - Hide this property value from content editors that don't have access to view sensitive information - Show on member profile - Allow this property value to be displayed on the member profile page - tab has no sort order - Where is this composition used? - This composition is currently used in the composition of the following content types: - Allow variations - Allow vary by culture - Allow segmentation - Vary by culture - Vary by segments - Allow editors to create content of this type in different languages. - Allow editors to create content of different languages. - Allow editors to create segments of this content. - Allow varying by culture - Allow segmentation - Element type - Is an Element type - An Element type is meant to be used for instance in Nested Content, and not in the tree. - A document type cannot be changed to an Element type once it has been used to create one or more content items. - This is not applicable for an Element type - You have made changes to this property. Are you sure you want to discard them? - - - Add language - Mandatory language - Properties on this language have to be filled out before the node can be published. - Default language - An Umbraco site can only have one default language set. - Switching default language may result in default content missing. - Falls back to - No fall back language - To allow multi-lingual content to fall back to another language if not present in the requested language, select it here. - Fall back language - none - - - - Add parameter - Edit parameter - Enter macro name - Parameters - Define the parameters that should be available when using this macro. - Select partial view macro file - - - Building models - this can take a bit of time, don't worry - Models generated - Models could not be generated - Models generation has failed, see exception in U log - - - Add fallback field - Fallback field - Add default value - Default value - Fallback field - Default value - Casing - Encoding - Choose field - Convert line breaks - Yes, convert line breaks - Replaces line breaks with 'br' html tag - Custom Fields - Date only - Format and encoding - Format as date - Format the value as a date, or a date with time, according to the active culture - HTML encode - Will replace special characters by their HTML equivalent. - Will be inserted after the field value - Will be inserted before the field value - Lowercase - Modify output - None - Output sample - Insert after field - Insert before field - Recursive - Yes, make it recursive - Separator - Standard Fields - Uppercase - URL encode - Will format special characters in URLs - Will only be used when the field values above are empty - This field will only be used if the primary field is empty - Date and time - - - Translation details - Download XML DTD - Fields - Include subpages - - No translator users found. Please create a translator user before you start sending content to translation - The page '%0%' has been send to translation - Send the page '%0%' to translation - Total words - Translate to - Translation completed. - You can preview the pages, you've just translated, by clicking below. If the original page is found, you will get a comparison of the 2 pages. - Translation failed, the XML file might be corrupt - Translation options - Translator - Upload translation XML - - - Content - Content Templates - Media - Cache Browser - Recycle Bin - Created packages - Data Types - Dictionary - Installed packages - Install skin - Install starter kit - Languages - Install local package - Macros - Media Types - Members - Member Groups - Member Roles - Member Types - Document Types - Relation Types - Packages - Packages - Partial Views - Partial View Macro Files - Install from repository - Install Runway - Runway modules - Scripting Files - Scripts - Stylesheets - Templates - Log Viewer - Users - Settings - Templating - Third Party - - - New update ready - %0% is ready, click here for download - No connection to server - Error checking for update. Please review trace-stack for further information - - - Access - Based on the assigned groups and start nodes, the user has access to the following nodes - Assign access - Administrator - Category field - User created - Change Your Password - Change photo - New password - hasn't been locked out - The password hasn't been changed - Confirm new password - You can change your password for accessing the Umbraco Back Office by filling out the form below and click the 'Change Password' button - Content Channel - Create another user - Create new users to give them access to Umbraco. When a new user is created a password will be generated that you can share with the user. - Description field - Disable User - Document Type - Editor - Excerpt field - Failed login attempts - Go to user profile - Add groups to assign access and permissions - Invite another user - Invite new users to give them access to Umbraco. An invite email will be sent to the user with information on how to log in to Umbraco. Invites last for 72 hours. - Language - Set the language you will see in menus and dialogs - Last lockout date - Last login - Password last changed - Username - Media start node - Limit the media library to a specific start node - Media start nodes - Limit the media library to specific start nodes - Sections - Disable Umbraco Access - has not logged in yet - Old password - Password - Reset password - Your password has been changed! - Password changed - Please confirm the new password - Enter your new password - Your new password cannot be blank! - Current password - Invalid current password - There was a difference between the new password and the confirmed password. Please try again! - The confirmed password doesn't match the new password! - Replace child node permissions - You are currently modifying permissions for the pages: - Select pages to modify their permissions - Remove photo - Default permissions - Granular permissions - Set permissions for specific nodes - Profile - Search all children - Add sections to give users access - Select user groups - No start node selected - No start nodes selected - Content start node - Limit the content tree to a specific start node - Content start nodes - Limit the content tree to specific start nodes - User last updated - has been created - The new user has successfully been created. To log in to Umbraco use the password below. - User management - Name - User permissions - User group - has been invited - An invitation has been sent to the new user with details about how to log in to Umbraco. - Hello there and welcome to Umbraco! In just 1 minute you’ll be good to go, we just need you to setup a password and add a picture for your avatar. - Welcome to Umbraco! Unfortunately your invite has expired. Please contact your administrator and ask them to resend it. - Uploading a photo of yourself will make it easy for other users to recognize you. Click the circle above to upload your photo. - Writer - Change - Your profile - Your recent history - Session expires in - Invite user - Create user - Send invite - Back to users - Umbraco: Invitation - - - - - - - - - - - -
        - - - - - -
        - -
        - -
        -
        - - - - - - -
        -
        -
        - - - - -
        - - - - -
        -

        - Hi %0%, -

        -

        - You have been invited by %1% to the Umbraco Back Office. -

        -

        - Message from %1%: -
        - %2% -

        - - - - - - -
        - - - - - - -
        - - Click this link to accept the invite - -
        -
        -

        If you cannot click on the link, copy and paste this URL into your browser window:

        - - - - -
        - - %3% - -
        -

        -
        -
        -


        -
        -
        - - ]]>
        - Invite - Resending invitation... - Delete User - Are you sure you wish to delete this user account? - All - Active - Disabled - Locked out - Invited - Inactive - Name (A-Z) - Name (Z-A) - Newest - Oldest - Last login - No user groups have been added - - - Validation - No validation - Validate as an email address - Validate as a number - Validate as a URL - ...or enter a custom validation - Field is mandatory - Enter a custom validation error message (optional) - Enter a regular expression - Enter a custom validation error message (optional) - You need to add at least - You can only have - items - items selected - Invalid date - Not a number - Invalid email - Custom validation - %1% more.]]> - %1% too many.]]> - - - - Value is set to the recommended value: '%0%'. - Value was set to '%1%' for XPath '%2%' in configuration file '%3%'. - Expected value '%1%' for '%2%' in configuration file '%3%', but found '%0%'. - Found unexpected value '%0%' for '%2%' in configuration file '%3%'. - - Custom errors are set to '%0%'. - Custom errors are currently set to '%0%'. It is recommended to set this to '%1%' before go live. - Custom errors successfully set to '%0%'. - MacroErrors are set to '%0%'. - MacroErrors are set to '%0%' which will prevent some or all pages in your site from loading completely if there are any errors in macros. Rectifying this will set the value to '%1%'. - MacroErrors are now set to '%0%'. - - Try Skip IIS Custom Errors is set to '%0%' and you're using IIS version '%1%'. - Try Skip IIS Custom Errors is currently '%0%'. It is recommended to set this to '%1%' for your IIS version (%2%). - Try Skip IIS Custom Errors successfully set to '%0%'. - - File does not exist: '%0%'. - '%0%' in config file '%1%'.]]> - There was an error, check log for full error: %0%. - Database - The database schema is correct for this version of Umbraco - %0% problems were detected with your database schema (Check the log for details) - Some errors were detected while validating the database schema against the current version of Umbraco. - Your website's certificate is valid. - Certificate validation error: '%0%' - Your website's SSL certificate has expired. - Your website's SSL certificate is expiring in %0% days. - Error pinging the URL %0% - '%1%' - You are currently %0% viewing the site using the HTTPS scheme. - The appSetting 'Umbraco.Core.UseHttps' is set to 'false' in your web.config file. Once you access this site using the HTTPS scheme, that should be set to 'true'. - The appSetting 'Umbraco.Core.UseHttps' is set to '%0%' in your web.config file, your cookies are %1% marked as secure. - Could not update the 'Umbraco.Core.UseHttps' setting in your web.config file. Error: %0% - - Enable HTTPS - Sets umbracoSSL setting to true in the appSettings of the web.config file. - The appSetting 'Umbraco.Core.UseHttps' is now set to 'true' in your web.config file, your cookies will be marked as secure. - Fix - Cannot fix a check with a value comparison type of 'ShouldNotEqual'. - Cannot fix a check with a value comparison type of 'ShouldEqual' with a provided value. - Value to fix check not provided. - Debug compilation mode is disabled. - Debug compilation mode is currently enabled. It is recommended to disable this setting before go live. - Debug compilation mode successfully disabled. - Trace mode is disabled. - Trace mode is currently enabled. It is recommended to disable this setting before go live. - Trace mode successfully disabled. - All folders have the correct permissions set. - - %0%.]]> - %0%. If they aren't being written to no action need be taken.]]> - All files have the correct permissions set. - - %0%.]]> - %0%. If they aren't being written to no action need be taken.]]> - X-Frame-Options used to control whether a site can be IFRAMEd by another was found.]]> - X-Frame-Options used to control whether a site can be IFRAMEd by another was not found.]]> - Set Header in Config - Adds a value to the httpProtocol/customHeaders section of web.config to prevent the site being IFRAMEd by other websites. - A setting to create a header preventing IFRAMEing of the site by other websites has been added to your web.config file. - Could not update web.config file. Error: %0% - X-Content-Type-Options used to protect against MIME sniffing vulnerabilities was found.]]> - X-Content-Type-Options used to protect against MIME sniffing vulnerabilities was not found.]]> - Adds a value to the httpProtocol/customHeaders section of web.config to protect against MIME sniffing vulnerabilities. - A setting to create a header protecting against MIME sniffing vulnerabilities has been added to your web.config file. - Strict-Transport-Security, also known as the HSTS-header, was found.]]> - Strict-Transport-Security was not found.]]> - Adds the header 'Strict-Transport-Security' with the value 'max-age=10886400' to the httpProtocol/customHeaders section of web.config. Use this fix only if you will have your domains running with https for the next 18 weeks (minimum). - The HSTS header has been added to your web.config file. - X-XSS-Protection was found.]]> - X-XSS-Protection was not found.]]> - Adds the header 'X-XSS-Protection' with the value '1; mode=block' to the httpProtocol/customHeaders section of web.config. - The X-XSS-Protection header has been added to your web.config file. - - %0%.]]> - No headers revealing information about the website technology were found. - In the Web.config file, system.net/mailsettings could not be found. - In the Web.config file system.net/mailsettings section, the host is not configured. - SMTP settings are configured correctly and the service is operating as expected. - The SMTP server configured with host '%0%' and port '%1%' could not be reached. Please check to ensure the SMTP settings in the Web.config file system.net/mailsettings are correct. - %0%.]]> - %0%.]]> -

        Results of the scheduled Umbraco Health Checks run on %0% at %1% are as follows:

        %2%]]>
        - Umbraco Health Check Status: %0% - Check All Groups - Check group - - The health checker evaluates various areas of your site for best practice settings, configuration, potential problems, etc. You can easily fix problems by pressing a button. - You can add your own health checks, have a look at the documentation for more information about custom health checks.

        - ]]> -
        - - - Disable URL tracker - Enable URL tracker - Original URL - Redirected To - Redirect Url Management - The following URLs redirect to this content item: - No redirects have been made - When a published page gets renamed or moved a redirect will automatically be made to the new page. - Are you sure you want to remove the redirect from '%0%' to '%1%'? - Redirect URL removed. - Error removing redirect URL. - This will remove the redirect - Are you sure you want to disable the URL tracker? - URL tracker has now been disabled. - Error disabling the URL tracker, more information can be found in your log file. - URL tracker has now been enabled. - Error enabling the URL tracker, more information can be found in your log file. - - - No Dictionary items to choose from - - - %0% characters left.]]> - %1% too many.]]> - - - Trashed content with Id: {0} related to original parent content with Id: {1} - Trashed media with Id: {0} related to original parent media item with Id: {1} - Cannot automatically restore this item - There is no location where this item can be automatically restored. You can move the item manually using the tree below. - was restored under - - - Direction - Parent to child - Bidirectional - Parent - Child - Count - Relations - Created - Comment - Name - No relations for this relation type. - Relation Type - Relations - - - Getting Started - Redirect URL Management - Content - Welcome - Examine Management - Published Status - Models Builder - Health Check - Profiling - Getting Started - Install Umbraco Forms - - - Go back - Active layout: - Jump to - group - passed - warning - failed - suggestion - Check passed - Check failed - Open backoffice search - Open/Close backoffice help - Open/Close your profile options - Setup Culture and Hostnames for %0% - Create new node under %0% - Setup Public access on %0% - Setup Permissions on %0% - Change sort order for %0% - Create Content Template based on %0% - Open context menu for - Current language - Switch language to - Create new folder - Partial View - Partial View Macro - Member - Data type - Search the redirect dashboard - Search the user group section - Search the users section - Create item - Create - Edit - Name - Add new row - View more options - - - References - This Data Type has no references. - Used in Document Types - No references to Document Types. - Used in Media Types - No references to Media Types. - Used in Member Types - No references to Member Types. - Used by - Used in Documents - Used in Members - Used in Media - - - Log Levels - Saved Searches - Total Items - Timestamp - Level - Machine - Message - Exception - Properties - Search With Google - Search this message with Google - Search With Bing - Search this message with Bing - Search Our Umbraco - Search this message on Our Umbraco forums and docs - Search Our Umbraco with Google - Search Our Umbraco forums using Google - Search Umbraco Source - Search within Umbraco source code on Github - Search Umbraco Issues - Search Umbraco Issues on Github - Delete this search - Find Logs with Request ID - Find Logs with Namespace - Find Logs with Machine Name - Open - - - Copy %0% - %0% from %1% - Remove all items - Clear clipboard - - - Open Property Actions - - - Wait - Refresh status - Memory Cache - - - - Reload - Database Cache - - Rebuilding can be expensive. - Use it when reloading is not enough, and you think that the database cache has not been - properly generated—which would indicate some critical Umbraco issue. - ]]> - - Rebuild - Internals - - not need to use it. - ]]> - - Collect - Published Cache Status - Caches - - - Performance profiling - - - Umbraco currently runs in debug mode. This means you can use the built-in performance profiler to assess the performance when rendering pages. -

        -

        - If you want to activate the profiler for a specific page rendering, simply add umbDebug=true to the querystring when requesting the page. -

        -

        - If you want the profiler to be activated by default for all page renderings, you can use the toggle below. - It will set a cookie in your browser, which then activates the profiler automatically. - In other words, the profiler will only be active by default in your browser - not everyone else's. -

        - ]]> -
        - Activate the profiler by default - Friendly reminder - - - You should never let a production site run in debug mode. Debug mode is turned off by setting debug="false" on the <compilation /> element in web.config. -

        - ]]> -
        - - - Umbraco currently does not run in debug mode, so you can't use the built-in profiler. This is how it should be for a production site. -

        -

        - Debug mode is turned on by setting debug="true" on the <compilation /> element in web.config. -

        - ]]> -
        - - - Hours of Umbraco training videos are only a click away - - Want to master Umbraco? Spend a couple of minutes learning some best practices by watching one of these videos about using Umbraco. And visit umbraco.tv for even more Umbraco videos

        - ]]> -
        - To get you started - - - Start here - This section contains the building blocks for your Umbraco site. Follow the below links to find out more about working with the items in the Settings section - Find out more - - in the Documentation section of Our Umbraco - ]]> - - - Community Forum - ]]> - - - tutorial videos (some are free, some require a subscription) - ]]> - - - productivity boosting tools and commercial support - ]]> - - - training and certification opportunities - ]]> - - - - Welcome to The Friendly CMS - Thank you for choosing Umbraco - we think this could be the beginning of something beautiful. While it may feel overwhelming at first, we've done a lot to make the learning curve as smooth and fast as possible. - - - Umbraco Forms - Create forms using an intuitive drag and drop interface. From simple contact forms that sends e-mails to advanced questionaires that integrate with CRM systems. Your clients will love it! - - - Create new block - Attach a settings section - Select view - Select stylesheet - Choose thumbnail - Create new - Custom stylesheet - Add stylesheet - Editor apperance - Data models - Catalogue appearance - Background color - Icon color - Content model - Label - Custom view - Settings model - Overlay editor size - Add custom view - Add settings - Overwrite label template - %0%.]]> - Content using this block will be lost. - - Thumbnail - Add thumbnail - Create empty - Clipboard - Settings - - - What are Content Templates? - Content Templates are pre-defined content that can be selected when creating a new content node. - How do I create a Content Template? - - There are two ways to create a Content Template:

        -
          -
        • Right-click a content node and select "Create Content Template" to create a new Content Template.
        • -
        • Right-click the Content Templates tree in the Settings section and select the Document Type you want to create a Content Template for.
        • -
        -

        Once given a name, editors can start using the Content Template as a foundation for their new page.

        - ]]> -
        - How do I manage Content Templates? - You can edit and delete Content Templates from the "Content Templates" tree in the Settings section. Expand the Document Type which the Content Template is based on and click it to edit or delete it. - -
        + This displays the selected version as HTML, if you wish to see the difference between 2 versions at the same time, use the diff view + Rollback to + Select version + View + + + Edit script file + + + Concierge + Content + Courier + Developer + Forms + Help + Umbraco Configuration Wizard + Media + Members + Newsletters + Packages + Settings + Statistics + Translation + Users + + + Tours + The best Umbraco video tutorials + Visit our.umbraco.com + Visit umbraco.tv + + + Default template + To import a document type, find the ".udt" file on your computer by clicking the "Browse" button and click "Import" (you'll be asked for confirmation on the next screen) + New Tab Title + Node type + Type + Stylesheet + Script + Tab + Tab Title + Tabs + Master Content Type enabled + This Content Type uses + as a Master Content Type. Tabs from Master Content Types are not shown and can only be edited on the Master Content Type itself + No properties defined on this tab. Click on the "add a new property" link at the top to create a new property. + Create matching template + Add icon + + + Sort order + Creation date + Sorting complete. + Drag the different items up or down below to set how they should be arranged. Or click the column headers to sort the entire collection of items + + + + Validation + Validation errors must be fixed before the item can be saved + Failed + Saved + Insufficient user permissions, could not complete the operation + Cancelled + Operation was cancelled by a 3rd party add-in + Publishing was cancelled by a 3rd party add-in + Property type already exists + Property type created + DataType: %1%]]> + Propertytype deleted + Document Type saved + Tab created + Tab deleted + Tab with id: %0% deleted + Stylesheet not saved + Stylesheet saved + Stylesheet saved without any errors + Datatype saved + Dictionary item saved + Publishing failed because the parent page isn't published + Content published + and visible on the website + Content saved + Remember to publish to make changes visible + Sent For Approval + Changes have been sent for approval + Media saved + Member group saved + Media saved without any errors + Member saved + Stylesheet Property Saved + Stylesheet saved + Template saved + Error saving user (check log) + User Saved + User type saved + User group saved + Cultures and hostnames saved + Error saving cultures and hostnames + File not saved + file could not be saved. Please check file permissions + File saved + File saved without any errors + Language saved + Media Type saved + Member Type saved + Member Group saved + Template not saved + Please make sure that you do not have 2 templates with the same alias + Template saved + Template saved without any errors! + Content unpublished + Partial view saved + Partial view saved without any errors! + Partial view not saved + An error occurred saving the file. + Permissions saved for + Deleted %0% user groups + %0% was deleted + Enabled %0% users + Disabled %0% users + %0% is now enabled + %0% is now disabled + User groups have been set + Unlocked %0% users + %0% is now unlocked + Member was exported to file + An error occurred while exporting the member + User %0% was deleted + Invite user + Invitation has been re-sent to %0% + Document type was exported to file + An error occurred while exporting the document type + + + Add style + Edit style + Rich text editor styles + Define the styles that should be available in the rich text editor for this stylesheet + Edit stylesheet + Edit stylesheet property + The name displayed in the editor style selector + Preview + How the text will look like in the rich text editor. + Selector + Uses CSS syntax, e.g. "h1" or ".redHeader" + Styles + The CSS that should be applied in the rich text editor, e.g. "color:red;" + Code + Editor + + + Failed to delete template with ID %0% + Edit template + Sections + Insert content area + Insert content area placeholder + Insert + Choose what to insert into your template + Dictionary item + A dictionary item is a placeholder for a translatable piece of text, which makes it easy to create designs for multilingual websites. + Macro + + A Macro is a configurable component which is great for + reusable parts of your design, where you need the option to provide parameters, + such as galleries, forms and lists. + + Value + Displays the value of a named field from the current page, with options to modify the value or fallback to alternative values. + Partial view + + A partial view is a separate template file which can be rendered inside another + template, it's great for reusing markup or for separating complex templates into separate files. + + Master template + No master + Render child template + @RenderBody() placeholder. + ]]> + Define a named section + @section { ... }. This can be rendered in a + specific area of the parent of this template, by using @RenderSection. + ]]> + Render a named section + @RenderSection(name) placeholder. + This renders an area of a child template which is wrapped in a corresponding @section [name]{ ... } definition. + ]]> + Section Name + Section is mandatory + + If mandatory, the child template must contain a @section definition, otherwise an error is shown. + + Query builder + items returned, in + copy to clipboard + I want + all content + content of type "%0%" + from + my website + where + and + is + is not + before + before (including selected date) + after + after (including selected date) + equals + does not equal + contains + does not contain + greater than + greater than or equal to + less than + less than or equal to + Id + Name + Created Date + Last Updated Date + order by + ascending + descending + Template + + + Image + Macro + Choose type of content + Choose a layout + Add a row + Add content + Drop content + Settings applied + This content is not allowed here + This content is allowed here + Click to embed + Click to insert image + Image caption... + Write here... + Grid Layouts + Layouts are the overall work area for the grid editor, usually you only need one or two different layouts + Add Grid Layout + Adjust the layout by setting column widths and adding additional sections + Row configurations + Rows are predefined cells arranged horizontally + Add row configuration + Adjust the row by setting cell widths and adding additional cells + Columns + Total combined number of columns in the grid layout + Settings + Configure what settings editors can change + Styles + Configure what styling editors can change + Allow all editors + Allow all row configurations + Maximum items + Leave blank or set to 0 for unlimited + Set as default + Choose extra + Choose default + are added + Warning + You are deleting the row configuration + + Deleting a row configuration name will result in loss of data for any existing content that is based on this configuration. + + + + Compositions + Group + You have not added any groups + Add group + Inherited from + Add property + Required label + Enable list view + Configures the content item to show a sortable and searchable list of its children, the children will not be shown in the tree + Allowed Templates + Choose which templates editors are allowed to use on content of this type + Allow as root + Allow editors to create content of this type in the root of the content tree. + Allowed child node types + Allow content of the specified types to be created underneath content of this type. + Choose child node + Inherit tabs and properties from an existing document type. New tabs will be added to the current document type or merged if a tab with an identical name exists. + This content type is used in a composition, and therefore cannot be composed itself. + There are no content types available to use as a composition. + Removing a composition will delete all the associated property data. Once you save the document type there's no way back. + Create new + Use existing + Editor settings + Configuration + Yes, delete + was moved underneath + was copied underneath + Select the folder to move + Select the folder to copy + to in the tree structure below + All Document types + All Documents + All media items + using this document type will be deleted permanently, please confirm you want to delete these as well. + using this media type will be deleted permanently, please confirm you want to delete these as well. + using this member type will be deleted permanently, please confirm you want to delete these as well + and all documents using this type + and all media items using this type + and all members using this type + Member can edit + Allow this property value to be edited by the member on their profile page + Is sensitive data + Hide this property value from content editors that don't have access to view sensitive information + Show on member profile + Allow this property value to be displayed on the member profile page + tab has no sort order + Where is this composition used? + This composition is currently used in the composition of the following content types: + Allow variations + Allow vary by culture + Allow segmentation + Vary by culture + Vary by segments + Allow editors to create content of this type in different languages. + Allow editors to create content of different languages. + Allow editors to create segments of this content. + Allow varying by culture + Allow segmentation + Element type + Is an Element type + An Element type is meant to be used for instance in Nested Content, and not in the tree. + A document type cannot be changed to an Element type once it has been used to create one or more content items. + This is not applicable for an Element type + You have made changes to this property. Are you sure you want to discard them? + + + Add language + Mandatory language + Properties on this language have to be filled out before the node can be published. + Default language + An Umbraco site can only have one default language set. + Switching default language may result in default content missing. + Falls back to + No fall back language + To allow multi-lingual content to fall back to another language if not present in the requested language, select it here. + Fall back language + none + + + + Add parameter + Edit parameter + Enter macro name + Parameters + Define the parameters that should be available when using this macro. + Select partial view macro file + + + Building models + this can take a bit of time, don't worry + Models generated + Models could not be generated + Models generation has failed, see exception in U log + + + Add fallback field + Fallback field + Add default value + Default value + Fallback field + Default value + Casing + Encoding + Choose field + Convert line breaks + Yes, convert line breaks + Replaces line breaks with 'br' html tag + Custom Fields + Date only + Format and encoding + Format as date + Format the value as a date, or a date with time, according to the active culture + HTML encode + Will replace special characters by their HTML equivalent. + Will be inserted after the field value + Will be inserted before the field value + Lowercase + Modify output + None + Output sample + Insert after field + Insert before field + Recursive + Yes, make it recursive + Separator + Standard Fields + Uppercase + URL encode + Will format special characters in URLs + Will only be used when the field values above are empty + This field will only be used if the primary field is empty + Date and time + + + Translation details + Download XML DTD + Fields + Include subpages + + No translator users found. Please create a translator user before you start sending content to translation + The page '%0%' has been send to translation + Send the page '%0%' to translation + Total words + Translate to + Translation completed. + You can preview the pages, you've just translated, by clicking below. If the original page is found, you will get a comparison of the 2 pages. + Translation failed, the XML file might be corrupt + Translation options + Translator + Upload translation XML + + + Content + Content Templates + Media + Cache Browser + Recycle Bin + Created packages + Data Types + Dictionary + Installed packages + Install skin + Install starter kit + Languages + Install local package + Macros + Media Types + Members + Member Groups + Member Roles + Member Types + Document Types + Relation Types + Packages + Packages + Partial Views + Partial View Macro Files + Install from repository + Install Runway + Runway modules + Scripting Files + Scripts + Stylesheets + Templates + Log Viewer + Users + Settings + Templating + Third Party + + + New update ready + %0% is ready, click here for download + No connection to server + Error checking for update. Please review trace-stack for further information + + + Access + Based on the assigned groups and start nodes, the user has access to the following nodes + Assign access + Administrator + Category field + User created + Change Your Password + Change photo + New password + hasn't been locked out + The password hasn't been changed + Confirm new password + You can change your password for accessing the Umbraco Back Office by filling out the form below and click the 'Change Password' button + Content Channel + Create another user + Create new users to give them access to Umbraco. When a new user is created a password will be generated that you can share with the user. + Description field + Disable User + Document Type + Editor + Excerpt field + Failed login attempts + Go to user profile + Add groups to assign access and permissions + Invite another user + Invite new users to give them access to Umbraco. An invite email will be sent to the user with information on how to log in to Umbraco. Invites last for 72 hours. + Language + Set the language you will see in menus and dialogs + Last lockout date + Last login + Password last changed + Username + Media start node + Limit the media library to a specific start node + Media start nodes + Limit the media library to specific start nodes + Sections + Disable Umbraco Access + has not logged in yet + Old password + Password + Reset password + Your password has been changed! + Password changed + Please confirm the new password + Enter your new password + Your new password cannot be blank! + Current password + Invalid current password + There was a difference between the new password and the confirmed password. Please try again! + The confirmed password doesn't match the new password! + Replace child node permissions + You are currently modifying permissions for the pages: + Select pages to modify their permissions + Remove photo + Default permissions + Granular permissions + Set permissions for specific nodes + Profile + Search all children + Add sections to give users access + Select user groups + No start node selected + No start nodes selected + Content start node + Limit the content tree to a specific start node + Content start nodes + Limit the content tree to specific start nodes + User last updated + has been created + The new user has successfully been created. To log in to Umbraco use the password below. + User management + Name + User permissions + User group + has been invited + An invitation has been sent to the new user with details about how to log in to Umbraco. + Hello there and welcome to Umbraco! In just 1 minute you’ll be good to go, we just need you to setup a password and add a picture for your avatar. + Welcome to Umbraco! Unfortunately your invite has expired. Please contact your administrator and ask them to resend it. + Uploading a photo of yourself will make it easy for other users to recognize you. Click the circle above to upload your photo. + Writer + Change + Your profile + Your recent history + Session expires in + Invite user + Create user + Send invite + Back to users + Umbraco: Invitation + + + + + + + + + + + +
        + + + + + +
        + +
        + +
        +
        + + + + + + +
        +
        +
        + + + + +
        + + + + +
        +

        + Hi %0%, +

        +

        + You have been invited by %1% to the Umbraco Back Office. +

        +

        + Message from %1%: +
        + %2% +

        + + + + + + +
        + + + + + + +
        + + Click this link to accept the invite + +
        +
        +

        If you cannot click on the link, copy and paste this URL into your browser window:

        + + + + +
        + + %3% + +
        +

        +
        +
        +


        +
        +
        + + ]]>
        + Invite + Resending invitation... + Delete User + Are you sure you wish to delete this user account? + All + Active + Disabled + Locked out + Invited + Inactive + Name (A-Z) + Name (Z-A) + Newest + Oldest + Last login + No user groups have been added + + + Validation + No validation + Validate as an email address + Validate as a number + Validate as a URL + ...or enter a custom validation + Field is mandatory + Enter a custom validation error message (optional) + Enter a regular expression + Enter a custom validation error message (optional) + You need to add at least + You can only have + items + items selected + Invalid date + Not a number + Invalid email + Custom validation + %1% more.]]> + %1% too many.]]> + + + + Value is set to the recommended value: '%0%'. + Value was set to '%1%' for XPath '%2%' in configuration file '%3%'. + Expected value '%1%' for '%2%' in configuration file '%3%', but found '%0%'. + Found unexpected value '%0%' for '%2%' in configuration file '%3%'. + + Custom errors are set to '%0%'. + Custom errors are currently set to '%0%'. It is recommended to set this to '%1%' before go live. + Custom errors successfully set to '%0%'. + MacroErrors are set to '%0%'. + MacroErrors are set to '%0%' which will prevent some or all pages in your site from loading completely if there are any errors in macros. Rectifying this will set the value to '%1%'. + MacroErrors are now set to '%0%'. + + Try Skip IIS Custom Errors is set to '%0%' and you're using IIS version '%1%'. + Try Skip IIS Custom Errors is currently '%0%'. It is recommended to set this to '%1%' for your IIS version (%2%). + Try Skip IIS Custom Errors successfully set to '%0%'. + + File does not exist: '%0%'. + '%0%' in config file '%1%'.]]> + There was an error, check log for full error: %0%. + Database - The database schema is correct for this version of Umbraco + %0% problems were detected with your database schema (Check the log for details) + Some errors were detected while validating the database schema against the current version of Umbraco. + Your website's certificate is valid. + Certificate validation error: '%0%' + Your website's SSL certificate has expired. + Your website's SSL certificate is expiring in %0% days. + Error pinging the URL %0% - '%1%' + You are currently %0% viewing the site using the HTTPS scheme. + The appSetting 'Umbraco.Core.UseHttps' is set to 'false' in your web.config file. Once you access this site using the HTTPS scheme, that should be set to 'true'. + The appSetting 'Umbraco.Core.UseHttps' is set to '%0%' in your web.config file, your cookies are %1% marked as secure. + Could not update the 'Umbraco.Core.UseHttps' setting in your web.config file. Error: %0% + + Enable HTTPS + Sets umbracoSSL setting to true in the appSettings of the web.config file. + The appSetting 'Umbraco.Core.UseHttps' is now set to 'true' in your web.config file, your cookies will be marked as secure. + Fix + Cannot fix a check with a value comparison type of 'ShouldNotEqual'. + Cannot fix a check with a value comparison type of 'ShouldEqual' with a provided value. + Value to fix check not provided. + Debug compilation mode is disabled. + Debug compilation mode is currently enabled. It is recommended to disable this setting before go live. + Debug compilation mode successfully disabled. + Trace mode is disabled. + Trace mode is currently enabled. It is recommended to disable this setting before go live. + Trace mode successfully disabled. + All folders have the correct permissions set. + + %0%.]]> + %0%. If they aren't being written to no action need be taken.]]> + All files have the correct permissions set. + + %0%.]]> + %0%. If they aren't being written to no action need be taken.]]> + X-Frame-Options used to control whether a site can be IFRAMEd by another was found.]]> + X-Frame-Options used to control whether a site can be IFRAMEd by another was not found.]]> + Set Header in Config + Adds a value to the httpProtocol/customHeaders section of web.config to prevent the site being IFRAMEd by other websites. + A setting to create a header preventing IFRAMEing of the site by other websites has been added to your web.config file. + Could not update web.config file. Error: %0% + X-Content-Type-Options used to protect against MIME sniffing vulnerabilities was found.]]> + X-Content-Type-Options used to protect against MIME sniffing vulnerabilities was not found.]]> + Adds a value to the httpProtocol/customHeaders section of web.config to protect against MIME sniffing vulnerabilities. + A setting to create a header protecting against MIME sniffing vulnerabilities has been added to your web.config file. + Strict-Transport-Security, also known as the HSTS-header, was found.]]> + Strict-Transport-Security was not found.]]> + Adds the header 'Strict-Transport-Security' with the value 'max-age=10886400' to the httpProtocol/customHeaders section of web.config. Use this fix only if you will have your domains running with https for the next 18 weeks (minimum). + The HSTS header has been added to your web.config file. + X-XSS-Protection was found.]]> + X-XSS-Protection was not found.]]> + Adds the header 'X-XSS-Protection' with the value '1; mode=block' to the httpProtocol/customHeaders section of web.config. + The X-XSS-Protection header has been added to your web.config file. + + %0%.]]> + No headers revealing information about the website technology were found. + In the Web.config file, system.net/mailsettings could not be found. + In the Web.config file system.net/mailsettings section, the host is not configured. + SMTP settings are configured correctly and the service is operating as expected. + The SMTP server configured with host '%0%' and port '%1%' could not be reached. Please check to ensure the SMTP settings in the Web.config file system.net/mailsettings are correct. + %0%.]]> + %0%.]]> +

        Results of the scheduled Umbraco Health Checks run on %0% at %1% are as follows:

        %2%]]>
        + Umbraco Health Check Status: %0% + Check All Groups + Check group + + The health checker evaluates various areas of your site for best practice settings, configuration, potential problems, etc. You can easily fix problems by pressing a button. + You can add your own health checks, have a look at the documentation for more information about custom health checks.

        + ]]> +
        + + + Disable URL tracker + Enable URL tracker + Original URL + Redirected To + Redirect Url Management + The following URLs redirect to this content item: + No redirects have been made + When a published page gets renamed or moved a redirect will automatically be made to the new page. + Are you sure you want to remove the redirect from '%0%' to '%1%'? + Redirect URL removed. + Error removing redirect URL. + This will remove the redirect + Are you sure you want to disable the URL tracker? + URL tracker has now been disabled. + Error disabling the URL tracker, more information can be found in your log file. + URL tracker has now been enabled. + Error enabling the URL tracker, more information can be found in your log file. + + + No Dictionary items to choose from + + + %0% characters left.]]> + %1% too many.]]> + + + Trashed content with Id: {0} related to original parent content with Id: {1} + Trashed media with Id: {0} related to original parent media item with Id: {1} + Cannot automatically restore this item + There is no location where this item can be automatically restored. You can move the item manually using the tree below. + was restored under + + + Direction + Parent to child + Bidirectional + Parent + Child + Count + Relations + Created + Comment + Name + No relations for this relation type. + Relation Type + Relations + + + Getting Started + Redirect URL Management + Content + Welcome + Examine Management + Published Status + Models Builder + Health Check + Profiling + Getting Started + Install Umbraco Forms + + + Go back + Active layout: + Jump to + group + passed + warning + failed + suggestion + Check passed + Check failed + Open backoffice search + Open/Close backoffice help + Open/Close your profile options + Setup Culture and Hostnames for %0% + Create new node under %0% + Setup Public access on %0% + Setup Permissions on %0% + Change sort order for %0% + Create Content Template based on %0% + Open context menu for + Current language + Switch language to + Create new folder + Partial View + Partial View Macro + Member + Data type + Search the redirect dashboard + Search the user group section + Search the users section + Create item + Create + Edit + Name + Add new row + View more options + + + References + This Data Type has no references. + Used in Document Types + No references to Document Types. + Used in Media Types + No references to Media Types. + Used in Member Types + No references to Member Types. + Used by + Used in Documents + Used in Members + Used in Media + + + Log Levels + Saved Searches + Total Items + Timestamp + Level + Machine + Message + Exception + Properties + Search With Google + Search this message with Google + Search With Bing + Search this message with Bing + Search Our Umbraco + Search this message on Our Umbraco forums and docs + Search Our Umbraco with Google + Search Our Umbraco forums using Google + Search Umbraco Source + Search within Umbraco source code on Github + Search Umbraco Issues + Search Umbraco Issues on Github + Delete this search + Find Logs with Request ID + Find Logs with Namespace + Find Logs with Machine Name + Open + + + Copy %0% + %0% from %1% + Remove all items + Clear clipboard + + + Open Property Actions + + + Wait + Refresh status + Memory Cache + + + + Reload + Database Cache + + Rebuilding can be expensive. + Use it when reloading is not enough, and you think that the database cache has not been + properly generated—which would indicate some critical Umbraco issue. + ]]> + + Rebuild + Internals + + not need to use it. + ]]> + + Collect + Published Cache Status + Caches + + + Performance profiling + + + Umbraco currently runs in debug mode. This means you can use the built-in performance profiler to assess the performance when rendering pages. +

        +

        + If you want to activate the profiler for a specific page rendering, simply add umbDebug=true to the querystring when requesting the page. +

        +

        + If you want the profiler to be activated by default for all page renderings, you can use the toggle below. + It will set a cookie in your browser, which then activates the profiler automatically. + In other words, the profiler will only be active by default in your browser - not everyone else's. +

        + ]]> +
        + Activate the profiler by default + Friendly reminder + + + You should never let a production site run in debug mode. Debug mode is turned off by setting debug="false" on the <compilation /> element in web.config. +

        + ]]> +
        + + + Umbraco currently does not run in debug mode, so you can't use the built-in profiler. This is how it should be for a production site. +

        +

        + Debug mode is turned on by setting debug="true" on the <compilation /> element in web.config. +

        + ]]> +
        + + + Hours of Umbraco training videos are only a click away + + Want to master Umbraco? Spend a couple of minutes learning some best practices by watching one of these videos about using Umbraco. And visit umbraco.tv for even more Umbraco videos

        + ]]> +
        + To get you started + + + Start here + This section contains the building blocks for your Umbraco site. Follow the below links to find out more about working with the items in the Settings section + Find out more + + in the Documentation section of Our Umbraco + ]]> + + + Community Forum + ]]> + + + tutorial videos (some are free, some require a subscription) + ]]> + + + productivity boosting tools and commercial support + ]]> + + + training and certification opportunities + ]]> + + + + Welcome to The Friendly CMS + Thank you for choosing Umbraco - we think this could be the beginning of something beautiful. While it may feel overwhelming at first, we've done a lot to make the learning curve as smooth and fast as possible. + + + Umbraco Forms + Create forms using an intuitive drag and drop interface. From simple contact forms that sends e-mails to advanced questionaires that integrate with CRM systems. Your clients will love it! + + + Create new block + Attach a settings section + Select view + Select stylesheet + Choose thumbnail + Create new + Custom stylesheet + Add stylesheet + Editor apperance + Data models + Catalogue appearance + Background color + Icon color + Content model + Label + Custom view + Settings model + Overlay editor size + Add custom view + Add settings + Overwrite label template + %0%.]]> + Content using this block will be lost. + + Thumbnail + Add thumbnail + Create empty + Clipboard + Settings + Advanced + Force hide content editor + + + What are Content Templates? + Content Templates are pre-defined content that can be selected when creating a new content node. + How do I create a Content Template? + + There are two ways to create a Content Template:

        +
          +
        • Right-click a content node and select "Create Content Template" to create a new Content Template.
        • +
        • Right-click the Content Templates tree in the Settings section and select the Document Type you want to create a Content Template for.
        • +
        +

        Once given a name, editors can start using the Content Template as a foundation for their new page.

        + ]]> +
        + How do I manage Content Templates? + You can edit and delete Content Templates from the "Content Templates" tree in the Settings section. Expand the Document Type which the Content Template is based on and click it to edit or delete it. + +
        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 93b73f25b4..fccd11a5aa 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml @@ -2485,6 +2485,8 @@ To manage your website, simply open the Umbraco back office and start adding con Create empty Clipboard Settings + Advanced + Force hide content editor What are Content Templates? diff --git a/src/Umbraco.Web/PropertyEditors/BlockListConfiguration.cs b/src/Umbraco.Web/PropertyEditors/BlockListConfiguration.cs index 4ff17c31f3..0ea48512d1 100644 --- a/src/Umbraco.Web/PropertyEditors/BlockListConfiguration.cs +++ b/src/Umbraco.Web/PropertyEditors/BlockListConfiguration.cs @@ -44,6 +44,9 @@ namespace Umbraco.Web.PropertyEditors [JsonProperty("editorSize")] public string EditorSize { get; set; } + + [JsonProperty("forceHideContentEditorInOverlay")] + public bool ForceHideContentEditorInOverlay { get; set; } } [ConfigurationField("validationLimit", "Amount", "numberrange", Description = "Set a required range of blocks")] From 8f7194df368d87d61da60892af4f8b67eaa0cd7c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Wed, 1 Jul 2020 10:30:20 +0200 Subject: [PATCH 285/377] remove line --- .../blocklist/umbBlockListPropertyEditor.component.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) 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 346c79cff7..a15deb72fb 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 @@ -237,8 +237,7 @@ var wasNotActiveBefore = blockObject.active !== true; activateBlock(blockObject); - - if (inlineEditing === true && openSettings !== true) { // dont open the editor overlay if block has hidden its content editor in overlays and we are requesting to open content, not settings. + // dont open the editor overlay if block has hidden its content editor in overlays and we are requesting to open content, not settings. if (openSettings !== true && blockObject.hideContentInOverlay === true) { return; } From d2678135feb01abe4f1c4ede236bbbc132a30cd3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Wed, 1 Jul 2020 10:32:20 +0200 Subject: [PATCH 286/377] bail out if we dont have settings. --- .../blocklist/umbBlockListPropertyEditor.component.js | 5 +++++ 1 file changed, 5 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 a15deb72fb..e817d9be0e 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 @@ -242,6 +242,11 @@ return; } + // if requesting to open settings but we dont have settings then return. + if (openSettings === true && blockObject.config.settingsElementTypeKey) { + return; + } + // make a clone to avoid editing model directly. var blockContentClone = Utilities.copy(blockObject.content); var blockSettingsClone = null; From 22aa76d6b17786ef0282ce49df12e220214b0f97 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Wed, 1 Jul 2020 10:33:13 +0200 Subject: [PATCH 287/377] remove unused line --- .../blocklist/umbBlockListPropertyEditor.component.js | 2 -- 1 file changed, 2 deletions(-) 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 e817d9be0e..66c6d92d28 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 @@ -254,8 +254,6 @@ if (blockObject.config.settingsElementTypeKey) { blockSettingsClone = Utilities.copy(blockObject.settings); } - - var hideContent = (openSettings === true && inlineEditing === true); var blockEditorModel = { hideContent: blockObject.hideContentInOverlay, From 69c2ab20ff0b2a75668c4d4f7142de71b56ebdcb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Wed, 1 Jul 2020 11:45:45 +0200 Subject: [PATCH 288/377] correct behaviour --- .../blocklist/umbBlockListPropertyEditor.component.js | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) 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 66c6d92d28..4ae0a90390 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 @@ -235,7 +235,6 @@ function editBlock(blockObject, openSettings) { var wasNotActiveBefore = blockObject.active !== true; - activateBlock(blockObject); // dont open the editor overlay if block has hidden its content editor in overlays and we are requesting to open content, not settings. if (openSettings !== true && blockObject.hideContentInOverlay === true) { @@ -243,10 +242,12 @@ } // if requesting to open settings but we dont have settings then return. - if (openSettings === true && blockObject.config.settingsElementTypeKey) { + if (openSettings === true && !blockObject.config.settingsElementTypeKey) { return; } + activateBlock(blockObject); + // make a clone to avoid editing model directly. var blockContentClone = Utilities.copy(blockObject.content); var blockSettingsClone = null; @@ -341,7 +342,11 @@ if(!(mouseEvent.ctrlKey || mouseEvent.metaKey)) { editorService.close(); if (added && vm.layout.length > createIndex) { - editBlock(vm.layout[createIndex].$block); + if (inlineEditing === true) { + activateBlock(vm.layout[createIndex].$block); + } else if (inlineEditing === false && vm.layout[createIndex].$block.hideContentInOverlay !== true) { + editBlock(vm.layout[createIndex].$block); + } } } }, From f1c5e19124a89fbc8de0430e99ff36bdad88ba36 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Wed, 1 Jul 2020 20:10:08 +0200 Subject: [PATCH 289/377] no need for var twice --- .../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 2848ddaac7..4adfa5b503 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 @@ -396,7 +396,7 @@ if (blockConfiguration === null) { console.error("The block entry of "+udi+" is not begin initialized cause its contentTypeKey is not allowed for this PropertyEditor"); } else { - var contentScaffold = this.getScaffoldFromKey(blockConfiguration.contentTypeKey); + contentScaffold = this.getScaffoldFromKey(blockConfiguration.contentTypeKey); if(contentScaffold === null) { console.error("The block entry of "+udi+" is not begin initialized cause its Element Type was not loaded."); } From c74b03a1b603904bee72aac2192b9ca6f5cc35cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Wed, 1 Jul 2020 20:10:28 +0200 Subject: [PATCH 290/377] check for object before prop --- .../blocklist.blockconfiguration.overlay.controller.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/blocklist.blockconfiguration.overlay.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/blocklist.blockconfiguration.overlay.controller.js index a42784f55a..0f58b84ee9 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/blocklist.blockconfiguration.overlay.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/prevalue/blocklist.blockconfiguration.overlay.controller.js @@ -132,11 +132,11 @@ vm.elementTypes[i] = args.documentType; } } - if (vm.contentPreview.key === key) { + if (vm.contentPreview && vm.contentPreview.key === key) { vm.contentPreview = args.documentType; $scope.$evalAsync(); } - if (vm.settingsPreview.key === key) { + if (vm.settingsPreview && vm.settingsPreview.key === key) { vm.settingsPreview = args.documentType; $scope.$evalAsync(); } From 9649b22e0d213eeabf38b73f0c7b6f94d9eeaade Mon Sep 17 00:00:00 2001 From: Shannon Date: Thu, 2 Jul 2020 15:13:05 +1000 Subject: [PATCH 291/377] remvoes comments and unneeded code --- .../validation/valserver.directive.js | 2 -- .../Filters/ContentSaveModelValidator.cs | 24 ------------------- 2 files changed, 26 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/validation/valserver.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/validation/valserver.directive.js index 04f1496a30..701a344838 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/validation/valserver.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/validation/valserver.directive.js @@ -58,8 +58,6 @@ function valServer(serverValidationManager) { function getPropertyValidationKey() { // Get the property validation path if there is one, this is how wiring up any nested/virtual property validation works var propertyValidationPath = umbPropCtrl ? umbPropCtrl.getValidationPath() : null; - // TODO: Is this going to break with nested content because it changes the alias? - // Hrm, don't think so because NC will use the property validation path return propertyValidationPath ? propertyValidationPath : currentProperty.alias; } diff --git a/src/Umbraco.Web/Editors/Filters/ContentSaveModelValidator.cs b/src/Umbraco.Web/Editors/Filters/ContentSaveModelValidator.cs index f9d7203d12..09995c1104 100644 --- a/src/Umbraco.Web/Editors/Filters/ContentSaveModelValidator.cs +++ b/src/Umbraco.Web/Editors/Filters/ContentSaveModelValidator.cs @@ -17,29 +17,5 @@ namespace Umbraco.Web.Editors.Filters { } - protected override void AddPropertyError(ContentItemSave model, ContentVariantSave modelWithProperties, IDataEditor editor, ContentPropertyDto property, ValidationResult validationResult, ModelStateDictionary modelState) - { - // Original idea: See if we can build up the JSON + JSON Path - // SD: I'm just keeping these notes here for later just to remind myself that we might want to take into account the tab number in validation - // which we might be able to get in the PropertyValidationService anyways? - - // Create a JSON + JSON Path key, see https://gist.github.com/Shazwazza/ad9fcbdb0fdacff1179a9eed88393aa6 - - //var json = new PropertyError - //{ - // Culture = property.Culture, - // Segment = property.Segment - //}; - - // TODO: Hrm, we can't get the tab index without a reference to the content type itself! the IContent doesn't contain a reference to groups/indexes - // BUT! I think it contains a reference to the group alias so we could use JSON Path for a group alias instead of index like: - // .tabs[?(@.alias=='Content')] - //var tabIndex = ?? - - //var jsonPath = "$.variants[0].tabs[0].properties[?(@.alias=='title')].value[0]"; - - base.AddPropertyError(model, modelWithProperties, editor, property, validationResult, modelState); - } - } } From de3d0e9f11ac86147d3e36caceda1051f50d021c Mon Sep 17 00:00:00 2001 From: Sebastiaan Janssen Date: Thu, 2 Jul 2020 10:36:45 +0200 Subject: [PATCH 292/377] Merge pull request #7021 from stevemegson/v8/pr/member-cmsContentNu OnMemberRefreshedEntity stores data as published rather than edited (cherry picked from commit a9bb1ff49e459a2fb71dd27fbac32eb370892a1c) --- .../PublishedCache/NuCache/PublishedSnapshotService.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs b/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs index 833b0a3665..a33d9ee427 100755 --- a/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs @@ -1290,7 +1290,7 @@ namespace Umbraco.Web.PublishedCache.NuCache var member = args.Entity; // refresh the edited data - OnRepositoryRefreshed(db, member, true); + OnRepositoryRefreshed(db, member, false); } private void OnRepositoryRefreshed(IUmbracoDatabase db, IContentBase content, bool published) From a88a5a0caec7a6c01bc4edd8fa56437e7a42f6ce Mon Sep 17 00:00:00 2001 From: Sebastiaan Janssen Date: Thu, 2 Jul 2020 10:38:10 +0200 Subject: [PATCH 293/377] Merge pull request #7984 from umbraco/v8/bugfix/7967-sqlmaindom Fixes #7967 - cannot start site with SqlMainDomLock when db isn't configured (cherry picked from commit cbfe643a7934100362c699906ed4b3094226d9da) --- src/Umbraco.Core/Runtime/SqlMainDomLock.cs | 76 +++++++++++++--------- 1 file changed, 46 insertions(+), 30 deletions(-) diff --git a/src/Umbraco.Core/Runtime/SqlMainDomLock.cs b/src/Umbraco.Core/Runtime/SqlMainDomLock.cs index 27b016b2af..5f5d0d607f 100644 --- a/src/Umbraco.Core/Runtime/SqlMainDomLock.cs +++ b/src/Umbraco.Core/Runtime/SqlMainDomLock.cs @@ -34,6 +34,7 @@ namespace Umbraco.Core.Runtime // unique id for our appdomain, this is more unique than the appdomain id which is just an INT counter to its safer _lockId = Guid.NewGuid().ToString(); _logger = logger; + _dbFactory = new UmbracoDatabaseFactory( Constants.System.UmbracoConnectionName, _logger, @@ -42,6 +43,12 @@ namespace Umbraco.Core.Runtime public async Task AcquireLockAsync(int millisecondsTimeout) { + if (!_dbFactory.Configured) + { + // if we aren't configured, then we're in an install state, in which case we have no choice but to assume we can acquire + return true; + } + if (!(_dbFactory.SqlContext.SqlSyntax is SqlServerSyntaxProvider sqlServerSyntaxProvider)) throw new NotSupportedException("SqlMainDomLock is only supported for Sql Server"); @@ -138,6 +145,12 @@ namespace Umbraco.Core.Runtime // poll every 1 second Thread.Sleep(1000); + if (!_dbFactory.Configured) + { + // if we aren't configured, we just keep looping since we can't query the db + continue; + } + lock (_locker) { // If cancellation has been requested we will just exit. Depending on timing of the shutdown, @@ -370,41 +383,44 @@ namespace Umbraco.Core.Runtime _cancellationTokenSource.Cancel(); _cancellationTokenSource.Dispose(); - var db = GetDatabase(); - try + if (_dbFactory.Configured) { - db.BeginTransaction(IsolationLevel.ReadCommitted); - - // get a write lock - _sqlServerSyntax.WriteLock(db, Constants.Locks.MainDom); - - // When we are disposed, it means we have released the MainDom lock - // and called all MainDom release callbacks, in this case - // if another maindom is actually coming online we need - // to signal to the MainDom coming online that we have shutdown. - // To do that, we update the existing main dom DB record with a suffixed "_updated" string. - // Otherwise, if we are just shutting down, we want to just delete the row. - if (_mainDomChanging) + var db = GetDatabase(); + try { - _logger.Debug("Releasing MainDom, updating row, new application is booting."); - db.Execute($"UPDATE umbracoKeyValue SET [value] = [value] + '{UpdatedSuffix}' WHERE [key] = @key", new { key = MainDomKey }); + db.BeginTransaction(IsolationLevel.ReadCommitted); + + // get a write lock + _sqlServerSyntax.WriteLock(db, Constants.Locks.MainDom); + + // When we are disposed, it means we have released the MainDom lock + // and called all MainDom release callbacks, in this case + // if another maindom is actually coming online we need + // to signal to the MainDom coming online that we have shutdown. + // To do that, we update the existing main dom DB record with a suffixed "_updated" string. + // Otherwise, if we are just shutting down, we want to just delete the row. + if (_mainDomChanging) + { + _logger.Debug("Releasing MainDom, updating row, new application is booting."); + db.Execute($"UPDATE umbracoKeyValue SET [value] = [value] + '{UpdatedSuffix}' WHERE [key] = @key", new { key = MainDomKey }); + } + else + { + _logger.Debug("Releasing MainDom, deleting row, application is shutting down."); + db.Execute("DELETE FROM umbracoKeyValue WHERE [key] = @key", new { key = MainDomKey }); + } } - else + catch (Exception ex) { - _logger.Debug("Releasing MainDom, deleting row, application is shutting down."); - db.Execute("DELETE FROM umbracoKeyValue WHERE [key] = @key", new { key = MainDomKey }); + ResetDatabase(); + _logger.Error(ex, "Unexpected error during dipsose."); + _hasError = true; + } + finally + { + db?.CompleteTransaction(); + ResetDatabase(); } - } - catch (Exception ex) - { - ResetDatabase(); - _logger.Error(ex, "Unexpected error during dipsose."); - _hasError = true; - } - finally - { - db?.CompleteTransaction(); - ResetDatabase(); } } } From b8d6ef64c72e9ca028216a839399731a09969f02 Mon Sep 17 00:00:00 2001 From: Shannon Date: Thu, 2 Jul 2020 15:36:15 +1000 Subject: [PATCH 294/377] Remove the usage of Parallel to run the populators (cherry picked from commit b078f856b9f6c77dd0233f8d7d51bd02b5d7f6da) --- src/Umbraco.Examine/IndexRebuilder.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Examine/IndexRebuilder.cs b/src/Umbraco.Examine/IndexRebuilder.cs index 786aecac71..02fb4fc2da 100644 --- a/src/Umbraco.Examine/IndexRebuilder.cs +++ b/src/Umbraco.Examine/IndexRebuilder.cs @@ -50,8 +50,11 @@ namespace Umbraco.Examine index.CreateIndex(); // clear the index } - //run the populators in parallel against all indexes - Parallel.ForEach(_populators, populator => populator.Populate(indexes)); + // run each populator over the indexes + foreach(var populator in _populators) + { + populator.Populate(indexes); + } } } From 1f29778a238440f346453475cc50339ce2e79bbe Mon Sep 17 00:00:00 2001 From: Sebastiaan Janssen Date: Thu, 2 Jul 2020 14:16:04 +0200 Subject: [PATCH 295/377] Bump version to 8.6.4 --- src/SolutionInfo.cs | 4 ++-- src/Umbraco.Web.UI/Umbraco.Web.UI.csproj | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/SolutionInfo.cs b/src/SolutionInfo.cs index 9294203699..4ab97229d4 100644 --- a/src/SolutionInfo.cs +++ b/src/SolutionInfo.cs @@ -18,5 +18,5 @@ using System.Resources; [assembly: AssemblyVersion("8.0.0")] // these are FYI and changed automatically -[assembly: AssemblyFileVersion("8.6.3")] -[assembly: AssemblyInformationalVersion("8.6.3")] +[assembly: AssemblyFileVersion("8.6.4")] +[assembly: AssemblyInformationalVersion("8.6.4")] diff --git a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj index e8ee470b53..0de757531a 100644 --- a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj +++ b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj @@ -345,9 +345,9 @@ False True - 8630 + 8640 / - http://localhost:8630 + http://localhost:8640 False False From 9f208a0ff12c42ede2a0bac841e86417bf1f74ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Fri, 3 Jul 2020 12:22:14 +0200 Subject: [PATCH 296/377] improve existence, destroy and watchers --- .../blockeditormodelobject.service.js | 53 ++++++++++++++----- .../umbelementeditorcontent.component.js | 8 ++- .../umbBlockListPropertyEditor.component.js | 2 + 3 files changed, 48 insertions(+), 15 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 4adfa5b503..ec232b4914 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 @@ -153,10 +153,13 @@ */ function createDataModelWatcher(blockObject, prop) { return function() { - // sync data: - prop.value = blockObject.data[prop.alias]; + if (prop.value !== blockObject.data[prop.alias]) { - blockObject.updateLabel(); + // sync data: + prop.value = blockObject.data[prop.alias]; + + blockObject.updateLabel(); + } } } /** @@ -164,8 +167,10 @@ */ function createLayoutSettingsModelWatcher(blockObject, prop) { return function() { - // sync data: - prop.value = blockObject.layout.settings[prop.alias]; + if (prop.value !== blockObject.layout.settings[prop.alias]) { + // sync data: + prop.value = blockObject.layout.settings[prop.alias]; + } } } @@ -174,8 +179,10 @@ */ function createContentModelPropWatcher(blockObject, prop) { return function() { - // sync data: - blockObject.data[prop.alias] = prop.value; + if (blockObject.data[prop.alias] !== prop.value) { + // sync data: + blockObject.data[prop.alias] = prop.value; + } blockObject.updateLabel(); } @@ -186,8 +193,10 @@ */ function createSettingsModelPropWatcher(blockObject, prop) { return function() { - // sync data: - blockObject.layout.settings[prop.alias] = prop.value; + if (blockObject.layout.settings[prop.alias] !== prop.value) { + // sync data: + blockObject.layout.settings[prop.alias] = prop.value; + } } } @@ -291,7 +300,10 @@ scaffoldKeys.forEach((contentTypeKey => { tasks.push(contentResource.getScaffoldByKey(-20, contentTypeKey).then(scaffold => { - this.scaffolds.push(replaceUnsupportedProperties(scaffold)); + // this.scaffolds might not exists anymore, this happens if this instance has been destroyed before the load is complete. + if (this.scaffolds) { + this.scaffolds.push(replaceUnsupportedProperties(scaffold)); + } })); })); @@ -423,9 +435,15 @@ blockObject.labelInterpolator = $interpolate(blockObject.config.label); } blockObject.__scope = this.isolatedScope; - blockObject.updateLabel = _.debounce(function () {this.__scope.$evalAsync(function() { - this.label = getBlockLabel(this); - }.bind(this))}.bind(blockObject), 10); + blockObject.updateLabel = _.debounce( + function () { + // Check wether scope still exists, maybe object was destoyed in these seconds. + if (this.__scope) { + this.label = getBlockLabel(this); + this.__scope.$evalAsync(); + } + }.bind(blockObject) + , 10); // make basics from scaffold blockObject.content = Utilities.copy(contentScaffold); @@ -487,8 +505,11 @@ delete this.__watchers; // help carbage collector: + delete this.config; delete this.layout; delete this.data; + delete this.content; + delete this.settings; // remove model from isolatedScope. delete this.__scope.blockObjects["_" + this.key]; @@ -496,6 +517,9 @@ // removes this method, making it unposible to destroy again. delete this.destroy; + + // lets remove the key to make things blow up if this is still referenced: + delete this.key; } return blockObject; @@ -511,8 +535,9 @@ * @param {Object} blockObject The BlockObject to be removed and destroyed. */ removeDataAndDestroyModel: function (blockObject) { + var udi = blockObject.content.udi; this.destroyBlockObject(blockObject); - this.removeDataByUdi(blockObject.content.udi); + this.removeDataByUdi(udi); }, /** diff --git a/src/Umbraco.Web.UI.Client/src/views/components/elementeditor/umbelementeditorcontent.component.js b/src/Umbraco.Web.UI.Client/src/views/components/elementeditor/umbelementeditorcontent.component.js index 6e4f2f42ae..6dfcb07098 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/elementeditor/umbelementeditorcontent.component.js +++ b/src/Umbraco.Web.UI.Client/src/views/components/elementeditor/umbelementeditorcontent.component.js @@ -12,9 +12,15 @@ } }); - function ElementEditorContentComponentController() { + function ElementEditorContentComponentController($scope) { // We need a controller for the component to work. + var vm = this; + + vm.getScope = getScope;// used by property editors to get a scope that is the root of split view, content apps etc. + function getScope() { + return $scope; + } } 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 4ae0a90390..02e53826b5 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 @@ -88,6 +88,8 @@ var scopeOfExistence = $scope; if(vm.umbVariantContentEditors && vm.umbVariantContentEditors.getScope) { scopeOfExistence = vm.umbVariantContentEditors.getScope(); + } else if(vm.umbElementEditorContent && vm.umbElementEditorContent.getScope) { + scopeOfExistence = vm.umbElementEditorContent.getScope(); } // Create Model Object, to manage our data for this Block Editor. From b017b3f1b62c727b7c97cea4a8474c17795ca0f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Fri, 3 Jul 2020 12:22:23 +0200 Subject: [PATCH 297/377] improve test script --- .../test/unit/common/services/block-editor-service.spec.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Umbraco.Web.UI.Client/test/unit/common/services/block-editor-service.spec.js b/src/Umbraco.Web.UI.Client/test/unit/common/services/block-editor-service.spec.js index b0e84a793e..e0cd2d0c93 100644 --- a/src/Umbraco.Web.UI.Client/test/unit/common/services/block-editor-service.spec.js +++ b/src/Umbraco.Web.UI.Client/test/unit/common/services/block-editor-service.spec.js @@ -209,6 +209,9 @@ var blockObject = modelObject.getBlockObject(layout[0]); + expect(blockObject).not.toBeUndefined(); + expect(blockObject).not.toBe(null); + // remove from layout; layout.splice(0, 1); From 290bb23fec2dd42047e458ad4880289be5e01f24 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Fri, 3 Jul 2020 13:41:23 +0200 Subject: [PATCH 298/377] update data structure to fit with data structure RFC v3 --- .../blockeditormodelobject.service.js | 89 ++++++++--- .../services/block-editor-service.spec.js | 144 ++++++++++++++++-- 2 files changed, 202 insertions(+), 31 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 ec232b4914..5cf0bbf944 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 @@ -136,7 +136,7 @@ // We also like to watch our data model to be able to capture changes coming from other places. if (forSettings === true) { - blockObject.__watchers.push(isolatedScope.$watch("blockObjects._" + blockObject.key + "." + "layout.settings" + "." + prop.alias, createLayoutSettingsModelWatcher(blockObject, prop))); + blockObject.__watchers.push(isolatedScope.$watch("blockObjects._" + blockObject.key + "." + "settingsData" + "." + prop.alias, createLayoutSettingsModelWatcher(blockObject, prop))); } else { blockObject.__watchers.push(isolatedScope.$watch("blockObjects._" + blockObject.key + "." + "data" + "." + prop.alias, createDataModelWatcher(blockObject, prop))); } @@ -167,9 +167,9 @@ */ function createLayoutSettingsModelWatcher(blockObject, prop) { return function() { - if (prop.value !== blockObject.layout.settings[prop.alias]) { + if (prop.value !== blockObject.settingsData[prop.alias]) { // sync data: - prop.value = blockObject.layout.settings[prop.alias]; + prop.value = blockObject.settingsData[prop.alias]; } } } @@ -193,9 +193,9 @@ */ function createSettingsModelPropWatcher(blockObject, prop) { return function() { - if (blockObject.layout.settings[prop.alias] !== prop.value) { + if (blockObject.settingsData[prop.alias] !== prop.value) { // sync data: - blockObject.layout.settings[prop.alias] = prop.value; + blockObject.settingsData[prop.alias] = prop.value; } } } @@ -245,7 +245,8 @@ // ensure basic part of data-structure is in place: this.value = propertyModelValue; this.value.layout = this.value.layout || {}; - this.value.data = this.value.data || []; + this.value.contentData = this.value.contentData || []; + this.value.settingsData = this.value.settingsData || []; this.propertyEditorAlias = propertyEditorAlias; this.blockConfigurations = blockConfigurations; @@ -459,16 +460,25 @@ var settingsScaffold = this.getScaffoldFromKey(blockConfiguration.settingsElementTypeKey); if (settingsScaffold !== null) { - layoutEntry.settings = layoutEntry.settings || {}; - - blockObject.settingsData = layoutEntry.settings; + if (!layoutEntry.settingsUdi) { + // if this block does not have settings data, then create it. This could happen because settings model has been added later than this content was created. + layoutEntry.settingsUdi = this._createSettingsEntry(blockConfiguration.settingsElementTypeKey); + } + + var settingsUdi = layoutEntry.settingsUdi; + + var settingsData = this._getSettingsByUdi(settingsUdi); + if (settingsData === null) { + console.error("Couldnt find content settings data of " + settingsUdi) + return null; + } + + blockObject.settingsData = settingsData; // make basics from scaffold blockObject.settings = Utilities.copy(settingsScaffold); - layoutEntry.settings = layoutEntry.settings || {}; - if (!layoutEntry.settings.key) { layoutEntry.settings.key = String.CreateGuid(); } - if (!layoutEntry.settings.contentTypeKey) { layoutEntry.settings.contentTypeKey = blockConfiguration.settingsElementTypeKey; } - mapToElementModel(blockObject.settings, layoutEntry.settings); + blockObject.settings.udi = settingsUdi; + mapToElementModel(blockObject.settings, settingsData); } } @@ -487,7 +497,7 @@ mapToPropertyModel(this.content, this.data); } if (this.config.settingsElementTypeKey !== null) { - mapToPropertyModel(this.settings, this.layout.settings); + mapToPropertyModel(this.settings, this.settingsData); } } @@ -508,6 +518,7 @@ delete this.config; delete this.layout; delete this.data; + delete this.settingsData; delete this.content; delete this.settings; @@ -536,8 +547,15 @@ */ removeDataAndDestroyModel: function (blockObject) { var udi = blockObject.content.udi; + var settingsUdi = null; + if (blockObject.settings) { + settingsUdi = blockObject.settings.udi; + } this.destroyBlockObject(blockObject); this.removeDataByUdi(udi); + if(settingsUdi) { + this.removeSettingsByUdi(settingsUdi); + } }, /** @@ -586,7 +604,7 @@ } if (blockConfiguration.settingsElementTypeKey != null) { - entry.settings = { key: String.CreateGuid(), contentTypeKey: blockConfiguration.settingsElementTypeKey }; + entry.settingsUdi = this._createSettingsEntry(blockConfiguration.settingsElementTypeKey) } return entry; @@ -641,26 +659,55 @@ contentTypeKey: elementTypeKey, udi: udiService.create("element") }; - this.value.data.push(content); + this.value.contentData.push(content); return content.udi; }, // private _getDataByUdi: function(udi) { - return this.value.data.find(entry => entry.udi === udi) || null; + return this.value.contentData.find(entry => entry.udi === udi) || null; }, /** * @ngdoc method * @name removeDataByUdi * @methodOf umbraco.services.blockEditorModelObject - * @description Removes the data of a given UDI. + * @description Removes the content data of a given UDI. * Notice this method does not remove the block from your layout, this will need to be handlede by the Property Editor since this services donst know about your layout structure. - * @param {string} udi The UDI of the data to be removed. + * @param {string} udi The UDI of the content data to be removed. */ removeDataByUdi: function(udi) { - const index = this.value.data.findIndex(o => o.udi === udi); + const index = this.value.contentData.findIndex(o => o.udi === udi); if (index !== -1) { - this.value.data.splice(index, 1); + this.value.contentData.splice(index, 1); + } + }, + + // private + _createSettingsEntry: function(elementTypeKey) { + var settings = { + contentTypeKey: elementTypeKey, + udi: udiService.create("element") + }; + this.value.settingsData.push(settings); + return settings.udi; + }, + // private + _getSettingsByUdi: function(udi) { + return this.value.settingsData.find(entry => entry.udi === udi) || null; + }, + + /** + * @ngdoc method + * @name removeSettingsByUdi + * @methodOf umbraco.services.blockEditorModelObject + * @description Removes the settings data of a given UDI. + * Notice this method does not remove the settingsUdi from your layout, this will need to be handlede by the Property Editor since this services donst know about your layout structure. + * @param {string} udi The UDI of the settings data to be removed. + */ + removeSettingsByUdi: function(udi) { + const index = this.value.settingsData.findIndex(o => o.udi === udi); + if (index !== -1) { + this.value.settingsData.splice(index, 1); } }, diff --git a/src/Umbraco.Web.UI.Client/test/unit/common/services/block-editor-service.spec.js b/src/Umbraco.Web.UI.Client/test/unit/common/services/block-editor-service.spec.js index e0cd2d0c93..8936f731bb 100644 --- a/src/Umbraco.Web.UI.Client/test/unit/common/services/block-editor-service.spec.js +++ b/src/Umbraco.Web.UI.Client/test/unit/common/services/block-editor-service.spec.js @@ -36,7 +36,7 @@ } ] }, - data: [ + contentData: [ { udi: 1234, contentTypeKey: "7C5B74D1-E2F9-45A3-AE4B-FC7A829BF8AB", @@ -45,6 +45,32 @@ ] }; + var blockWithSettingsConfigurationMock = { contentTypeKey: "7C5B74D1-E2F9-45A3-AE4B-FC7A829BF8AB", label:"Test label", settingsElementTypeKey: "7C5B74D1-E2F9-45A3-AE4B-FC7A829BF8AB", view: "testview.html"}; + var propertyModelWithSettingsMock = { + layout: { + "Umbraco.TestBlockEditor": [ + { + udi: 1234, + settingsUdi: 4567 + } + ] + }, + contentData: [ + { + udi: 1234, + contentTypeKey: "7C5B74D1-E2F9-45A3-AE4B-FC7A829BF8AB", + testproperty: "myTestValue" + } + ], + settingsData: [ + { + udi: 4567, + contentTypeKey: "7C5B74D1-E2F9-45A3-AE4B-FC7A829BF8AB", + testproperty: "myTestValueForSettings" + } + ] + }; + describe('init blockEditorModelObject', function () { it('fail if no model value', function () { @@ -110,8 +136,8 @@ var blockObject = modelObject.getBlockObject(layout[0]); expect(blockObject).not.toBeUndefined(); - expect(blockObject.data.udi).toBe(propertyModelMock.data[0].udi); - expect(blockObject.content.variants[0].tabs[0].properties[0].value).toBe(propertyModelMock.data[0].testproperty); + expect(blockObject.data.udi).toBe(propertyModelMock.contentData[0].udi); + expect(blockObject.content.variants[0].tabs[0].properties[0].value).toBe(propertyModelMock.contentData[0].testproperty); done(); }); @@ -135,9 +161,9 @@ $rootScope.$digest();// invoke angularJS Store. - expect(blockObject.data).toBe(propertyModel.data[0]); + expect(blockObject.data).toEqual(propertyModel.contentData[0]); expect(blockObject.data.testproperty).toBe("anotherTestValue"); - expect(propertyModel.data[0].testproperty).toBe("anotherTestValue"); + expect(propertyModel.contentData[0].testproperty).toBe("anotherTestValue"); // @@ -152,7 +178,7 @@ var propertyModel = angular.copy(propertyModelMock); var complexValue = {"list": ["A", "B", "C"]}; - propertyModel.data[0].testproperty = complexValue; + propertyModel.contentData[0].testproperty = complexValue; var modelObject = blockEditorService.createModelObject(propertyModel, "Umbraco.TestBlockEditor", [blockConfigurationMock], $scope, $scope); @@ -168,8 +194,8 @@ $rootScope.$digest();// invoke angularJS Store. - expect(propertyModel.data[0].testproperty.list[0]).toBe("AA"); - expect(propertyModel.data[0].testproperty.list.length).toBe(4); + expect(propertyModel.contentData[0].testproperty.list[0]).toBe("AA"); + expect(propertyModel.contentData[0].testproperty.list.length).toBe(4); done(); }); @@ -218,8 +244,8 @@ // remove from data; modelObject.removeDataAndDestroyModel(blockObject); - expect(propertyModel.data.length).toBe(0); - expect(propertyModel.data[0]).toBeUndefined(); + expect(propertyModel.contentData.length).toBe(0); + expect(propertyModel.contentData[0]).toBeUndefined(); expect(propertyModel.layout["Umbraco.TestBlockEditor"].length).toBe(0); expect(propertyModel.layout["Umbraco.TestBlockEditor"][0]).toBeUndefined(); @@ -229,6 +255,104 @@ }); + + + + + + it('getBlockObject of block with settings has values', function (done) { + + var propertyModel = angular.copy(propertyModelWithSettingsMock); + + var modelObject = blockEditorService.createModelObject(propertyModel, "Umbraco.TestBlockEditor", [blockWithSettingsConfigurationMock], $scope, $scope); + + modelObject.load().then(() => { + + var layout = modelObject.getLayout(); + + var blockObject = modelObject.getBlockObject(layout[0]); + + expect(blockObject).not.toBeUndefined(); + expect(blockObject.data.udi).toBe(propertyModel.contentData[0].udi); + expect(blockObject.content.variants[0].tabs[0].properties[0].value).toBe(propertyModel.contentData[0].testproperty); + + done(); + }); + + }); + + + it('getBlockObject of block with settings syncs primative values', function (done) { + + var propertyModel = angular.copy(propertyModelWithSettingsMock); + + var modelObject = blockEditorService.createModelObject(propertyModel, "Umbraco.TestBlockEditor", [blockWithSettingsConfigurationMock], $scope, $scope); + + modelObject.load().then(() => { + + var layout = modelObject.getLayout(); + + var blockObject = modelObject.getBlockObject(layout[0]); + + blockObject.content.variants[0].tabs[0].properties[0].value = "anotherTestValue"; + blockObject.settings.variants[0].tabs[0].properties[0].value = "anotherTestValueForSettings"; + + $rootScope.$digest();// invoke angularJS Store. + + expect(blockObject.data).toEqual(propertyModel.contentData[0]); + expect(blockObject.data.testproperty).toBe("anotherTestValue"); + expect(propertyModel.contentData[0].testproperty).toBe("anotherTestValue"); + + expect(blockObject.settingsData).toEqual(propertyModel.settingsData[0]); + expect(blockObject.settingsData.testproperty).toBe("anotherTestValueForSettings"); + expect(propertyModel.settingsData[0].testproperty).toBe("anotherTestValueForSettings"); + + // + + done(); + }); + + }); + + + it('getBlockObject of block with settings syncs values of object', function (done) { + + var propertyModel = angular.copy(propertyModelWithSettingsMock); + + var complexValue = {"list": ["A", "B", "C"]}; + propertyModel.contentData[0].testproperty = complexValue; + + var complexSettingsValue = {"list": ["A", "B", "C"]}; + propertyModel.settingsData[0].testproperty = complexSettingsValue; + + var modelObject = blockEditorService.createModelObject(propertyModel, "Umbraco.TestBlockEditor", [blockWithSettingsConfigurationMock], $scope, $scope); + + modelObject.load().then(() => { + + var layout = modelObject.getLayout(); + + var blockObject = modelObject.getBlockObject(layout[0]); + + blockObject.content.variants[0].tabs[0].properties[0].value.list[0] = "AA"; + blockObject.content.variants[0].tabs[0].properties[0].value.list.push("D"); + + blockObject.settings.variants[0].tabs[0].properties[0].value.list[0] = "settingsValue"; + blockObject.settings.variants[0].tabs[0].properties[0].value.list.push("settingsNewValue"); + + $rootScope.$digest();// invoke angularJS Store. + + expect(propertyModel.contentData[0].testproperty.list[0]).toBe("AA"); + expect(propertyModel.contentData[0].testproperty.list.length).toBe(4); + + expect(propertyModel.settingsData[0].testproperty.list[0]).toBe("settingsValue"); + expect(propertyModel.settingsData[0].testproperty.list.length).toBe(4); + + done(); + }); + + }); + + }); }); From c4e7929e684a330ea92d207bc85f067e33994004 Mon Sep 17 00:00:00 2001 From: Shannon Date: Tue, 7 Jul 2020 12:50:15 +1000 Subject: [PATCH 299/377] Converts umbProperty to a component, gets nested valPropertyMsg validators clearing (as a prototype), need to check TODOs, test inline editing, etc... --- .../property/umbproperty.directive.js | 136 ++++++++++-------- .../validation/valformmanager.directive.js | 129 +++++++++++------ .../validation/valpropertymsg.directive.js | 103 ++++++++----- .../validation/valserver.directive.js | 7 +- .../services/servervalidationmgr.service.js | 29 ++-- src/Umbraco.Web.UI.Client/src/less/main.less | 2 +- .../components/property/umb-property.html | 21 +-- .../NuCache/DataSource/DatabaseDataSource.cs | 6 +- 8 files changed, 267 insertions(+), 166 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/property/umbproperty.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/property/umbproperty.directive.js index 8b3f51f0f9..1d100494f6 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/property/umbproperty.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/property/umbproperty.directive.js @@ -3,8 +3,81 @@ * @name umbraco.directives.directive:umbProperty * @restrict E **/ -angular.module("umbraco.directives") - .directive('umbProperty', function (userService, serverValidationManager, udiService) { +(function () { + 'use strict'; + + angular + .module("umbraco.directives") + .component('umbProperty', { + templateUrl: 'views/components/property/umb-property.html', + controller: UmbPropertyController, + controllerAs: 'vm', + transclude: true, + require: { + parentUmbProperty: '?^^umbProperty' + }, + bindings: { + property: "=", + elementUdi: "@", + // optional, if set this will be used for the property alias validation path (hack required because NC changes the actual property.alias :/) + propertyAlias: "@", + showInherit: "<", + inheritsFrom: "<" + } + }); + + + + function UmbPropertyController($scope, userService, serverValidationManager, udiService, angularHelper) { + + const vm = this; + + vm.$onInit = onInit; + + vm.setPropertyError = function (errorMsg) { + vm.property.propertyErrorMessage = errorMsg; + }; + + vm.propertyActions = []; + vm.setPropertyActions = function (actions) { + vm.propertyActions = actions; + }; + + // returns the unique Id for the property to be used as the validation key for server side validation logic + vm.getValidationPath = function () { + + // the elementUdi will be empty when this is not a nested property + var propAlias = vm.propertyAlias ? vm.propertyAlias : vm.property.alias; + vm.elementUdi = ensureUdi(vm.elementUdi); + return serverValidationManager.createPropertyValidationKey(propAlias, vm.elementUdi); + } + + vm.getParentValidationPath = function () { + if (!vm.parentUmbProperty) { + return null; + } + return vm.parentUmbProperty.getValidationPath(); + } + + function onInit() { + vm.controlLabelTitle = null; + if (Umbraco.Sys.ServerVariables.isDebuggingEnabled) { + userService.getCurrentUser().then(function (u) { + if (u.allowedSections.indexOf("settings") !== -1 ? true : false) { + vm.controlLabelTitle = vm.property.alias; + } + }); + } + + vm.elementUdi = ensureUdi(vm.elementUdi); + + if (!vm.parentUmbProperty) { + // not found, then fallback to searching the scope chain, this may be needed when DOM inheritance isn't maintained but scope + // inheritance is (i.e.infinite editing) + var found = angularHelper.traverseScopeChain($scope, s => s && s.vm && s.vm.constructor.name === "UmbPropertyController"); + vm.parentUmbProperty = found ? found.vm : null; + } + } // if only a guid is passed in, we'll ensure a correct udi structure function ensureUdi(udi) { @@ -13,61 +86,6 @@ angular.module("umbraco.directives") } return udi; } + } - return { - scope: { - property: "=", - elementUdi: "@", - // optional, if set this will be used for the property alias validation path (hack required because NC changes the actual property.alias :/) - propertyAlias: "@", - showInherit: "<", - inheritsFrom: "<" - }, - transclude: true, - restrict: 'E', - replace: true, - templateUrl: 'views/components/property/umb-property.html', - link: function (scope, element, attr, ctrls) { - - scope.controlLabelTitle = null; - if(Umbraco.Sys.ServerVariables.isDebuggingEnabled) { - userService.getCurrentUser().then(function (u) { - if(u.allowedSections.indexOf("settings") !== -1 ? true : false) { - scope.controlLabelTitle = scope.property.alias; - } - }); - } - - scope.elementUdi = ensureUdi(scope.elementUdi); - - }, - //Define a controller for this directive to expose APIs to other directives - controller: function ($scope) { - - var self = this; - - //set the API properties/methods - - self.property = $scope.property; - self.setPropertyError = function (errorMsg) { - $scope.property.propertyErrorMessage = errorMsg; - }; - - $scope.propertyActions = []; - self.setPropertyActions = function(actions) { - $scope.propertyActions = actions; - }; - - // returns the unique Id for the property to be used as the validation key for server side validation logic - self.getValidationPath = function () { - - // the elementUdi will be empty when this is not a nested property - var propAlias = $scope.propertyAlias ? $scope.propertyAlias : $scope.property.alias; - $scope.elementUdi = ensureUdi($scope.elementUdi); - return serverValidationManager.createPropertyValidationKey(propAlias, $scope.elementUdi); - } - $scope.getValidationPath = self.getValidationPath; - - } - }; - }); +})(); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/validation/valformmanager.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/validation/valformmanager.directive.js index 6638ed4e6d..86ea94914a 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/validation/valformmanager.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/validation/valformmanager.directive.js @@ -12,48 +12,88 @@ * Another thing this directive does is to ensure that any .control-group that contains form elements that are invalid will * be marked with the 'error' css class. This ensures that labels included in that control group are styled correctly. **/ -function valFormManager(serverValidationManager, $rootScope, $timeout, $location, overlayService, eventsService, $routeParams, navigationService, editorService, localizationService) { +function valFormManager(serverValidationManager, $rootScope, $timeout, $location, overlayService, eventsService, $routeParams, navigationService, editorService, localizationService, angularHelper) { var SHOW_VALIDATION_CLASS_NAME = "show-validation"; var SAVING_EVENT_NAME = "formSubmitting"; var SAVED_EVENT_NAME = "formSubmitted"; + function notify(scope) { + scope.$broadcast("valStatusChanged", { form: scope.formCtrl }); + } + + function ValFormManagerController($scope) { + //This exposes an API for direct use with this directive + + // We need this as a way to reference this directive in the scope chain. Since this directive isn't a component and + // because it's an attribute instead of an element, we can't use controllerAs or anything like that. Plus since this is + // an attribute an isolated scope doesn't work so it's a bit weird. By doing this we are able to lookup the parent valFormManager + // in the scope hierarchy even if the DOM hierarchy doesn't match (i.e. in infinite editing) + $scope.valFormManager = this; + + var unsubscribe = []; + var self = this; + + //This is basically the same as a directive subscribing to an event but maybe a little + // nicer since the other directive can use this directive's API instead of a magical event + this.onValidationStatusChanged = function (cb) { + unsubscribe.push($scope.$on("valStatusChanged", function (evt, args) { + cb.apply(self, [evt, args]); + })); + }; + + this.showValidation = $scope.showValidation === true; + + this.notify = function () { + notify($scope); + } + + this.isValid = function () { + return !$scope.formCtrl.$invalid; + } + + //Ensure to remove the event handlers when this instance is destroyted + $scope.$on('$destroy', function () { + for (var u in unsubscribe) { + unsubscribe[u](); + } + }); + } + + /** + * Find's the valFormManager in the scope/DOM hierarchy + * @param {any} scope + * @param {any} ctrls + * @param {any} index + */ + function getAncestorValFormManager(scope, ctrls, index) { + + // first check the normal directive inheritance which relies on DOM inheritance + var found = ctrls[index]; + if (found) { + return found; + } + + // not found, then fallback to searching the scope chain, this may be needed when DOM inheritance isn't maintained but scope + // inheritance is (i.e.infinite editing) + var found = angularHelper.traverseScopeChain(scope, s => s && s.valFormManager && s.valFormManager.constructor.name === "ValFormManagerController"); + return found ? found.valFormManager : null; + } + return { require: ["form", "^^?valFormManager", "^^?valSubView"], restrict: "A", - controller: function($scope) { - //This exposes an API for direct use with this directive - - var unsubscribe = []; - var self = this; - - //This is basically the same as a directive subscribing to an event but maybe a little - // nicer since the other directive can use this directive's API instead of a magical event - this.onValidationStatusChanged = function (cb) { - unsubscribe.push($scope.$on("valStatusChanged", function(evt, args) { - cb.apply(self, [evt, args]); - })); - }; - - this.showValidation = $scope.showValidation === true; - - //Ensure to remove the event handlers when this instance is destroyted - $scope.$on('$destroy', function () { - for (var u in unsubscribe) { - unsubscribe[u](); - } - }); - }, + controller: ValFormManagerController, link: function (scope, element, attr, ctrls) { function notifySubView() { - if (subView){ + if (subView) { subView.valStatusChanged({ form: formCtrl, showValidation: scope.showValidation }); } } - var formCtrl = ctrls[0]; - var parentFormMgr = ctrls.length > 0 ? ctrls[1] : null; + var formCtrl = scope.formCtrl = ctrls[0]; + var parentFormMgr = scope.parentFormMgr = getAncestorValFormManager(scope, ctrls, 1); var subView = ctrls.length > 1 ? ctrls[2] : null; var labels = {}; @@ -81,14 +121,14 @@ function valFormManager(serverValidationManager, $rootScope, $timeout, $location var validatorLengths = _.map(formCtrl.$error, function (val, key) { // if there are child ng-forms, include the $error collections in those as well var innerErrorCount = _.reduce( - _.map(val, v => - _.reduce( - _.map(v.$error, e => e.length), - (m, n) => m + n - ) - ), - (memo, num) => memo + num - ); + _.map(val, v => + _.reduce( + _.map(v.$error, e => e.length), + (m, n) => m + n + ) + ), + (memo, num) => memo + num + ); return val.length + innerErrorCount; }); //sum up all numbers in the resulting array @@ -98,8 +138,9 @@ function valFormManager(serverValidationManager, $rootScope, $timeout, $location //this is the value we watch to notify of any validation changes on the form return sum; }, function (e) { - scope.$broadcast("valStatusChanged", { form: formCtrl }); + notify(scope); + notifySubView(); //find all invalid elements' .control-group's and apply the error class @@ -128,7 +169,7 @@ function valFormManager(serverValidationManager, $rootScope, $timeout, $location var unsubscribe = []; //listen for the forms saving event - unsubscribe.push(scope.$on(SAVING_EVENT_NAME, function(ev, args) { + unsubscribe.push(scope.$on(SAVING_EVENT_NAME, function (ev, args) { element.addClass(SHOW_VALIDATION_CLASS_NAME); scope.showValidation = true; notifySubView(); @@ -137,7 +178,7 @@ function valFormManager(serverValidationManager, $rootScope, $timeout, $location })); //listen for the forms saved event - unsubscribe.push(scope.$on(SAVED_EVENT_NAME, function(ev, args) { + unsubscribe.push(scope.$on(SAVED_EVENT_NAME, function (ev, args) { //remove validation class element.removeClass(SHOW_VALIDATION_CLASS_NAME); scope.showValidation = false; @@ -151,7 +192,7 @@ function valFormManager(serverValidationManager, $rootScope, $timeout, $location //This handles the 'unsaved changes' dialog which is triggered when a route is attempting to be changed but // the form has pending changes - var locationEvent = $rootScope.$on('$locationChangeStart', function(event, nextLocation, currentLocation) { + var locationEvent = $rootScope.$on('$locationChangeStart', function (event, nextLocation, currentLocation) { var infiniteEditors = editorService.getEditors(); @@ -178,10 +219,10 @@ function valFormManager(serverValidationManager, $rootScope, $timeout, $location "disableEscKey": true, "submitButtonLabel": labels.stayButton, "closeButtonLabel": labels.discardChangesButton, - submit: function() { + submit: function () { overlayService.close(); }, - close: function() { + close: function () { // close all editors editorService.closeAll(); // allow redirection @@ -190,7 +231,7 @@ function valFormManager(serverValidationManager, $rootScope, $timeout, $location var parts = nextPath.split("?"); var query = {}; if (parts.length > 1) { - _.each(parts[1].split("&"), function(q) { + _.each(parts[1].split("&"), function (q) { var keyVal = q.split("="); query[keyVal[0]] = keyVal[1]; }); @@ -215,13 +256,13 @@ function valFormManager(serverValidationManager, $rootScope, $timeout, $location unsubscribe.push(locationEvent); //Ensure to remove the event handler when this instance is destroyted - scope.$on('$destroy', function() { + scope.$on('$destroy', function () { for (var u in unsubscribe) { unsubscribe[u](); } }); - $timeout(function(){ + $timeout(function () { formCtrl.$setPristine(); }, 1000); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/validation/valpropertymsg.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/validation/valpropertymsg.directive.js index 27fa67695b..30ba459baf 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/validation/valpropertymsg.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/validation/valpropertymsg.directive.js @@ -8,24 +8,26 @@ * We will listen for server side validation changes * and when an error is detected for this property we'll show the error message. * In order for this directive to work, the valFormManager directive must be placed on the containing form. +* We don't set the validity of this validator to false when client side validation fails, only when server side +* validation fails however we do respond to the client side validation changes to display error and adjust UI state. **/ -function valPropertyMsg(serverValidationManager, localizationService) { +function valPropertyMsg(serverValidationManager, localizationService, angularHelper) { return { - require: ['^^form', '^^valFormManager', '^^umbProperty', '?^^umbVariantContent'], + require: ['^^form', '^^valFormManager', '^^umbProperty', '?^^umbVariantContent', '?^^valPropertyMsg'], replace: true, restrict: "E", template: "
        {{errorMsg}}
        ", scope: {}, link: function (scope, element, attrs, ctrl) { - + var unsubscribe = []; var watcher = null; var hasError = false; //create properties on our custom scope so we can use it in our template - scope.errorMsg = ""; - + scope.errorMsg = ""; + //the property form controller api var formCtrl = ctrl[0]; //the valFormManager controller api @@ -33,16 +35,16 @@ function valPropertyMsg(serverValidationManager, localizationService) { //the property controller api var umbPropCtrl = ctrl[2]; //the variants controller api - var umbVariantCtrl = ctrl[3]; - + var umbVariantCtrl = ctrl[3]; + var currentProperty = umbPropCtrl.property; scope.currentProperty = currentProperty; var propertyValidationKey = umbPropCtrl.getValidationPath(); var currentCulture = currentProperty.culture; - var currentSegment = currentProperty.segment; - + var currentSegment = currentProperty.segment; + // validation object won't exist when editor loads outside the content form (ie in settings section when modifying a content type) var isMandatory = currentProperty.validation ? currentProperty.validation.mandatory : undefined; @@ -63,10 +65,10 @@ function valPropertyMsg(serverValidationManager, localizationService) { return; } } - + // if we have reached this part, and there is no culture, then lets fallback to invariant. To get the validation feedback for invariant language. currentCulture = currentCulture || "invariant"; - + // Gets the error message to display function getErrorMsg() { @@ -93,7 +95,14 @@ function valPropertyMsg(serverValidationManager, localizationService) { // we need to re-validate it for the server side validator so the user can resubmit // the form. Of course normal client-side validators will continue to execute. function startWatch() { + + // TODO: Can we watch on something other than the value?? This doesn't work for complex editors especially once that have a + // viewmodel/model setup the value that this is watching doesn't actually get updated by a sub-editor in all cases. + // we can probably watch the formCtrl view value? But then we also don't want this to watch complex values that have sub editors anyways + // since that might end up clearing the whole chain of valPropertyMsg when a sub value is changed (in some cases, not with the block editor). + //if there's not already a watch + if (!watcher) { watcher = scope.$watch("currentProperty.value", function (newValue, oldValue) { @@ -107,20 +116,19 @@ function valPropertyMsg(serverValidationManager, localizationService) { if (Utilities.isArray(formCtrl.$error[e])) { errCount++; } - } + } //we are explicitly checking for valServer errors here, since we shouldn't auto clear // based on other errors. We'll also check if there's no other validation errors apart from valPropertyMsg, if valPropertyMsg // is the only one, then we'll clear. - if (errCount === 0 - || (errCount === 1 && Utilities.isArray(formCtrl.$error.valPropertyMsg)) + if (errCount === 0 + || (errCount === 1 && Utilities.isArray(formCtrl.$error.valPropertyMsg)) || (formCtrl.$invalid && Utilities.isArray(formCtrl.$error.valServer))) { - scope.errorMsg = ""; - formCtrl.$setValidity('valPropertyMsg', true); + resetError(); } else if (showValidation && scope.errorMsg === "") { - formCtrl.$setValidity('valPropertyMsg', false); + formCtrl.$setValidity('valPropertyMsg', false, formCtrl); scope.errorMsg = getErrorMsg(); } }, true); @@ -135,6 +143,35 @@ function valPropertyMsg(serverValidationManager, localizationService) { } } + function resetError() { + var hadError = hasError; + hasError = false; + formCtrl.$setValidity('valPropertyMsg', true, formCtrl); + scope.errorMsg = ""; + stopWatch(); + + // if we had an error, then check on the current valFormManager to see if it's + // now valid, if it is it means that the containing form (i.e. the form rendering) + // properties for an element/content type) is now valid which means we can clear + // the parent's valPropertyMsg if there is one. This will only occur with complex editors + // where we have nested umb-property components. + if (hadError) { + scope.$evalAsync(function () { + if (valFormManager.isValid()) { + // TODO: I think we might have to use formCtrl.$$parentForm here since when using inline editing like + // nestedcontent style, there won't be a 'parent' valFormManager, or will there? i'm unsure + var parentValidationKey = umbPropCtrl.getParentValidationPath(); + if (parentValidationKey) { + var parentForm = formCtrl.$$parentForm; + serverValidationManager.removePropertyError(parentValidationKey, currentCulture, "", currentSegment); + } + } + }); + + + } + } + function checkValidationStatus() { if (formCtrl.$invalid) { //first we need to check if the valPropertyMsg validity is invalid @@ -145,12 +182,11 @@ function valPropertyMsg(serverValidationManager, localizationService) { } //if there are any errors in the current property form that are not valPropertyMsg else if (_.without(_.keys(formCtrl.$error), "valPropertyMsg").length > 0) { - + // errors exist, but if the property is NOT mandatory and has no value, the errors should be cleared if (isMandatory !== undefined && isMandatory === false && !currentProperty.value) { - hasError = false; - showValidation = false; - scope.errorMsg = ""; + + resetError(); // if there's no value, the controls can be reset, which clears the error state on formCtrl for (let control of formCtrl.$getControls()) { @@ -167,13 +203,11 @@ function valPropertyMsg(serverValidationManager, localizationService) { } } else { - hasError = false; - scope.errorMsg = ""; + resetError(); } } else { - hasError = false; - scope.errorMsg = ""; + resetError(); } } @@ -203,17 +237,14 @@ function valPropertyMsg(serverValidationManager, localizationService) { startWatch(); } else if (!hasError) { - scope.errorMsg = ""; - stopWatch(); + resetError(); } })); //listen for the forms saved event unsubscribe.push(scope.$on("formSubmitted", function (ev, args) { showValidation = false; - scope.errorMsg = ""; - formCtrl.$setValidity('valPropertyMsg', true); - stopWatch(); + resetError(); })); //listen for server validation changes @@ -224,7 +255,7 @@ function valPropertyMsg(serverValidationManager, localizationService) { // indicate that a content property is invalid at the property level since developers may not actually implement // the correct field validation in their property editors. - if (scope.currentProperty) { //this can be null if no property was assigned + if (scope.currentProperty) { //this can be null if no property was assigned, TODO: I don't believe it can? If it was null we'd get errors above function serverValidationManagerCallback(isValid, propertyErrors, allErrors) { hasError = !isValid; @@ -232,14 +263,11 @@ function valPropertyMsg(serverValidationManager, localizationService) { //set the error message to the server message scope.errorMsg = propertyErrors[0].errorMsg ? propertyErrors[0].errorMsg : labels.propertyHasErrors; //flag that the current validator is invalid - formCtrl.$setValidity('valPropertyMsg', false); + formCtrl.$setValidity('valPropertyMsg', false, formCtrl); startWatch(); } else { - scope.errorMsg = ""; - //flag that the current validator is valid - formCtrl.$setValidity('valPropertyMsg', true); - stopWatch(); + resetError(); } } @@ -248,9 +276,8 @@ function valPropertyMsg(serverValidationManager, localizationService) { "", serverValidationManagerCallback, currentSegment - ) + ) ); - } //when the scope is disposed we need to unsubscribe diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/validation/valserver.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/validation/valserver.directive.js index 701a344838..f002d83360 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/validation/valserver.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/validation/valserver.directive.js @@ -61,8 +61,8 @@ function valServer(serverValidationManager) { return propertyValidationPath ? propertyValidationPath : currentProperty.alias; } - //Need to watch the value model for it to change, previously we had subscribed to - //modelCtrl.$viewChangeListeners but this is not good enough if you have an editor that + // Need to watch the value model for it to change, previously we had subscribed to + // modelCtrl.$viewChangeListeners but this is not good enough if you have an editor that // doesn't specifically have a 2 way ng binding. This is required because when we // have a server error we actually invalidate the form which means it cannot be // resubmitted. So once a field is changed that has a server error assigned to it @@ -81,6 +81,7 @@ function valServer(serverValidationManager) { if (modelCtrl.$invalid) { modelCtrl.$setValidity('valServer', true); + console.log("valServer cleared (watch)"); //clear the server validation entry serverValidationManager.removePropertyError(getPropertyValidationKey(), currentCulture, fieldName, currentSegment); @@ -101,12 +102,14 @@ function valServer(serverValidationManager) { function serverValidationManagerCallback(isValid, propertyErrors, allErrors) { if (!isValid) { modelCtrl.$setValidity('valServer', false); + console.log("valServer error"); //assign an error msg property to the current validator modelCtrl.errorMsg = propertyErrors[0].errorMsg; startWatch(); } else { modelCtrl.$setValidity('valServer', true); + console.log("valServer cleared"); //reset the error message modelCtrl.errorMsg = ""; stopWatch(); diff --git a/src/Umbraco.Web.UI.Client/src/common/services/servervalidationmgr.service.js b/src/Umbraco.Web.UI.Client/src/common/services/servervalidationmgr.service.js index 5e65f49794..c6382f1f73 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/servervalidationmgr.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/servervalidationmgr.service.js @@ -16,10 +16,10 @@ function serverValidationManager($timeout, udiService) { var items = []; /** calls the callback specified with the errors specified, used internally */ - function executeCallback(errorsForCallback, callback, culture, segment) { + function executeCallback(errorsForCallback, callback, culture, segment, isValid) { callback.apply(instance, [ - false, // pass in a value indicating it is invalid + isValid, // pass in a value indicating it is invalid errorsForCallback, // pass in the errors for this item items, // pass in all errors in total culture, // pass the culture that we are listing for. @@ -99,21 +99,21 @@ function serverValidationManager($timeout, udiService) { //its a field error callback var fieldErrors = getFieldErrors(cb.fieldName); if (fieldErrors.length > 0) { - executeCallback(fieldErrors, cb.callback, cb.culture, cb.segment); + executeCallback(fieldErrors, cb.callback, cb.culture, cb.segment, false); } } else if (cb.propertyAlias != null) { //its a property error var propErrors = getPropertyErrors(cb.propertyAlias, cb.culture, cb.segment, cb.fieldName); if (propErrors.length > 0) { - executeCallback(propErrors, cb.callback, cb.culture, cb.segment); + executeCallback(propErrors, cb.callback, cb.culture, cb.segment, false); } } else { //its a variant error var variantErrors = getVariantErrors(cb.culture, cb.segment); if (variantErrors.length > 0) { - executeCallback(variantErrors, cb.callback, cb.culture, cb.segment); + executeCallback(variantErrors, cb.callback, cb.culture, cb.segment, false); } } } @@ -247,7 +247,7 @@ function serverValidationManager($timeout, udiService) { var cbs = getFieldCallbacks(fieldName); //call each callback for this error for (var cb in cbs) { - executeCallback(errorsForCallback, cbs[cb].callback, null, null); + executeCallback(errorsForCallback, cbs[cb].callback, null, null, false); } } @@ -309,14 +309,14 @@ function serverValidationManager($timeout, udiService) { var cbs = getPropertyCallbacks(propertyAlias, culture, fieldName, segment); //call each callback for this error for (var cb in cbs) { - executeCallback(errorsForCallback, cbs[cb].callback, culture, segment); + executeCallback(errorsForCallback, cbs[cb].callback, culture, segment, false); } //execute variant specific callbacks here too when a propery error is added var variantCbs = getVariantCallbacks(culture, segment); //call each callback for this error for (var cb in variantCbs) { - executeCallback(errorsForCallback, variantCbs[cb].callback, culture, segment); + executeCallback(errorsForCallback, variantCbs[cb].callback, culture, segment, false); } } @@ -679,7 +679,18 @@ function serverValidationManager($timeout, udiService) { //remove the item items = _.reject(items, function (item) { - return (item.propertyAlias === propertyAlias && item.culture === culture && item.segment === segment && (item.fieldName === fieldName || (fieldName === undefined || fieldName === ""))); + var found = (item.propertyAlias === propertyAlias && item.culture === culture && item.segment === segment && (item.fieldName === fieldName || (fieldName === undefined || fieldName === ""))); + if (found) { + //find all errors for this item + var errorsForCallback = getPropertyErrors(propertyAlias, culture, segment, fieldName); + //we should now call all of the call backs registered for this error + var cbs = getPropertyCallbacks(propertyAlias, culture, fieldName, segment); + //call each callback for this error to tell them it is now valid + for (var cb in cbs) { + executeCallback(errorsForCallback, cbs[cb].callback, culture, segment, true); + } + } + return found; }); }, diff --git a/src/Umbraco.Web.UI.Client/src/less/main.less b/src/Umbraco.Web.UI.Client/src/less/main.less index 57d867ccd5..ce20b8dc88 100644 --- a/src/Umbraco.Web.UI.Client/src/less/main.less +++ b/src/Umbraco.Web.UI.Client/src/less/main.less @@ -151,7 +151,7 @@ h6.-black { } } -.umb-property:last-of-type .umb-control-group { +umb-property:last-of-type .umb-control-group { &::after { margin-top: 0px; height: 0; diff --git a/src/Umbraco.Web.UI.Client/src/views/components/property/umb-property.html b/src/Umbraco.Web.UI.Client/src/views/components/property/umb-property.html index be8495c30a..b3aa7735a2 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/property/umb-property.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/property/umb-property.html @@ -1,31 +1,32 @@
        -
        +
        -
        {{ getValidationPath() }}
        +
        {{ vm.getValidationPath() }}
        -
        - - {{inheritsFrom}} +
        + + {{vm.inheritsFrom}} -
        diff --git a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/DatabaseDataSource.cs b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/DatabaseDataSource.cs index 694dac04df..f62014a368 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/DataSource/DatabaseDataSource.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/DataSource/DatabaseDataSource.cs @@ -208,7 +208,7 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource if (dto.EditData == null) { if (Debugger.IsAttached) - throw new Exception("Missing cmsContentNu edited content for node " + dto.Id + ", consider rebuilding."); + throw new InvalidOperationException("Missing cmsContentNu edited content for node " + dto.Id + ", consider rebuilding."); Current.Logger.Warn("Missing cmsContentNu edited content for node {NodeId}, consider rebuilding.", dto.Id); } else @@ -235,7 +235,7 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource if (dto.PubData == null) { if (Debugger.IsAttached) - throw new Exception("Missing cmsContentNu published content for node " + dto.Id + ", consider rebuilding."); + throw new InvalidOperationException("Missing cmsContentNu published content for node " + dto.Id + ", consider rebuilding."); Current.Logger.Warn("Missing cmsContentNu published content for node {NodeId}, consider rebuilding.", dto.Id); } else @@ -274,7 +274,7 @@ namespace Umbraco.Web.PublishedCache.NuCache.DataSource private static ContentNodeKit CreateMediaNodeKit(ContentSourceDto dto) { if (dto.EditData == null) - throw new Exception("No data for media " + dto.Id); + throw new InvalidOperationException("No data for media " + dto.Id); var nested = DeserializeNestedData(dto.EditData); From 6004d7adaacd65d74510078d47f1e5b74f2f8a61 Mon Sep 17 00:00:00 2001 From: Shannon Date: Tue, 7 Jul 2020 13:03:54 +1000 Subject: [PATCH 300/377] seems to work with inline now --- .../validation/valpropertymsg.directive.js | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/validation/valpropertymsg.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/validation/valpropertymsg.directive.js index 30ba459baf..e44e36999a 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/validation/valpropertymsg.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/validation/valpropertymsg.directive.js @@ -157,15 +157,21 @@ function valPropertyMsg(serverValidationManager, localizationService, angularHel // where we have nested umb-property components. if (hadError) { scope.$evalAsync(function () { - if (valFormManager.isValid()) { - // TODO: I think we might have to use formCtrl.$$parentForm here since when using inline editing like - // nestedcontent style, there won't be a 'parent' valFormManager, or will there? i'm unsure - var parentValidationKey = umbPropCtrl.getParentValidationPath(); - if (parentValidationKey) { - var parentForm = formCtrl.$$parentForm; + + // we need to navigate the parentForm here, unfortunately there's no real alternative unless we create our own directive + // of some sort but that would also get messy. This works though since in this case we're always going to be in the property + // form and the parent form about this will contain any invalid flags for all the other sibling properties. So when that is + // no longer invalid, we can check if we have a parent validation key (meaning we'd be nested inside of umb-property) and + // we can clear that server error. + // TODO: If there is another server error for this property though this might clear it inadvertently, at this time I'm unsure how to deal with that. + var parentValidationKey = umbPropCtrl.getParentValidationPath(); + if (parentValidationKey) { + var parentForm = formCtrl.$$parentForm; + if (parentForm && !parentForm.$invalid) { serverValidationManager.removePropertyError(parentValidationKey, currentCulture, "", currentSegment); } } + }); From f4e31e1dbaf15830f50ca5da5d4271d0fa9c906f Mon Sep 17 00:00:00 2001 From: Shannon Date: Tue, 7 Jul 2020 14:40:55 +1000 Subject: [PATCH 301/377] Ensures valid/non valid errors are handled all the time with the serverValidationManager (need to write tests for that), tries to recursively clear property errors, thought that works it clears errors when there are other child nested errors, need to rethink some things. --- .../property/umbproperty.directive.js | 3 +- .../validation/valpropertymsg.directive.js | 54 +++++++++++++++---- .../services/servervalidationmgr.service.js | 35 +++++------- 3 files changed, 60 insertions(+), 32 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/property/umbproperty.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/property/umbproperty.directive.js index 1d100494f6..a67f51bfed 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/property/umbproperty.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/property/umbproperty.directive.js @@ -14,7 +14,8 @@ controllerAs: 'vm', transclude: true, require: { - parentUmbProperty: '?^^umbProperty' + parentUmbProperty: '?^^umbProperty', + parentForm: '?^^form' }, bindings: { property: "=", diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/validation/valpropertymsg.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/validation/valpropertymsg.directive.js index e44e36999a..c586c88d38 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/validation/valpropertymsg.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/validation/valpropertymsg.directive.js @@ -158,19 +158,53 @@ function valPropertyMsg(serverValidationManager, localizationService, angularHel if (hadError) { scope.$evalAsync(function () { - // we need to navigate the parentForm here, unfortunately there's no real alternative unless we create our own directive - // of some sort but that would also get messy. This works though since in this case we're always going to be in the property - // form and the parent form about this will contain any invalid flags for all the other sibling properties. So when that is - // no longer invalid, we can check if we have a parent validation key (meaning we'd be nested inside of umb-property) and - // we can clear that server error. - // TODO: If there is another server error for this property though this might clear it inadvertently, at this time I'm unsure how to deal with that. - var parentValidationKey = umbPropCtrl.getParentValidationPath(); - if (parentValidationKey) { - var parentForm = formCtrl.$$parentForm; - if (parentForm && !parentForm.$invalid) { + // TODO: This does not work :( :( :( + // We cannot clear a val-property-msg because another nested child might have server validation errors too. + // I 'think' we might be able to set the UI validation of this val-property-msg based on the child validators as well + // as the server validator so it can 'just' unset itself if all child validators are cleared. Can it be done? + + // Here we loop over the umbProperty hierarchy to see if we should clear the val-property-msg server validation key. + // we will clear the key if the parent for is valid, or if the parent form is only invalid due to a single val-property-msg error. + var currUmbProperty = umbPropCtrl; + var parentValidationKey = currUmbProperty.getParentValidationPath(); + while (currUmbProperty && parentValidationKey) { + + if (!currUmbProperty.parentForm.$invalid || (_.keys(currUmbProperty.parentForm.$error).length === 1 && currUmbProperty.parentForm.$error.valPropertyMsg)) { serverValidationManager.removePropertyError(parentValidationKey, currentCulture, "", currentSegment); + + // re-assign and loop + if (currUmbProperty !== umbPropCtrl.parentUmbProperty) { + currUmbProperty = umbPropCtrl.parentUmbProperty; + parentValidationKey = currUmbProperty ? currUmbProperty.getParentValidationPath() : null; + } + else { + break; + } + } + else { + break; } } + + //// we need to navigate the parentForm here, unfortunately there's no real alternative unless we create our own directive + //// of some sort but that would also get messy. This works though since in this case we're always going to be in the property + //// form and the parent form about this will contain any invalid flags for all the other sibling properties. So when that is + //// no longer invalid, we can check if we have a parent validation key (meaning we'd be nested inside of umb-property) and + //// we can clear that server error. + //// TODO: If there is another server error for this property though this might clear it inadvertently, at this time I'm unsure how to deal with that. + //var parentValidationKey = umbPropCtrl.getParentValidationPath(); + //if (parentValidationKey) { + // // TODO: Instead of using the parent form, can we 'just' use umbProperty again which itself can check if it's + // // parent form is valid? then below we can call in a loop each parent umb property check if it has a parent validation + // // path and check if it's form is valid, this will recursively perform this logic up the chain. + // var parentForm = formCtrl.$$parentForm; + // if (parentForm && !parentForm.$invalid) { + // // TODO: Though this works for one level, if you have errors at level 1 and 2, clear errors at level when + // // and then level 2, then only the val-property-msg is cleared at level 1 and not also at level 0. + // // So we still need to recurse up the chain to deal with this + // serverValidationManager.removePropertyError(parentValidationKey, currentCulture, "", currentSegment); + // } + //} }); diff --git a/src/Umbraco.Web.UI.Client/src/common/services/servervalidationmgr.service.js b/src/Umbraco.Web.UI.Client/src/common/services/servervalidationmgr.service.js index c6382f1f73..8999efda64 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/servervalidationmgr.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/servervalidationmgr.service.js @@ -92,29 +92,27 @@ function serverValidationManager($timeout, udiService) { }); } + /** Call all registered callbacks indicating if the data they are subscribed to is valid or invalid */ function notifyCallbacks() { for (var i = 0; i < callbacks.length; i++) { var cb = callbacks[i]; if (cb.propertyAlias === null && cb.fieldName !== null) { //its a field error callback var fieldErrors = getFieldErrors(cb.fieldName); - if (fieldErrors.length > 0) { - executeCallback(fieldErrors, cb.callback, cb.culture, cb.segment, false); - } + const valid = fieldErrors.length === 0; + executeCallback(fieldErrors, cb.callback, cb.culture, cb.segment, valid); } else if (cb.propertyAlias != null) { //its a property error var propErrors = getPropertyErrors(cb.propertyAlias, cb.culture, cb.segment, cb.fieldName); - if (propErrors.length > 0) { - executeCallback(propErrors, cb.callback, cb.culture, cb.segment, false); - } + const valid = propErrors.length === 0; + executeCallback(propErrors, cb.callback, cb.culture, cb.segment, valid); } else { //its a variant error var variantErrors = getVariantErrors(cb.culture, cb.segment); - if (variantErrors.length > 0) { - executeCallback(variantErrors, cb.callback, cb.culture, cb.segment, false); - } + const valid = variantErrors.length === 0; + executeCallback(variantErrors, cb.callback, cb.culture, cb.segment, valid); } } } @@ -678,20 +676,15 @@ function serverValidationManager($timeout, udiService) { } //remove the item + var count = items.length; items = _.reject(items, function (item) { - var found = (item.propertyAlias === propertyAlias && item.culture === culture && item.segment === segment && (item.fieldName === fieldName || (fieldName === undefined || fieldName === ""))); - if (found) { - //find all errors for this item - var errorsForCallback = getPropertyErrors(propertyAlias, culture, segment, fieldName); - //we should now call all of the call backs registered for this error - var cbs = getPropertyCallbacks(propertyAlias, culture, fieldName, segment); - //call each callback for this error to tell them it is now valid - for (var cb in cbs) { - executeCallback(errorsForCallback, cbs[cb].callback, culture, segment, true); - } - } - return found; + return (item.propertyAlias === propertyAlias && item.culture === culture && item.segment === segment && (item.fieldName === fieldName || (fieldName === undefined || fieldName === ""))); }); + + if (items.length !== count) { + // removal was successful, re-notify all subscribers + notify(); + } }, reset: reset, From 8814875b95d9586863e1a8e7f2215a5c09056b2c Mon Sep 17 00:00:00 2001 From: Shannon Date: Tue, 7 Jul 2020 16:05:54 +1000 Subject: [PATCH 302/377] fixes error logging --- .../Compose/DatabaseServerRegistrarAndMessengerComponent.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web/Compose/DatabaseServerRegistrarAndMessengerComponent.cs b/src/Umbraco.Web/Compose/DatabaseServerRegistrarAndMessengerComponent.cs index 6dfad8c45b..2fa9d80779 100644 --- a/src/Umbraco.Web/Compose/DatabaseServerRegistrarAndMessengerComponent.cs +++ b/src/Umbraco.Web/Compose/DatabaseServerRegistrarAndMessengerComponent.cs @@ -218,7 +218,7 @@ namespace Umbraco.Web.Compose } catch (Exception e) { - _logger.Error("Failed (will repeat).", e); + _logger.Error(e, "Failed (will repeat)."); } return true; // repeat } From a233264c8e3abaa935dfb7d6f43b9a22642cff73 Mon Sep 17 00:00:00 2001 From: Shannon Date: Tue, 7 Jul 2020 16:26:32 +1000 Subject: [PATCH 303/377] Ensure index rebuilding doesn't short circuit if one of the populators fails --- src/Umbraco.Examine/IndexRebuilder.cs | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/src/Umbraco.Examine/IndexRebuilder.cs b/src/Umbraco.Examine/IndexRebuilder.cs index 02fb4fc2da..b14ff25c57 100644 --- a/src/Umbraco.Examine/IndexRebuilder.cs +++ b/src/Umbraco.Examine/IndexRebuilder.cs @@ -3,6 +3,8 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Examine; +using Umbraco.Core.Composing; +using Umbraco.Core.Logging; namespace Umbraco.Examine { @@ -12,12 +14,20 @@ namespace Umbraco.Examine ///
        public class IndexRebuilder { + private readonly IProfilingLogger _logger; private readonly IEnumerable _populators; public IExamineManager ExamineManager { get; } + [Obsolete("Use constructor with all dependencies")] public IndexRebuilder(IExamineManager examineManager, IEnumerable populators) + : this(Current.ProfilingLogger, examineManager, populators) + { + } + + public IndexRebuilder(IProfilingLogger logger, IExamineManager examineManager, IEnumerable populators) { _populators = populators; + _logger = logger; ExamineManager = examineManager; } @@ -53,7 +63,14 @@ namespace Umbraco.Examine // run each populator over the indexes foreach(var populator in _populators) { - populator.Populate(indexes); + try + { + populator.Populate(indexes); + } + catch (Exception e) + { + _logger.Error(e, "Index populating failed for populator {Populator}", populator.GetType()); + } } } From 6aa49242f2e2c4453a3dc2d6cd9032151efaef83 Mon Sep 17 00:00:00 2001 From: Shannon Date: Tue, 7 Jul 2020 17:53:33 +1000 Subject: [PATCH 304/377] Don't try to reuse db instances, thsi can result in potential zombie transactions --- src/Umbraco.Core/Runtime/SqlMainDomLock.cs | 176 +++++++++------------ 1 file changed, 74 insertions(+), 102 deletions(-) diff --git a/src/Umbraco.Core/Runtime/SqlMainDomLock.cs b/src/Umbraco.Core/Runtime/SqlMainDomLock.cs index 5f5d0d607f..8e2e688d66 100644 --- a/src/Umbraco.Core/Runtime/SqlMainDomLock.cs +++ b/src/Umbraco.Core/Runtime/SqlMainDomLock.cs @@ -56,14 +56,13 @@ namespace Umbraco.Core.Runtime _logger.Debug("Acquiring lock..."); - var db = GetDatabase(); - var tempId = Guid.NewGuid().ToString(); + using var db = _dbFactory.CreateDatabase(); + using var transaction = db.GetTransaction(IsolationLevel.ReadCommitted); + try { - db.BeginTransaction(IsolationLevel.ReadCommitted); - try { // wait to get a write lock @@ -73,6 +72,8 @@ namespace Umbraco.Core.Runtime { if (IsLockTimeoutException(ex)) { + // TODO: Do we want to retry? We haven't seen any timeout exceptions here so not sure it's important at this stage + _logger.Error(ex, "Sql timeout occurred, could not acquire MainDom."); _hasError = true; return false; @@ -82,15 +83,12 @@ namespace Umbraco.Core.Runtime throw; } - var result = InsertLockRecord(tempId); //we change the row to a random Id to signal other MainDom to shutdown + var result = InsertLockRecord(tempId, db); //we change the row to a random Id to signal other MainDom to shutdown if (result == RecordPersistenceType.Insert) { // if we've inserted, then there was no MainDom so we can instantly acquire - // TODO: see the other TODO, could we just delete the row and that would indicate that we - // are MainDom? then we don't leave any orphan rows behind. - - InsertLockRecord(_lockId); // so update with our appdomain id + InsertLockRecord(_lockId, db); // so update with our appdomain id _logger.Debug("Acquired with ID {LockId}", _lockId); return true; } @@ -100,16 +98,12 @@ namespace Umbraco.Core.Runtime } catch (Exception ex) { - ResetDatabase(); // unexpected _logger.Error(ex, "Unexpected error, cannot acquire MainDom"); _hasError = true; return false; } - finally - { - db?.CompleteTransaction(); - } + return await WaitForExistingAsync(tempId, millisecondsTimeout); } @@ -160,12 +154,10 @@ namespace Umbraco.Core.Runtime if (_cancellationTokenSource.IsCancellationRequested) return; - var db = GetDatabase(); - + using var db = _dbFactory.CreateDatabase(); + using var transaction = db.GetTransaction(IsolationLevel.ReadCommitted); try { - db.BeginTransaction(IsolationLevel.ReadCommitted); - // get a read lock _sqlServerSyntax.ReadLock(db, Constants.Locks.MainDom); @@ -173,7 +165,7 @@ namespace Umbraco.Core.Runtime // we are still the maindom. An empty value might be better because then we won't have any orphan rows // if the app is terminated. Could that work? - if (!IsMainDomValue(_lockId)) + if (!IsMainDomValue(_lockId, db)) { // we are no longer main dom, another one has come online, exit _mainDomChanging = true; @@ -183,7 +175,11 @@ namespace Umbraco.Core.Runtime } catch (Exception ex) { - ResetDatabase(); + // TODO: We need to make this more resilient to Azure Sql and timeout issues and not just exit causing the + // app to restart. We don't wan to restart unless we know there's another appdomain online + + //ResetDatabase(); + // unexpected _logger.Error(ex, "Unexpected error, listening is canceled."); _hasError = true; @@ -191,30 +187,13 @@ namespace Umbraco.Core.Runtime } finally { - db?.CompleteTransaction(); + transaction.Complete(); } } } } - private void ResetDatabase() - { - if (_db.InTransaction) - _db.AbortTransaction(); - _db.Dispose(); - _db = null; - } - - private IUmbracoDatabase GetDatabase() - { - if (_db != null) - return _db; - - _db = _dbFactory.CreateDatabase(); - return _db; - } - /// /// Wait for any existing MainDom to release so we can continue booting /// @@ -227,67 +206,64 @@ namespace Umbraco.Core.Runtime return Task.Run(() => { - var db = GetDatabase(); + // ensure this is disposed when this thread ends + using var db = _dbFactory.CreateDatabase(); + var watch = new Stopwatch(); watch.Start(); - while(true) + while (true) { // poll very often, we need to take over as fast as we can - Thread.Sleep(100); + Thread.Sleep(500); - try + using (var transaction = db.GetTransaction(IsolationLevel.ReadCommitted)) { - db.BeginTransaction(IsolationLevel.ReadCommitted); - - // get a read lock - _sqlServerSyntax.ReadLock(db, Constants.Locks.MainDom); - - // the row - var mainDomRows = db.Fetch("SELECT * FROM umbracoKeyValue WHERE [key] = @key", new { key = MainDomKey }); - - if (mainDomRows.Count == 0 || mainDomRows[0].Value == updatedTempId) + try { - // the other main dom has updated our record - // Or the other maindom shutdown super fast and just deleted the record - // which indicates that we - // can acquire it and it has shutdown. + // get a read lock + _sqlServerSyntax.ReadLock(db, Constants.Locks.MainDom); - _sqlServerSyntax.WriteLock(db, Constants.Locks.MainDom); + // the row + var mainDomRows = db.Fetch("SELECT * FROM umbracoKeyValue WHERE [key] = @key", new { key = MainDomKey }); - // so now we update the row with our appdomain id - InsertLockRecord(_lockId); - _logger.Debug("Acquired with ID {LockId}", _lockId); - return true; + if (mainDomRows.Count == 0 || mainDomRows[0].Value == updatedTempId) + { + // the other main dom has updated our record + // Or the other maindom shutdown super fast and just deleted the record + // which indicates that we + // can acquire it and it has shutdown. + + _sqlServerSyntax.WriteLock(db, Constants.Locks.MainDom); + + // so now we update the row with our appdomain id + InsertLockRecord(_lockId, db); + _logger.Debug("Acquired with ID {LockId}", _lockId); + transaction.Complete(); + return true; + } + else if (mainDomRows.Count == 1 && !mainDomRows[0].Value.StartsWith(tempId)) + { + // in this case, the prefixed ID is different which means + // another new AppDomain has come online and is wanting to take over. In that case, we will not + // acquire. + + _logger.Debug("Cannot acquire, another booting application detected."); + return false; + } } - else if (mainDomRows.Count == 1 && !mainDomRows[0].Value.StartsWith(tempId)) + catch (Exception ex) { - // in this case, the prefixed ID is different which means - // another new AppDomain has come online and is wanting to take over. In that case, we will not - // acquire. - - _logger.Debug("Cannot acquire, another booting application detected."); - - return false; - } - } - catch (Exception ex) - { - ResetDatabase(); - - if (IsLockTimeoutException(ex)) - { - _logger.Error(ex, "Sql timeout occurred, waiting for existing MainDom is canceled."); + if (IsLockTimeoutException(ex)) + { + _logger.Error(ex, "Sql timeout occurred, waiting for existing MainDom is canceled."); + _hasError = true; + return false; + } + // unexpected + _logger.Error(ex, "Unexpected error, waiting for existing MainDom is canceled."); _hasError = true; return false; } - // unexpected - _logger.Error(ex, "Unexpected error, waiting for existing MainDom is canceled."); - _hasError = true; - return false; - } - finally - { - db?.CompleteTransaction(); } if (watch.ElapsedMilliseconds >= millisecondsTimeout) @@ -301,21 +277,22 @@ namespace Umbraco.Core.Runtime _logger.Debug("Timeout elapsed, assuming orphan row, acquiring MainDom."); + using var transaction = db.GetTransaction(IsolationLevel.ReadCommitted); + try { - db.BeginTransaction(IsolationLevel.ReadCommitted); - _sqlServerSyntax.WriteLock(db, Constants.Locks.MainDom); // so now we update the row with our appdomain id - InsertLockRecord(_lockId); + InsertLockRecord(_lockId, db); _logger.Debug("Acquired with ID {LockId}", _lockId); + + transaction.Complete(); + return true; } catch (Exception ex) { - ResetDatabase(); - if (IsLockTimeoutException(ex)) { // something is wrong, we cannot acquire, not much we can do @@ -327,10 +304,6 @@ namespace Umbraco.Core.Runtime _hasError = true; return false; } - finally - { - db?.CompleteTransaction(); - } } } }, _cancellationTokenSource.Token); @@ -339,9 +312,8 @@ namespace Umbraco.Core.Runtime /// /// Inserts or updates the key/value row /// - private RecordPersistenceType InsertLockRecord(string id) + private RecordPersistenceType InsertLockRecord(string id, IUmbracoDatabase db) { - var db = GetDatabase(); return db.InsertOrUpdate(new KeyValueDto { Key = MainDomKey, @@ -354,9 +326,8 @@ namespace Umbraco.Core.Runtime /// Checks if the DB row value is equals the value /// /// - private bool IsMainDomValue(string val) + private bool IsMainDomValue(string val, IUmbracoDatabase db) { - var db = GetDatabase(); return db.ExecuteScalar("SELECT COUNT(*) FROM umbracoKeyValue WHERE [key] = @key AND [value] = @val", new { key = MainDomKey, val = val }) == 1; } @@ -385,7 +356,10 @@ namespace Umbraco.Core.Runtime if (_dbFactory.Configured) { - var db = GetDatabase(); + // ensure this is disposed when this thread ends + using var db = _dbFactory.CreateDatabase(); + using var transaction = db.GetTransaction(IsolationLevel.ReadCommitted); + try { db.BeginTransaction(IsolationLevel.ReadCommitted); @@ -412,14 +386,12 @@ namespace Umbraco.Core.Runtime } catch (Exception ex) { - ResetDatabase(); _logger.Error(ex, "Unexpected error during dipsose."); _hasError = true; } finally { - db?.CompleteTransaction(); - ResetDatabase(); + transaction.Complete(); } } } From 65101beaf668697300b2a3bd691679e67dec3a73 Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 8 Jul 2020 13:00:30 +1000 Subject: [PATCH 305/377] transactions for sqlmaindom --- src/Umbraco.Core/Runtime/SqlMainDomLock.cs | 24 ++++++++++++++-------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/src/Umbraco.Core/Runtime/SqlMainDomLock.cs b/src/Umbraco.Core/Runtime/SqlMainDomLock.cs index 8e2e688d66..049a7a9400 100644 --- a/src/Umbraco.Core/Runtime/SqlMainDomLock.cs +++ b/src/Umbraco.Core/Runtime/SqlMainDomLock.cs @@ -103,6 +103,10 @@ namespace Umbraco.Core.Runtime _hasError = true; return false; } + finally + { + transaction.Complete(); + } return await WaitForExistingAsync(tempId, millisecondsTimeout); @@ -237,8 +241,7 @@ namespace Umbraco.Core.Runtime // so now we update the row with our appdomain id InsertLockRecord(_lockId, db); - _logger.Debug("Acquired with ID {LockId}", _lockId); - transaction.Complete(); + _logger.Debug("Acquired with ID {LockId}", _lockId); return true; } else if (mainDomRows.Count == 1 && !mainDomRows[0].Value.StartsWith(tempId)) @@ -264,6 +267,10 @@ namespace Umbraco.Core.Runtime _hasError = true; return false; } + finally + { + transaction.Complete(); + } } if (watch.ElapsedMilliseconds >= millisecondsTimeout) @@ -286,9 +293,6 @@ namespace Umbraco.Core.Runtime // so now we update the row with our appdomain id InsertLockRecord(_lockId, db); _logger.Debug("Acquired with ID {LockId}", _lockId); - - transaction.Complete(); - return true; } catch (Exception ex) @@ -304,6 +308,10 @@ namespace Umbraco.Core.Runtime _hasError = true; return false; } + finally + { + transaction.Complete(); + } } } }, _cancellationTokenSource.Token); @@ -362,8 +370,6 @@ namespace Umbraco.Core.Runtime try { - db.BeginTransaction(IsolationLevel.ReadCommitted); - // get a write lock _sqlServerSyntax.WriteLock(db, Constants.Locks.MainDom); @@ -376,12 +382,12 @@ namespace Umbraco.Core.Runtime if (_mainDomChanging) { _logger.Debug("Releasing MainDom, updating row, new application is booting."); - db.Execute($"UPDATE umbracoKeyValue SET [value] = [value] + '{UpdatedSuffix}' WHERE [key] = @key", new { key = MainDomKey }); + var count = db.Execute($"UPDATE umbracoKeyValue SET [value] = [value] + '{UpdatedSuffix}' WHERE [key] = @key", new { key = MainDomKey }); } else { _logger.Debug("Releasing MainDom, deleting row, application is shutting down."); - db.Execute("DELETE FROM umbracoKeyValue WHERE [key] = @key", new { key = MainDomKey }); + var count = db.Execute("DELETE FROM umbracoKeyValue WHERE [key] = @key", new { key = MainDomKey }); } } catch (Exception ex) From 53db2df3907eabf76c1117a7d40a260ec1dcfb58 Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 8 Jul 2020 13:01:39 +1000 Subject: [PATCH 306/377] Fix for PerformScheduledPublishInternal, don't use yield returns within a using! this will not work and transactions/connections will be lost --- .../Services/Implement/ContentService.cs | 34 ++++++++++++------- 1 file changed, 22 insertions(+), 12 deletions(-) diff --git a/src/Umbraco.Core/Services/Implement/ContentService.cs b/src/Umbraco.Core/Services/Implement/ContentService.cs index 1558b0170b..6b7a858d3e 100644 --- a/src/Umbraco.Core/Services/Implement/ContentService.cs +++ b/src/Umbraco.Core/Services/Implement/ContentService.cs @@ -1369,6 +1369,8 @@ namespace Umbraco.Core.Services.Implement { var evtMsgs = EventMessagesFactory.Get(); + var results = new List(); + using (var scope = ScopeProvider.CreateScope()) { scope.WriteLock(Constants.Locks.ContentTree); @@ -1377,7 +1379,6 @@ namespace Umbraco.Core.Services.Implement foreach (var d in _documentRepository.GetContentForRelease(date)) { - PublishResult result; if (d.ContentType.VariesByCulture()) { //find which cultures have pending schedules @@ -1391,7 +1392,10 @@ namespace Umbraco.Core.Services.Implement var saveEventArgs = new ContentSavingEventArgs(d, evtMsgs); if (scope.Events.DispatchCancelable(Saving, this, saveEventArgs, nameof(Saving))) - yield return new PublishResult(PublishResultType.FailedPublishCancelledByEvent, evtMsgs, d); + { + results.Add(new PublishResult(PublishResultType.FailedPublishCancelledByEvent, evtMsgs, d)); + continue; + } var publishing = true; foreach (var culture in pendingCultures) @@ -1413,6 +1417,8 @@ namespace Umbraco.Core.Services.Implement if (!publishing) break; // no point continuing } + PublishResult result; + if (d.Trashed) result = new PublishResult(PublishResultType.FailedPublishIsTrashed, evtMsgs, d); else if (!publishing) @@ -1420,31 +1426,30 @@ namespace Umbraco.Core.Services.Implement else result = CommitDocumentChangesInternal(scope, d, saveEventArgs, allLangs, d.WriterId); - if (result.Success == false) Logger.Error(null, "Failed to publish document id={DocumentId}, reason={Reason}.", d.Id, result.Result); - yield return result; + results.Add(result); } else { //Clear this schedule d.ContentSchedule.Clear(ContentScheduleAction.Release, date); - result = d.Trashed + var result = d.Trashed ? new PublishResult(PublishResultType.FailedPublishIsTrashed, evtMsgs, d) : SaveAndPublish(d, userId: d.WriterId); if (result.Success == false) Logger.Error(null, "Failed to publish document id={DocumentId}, reason={Reason}.", d.Id, result.Result); - yield return result; + results.Add(result); } } foreach (var d in _documentRepository.GetContentForExpiration(date)) { - PublishResult result; + if (d.ContentType.VariesByCulture()) { //find which cultures have pending schedules @@ -1458,7 +1463,10 @@ namespace Umbraco.Core.Services.Implement var saveEventArgs = new ContentSavingEventArgs(d, evtMsgs); if (scope.Events.DispatchCancelable(Saving, this, saveEventArgs, nameof(Saving))) - yield return new PublishResult(PublishResultType.FailedPublishCancelledByEvent, evtMsgs, d); + { + results.Add(new PublishResult(PublishResultType.FailedPublishCancelledByEvent, evtMsgs, d)); + continue; + } foreach (var c in pendingCultures) { @@ -1468,20 +1476,20 @@ namespace Umbraco.Core.Services.Implement d.UnpublishCulture(c); } - result = CommitDocumentChangesInternal(scope, d, saveEventArgs, allLangs, d.WriterId); + var result = CommitDocumentChangesInternal(scope, d, saveEventArgs, allLangs, d.WriterId); if (result.Success == false) Logger.Error(null, "Failed to publish document id={DocumentId}, reason={Reason}.", d.Id, result.Result); - yield return result; + results.Add(result); } else { //Clear this schedule d.ContentSchedule.Clear(ContentScheduleAction.Expire, date); - result = Unpublish(d, userId: d.WriterId); + var result = Unpublish(d, userId: d.WriterId); if (result.Success == false) Logger.Error(null, "Failed to unpublish document id={DocumentId}, reason={Reason}.", d.Id, result.Result); - yield return result; + results.Add(result); } @@ -1491,6 +1499,8 @@ namespace Umbraco.Core.Services.Implement scope.Complete(); } + + return results; } // utility 'PublishCultures' func used by SaveAndPublishBranch From 651756d96a673316b0cb2cc8a70975423714ab12 Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 8 Jul 2020 13:39:27 +1000 Subject: [PATCH 307/377] Ensure we don't shutdown MainDom if there is an error while listening, only shutdown if the appdomain is triggered to shutdown, else we'll keep listening/logging --- src/Umbraco.Core/Runtime/SqlMainDomLock.cs | 54 +++++++++------------- 1 file changed, 23 insertions(+), 31 deletions(-) diff --git a/src/Umbraco.Core/Runtime/SqlMainDomLock.cs b/src/Umbraco.Core/Runtime/SqlMainDomLock.cs index 049a7a9400..dc928ed440 100644 --- a/src/Umbraco.Core/Runtime/SqlMainDomLock.cs +++ b/src/Umbraco.Core/Runtime/SqlMainDomLock.cs @@ -6,7 +6,6 @@ using System.Linq; using System.Security.Cryptography; using System.Threading; using System.Threading.Tasks; -using System.Web; using Umbraco.Core.Logging; using Umbraco.Core.Persistence; using Umbraco.Core.Persistence.Dtos; @@ -21,12 +20,11 @@ namespace Umbraco.Core.Runtime private const string MainDomKeyPrefix = "Umbraco.Core.Runtime.SqlMainDom"; private const string UpdatedSuffix = "_updated"; private readonly ILogger _logger; - private IUmbracoDatabase _db; private CancellationTokenSource _cancellationTokenSource = new CancellationTokenSource(); private SqlServerSyntaxProvider _sqlServerSyntax = new SqlServerSyntaxProvider(); private bool _mainDomChanging = false; private readonly UmbracoDatabaseFactory _dbFactory; - private bool _hasError; + private bool _errorDuringAcquiring; private object _locker = new object(); public SqlMainDomLock(ILogger logger) @@ -68,14 +66,12 @@ namespace Umbraco.Core.Runtime // wait to get a write lock _sqlServerSyntax.WriteLock(db, TimeSpan.FromMilliseconds(millisecondsTimeout), Constants.Locks.MainDom); } - catch (Exception ex) + catch(SqlException ex) { if (IsLockTimeoutException(ex)) { - // TODO: Do we want to retry? We haven't seen any timeout exceptions here so not sure it's important at this stage - _logger.Error(ex, "Sql timeout occurred, could not acquire MainDom."); - _hasError = true; + _errorDuringAcquiring = true; return false; } @@ -100,7 +96,7 @@ namespace Umbraco.Core.Runtime { // unexpected _logger.Error(ex, "Unexpected error, cannot acquire MainDom"); - _hasError = true; + _errorDuringAcquiring = true; return false; } finally @@ -114,7 +110,7 @@ namespace Umbraco.Core.Runtime public Task ListenAsync() { - if (_hasError) + if (_errorDuringAcquiring) { _logger.Warn("Could not acquire MainDom, listening is canceled."); return Task.CompletedTask; @@ -140,8 +136,9 @@ namespace Umbraco.Core.Runtime { while (true) { - // poll every 1 second - Thread.Sleep(1000); + // poll every 1.5 second + // local testing shows the actual query to be executed from client/server is approx 300ms but would change depending on environment/IO + Thread.Sleep(2000); if (!_dbFactory.Configured) { @@ -165,10 +162,6 @@ namespace Umbraco.Core.Runtime // get a read lock _sqlServerSyntax.ReadLock(db, Constants.Locks.MainDom); - // TODO: We could in theory just check if the main dom row doesn't exist, that could indicate that - // we are still the maindom. An empty value might be better because then we won't have any orphan rows - // if the app is terminated. Could that work? - if (!IsMainDomValue(_lockId, db)) { // we are no longer main dom, another one has come online, exit @@ -179,15 +172,14 @@ namespace Umbraco.Core.Runtime } catch (Exception ex) { - // TODO: We need to make this more resilient to Azure Sql and timeout issues and not just exit causing the - // app to restart. We don't wan to restart unless we know there's another appdomain online + // unexpected, if this occurs MainDom will be shutdown! + _logger.Error(ex, "Unexpected error during listening."); - //ResetDatabase(); + // We need to keep on listening unless we've been notified by our own AppDomain to shutdown since + // we don't want to shutdown resources controlled by MainDom inadvertently. We'll just keep listening otherwise. + if (_cancellationTokenSource.IsCancellationRequested) + return; - // unexpected - _logger.Error(ex, "Unexpected error, listening is canceled."); - _hasError = true; - return; } finally { @@ -218,7 +210,8 @@ namespace Umbraco.Core.Runtime while (true) { // poll very often, we need to take over as fast as we can - Thread.Sleep(500); + // local testing shows the actual query to be executed from client/server is approx 300ms but would change depending on environment/IO + Thread.Sleep(1000); using (var transaction = db.GetTransaction(IsolationLevel.ReadCommitted)) { @@ -256,15 +249,15 @@ namespace Umbraco.Core.Runtime } catch (Exception ex) { - if (IsLockTimeoutException(ex)) + if (IsLockTimeoutException(ex as SqlException)) { _logger.Error(ex, "Sql timeout occurred, waiting for existing MainDom is canceled."); - _hasError = true; + _errorDuringAcquiring = true; return false; } // unexpected _logger.Error(ex, "Unexpected error, waiting for existing MainDom is canceled."); - _hasError = true; + _errorDuringAcquiring = true; return false; } finally @@ -297,15 +290,15 @@ namespace Umbraco.Core.Runtime } catch (Exception ex) { - if (IsLockTimeoutException(ex)) + if (IsLockTimeoutException(ex as SqlException)) { // something is wrong, we cannot acquire, not much we can do _logger.Error(ex, "Sql timeout occurred, could not forcibly acquire MainDom."); - _hasError = true; + _errorDuringAcquiring = true; return false; } _logger.Error(ex, "Unexpected error, could not forcibly acquire MainDom."); - _hasError = true; + _errorDuringAcquiring = true; return false; } finally @@ -345,7 +338,7 @@ namespace Umbraco.Core.Runtime /// /// /// - private bool IsLockTimeoutException(Exception exception) => exception is SqlException sqlException && sqlException.Number == 1222; + private bool IsLockTimeoutException(SqlException sqlException) => sqlException?.Number == 1222; #region IDisposable Support private bool _disposedValue = false; // To detect redundant calls @@ -393,7 +386,6 @@ namespace Umbraco.Core.Runtime catch (Exception ex) { _logger.Error(ex, "Unexpected error during dipsose."); - _hasError = true; } finally { From a947fa31211a550a1780c4732981db8b2c6f00c1 Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 8 Jul 2020 14:51:35 +1000 Subject: [PATCH 308/377] comments --- src/Umbraco.Core/Runtime/SqlMainDomLock.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Core/Runtime/SqlMainDomLock.cs b/src/Umbraco.Core/Runtime/SqlMainDomLock.cs index dc928ed440..eb053933da 100644 --- a/src/Umbraco.Core/Runtime/SqlMainDomLock.cs +++ b/src/Umbraco.Core/Runtime/SqlMainDomLock.cs @@ -136,7 +136,7 @@ namespace Umbraco.Core.Runtime { while (true) { - // poll every 1.5 second + // poll every couple of seconds // local testing shows the actual query to be executed from client/server is approx 300ms but would change depending on environment/IO Thread.Sleep(2000); From 7819d1acf55a48a023c3d773eae36f6940ba7be6 Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 8 Jul 2020 14:53:53 +1000 Subject: [PATCH 309/377] comments --- src/Umbraco.Core/Runtime/SqlMainDomLock.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Umbraco.Core/Runtime/SqlMainDomLock.cs b/src/Umbraco.Core/Runtime/SqlMainDomLock.cs index eb053933da..fe6acd5485 100644 --- a/src/Umbraco.Core/Runtime/SqlMainDomLock.cs +++ b/src/Umbraco.Core/Runtime/SqlMainDomLock.cs @@ -172,7 +172,6 @@ namespace Umbraco.Core.Runtime } catch (Exception ex) { - // unexpected, if this occurs MainDom will be shutdown! _logger.Error(ex, "Unexpected error during listening."); // We need to keep on listening unless we've been notified by our own AppDomain to shutdown since From 759016185843164cde10cace4b787a8fe46b7624 Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 8 Jul 2020 14:54:26 +1000 Subject: [PATCH 310/377] comments --- src/Umbraco.Core/Runtime/SqlMainDomLock.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Umbraco.Core/Runtime/SqlMainDomLock.cs b/src/Umbraco.Core/Runtime/SqlMainDomLock.cs index fe6acd5485..f9d1889442 100644 --- a/src/Umbraco.Core/Runtime/SqlMainDomLock.cs +++ b/src/Umbraco.Core/Runtime/SqlMainDomLock.cs @@ -201,7 +201,7 @@ namespace Umbraco.Core.Runtime return Task.Run(() => { - // ensure this is disposed when this thread ends + using var db = _dbFactory.CreateDatabase(); var watch = new Stopwatch(); @@ -356,7 +356,6 @@ namespace Umbraco.Core.Runtime if (_dbFactory.Configured) { - // ensure this is disposed when this thread ends using var db = _dbFactory.CreateDatabase(); using var transaction = db.GetTransaction(IsolationLevel.ReadCommitted); From b80dc8f34b1cc8289bcbda74519fa09f5cf8ddbf Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 8 Jul 2020 15:07:12 +1000 Subject: [PATCH 311/377] readability --- src/Umbraco.Core/Runtime/SqlMainDomLock.cs | 192 +++++++++++---------- 1 file changed, 102 insertions(+), 90 deletions(-) diff --git a/src/Umbraco.Core/Runtime/SqlMainDomLock.cs b/src/Umbraco.Core/Runtime/SqlMainDomLock.cs index f9d1889442..209b99aef1 100644 --- a/src/Umbraco.Core/Runtime/SqlMainDomLock.cs +++ b/src/Umbraco.Core/Runtime/SqlMainDomLock.cs @@ -201,7 +201,6 @@ namespace Umbraco.Core.Runtime return Task.Run(() => { - using var db = _dbFactory.CreateDatabase(); var watch = new Stopwatch(); @@ -212,103 +211,116 @@ namespace Umbraco.Core.Runtime // local testing shows the actual query to be executed from client/server is approx 300ms but would change depending on environment/IO Thread.Sleep(1000); - using (var transaction = db.GetTransaction(IsolationLevel.ReadCommitted)) - { - try - { - // get a read lock - _sqlServerSyntax.ReadLock(db, Constants.Locks.MainDom); - - // the row - var mainDomRows = db.Fetch("SELECT * FROM umbracoKeyValue WHERE [key] = @key", new { key = MainDomKey }); - - if (mainDomRows.Count == 0 || mainDomRows[0].Value == updatedTempId) - { - // the other main dom has updated our record - // Or the other maindom shutdown super fast and just deleted the record - // which indicates that we - // can acquire it and it has shutdown. - - _sqlServerSyntax.WriteLock(db, Constants.Locks.MainDom); - - // so now we update the row with our appdomain id - InsertLockRecord(_lockId, db); - _logger.Debug("Acquired with ID {LockId}", _lockId); - return true; - } - else if (mainDomRows.Count == 1 && !mainDomRows[0].Value.StartsWith(tempId)) - { - // in this case, the prefixed ID is different which means - // another new AppDomain has come online and is wanting to take over. In that case, we will not - // acquire. - - _logger.Debug("Cannot acquire, another booting application detected."); - return false; - } - } - catch (Exception ex) - { - if (IsLockTimeoutException(ex as SqlException)) - { - _logger.Error(ex, "Sql timeout occurred, waiting for existing MainDom is canceled."); - _errorDuringAcquiring = true; - return false; - } - // unexpected - _logger.Error(ex, "Unexpected error, waiting for existing MainDom is canceled."); - _errorDuringAcquiring = true; - return false; - } - finally - { - transaction.Complete(); - } - } + var acquired = TryAcquire(db, tempId, updatedTempId); + if (acquired.HasValue) + return acquired.Value; if (watch.ElapsedMilliseconds >= millisecondsTimeout) { - // if the timeout has elapsed, it either means that the other main dom is taking too long to shutdown, - // or it could mean that the previous appdomain was terminated and didn't clear out the main dom SQL row - // and it's just been left as an orphan row. - // There's really know way of knowing unless we are constantly updating the row for the current maindom - // which isn't ideal. - // So... we're going to 'just' take over, if the writelock works then we'll assume we're ok - - _logger.Debug("Timeout elapsed, assuming orphan row, acquiring MainDom."); - - using var transaction = db.GetTransaction(IsolationLevel.ReadCommitted); - - try - { - _sqlServerSyntax.WriteLock(db, Constants.Locks.MainDom); - - // so now we update the row with our appdomain id - InsertLockRecord(_lockId, db); - _logger.Debug("Acquired with ID {LockId}", _lockId); - return true; - } - catch (Exception ex) - { - if (IsLockTimeoutException(ex as SqlException)) - { - // something is wrong, we cannot acquire, not much we can do - _logger.Error(ex, "Sql timeout occurred, could not forcibly acquire MainDom."); - _errorDuringAcquiring = true; - return false; - } - _logger.Error(ex, "Unexpected error, could not forcibly acquire MainDom."); - _errorDuringAcquiring = true; - return false; - } - finally - { - transaction.Complete(); - } + return AcquireWhenMaxWaitTimeElapsed(db); } } }, _cancellationTokenSource.Token); } + private bool? TryAcquire(IUmbracoDatabase db, string tempId, string updatedTempId) + { + using var transaction = db.GetTransaction(IsolationLevel.ReadCommitted); + + try + { + // get a read lock + _sqlServerSyntax.ReadLock(db, Constants.Locks.MainDom); + + // the row + var mainDomRows = db.Fetch("SELECT * FROM umbracoKeyValue WHERE [key] = @key", new { key = MainDomKey }); + + if (mainDomRows.Count == 0 || mainDomRows[0].Value == updatedTempId) + { + // the other main dom has updated our record + // Or the other maindom shutdown super fast and just deleted the record + // which indicates that we + // can acquire it and it has shutdown. + + _sqlServerSyntax.WriteLock(db, Constants.Locks.MainDom); + + // so now we update the row with our appdomain id + InsertLockRecord(_lockId, db); + _logger.Debug("Acquired with ID {LockId}", _lockId); + return true; + } + else if (mainDomRows.Count == 1 && !mainDomRows[0].Value.StartsWith(tempId)) + { + // in this case, the prefixed ID is different which means + // another new AppDomain has come online and is wanting to take over. In that case, we will not + // acquire. + + _logger.Debug("Cannot acquire, another booting application detected."); + return false; + } + } + catch (Exception ex) + { + if (IsLockTimeoutException(ex as SqlException)) + { + _logger.Error(ex, "Sql timeout occurred, waiting for existing MainDom is canceled."); + _errorDuringAcquiring = true; + return false; + } + // unexpected + _logger.Error(ex, "Unexpected error, waiting for existing MainDom is canceled."); + _errorDuringAcquiring = true; + return false; + } + finally + { + transaction.Complete(); + } + + return null; // continue + } + + private bool AcquireWhenMaxWaitTimeElapsed(IUmbracoDatabase db) + { + // if the timeout has elapsed, it either means that the other main dom is taking too long to shutdown, + // or it could mean that the previous appdomain was terminated and didn't clear out the main dom SQL row + // and it's just been left as an orphan row. + // There's really know way of knowing unless we are constantly updating the row for the current maindom + // which isn't ideal. + // So... we're going to 'just' take over, if the writelock works then we'll assume we're ok + + _logger.Debug("Timeout elapsed, assuming orphan row, acquiring MainDom."); + + using var transaction = db.GetTransaction(IsolationLevel.ReadCommitted); + + try + { + _sqlServerSyntax.WriteLock(db, Constants.Locks.MainDom); + + // so now we update the row with our appdomain id + InsertLockRecord(_lockId, db); + _logger.Debug("Acquired with ID {LockId}", _lockId); + return true; + } + catch (Exception ex) + { + if (IsLockTimeoutException(ex as SqlException)) + { + // something is wrong, we cannot acquire, not much we can do + _logger.Error(ex, "Sql timeout occurred, could not forcibly acquire MainDom."); + _errorDuringAcquiring = true; + return false; + } + _logger.Error(ex, "Unexpected error, could not forcibly acquire MainDom."); + _errorDuringAcquiring = true; + return false; + } + finally + { + transaction.Complete(); + } + } + /// /// Inserts or updates the key/value row /// From f0dea44b4de0bae48a68bb7d963d316f29363d79 Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 8 Jul 2020 15:47:26 +1000 Subject: [PATCH 312/377] Adds Load Test controller to test data project (instead of just being stored in my personal repo) --- src/Umbraco.TestData/LoadTestController.cs | 371 +++++++++++++++++++ src/Umbraco.TestData/Umbraco.TestData.csproj | 1 + src/Umbraco.Web.UI/Umbraco.Web.UI.csproj | 7 +- 3 files changed, 378 insertions(+), 1 deletion(-) create mode 100644 src/Umbraco.TestData/LoadTestController.cs diff --git a/src/Umbraco.TestData/LoadTestController.cs b/src/Umbraco.TestData/LoadTestController.cs new file mode 100644 index 0000000000..97665dd084 --- /dev/null +++ b/src/Umbraco.TestData/LoadTestController.cs @@ -0,0 +1,371 @@ +using System; +using System.Threading; +using System.Linq; +using System.Web.Mvc; +using Umbraco.Core.Services; +using Umbraco.Core.Models; +using System.Web; +using System.Web.Hosting; +using System.Web.Routing; +using System.Diagnostics; +using Umbraco.Core.Composing; +using System.Configuration; + +// see https://github.com/Shazwazza/UmbracoScripts/tree/master/src/LoadTesting + +namespace Umbraco.TestData +{ + public class LoadTestController : Controller + { + public LoadTestController(ServiceContext serviceContext) + { + _serviceContext = serviceContext; + } + + private static readonly Random _random = new Random(); + private static readonly object _locko = new object(); + + private static volatile int _containerId = -1; + + private const string _containerAlias = "LoadTestContainer"; + private const string _contentAlias = "LoadTestContent"; + private const int _textboxDefinitionId = -88; + private const int _maxCreate = 1000; + + private static readonly string HeadHtml = @" + + LoadTest + + + +
        +

        LoadTest

        +
        " + System.Configuration.ConfigurationManager.AppSettings["umbracoConfigurationStatus"] + @"
        +
        +"; + + private const string FootHtml = @" +"; + + private static readonly string _containerTemplateText = @" +@inherits Umbraco.Web.Mvc.UmbracoViewPage +@{ + Layout = null; + var container = Umbraco.ContentAtRoot().OfTypes(""" + _containerAlias + @""").FirstOrDefault(); + var contents = container.Children().ToArray(); + var groups = contents.GroupBy(x => x.Value(""origin"")); + var id = contents.Length > 0 ? contents[0].Id : -1; + var wurl = Request.QueryString[""u""] == ""1""; + var missing = contents.Length > 0 && contents[contents.Length - 1].Id - contents[0].Id >= contents.Length; +} +" + HeadHtml + @" +
        +@contents.Length items +
          +@foreach (var group in groups) +{ +
        • @group.Key: @group.Count()
        • +} +
        +
        +@foreach (var content in contents) +{ + while (content.Id > id) + { +
        @id :: MISSING
        + id++; + } + if (wurl) + { +
        @content.Id :: @content.Name :: @content.Url
        + } + else + { +
        @content.Id :: @content.Name
        + } id++; +} +
        +" + FootHtml; + private readonly ServiceContext _serviceContext; + + private ActionResult ContentHtml(string s) + { + return Content(HeadHtml + s + FootHtml); + } + + public ActionResult Index() + { + var res = EnsureInitialize(); + if (res != null) return res; + + var html = @"Welcome. You can: + +"; + + return ContentHtml(html); + } + + private ActionResult EnsureInitialize() + { + if (_containerId > 0) return null; + + lock (_locko) + { + if (_containerId > 0) return null; + + var contentTypeService = _serviceContext.ContentTypeService; + var contentType = contentTypeService.Get(_contentAlias); + if (contentType == null) + return ContentHtml("Not installed, first you must install."); + + var containerType = contentTypeService.Get(_containerAlias); + if (containerType == null) + return ContentHtml("Panic! Container type is missing."); + + var contentService = _serviceContext.ContentService; + var container = contentService.GetPagedOfType(containerType.Id, 0, 100, out _, null).FirstOrDefault(); + if (container == null) + return ContentHtml("Panic! Container is missing."); + + _containerId = container.Id; + return null; + } + } + + public ActionResult Install() + { + var dataTypeService = _serviceContext.DataTypeService; + + //var dataType = dataTypeService.GetAll(Constants.DataTypes.DefaultContentListView); + + + //if (!dict.ContainsKey("pageSize")) dict["pageSize"] = new PreValue("10"); + //dict["pageSize"].Value = "200"; + //dataTypeService.SavePreValues(dataType, dict); + + var contentTypeService = _serviceContext.ContentTypeService; + + var contentType = new ContentType(-1) + { + Alias = _contentAlias, + Name = "LoadTest Content", + Description = "Content for LoadTest", + Icon = "icon-document" + }; + var def = _serviceContext.DataTypeService.GetDataType(_textboxDefinitionId); + contentType.AddPropertyType(new PropertyType(def) + { + Name = "Origin", + Alias = "origin", + Description = "The origin of the content.", + }); + contentTypeService.Save(contentType); + + var containerTemplate = ImportTemplate(_serviceContext, + "LoadTestContainer", "LoadTestContainer", _containerTemplateText); + + var containerType = new ContentType(-1) + { + Alias = _containerAlias, + Name = "LoadTest Container", + Description = "Container for LoadTest content", + Icon = "icon-document", + AllowedAsRoot = true, + IsContainer = true + }; + containerType.AllowedContentTypes = containerType.AllowedContentTypes.Union(new[] + { + new ContentTypeSort(new Lazy(() => contentType.Id), 0, contentType.Alias), + }); + containerType.AllowedTemplates = containerType.AllowedTemplates.Union(new[] { containerTemplate }); + containerType.SetDefaultTemplate(containerTemplate); + contentTypeService.Save(containerType); + + var contentService = _serviceContext.ContentService; + var content = contentService.Create("LoadTestContainer", -1, _containerAlias); + contentService.SaveAndPublish(content); + + return ContentHtml("Installed."); + } + + public ActionResult Create(int n = 1, int r = 0, string o = null) + { + var res = EnsureInitialize(); + if (res != null) return res; + + if (r < 0) r = 0; + if (r > 100) r = 100; + var restart = GetRandom(0, 100) > (100 - r); + + var contentService = _serviceContext.ContentService; + + if (n < 1) n = 1; + if (n > _maxCreate) n = _maxCreate; + for (int i = 0; i < n; i++) + { + var name = Guid.NewGuid().ToString("N").ToUpper() + "-" + (restart ? "R" : "X") + "-" + o; + var content = contentService.Create(name, _containerId, _contentAlias); + content.SetValue("origin", o); + contentService.SaveAndPublish(content); + } + + if (restart) + DoRestart(); + + return ContentHtml("Created " + n + " content" + + (restart ? ", and restarted" : "") + + "."); + } + + private int GetRandom(int minValue, int maxValue) + { + lock (_locko) + { + return _random.Next(minValue, maxValue); + } + } + + public ActionResult Clear() + { + var res = EnsureInitialize(); + if (res != null) return res; + + var contentType = _serviceContext.ContentTypeService.Get(_contentAlias); + _serviceContext.ContentService.DeleteOfType(contentType.Id); + + return ContentHtml("Cleared."); + } + + private void DoRestart() + { + HttpContext.User = null; + System.Web.HttpContext.Current.User = null; + Thread.CurrentPrincipal = null; + HttpRuntime.UnloadAppDomain(); + } + + public ActionResult Restart() + { + DoRestart(); + + return ContentHtml("Restarted."); + } + + public ActionResult Die() + { + var timer = new System.Threading.Timer(_ => + { + throw new Exception("die!"); + }); + timer.Change(100, 0); + + return ContentHtml("Dying."); + } + + public ActionResult Domains() + { + var currentDomain = AppDomain.CurrentDomain; + var currentName = currentDomain.FriendlyName; + var pos = currentName.IndexOf('-'); + if (pos > 0) currentName = currentName.Substring(0, pos); + + var text = new System.Text.StringBuilder(); + text.Append("
        Process ID: " + Process.GetCurrentProcess().Id + "
        "); + text.Append("
        "); + text.Append("
        IIS Site: " + HostingEnvironment.ApplicationHost.GetSiteName() + "
        "); + text.Append("
        App ID: " + currentName + "
        "); + //text.Append("
        AppPool: " + Zbu.WebManagement.AppPoolHelper.GetCurrentApplicationPoolName() + "
        "); + text.Append("
        "); + + text.Append("
        Domains:
          "); + text.Append("
        • Not implemented.
        • "); + /* + foreach (var domain in Zbu.WebManagement.AppDomainHelper.GetAppDomains().OrderBy(x => x.Id)) + { + var name = domain.FriendlyName; + pos = name.IndexOf('-'); + if (pos > 0) name = name.Substring(0, pos); + text.Append("
        • " + +"[" + domain.Id + "] " + name + + (domain.IsDefaultAppDomain() ? " (default)" : "") + + (domain.Id == currentDomain.Id ? " (current)" : "") + + "
        • "); + } + */ + text.Append("
        "); + + return ContentHtml(text.ToString()); + } + + public ActionResult Recycle() + { + return ContentHtml("Not implemented—please use IIS console."); + } + + private static Template ImportTemplate(ServiceContext svces, string name, string alias, string text, ITemplate master = null) + { + var t = new Template(name, alias) { Content = text }; + if (master != null) + t.SetMasterTemplate(master); + svces.FileService.SaveTemplate(t); + return t; + } + } + + public class TestComponent : IComponent + { + public void Initialize() + { + if (ConfigurationManager.AppSettings["Umbraco.TestData.Enabled"] != "true") + return; + + RouteTable.Routes.MapRoute( + name: "LoadTest", + url: "LoadTest/{action}", + defaults: new + { + controller = "LoadTest", + action = "Index" + }, + namespaces: new[] { "Umbraco.TestData" } + ); + } + + public void Terminate() + { + } + } + + public class TestComposer : ComponentComposer, IUserComposer + { + public override void Compose(Composition composition) + { + base.Compose(composition); + + if (ConfigurationManager.AppSettings["Umbraco.TestData.Enabled"] != "true") + return; + + composition.Register(typeof(LoadTestController), Lifetime.Request); + } + } +} diff --git a/src/Umbraco.TestData/Umbraco.TestData.csproj b/src/Umbraco.TestData/Umbraco.TestData.csproj index d61321ebb8..a3753cc17b 100644 --- a/src/Umbraco.TestData/Umbraco.TestData.csproj +++ b/src/Umbraco.TestData/Umbraco.TestData.csproj @@ -41,6 +41,7 @@ + diff --git a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj index 0de757531a..009659c0e2 100644 --- a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj +++ b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj @@ -12,7 +12,8 @@ {349c5851-65df-11da-9384-00065b846f21};{fae04ec0-301f-11d3-bf4b-00c04f79efbc} OnBuildSuccess true - 44331 + + enabled disabled false @@ -126,6 +127,10 @@ {52ac0ba8-a60e-4e36-897b-e8b97a54ed1c} Umbraco.ModelsBuilder.Embedded + + {fb5676ed-7a69-492c-b802-e7b24144c0fc} + Umbraco.TestData + {651e1350-91b6-44b7-bd60-7207006d7003} Umbraco.Web From 384531ea6878c87c86f133c2499c3208382e3b4c Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 8 Jul 2020 15:59:35 +1000 Subject: [PATCH 313/377] removes comments and no need for private method --- src/Umbraco.Core/Services/Implement/ContentService.cs | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/Umbraco.Core/Services/Implement/ContentService.cs b/src/Umbraco.Core/Services/Implement/ContentService.cs index 6b7a858d3e..93e7f0a5df 100644 --- a/src/Umbraco.Core/Services/Implement/ContentService.cs +++ b/src/Umbraco.Core/Services/Implement/ContentService.cs @@ -1359,13 +1359,7 @@ namespace Umbraco.Core.Services.Implement } /// - public IEnumerable PerformScheduledPublish(DateTime date) - => PerformScheduledPublishInternal(date).ToList(); - - // beware! this method yields results, so the returned IEnumerable *must* be - // enumerated for anything to happen - dangerous, so private + exposed via - // the public method above, which forces ToList(). - private IEnumerable PerformScheduledPublishInternal(DateTime date) + public IEnumerable PerformScheduledPublish(DateTime date) { var evtMsgs = EventMessagesFactory.Get(); From e1757178b9605772d5c4c2fb70e70f918f567f4e Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 8 Jul 2020 17:26:31 +1000 Subject: [PATCH 314/377] Fixes our sql azure transient fault detection to be inline with current standards, adds a scope for the health check schedule tasks --- ...SqlAzureTransientErrorDetectionStrategy.cs | 44 ++++++++++++------- .../Scheduling/HealthCheckNotifier.cs | 10 ++++- src/Umbraco.Web/Scheduling/LogScrubber.cs | 3 +- .../Scheduling/ScheduledPublishing.cs | 41 ++++++++--------- .../Scheduling/SchedulerComponent.cs | 2 +- 5 files changed, 57 insertions(+), 43 deletions(-) diff --git a/src/Umbraco.Core/Persistence/FaultHandling/Strategies/SqlAzureTransientErrorDetectionStrategy.cs b/src/Umbraco.Core/Persistence/FaultHandling/Strategies/SqlAzureTransientErrorDetectionStrategy.cs index 849fd35fad..f763594616 100644 --- a/src/Umbraco.Core/Persistence/FaultHandling/Strategies/SqlAzureTransientErrorDetectionStrategy.cs +++ b/src/Umbraco.Core/Persistence/FaultHandling/Strategies/SqlAzureTransientErrorDetectionStrategy.cs @@ -4,6 +4,10 @@ using System.Data.SqlClient; namespace Umbraco.Core.Persistence.FaultHandling.Strategies { + // See https://docs.microsoft.com/en-us/azure/azure-sql/database/troubleshoot-common-connectivity-issues + // Also we could just use the nuget package instead https://www.nuget.org/packages/EnterpriseLibrary.TransientFaultHandling/ ? + // but i guess that's not netcore so we'll just leave it. + /// /// Provides the transient error detection logic for transient faults that are specific to SQL Azure. /// @@ -71,7 +75,7 @@ namespace Umbraco.Core.Persistence.FaultHandling.Strategies /// Determines whether the specified exception represents a transient failure that can be compensated by a retry. /// /// The exception object to be verified. - /// True if the specified exception is considered as transient, otherwise false. + /// true if the specified exception is considered as transient; otherwise, false. public bool IsTransient(Exception ex) { if (ex != null) @@ -97,40 +101,50 @@ namespace Umbraco.Core.Persistence.FaultHandling.Strategies return true; - // SQL Error Code: 40197 - // The service has encountered an error processing your request. Please try again. - case 40197: + // SQL Error Code: 10928 + // Resource ID: %d. The %s limit for the database is %d and has been reached. + case 10928: + // SQL Error Code: 10929 + // Resource ID: %d. The %s minimum guarantee is %d, maximum limit is %d and the current usage for the database is %d. + // However, the server is currently too busy to support requests greater than %d for this database. + case 10929: // SQL Error Code: 10053 // A transport-level error has occurred when receiving results from the server. // An established connection was aborted by the software in your host machine. case 10053: // SQL Error Code: 10054 - // A transport-level error has occurred when sending the request to the server. + // A transport-level error has occurred when sending the request to the server. // (provider: TCP Provider, error: 0 - An existing connection was forcibly closed by the remote host.) case 10054: // SQL Error Code: 10060 - // A network-related or instance-specific error occurred while establishing a connection to SQL Server. - // The server was not found or was not accessible. Verify that the instance name is correct and that SQL Server - // is configured to allow remote connections. (provider: TCP Provider, error: 0 - A connection attempt failed - // because the connected party did not properly respond after a period of time, or established connection failed + // A network-related or instance-specific error occurred while establishing a connection to SQL Server. + // The server was not found or was not accessible. Verify that the instance name is correct and that SQL Server + // is configured to allow remote connections. (provider: TCP Provider, error: 0 - A connection attempt failed + // because the connected party did not properly respond after a period of time, or established connection failed // because connected host has failed to respond.)"} case 10060: + // SQL Error Code: 40197 + // The service has encountered an error processing your request. Please try again. + case 40197: + // SQL Error Code: 40540 + // The service has encountered an error processing your request. Please try again. + case 40540: // SQL Error Code: 40613 - // Database XXXX on server YYYY is not currently available. Please retry the connection later. If the problem persists, contact customer + // Database XXXX on server YYYY is not currently available. Please retry the connection later. If the problem persists, contact customer // support, and provide them the session tracing ID of ZZZZZ. case 40613: // SQL Error Code: 40143 // The service has encountered an error processing your request. Please try again. case 40143: // SQL Error Code: 233 - // The client was unable to establish a connection because of an error during connection initialization process before login. - // Possible causes include the following: the client tried to connect to an unsupported version of SQL Server; the server was too busy - // to accept new connections; or there was a resource limitation (insufficient memory or maximum allowed connections) on the server. + // The client was unable to establish a connection because of an error during connection initialization process before login. + // Possible causes include the following: the client tried to connect to an unsupported version of SQL Server; the server was too busy + // to accept new connections; or there was a resource limitation (insufficient memory or maximum allowed connections) on the server. // (provider: TCP Provider, error: 0 - An existing connection was forcibly closed by the remote host.) case 233: // SQL Error Code: 64 - // A connection was successfully established with the server, but then an error occurred during the login process. - // (provider: TCP Provider, error: 0 - The specified network name is no longer available.) + // A connection was successfully established with the server, but then an error occurred during the login process. + // (provider: TCP Provider, error: 0 - The specified network name is no longer available.) case 64: // DBNETLIB Error Code: 20 // The instance of SQL Server you attempted to connect to does not support encryption. diff --git a/src/Umbraco.Web/Scheduling/HealthCheckNotifier.cs b/src/Umbraco.Web/Scheduling/HealthCheckNotifier.cs index 746bc61e34..9a4a4f0e2c 100644 --- a/src/Umbraco.Web/Scheduling/HealthCheckNotifier.cs +++ b/src/Umbraco.Web/Scheduling/HealthCheckNotifier.cs @@ -5,6 +5,7 @@ using Umbraco.Core; using Umbraco.Core.Composing; using Umbraco.Core.Configuration; using Umbraco.Core.Logging; +using Umbraco.Core.Scoping; using Umbraco.Core.Sync; using Umbraco.Web.HealthCheck; @@ -15,16 +16,17 @@ namespace Umbraco.Web.Scheduling private readonly IRuntimeState _runtimeState; private readonly HealthCheckCollection _healthChecks; private readonly HealthCheckNotificationMethodCollection _notifications; + private readonly IScopeProvider _scopeProvider; private readonly IProfilingLogger _logger; public HealthCheckNotifier(IBackgroundTaskRunner runner, int delayMilliseconds, int periodMilliseconds, HealthCheckCollection healthChecks, HealthCheckNotificationMethodCollection notifications, - IRuntimeState runtimeState, - IProfilingLogger logger) + IScopeProvider scopeProvider, IRuntimeState runtimeState, IProfilingLogger logger) : base(runner, delayMilliseconds, periodMilliseconds) { _healthChecks = healthChecks; _notifications = notifications; + _scopeProvider = scopeProvider; _runtimeState = runtimeState; _logger = logger; } @@ -51,6 +53,10 @@ namespace Umbraco.Web.Scheduling return false; // do NOT repeat, going down } + // Ensure we use an explicit scope since we are running on a background thread and plugin health + // checks can be making service/database calls so we want to ensure the CallContext/Ambient scope + // isn't used since that can be problematic. + using (var scope = _scopeProvider.CreateScope()) using (_logger.DebugDuration("Health checks executing", "Health checks complete")) { var healthCheckConfig = Current.Configs.HealthChecks(); diff --git a/src/Umbraco.Web/Scheduling/LogScrubber.cs b/src/Umbraco.Web/Scheduling/LogScrubber.cs index db13a80f9b..ffdb584c7a 100644 --- a/src/Umbraco.Web/Scheduling/LogScrubber.cs +++ b/src/Umbraco.Web/Scheduling/LogScrubber.cs @@ -70,8 +70,7 @@ namespace Umbraco.Web.Scheduling return false; // do NOT repeat, going down } - // running on a background task, and Log.CleanLogs uses the old SqlHelper, - // better wrap in a scope and ensure it's all cleaned up and nothing leaks + // Ensure we use an explicit scope since we are running on a background thread. using (var scope = _scopeProvider.CreateScope()) using (_logger.DebugDuration("Log scrubbing executing", "Log scrubbing complete")) { diff --git a/src/Umbraco.Web/Scheduling/ScheduledPublishing.cs b/src/Umbraco.Web/Scheduling/ScheduledPublishing.cs index 2e79e40d7a..97afe25e22 100644 --- a/src/Umbraco.Web/Scheduling/ScheduledPublishing.cs +++ b/src/Umbraco.Web/Scheduling/ScheduledPublishing.cs @@ -2,6 +2,7 @@ using System.Linq; using Umbraco.Core; using Umbraco.Core.Logging; +using Umbraco.Core.Scoping; using Umbraco.Core.Services; using Umbraco.Core.Sync; @@ -55,30 +56,24 @@ namespace Umbraco.Web.Scheduling try { - // ensure we run with an UmbracoContext, because this may run in a background task, - // yet developers may be using the 'current' UmbracoContext in the event handlers - // - // TODO: or maybe not, CacheRefresherComponent already ensures a context when handling events - // - UmbracoContext 'current' needs to be refactored and cleaned up - // - batched messenger should not depend on a current HttpContext - // but then what should be its "scope"? could we attach it to scopes? - // - and we should definitively *not* have to flush it here (should be auto) - // - using (var contextReference = _umbracoContextFactory.EnsureUmbracoContext()) + // We don't need an explicit scope here because PerformScheduledPublish creates it's own scope + // so it's safe as it will create it's own ambient scope. + // Ensure we run with an UmbracoContext, because this will run in a background task, + // and developers may be using the UmbracoContext in the event handlers. + + using var contextReference = _umbracoContextFactory.EnsureUmbracoContext(); + try { - try - { - // run - var result = _contentService.PerformScheduledPublish(DateTime.Now); - foreach (var grouped in result.GroupBy(x => x.Result)) - _logger.Info("Scheduled publishing result: '{StatusCount}' items with status {Status}", grouped.Count(), grouped.Key); - } - finally - { - // if running on a temp context, we have to flush the messenger - if (contextReference.IsRoot && Composing.Current.ServerMessenger is BatchedDatabaseServerMessenger m) - m.FlushBatch(); - } + // run + var result = _contentService.PerformScheduledPublish(DateTime.Now); + foreach (var grouped in result.GroupBy(x => x.Result)) + _logger.Info("Scheduled publishing result: '{StatusCount}' items with status {Status}", grouped.Count(), grouped.Key); + } + finally + { + // if running on a temp context, we have to flush the messenger + if (contextReference.IsRoot && Composing.Current.ServerMessenger is BatchedDatabaseServerMessenger m) + m.FlushBatch(); } } catch (Exception ex) diff --git a/src/Umbraco.Web/Scheduling/SchedulerComponent.cs b/src/Umbraco.Web/Scheduling/SchedulerComponent.cs index a08289186f..f6ce11f939 100644 --- a/src/Umbraco.Web/Scheduling/SchedulerComponent.cs +++ b/src/Umbraco.Web/Scheduling/SchedulerComponent.cs @@ -155,7 +155,7 @@ namespace Umbraco.Web.Scheduling } var periodInMilliseconds = healthCheckConfig.NotificationSettings.PeriodInHours * 60 * 60 * 1000; - var task = new HealthCheckNotifier(_healthCheckRunner, delayInMilliseconds, periodInMilliseconds, healthChecks, notifications, _runtime, logger); + var task = new HealthCheckNotifier(_healthCheckRunner, delayInMilliseconds, periodInMilliseconds, healthChecks, notifications, _scopeProvider, _runtime, logger); _healthCheckRunner.TryAdd(task); return task; } From df61f30393d1107dccf980a5d0511b72b57dc3cf Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 8 Jul 2020 18:16:12 +1000 Subject: [PATCH 315/377] refactors scheduled publishing logic - splits into 2x scopes/2x trans, only take a write lock when necessary --- .../Repositories/IDocumentRepository.cs | 5 + .../Implement/DocumentRepository.cs | 31 ++++ .../Services/Implement/ContentService.cs | 158 ++++++++++-------- 3 files changed, 126 insertions(+), 68 deletions(-) diff --git a/src/Umbraco.Core/Persistence/Repositories/IDocumentRepository.cs b/src/Umbraco.Core/Persistence/Repositories/IDocumentRepository.cs index fc5382499f..0971b2047a 100644 --- a/src/Umbraco.Core/Persistence/Repositories/IDocumentRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/IDocumentRepository.cs @@ -12,6 +12,11 @@ namespace Umbraco.Core.Persistence.Repositories /// void ClearSchedule(DateTime date); + void ClearSchedule(DateTime date, ContentScheduleAction action); + + bool HasContentForExpiration(DateTime date); + bool HasContentForRelease(DateTime date); + /// /// Gets objects having an expiration date before (lower than, or equal to) a specified date. /// diff --git a/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs b/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs index ccfa8209fb..a34aadd70f 100644 --- a/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs +++ b/src/Umbraco.Core/Persistence/Repositories/Implement/DocumentRepository.cs @@ -1000,6 +1000,37 @@ namespace Umbraco.Core.Persistence.Repositories.Implement Database.Execute(sql); } + /// + public void ClearSchedule(DateTime date, ContentScheduleAction action) + { + var a = action.ToString(); + var sql = Sql().Delete().Where(x => x.Date <= date && x.Action == a); + Database.Execute(sql); + } + + private Sql GetSqlForHasScheduling(ContentScheduleAction action, DateTime date) + { + var template = SqlContext.Templates.Get("Umbraco.Core.DocumentRepository.GetSqlForHasScheduling", tsql => tsql + .SelectCount() + .From() + .Where(x => x.Action == SqlTemplate.Arg("action") && x.Date <= SqlTemplate.Arg("date"))); + + var sql = template.Sql(action.ToString(), date); + return sql; + } + + public bool HasContentForExpiration(DateTime date) + { + var sql = GetSqlForHasScheduling(ContentScheduleAction.Expire, date); + return Database.ExecuteScalar(sql) > 0; + } + + public bool HasContentForRelease(DateTime date) + { + var sql = GetSqlForHasScheduling(ContentScheduleAction.Release, date); + return Database.ExecuteScalar(sql) > 0; + } + /// public IEnumerable GetContentForRelease(DateTime date) { diff --git a/src/Umbraco.Core/Services/Implement/ContentService.cs b/src/Umbraco.Core/Services/Implement/ContentService.cs index 93e7f0a5df..2a23a1adad 100644 --- a/src/Umbraco.Core/Services/Implement/ContentService.cs +++ b/src/Umbraco.Core/Services/Implement/ContentService.cs @@ -30,7 +30,7 @@ namespace Umbraco.Core.Services.Implement private IQuery _queryNotTrashed; //TODO: The non-lazy object should be injected private readonly Lazy _propertyValidationService = new Lazy(() => new PropertyValidationService()); - + #region Constructors @@ -875,7 +875,7 @@ namespace Umbraco.Core.Services.Implement throw new NotSupportedException($"Culture \"{culture}\" is not supported by invariant content types."); } - if(content.Name != null && content.Name.Length > 255) + if (content.Name != null && content.Name.Length > 255) { throw new InvalidOperationException("Name cannot be more than 255 characters in length."); } @@ -1243,7 +1243,7 @@ namespace Umbraco.Core.Services.Implement if (culturesUnpublishing != null) { // This will mean that that we unpublished a mandatory culture or we unpublished the last culture. - + var langs = string.Join(", ", allLangs .Where(x => culturesUnpublishing.InvariantContains(x.IsoCode)) .Select(x => x.CultureName)); @@ -1252,7 +1252,7 @@ namespace Umbraco.Core.Services.Implement if (publishResult == null) throw new PanicException("publishResult == null - should not happen"); - switch(publishResult.Result) + switch (publishResult.Result) { case PublishResultType.FailedPublishMandatoryCultureMissing: //occurs when a mandatory culture was unpublished (which means we tried publishing the document without a mandatory culture) @@ -1266,7 +1266,7 @@ namespace Umbraco.Core.Services.Implement Audit(AuditType.Unpublish, userId, content.Id, "Unpublished (last language unpublished)"); return new PublishResult(PublishResultType.SuccessUnpublishLastCulture, evtMsgs, content); } - + } Audit(AuditType.Unpublish, userId, content.Id); @@ -1286,7 +1286,7 @@ namespace Umbraco.Core.Services.Implement changeType = TreeChangeTypes.RefreshBranch; // whole branch else if (isNew == false && previouslyPublished) changeType = TreeChangeTypes.RefreshNode; // single node - + // invalidate the node/branch if (!branchOne) // for branches, handled by SaveAndPublishBranch @@ -1359,17 +1359,88 @@ namespace Umbraco.Core.Services.Implement } /// - public IEnumerable PerformScheduledPublish(DateTime date) + public IEnumerable PerformScheduledPublish(DateTime date) { + var allLangs = new Lazy>(() => _languageRepository.GetMany().ToList()); var evtMsgs = EventMessagesFactory.Get(); - var results = new List(); - using (var scope = ScopeProvider.CreateScope()) + PerformScheduledPublishingRelease(date, results, evtMsgs, allLangs); + PerformScheduledPublishingExpiration(date, results, evtMsgs, allLangs); + + return results; + } + + private void PerformScheduledPublishingExpiration(DateTime date, List results, EventMessages evtMsgs, Lazy> allLangs) + { + using var scope = ScopeProvider.CreateScope(); + + // do a fast read without any locks since this executes often to see if we even need to proceed + if (_documentRepository.HasContentForExpiration(date)) { + // now take a write lock since we'll be updating scope.WriteLock(Constants.Locks.ContentTree); - var allLangs = _languageRepository.GetMany().ToList(); + foreach (var d in _documentRepository.GetContentForExpiration(date)) + { + if (d.ContentType.VariesByCulture()) + { + //find which cultures have pending schedules + var pendingCultures = d.ContentSchedule.GetPending(ContentScheduleAction.Expire, date) + .Select(x => x.Culture) + .Distinct() + .ToList(); + + if (pendingCultures.Count == 0) + continue; //shouldn't happen but no point in continuing if there's nothing there + + var saveEventArgs = new ContentSavingEventArgs(d, evtMsgs); + if (scope.Events.DispatchCancelable(Saving, this, saveEventArgs, nameof(Saving))) + { + results.Add(new PublishResult(PublishResultType.FailedPublishCancelledByEvent, evtMsgs, d)); + continue; + } + + foreach (var c in pendingCultures) + { + //Clear this schedule for this culture + d.ContentSchedule.Clear(c, ContentScheduleAction.Expire, date); + //set the culture to be published + d.UnpublishCulture(c); + } + + var result = CommitDocumentChangesInternal(scope, d, saveEventArgs, allLangs.Value, d.WriterId); + if (result.Success == false) + Logger.Error(null, "Failed to publish document id={DocumentId}, reason={Reason}.", d.Id, result.Result); + results.Add(result); + + } + else + { + //Clear this schedule + d.ContentSchedule.Clear(ContentScheduleAction.Expire, date); + var result = Unpublish(d, userId: d.WriterId); + if (result.Success == false) + Logger.Error(null, "Failed to unpublish document id={DocumentId}, reason={Reason}.", d.Id, result.Result); + results.Add(result); + } + } + + _documentRepository.ClearSchedule(date, ContentScheduleAction.Expire); + } + + scope.Complete(); + } + + private void PerformScheduledPublishingRelease(DateTime date, List results, EventMessages evtMsgs, Lazy> allLangs) + { + using var scope = ScopeProvider.CreateScope(); + + // do a fast read without any locks since this executes often to see if we even need to proceed + if (_documentRepository.HasContentForRelease(date)) + { + // now take a write lock since we'll be updating + scope.WriteLock(Constants.Locks.ContentTree); foreach (var d in _documentRepository.GetContentForRelease(date)) { @@ -1382,13 +1453,13 @@ namespace Umbraco.Core.Services.Implement .ToList(); if (pendingCultures.Count == 0) - break; //shouldn't happen but no point in continuing if there's nothing there + continue; //shouldn't happen but no point in continuing if there's nothing there var saveEventArgs = new ContentSavingEventArgs(d, evtMsgs); if (scope.Events.DispatchCancelable(Saving, this, saveEventArgs, nameof(Saving))) { results.Add(new PublishResult(PublishResultType.FailedPublishCancelledByEvent, evtMsgs, d)); - continue; + continue; // this document is canceled move next } var publishing = true; @@ -1401,14 +1472,14 @@ namespace Umbraco.Core.Services.Implement //publish the culture values and validate the property values, if validation fails, log the invalid properties so the develeper has an idea of what has failed Property[] invalidProperties = null; - var impact = CultureImpact.Explicit(culture, IsDefaultCulture(allLangs, culture)); + var impact = CultureImpact.Explicit(culture, IsDefaultCulture(allLangs.Value, culture)); var tryPublish = d.PublishCulture(impact) && _propertyValidationService.Value.IsPropertyDataValid(d, out invalidProperties, impact); if (invalidProperties != null && invalidProperties.Length > 0) Logger.Warn("Scheduled publishing will fail for document {DocumentId} and culture {Culture} because of invalid properties {InvalidProperties}", d.Id, culture, string.Join(",", invalidProperties.Select(x => x.Alias))); publishing &= tryPublish; //set the culture to be published - if (!publishing) break; // no point continuing + if (!publishing) continue; // no point continuing } PublishResult result; @@ -1418,7 +1489,7 @@ namespace Umbraco.Core.Services.Implement else if (!publishing) result = new PublishResult(PublishResultType.FailedPublishContentInvalid, evtMsgs, d); else - result = CommitDocumentChangesInternal(scope, d, saveEventArgs, allLangs, d.WriterId); + result = CommitDocumentChangesInternal(scope, d, saveEventArgs, allLangs.Value, d.WriterId); if (result.Success == false) Logger.Error(null, "Failed to publish document id={DocumentId}, reason={Reason}.", d.Id, result.Result); @@ -1441,60 +1512,11 @@ namespace Umbraco.Core.Services.Implement } } - foreach (var d in _documentRepository.GetContentForExpiration(date)) - { - - if (d.ContentType.VariesByCulture()) - { - //find which cultures have pending schedules - var pendingCultures = d.ContentSchedule.GetPending(ContentScheduleAction.Expire, date) - .Select(x => x.Culture) - .Distinct() - .ToList(); + _documentRepository.ClearSchedule(date, ContentScheduleAction.Release); - if (pendingCultures.Count == 0) - break; //shouldn't happen but no point in continuing if there's nothing there - - var saveEventArgs = new ContentSavingEventArgs(d, evtMsgs); - if (scope.Events.DispatchCancelable(Saving, this, saveEventArgs, nameof(Saving))) - { - results.Add(new PublishResult(PublishResultType.FailedPublishCancelledByEvent, evtMsgs, d)); - continue; - } - - foreach (var c in pendingCultures) - { - //Clear this schedule for this culture - d.ContentSchedule.Clear(c, ContentScheduleAction.Expire, date); - //set the culture to be published - d.UnpublishCulture(c); - } - - var result = CommitDocumentChangesInternal(scope, d, saveEventArgs, allLangs, d.WriterId); - if (result.Success == false) - Logger.Error(null, "Failed to publish document id={DocumentId}, reason={Reason}.", d.Id, result.Result); - results.Add(result); - - } - else - { - //Clear this schedule - d.ContentSchedule.Clear(ContentScheduleAction.Expire, date); - var result = Unpublish(d, userId: d.WriterId); - if (result.Success == false) - Logger.Error(null, "Failed to unpublish document id={DocumentId}, reason={Reason}.", d.Id, result.Result); - results.Add(result); - } - - - } - - _documentRepository.ClearSchedule(date); - - scope.Complete(); } - return results; + scope.Complete(); } // utility 'PublishCultures' func used by SaveAndPublishBranch @@ -2627,7 +2649,7 @@ namespace Umbraco.Core.Services.Implement // there will be nothing to publish/unpublish. return new PublishResult(PublishResultType.FailedPublishNothingToPublish, evtMsgs, content); } - + // missing mandatory culture = cannot be published var mandatoryCultures = allLangs.Where(x => x.IsMandatory).Select(x => x.IsoCode); @@ -3140,6 +3162,6 @@ namespace Umbraco.Core.Services.Implement #endregion - + } } From 6dfb31f42b6e6e9be2fa70a696bc8649c4735412 Mon Sep 17 00:00:00 2001 From: Shannon Date: Thu, 9 Jul 2020 13:40:35 +1000 Subject: [PATCH 316/377] revert ssl port --- src/Umbraco.Web.UI/Umbraco.Web.UI.csproj | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj index 009659c0e2..7b3a24512f 100644 --- a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj +++ b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj @@ -12,8 +12,7 @@ {349c5851-65df-11da-9384-00065b846f21};{fae04ec0-301f-11d3-bf4b-00c04f79efbc} OnBuildSuccess true - - + 44331 enabled disabled false From 15b903129e313baff3a6e662005b5c39ce635416 Mon Sep 17 00:00:00 2001 From: Shannon Date: Thu, 9 Jul 2020 14:32:48 +1000 Subject: [PATCH 317/377] comments --- src/Umbraco.Core/Services/Implement/ContentService.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Umbraco.Core/Services/Implement/ContentService.cs b/src/Umbraco.Core/Services/Implement/ContentService.cs index 2a23a1adad..f8b63a2c40 100644 --- a/src/Umbraco.Core/Services/Implement/ContentService.cs +++ b/src/Umbraco.Core/Services/Implement/ContentService.cs @@ -1392,7 +1392,7 @@ namespace Umbraco.Core.Services.Implement .ToList(); if (pendingCultures.Count == 0) - continue; //shouldn't happen but no point in continuing if there's nothing there + continue; //shouldn't happen but no point in processing this document if there's nothing there var saveEventArgs = new ContentSavingEventArgs(d, evtMsgs); if (scope.Events.DispatchCancelable(Saving, this, saveEventArgs, nameof(Saving))) @@ -1453,7 +1453,7 @@ namespace Umbraco.Core.Services.Implement .ToList(); if (pendingCultures.Count == 0) - continue; //shouldn't happen but no point in continuing if there's nothing there + continue; //shouldn't happen but no point in processing this document if there's nothing there var saveEventArgs = new ContentSavingEventArgs(d, evtMsgs); if (scope.Events.DispatchCancelable(Saving, this, saveEventArgs, nameof(Saving))) @@ -1479,7 +1479,7 @@ namespace Umbraco.Core.Services.Implement d.Id, culture, string.Join(",", invalidProperties.Select(x => x.Alias))); publishing &= tryPublish; //set the culture to be published - if (!publishing) continue; // no point continuing + if (!publishing) continue; // move to next document } PublishResult result; From 0196e55c19c5249fcc0dad013775c36a701b8977 Mon Sep 17 00:00:00 2001 From: Shannon Date: Thu, 9 Jul 2020 16:33:01 +1000 Subject: [PATCH 318/377] Cherry picks fix #7994 for #5151 --- .../Services/UserServiceExtensions.cs | 13 +++++++ src/Umbraco.Examine/ContentValueSetBuilder.cs | 34 +++++++++++++++++-- .../Services/UserServiceTests.cs | 18 ++++++++++ .../UmbracoExamine/IndexInitializer.cs | 16 ++++++--- src/Umbraco.Tests/UmbracoExamine/IndexTest.cs | 10 +++--- .../UmbracoExamine/SearchTests.cs | 2 +- src/Umbraco.Web/Search/ExamineComposer.cs | 3 ++ 7 files changed, 83 insertions(+), 13 deletions(-) diff --git a/src/Umbraco.Core/Services/UserServiceExtensions.cs b/src/Umbraco.Core/Services/UserServiceExtensions.cs index 82cab07b25..c365f1ccc2 100644 --- a/src/Umbraco.Core/Services/UserServiceExtensions.cs +++ b/src/Umbraco.Core/Services/UserServiceExtensions.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Linq; using System.Web.Security; using Umbraco.Core.Models.Membership; @@ -116,5 +117,17 @@ namespace Umbraco.Core.Services var permissionCollection = userService.GetPermissions(user, nodeId); return permissionCollection.SelectMany(c => c.AssignedPermissions).Distinct().ToArray(); } + + internal static IEnumerable GetProfilesById(this IUserService userService, params int[] ids) + { + var fullUsers = userService.GetUsersById(ids); + + return fullUsers.Select(user => + { + var asProfile = user as IProfile; + return asProfile ?? new UserProfile(user.Id, user.Name); + }); + + } } } diff --git a/src/Umbraco.Examine/ContentValueSetBuilder.cs b/src/Umbraco.Examine/ContentValueSetBuilder.cs index 9cbc311639..b8477a9047 100644 --- a/src/Umbraco.Examine/ContentValueSetBuilder.cs +++ b/src/Umbraco.Examine/ContentValueSetBuilder.cs @@ -1,9 +1,13 @@ using Examine; +using System; using System.Collections.Generic; using System.Linq; using Umbraco.Core; +using Umbraco.Core.Composing; using Umbraco.Core.Models; +using Umbraco.Core.Models.Membership; using Umbraco.Core.PropertyEditors; +using Umbraco.Core.Scoping; using Umbraco.Core.Services; using Umbraco.Core.Strings; @@ -16,20 +20,46 @@ namespace Umbraco.Examine { private readonly UrlSegmentProviderCollection _urlSegmentProviders; private readonly IUserService _userService; + private readonly IScopeProvider _scopeProvider; + + [Obsolete("Use the other ctor instead")] + public ContentValueSetBuilder(PropertyEditorCollection propertyEditors, + UrlSegmentProviderCollection urlSegmentProviders, + IUserService userService, + bool publishedValuesOnly) + : this(propertyEditors, urlSegmentProviders, userService, Current.ScopeProvider, publishedValuesOnly) + { + } public ContentValueSetBuilder(PropertyEditorCollection propertyEditors, UrlSegmentProviderCollection urlSegmentProviders, IUserService userService, + IScopeProvider scopeProvider, bool publishedValuesOnly) : base(propertyEditors, publishedValuesOnly) { _urlSegmentProviders = urlSegmentProviders; _userService = userService; + _scopeProvider = scopeProvider; } /// public override IEnumerable GetValueSets(params IContent[] content) { + Dictionary creatorIds; + Dictionary writerIds; + + // We can lookup all of the creator/writer names at once which can save some + // processing below instead of one by one. + using (var scope = _scopeProvider.CreateScope()) + { + creatorIds = _userService.GetProfilesById(content.Select(x => x.CreatorId).ToArray()) + .ToDictionary(x => x.Id, x => x); + writerIds = _userService.GetProfilesById(content.Select(x => x.WriterId).ToArray()) + .ToDictionary(x => x.Id, x => x); + scope.Complete(); + } + // TODO: There is a lot of boxing going on here and ultimately all values will be boxed by Lucene anyways // but I wonder if there's a way to reduce the boxing that we have to do or if it will matter in the end since // Lucene will do it no matter what? One idea was to create a `FieldValue` struct which would contain `object`, `object[]`, `ValueType` and `ValueType[]` @@ -58,8 +88,8 @@ namespace Umbraco.Examine {"urlName", urlValue?.Yield() ?? Enumerable.Empty()}, //Always add invariant urlName {"path", c.Path?.Yield() ?? Enumerable.Empty()}, {"nodeType", c.ContentType.Id.ToString().Yield() ?? Enumerable.Empty()}, - {"creatorName", (c.GetCreatorProfile(_userService)?.Name ?? "??").Yield() }, - {"writerName",(c.GetWriterProfile(_userService)?.Name ?? "??").Yield() }, + {"creatorName", (creatorIds.TryGetValue(c.CreatorId, out var creatorProfile) ? creatorProfile.Name : "??").Yield() }, + {"writerName", (writerIds.TryGetValue(c.WriterId, out var writerProfile) ? writerProfile.Name : "??").Yield() }, {"writerID", new object[] {c.WriterId}}, {"templateID", new object[] {c.TemplateId ?? 0}}, {UmbracoContentIndex.VariesByCultureFieldName, new object[] {"n"}}, diff --git a/src/Umbraco.Tests/Services/UserServiceTests.cs b/src/Umbraco.Tests/Services/UserServiceTests.cs index a96385a923..016085c352 100644 --- a/src/Umbraco.Tests/Services/UserServiceTests.cs +++ b/src/Umbraco.Tests/Services/UserServiceTests.cs @@ -924,6 +924,24 @@ namespace Umbraco.Tests.Services Assert.AreEqual(user.Id, profile.Id); } + [Test] + public void Get_By_Profile_Id_Must_return_null_if_user_not_exists() + { + var profile = ServiceContext.UserService.GetProfileById(42); + + // Assert + Assert.IsNull(profile); + } + + [Test] + public void GetProfilesById_Must_empty_if_users_not_exists() + { + var profiles = ServiceContext.UserService.GetProfilesById(42); + + // Assert + CollectionAssert.IsEmpty(profiles); + } + [Test] public void Get_User_By_Username() { diff --git a/src/Umbraco.Tests/UmbracoExamine/IndexInitializer.cs b/src/Umbraco.Tests/UmbracoExamine/IndexInitializer.cs index 1653de827d..e9f18d8947 100644 --- a/src/Umbraco.Tests/UmbracoExamine/IndexInitializer.cs +++ b/src/Umbraco.Tests/UmbracoExamine/IndexInitializer.cs @@ -30,16 +30,22 @@ namespace Umbraco.Tests.UmbracoExamine /// internal static class IndexInitializer { - public static ContentValueSetBuilder GetContentValueSetBuilder(PropertyEditorCollection propertyEditors, bool publishedValuesOnly) + public static ContentValueSetBuilder GetContentValueSetBuilder(PropertyEditorCollection propertyEditors, IScopeProvider scopeProvider, bool publishedValuesOnly) { - var contentValueSetBuilder = new ContentValueSetBuilder(propertyEditors, new UrlSegmentProviderCollection(new[] { new DefaultUrlSegmentProvider() }), GetMockUserService(), publishedValuesOnly); + var contentValueSetBuilder = new ContentValueSetBuilder( + propertyEditors, + new UrlSegmentProviderCollection(new[] { new DefaultUrlSegmentProvider() }), + GetMockUserService(), + scopeProvider, + publishedValuesOnly); + return contentValueSetBuilder; } - public static ContentIndexPopulator GetContentIndexRebuilder(PropertyEditorCollection propertyEditors, IContentService contentService, ISqlContext sqlContext, bool publishedValuesOnly) + public static ContentIndexPopulator GetContentIndexRebuilder(PropertyEditorCollection propertyEditors, IContentService contentService, IScopeProvider scopeProvider, bool publishedValuesOnly) { - var contentValueSetBuilder = GetContentValueSetBuilder(propertyEditors, publishedValuesOnly); - var contentIndexDataSource = new ContentIndexPopulator(true, null, contentService, sqlContext, contentValueSetBuilder); + var contentValueSetBuilder = GetContentValueSetBuilder(propertyEditors, scopeProvider, publishedValuesOnly); + var contentIndexDataSource = new ContentIndexPopulator(true, null, contentService, scopeProvider.SqlContext, contentValueSetBuilder); return contentIndexDataSource; } diff --git a/src/Umbraco.Tests/UmbracoExamine/IndexTest.cs b/src/Umbraco.Tests/UmbracoExamine/IndexTest.cs index 9e59422310..acb26fb8f6 100644 --- a/src/Umbraco.Tests/UmbracoExamine/IndexTest.cs +++ b/src/Umbraco.Tests/UmbracoExamine/IndexTest.cs @@ -29,7 +29,7 @@ namespace Umbraco.Tests.UmbracoExamine [Test] public void Index_Property_Data_With_Value_Indexer() { - var contentValueSetBuilder = IndexInitializer.GetContentValueSetBuilder(Factory.GetInstance(), false); + var contentValueSetBuilder = IndexInitializer.GetContentValueSetBuilder(Factory.GetInstance(), ScopeProvider, false); using (var luceneDir = new RandomIdRamDirectory()) using (var indexer = IndexInitializer.GetUmbracoIndexer(ProfilingLogger, luceneDir, @@ -121,7 +121,7 @@ namespace Umbraco.Tests.UmbracoExamine [Test] public void Rebuild_Index() { - var contentRebuilder = IndexInitializer.GetContentIndexRebuilder(Factory.GetInstance(), IndexInitializer.GetMockContentService(), ScopeProvider.SqlContext, false); + var contentRebuilder = IndexInitializer.GetContentIndexRebuilder(Factory.GetInstance(), IndexInitializer.GetMockContentService(), ScopeProvider, false); var mediaRebuilder = IndexInitializer.GetMediaIndexRebuilder(Factory.GetInstance(), IndexInitializer.GetMockMediaService()); using (var luceneDir = new RandomIdRamDirectory()) @@ -149,7 +149,7 @@ namespace Umbraco.Tests.UmbracoExamine [Test] public void Index_Protected_Content_Not_Indexed() { - var rebuilder = IndexInitializer.GetContentIndexRebuilder(Factory.GetInstance(), IndexInitializer.GetMockContentService(), ScopeProvider.SqlContext, false); + var rebuilder = IndexInitializer.GetContentIndexRebuilder(Factory.GetInstance(), IndexInitializer.GetMockContentService(), ScopeProvider, false); using (var luceneDir = new RandomIdRamDirectory()) @@ -274,7 +274,7 @@ namespace Umbraco.Tests.UmbracoExamine [Test] public void Index_Reindex_Content() { - var rebuilder = IndexInitializer.GetContentIndexRebuilder(Factory.GetInstance(), IndexInitializer.GetMockContentService(), ScopeProvider.SqlContext, false); + var rebuilder = IndexInitializer.GetContentIndexRebuilder(Factory.GetInstance(), IndexInitializer.GetMockContentService(), ScopeProvider, false); using (var luceneDir = new RandomIdRamDirectory()) using (var indexer = IndexInitializer.GetUmbracoIndexer(ProfilingLogger, luceneDir, validator: new ContentValueSetValidator(false))) @@ -315,7 +315,7 @@ namespace Umbraco.Tests.UmbracoExamine public void Index_Delete_Index_Item_Ensure_Heirarchy_Removed() { - var rebuilder = IndexInitializer.GetContentIndexRebuilder(Factory.GetInstance(), IndexInitializer.GetMockContentService(), ScopeProvider.SqlContext, false); + var rebuilder = IndexInitializer.GetContentIndexRebuilder(Factory.GetInstance(), IndexInitializer.GetMockContentService(), ScopeProvider, false); using (var luceneDir = new RandomIdRamDirectory()) using (var indexer = IndexInitializer.GetUmbracoIndexer(ProfilingLogger, luceneDir)) diff --git a/src/Umbraco.Tests/UmbracoExamine/SearchTests.cs b/src/Umbraco.Tests/UmbracoExamine/SearchTests.cs index a45a33ec00..96e8892cd1 100644 --- a/src/Umbraco.Tests/UmbracoExamine/SearchTests.cs +++ b/src/Umbraco.Tests/UmbracoExamine/SearchTests.cs @@ -55,7 +55,7 @@ namespace Umbraco.Tests.UmbracoExamine allRecs); var propertyEditors = Factory.GetInstance(); - var rebuilder = IndexInitializer.GetContentIndexRebuilder(propertyEditors, contentService, ScopeProvider.SqlContext, true); + var rebuilder = IndexInitializer.GetContentIndexRebuilder(propertyEditors, contentService, ScopeProvider, true); using (var luceneDir = new RandomIdRamDirectory()) using (var indexer = IndexInitializer.GetUmbracoIndexer(ProfilingLogger, luceneDir)) diff --git a/src/Umbraco.Web/Search/ExamineComposer.cs b/src/Umbraco.Web/Search/ExamineComposer.cs index b30f0cbe03..64eeb6978a 100644 --- a/src/Umbraco.Web/Search/ExamineComposer.cs +++ b/src/Umbraco.Web/Search/ExamineComposer.cs @@ -4,6 +4,7 @@ using Umbraco.Core; using Umbraco.Core.Composing; using Umbraco.Core.Models; using Umbraco.Core.PropertyEditors; +using Umbraco.Core.Scoping; using Umbraco.Core.Services; using Umbraco.Core.Strings; using Umbraco.Examine; @@ -36,12 +37,14 @@ namespace Umbraco.Web.Search factory.GetInstance(), factory.GetInstance(), factory.GetInstance(), + factory.GetInstance(), true)); composition.RegisterUnique(factory => new ContentValueSetBuilder( factory.GetInstance(), factory.GetInstance(), factory.GetInstance(), + factory.GetInstance(), false)); composition.RegisterUnique, MediaValueSetBuilder>(); composition.RegisterUnique, MemberValueSetBuilder>(); From 0722adba2f973924b3e22b69a7fcb791e7560e4a Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 10 Jul 2020 08:44:43 +1000 Subject: [PATCH 319/377] WIP - with lots of notes --- .../lib/umbraco/Extensions.js | 12 ++ .../validation/valpropertymsg.directive.js | 88 +++++++---- .../validation/valserver.directive.js | 17 +-- .../common/services/angularhelper.service.js | 48 +++--- .../services/contenteditinghelper.service.js | 1 + .../src/common/services/formhelper.service.js | 9 +- .../services/servervalidationmgr.service.js | 127 +++++++++++----- .../blockeditor/blockeditor.controller.js | 3 +- .../blockeditor/blockeditor.html | 5 +- .../components/property/umb-property.html | 1 + .../server-validation-manager.spec.js | 142 ++++++++++-------- 11 files changed, 286 insertions(+), 167 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/lib/umbraco/Extensions.js b/src/Umbraco.Web.UI.Client/lib/umbraco/Extensions.js index 823d3d526d..54fda13a0d 100644 --- a/src/Umbraco.Web.UI.Client/lib/umbraco/Extensions.js +++ b/src/Umbraco.Web.UI.Client/lib/umbraco/Extensions.js @@ -69,6 +69,18 @@ }; } + if (!String.prototype.trimStartSpecial) { + /** trimSpecial extension method for string */ + // Removes all non printable chars from beginning of a string + String.prototype.trimStartSpecial = function () { + var index = 0; + while (this.charCodeAt(index) <= 46) { + index++; + } + return this.substr(index); + }; + } + if (!String.prototype.startsWith) { /** startsWith extension method for string */ String.prototype.startsWith = function (str) { diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/validation/valpropertymsg.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/validation/valpropertymsg.directive.js index c586c88d38..353ff2a137 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/validation/valpropertymsg.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/validation/valpropertymsg.directive.js @@ -158,33 +158,33 @@ function valPropertyMsg(serverValidationManager, localizationService, angularHel if (hadError) { scope.$evalAsync(function () { - // TODO: This does not work :( :( :( - // We cannot clear a val-property-msg because another nested child might have server validation errors too. - // I 'think' we might be able to set the UI validation of this val-property-msg based on the child validators as well - // as the server validator so it can 'just' unset itself if all child validators are cleared. Can it be done? + //// TODO: This does not work :( :( :( + //// We cannot clear a val-property-msg because another nested child might have server validation errors too. + //// I 'think' we might be able to set the UI validation of this val-property-msg based on the child validators as well + //// as the server validator so it can 'just' unset itself if all child validators are cleared. Can it be done? - // Here we loop over the umbProperty hierarchy to see if we should clear the val-property-msg server validation key. - // we will clear the key if the parent for is valid, or if the parent form is only invalid due to a single val-property-msg error. - var currUmbProperty = umbPropCtrl; - var parentValidationKey = currUmbProperty.getParentValidationPath(); - while (currUmbProperty && parentValidationKey) { + //// Here we loop over the umbProperty hierarchy to see if we should clear the val-property-msg server validation key. + //// we will clear the key if the parent for is valid, or if the parent form is only invalid due to a single val-property-msg error. + //var currUmbProperty = umbPropCtrl; + //var parentValidationKey = currUmbProperty.getParentValidationPath(); + //while (currUmbProperty && parentValidationKey) { - if (!currUmbProperty.parentForm.$invalid || (_.keys(currUmbProperty.parentForm.$error).length === 1 && currUmbProperty.parentForm.$error.valPropertyMsg)) { - serverValidationManager.removePropertyError(parentValidationKey, currentCulture, "", currentSegment); + // if (!currUmbProperty.parentForm.$invalid || (_.keys(currUmbProperty.parentForm.$error).length === 1 && currUmbProperty.parentForm.$error.valPropertyMsg)) { + // serverValidationManager.removePropertyError(parentValidationKey, currentCulture, "", currentSegment); - // re-assign and loop - if (currUmbProperty !== umbPropCtrl.parentUmbProperty) { - currUmbProperty = umbPropCtrl.parentUmbProperty; - parentValidationKey = currUmbProperty ? currUmbProperty.getParentValidationPath() : null; - } - else { - break; - } - } - else { - break; - } - } + // // re-assign and loop + // if (currUmbProperty !== umbPropCtrl.parentUmbProperty) { + // currUmbProperty = umbPropCtrl.parentUmbProperty; + // parentValidationKey = currUmbProperty ? currUmbProperty.getParentValidationPath() : null; + // } + // else { + // break; + // } + // } + // else { + // break; + // } + //} //// we need to navigate the parentForm here, unfortunately there's no real alternative unless we create our own directive //// of some sort but that would also get messy. This works though since in this case we're always going to be in the property @@ -205,19 +205,43 @@ function valPropertyMsg(serverValidationManager, localizationService, angularHel // serverValidationManager.removePropertyError(parentValidationKey, currentCulture, "", currentSegment); // } //} - + }); - - - } + + + } } + //function checkFormValidation(f) { + + // if (!angularHelper.isForm(f)) { + // throw "The object is not an angular Form"; + // } + + // // if there's no value, the controls can be reset, which clears the error state on formCtrl + // for (let control of formCtrl.$getControls()) { + // control.$setValidity(); + // } + + //} + function checkValidationStatus() { if (formCtrl.$invalid) { //first we need to check if the valPropertyMsg validity is invalid if (formCtrl.$error.valPropertyMsg && formCtrl.$error.valPropertyMsg.length > 0) { //since we already have an error we'll just return since this means we've already set the // hasError and errorMsg properties which occurs below in the serverValidationManager.subscribe + + // TODO: This does not work! we cannot just clear if there are no errors in child controls because child controls + // won't even have been loaded yet so this will just instantly clear them + //// At this stage we might have an error assigned because it was assigned based on validation hierarchy from the server, + //// BUT one or ALL of the child server (and client) validations may be cleared at this point. We will know if we have an + //// explicitly assigned error based on the error message assigned, if it is a non-explicit error (meaning that the error + //// was assigned because it has child errors) then the message will just be: labels.propertyHasErrors + //if (scope.errorMsg === labels.propertyHasErrors && _.every(formCtrl.$getControls(), c => c.$valid)) { + // resetError(); + //} + return; } //if there are any errors in the current property form that are not valPropertyMsg @@ -295,7 +319,7 @@ function valPropertyMsg(serverValidationManager, localizationService, angularHel // indicate that a content property is invalid at the property level since developers may not actually implement // the correct field validation in their property editors. - if (scope.currentProperty) { //this can be null if no property was assigned, TODO: I don't believe it can? If it was null we'd get errors above + if (scope.currentProperty) { //this can be null if no property was assigned function serverValidationManagerCallback(isValid, propertyErrors, allErrors) { hasError = !isValid; @@ -311,13 +335,13 @@ function valPropertyMsg(serverValidationManager, localizationService, angularHel } } - unsubscribe.push(serverValidationManager.subscribe(propertyValidationKey, + unsubscribe.push(serverValidationManager.subscribe( + propertyValidationKey, currentCulture, "", serverValidationManagerCallback, currentSegment - ) - ); + )); } //when the scope is disposed we need to unsubscribe diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/validation/valserver.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/validation/valserver.directive.js index f002d83360..2846a9d196 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/validation/valserver.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/validation/valserver.directive.js @@ -55,11 +55,8 @@ function valServer(serverValidationManager) { } } - function getPropertyValidationKey() { - // Get the property validation path if there is one, this is how wiring up any nested/virtual property validation works - var propertyValidationPath = umbPropCtrl ? umbPropCtrl.getValidationPath() : null; - return propertyValidationPath ? propertyValidationPath : currentProperty.alias; - } + // Get the property validation path if there is one, this is how wiring up any nested/virtual property validation works + var propertyValidationPath = umbPropCtrl ? umbPropCtrl.getValidationPath() : currentProperty.alias; // Need to watch the value model for it to change, previously we had subscribed to // modelCtrl.$viewChangeListeners but this is not good enough if you have an editor that @@ -81,10 +78,10 @@ function valServer(serverValidationManager) { if (modelCtrl.$invalid) { modelCtrl.$setValidity('valServer', true); - console.log("valServer cleared (watch)"); + console.log("valServer cleared (watch) " + propertyValidationPath); //clear the server validation entry - serverValidationManager.removePropertyError(getPropertyValidationKey(), currentCulture, fieldName, currentSegment); + serverValidationManager.removePropertyError(propertyValidationPath, currentCulture, fieldName, currentSegment); stopWatch(); } }, true); @@ -102,14 +99,14 @@ function valServer(serverValidationManager) { function serverValidationManagerCallback(isValid, propertyErrors, allErrors) { if (!isValid) { modelCtrl.$setValidity('valServer', false); - console.log("valServer error"); + console.log("valServer error " + propertyValidationPath); //assign an error msg property to the current validator modelCtrl.errorMsg = propertyErrors[0].errorMsg; startWatch(); } else { modelCtrl.$setValidity('valServer', true); - console.log("valServer cleared"); + console.log("valServer cleared " + propertyValidationPath); //reset the error message modelCtrl.errorMsg = ""; stopWatch(); @@ -119,7 +116,7 @@ function valServer(serverValidationManager) { unsubscribe.push(serverValidationManager.subscribe( - getPropertyValidationKey(), + propertyValidationPath, currentCulture, fieldName, serverValidationManagerCallback, diff --git a/src/Umbraco.Web.UI.Client/src/common/services/angularhelper.service.js b/src/Umbraco.Web.UI.Client/src/common/services/angularhelper.service.js index 325c6255a5..12247f15b5 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/angularhelper.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/angularhelper.service.js @@ -7,6 +7,9 @@ * Some angular helper/extension methods */ function angularHelper($q) { + + var requiredFormProps = ["$error", "$name", "$dirty", "$pristine", "$valid", "$submitted", "$pending"]; + return { /** @@ -100,6 +103,28 @@ function angularHelper($q) { } }, + + isForm: function (obj) { + + // a method to check that the collection of object prop names contains the property name expected + function allPropertiesExist(objectPropNames) { + //ensure that every required property name exists on the current object + return _.every(requiredFormProps, function (item) { + return _.contains(objectPropNames, item); + }); + } + + //get the keys of the property names for the current object + var props = _.keys(obj); + //if the length isn't correct, try the next prop + if (props.length < requiredFormProps.length) { + return false; + } + + //ensure that every required property name exists on the current scope property + return allPropertiesExist(props); + }, + /** * @ngdoc function * @name getCurrentForm @@ -121,31 +146,10 @@ function angularHelper($q) { // is to inject the $element object and use: $element.inheritedData('$formController'); var form = null; - var requiredFormProps = ["$error", "$name", "$dirty", "$pristine", "$valid", "$submitted", "$pending"]; - - // a method to check that the collection of object prop names contains the property name expected - function propertyExists(objectPropNames) { - //ensure that every required property name exists on the current scope property - return _.every(requiredFormProps, function (item) { - - return _.contains(objectPropNames, item); - }); - } for (var p in scope) { - if (_.isObject(scope[p]) && p !== "this" && p.substr(0, 1) !== "$") { - //get the keys of the property names for the current property - var props = _.keys(scope[p]); - //if the length isn't correct, try the next prop - if (props.length < requiredFormProps.length) { - continue; - } - - //ensure that every required property name exists on the current scope property - var containProperty = propertyExists(props); - - if (containProperty) { + if (this.isForm(scope[p])) { form = scope[p]; break; } diff --git a/src/Umbraco.Web.UI.Client/src/common/services/contenteditinghelper.service.js b/src/Umbraco.Web.UI.Client/src/common/services/contenteditinghelper.service.js index bfcc0d536e..94e04acf6b 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/contenteditinghelper.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/contenteditinghelper.service.js @@ -614,6 +614,7 @@ function contentEditingHelper(fileManager, $q, $location, $routeParams, editorSt formHelper.handleServerValidation(args.err.data.ModelState); //add model state errors to notifications + // TODO: Need to ignore complex messages if (args.showNotifications) { for (var e in args.err.data.ModelState) { notificationsService.error("Validation", args.err.data.ModelState[e][0]); diff --git a/src/Umbraco.Web.UI.Client/src/common/services/formhelper.service.js b/src/Umbraco.Web.UI.Client/src/common/services/formhelper.service.js index adca9b30a5..49980a8c41 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/formhelper.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/formhelper.service.js @@ -54,8 +54,13 @@ function formHelper(angularHelper, serverValidationManager, notificationsService } } - //reset the server validations - serverValidationManager.reset(); + //reset the server validations if required (default is true), otherwise notify existing ones of changes + if (!args.keepServerValidation) { + serverValidationManager.reset(); + } + else { + serverValidationManager.notify(); + } return true; }, diff --git a/src/Umbraco.Web.UI.Client/src/common/services/servervalidationmgr.service.js b/src/Umbraco.Web.UI.Client/src/common/services/servervalidationmgr.service.js index 8999efda64..4cec5a00fe 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/servervalidationmgr.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/servervalidationmgr.service.js @@ -10,9 +10,29 @@ */ function serverValidationManager($timeout, udiService) { + // TODO: It would be nicer to just SHA1 hash these 'keys' instead of having the keys be compound values + // It would be another lib dependency or we could embed this https://github.com/emn178/js-sha1 + // this would remove the need for the auto-generated 'id' values since the hash would be the actual id value. + + // The array of callback objects, each object 'key' is: + // - propertyAlias + // - culture + // - fieldName + // - segment + // The object also contains: + // - callback (function) + // - id (unique identifier, auto-generated, used internally for unsubscribing the callback) var callbacks = []; - // The array of error messages + // The array of error message objects, each object 'key' is: + // - propertyAlias + // - culture + // - fieldName + // - segment + // The object also contains: + // - errorMsg + // - id (unique identifier, auto-generated, used internally for mapping parent/child property validation messages) + // - parentId (used to map parent/child property validation messages) var items = []; /** calls the callback specified with the errors specified, used internally */ @@ -41,6 +61,13 @@ function serverValidationManager($timeout, udiService) { */ function notify() { $timeout(function () { + + console.log(`VAL-ERROR-COUNT: ${items.length}`); + for (var i = 0; i < items.length; i++) { + var item = items[i]; + console.log(`VAL-ERROR [${item.propertyAlias}] [${item.culture}] [${item.fieldName}] [${item.segment}]`) + } + notifyCallbacks(); }); } @@ -76,7 +103,14 @@ function serverValidationManager($timeout, udiService) { return (item.propertyAlias === propertyAlias && item.culture === culture && item.segment === segment && (item.fieldName === fieldName || (fieldName === undefined || fieldName === ""))); }); } - + + function getPropertyErrorById(id) { + //find all errors for this id + return _.find(items, function (item) { + return (item.id === id); + }); + } + function getVariantErrors(culture, segment) { if (!culture) { @@ -118,35 +152,47 @@ function serverValidationManager($timeout, udiService) { } /** - * Returns a dictionary of id (of the block) and it's corresponding validation ModelState + * Flattens the complex errror result json into an array of the block's id/parent id and it's corresponding validation ModelState * @param {any} errorMsg + * @param {any} the id of the parentId (if any) */ - function parseComplexEditorError(errorMsg) { + function parseComplexEditorError(errorMsg, parentId) { - var json = JSON.parse(errorMsg); + var json = Utilities.isArray(errorMsg) ? errorMsg : JSON.parse(errorMsg); - var result = {}; + var result = []; - function extractModelState(validation) { + function extractModelState(validation, pid) { if (validation.$id && validation.ModelState) { - result[validation.$id] = validation.ModelState; + var ms = { + id: validation.$id, + elementUdi: udiService.build("element", validation.$id), + parentId: pid, + modelState: validation.ModelState + }; + result.push(ms); + return ms; } + return null; } - function iterateErrorBlocks(blocks) { + function iterateErrorBlocks(blocks, pid) { for (var i = 0; i < blocks.length; i++) { var validation = blocks[i]; - extractModelState(validation); + var ms = extractModelState(validation, pid); + if (!ms) { + continue; + } var nested = _.omit(validation, "$id", "$elementTypeAlias", "ModelState"); for (const [key, value] of Object.entries(nested)) { if (Array.isArray(value)) { - iterateErrorBlocks(value); // recurse + iterateErrorBlocks(value, ms.id); // recurse } } } } - iterateErrorBlocks(json); + iterateErrorBlocks(json, parentId); return result; } @@ -258,7 +304,7 @@ function serverValidationManager($timeout, udiService) { * @description * Adds an error message for the content property */ - function addPropertyError(propertyAlias, culture, fieldName, errorMsg, segment) { + function addPropertyError(propertyAlias, culture, fieldName, errorMsg, segment, parentId) { if (!propertyAlias) { return; @@ -277,14 +323,19 @@ function serverValidationManager($timeout, udiService) { errorMsg = ""; } - // if the error message is json it's a complex editor validation response that we need to parse - if (errorMsg.startsWith("[")) { + var id = String.CreateGuid(); - var idsToErrors = parseComplexEditorError(errorMsg); - for (const [key, value] of Object.entries(idsToErrors)) { - const elementUdi = udiService.build("element", key); - addErrorsForModelState(value, elementUdi); - } + // remove all non printable chars and whitespace from the string + if (Utilities.isString(errorMsg)) { + errorMsg = errorMsg.trimStartSpecial().trim(); + } + + // if the error message is json it's a complex editor validation response that we need to parse + if ((Utilities.isString(errorMsg) && errorMsg.startsWith("[")) || Utilities.isArray(errorMsg)) { + + var idsToErrors = parseComplexEditorError(errorMsg, id); + console.log("idsToErrors = " + JSON.stringify(idsToErrors)); + idsToErrors.forEach(x => addErrorsForModelState(x.modelState, x.elementUdi, x.parentId)); // We need to clear the error message else it will show up as a giant json block against the property errorMsg = ""; @@ -293,6 +344,8 @@ function serverValidationManager($timeout, udiService) { //only add the item if it doesn't exist if (!hasPropertyError(propertyAlias, culture, fieldName, segment)) { items.push({ + id: id, + parentId: parentId, propertyAlias: propertyAlias, culture: culture, segment: segment, @@ -368,10 +421,16 @@ function serverValidationManager($timeout, udiService) { * @methodOf umbraco.services.serverValidationManager * @param {any} modelState * @param {any} elementUdi optional parameter specifying a nested element's UDI for which this property belongs (for complex editors) + * @param {any} parentId optional parameter specifying the parentId validation item object (for complex editors) * @description * This wires up all of the server validation model state so that valServer and valServerField directives work */ - function addErrorsForModelState(modelState, elementUdi) { + function addErrorsForModelState(modelState, elementUdi, parentId) { + + if (!Utilities.isObject(modelState)) { + throw "modelState is not an object"; + } + for (const [key, value] of Object.entries(modelState)) { //This is where things get interesting.... @@ -437,7 +496,7 @@ function serverValidationManager($timeout, udiService) { } // add a generic error for the property - addPropertyError(propertyValidationKey, culture, htmlFieldReference, value && Array.isArray(value) && value.length > 0 ? value[0] : null, segment); + addPropertyError(propertyValidationKey, culture, htmlFieldReference, value && Array.isArray(value) && value.length > 0 ? value[0] : null, segment, parentId); } else { @@ -541,6 +600,8 @@ function serverValidationManager($timeout, udiService) { return; } + console.log(`serverValidationMgr subscribed [${propertyAlias}] [${culture}] [${fieldName}] [${segment}]`); + var id = String.CreateGuid(); //normalize culture to "invariant" @@ -700,23 +761,15 @@ function serverValidationManager($timeout, udiService) { * Gets the error message for the content property */ getPropertyError: function (propertyAlias, culture, fieldName, segment) { - - //normalize culture to "invariant" - if (!culture) { - culture = "invariant"; + var errors = getPropertyErrors(propertyAlias, culture, segment, fieldName); + if (errors.length > 0) { // should only ever contain one + return errors[0]; } - //normalize segment to null - if (!segment) { - segment = null; - } - - var err = _.find(items, function (item) { - //return true if the property alias matches and if an empty field name is specified or the field name matches - return (item.propertyAlias === propertyAlias && item.culture === culture && item.segment === segment && (item.fieldName === fieldName || (fieldName === undefined || fieldName === ""))); - }); - return err; + return undefined; }, - + + getPropertyErrorById: getPropertyErrorById, + /** * @ngdoc function * @name getFieldError 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 da7ba52536..a62b632d38 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 @@ -56,7 +56,8 @@ angular.module("umbraco") vm.submitAndClose = function () { if (vm.model && vm.model.submit) { - if (formHelper.submitForm({ scope: $scope })) { + // always keep server validations since this will be a nested editor and server validations are global + if (formHelper.submitForm({ scope: $scope, formCtrl: vm.blockForm, keepServerValidation: true })) { vm.model.submit(vm.model); } } diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/blockeditor/blockeditor.html b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/blockeditor/blockeditor.html index ce157c95f5..01ce137259 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/blockeditor/blockeditor.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/blockeditor/blockeditor.html @@ -1,8 +1,11 @@
        - + + +
        {{vm.blockForm.$valid}}
        + {{ vm.getValidationPath() }} +
        diff --git a/src/Umbraco.Web.UI.Client/test/unit/common/services/server-validation-manager.spec.js b/src/Umbraco.Web.UI.Client/test/unit/common/services/server-validation-manager.spec.js index 009043cae7..c9e2faf089 100644 --- a/src/Umbraco.Web.UI.Client/test/unit/common/services/server-validation-manager.spec.js +++ b/src/Umbraco.Web.UI.Client/test/unit/common/services/server-validation-manager.spec.js @@ -4,7 +4,8 @@ beforeEach(module('umbraco.services')); beforeEach(inject(function ($injector) { - serverValidationManager = $injector.get('serverValidationManager'); + serverValidationManager = $injector.get('serverValidationManager'); + serverValidationManager.clear(); })); describe('managing field validation errors', function () { @@ -316,6 +317,51 @@ describe('managing complex editor validation errors', function () { + // this root element doesn't have it's own attached errors, instead it has model state just + // showing that it has errors within it's nested properties. that ModelState is automatically + // added on the server side. + var nonRootLevelComplexValidationMsg = `[ + { + "$elementTypeAlias": "addressBook", + "$id": "34E3A26C-103D-4A05-AB9D-7E14032309C3", + "addresses": + [ + { + "$elementTypeAlias": "addressInfo", + "$id": "FBEAEE8F-4BC9-43EE-8B81-FCA8978850F1", + "ModelState": + { + "_Properties.city.invariant.null.country": [ + "City is not in Australia" + ], + "_Properties.city.invariant.null.capital": [ + "Not a capital city" + ] + } + }, + { + "$elementTypeAlias": "addressInfo", + "$id": "7170A4DD-2441-4B1B-A8D3-437D75C4CBC9", + "ModelState": + { + "_Properties.city.invariant.null.country": [ + "City is not in Australia" + ], + "_Properties.city.invariant.null.capital": [ + "Not a capital city" + ] + } + } + ], + "ModelState": + { + "_Properties.addresses.invariant.null": [ + "" + ] + } + } +]`; + it('create dictionary of id to ModelState', function () { //arrange @@ -365,80 +411,52 @@ ]`; //act - var ids = serverValidationManager.parseComplexEditorError(complexValidationMsg); + var ms = serverValidationManager.parseComplexEditorError(complexValidationMsg); //assert - var keys = Object.keys(ids); - - expect(keys.length).toEqual(3); - expect(keys[0]).toEqual("34E3A26C-103D-4A05-AB9D-7E14032309C3"); - expect(keys[1]).toEqual("FBEAEE8F-4BC9-43EE-8B81-FCA8978850F1"); - expect(keys[2]).toEqual("7170A4DD-2441-4B1B-A8D3-437D75C4CBC9"); + expect(ms.length).toEqual(3); + expect(ms[0].id).toEqual("34E3A26C-103D-4A05-AB9D-7E14032309C3"); + expect(ms[1].id).toEqual("FBEAEE8F-4BC9-43EE-8B81-FCA8978850F1"); + expect(ms[2].id).toEqual("7170A4DD-2441-4B1B-A8D3-437D75C4CBC9"); }); it('create dictionary of id to ModelState with inherited errors', function () { - // arrange - // this root element doesn't have it's own attached errors, instead it has model state just - // showing that it has errors within it's nested properties. that ModelState is automatically - // added on the server side. - var complexValidationMsg = `[ - { - "$elementTypeAlias": "addressBook", - "$id": "34E3A26C-103D-4A05-AB9D-7E14032309C3", - "addresses": - [ - { - "$elementTypeAlias": "addressInfo", - "$id": "FBEAEE8F-4BC9-43EE-8B81-FCA8978850F1", - "ModelState": - { - "_Properties.city.invariant.null.country": [ - "City is not in Australia" - ], - "_Properties.city.invariant.null.capital": [ - "Not a capital city" - ] - } - }, - { - "$elementTypeAlias": "addressInfo", - "$id": "7170A4DD-2441-4B1B-A8D3-437D75C4CBC9", - "ModelState": - { - "_Properties.city.invariant.null.country": [ - "City is not in Australia" - ], - "_Properties.city.invariant.null.capital": [ - "Not a capital city" - ] - } - } - ], - "ModelState": - { - "_Properties.addresses.invariant.null": [ - "" - ] - } - } -]`; - //act - var ids = serverValidationManager.parseComplexEditorError(complexValidationMsg); + var ms = serverValidationManager.parseComplexEditorError(nonRootLevelComplexValidationMsg); //assert - var keys = Object.keys(ids); - - expect(keys.length).toEqual(3); - expect(keys[0]).toEqual("34E3A26C-103D-4A05-AB9D-7E14032309C3"); - var item0ModelState = ids["34E3A26C-103D-4A05-AB9D-7E14032309C3"]; + expect(ms.length).toEqual(3); + expect(ms[0].id).toEqual("34E3A26C-103D-4A05-AB9D-7E14032309C3"); + var item0ModelState = ms[0].modelState; expect(Object.keys(item0ModelState).length).toEqual(1); expect(item0ModelState["_Properties.addresses.invariant.null"].length).toEqual(1); expect(item0ModelState["_Properties.addresses.invariant.null"][0]).toEqual(""); - expect(keys[1]).toEqual("FBEAEE8F-4BC9-43EE-8B81-FCA8978850F1"); - expect(keys[2]).toEqual("7170A4DD-2441-4B1B-A8D3-437D75C4CBC9"); + expect(ms[1].id).toEqual("FBEAEE8F-4BC9-43EE-8B81-FCA8978850F1"); + expect(ms[2].id).toEqual("7170A4DD-2441-4B1B-A8D3-437D75C4CBC9"); + + }); + + it('add errors for ModelState with inherited errors', function () { + + //act + let modelState = { + "_Properties.blockFeatures.invariant.null": [ + nonRootLevelComplexValidationMsg + ] + }; + serverValidationManager.addErrorsForModelState(modelState); + + //assert + console.log(JSON.stringify(serverValidationManager.items)); + let propError = serverValidationManager.getPropertyError("umb://element/FBEAEE8F4BC943EE8B81FCA8978850F1/city"); + expect(propError).toBeDefined(); + console.log(JSON.stringify(propError)); + expect(propError.parentId).toBeDefined(); + let parentError = serverValidationManager.getPropertyErrorById(propError.parentId); + expect(parentError).toBeDefined(); + expect(parentError.propertyAlias).toEqual("umb://element/34E3A26C103D4A05AB9D7E14032309C3/addresses"); }); From bd3bc81b7c393d9ac8b348b5ff2b290db3792010 Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 10 Jul 2020 09:25:20 +1000 Subject: [PATCH 320/377] Gets validation paths generated and tested --- .../services/servervalidationmgr.service.js | 73 ++++++------------- .../server-validation-manager.spec.js | 37 ++++++---- ...omplexEditorElementTypeValidationResult.cs | 2 + .../Validation/ValidationResultConverter.cs | 2 + 4 files changed, 49 insertions(+), 65 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/services/servervalidationmgr.service.js b/src/Umbraco.Web.UI.Client/src/common/services/servervalidationmgr.service.js index 4cec5a00fe..3e7a3ab496 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/servervalidationmgr.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/servervalidationmgr.service.js @@ -8,14 +8,14 @@ * is for user defined properties (called Properties) and the other is for field properties which are attached to the native * model objects (not user defined). The methods below are named according to these rules: Properties vs Fields. */ -function serverValidationManager($timeout, udiService) { +function serverValidationManager($timeout) { // TODO: It would be nicer to just SHA1 hash these 'keys' instead of having the keys be compound values // It would be another lib dependency or we could embed this https://github.com/emn178/js-sha1 // this would remove the need for the auto-generated 'id' values since the hash would be the actual id value. // The array of callback objects, each object 'key' is: - // - propertyAlias + // - propertyAlias (this is the property's 'path' if it's a nested error) // - culture // - fieldName // - segment @@ -25,14 +25,12 @@ function serverValidationManager($timeout, udiService) { var callbacks = []; // The array of error message objects, each object 'key' is: - // - propertyAlias + // - propertyAlias (this is the property's 'path' if it's a nested error) // - culture // - fieldName // - segment // The object also contains: // - errorMsg - // - id (unique identifier, auto-generated, used internally for mapping parent/child property validation messages) - // - parentId (used to map parent/child property validation messages) var items = []; /** calls the callback specified with the errors specified, used internally */ @@ -104,13 +102,6 @@ function serverValidationManager($timeout, udiService) { }); } - function getPropertyErrorById(id) { - //find all errors for this id - return _.find(items, function (item) { - return (item.id === id); - }); - } - function getVariantErrors(culture, segment) { if (!culture) { @@ -154,20 +145,18 @@ function serverValidationManager($timeout, udiService) { /** * Flattens the complex errror result json into an array of the block's id/parent id and it's corresponding validation ModelState * @param {any} errorMsg - * @param {any} the id of the parentId (if any) + * @param {any} parentPropertyAlias The parent property alias for the json error */ - function parseComplexEditorError(errorMsg, parentId) { + function parseComplexEditorError(errorMsg, parentPropertyAlias) { var json = Utilities.isArray(errorMsg) ? errorMsg : JSON.parse(errorMsg); var result = []; - function extractModelState(validation, pid) { + function extractModelState(validation, parentPath) { if (validation.$id && validation.ModelState) { var ms = { - id: validation.$id, - elementUdi: udiService.build("element", validation.$id), - parentId: pid, + validationPath: `${parentPath}/${validation.$id}`, modelState: validation.ModelState }; result.push(ms); @@ -176,23 +165,23 @@ function serverValidationManager($timeout, udiService) { return null; } - function iterateErrorBlocks(blocks, pid) { + function iterateErrorBlocks(blocks, parentPath) { for (var i = 0; i < blocks.length; i++) { var validation = blocks[i]; - var ms = extractModelState(validation, pid); + var ms = extractModelState(validation, parentPath); if (!ms) { continue; } var nested = _.omit(validation, "$id", "$elementTypeAlias", "ModelState"); for (const [key, value] of Object.entries(nested)) { if (Array.isArray(value)) { - iterateErrorBlocks(value, ms.id); // recurse + iterateErrorBlocks(value, `${ms.validationPath}/${key}`); // recurse } } } } - iterateErrorBlocks(json, parentId); + iterateErrorBlocks(json, parentPropertyAlias); return result; } @@ -304,7 +293,7 @@ function serverValidationManager($timeout, udiService) { * @description * Adds an error message for the content property */ - function addPropertyError(propertyAlias, culture, fieldName, errorMsg, segment, parentId) { + function addPropertyError(propertyAlias, culture, fieldName, errorMsg, segment) { if (!propertyAlias) { return; @@ -323,9 +312,8 @@ function serverValidationManager($timeout, udiService) { errorMsg = ""; } - var id = String.CreateGuid(); - // remove all non printable chars and whitespace from the string + // (this can be a json string for complex errors and in some test cases contains odd whitespace) if (Utilities.isString(errorMsg)) { errorMsg = errorMsg.trimStartSpecial().trim(); } @@ -333,9 +321,9 @@ function serverValidationManager($timeout, udiService) { // if the error message is json it's a complex editor validation response that we need to parse if ((Utilities.isString(errorMsg) && errorMsg.startsWith("[")) || Utilities.isArray(errorMsg)) { - var idsToErrors = parseComplexEditorError(errorMsg, id); - console.log("idsToErrors = " + JSON.stringify(idsToErrors)); - idsToErrors.forEach(x => addErrorsForModelState(x.modelState, x.elementUdi, x.parentId)); + // flatten the json structure, create validation paths for each property and add each as a property error + var idsToErrors = parseComplexEditorError(errorMsg, propertyAlias); + idsToErrors.forEach(x => addErrorsForModelState(x.modelState, x.validationPath)); // We need to clear the error message else it will show up as a giant json block against the property errorMsg = ""; @@ -344,8 +332,6 @@ function serverValidationManager($timeout, udiService) { //only add the item if it doesn't exist if (!hasPropertyError(propertyAlias, culture, fieldName, segment)) { items.push({ - id: id, - parentId: parentId, propertyAlias: propertyAlias, culture: culture, segment: segment, @@ -420,12 +406,11 @@ function serverValidationManager($timeout, udiService) { * @name addErrorsForModelState * @methodOf umbraco.services.serverValidationManager * @param {any} modelState - * @param {any} elementUdi optional parameter specifying a nested element's UDI for which this property belongs (for complex editors) - * @param {any} parentId optional parameter specifying the parentId validation item object (for complex editors) + * @param {any} parentValidationPath optional parameter specifying a nested element's UDI for which this property belongs (for complex editors) * @description * This wires up all of the server validation model state so that valServer and valServerField directives work */ - function addErrorsForModelState(modelState, elementUdi, parentId) { + function addErrorsForModelState(modelState, parentValidationPath) { if (!Utilities.isObject(modelState)) { throw "modelState is not an object"; @@ -449,12 +434,6 @@ function serverValidationManager($timeout, udiService) { //There will always be at least 4 parts for content properties since all model errors for properties are prefixed with "_Properties" //If it is not prefixed with "_Properties" that means the error is for a field of the object directly. - // TODO: This 4 part dot notation isn't ideal and instead it would probably be nicer to have a json structure as the key (which could be converted - // to base64 if we cannot do that since it's a 'key'). That way the key can be flexible and 'future proof' since I'm sure something in the future - // will change for this. Another idea is to just have a single key for one property type and have the model error a json structure that handles - // everything. This would probably be the 'nicest' way but would require quite a lot of work. We are part way there with how we are doing - // validation for complex editors. - // Example: "_Properties.headerImage.en-US.mySegment.myField" // * it's for a property since it has a _Properties prefix // * it's for the headerImage property type @@ -468,9 +447,9 @@ function serverValidationManager($timeout, udiService) { // user defined properties with custom controls. if (parts.length > 1 && parts[0] === "_Properties") { - // create the validation key, might just be the prop alias but if it's nested will be a unique udi - // like umb://element/GUID/propertyAlias - var propertyValidationKey = createPropertyValidationKey(parts[1], elementUdi); + // create the validation key, might just be the prop alias but if it's nested will be validation path + // like "myBlockEditor/34E3A26C-103D-4A05-AB9D-7E14032309C3/addresses/7170A4DD-2441-4B1B-A8D3-437D75C4CBC9/city" + var propertyValidationKey = createPropertyValidationKey(parts[1], parentValidationPath); var culture = null; if (parts.length > 2) { @@ -496,7 +475,7 @@ function serverValidationManager($timeout, udiService) { } // add a generic error for the property - addPropertyError(propertyValidationKey, culture, htmlFieldReference, value && Array.isArray(value) && value.length > 0 ? value[0] : null, segment, parentId); + addPropertyError(propertyValidationKey, culture, htmlFieldReference, value && Array.isArray(value) && value.length > 0 ? value[0] : null, segment); } else { @@ -507,9 +486,8 @@ function serverValidationManager($timeout, udiService) { } } - // TODO: Write a test or two for this and probs a bunch of other things here too! - function createPropertyValidationKey(propertyAlias, elementUdi) { - return elementUdi ? (elementUdi + "/" + propertyAlias) : propertyAlias; + function createPropertyValidationKey(propertyAlias, parentValidationPath) { + return parentValidationPath ? (parentValidationPath + "/" + propertyAlias) : propertyAlias; } /** @@ -645,7 +623,6 @@ function serverValidationManager($timeout, udiService) { // Now notify the registrations for this callback if we've previously been notified and we're not cleared. // This will happen for dynamically shown editors, like complex editors that load in sub element types. - // TODO: We need to see what the repercussions of this are in other editors! notify(); //return a function to unsubscribe this subscription by uniqueId @@ -768,8 +745,6 @@ function serverValidationManager($timeout, udiService) { return undefined; }, - getPropertyErrorById: getPropertyErrorById, - /** * @ngdoc function * @name getFieldError diff --git a/src/Umbraco.Web.UI.Client/test/unit/common/services/server-validation-manager.spec.js b/src/Umbraco.Web.UI.Client/test/unit/common/services/server-validation-manager.spec.js index c9e2faf089..25831b91f3 100644 --- a/src/Umbraco.Web.UI.Client/test/unit/common/services/server-validation-manager.spec.js +++ b/src/Umbraco.Web.UI.Client/test/unit/common/services/server-validation-manager.spec.js @@ -411,30 +411,30 @@ ]`; //act - var ms = serverValidationManager.parseComplexEditorError(complexValidationMsg); + var ms = serverValidationManager.parseComplexEditorError(complexValidationMsg, "myBlockEditor"); //assert expect(ms.length).toEqual(3); - expect(ms[0].id).toEqual("34E3A26C-103D-4A05-AB9D-7E14032309C3"); - expect(ms[1].id).toEqual("FBEAEE8F-4BC9-43EE-8B81-FCA8978850F1"); - expect(ms[2].id).toEqual("7170A4DD-2441-4B1B-A8D3-437D75C4CBC9"); + expect(ms[0].validationPath).toEqual("myBlockEditor/34E3A26C-103D-4A05-AB9D-7E14032309C3"); + expect(ms[1].validationPath).toEqual("myBlockEditor/34E3A26C-103D-4A05-AB9D-7E14032309C3/addresses/FBEAEE8F-4BC9-43EE-8B81-FCA8978850F1"); + expect(ms[2].validationPath).toEqual("myBlockEditor/34E3A26C-103D-4A05-AB9D-7E14032309C3/addresses/7170A4DD-2441-4B1B-A8D3-437D75C4CBC9"); }); it('create dictionary of id to ModelState with inherited errors', function () { //act - var ms = serverValidationManager.parseComplexEditorError(nonRootLevelComplexValidationMsg); + var ms = serverValidationManager.parseComplexEditorError(nonRootLevelComplexValidationMsg, "myBlockEditor"); //assert expect(ms.length).toEqual(3); - expect(ms[0].id).toEqual("34E3A26C-103D-4A05-AB9D-7E14032309C3"); + expect(ms[0].validationPath).toEqual("myBlockEditor/34E3A26C-103D-4A05-AB9D-7E14032309C3"); var item0ModelState = ms[0].modelState; expect(Object.keys(item0ModelState).length).toEqual(1); expect(item0ModelState["_Properties.addresses.invariant.null"].length).toEqual(1); expect(item0ModelState["_Properties.addresses.invariant.null"][0]).toEqual(""); - expect(ms[1].id).toEqual("FBEAEE8F-4BC9-43EE-8B81-FCA8978850F1"); - expect(ms[2].id).toEqual("7170A4DD-2441-4B1B-A8D3-437D75C4CBC9"); + expect(ms[1].validationPath).toEqual("myBlockEditor/34E3A26C-103D-4A05-AB9D-7E14032309C3/addresses/FBEAEE8F-4BC9-43EE-8B81-FCA8978850F1"); + expect(ms[2].validationPath).toEqual("myBlockEditor/34E3A26C-103D-4A05-AB9D-7E14032309C3/addresses/7170A4DD-2441-4B1B-A8D3-437D75C4CBC9"); }); @@ -449,15 +449,20 @@ serverValidationManager.addErrorsForModelState(modelState); //assert - console.log(JSON.stringify(serverValidationManager.items)); - let propError = serverValidationManager.getPropertyError("umb://element/FBEAEE8F4BC943EE8B81FCA8978850F1/city"); - expect(propError).toBeDefined(); - console.log(JSON.stringify(propError)); - expect(propError.parentId).toBeDefined(); - let parentError = serverValidationManager.getPropertyErrorById(propError.parentId); - expect(parentError).toBeDefined(); - expect(parentError.propertyAlias).toEqual("umb://element/34E3A26C103D4A05AB9D7E14032309C3/addresses"); + var propertyErrors = [ + "blockFeatures", + "blockFeatures/34E3A26C-103D-4A05-AB9D-7E14032309C3/addresses", + "blockFeatures/34E3A26C-103D-4A05-AB9D-7E14032309C3/addresses/FBEAEE8F-4BC9-43EE-8B81-FCA8978850F1/city", + "blockFeatures/34E3A26C-103D-4A05-AB9D-7E14032309C3/addresses/7170A4DD-2441-4B1B-A8D3-437D75C4CBC9/city" + ] + // These will all exist + propertyErrors.forEach(x => expect(serverValidationManager.getPropertyError(x)).toBeDefined()); + // These field errors also exist + expect(serverValidationManager.getPropertyError(propertyErrors[2], null, "country")).toBeDefined(); + expect(serverValidationManager.getPropertyError(propertyErrors[2], null, "capital")).toBeDefined(); + expect(serverValidationManager.getPropertyError(propertyErrors[3], null, "country")).toBeDefined(); + expect(serverValidationManager.getPropertyError(propertyErrors[3], null, "capital")).toBeDefined(); }); }); diff --git a/src/Umbraco.Web/PropertyEditors/Validation/ComplexEditorElementTypeValidationResult.cs b/src/Umbraco.Web/PropertyEditors/Validation/ComplexEditorElementTypeValidationResult.cs index a7d69b1a40..132818ce59 100644 --- a/src/Umbraco.Web/PropertyEditors/Validation/ComplexEditorElementTypeValidationResult.cs +++ b/src/Umbraco.Web/PropertyEditors/Validation/ComplexEditorElementTypeValidationResult.cs @@ -17,6 +17,8 @@ namespace Umbraco.Web.PropertyEditors.Validation } public IList ValidationResults { get; } = new List(); + + // TODO: We don't use this anywhere, though it's nice for debugging public string ElementTypeAlias { get; } public Guid BlockId { get; } } diff --git a/src/Umbraco.Web/PropertyEditors/Validation/ValidationResultConverter.cs b/src/Umbraco.Web/PropertyEditors/Validation/ValidationResultConverter.cs index 40fc5cc36c..890b9f5f6f 100644 --- a/src/Umbraco.Web/PropertyEditors/Validation/ValidationResultConverter.cs +++ b/src/Umbraco.Web/PropertyEditors/Validation/ValidationResultConverter.cs @@ -56,6 +56,8 @@ namespace Umbraco.Web.PropertyEditors.Validation var joElementType = new JObject { { "$id", elementTypeValidationResult.BlockId }, + + // TODO: We don't use this anywhere, though it's nice for debugging { "$elementTypeAlias", elementTypeValidationResult.ElementTypeAlias } }; From 00c83182e78066905720b8cddd7e1003a6dd127d Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 10 Jul 2020 12:22:22 +1000 Subject: [PATCH 321/377] get validation path prefixes matching in serverValidationManager --- .../services/servervalidationmgr.service.js | 72 +++++++++++-------- .../server-validation-manager.spec.js | 28 +++++++- 2 files changed, 69 insertions(+), 31 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/services/servervalidationmgr.service.js b/src/Umbraco.Web.UI.Client/src/common/services/servervalidationmgr.service.js index 3e7a3ab496..8801c3aac8 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/servervalidationmgr.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/servervalidationmgr.service.js @@ -10,18 +10,14 @@ */ function serverValidationManager($timeout) { - // TODO: It would be nicer to just SHA1 hash these 'keys' instead of having the keys be compound values - // It would be another lib dependency or we could embed this https://github.com/emn178/js-sha1 - // this would remove the need for the auto-generated 'id' values since the hash would be the actual id value. - - // The array of callback objects, each object 'key' is: + // The array of callback objects, each object is: // - propertyAlias (this is the property's 'path' if it's a nested error) // - culture // - fieldName // - segment - // The object also contains: // - callback (function) // - id (unique identifier, auto-generated, used internally for unsubscribing the callback) + // - matchPrefix (used for complex properties, default is false, if set to true the callback will fire for any item with this propertyAlias prefix) var callbacks = []; // The array of error message objects, each object 'key' is: @@ -53,9 +49,7 @@ function serverValidationManager($timeout) { * @function * * @description - * This method isn't used very often but can be used if all subscriptions need to be notified again. This can be - * handy if a view needs to be reloaded/rebuild like when switching variants in the content editor. This is also used - * when a new subscription occurs and there is already registered errors like dynamically created/shown editors. + * Notifies all subscriptions again. Called when there are changes to subscriptions or errors. */ function notify() { $timeout(function () { @@ -81,7 +75,7 @@ function serverValidationManager($timeout) { }); } - function getPropertyErrors(propertyAlias, culture, segment, fieldName) { + function getPropertyErrors(propertyAlias, culture, segment, fieldName, matchPrefixValidationPath) { if (!Utilities.isString(propertyAlias)) { throw "propertyAlias must be a string"; } @@ -98,7 +92,11 @@ function serverValidationManager($timeout) { //find all errors for this property return _.filter(items, function (item) { - return (item.propertyAlias === propertyAlias && item.culture === culture && item.segment === segment && (item.fieldName === fieldName || (fieldName === undefined || fieldName === ""))); + return ((matchPrefixValidationPath ? (item.propertyAlias === propertyAlias || propertyAlias.startsWith(item.propertyAlias + '/')) : item.propertyAlias === propertyAlias) + && item.culture === culture + && item.segment === segment + // ignore field matching if + && (matchPrefixValidationPath || (item.fieldName === fieldName || (fieldName === undefined || fieldName === "")))); }); } @@ -119,27 +117,27 @@ function serverValidationManager($timeout) { /** Call all registered callbacks indicating if the data they are subscribed to is valid or invalid */ function notifyCallbacks() { - for (var i = 0; i < callbacks.length; i++) { - var cb = callbacks[i]; + + callbacks.forEach(cb => { if (cb.propertyAlias === null && cb.fieldName !== null) { //its a field error callback - var fieldErrors = getFieldErrors(cb.fieldName); + const fieldErrors = getFieldErrors(cb.fieldName); const valid = fieldErrors.length === 0; executeCallback(fieldErrors, cb.callback, cb.culture, cb.segment, valid); } else if (cb.propertyAlias != null) { //its a property error - var propErrors = getPropertyErrors(cb.propertyAlias, cb.culture, cb.segment, cb.fieldName); + const propErrors = getPropertyErrors(cb.propertyAlias, cb.culture, cb.segment, cb.fieldName, cb.matchPrefix); const valid = propErrors.length === 0; executeCallback(propErrors, cb.callback, cb.culture, cb.segment, valid); } else { //its a variant error - var variantErrors = getVariantErrors(cb.culture, cb.segment); + const variantErrors = getVariantErrors(cb.culture, cb.segment); const valid = variantErrors.length === 0; executeCallback(variantErrors, cb.callback, cb.culture, cb.segment, valid); } - } + }); } /** @@ -208,9 +206,14 @@ function serverValidationManager($timeout) { segment = null; } - var found = _.filter(callbacks, function (item) { + var found = _.filter(callbacks, function (cb) { + //returns any callback that have been registered directly against the field and for only the property - return (item.propertyAlias === propertyAlias && item.culture === culture && item.segment === segment && (item.fieldName === fieldName || (item.fieldName === undefined || item.fieldName === ""))); + return ((cb.matchPrefix ? (cb.propertyAlias === propertyAlias || propertyAlias.startsWith(cb.propertyAlias + '/')) : cb.propertyAlias === propertyAlias) + && cb.culture === culture + && cb.segment === segment + // if the callback is configured to patch prefix then we ignore the field value + && (cb.matchPrefix || (cb.fieldName === fieldName || (cb.fieldName === undefined || cb.fieldName === "")))); }); return found; } @@ -341,20 +344,28 @@ function serverValidationManager($timeout) { } //find all errors for this item - var errorsForCallback = getPropertyErrors(propertyAlias, culture, segment, fieldName); - //we should now call all of the call backs registered for this error + var propertyErrors = getPropertyErrors(propertyAlias, culture, segment, fieldName); + var propertyPrefixErrors = getPropertyErrors(propertyAlias, culture, segment, fieldName, true); + + //now call all of the call backs registered for this error var cbs = getPropertyCallbacks(propertyAlias, culture, fieldName, segment); + //call each callback for this error - for (var cb in cbs) { - executeCallback(errorsForCallback, cbs[cb].callback, culture, segment, false); - } + cbs.forEach(cb => { + if (cb.matchPrefix) { + executeCallback(propertyPrefixErrors, cb.callback, culture, segment, false); + } + else { + executeCallback(propertyErrors, cb.callback, culture, segment, false); + } + }); //execute variant specific callbacks here too when a propery error is added var variantCbs = getVariantCallbacks(culture, segment); //call each callback for this error - for (var cb in variantCbs) { - executeCallback(errorsForCallback, variantCbs[cb].callback, culture, segment, false); - } + variantCbs.forEach(cb => { + executeCallback(propertyErrors, cb.callback, culture, segment, false); + }); } /** @@ -573,12 +584,12 @@ function serverValidationManager($timeout) { * field alias to listen for. * If propertyAlias is null, then this subscription is for a field property (not a user defined property). */ - subscribe: function (propertyAlias, culture, fieldName, callback, segment) { + subscribe: function (propertyAlias, culture, fieldName, callback, segment, matchValidationPathPrefix) { if (!callback) { return; } - console.log(`serverValidationMgr subscribed [${propertyAlias}] [${culture}] [${fieldName}] [${segment}]`); + console.log(`serverValidationMgr subscribe [${propertyAlias}] [${culture}] [${fieldName}] [${segment}]`); var id = String.CreateGuid(); @@ -610,7 +621,8 @@ function serverValidationManager($timeout) { segment: segment, fieldName: fieldName, callback: callback, - id: id + id: id, + matchPrefix: matchValidationPathPrefix }); } diff --git a/src/Umbraco.Web.UI.Client/test/unit/common/services/server-validation-manager.spec.js b/src/Umbraco.Web.UI.Client/test/unit/common/services/server-validation-manager.spec.js index 25831b91f3..27258c96a4 100644 --- a/src/Umbraco.Web.UI.Client/test/unit/common/services/server-validation-manager.spec.js +++ b/src/Umbraco.Web.UI.Client/test/unit/common/services/server-validation-manager.spec.js @@ -611,7 +611,33 @@ expect(numCalled).toEqual(2); }); - + + it('can subscribe to a property validation path prefix', function () { + var numCalled = 0; + + //arrange + serverValidationManager.subscribe("myProperty", null, null, function (isValid, propertyErrors, allErrors) { + numCalled++; + // since this is matching on prefix, there should be as many property errors as numCalled + expect(propertyErrors.length).toEqual(numCalled); + }, null, true); + + //act + // will match: + serverValidationManager.addPropertyError("myProperty", null, null, "property error", null); + serverValidationManager.addPropertyError("myProperty", null, "value1", "value error", null); + serverValidationManager.addPropertyError("myProperty/34E3A26C-103D-4A05-AB9D-7E14032309C3/addresses/FBEAEE8F-4BC9-43EE-8B81-FCA8978850F1/city", null, null, "property error", null); + serverValidationManager.addPropertyError("myProperty/34E3A26C-103D-4A05-AB9D-7E14032309C3/addresses/FBEAEE8F-4BC9-43EE-8B81-FCA8978850F1/city", null, "value1", "value error", null); + // won't match: + serverValidationManager.addPropertyError("myProperty", "en-US", null, "property error", null); + serverValidationManager.addPropertyError("myProperty/34E3A26C-103D-4A05-AB9D-7E14032309C3/addresses/FBEAEE8F-4BC9-43EE-8B81-FCA8978850F1/city", "en-US", null, "property error", null); + serverValidationManager.addPropertyError("otherProperty", null, null, "property error", null); + serverValidationManager.addPropertyError("otherProperty", null, "value1", "value error", null); + + //assert + expect(numCalled).toEqual(4); + }); + // TODO: Finish testing the rest! }); From bbbb7a052ba2caeb4c8fc22b03af0d7deb09a620 Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 10 Jul 2020 18:06:11 +1000 Subject: [PATCH 322/377] Getting close, adds tests, fixese the key being set on the block, gets errors clearing, now need to make the UI behave. --- .../property/umbproperty.directive.js | 27 +- .../validation/valpropertymsg.directive.js | 260 +++++++----------- .../validation/valserver.directive.js | 1 + .../blockeditormodelobject.service.js | 4 +- .../services/servervalidationmgr.service.js | 103 ++++--- .../src/common/services/udi.service.js | 15 + .../umb-element-editor-content.component.html | 2 +- .../nestedcontent/nestedcontent.editor.html | 2 +- .../server-validation-manager.spec.js | 54 +++- 9 files changed, 222 insertions(+), 246 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/property/umbproperty.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/property/umbproperty.directive.js index a67f51bfed..2b2f36dd7d 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/property/umbproperty.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/property/umbproperty.directive.js @@ -19,7 +19,7 @@ }, bindings: { property: "=", - elementUdi: "@", + elementKey: "@", // optional, if set this will be used for the property alias validation path (hack required because NC changes the actual property.alias :/) propertyAlias: "@", showInherit: "<", @@ -44,20 +44,14 @@ vm.propertyActions = actions; }; - // returns the unique Id for the property to be used as the validation key for server side validation logic + // returns the validation path for the property to be used as the validation key for server side validation logic vm.getValidationPath = function () { - // the elementUdi will be empty when this is not a nested property + var parentValidationPath = vm.parentUmbProperty ? vm.parentUmbProperty.getValidationPath() : null; var propAlias = vm.propertyAlias ? vm.propertyAlias : vm.property.alias; - vm.elementUdi = ensureUdi(vm.elementUdi); - return serverValidationManager.createPropertyValidationKey(propAlias, vm.elementUdi); - } - - vm.getParentValidationPath = function () { - if (!vm.parentUmbProperty) { - return null; - } - return vm.parentUmbProperty.getValidationPath(); + // the elementKey will be empty when this is not a nested property + var valPath = vm.elementKey ? vm.elementKey + "/" + propAlias : propAlias; + return serverValidationManager.createPropertyValidationKey(valPath, parentValidationPath); } function onInit() { @@ -70,8 +64,6 @@ }); } - vm.elementUdi = ensureUdi(vm.elementUdi); - if (!vm.parentUmbProperty) { // not found, then fallback to searching the scope chain, this may be needed when DOM inheritance isn't maintained but scope // inheritance is (i.e.infinite editing) @@ -80,13 +72,6 @@ } } - // if only a guid is passed in, we'll ensure a correct udi structure - function ensureUdi(udi) { - if (udi && !udi.startsWith("umb://")) { - udi = udiService.build("element", udi); - } - return udi; - } } })(); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/validation/valpropertymsg.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/validation/valpropertymsg.directive.js index 353ff2a137..fe3369f731 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/validation/valpropertymsg.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/validation/valpropertymsg.directive.js @@ -24,6 +24,7 @@ function valPropertyMsg(serverValidationManager, localizationService, angularHel var unsubscribe = []; var watcher = null; var hasError = false; + var hasServerError = false; // tracks if this validator has an explicit server validator key attached to it //create properties on our custom scope so we can use it in our template scope.errorMsg = ""; @@ -49,9 +50,6 @@ function valPropertyMsg(serverValidationManager, localizationService, angularHel var isMandatory = currentProperty.validation ? currentProperty.validation.mandatory : undefined; var labels = {}; - localizationService.localize("errors_propertyHasErrors").then(function (data) { - labels.propertyHasErrors = data; - }); if (umbVariantCtrl) { //if we are inside of an umbVariantContent directive @@ -69,7 +67,6 @@ function valPropertyMsg(serverValidationManager, localizationService, angularHel // if we have reached this part, and there is no culture, then lets fallback to invariant. To get the validation feedback for invariant language. currentCulture = currentCulture || "invariant"; - // Gets the error message to display function getErrorMsg() { //this can be null if no property was assigned @@ -96,11 +93,6 @@ function valPropertyMsg(serverValidationManager, localizationService, angularHel // the form. Of course normal client-side validators will continue to execute. function startWatch() { - // TODO: Can we watch on something other than the value?? This doesn't work for complex editors especially once that have a - // viewmodel/model setup the value that this is watching doesn't actually get updated by a sub-editor in all cases. - // we can probably watch the formCtrl view value? But then we also don't want this to watch complex values that have sub editors anyways - // since that might end up clearing the whole chain of valPropertyMsg when a sub value is changed (in some cases, not with the block editor). - //if there's not already a watch if (!watcher) { @@ -125,6 +117,25 @@ function valPropertyMsg(serverValidationManager, localizationService, angularHel if (errCount === 0 || (errCount === 1 && Utilities.isArray(formCtrl.$error.valPropertyMsg)) || (formCtrl.$invalid && Utilities.isArray(formCtrl.$error.valServer))) { + + console.log("RESETTING ERROR FROM WATCH " + propertyValidationKey + " - " + hasServerError); + + if (hasServerError) { + + // check if we can clear it based on child server errors, if we are the only explicit one remaining we can clear ourselves + var nestedErrs = serverValidationManager.getPropertyErrorsByValidationPath( + propertyValidationKey, + currentCulture, + "", + currentSegment, + true); + if (nestedErrs.length === 1 && nestedErrs[0].propertyAlias === propertyValidationKey) { + + console.log("CLEARING SERVER VAL FROM WATCH " + propertyValidationKey); + serverValidationManager.removePropertyError(propertyValidationKey, currentCulture, "", currentSegment); + } + } + resetError(); } else if (showValidation && scope.errorMsg === "") { @@ -144,104 +155,18 @@ function valPropertyMsg(serverValidationManager, localizationService, angularHel } function resetError() { - var hadError = hasError; hasError = false; formCtrl.$setValidity('valPropertyMsg', true, formCtrl); scope.errorMsg = ""; stopWatch(); - - // if we had an error, then check on the current valFormManager to see if it's - // now valid, if it is it means that the containing form (i.e. the form rendering) - // properties for an element/content type) is now valid which means we can clear - // the parent's valPropertyMsg if there is one. This will only occur with complex editors - // where we have nested umb-property components. - if (hadError) { - scope.$evalAsync(function () { - - //// TODO: This does not work :( :( :( - //// We cannot clear a val-property-msg because another nested child might have server validation errors too. - //// I 'think' we might be able to set the UI validation of this val-property-msg based on the child validators as well - //// as the server validator so it can 'just' unset itself if all child validators are cleared. Can it be done? - - //// Here we loop over the umbProperty hierarchy to see if we should clear the val-property-msg server validation key. - //// we will clear the key if the parent for is valid, or if the parent form is only invalid due to a single val-property-msg error. - //var currUmbProperty = umbPropCtrl; - //var parentValidationKey = currUmbProperty.getParentValidationPath(); - //while (currUmbProperty && parentValidationKey) { - - // if (!currUmbProperty.parentForm.$invalid || (_.keys(currUmbProperty.parentForm.$error).length === 1 && currUmbProperty.parentForm.$error.valPropertyMsg)) { - // serverValidationManager.removePropertyError(parentValidationKey, currentCulture, "", currentSegment); - - // // re-assign and loop - // if (currUmbProperty !== umbPropCtrl.parentUmbProperty) { - // currUmbProperty = umbPropCtrl.parentUmbProperty; - // parentValidationKey = currUmbProperty ? currUmbProperty.getParentValidationPath() : null; - // } - // else { - // break; - // } - // } - // else { - // break; - // } - //} - - //// we need to navigate the parentForm here, unfortunately there's no real alternative unless we create our own directive - //// of some sort but that would also get messy. This works though since in this case we're always going to be in the property - //// form and the parent form about this will contain any invalid flags for all the other sibling properties. So when that is - //// no longer invalid, we can check if we have a parent validation key (meaning we'd be nested inside of umb-property) and - //// we can clear that server error. - //// TODO: If there is another server error for this property though this might clear it inadvertently, at this time I'm unsure how to deal with that. - //var parentValidationKey = umbPropCtrl.getParentValidationPath(); - //if (parentValidationKey) { - // // TODO: Instead of using the parent form, can we 'just' use umbProperty again which itself can check if it's - // // parent form is valid? then below we can call in a loop each parent umb property check if it has a parent validation - // // path and check if it's form is valid, this will recursively perform this logic up the chain. - // var parentForm = formCtrl.$$parentForm; - // if (parentForm && !parentForm.$invalid) { - // // TODO: Though this works for one level, if you have errors at level 1 and 2, clear errors at level when - // // and then level 2, then only the val-property-msg is cleared at level 1 and not also at level 0. - // // So we still need to recurse up the chain to deal with this - // serverValidationManager.removePropertyError(parentValidationKey, currentCulture, "", currentSegment); - // } - //} - - }); - - - } } - //function checkFormValidation(f) { - - // if (!angularHelper.isForm(f)) { - // throw "The object is not an angular Form"; - // } - - // // if there's no value, the controls can be reset, which clears the error state on formCtrl - // for (let control of formCtrl.$getControls()) { - // control.$setValidity(); - // } - - //} - function checkValidationStatus() { if (formCtrl.$invalid) { //first we need to check if the valPropertyMsg validity is invalid if (formCtrl.$error.valPropertyMsg && formCtrl.$error.valPropertyMsg.length > 0) { //since we already have an error we'll just return since this means we've already set the - // hasError and errorMsg properties which occurs below in the serverValidationManager.subscribe - - // TODO: This does not work! we cannot just clear if there are no errors in child controls because child controls - // won't even have been loaded yet so this will just instantly clear them - //// At this stage we might have an error assigned because it was assigned based on validation hierarchy from the server, - //// BUT one or ALL of the child server (and client) validations may be cleared at this point. We will know if we have an - //// explicitly assigned error based on the error message assigned, if it is a non-explicit error (meaning that the error - //// was assigned because it has child errors) then the message will just be: labels.propertyHasErrors - //if (scope.errorMsg === labels.propertyHasErrors && _.every(formCtrl.$getControls(), c => c.$valid)) { - // resetError(); - //} - + //hasError and errorMsg properties which occurs below in the serverValidationManager.subscribe return; } //if there are any errors in the current property form that are not valPropertyMsg @@ -275,73 +200,92 @@ function valPropertyMsg(serverValidationManager, localizationService, angularHel } } - //if there's any remaining errors in the server validation service then we should show them. - var showValidation = serverValidationManager.items.length > 0; - if (!showValidation) { - //We can either get the form submitted status by the parent directive valFormManager (if we add a property to it) - //or we can just check upwards in the DOM for the css class (easier for now). - //The initial hidden state can't always be hidden because when we switch variants in the content editor we cannot - //reset the status. - showValidation = element.closest(".show-validation").length > 0; - } + function onInit() { + localizationService.localize("errors_propertyHasErrors").then(function (data) { - //listen for form validation changes. - //The alternative is to add a watch to formCtrl.$invalid but that would lead to many more watches then - // subscribing to this single watch. - valFormManager.onValidationStatusChanged(function (evt, args) { - checkValidationStatus(); - }); + labels.propertyHasErrors = data; - //listen for the forms saving event - unsubscribe.push(scope.$on("formSubmitting", function (ev, args) { - showValidation = true; - if (hasError && scope.errorMsg === "") { - scope.errorMsg = getErrorMsg(); - startWatch(); - } - else if (!hasError) { - resetError(); - } - })); - - //listen for the forms saved event - unsubscribe.push(scope.$on("formSubmitted", function (ev, args) { - showValidation = false; - resetError(); - })); - - //listen for server validation changes - // NOTE: we pass in "" in order to listen for all validation changes to the content property, not for - // validation changes to fields in the property this is because some server side validators may not - // return the field name for which the error belongs too, just the property for which it belongs. - // It's important to note that we need to subscribe to server validation changes here because we always must - // indicate that a content property is invalid at the property level since developers may not actually implement - // the correct field validation in their property editors. - - if (scope.currentProperty) { //this can be null if no property was assigned - - function serverValidationManagerCallback(isValid, propertyErrors, allErrors) { - hasError = !isValid; - if (hasError) { - //set the error message to the server message - scope.errorMsg = propertyErrors[0].errorMsg ? propertyErrors[0].errorMsg : labels.propertyHasErrors; - //flag that the current validator is invalid - formCtrl.$setValidity('valPropertyMsg', false, formCtrl); - startWatch(); + //if there's any remaining errors in the server validation service then we should show them. + var showValidation = serverValidationManager.items.length > 0; + if (!showValidation) { + //We can either get the form submitted status by the parent directive valFormManager (if we add a property to it) + //or we can just check upwards in the DOM for the css class (easier for now). + //The initial hidden state can't always be hidden because when we switch variants in the content editor we cannot + //reset the status. + showValidation = element.closest(".show-validation").length > 0; } - else { + + //listen for form validation changes. + //The alternative is to add a watch to formCtrl.$invalid but that would lead to many more watches then + // subscribing to this single watch. + valFormManager.onValidationStatusChanged(function (evt, args) { + checkValidationStatus(); + }); + + //listen for the forms saving event + unsubscribe.push(scope.$on("formSubmitting", function (ev, args) { + showValidation = true; + if (hasError && scope.errorMsg === "") { + scope.errorMsg = getErrorMsg(); + startWatch(); + } + else if (!hasError) { + resetError(); + } + })); + + //listen for the forms saved event + unsubscribe.push(scope.$on("formSubmitted", function (ev, args) { + showValidation = false; resetError(); - } - } + })); - unsubscribe.push(serverValidationManager.subscribe( - propertyValidationKey, - currentCulture, - "", - serverValidationManagerCallback, - currentSegment - )); + if (scope.currentProperty) { //this can be null if no property was assigned + + //listen for server validation changes + // NOTE: we pass in "" in order to listen for all validation changes to the content property, not for + // validation changes to fields in the property this is because some server side validators may not + // return the field name for which the error belongs too, just the property for which it belongs. + // It's important to note that we need to subscribe to server validation changes here because we always must + // indicate that a content property is invalid at the property level since developers may not actually implement + // the correct field validation in their property editors. + + function serverValidationManagerCallback(isValid, propertyErrors, allErrors) { + var hadError = hasError; + hasError = !isValid; + if (hasError) { + + // check if one of the errors is explicitly assigned to our validation key + hasServerError = _.some(propertyErrors, x => x.propertyAlias === propertyValidationKey); + if (hadError && hasServerError && propertyErrors.length === 1) { + // we're the only error remaining in the server validation + console.log("ONLY ERROR REMAINING " + propertyValidationKey); + } + + //set the error message to the server message + scope.errorMsg = propertyErrors[0].errorMsg ? propertyErrors[0].errorMsg : labels.propertyHasErrors; + //flag that the current validator is invalid + console.log("valPropertyMsg invalid - " + propertyValidationKey); + formCtrl.$setValidity('valPropertyMsg', false, formCtrl); + startWatch(); + } + else { + resetError(); + } + } + + unsubscribe.push(serverValidationManager.subscribe( + propertyValidationKey, + currentCulture, + "", + serverValidationManagerCallback, + currentSegment, + true // match property validation path prefix + )); + } + + }); } //when the scope is disposed we need to unsubscribe @@ -351,6 +295,8 @@ function valPropertyMsg(serverValidationManager, localizationService, angularHel unsubscribe[u](); } }); + + onInit(); } diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/validation/valserver.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/validation/valserver.directive.js index 2846a9d196..84542bf184 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/validation/valserver.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/validation/valserver.directive.js @@ -82,6 +82,7 @@ function valServer(serverValidationManager) { //clear the server validation entry serverValidationManager.removePropertyError(propertyValidationPath, currentCulture, fieldName, currentSegment); + stopWatch(); } }, true); 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 ec232b4914..4920f2b911 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 @@ -375,7 +375,7 @@ * @name getBlockObject * @methodOf umbraco.services.blockEditorModelObject * @description Retrieve a Block Object for the given layout entry. - * The Block Object offers the nesecary data to display and edit a block. + * The Block Object offers the necessary data to display and edit a block. * The Block Object setups live syncronization of content and settings models back to the data of your Property Editor model. * The returned object, named ´BlockObject´, contains several usefull models to make editing of this block happen. * The ´BlockObject´ contains the following properties: @@ -448,6 +448,8 @@ // make basics from scaffold blockObject.content = Utilities.copy(contentScaffold); blockObject.content.udi = udi; + // Change the content.key to the GUID part of the udi, else it's just random which we don't want, it should be consistent + blockObject.content.key = udiService.getKey(udi); mapToElementModel(blockObject.content, dataModel); diff --git a/src/Umbraco.Web.UI.Client/src/common/services/servervalidationmgr.service.js b/src/Umbraco.Web.UI.Client/src/common/services/servervalidationmgr.service.js index 8801c3aac8..8b900bc6ea 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/servervalidationmgr.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/servervalidationmgr.service.js @@ -52,6 +52,9 @@ function serverValidationManager($timeout) { * Notifies all subscriptions again. Called when there are changes to subscriptions or errors. */ function notify() { + + console.log("NOTIFY!"); + $timeout(function () { console.log(`VAL-ERROR-COUNT: ${items.length}`); @@ -92,7 +95,8 @@ function serverValidationManager($timeout) { //find all errors for this property return _.filter(items, function (item) { - return ((matchPrefixValidationPath ? (item.propertyAlias === propertyAlias || propertyAlias.startsWith(item.propertyAlias + '/')) : item.propertyAlias === propertyAlias) + return ((matchPrefixValidationPath ? (item.propertyAlias === propertyAlias || (item.propertyAlias && item.propertyAlias.startsWith(propertyAlias + '/'))) : item.propertyAlias === propertyAlias) + //return ((matchPrefixValidationPath ? (item.propertyAlias === propertyAlias || propertyAlias.startsWith(item.propertyAlias + '/')) : item.propertyAlias === propertyAlias) && item.culture === culture && item.segment === segment // ignore field matching if @@ -115,29 +119,36 @@ function serverValidationManager($timeout) { }); } + function notifyCallback(cb) { + if (cb.propertyAlias === null && cb.fieldName !== null) { + //its a field error callback + const fieldErrors = getFieldErrors(cb.fieldName); + const valid = fieldErrors.length === 0; + executeCallback(fieldErrors, cb.callback, cb.culture, cb.segment, valid); + } + else if (cb.propertyAlias != null) { + //its a property error + const propErrors = getPropertyErrors(cb.propertyAlias, cb.culture, cb.segment, cb.fieldName, cb.matchPrefix); + const valid = propErrors.length === 0; + executeCallback(propErrors, cb.callback, cb.culture, cb.segment, valid); + } + else { + //its a variant error + const variantErrors = getVariantErrors(cb.culture, cb.segment); + const valid = variantErrors.length === 0; + executeCallback(variantErrors, cb.callback, cb.culture, cb.segment, valid); + } + } + /** Call all registered callbacks indicating if the data they are subscribed to is valid or invalid */ function notifyCallbacks() { - callbacks.forEach(cb => { - if (cb.propertyAlias === null && cb.fieldName !== null) { - //its a field error callback - const fieldErrors = getFieldErrors(cb.fieldName); - const valid = fieldErrors.length === 0; - executeCallback(fieldErrors, cb.callback, cb.culture, cb.segment, valid); - } - else if (cb.propertyAlias != null) { - //its a property error - const propErrors = getPropertyErrors(cb.propertyAlias, cb.culture, cb.segment, cb.fieldName, cb.matchPrefix); - const valid = propErrors.length === 0; - executeCallback(propErrors, cb.callback, cb.culture, cb.segment, valid); - } - else { - //its a variant error - const variantErrors = getVariantErrors(cb.culture, cb.segment); - const valid = variantErrors.length === 0; - executeCallback(variantErrors, cb.callback, cb.culture, cb.segment, valid); - } - }); + // nothing to call + if (items.length === 0) { + return; + } + + callbacks.forEach(cb => notifyCallback(cb)); } /** @@ -343,29 +354,7 @@ function serverValidationManager($timeout) { }); } - //find all errors for this item - var propertyErrors = getPropertyErrors(propertyAlias, culture, segment, fieldName); - var propertyPrefixErrors = getPropertyErrors(propertyAlias, culture, segment, fieldName, true); - - //now call all of the call backs registered for this error - var cbs = getPropertyCallbacks(propertyAlias, culture, fieldName, segment); - - //call each callback for this error - cbs.forEach(cb => { - if (cb.matchPrefix) { - executeCallback(propertyPrefixErrors, cb.callback, culture, segment, false); - } - else { - executeCallback(propertyErrors, cb.callback, culture, segment, false); - } - }); - - //execute variant specific callbacks here too when a propery error is added - var variantCbs = getVariantCallbacks(culture, segment); - //call each callback for this error - variantCbs.forEach(cb => { - executeCallback(propertyErrors, cb.callback, culture, segment, false); - }); + notifyCallbacks(); } /** @@ -602,30 +591,33 @@ function serverValidationManager($timeout) { segment = null; } - if (propertyAlias === null) { + let cb = null; - callbacks.push({ + if (propertyAlias === null) { + cb = { propertyAlias: null, culture: culture, segment: segment, fieldName: fieldName, callback: callback, id: id - }); + }; } else if (propertyAlias !== undefined) { - callbacks.push({ + cb = { propertyAlias: propertyAlias, - culture: culture, + culture: culture, segment: segment, fieldName: fieldName, callback: callback, id: id, matchPrefix: matchValidationPathPrefix - }); + }; } + callbacks.push(cb); + function unsubscribeId() { //remove all callbacks for the content field callbacks = _.reject(callbacks, function (item) { @@ -633,9 +625,8 @@ function serverValidationManager($timeout) { }); } - // Now notify the registrations for this callback if we've previously been notified and we're not cleared. - // This will happen for dynamically shown editors, like complex editors that load in sub element types. - notify(); + // Notify the new callback + notifyCallback(cb); //return a function to unsubscribe this subscription by uniqueId return unsubscribeId; @@ -733,7 +724,7 @@ function serverValidationManager($timeout) { if (items.length !== count) { // removal was successful, re-notify all subscribers - notify(); + notifyCallbacks(); } }, @@ -757,6 +748,10 @@ function serverValidationManager($timeout) { return undefined; }, + getPropertyErrorsByValidationPath: function (propertyAlias, culture, segment) { + return getPropertyErrors(propertyAlias, culture, segment, "", true); + }, + /** * @ngdoc function * @name getFieldError diff --git a/src/Umbraco.Web.UI.Client/src/common/services/udi.service.js b/src/Umbraco.Web.UI.Client/src/common/services/udi.service.js index 980d5ddc72..3d3db8d346 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/udi.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/udi.service.js @@ -27,6 +27,21 @@ build: function (entityType, guid) { return "umb://" + entityType + "/" + (guid.replace(/-/g, "")); + }, + + getKey: function (udi) { + if (!Utilities.isString(udi)) { + throw "udi is not a string"; + } + if (!udi.startsWith("umb://")) { + throw "udi does not start with umb://"; + } + var withoutScheme = udi.substr("umb://".length); + var withoutHost = withoutScheme.substr(withoutScheme.indexOf("/") + 1).trim(); + if (withoutHost.length !== 32) { + throw "udi is not 32 chars"; + } + return `${withoutHost.substr(0, 8)}-${withoutHost.substr(8, 4)}-${withoutHost.substr(12, 4)}-${withoutHost.substr(16, 4)}-${withoutHost.substr(20)}`; } } } diff --git a/src/Umbraco.Web.UI.Client/src/views/components/elementeditor/umb-element-editor-content.component.html b/src/Umbraco.Web.UI.Client/src/views/components/elementeditor/umb-element-editor-content.component.html index 0c6ccbd22f..fae639562f 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/elementeditor/umb-element-editor-content.component.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/elementeditor/umb-element-editor-content.component.html @@ -13,7 +13,7 @@ data-element="property-{{property.alias}}" ng-repeat="property in group.properties track by property.alias" property="property" - element-udi="{{vm.model.udi}}" + element-key="{{vm.model.key}}" show-inherit="vm.model.variants.length > 1 && !property.culture && !activeVariant.language.isDefault" inherits-from="defaultVariant.language.name"> diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.editor.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.editor.html index 968dc63265..125e920fe6 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.editor.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/nestedcontent/nestedcontent.editor.html @@ -3,7 +3,7 @@ diff --git a/src/Umbraco.Web.UI.Client/test/unit/common/services/server-validation-manager.spec.js b/src/Umbraco.Web.UI.Client/test/unit/common/services/server-validation-manager.spec.js index 27258c96a4..0627063be5 100644 --- a/src/Umbraco.Web.UI.Client/test/unit/common/services/server-validation-manager.spec.js +++ b/src/Umbraco.Web.UI.Client/test/unit/common/services/server-validation-manager.spec.js @@ -1,9 +1,11 @@ describe('serverValidationManager tests', function () { - var serverValidationManager; + var $rootScope, serverValidationManager, $timeout; beforeEach(module('umbraco.services')); beforeEach(inject(function ($injector) { + $rootScope = $injector.get('$rootScope'); + $timeout = $injector.get('$timeout'); serverValidationManager = $injector.get('serverValidationManager'); serverValidationManager.clear(); })); @@ -480,7 +482,9 @@ allErrors: allErrors }; }, null); + + //act serverValidationManager.addFieldError("Name", "Required"); serverValidationManager.addPropertyError("myProperty", null, "value1", "Some value 1", null); @@ -501,7 +505,9 @@ var cb2 = function () { }; serverValidationManager.subscribe(null, null, "Name", cb1, null); + serverValidationManager.subscribe(null, null, "Title", cb2, null); + //act serverValidationManager.addFieldError("Name", "Required"); @@ -537,6 +543,7 @@ }; }, null); + serverValidationManager.subscribe("myProperty", null, "", function (isValid, propertyErrors, allErrors) { numCalled++; args2 = { @@ -545,6 +552,9 @@ allErrors: allErrors }; }, null); + + + console.log("NOW ADDING ERRORS " + numCalled); //act serverValidationManager.addPropertyError("myProperty", null, "value1", "Some value 1", null); @@ -587,6 +597,7 @@ allErrors: allErrors }; }, null); + serverValidationManager.subscribe(null, "es-ES", null, function (isValid, propertyErrors, allErrors) { numCalled++; @@ -596,6 +607,7 @@ allErrors: allErrors }; }, null); + //act serverValidationManager.addPropertyError("myProperty", null, "value1", "Some value 1", null); @@ -604,28 +616,30 @@ serverValidationManager.addPropertyError("myProperty", "fr-FR", "", "Some value 3", null); //assert - expect(args1).not.toBeUndefined(); expect(args1.isValid).toBe(false); + expect(args2.isValid).toBe(true); // no errors registered for this callback - expect(args2).toBeUndefined(); - - expect(numCalled).toEqual(2); + expect(numCalled).toEqual(8); // both subscriptions will be called once per addPropertyError }); it('can subscribe to a property validation path prefix', function () { - var numCalled = 0; + var callbackA = []; + var callbackB = []; //arrange serverValidationManager.subscribe("myProperty", null, null, function (isValid, propertyErrors, allErrors) { - numCalled++; - // since this is matching on prefix, there should be as many property errors as numCalled - expect(propertyErrors.length).toEqual(numCalled); + callbackA.push(propertyErrors); + }, null, true); + + serverValidationManager.subscribe("myProperty/34E3A26C-103D-4A05-AB9D-7E14032309C3/addresses", null, null, function (isValid, propertyErrors, allErrors) { + callbackB.push(propertyErrors); }, null, true); //act - // will match: + // will match A: serverValidationManager.addPropertyError("myProperty", null, null, "property error", null); serverValidationManager.addPropertyError("myProperty", null, "value1", "value error", null); + // will match A + B serverValidationManager.addPropertyError("myProperty/34E3A26C-103D-4A05-AB9D-7E14032309C3/addresses/FBEAEE8F-4BC9-43EE-8B81-FCA8978850F1/city", null, null, "property error", null); serverValidationManager.addPropertyError("myProperty/34E3A26C-103D-4A05-AB9D-7E14032309C3/addresses/FBEAEE8F-4BC9-43EE-8B81-FCA8978850F1/city", null, "value1", "value error", null); // won't match: @@ -635,7 +649,25 @@ serverValidationManager.addPropertyError("otherProperty", null, "value1", "value error", null); //assert - expect(numCalled).toEqual(4); + + // both will be called each time addPropertyError is called + expect(callbackA.length).toEqual(8); + expect(callbackB.length).toEqual(8); + expect(callbackA[callbackA.length - 1].length).toEqual(4); // 4 errors for A + expect(callbackB[callbackB.length - 1].length).toEqual(2); // 2 errors for B + + // clear the data and notify + callbackA = []; + callbackB = []; + + serverValidationManager.notify(); + $timeout.flush(); + + expect(callbackA.length).toEqual(1); + expect(callbackB.length).toEqual(1); + expect(callbackA[0].length).toEqual(4); // 4 errors for A + expect(callbackB[0].length).toEqual(2); // 2 errors for B + }); // TODO: Finish testing the rest! From 51c338f04b7ad2c2e8dbb1b8462fe6de058c317b Mon Sep 17 00:00:00 2001 From: Shannon Date: Tue, 14 Jul 2020 09:43:10 +1000 Subject: [PATCH 323/377] Fixes block list editor to correctly re-sync it's view model after persisting --- .../blockeditormodelobject.service.js | 45 ++++++++++++++----- .../umbBlockListPropertyEditor.component.js | 30 +++++++------ 2 files changed, 51 insertions(+), 24 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 4920f2b911..4cd2720216 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 @@ -256,13 +256,34 @@ this.isolatedScope.blockObjects = {}; this.__watchers.push(this.isolatedScope.$on("$destroy", this.destroy.bind(this))); - this.__watchers.push(propertyEditorScope.$on("postFormSubmitting", this.sync.bind(this))); }; BlockEditorModelObject.prototype = { + update: function (propertyModelValue, propertyEditorScope) { + // clear watchers + this.__watchers.forEach(w => { w(); }); + delete this.__watchers; + + // clear block objects + for (const key in this.isolatedScope.blockObjects) { + this.destroyBlockObject(this.isolatedScope.blockObjects[key]); + } + this.isolatedScope.blockObjects = {}; + + // update our values + this.value = propertyModelValue; + this.value.layout = this.value.layout || {}; + this.value.data = this.value.data || []; + + // re-create the watchers + this.__watchers = []; + this.__watchers.push(this.isolatedScope.$on("$destroy", this.destroy.bind(this))); + this.__watchers.push(propertyEditorScope.$on("postFormSubmitting", this.sync.bind(this))); + }, + /** * @ngdoc method * @name getBlockConfiguration @@ -279,8 +300,8 @@ * @ngdoc method * @name load * @methodOf umbraco.services.blockEditorModelObject - * @description Load the scaffolding models for the given configuration, these are needed to provide usefull models for each block. - * @param {Object} blockObject BlockObject to recive data values from. + * @description Load the scaffolding models for the given configuration, these are needed to provide useful models for each block. + * @param {Object} blockObject BlockObject to receive data values from. * @returns {Promise} A Promise object which resolves when all scaffold models are loaded. */ load: function() { @@ -295,7 +316,7 @@ } }); - // removing dublicates. + // removing duplicates. scaffoldKeys = scaffoldKeys.filter((value, index, self) => self.indexOf(value) === index); scaffoldKeys.forEach((contentTypeKey => { @@ -474,7 +495,7 @@ } } - blockObject.retriveValuesFrom = function(content, settings) { + blockObject.retrieveValuesFrom = function(content, settings) { if (this.content !== null) { mapElementValues(content, this.content); } @@ -484,7 +505,7 @@ } - blockObject.sync = function() { + blockObject.sync = function () { if (this.content !== null) { mapToPropertyModel(this.content, this.data); } @@ -501,13 +522,14 @@ addWatchers(blockObject, this.isolatedScope); addWatchers(blockObject, this.isolatedScope, true); - blockObject.destroy = function() { + blockObject.destroy = function () { // remove property value watchers: this.__watchers.forEach(w => { w(); }); delete this.__watchers; // help carbage collector: delete this.config; + delete this.layout; delete this.data; delete this.content; @@ -515,9 +537,12 @@ // remove model from isolatedScope. delete this.__scope.blockObjects["_" + this.key]; + // NOTE: It seems like we should call this.__scope.$destroy(); since that is the only way to remove a scope from it's parent, + // however that is not the case since __scope is actually this.isolatedScope which gets cleaned up when the outer scope is + // destroyed. If we do that here it breaks the scope chain and validation. delete this.__scope; - // removes this method, making it unposible to destroy again. + // removes this method, making it impossible to destroy again. delete this.destroy; // lets remove the key to make things blow up if this is still referenced: @@ -623,8 +648,6 @@ }, - - /** * @ngdoc method * @name sync @@ -638,6 +661,7 @@ }, // private + // TODO: Then this can just be a method in the outer scope _createDataEntry: function(elementTypeKey) { var content = { contentTypeKey: elementTypeKey, @@ -647,6 +671,7 @@ return content.udi; }, // private + // TODO: Then this can just be a method in the outer scope _getDataByUdi: function(udi) { return this.value.data.find(entry => entry.udi === udi) || null; }, 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 d433c63176..ce3d632f84 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 @@ -53,8 +53,8 @@ } vm.supportCopy = clipboardService.isSupported(); - vm.layout = [];// The layout object specific to this Block Editor, will be a direct reference from Property Model. - vm.availableBlockTypes = [];// Available block entries of this property editor. + vm.layout = []; // The layout object specific to this Block Editor, will be a direct reference from Property Model. + vm.availableBlockTypes = []; // Available block entries of this property editor. var labels = {}; vm.labels = labels; @@ -63,12 +63,12 @@ labels.content_createEmpty = data[1]; }); - - - - vm.$onInit = function() { + // set the onValueChanged callback, this will tell us if the block list model changed on the server + // once the data is submitted. If so we need to re-initialize + vm.model.onValueChanged = onServerValueChanged; + inlineEditing = vm.model.config.useInlineEditingAsDefault; liveEditing = vm.model.config.useLiveEditing; @@ -121,8 +121,12 @@ } }; - - + // Called when we save the value, the server may return an updated data and our value is re-synced + // we need to deal with that here so that our model values are all in sync so we basically re-initialize. + function onServerValueChanged(newVal, oldVal) { + modelObject.update(newVal, $scope); + onLoaded(); + } function setDirty() { if (vm.propertyForm) { @@ -137,7 +141,7 @@ // Append the blockObjects to our layout. vm.layout.forEach(entry => { - // $block must have the data property to be a valid BlockObject, if not its concidered as a destroyed blockObject. + // $block must have the data property to be a valid BlockObject, if not its considered as a destroyed blockObject. if (entry.$block === undefined || entry.$block === null || entry.$block.data === undefined) { var block = getBlockObject(entry); @@ -176,7 +180,7 @@ block.hideContentInOverlay = block.config.forceHideContentEditorInOverlay === true || inlineEditing === true; block.showSettings = block.config.settingsElementTypeKey != null; - block.showCopy = vm.supportCopy && block.config.contentTypeKey != null;// if we have content, otherwise it dosnt make sense to copy. + block.showCopy = vm.supportCopy && block.config.contentTypeKey != null;// if we have content, otherwise it doesn't make sense to copy. return block; } @@ -211,8 +215,6 @@ } - - function deleteBlock(block) { var layoutIndex = vm.layout.findIndex(entry => entry.udi === block.content.udi); @@ -276,7 +278,7 @@ if (liveEditing === false) { // transfer values when submitting in none-liveediting mode. - blockObject.retriveValuesFrom(blockEditorModel.content, blockEditorModel.settings); + blockObject.retrieveValuesFrom(blockEditorModel.content, blockEditorModel.settings); } blockObject.active = false; @@ -286,7 +288,7 @@ if (liveEditing === true) { // revert values when closing in liveediting mode. - blockObject.retriveValuesFrom(blockContentClone, blockSettingsClone); + blockObject.retrieveValuesFrom(blockContentClone, blockSettingsClone); } if (wasNotActiveBefore === true) { From 212dc7f0faab5f9148dd6ba06ccd0365aab924a1 Mon Sep 17 00:00:00 2001 From: Shannon Date: Tue, 14 Jul 2020 10:08:59 +1000 Subject: [PATCH 324/377] notes --- src/Umbraco.ModelsBuilder.Embedded/PureLiveModelFactory.cs | 4 +++- src/Umbraco.Tests.Benchmarks/CtorInvokeBenchmarks.cs | 2 ++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Umbraco.ModelsBuilder.Embedded/PureLiveModelFactory.cs b/src/Umbraco.ModelsBuilder.Embedded/PureLiveModelFactory.cs index 0e125759c6..912d0e3363 100644 --- a/src/Umbraco.ModelsBuilder.Embedded/PureLiveModelFactory.cs +++ b/src/Umbraco.ModelsBuilder.Embedded/PureLiveModelFactory.cs @@ -542,8 +542,10 @@ namespace Umbraco.ModelsBuilder.Embedded if (modelInfos.TryGetValue(typeName, out var modelInfo)) throw new InvalidOperationException($"Both types {type.FullName} and {modelInfo.ModelType.FullName} want to be a model type for content type with alias \"{typeName}\"."); - // fixme use Core's ReflectionUtilities.EmitCtor !! + // TODO: use Core's ReflectionUtilities.EmitCtor !! // Yes .. DynamicMethod is uber slow + // TODO: But perhaps https://docs.microsoft.com/en-us/dotnet/api/system.reflection.emit.constructorbuilder?view=netcore-3.1 is better still? + // See CtorInvokeBenchmarks var meth = new DynamicMethod(string.Empty, typeof(IPublishedElement), ctorArgTypes, type.Module, true); var gen = meth.GetILGenerator(); gen.Emit(OpCodes.Ldarg_0); diff --git a/src/Umbraco.Tests.Benchmarks/CtorInvokeBenchmarks.cs b/src/Umbraco.Tests.Benchmarks/CtorInvokeBenchmarks.cs index 8d15613791..37fe952851 100644 --- a/src/Umbraco.Tests.Benchmarks/CtorInvokeBenchmarks.cs +++ b/src/Umbraco.Tests.Benchmarks/CtorInvokeBenchmarks.cs @@ -16,6 +16,8 @@ namespace Umbraco.Tests.Benchmarks // - it's faster to get+invoke the ctor // - emitting the ctor is unless if invoked only 1 + // TODO: Check out https://docs.microsoft.com/en-us/dotnet/api/system.reflection.emit.constructorbuilder?view=netcore-3.1 ? + //[Config(typeof(Config))] [MemoryDiagnoser] public class CtorInvokeBenchmarks From 5296213babcd7d84f7ce506da6b3e14b5443074f Mon Sep 17 00:00:00 2001 From: Shannon Date: Tue, 14 Jul 2020 10:09:25 +1000 Subject: [PATCH 325/377] fixes udi service infinite loop --- src/Umbraco.Web.UI.Client/src/common/services/udi.service.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/services/udi.service.js b/src/Umbraco.Web.UI.Client/src/common/services/udi.service.js index 3d3db8d346..7b08c68e4e 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/udi.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/udi.service.js @@ -22,7 +22,7 @@ * @returns {string} The generated UDI */ create: function(entityType) { - return this.create(entityType, String.CreateGuid()); + return this.build(entityType, String.CreateGuid()); }, build: function (entityType, guid) { From f9fdbc41ab61fba65955f1af119ba050d0e704cc Mon Sep 17 00:00:00 2001 From: Shannon Date: Tue, 14 Jul 2020 13:42:35 +1000 Subject: [PATCH 326/377] removes notes from valPropertyMsg --- .../validation/valpropertymsg.directive.js | 99 ++++++++++--------- 1 file changed, 53 insertions(+), 46 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/validation/valpropertymsg.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/validation/valpropertymsg.directive.js index fe3369f731..9501a90948 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/validation/valpropertymsg.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/validation/valpropertymsg.directive.js @@ -23,8 +23,7 @@ function valPropertyMsg(serverValidationManager, localizationService, angularHel var unsubscribe = []; var watcher = null; - var hasError = false; - var hasServerError = false; // tracks if this validator has an explicit server validator key attached to it + var hasError = false; // tracks if there is a child error or an explicit error //create properties on our custom scope so we can use it in our template scope.errorMsg = ""; @@ -50,6 +49,7 @@ function valPropertyMsg(serverValidationManager, localizationService, angularHel var isMandatory = currentProperty.validation ? currentProperty.validation.mandatory : undefined; var labels = {}; + var showValidation = false; if (umbVariantCtrl) { //if we are inside of an umbVariantContent directive @@ -85,6 +85,50 @@ function valPropertyMsg(serverValidationManager, localizationService, angularHel return labels.propertyHasErrors; } + // return true if there is only a single error left on the property form of either valPropertyMsg or valServer + function shouldClearError() { + var errCount = 0; + + for (var e in formCtrl.$error) { + if (Utilities.isArray(formCtrl.$error[e])) { + errCount++; + } + } + + //we are explicitly checking for valServer errors here, since we shouldn't auto clear + // based on other errors. We'll also check if there's no other validation errors apart from valPropertyMsg, if valPropertyMsg + // is the only one, then we'll clear. + + if (errCount === 0 + || (errCount === 1 && hasExplicitError()) + || (formCtrl.$invalid && Utilities.isArray(formCtrl.$error.valServer))) { + + return true; + } + + return false; + } + + // returns true if there is an explicit valPropertyMsg validation error on the form + function hasExplicitError() { + return Utilities.isArray(formCtrl.$error.valPropertyMsg); + } + + // returns true if there is only a single server validation error for this property validation key in it's validation path + function isLastServerError() { + var nestedErrs = serverValidationManager.getPropertyErrorsByValidationPath( + propertyValidationKey, + currentCulture, + "", + currentSegment, + true); + if (nestedErrs.length === 1 && nestedErrs[0].propertyAlias === propertyValidationKey) { + + return true; + } + return false; + } + // We need to subscribe to any changes to our model (based on user input) // This is required because when we have a server error we actually invalidate // the form which means it cannot be resubmitted. @@ -102,38 +146,11 @@ function valPropertyMsg(serverValidationManager, localizationService, angularHel return; } - var errCount = 0; + if (shouldClearError()) { - for (var e in formCtrl.$error) { - if (Utilities.isArray(formCtrl.$error[e])) { - errCount++; - } - } - - //we are explicitly checking for valServer errors here, since we shouldn't auto clear - // based on other errors. We'll also check if there's no other validation errors apart from valPropertyMsg, if valPropertyMsg - // is the only one, then we'll clear. - - if (errCount === 0 - || (errCount === 1 && Utilities.isArray(formCtrl.$error.valPropertyMsg)) - || (formCtrl.$invalid && Utilities.isArray(formCtrl.$error.valServer))) { - - console.log("RESETTING ERROR FROM WATCH " + propertyValidationKey + " - " + hasServerError); - - if (hasServerError) { - - // check if we can clear it based on child server errors, if we are the only explicit one remaining we can clear ourselves - var nestedErrs = serverValidationManager.getPropertyErrorsByValidationPath( - propertyValidationKey, - currentCulture, - "", - currentSegment, - true); - if (nestedErrs.length === 1 && nestedErrs[0].propertyAlias === propertyValidationKey) { - - console.log("CLEARING SERVER VAL FROM WATCH " + propertyValidationKey); - serverValidationManager.removePropertyError(propertyValidationKey, currentCulture, "", currentSegment); - } + // check if we can clear it based on child server errors, if we are the only explicit one remaining we can clear ourselves + if (isLastServerError()) { + serverValidationManager.removePropertyError(propertyValidationKey, currentCulture, "", currentSegment); } resetError(); @@ -207,7 +224,7 @@ function valPropertyMsg(serverValidationManager, localizationService, angularHel labels.propertyHasErrors = data; //if there's any remaining errors in the server validation service then we should show them. - var showValidation = serverValidationManager.items.length > 0; + showValidation = serverValidationManager.items.length > 0; if (!showValidation) { //We can either get the form submitted status by the parent directive valFormManager (if we add a property to it) //or we can just check upwards in the DOM for the css class (easier for now). @@ -243,8 +260,8 @@ function valPropertyMsg(serverValidationManager, localizationService, angularHel if (scope.currentProperty) { //this can be null if no property was assigned - //listen for server validation changes - // NOTE: we pass in "" in order to listen for all validation changes to the content property, not for + // listen for server validation changes for property validation path prefix. + // We pass in "" in order to listen for all validation changes to the content property, not for // validation changes to fields in the property this is because some server side validators may not // return the field name for which the error belongs too, just the property for which it belongs. // It's important to note that we need to subscribe to server validation changes here because we always must @@ -252,21 +269,11 @@ function valPropertyMsg(serverValidationManager, localizationService, angularHel // the correct field validation in their property editors. function serverValidationManagerCallback(isValid, propertyErrors, allErrors) { - var hadError = hasError; hasError = !isValid; if (hasError) { - - // check if one of the errors is explicitly assigned to our validation key - hasServerError = _.some(propertyErrors, x => x.propertyAlias === propertyValidationKey); - if (hadError && hasServerError && propertyErrors.length === 1) { - // we're the only error remaining in the server validation - console.log("ONLY ERROR REMAINING " + propertyValidationKey); - } - //set the error message to the server message scope.errorMsg = propertyErrors[0].errorMsg ? propertyErrors[0].errorMsg : labels.propertyHasErrors; //flag that the current validator is invalid - console.log("valPropertyMsg invalid - " + propertyValidationKey); formCtrl.$setValidity('valPropertyMsg', false, formCtrl); startWatch(); } From b37a4d151b803ea853554bdaa9f7156ce7d26c12 Mon Sep 17 00:00:00 2001 From: Shannon Date: Tue, 14 Jul 2020 13:44:59 +1000 Subject: [PATCH 327/377] removes console.log outputs --- .../common/directives/validation/valserver.directive.js | 5 +---- .../src/common/services/servervalidationmgr.service.js | 8 -------- 2 files changed, 1 insertion(+), 12 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/validation/valserver.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/validation/valserver.directive.js index 84542bf184..6ad8ff1e9b 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/validation/valserver.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/validation/valserver.directive.js @@ -78,8 +78,7 @@ function valServer(serverValidationManager) { if (modelCtrl.$invalid) { modelCtrl.$setValidity('valServer', true); - console.log("valServer cleared (watch) " + propertyValidationPath); - + //clear the server validation entry serverValidationManager.removePropertyError(propertyValidationPath, currentCulture, fieldName, currentSegment); @@ -100,14 +99,12 @@ function valServer(serverValidationManager) { function serverValidationManagerCallback(isValid, propertyErrors, allErrors) { if (!isValid) { modelCtrl.$setValidity('valServer', false); - console.log("valServer error " + propertyValidationPath); //assign an error msg property to the current validator modelCtrl.errorMsg = propertyErrors[0].errorMsg; startWatch(); } else { modelCtrl.$setValidity('valServer', true); - console.log("valServer cleared " + propertyValidationPath); //reset the error message modelCtrl.errorMsg = ""; stopWatch(); diff --git a/src/Umbraco.Web.UI.Client/src/common/services/servervalidationmgr.service.js b/src/Umbraco.Web.UI.Client/src/common/services/servervalidationmgr.service.js index 8b900bc6ea..657c4e75fe 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/servervalidationmgr.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/servervalidationmgr.service.js @@ -53,16 +53,10 @@ function serverValidationManager($timeout) { */ function notify() { - console.log("NOTIFY!"); - $timeout(function () { - - console.log(`VAL-ERROR-COUNT: ${items.length}`); for (var i = 0; i < items.length; i++) { var item = items[i]; - console.log(`VAL-ERROR [${item.propertyAlias}] [${item.culture}] [${item.fieldName}] [${item.segment}]`) } - notifyCallbacks(); }); } @@ -578,8 +572,6 @@ function serverValidationManager($timeout) { return; } - console.log(`serverValidationMgr subscribe [${propertyAlias}] [${culture}] [${fieldName}] [${segment}]`); - var id = String.CreateGuid(); //normalize culture to "invariant" From 551a5a82ce5ef50ebbf0c290f8a848f48f7bf26a Mon Sep 17 00:00:00 2001 From: Shannon Date: Tue, 14 Jul 2020 15:23:09 +1000 Subject: [PATCH 328/377] Ensures we don't display the complex json as notification --- .../validation/valpropertymsg.directive.js | 2 +- .../services/contenteditinghelper.service.js | 24 ++++++++++++++++--- 2 files changed, 22 insertions(+), 4 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/validation/valpropertymsg.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/validation/valpropertymsg.directive.js index 9501a90948..a85a9c38fe 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/validation/valpropertymsg.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/validation/valpropertymsg.directive.js @@ -272,7 +272,7 @@ function valPropertyMsg(serverValidationManager, localizationService, angularHel hasError = !isValid; if (hasError) { //set the error message to the server message - scope.errorMsg = propertyErrors[0].errorMsg ? propertyErrors[0].errorMsg : labels.propertyHasErrors; + scope.errorMsg = propertyErrors.length > 0 ? labels.propertyHasErrors : propertyErrors[0].errorMsg || labels.propertyHasErrors; //flag that the current validator is invalid formCtrl.$setValidity('valPropertyMsg', false, formCtrl); startWatch(); diff --git a/src/Umbraco.Web.UI.Client/src/common/services/contenteditinghelper.service.js b/src/Umbraco.Web.UI.Client/src/common/services/contenteditinghelper.service.js index 94e04acf6b..c73b6e1848 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/contenteditinghelper.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/contenteditinghelper.service.js @@ -32,6 +32,26 @@ function contentEditingHelper(fileManager, $q, $location, $routeParams, editorSt return true; } + function showNotificationsForModelsState(ms) { + for (const [key, value] of Object.entries(ms)) { + + var errorMsg = value[0]; + // if the error message is json it's a complex editor validation response that we need to parse + if ((Utilities.isString(errorMsg) && errorMsg.startsWith("[")) || Utilities.isArray(errorMsg)) { + // flatten the json structure, create validation paths for each property and add each as a property error + var idsToErrors = serverValidationManager.parseComplexEditorError(errorMsg, ""); + idsToErrors.forEach(x => { + if (x.modelState) { + showNotificationsForModelsState(x.modelState); + } + }); + } + else if (value[0]) { + notificationsService.error("Validation", value[0]); + } + } + } + return { //TODO: We need to move some of this to formHelper for saving, too many editors use this method for saving when this entire @@ -616,9 +636,7 @@ function contentEditingHelper(fileManager, $q, $location, $routeParams, editorSt //add model state errors to notifications // TODO: Need to ignore complex messages if (args.showNotifications) { - for (var e in args.err.data.ModelState) { - notificationsService.error("Validation", args.err.data.ModelState[e][0]); - } + showNotificationsForModelsState(args.err.data.ModelState); } if (!this.redirectToCreatedContent(args.err.data.id, args.softRedirect) || args.softRedirect) { From 1795153fade280484a16a56294b904ea489f3610 Mon Sep 17 00:00:00 2001 From: Shannon Date: Tue, 14 Jul 2020 16:42:10 +1000 Subject: [PATCH 329/377] fixes exception handling in content editor --- .../components/content/edit.controller.js | 27 +++++++++---------- .../services/umbrequesthelper.service.js | 14 +++++----- 2 files changed, 21 insertions(+), 20 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/content/edit.controller.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/content/edit.controller.js index c07bb9bc83..7f6d1cc728 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/content/edit.controller.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/content/edit.controller.js @@ -4,7 +4,8 @@ function ContentEditController($rootScope, $scope, $routeParams, $q, $window, appState, contentResource, entityResource, navigationService, notificationsService, serverValidationManager, contentEditingHelper, localizationService, formHelper, umbRequestHelper, - editorState, $http, eventsService, overlayService, $location, localStorageService, treeService) { + editorState, $http, eventsService, overlayService, $location, localStorageService, treeService, + $exceptionHandler) { var evts = []; var infiniteMode = $scope.infiniteModel && $scope.infiniteModel.infiniteMode; @@ -620,8 +621,7 @@ model.submitButtonState = "error"; //re-map the dialog model since we've re-bound the properties dialog.variants = $scope.content.variants; - //don't reject, we've handled the error - return $q.when(err); + $exceptionHandler(err); }); }, close: function () { @@ -642,8 +642,9 @@ action: "sendToPublish" }).then(function () { $scope.page.buttonGroupState = "success"; - }, function () { + }, function (err) { $scope.page.buttonGroupState = "error"; + $exceptionHandler(err); });; } }; @@ -679,8 +680,7 @@ model.submitButtonState = "error"; //re-map the dialog model since we've re-bound the properties dialog.variants = $scope.content.variants; - //don't reject, we've handled the error - return $q.when(err); + $exceptionHandler(err); }); }, close: function () { @@ -703,8 +703,9 @@ action: "publish" }).then(function () { $scope.page.buttonGroupState = "success"; - }, function () { + }, function (err) { $scope.page.buttonGroupState = "error"; + $exceptionHandler(err); }); } }; @@ -742,8 +743,7 @@ model.submitButtonState = "error"; //re-map the dialog model since we've re-bound the properties dialog.variants = $scope.content.variants; - //don't reject, we've handled the error - return $q.when(err); + $exceptionHandler(err); }); }, close: function (oldModel) { @@ -766,8 +766,9 @@ action: "save" }).then(function () { $scope.page.saveButtonState = "success"; - }, function () { + }, function (err) { $scope.page.saveButtonState = "error"; + $exceptionHandler(err); }); } @@ -820,8 +821,7 @@ model.submitButtonState = "error"; //re-map the dialog model since we've re-bound the properties dialog.variants = Utilities.copy($scope.content.variants); - //don't reject, we've handled the error - return $q.when(err); + $exceptionHandler(err); }); }, @@ -880,8 +880,7 @@ model.submitButtonState = "error"; //re-map the dialog model since we've re-bound the properties dialog.variants = $scope.content.variants; - //don't reject, we've handled the error - return $q.when(err); + $exceptionHandler(err); }); }, diff --git a/src/Umbraco.Web.UI.Client/src/common/services/umbrequesthelper.service.js b/src/Umbraco.Web.UI.Client/src/common/services/umbrequesthelper.service.js index 4cbc5e567a..7b43b239ea 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/umbrequesthelper.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/umbrequesthelper.service.js @@ -165,11 +165,9 @@ function umbRequestHelper($http, $q, notificationsService, eventsService, formHe return; //sometimes oddly this happens, nothing we can do } - if (!response.status && response.message && response.stack) { - //this is a JS/angular error that we should deal with - return $q.reject({ - errorMsg: response.message - }); + if (!response.status) { + //this is a JS/angular error + return $q.reject(response); } //invoke the callback @@ -280,7 +278,11 @@ function umbRequestHelper($http, $q, notificationsService, eventsService, formHe //the data returned is the up-to-date data so the UI will refresh return $q.resolve(response.data); }, function (response) { - //failure callback + + if (!response.status) { + //this is a JS/angular error + return $q.reject(response); + } //when there's a 500 (unhandled) error show a YSOD overlay if debugging is enabled. if (response.status >= 500 && response.status < 600) { From 0b78b277e3ee896d6fc3aa6fe9c28bc47cedb198 Mon Sep 17 00:00:00 2001 From: Shannon Date: Tue, 14 Jul 2020 16:43:30 +1000 Subject: [PATCH 330/377] remove testing html --- .../views/common/infiniteeditors/blockeditor/blockeditor.html | 2 -- .../src/views/components/property/umb-property.html | 3 --- 2 files changed, 5 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/blockeditor/blockeditor.html b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/blockeditor/blockeditor.html index 01ce137259..de18f13d2c 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/blockeditor/blockeditor.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/blockeditor/blockeditor.html @@ -4,8 +4,6 @@ -
        {{vm.blockForm.$valid}}
        - -
        {{ vm.getValidationPath() }}
        - -
        From 74ab1c0262b4755c95045323c7400087345b87e2 Mon Sep 17 00:00:00 2001 From: Shannon Date: Tue, 14 Jul 2020 18:09:46 +1000 Subject: [PATCH 331/377] Updates serverValidationManager to have different matching options, fixes unit tests, now need to write tests for the Suffix match --- .../validation/valpropertymsg.directive.js | 2 +- .../validation/valserver.directive.js | 2 - .../services/servervalidationmgr.service.js | 57 ++++++++++++++----- .../server-validation-manager.spec.js | 46 +++++++++------ 4 files changed, 73 insertions(+), 34 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/validation/valpropertymsg.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/validation/valpropertymsg.directive.js index a85a9c38fe..a34c4aa0cc 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/validation/valpropertymsg.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/validation/valpropertymsg.directive.js @@ -288,7 +288,7 @@ function valPropertyMsg(serverValidationManager, localizationService, angularHel "", serverValidationManagerCallback, currentSegment, - true // match property validation path prefix + { matchPrefix: true } // match property validation path prefix )); } diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/validation/valserver.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/validation/valserver.directive.js index 6ad8ff1e9b..b9b9b98174 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/validation/valserver.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/validation/valserver.directive.js @@ -111,8 +111,6 @@ function valServer(serverValidationManager) { } } - - unsubscribe.push(serverValidationManager.subscribe( propertyValidationPath, currentCulture, diff --git a/src/Umbraco.Web.UI.Client/src/common/services/servervalidationmgr.service.js b/src/Umbraco.Web.UI.Client/src/common/services/servervalidationmgr.service.js index 657c4e75fe..378eb79dc7 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/servervalidationmgr.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/servervalidationmgr.service.js @@ -17,7 +17,7 @@ function serverValidationManager($timeout) { // - segment // - callback (function) // - id (unique identifier, auto-generated, used internally for unsubscribing the callback) - // - matchPrefix (used for complex properties, default is false, if set to true the callback will fire for any item with this propertyAlias prefix) + // - options (used for complex properties, can contain options.matchPrefix options.matchSuffix if either are set to true the callback will fire for any item with this propertyAlias prefix or suffix) var callbacks = []; // The array of error message objects, each object 'key' is: @@ -28,7 +28,12 @@ function serverValidationManager($timeout) { // The object also contains: // - errorMsg var items = []; - + + var defaultMatchOptions = { + matchPrefix: false, + matchSuffix: false + } + /** calls the callback specified with the errors specified, used internally */ function executeCallback(errorsForCallback, callback, culture, segment, isValid) { @@ -72,7 +77,7 @@ function serverValidationManager($timeout) { }); } - function getPropertyErrors(propertyAlias, culture, segment, fieldName, matchPrefixValidationPath) { + function getPropertyErrors(propertyAlias, culture, segment, fieldName, options) { if (!Utilities.isString(propertyAlias)) { throw "propertyAlias must be a string"; } @@ -87,14 +92,26 @@ function serverValidationManager($timeout) { segment = null; } + if (!options) { + options = defaultMatchOptions; + } + //find all errors for this property return _.filter(items, function (item) { - return ((matchPrefixValidationPath ? (item.propertyAlias === propertyAlias || (item.propertyAlias && item.propertyAlias.startsWith(propertyAlias + '/'))) : item.propertyAlias === propertyAlias) - //return ((matchPrefixValidationPath ? (item.propertyAlias === propertyAlias || propertyAlias.startsWith(item.propertyAlias + '/')) : item.propertyAlias === propertyAlias) + + var matchProp = options.matchPrefix + ? (item.propertyAlias === propertyAlias || (item.propertyAlias && item.propertyAlias.startsWith(propertyAlias + '/'))) + : options.matchSuffix + ? (item.propertyAlias === propertyAlias || (item.propertyAlias && item.propertyAlias.endsWith('/' + propertyAlias))) + : item.propertyAlias === propertyAlias; + + var ignoreField = options.matchPrefix || options.matchSuffix; + + return matchProp && item.culture === culture && item.segment === segment - // ignore field matching if - && (matchPrefixValidationPath || (item.fieldName === fieldName || (fieldName === undefined || fieldName === "")))); + // ignore field matching if match options are used + && (ignoreField || (item.fieldName === fieldName || (fieldName === undefined || fieldName === ""))); }); } @@ -122,7 +139,7 @@ function serverValidationManager($timeout) { } else if (cb.propertyAlias != null) { //its a property error - const propErrors = getPropertyErrors(cb.propertyAlias, cb.culture, cb.segment, cb.fieldName, cb.matchPrefix); + const propErrors = getPropertyErrors(cb.propertyAlias, cb.culture, cb.segment, cb.fieldName, cb.options); const valid = propErrors.length === 0; executeCallback(propErrors, cb.callback, cb.culture, cb.segment, valid); } @@ -213,12 +230,24 @@ function serverValidationManager($timeout) { var found = _.filter(callbacks, function (cb) { + if (!cb.options) { + cb.options = defaultMatchOptions; + } + + var matchProp = cb.options.matchPrefix + ? (cb.propertyAlias === propertyAlias || propertyAlias.startsWith(cb.propertyAlias + '/')) + : cb.options.matchSuffix + ? (cb.propertyAlias === propertyAlias || propertyAlias.endsWith(cb.propertyAlias + '/')) + : cb.propertyAlias === propertyAlias; + + var ignoreField = cb.options.matchPrefix || cb.options.matchSuffix; + //returns any callback that have been registered directly against the field and for only the property - return ((cb.matchPrefix ? (cb.propertyAlias === propertyAlias || propertyAlias.startsWith(cb.propertyAlias + '/')) : cb.propertyAlias === propertyAlias) + return matchProp && cb.culture === culture && cb.segment === segment // if the callback is configured to patch prefix then we ignore the field value - && (cb.matchPrefix || (cb.fieldName === fieldName || (cb.fieldName === undefined || cb.fieldName === "")))); + && (ignoreField || (cb.fieldName === fieldName || (cb.fieldName === undefined || cb.fieldName === ""))); }); return found; } @@ -567,7 +596,7 @@ function serverValidationManager($timeout) { * field alias to listen for. * If propertyAlias is null, then this subscription is for a field property (not a user defined property). */ - subscribe: function (propertyAlias, culture, fieldName, callback, segment, matchValidationPathPrefix) { + subscribe: function (propertyAlias, culture, fieldName, callback, segment, options) { if (!callback) { return; } @@ -604,7 +633,7 @@ function serverValidationManager($timeout) { fieldName: fieldName, callback: callback, id: id, - matchPrefix: matchValidationPathPrefix + options: options }; } @@ -740,8 +769,8 @@ function serverValidationManager($timeout) { return undefined; }, - getPropertyErrorsByValidationPath: function (propertyAlias, culture, segment) { - return getPropertyErrors(propertyAlias, culture, segment, "", true); + getPropertyErrorsByValidationPath: function (propertyAlias, culture, segment, options) { + return getPropertyErrors(propertyAlias, culture, segment, "", options); }, /** diff --git a/src/Umbraco.Web.UI.Client/test/unit/common/services/server-validation-manager.spec.js b/src/Umbraco.Web.UI.Client/test/unit/common/services/server-validation-manager.spec.js index 0627063be5..c5c6177444 100644 --- a/src/Umbraco.Web.UI.Client/test/unit/common/services/server-validation-manager.spec.js +++ b/src/Umbraco.Web.UI.Client/test/unit/common/services/server-validation-manager.spec.js @@ -533,6 +533,7 @@ var args1; var args2; var numCalled = 0; + var numCalledWithErrors = 0; //arrange serverValidationManager.subscribe("myProperty", null, "value1", function (isValid, propertyErrors, allErrors) { @@ -546,6 +547,9 @@ serverValidationManager.subscribe("myProperty", null, "", function (isValid, propertyErrors, allErrors) { numCalled++; + if (propertyErrors.length > 0) { + numCalledWithErrors++; + } args2 = { isValid: isValid, propertyErrors: propertyErrors, @@ -553,9 +557,6 @@ }; }, null); - - console.log("NOW ADDING ERRORS " + numCalled); - //act serverValidationManager.addPropertyError("myProperty", null, "value1", "Some value 1", null); serverValidationManager.addPropertyError("myProperty", null, "value2", "Some value 2", null); @@ -578,19 +579,23 @@ expect(args2.propertyErrors[0].errorMsg).toEqual("Some value 1"); expect(args2.propertyErrors[1].errorMsg).toEqual("Some value 2"); expect(args2.allErrors.length).toEqual(2); - //Even though only 2 errors are added, the callback is called 3 times because any call to addPropertyError will invoke the callback - // if the property has errors existing. - expect(numCalled).toEqual(3); + //3 errors are added but a call to subscribe also calls the callback + expect(numCalled).toEqual(4); + expect(numCalledWithErrors).toEqual(3); }); it('can subscribe to a culture error for both a property and its sub field', function () { var args1; var args2; var numCalled = 0; + var numCalledWithErrors = 0; //arrange serverValidationManager.subscribe(null, "en-US", null, function (isValid, propertyErrors, allErrors) { numCalled++; + if (propertyErrors.length > 0) { + numCalledWithErrors++; + } args1 = { isValid: isValid, propertyErrors: propertyErrors, @@ -601,25 +606,28 @@ serverValidationManager.subscribe(null, "es-ES", null, function (isValid, propertyErrors, allErrors) { numCalled++; + if (propertyErrors.length > 0) { + numCalledWithErrors++; + } args2 = { isValid: isValid, propertyErrors: propertyErrors, allErrors: allErrors }; }, null); - //act - serverValidationManager.addPropertyError("myProperty", null, "value1", "Some value 1", null); - serverValidationManager.addPropertyError("myProperty", "en-US", "value1", "Some value 1", null); - serverValidationManager.addPropertyError("myProperty", "en-US", "value2", "Some value 2", null); - serverValidationManager.addPropertyError("myProperty", "fr-FR", "", "Some value 3", null); + serverValidationManager.addPropertyError("myProperty", null, "value1", "Some value 1", null); // doesn't match + serverValidationManager.addPropertyError("myProperty", "en-US", "value1", "Some value 1", null); // matches callback 1 + serverValidationManager.addPropertyError("myProperty", "en-US", "value2", "Some value 2", null); // matches callback 1 + serverValidationManager.addPropertyError("myProperty", "fr-FR", "", "Some value 3", null); // doesn't match - but all callbacks still execute //assert expect(args1.isValid).toBe(false); expect(args2.isValid).toBe(true); // no errors registered for this callback - expect(numCalled).toEqual(8); // both subscriptions will be called once per addPropertyError + expect(numCalled).toEqual(10); // both subscriptions will be called once per addPropertyError and also called on subscribe + expect(numCalledWithErrors).toEqual(3); // the first subscription is called 3 times with errors because the 4th time we call addPropertyError all callbacks still execute }); it('can subscribe to a property validation path prefix', function () { @@ -628,12 +636,16 @@ //arrange serverValidationManager.subscribe("myProperty", null, null, function (isValid, propertyErrors, allErrors) { - callbackA.push(propertyErrors); - }, null, true); + if (propertyErrors.length > 0) { + callbackA.push(propertyErrors); + } + }, null, { matchPrefix: true }); serverValidationManager.subscribe("myProperty/34E3A26C-103D-4A05-AB9D-7E14032309C3/addresses", null, null, function (isValid, propertyErrors, allErrors) { - callbackB.push(propertyErrors); - }, null, true); + if (propertyErrors.length > 0) { + callbackB.push(propertyErrors); + } + }, null, { matchPrefix: true }); //act // will match A: @@ -652,7 +664,7 @@ // both will be called each time addPropertyError is called expect(callbackA.length).toEqual(8); - expect(callbackB.length).toEqual(8); + expect(callbackB.length).toEqual(6); // B - will only be called 6 times with errors because the first 2 calls to addPropertyError haven't added errors for B yet expect(callbackA[callbackA.length - 1].length).toEqual(4); // 4 errors for A expect(callbackB[callbackB.length - 1].length).toEqual(2); // 2 errors for B From f759ab4d36f646d2ecdb0f5c1873d4a02a64a9bb Mon Sep 17 00:00:00 2001 From: Shannon Date: Tue, 14 Jul 2020 09:43:10 +1000 Subject: [PATCH 332/377] Fixes block list editor to correctly re-sync it's view model after persisting --- .../blockeditormodelobject.service.js | 45 ++++++++++++++----- .../umbBlockListPropertyEditor.component.js | 30 +++++++------ 2 files changed, 51 insertions(+), 24 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 ec232b4914..7a80d452be 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 @@ -256,13 +256,34 @@ this.isolatedScope.blockObjects = {}; this.__watchers.push(this.isolatedScope.$on("$destroy", this.destroy.bind(this))); - this.__watchers.push(propertyEditorScope.$on("postFormSubmitting", this.sync.bind(this))); }; BlockEditorModelObject.prototype = { + update: function (propertyModelValue, propertyEditorScope) { + // clear watchers + this.__watchers.forEach(w => { w(); }); + delete this.__watchers; + + // clear block objects + for (const key in this.isolatedScope.blockObjects) { + this.destroyBlockObject(this.isolatedScope.blockObjects[key]); + } + this.isolatedScope.blockObjects = {}; + + // update our values + this.value = propertyModelValue; + this.value.layout = this.value.layout || {}; + this.value.data = this.value.data || []; + + // re-create the watchers + this.__watchers = []; + this.__watchers.push(this.isolatedScope.$on("$destroy", this.destroy.bind(this))); + this.__watchers.push(propertyEditorScope.$on("postFormSubmitting", this.sync.bind(this))); + }, + /** * @ngdoc method * @name getBlockConfiguration @@ -279,8 +300,8 @@ * @ngdoc method * @name load * @methodOf umbraco.services.blockEditorModelObject - * @description Load the scaffolding models for the given configuration, these are needed to provide usefull models for each block. - * @param {Object} blockObject BlockObject to recive data values from. + * @description Load the scaffolding models for the given configuration, these are needed to provide useful models for each block. + * @param {Object} blockObject BlockObject to receive data values from. * @returns {Promise} A Promise object which resolves when all scaffold models are loaded. */ load: function() { @@ -295,7 +316,7 @@ } }); - // removing dublicates. + // removing duplicates. scaffoldKeys = scaffoldKeys.filter((value, index, self) => self.indexOf(value) === index); scaffoldKeys.forEach((contentTypeKey => { @@ -472,7 +493,7 @@ } } - blockObject.retriveValuesFrom = function(content, settings) { + blockObject.retrieveValuesFrom = function(content, settings) { if (this.content !== null) { mapElementValues(content, this.content); } @@ -482,7 +503,7 @@ } - blockObject.sync = function() { + blockObject.sync = function () { if (this.content !== null) { mapToPropertyModel(this.content, this.data); } @@ -499,13 +520,14 @@ addWatchers(blockObject, this.isolatedScope); addWatchers(blockObject, this.isolatedScope, true); - blockObject.destroy = function() { + blockObject.destroy = function () { // remove property value watchers: this.__watchers.forEach(w => { w(); }); delete this.__watchers; // help carbage collector: delete this.config; + delete this.layout; delete this.data; delete this.content; @@ -513,9 +535,12 @@ // remove model from isolatedScope. delete this.__scope.blockObjects["_" + this.key]; + // NOTE: It seems like we should call this.__scope.$destroy(); since that is the only way to remove a scope from it's parent, + // however that is not the case since __scope is actually this.isolatedScope which gets cleaned up when the outer scope is + // destroyed. If we do that here it breaks the scope chain and validation. delete this.__scope; - // removes this method, making it unposible to destroy again. + // removes this method, making it impossible to destroy again. delete this.destroy; // lets remove the key to make things blow up if this is still referenced: @@ -621,8 +646,6 @@ }, - - /** * @ngdoc method * @name sync @@ -636,6 +659,7 @@ }, // private + // TODO: Then this can just be a method in the outer scope _createDataEntry: function(elementTypeKey) { var content = { contentTypeKey: elementTypeKey, @@ -645,6 +669,7 @@ return content.udi; }, // private + // TODO: Then this can just be a method in the outer scope _getDataByUdi: function(udi) { return this.value.data.find(entry => entry.udi === udi) || null; }, 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 02e53826b5..919715e074 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 @@ -53,8 +53,8 @@ } vm.supportCopy = clipboardService.isSupported(); - vm.layout = [];// The layout object specific to this Block Editor, will be a direct reference from Property Model. - vm.availableBlockTypes = [];// Available block entries of this property editor. + vm.layout = []; // The layout object specific to this Block Editor, will be a direct reference from Property Model. + vm.availableBlockTypes = []; // Available block entries of this property editor. var labels = {}; vm.labels = labels; @@ -63,12 +63,12 @@ labels.content_createEmpty = data[1]; }); - - - - vm.$onInit = function() { + // set the onValueChanged callback, this will tell us if the block list model changed on the server + // once the data is submitted. If so we need to re-initialize + vm.model.onValueChanged = onServerValueChanged; + inlineEditing = vm.model.config.useInlineEditingAsDefault; liveEditing = vm.model.config.useLiveEditing; @@ -121,8 +121,12 @@ } }; - - + // Called when we save the value, the server may return an updated data and our value is re-synced + // we need to deal with that here so that our model values are all in sync so we basically re-initialize. + function onServerValueChanged(newVal, oldVal) { + modelObject.update(newVal, $scope); + onLoaded(); + } function setDirty() { if (vm.propertyForm) { @@ -137,7 +141,7 @@ // Append the blockObjects to our layout. vm.layout.forEach(entry => { - // $block must have the data property to be a valid BlockObject, if not its concidered as a destroyed blockObject. + // $block must have the data property to be a valid BlockObject, if not its considered as a destroyed blockObject. if (entry.$block === undefined || entry.$block === null || entry.$block.data === undefined) { var block = getBlockObject(entry); @@ -176,7 +180,7 @@ block.hideContentInOverlay = block.config.forceHideContentEditorInOverlay === true || inlineEditing === true; block.showSettings = block.config.settingsElementTypeKey != null; - block.showCopy = vm.supportCopy && block.config.contentTypeKey != null;// if we have content, otherwise it dosnt make sense to copy. + block.showCopy = vm.supportCopy && block.config.contentTypeKey != null;// if we have content, otherwise it doesn't make sense to copy. return block; } @@ -211,8 +215,6 @@ } - - function deleteBlock(block) { var layoutIndex = vm.layout.findIndex(entry => entry.udi === block.content.udi); @@ -269,7 +271,7 @@ if (liveEditing === false) { // transfer values when submitting in none-liveediting mode. - blockObject.retriveValuesFrom(blockEditorModel.content, blockEditorModel.settings); + blockObject.retrieveValuesFrom(blockEditorModel.content, blockEditorModel.settings); } blockObject.active = false; @@ -279,7 +281,7 @@ if (liveEditing === true) { // revert values when closing in liveediting mode. - blockObject.retriveValuesFrom(blockContentClone, blockSettingsClone); + blockObject.retrieveValuesFrom(blockContentClone, blockSettingsClone); } if (wasNotActiveBefore === true) { From f4d8d0e28e9ed32172e3201afe62494a092ac6d3 Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 15 Jul 2020 15:08:17 +1000 Subject: [PATCH 333/377] adds ability to match suffix, prefix and contains for the serverValidationManager + tests --- .../services/servervalidationmgr.service.js | 45 +++++---- .../server-validation-manager.spec.js | 95 ++++++++++++++++++- 2 files changed, 119 insertions(+), 21 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/services/servervalidationmgr.service.js b/src/Umbraco.Web.UI.Client/src/common/services/servervalidationmgr.service.js index 378eb79dc7..a5392d16ee 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/servervalidationmgr.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/servervalidationmgr.service.js @@ -17,7 +17,7 @@ function serverValidationManager($timeout) { // - segment // - callback (function) // - id (unique identifier, auto-generated, used internally for unsubscribing the callback) - // - options (used for complex properties, can contain options.matchPrefix options.matchSuffix if either are set to true the callback will fire for any item with this propertyAlias prefix or suffix) + // - options (used for complex properties, can contain options.matchType which can be either "suffix" or "prefix" or "contains") var callbacks = []; // The array of error message objects, each object 'key' is: @@ -30,8 +30,7 @@ function serverValidationManager($timeout) { var items = []; var defaultMatchOptions = { - matchPrefix: false, - matchSuffix: false + matchType: null } /** calls the callback specified with the errors specified, used internally */ @@ -99,19 +98,25 @@ function serverValidationManager($timeout) { //find all errors for this property return _.filter(items, function (item) { - var matchProp = options.matchPrefix - ? (item.propertyAlias === propertyAlias || (item.propertyAlias && item.propertyAlias.startsWith(propertyAlias + '/'))) - : options.matchSuffix - ? (item.propertyAlias === propertyAlias || (item.propertyAlias && item.propertyAlias.endsWith('/' + propertyAlias))) - : item.propertyAlias === propertyAlias; + if (!item.propertyAlias) { + return false; + } - var ignoreField = options.matchPrefix || options.matchSuffix; + var matchProp = item.propertyAlias === propertyAlias + ? true + : options.matchType === "prefix" + ? item.propertyAlias.startsWith(propertyAlias + '/') + : options.matchType === "suffix" + ? item.propertyAlias.endsWith('/' + propertyAlias) + : options.matchType === "contains" + ? item.propertyAlias.includes('/' + propertyAlias + '/') + : false; return matchProp && item.culture === culture && item.segment === segment // ignore field matching if match options are used - && (ignoreField || (item.fieldName === fieldName || (fieldName === undefined || fieldName === ""))); + && (options.matchType || (item.fieldName === fieldName || (fieldName === undefined || fieldName === ""))); }); } @@ -234,20 +239,22 @@ function serverValidationManager($timeout) { cb.options = defaultMatchOptions; } - var matchProp = cb.options.matchPrefix - ? (cb.propertyAlias === propertyAlias || propertyAlias.startsWith(cb.propertyAlias + '/')) - : cb.options.matchSuffix - ? (cb.propertyAlias === propertyAlias || propertyAlias.endsWith(cb.propertyAlias + '/')) - : cb.propertyAlias === propertyAlias; - - var ignoreField = cb.options.matchPrefix || cb.options.matchSuffix; + var matchProp = cb.propertyAlias === propertyAlias + ? true + : cb.options.matchType === "prefix" + ? propertyAlias.startsWith(cb.propertyAlias + '/') + : cb.options.matchType === "suffix" + ? propertyAlias.endsWith('/' + cb.propertyAlias) + : cb.options.matchType === "contains" + ? propertyAlias.includes('/' + cb.propertyAlias + '/') + : false; //returns any callback that have been registered directly against the field and for only the property return matchProp && cb.culture === culture && cb.segment === segment - // if the callback is configured to patch prefix then we ignore the field value - && (ignoreField || (cb.fieldName === fieldName || (cb.fieldName === undefined || cb.fieldName === ""))); + // ignore field matching if match options are used + && (cb.options.matchType || (cb.fieldName === fieldName || (cb.fieldName === undefined || cb.fieldName === ""))); }); return found; } diff --git a/src/Umbraco.Web.UI.Client/test/unit/common/services/server-validation-manager.spec.js b/src/Umbraco.Web.UI.Client/test/unit/common/services/server-validation-manager.spec.js index c5c6177444..ab2a5cdd69 100644 --- a/src/Umbraco.Web.UI.Client/test/unit/common/services/server-validation-manager.spec.js +++ b/src/Umbraco.Web.UI.Client/test/unit/common/services/server-validation-manager.spec.js @@ -639,13 +639,13 @@ if (propertyErrors.length > 0) { callbackA.push(propertyErrors); } - }, null, { matchPrefix: true }); + }, null, { matchType: "prefix" }); serverValidationManager.subscribe("myProperty/34E3A26C-103D-4A05-AB9D-7E14032309C3/addresses", null, null, function (isValid, propertyErrors, allErrors) { if (propertyErrors.length > 0) { callbackB.push(propertyErrors); } - }, null, { matchPrefix: true }); + }, null, { matchType: "prefix" }); //act // will match A: @@ -682,6 +682,97 @@ }); + it('can subscribe to a property validation path suffix', function () { + var callbackA = []; + var callbackB = []; + + //arrange + serverValidationManager.subscribe("myProperty", null, null, function (isValid, propertyErrors, allErrors) { + if (propertyErrors.length > 0) { + callbackA.push(propertyErrors); + } + }, null, { matchType: "suffix" }); + + serverValidationManager.subscribe("city", null, null, function (isValid, propertyErrors, allErrors) { + if (propertyErrors.length > 0) { + callbackB.push(propertyErrors); + } + }, null, { matchType: "suffix" }); + + //act + // will match A: + serverValidationManager.addPropertyError("myProperty", null, null, "property error", null); + serverValidationManager.addPropertyError("myProperty", null, "value1", "value error", null); + // will match B + serverValidationManager.addPropertyError("myProperty/34E3A26C-103D-4A05-AB9D-7E14032309C3/addresses/FBEAEE8F-4BC9-43EE-8B81-FCA8978850F1/city", null, null, "property error", null); + serverValidationManager.addPropertyError("myProperty/34E3A26C-103D-4A05-AB9D-7E14032309C3/addresses/FBEAEE8F-4BC9-43EE-8B81-FCA8978850F1/city", null, "value1", "value error", null); + // won't match: + serverValidationManager.addPropertyError("myProperty", "en-US", null, "property error", null); + serverValidationManager.addPropertyError("myProperty/34E3A26C-103D-4A05-AB9D-7E14032309C3/addresses/FBEAEE8F-4BC9-43EE-8B81-FCA8978850F1/city", "en-US", null, "property error", null); + serverValidationManager.addPropertyError("otherProperty", null, null, "property error", null); + serverValidationManager.addPropertyError("otherProperty", null, "value1", "value error", null); + + //assert + + // both will be called each time addPropertyError is called + expect(callbackA.length).toEqual(8); + expect(callbackB.length).toEqual(6); // B - will only be called 6 times with errors because the first 2 calls to addPropertyError haven't added errors for B yet + expect(callbackA[callbackA.length - 1].length).toEqual(2); // 2 errors for A + expect(callbackB[callbackB.length - 1].length).toEqual(2); // 2 errors for B + + // clear the data and notify + callbackA = []; + callbackB = []; + + serverValidationManager.notify(); + $timeout.flush(); + + expect(callbackA.length).toEqual(1); + expect(callbackB.length).toEqual(1); + expect(callbackA[0].length).toEqual(2); // 2 errors for A + expect(callbackB[0].length).toEqual(2); // 2 errors for B + + }); + + it('can subscribe to a property validation path contains', function () { + var callbackA = []; + + //arrange + serverValidationManager.subscribe("addresses", null, null, function (isValid, propertyErrors, allErrors) { + if (propertyErrors.length > 0) { + callbackA.push(propertyErrors); + } + }, null, { matchType: "contains" }); + + //act + // will match A: + serverValidationManager.addPropertyError("addresses", null, null, "property error", null); + serverValidationManager.addPropertyError("addresses", null, "value1", "value error", null); + serverValidationManager.addPropertyError("myProperty/34E3A26C-103D-4A05-AB9D-7E14032309C3/addresses/FBEAEE8F-4BC9-43EE-8B81-FCA8978850F1/city", null, null, "property error", null); + serverValidationManager.addPropertyError("myProperty/34E3A26C-103D-4A05-AB9D-7E14032309C3/addresses/FBEAEE8F-4BC9-43EE-8B81-FCA8978850F1/city", null, "value1", "value error", null); + // won't match: + serverValidationManager.addPropertyError("addresses", "en-US", null, "property error", null); + serverValidationManager.addPropertyError("addresses/34E3A26C-103D-4A05-AB9D-7E14032309C3/addresses/FBEAEE8F-4BC9-43EE-8B81-FCA8978850F1/city", "en-US", null, "property error", null); + serverValidationManager.addPropertyError("otherProperty", null, null, "property error", null); + serverValidationManager.addPropertyError("otherProperty", null, "value1", "value error", null); + + //assert + + // both will be called each time addPropertyError is called + expect(callbackA.length).toEqual(8); + expect(callbackA[callbackA.length - 1].length).toEqual(4); // 4 errors for A + + // clear the data and notify + callbackA = []; + + serverValidationManager.notify(); + $timeout.flush(); + + expect(callbackA.length).toEqual(1); + expect(callbackA[0].length).toEqual(4); // 4 errors for A + + }); + // TODO: Finish testing the rest! }); From b32d92ed075a7d1e93e9303b5d8bf76101dbcd56 Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 15 Jul 2020 16:49:54 +1000 Subject: [PATCH 334/377] Add valServerMatch which we now use to highlight block rows if they have errors. --- .../validation/valpropertymsg.directive.js | 6 +- .../validation/valserver.directive.js | 8 +- .../validation/valservermatch.directive.js | 89 +++++++++++++++++++ .../blockeditormodelobject.service.js | 6 +- .../blockeditor/blockeditor.controller.js | 14 ++- .../labelblock/labelblock.editor.html | 17 ++-- .../services/block-editor-service.spec.js | 2 +- 7 files changed, 121 insertions(+), 21 deletions(-) create mode 100644 src/Umbraco.Web.UI.Client/src/common/directives/validation/valservermatch.directive.js diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/validation/valpropertymsg.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/validation/valpropertymsg.directive.js index a34c4aa0cc..181d204336 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/validation/valpropertymsg.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/validation/valpropertymsg.directive.js @@ -288,7 +288,7 @@ function valPropertyMsg(serverValidationManager, localizationService, angularHel "", serverValidationManagerCallback, currentSegment, - { matchPrefix: true } // match property validation path prefix + { matchType: "suffix" } // match property validation path prefix )); } @@ -298,9 +298,7 @@ function valPropertyMsg(serverValidationManager, localizationService, angularHel //when the scope is disposed we need to unsubscribe scope.$on('$destroy', function () { stopWatch(); - for (var u in unsubscribe) { - unsubscribe[u](); - } + unsubscribe.forEach(u => u()); }); onInit(); diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/validation/valserver.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/validation/valserver.directive.js index b9b9b98174..ea6087d4e9 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/validation/valserver.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/validation/valserver.directive.js @@ -13,14 +13,14 @@ function valServer(serverValidationManager) { link: function (scope, element, attr, ctrls) { var modelCtrl = ctrls[0]; - var umbPropCtrl = ctrls.length > 1 ? ctrls[1] : null; + var umbPropCtrl = ctrls[1]; if (!umbPropCtrl) { //we cannot proceed, this validator will be disabled return; } // optional reference to the varaint-content-controller, needed to avoid validation when the field is invariant on non-default languages. - var umbVariantCtrl = ctrls.length > 2 ? ctrls[2] : null; + var umbVariantCtrl = ctrls[2]; var currentProperty = umbPropCtrl.property; var currentCulture = currentProperty.culture; @@ -121,9 +121,7 @@ function valServer(serverValidationManager) { scope.$on('$destroy', function () { stopWatch(); - for (var u in unsubscribe) { - unsubscribe[u](); - } + unsubscribe.forEach(u => u()); }); } }; diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/validation/valservermatch.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/validation/valservermatch.directive.js new file mode 100644 index 0000000000..766f3f6755 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/common/directives/validation/valservermatch.directive.js @@ -0,0 +1,89 @@ + +function valServerMatch(serverValidationManager) { + return { + require: ['form', '^^umbProperty', '?^^umbVariantContent'], + restrict: "A", + scope: { + valServerMatch: "=" + }, + link: function (scope, element, attr, ctrls) { + + var formCtrl = ctrls[0]; + var umbPropCtrl = ctrls[1]; + if (!umbPropCtrl) { + //we cannot proceed, this validator will be disabled + return; + } + + // optional reference to the varaint-content-controller, needed to avoid validation when the field is invariant on non-default languages. + var umbVariantCtrl = ctrls[2]; + + var currentProperty = umbPropCtrl.property; + var currentCulture = currentProperty.culture; + var currentSegment = currentProperty.segment; + + if (umbVariantCtrl) { + //if we are inside of an umbVariantContent directive + + var currentVariant = umbVariantCtrl.editor.content; + + // Lets check if we have variants and we are on the default language then ... + if (umbVariantCtrl.content.variants.length > 1 && (!currentVariant.language || !currentVariant.language.isDefault) && !currentCulture && !currentSegment && !currentProperty.unlockInvariantValue) { + //This property is locked cause its a invariant property shown on a non-default language. + //Therefor do not validate this field. + return; + } + } + + // if we have reached this part, and there is no culture, then lets fallback to invariant. To get the validation feedback for invariant language. + currentCulture = currentCulture || "invariant"; + + var unsubscribe = []; + + //subscribe to the server validation changes + function serverValidationManagerCallback(isValid, propertyErrors, allErrors) { + if (!isValid) { + formCtrl.$setValidity('valServerMatch', false, formCtrl); + } + else { + formCtrl.$setValidity('valServerMatch', true, formCtrl); + } + } + + if (Utilities.isObject(scope.valServerMatch)) { + var allowedKeys = ["contains", "prefix", "suffix"]; + Object.keys(scope.valServerMatch).forEach(k => { + if (allowedKeys.indexOf(k) === -1) { + throw "valServerMatch dictionary keys must be one of " + allowedKeys.join(); + } + + unsubscribe.push(serverValidationManager.subscribe( + scope.valServerMatch[k], + currentCulture, + "", + serverValidationManagerCallback, + currentSegment, + { matchType: k } // specify the match type + )); + + }); + } + else if (Utilities.isString(scope.valServerMatch)) { + unsubscribe.push(serverValidationManager.subscribe( + scope.valServerMatch, + currentCulture, + "", + serverValidationManagerCallback, + currentSegment)); + } + else { + throw "valServerMatch value must be a string or a dictionary"; + } + + scope.$on('$destroy', function () { + unsubscribe.forEach(u => u()); + }); + } + }; +} +angular.module('umbraco.directives.validation').directive("valServerMatch", valServerMatch); 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 4cd2720216..9259e4bf64 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 @@ -129,9 +129,9 @@ var prop = tab.properties[p]; // Watch value of property since this is the only value we want to keep synced. - // Do notice that it is not performing a deep watch, meaning that we are only watching primatives and changes directly to the object of property-value. - // But we like to sync non-primative values as well! Yes, and this does happen, just not through this code, but through the nature of JavaScript. - // Non-primative values act as references to the same data and are therefor synced. + // Do notice that it is not performing a deep watch, meaning that we are only watching primitive and changes directly to the object of property-value. + // But we like to sync non-primitive values as well! Yes, and this does happen, just not through this code, but through the nature of JavaScript. + // Non-primitive values act as references to the same data and are therefor synced. blockObject.__watchers.push(isolatedScope.$watch("blockObjects._" + blockObject.key + "." + field + ".variants[0].tabs[" + t + "].properties[" + p + "].value", watcherCreator(blockObject, prop))); // We also like to watch our data model to be able to capture changes coming from other places. 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 a62b632d38..daded4d9a9 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 @@ -63,8 +63,20 @@ angular.module("umbraco") } } - vm.close = function() { + vm.close = function () { if (vm.model && vm.model.close) { + // TODO: At this stage there could very well have been server errors that have been cleared + // but if we 'close' we are basically cancelling the value changes which means we'd want to cancel + // all of the server errors just cleared. It would be possible to do that but also quite annoying. + // The rudimentary way would be to: + // * Track all cleared server errors here by subscribing to the prefix validation of controls contained here + // * If this is closed, re-add all of those server validation errors + // A more robust way to do this would be to: + // * Add functionality to the serverValidationManager whereby we can remove validation errors and it will + // maintain a copy of the original errors + // * 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 + // 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/blocklistentryeditors/labelblock/labelblock.editor.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklistentryeditors/labelblock/labelblock.editor.html index 91c8468d2d..90d0f4bcb4 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklistentryeditors/labelblock/labelblock.editor.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklistentryeditors/labelblock/labelblock.editor.html @@ -1,7 +1,10 @@ - + + + diff --git a/src/Umbraco.Web.UI.Client/test/unit/common/services/block-editor-service.spec.js b/src/Umbraco.Web.UI.Client/test/unit/common/services/block-editor-service.spec.js index e0cd2d0c93..7f69f0594e 100644 --- a/src/Umbraco.Web.UI.Client/test/unit/common/services/block-editor-service.spec.js +++ b/src/Umbraco.Web.UI.Client/test/unit/common/services/block-editor-service.spec.js @@ -119,7 +119,7 @@ }); - it('getBlockObject syncs primative values', function (done) { + it('getBlockObject syncs primitive values', function (done) { var propertyModel = angular.copy(propertyModelMock); From 12915459081746481b4e87e28dedd3327241d37c Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 15 Jul 2020 17:54:03 +1000 Subject: [PATCH 335/377] Fixes styling since umb-property is a component, fixes binding to the property validation path --- .../validation/valpropertymsg.directive.js | 12 +++++------- .../inlineblock/inlineblock.editor.less | 2 +- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/validation/valpropertymsg.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/validation/valpropertymsg.directive.js index 181d204336..ec95baffb2 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/validation/valpropertymsg.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/validation/valpropertymsg.directive.js @@ -40,8 +40,6 @@ function valPropertyMsg(serverValidationManager, localizationService, angularHel var currentProperty = umbPropCtrl.property; scope.currentProperty = currentProperty; - var propertyValidationKey = umbPropCtrl.getValidationPath(); - var currentCulture = currentProperty.culture; var currentSegment = currentProperty.segment; @@ -72,7 +70,7 @@ function valPropertyMsg(serverValidationManager, localizationService, angularHel //this can be null if no property was assigned if (scope.currentProperty) { //first try to get the error msg from the server collection - var err = serverValidationManager.getPropertyError(propertyValidationKey, null, "", null); + var err = serverValidationManager.getPropertyError(umbPropCtrl.getValidationPath(), null, "", null); //if there's an error message use it if (err && err.errorMsg) { return err.errorMsg; @@ -115,7 +113,7 @@ function valPropertyMsg(serverValidationManager, localizationService, angularHel } // returns true if there is only a single server validation error for this property validation key in it's validation path - function isLastServerError() { + function isLastServerError(propertyValidationKey) { var nestedErrs = serverValidationManager.getPropertyErrorsByValidationPath( propertyValidationKey, currentCulture, @@ -147,9 +145,9 @@ function valPropertyMsg(serverValidationManager, localizationService, angularHel } if (shouldClearError()) { - + var propertyValidationKey = umbPropCtrl.getValidationPath(); // check if we can clear it based on child server errors, if we are the only explicit one remaining we can clear ourselves - if (isLastServerError()) { + if (isLastServerError(propertyValidationKey)) { serverValidationManager.removePropertyError(propertyValidationKey, currentCulture, "", currentSegment); } @@ -283,7 +281,7 @@ function valPropertyMsg(serverValidationManager, localizationService, angularHel } unsubscribe.push(serverValidationManager.subscribe( - propertyValidationKey, + umbPropCtrl.getValidationPath(), currentCulture, "", serverValidationManagerCallback, diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklistentryeditors/inlineblock/inlineblock.editor.less b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklistentryeditors/inlineblock/inlineblock.editor.less index 47aa5b62ca..d07265f7db 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklistentryeditors/inlineblock/inlineblock.editor.less +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklistentryeditors/inlineblock/inlineblock.editor.less @@ -63,7 +63,7 @@ box-shadow: none; margin-top: 10px; margin-bottom: 0; - > .umb-group-panel__content > .umb-property { + > .umb-group-panel__content .umb-property { margin-bottom: 20px; } } From b7e25eed21e29ea449918eb8b7d7efac676aa316 Mon Sep 17 00:00:00 2001 From: Shannon Date: Thu, 16 Jul 2020 17:31:46 +1000 Subject: [PATCH 336/377] Yay! gets it all working for the block editor, refactored counting form errors recursively to actually work and be readable, updates valPropertyMsg with all of the odd edge cases. --- .../components/content/edit.controller.js | 23 ++++-- .../validation/valformmanager.directive.js | 52 ++++--------- .../validation/valpropertymsg.directive.js | 58 ++++++++------- .../common/services/angularhelper.service.js | 74 ++++++++++++++----- .../services/servervalidationmgr.service.js | 4 +- 5 files changed, 116 insertions(+), 95 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/content/edit.controller.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/content/edit.controller.js index 7f6d1cc728..68b19abf08 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/content/edit.controller.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/content/edit.controller.js @@ -516,6 +516,12 @@ } } + function handleHttpException(err) { + if (!err.status) { + $exceptionHandler(err); + } + } + /** Just shows a simple notification that there are client side validation issues to be fixed */ function showValidationNotification() { //TODO: We need to make the validation UI much better, there's a lot of inconsistencies in v8 including colors, issues with the property groups and validation errors between variants @@ -576,6 +582,7 @@ overlayService.close(); }, function (err) { $scope.page.buttonGroupState = 'error'; + handleHttpException(err); }); @@ -621,7 +628,7 @@ model.submitButtonState = "error"; //re-map the dialog model since we've re-bound the properties dialog.variants = $scope.content.variants; - $exceptionHandler(err); + handleHttpException(err); }); }, close: function () { @@ -644,7 +651,7 @@ $scope.page.buttonGroupState = "success"; }, function (err) { $scope.page.buttonGroupState = "error"; - $exceptionHandler(err); + handleHttpException(err); });; } }; @@ -680,7 +687,7 @@ model.submitButtonState = "error"; //re-map the dialog model since we've re-bound the properties dialog.variants = $scope.content.variants; - $exceptionHandler(err); + handleHttpException(err); }); }, close: function () { @@ -705,7 +712,7 @@ $scope.page.buttonGroupState = "success"; }, function (err) { $scope.page.buttonGroupState = "error"; - $exceptionHandler(err); + handleHttpException(err); }); } }; @@ -743,7 +750,7 @@ model.submitButtonState = "error"; //re-map the dialog model since we've re-bound the properties dialog.variants = $scope.content.variants; - $exceptionHandler(err); + handleHttpException(err); }); }, close: function (oldModel) { @@ -768,7 +775,7 @@ $scope.page.saveButtonState = "success"; }, function (err) { $scope.page.saveButtonState = "error"; - $exceptionHandler(err); + handleHttpException(err); }); } @@ -821,7 +828,7 @@ model.submitButtonState = "error"; //re-map the dialog model since we've re-bound the properties dialog.variants = Utilities.copy($scope.content.variants); - $exceptionHandler(err); + handleHttpException(err); }); }, @@ -880,7 +887,7 @@ model.submitButtonState = "error"; //re-map the dialog model since we've re-bound the properties dialog.variants = $scope.content.variants; - $exceptionHandler(err); + handleHttpException(err); }); }, diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/validation/valformmanager.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/validation/valformmanager.directive.js index 86ea94914a..d91234e583 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/validation/valformmanager.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/validation/valformmanager.directive.js @@ -112,46 +112,22 @@ function valFormManager(serverValidationManager, $rootScope, $timeout, $location }); //watch the list of validation errors to notify the application of any validation changes - scope.$watch(function () { - //the validators are in the $error collection: https://docs.angularjs.org/api/ng/type/form.FormController#$error - //since each key is the validator name (i.e. 'required') we can't just watch the number of keys, we need to watch - //the sum of the items inside of each key + scope.$watch(() => angularHelper.countAllFormErrors(formCtrl), + function (e) { + + notify(scope); + + notifySubView(); + + //find all invalid elements' .control-group's and apply the error class + var inError = element.find(".control-group .ng-invalid").closest(".control-group"); + inError.addClass("error"); + + //find all control group's that have no error and ensure the class is removed + var noInError = element.find(".control-group .ng-valid").closest(".control-group").not(inError); + noInError.removeClass("error"); - //get the lengths of each array for each key in the $error collection - var validatorLengths = _.map(formCtrl.$error, function (val, key) { - // if there are child ng-forms, include the $error collections in those as well - var innerErrorCount = _.reduce( - _.map(val, v => - _.reduce( - _.map(v.$error, e => e.length), - (m, n) => m + n - ) - ), - (memo, num) => memo + num - ); - return val.length + innerErrorCount; }); - //sum up all numbers in the resulting array - var sum = _.reduce(validatorLengths, function (memo, num) { - return memo + num; - }, 0); - //this is the value we watch to notify of any validation changes on the form - return sum; - }, function (e) { - - notify(scope); - - notifySubView(); - - //find all invalid elements' .control-group's and apply the error class - var inError = element.find(".control-group .ng-invalid").closest(".control-group"); - inError.addClass("error"); - - //find all control group's that have no error and ensure the class is removed - var noInError = element.find(".control-group .ng-valid").closest(".control-group").not(inError); - noInError.removeClass("error"); - - }); //This tracks if the user is currently saving a new item, we use this to determine // if we should display the warning dialog that they are leaving the page - if a new item diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/validation/valpropertymsg.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/validation/valpropertymsg.directive.js index ec95baffb2..ccb8e2e6aa 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/validation/valpropertymsg.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/validation/valpropertymsg.directive.js @@ -84,26 +84,36 @@ function valPropertyMsg(serverValidationManager, localizationService, angularHel } // return true if there is only a single error left on the property form of either valPropertyMsg or valServer - function shouldClearError() { - var errCount = 0; + function checkAndClearError() { - for (var e in formCtrl.$error) { - if (Utilities.isArray(formCtrl.$error[e])) { - errCount++; - } + var errCount = angularHelper.countAllFormErrors(formCtrl); + + if (errCount === 0) { + resetError(); + return true; } - //we are explicitly checking for valServer errors here, since we shouldn't auto clear - // based on other errors. We'll also check if there's no other validation errors apart from valPropertyMsg, if valPropertyMsg - // is the only one, then we'll clear. - - if (errCount === 0 - || (errCount === 1 && hasExplicitError()) - || (formCtrl.$invalid && Utilities.isArray(formCtrl.$error.valServer))) { + if (errCount > 2) { + return false; + } + var hasValServer = Utilities.isArray(formCtrl.$error.valServer); + if (errCount === 1 && hasValServer) { return true; } + var hasOwnErr = hasExplicitError(); + if ((errCount === 1 && hasOwnErr) || (errCount === 2 && hasOwnErr && hasValServer)) { + + var propertyValidationPath = umbPropCtrl.getValidationPath(); + // check if we can clear it based on child server errors, if we are the only explicit one remaining we can clear ourselves + if (isLastServerError(propertyValidationPath)) { + serverValidationManager.removePropertyError(propertyValidationPath, currentCulture, "", currentSegment); + return true; + } + return false; + } + return false; } @@ -113,14 +123,13 @@ function valPropertyMsg(serverValidationManager, localizationService, angularHel } // returns true if there is only a single server validation error for this property validation key in it's validation path - function isLastServerError(propertyValidationKey) { + function isLastServerError(propertyValidationPath) { var nestedErrs = serverValidationManager.getPropertyErrorsByValidationPath( - propertyValidationKey, + propertyValidationPath, currentCulture, - "", currentSegment, - true); - if (nestedErrs.length === 1 && nestedErrs[0].propertyAlias === propertyValidationKey) { + { matchType: "prefix" }); + if (nestedErrs.length === 0 || (nestedErrs.length === 1 && nestedErrs[0].propertyAlias === propertyValidationPath)) { return true; } @@ -144,13 +153,7 @@ function valPropertyMsg(serverValidationManager, localizationService, angularHel return; } - if (shouldClearError()) { - var propertyValidationKey = umbPropCtrl.getValidationPath(); - // check if we can clear it based on child server errors, if we are the only explicit one remaining we can clear ourselves - if (isLastServerError(propertyValidationKey)) { - serverValidationManager.removePropertyError(propertyValidationKey, currentCulture, "", currentSegment); - } - + if (checkAndClearError()) { resetError(); } else if (showValidation && scope.errorMsg === "") { @@ -170,10 +173,11 @@ function valPropertyMsg(serverValidationManager, localizationService, angularHel } function resetError() { + stopWatch(); hasError = false; formCtrl.$setValidity('valPropertyMsg', true, formCtrl); scope.errorMsg = ""; - stopWatch(); + } function checkValidationStatus() { @@ -270,7 +274,7 @@ function valPropertyMsg(serverValidationManager, localizationService, angularHel hasError = !isValid; if (hasError) { //set the error message to the server message - scope.errorMsg = propertyErrors.length > 0 ? labels.propertyHasErrors : propertyErrors[0].errorMsg || labels.propertyHasErrors; + scope.errorMsg = propertyErrors.length > 1 ? labels.propertyHasErrors : propertyErrors[0].errorMsg || labels.propertyHasErrors; //flag that the current validator is invalid formCtrl.$setValidity('valPropertyMsg', false, formCtrl); startWatch(); diff --git a/src/Umbraco.Web.UI.Client/src/common/services/angularhelper.service.js b/src/Umbraco.Web.UI.Client/src/common/services/angularhelper.service.js index 12247f15b5..8c384455bb 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/angularhelper.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/angularhelper.service.js @@ -10,8 +10,61 @@ function angularHelper($q) { var requiredFormProps = ["$error", "$name", "$dirty", "$pristine", "$valid", "$submitted", "$pending"]; + function collectAllFormErrorsRecursively(formCtrl, allErrors) { + // loop over the error dictionary (see https://docs.angularjs.org/api/ng/type/form.FormController#$error) + var keys = Object.keys(formCtrl.$error); + if (keys.length === 0) { + return; + } + keys.forEach(validationKey => { + var ctrls = formCtrl.$error[validationKey]; + ctrls.forEach(ctrl => { + if (isForm(ctrl)) { + // sometimes the control in error is the same form so we cannot recurse else we'll cause an infinite loop + // and in this case it means the error is assigned directly to the form, not a control + if (ctrl === formCtrl) { + allErrors.push(ctrl); // add the error + return; + } + // recurse with the sub form + collectAllFormErrorsRecursively(ctrl, allErrors); + } + else { + // it's a normal control + allErrors.push(ctrl); // add the error + } + }); + }); + } + + function isForm(obj) { + // a method to check that the collection of object prop names contains the property name expected + function allPropertiesExist(objectPropNames) { + //ensure that every required property name exists on the current object + return _.every(requiredFormProps, function (item) { + return _.contains(objectPropNames, item); + }); + } + + //get the keys of the property names for the current object + var props = _.keys(obj); + //if the length isn't correct, try the next prop + if (props.length < requiredFormProps.length) { + return false; + } + + //ensure that every required property name exists on the current scope property + return allPropertiesExist(props); + } + return { + countAllFormErrors: function (formCtrl) { + var allErrors = []; + collectAllFormErrorsRecursively(formCtrl, allErrors); + return allErrors.length; + }, + /** * Will traverse up the $scope chain to all ancestors until the predicate matches for the current scope or until it's at the root. * @param {any} scope @@ -104,26 +157,7 @@ function angularHelper($q) { }, - isForm: function (obj) { - - // a method to check that the collection of object prop names contains the property name expected - function allPropertiesExist(objectPropNames) { - //ensure that every required property name exists on the current object - return _.every(requiredFormProps, function (item) { - return _.contains(objectPropNames, item); - }); - } - - //get the keys of the property names for the current object - var props = _.keys(obj); - //if the length isn't correct, try the next prop - if (props.length < requiredFormProps.length) { - return false; - } - - //ensure that every required property name exists on the current scope property - return allPropertiesExist(props); - }, + isForm: isForm, /** * @ngdoc function diff --git a/src/Umbraco.Web.UI.Client/src/common/services/servervalidationmgr.service.js b/src/Umbraco.Web.UI.Client/src/common/services/servervalidationmgr.service.js index a5392d16ee..53309d63f5 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/servervalidationmgr.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/servervalidationmgr.service.js @@ -776,8 +776,8 @@ function serverValidationManager($timeout) { return undefined; }, - getPropertyErrorsByValidationPath: function (propertyAlias, culture, segment, options) { - return getPropertyErrors(propertyAlias, culture, segment, "", options); + getPropertyErrorsByValidationPath: function (propertyValidationPath, culture, segment, options) { + return getPropertyErrors(propertyValidationPath, culture, segment, "", options); }, /** From 2af1feadfcd851dbfb067659edd38ef8e453c5fb Mon Sep 17 00:00:00 2001 From: Shannon Date: Thu, 16 Jul 2020 17:49:23 +1000 Subject: [PATCH 337/377] adds error styling --- .../inlineblock/inlineblock.editor.html | 24 ++++++++++++------- .../inlineblock/inlineblock.editor.less | 15 ++++++++---- .../labelblock/labelblock.editor.html | 2 +- .../labelblock/labelblock.editor.less | 14 ++++++----- 4 files changed, 34 insertions(+), 21 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklistentryeditors/inlineblock/inlineblock.editor.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklistentryeditors/inlineblock/inlineblock.editor.html index 60b3542d6c..a76894ac51 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklistentryeditors/inlineblock/inlineblock.editor.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklistentryeditors/inlineblock/inlineblock.editor.html @@ -1,10 +1,16 @@ -
        - -
        - + +
        + +
        + +
        -
        + diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklistentryeditors/inlineblock/inlineblock.editor.less b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklistentryeditors/inlineblock/inlineblock.editor.less index d07265f7db..babbcbf2b7 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklistentryeditors/inlineblock/inlineblock.editor.less +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklistentryeditors/inlineblock/inlineblock.editor.less @@ -1,5 +1,4 @@ .blockelement-inlineblock-editor { - display: block; margin-bottom: 4px; margin-top: 4px; @@ -14,25 +13,24 @@ > button { width: 100%; min-height: 48px; - cursor: pointer; color: @ui-action-discreet-type; - text-align: left; padding-left: 10px; padding-bottom: 2px; - user-select: none; - + .caret { transform: rotate(-90deg); transition: transform 80ms ease-out; } + i { font-size: 22px; display: inline-block; vertical-align: middle; } + span { display: inline-block; vertical-align: middle; @@ -47,12 +45,19 @@ .umb-block-list__block.--active & { border-color: @gray-8; box-shadow: 0 0 2px 0px rgba(0, 0, 0, 0.05); + > button { > .caret { transform: rotate(0deg); } } } + + &.--error { + color: @ui-active-type; + border-color: @ui-active; + background-color: @ui-active; + } } .blockelement-inlineblock-editor__inner { border-top: 1px solid @gray-8; diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklistentryeditors/labelblock/labelblock.editor.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklistentryeditors/labelblock/labelblock.editor.html index 90d0f4bcb4..6d3f139db2 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklistentryeditors/labelblock/labelblock.editor.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklistentryeditors/labelblock/labelblock.editor.html @@ -2,7 +2,7 @@ - +
        + +
        + +
        + + +
        +
        + + +
        + +
        -
        - -
        - -
        +
        From 0afdd6941642deded50c82ed56affca0590a0f84 Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 17 Jul 2020 01:40:32 +1000 Subject: [PATCH 340/377] Fixes deleting nodes with server validation and also ensuring that the dirty state is set on the form --- .../overlays/umboverlay.directive.js | 2 +- .../validation/valpropertymsg.directive.js | 1 - .../src/common/services/formhelper.service.js | 2 +- .../services/servervalidationmgr.service.js | 24 ++++--------------- .../umbBlockListPropertyEditor.component.js | 15 +++++++++--- .../nestedcontent/nestedcontent.controller.js | 15 ++++++++---- 6 files changed, 29 insertions(+), 30 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/overlays/umboverlay.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/overlays/umboverlay.directive.js index ad396e7a9a..f595a14664 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/overlays/umboverlay.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/overlays/umboverlay.directive.js @@ -463,7 +463,7 @@ Opens an overlay to show a custom YSOD.
        scope.submitForm = function (model) { if (scope.model.submit) { - if (formHelper.submitForm({ scope: scope, skipValidation: scope.model.skipFormValidation })) { + if (formHelper.submitForm({ scope: scope, skipValidation: scope.model.skipFormValidation, keepServerValidation: true })) { if (scope.model.confirmSubmit && scope.model.confirmSubmit.enable && !scope.directive.enableConfirmButton) { //wrap in a when since we don't know if this is a promise or not diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/validation/valpropertymsg.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/validation/valpropertymsg.directive.js index 0115b6bee9..979ac23bb3 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/validation/valpropertymsg.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/validation/valpropertymsg.directive.js @@ -309,7 +309,6 @@ function valPropertyMsg(serverValidationManager, localizationService, angularHel if (propertyErrors.length === 1 && hadError && !formCtrl.$pristine) { var propertyValidationPath = umbPropCtrl.getValidationPath(); - console.log("only 1 left, clearing! " + propertyValidationPath); serverValidationManager.removePropertyError(propertyValidationPath, currentCulture, "", currentSegment); resetError(); } diff --git a/src/Umbraco.Web.UI.Client/src/common/services/formhelper.service.js b/src/Umbraco.Web.UI.Client/src/common/services/formhelper.service.js index 49980a8c41..962961729b 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/formhelper.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/formhelper.service.js @@ -44,7 +44,7 @@ function formHelper(angularHelper, serverValidationManager, notificationsService //the first thing any form must do is broadcast the formSubmitting event args.scope.$broadcast("formSubmitting", { scope: args.scope, action: args.action }); - // Some property editors need to performe an action after all property editors have reacted to the formSubmitting. + // Some property editors need to perform an action after all property editors have reacted to the formSubmitting. args.scope.$broadcast("postFormSubmitting", { scope: args.scope, action: args.action }); //then check if the form is valid diff --git a/src/Umbraco.Web.UI.Client/src/common/services/servervalidationmgr.service.js b/src/Umbraco.Web.UI.Client/src/common/services/servervalidationmgr.service.js index e6f114634f..b61ac74b2c 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/servervalidationmgr.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/servervalidationmgr.service.js @@ -738,28 +738,12 @@ function serverValidationManager($timeout) { * @description * Removes an error message for the content property */ - removePropertyError: function (propertyAlias, culture, fieldName, segment) { + removePropertyError: function (propertyAlias, culture, fieldName, segment, options) { - if (!propertyAlias) { - return; - } + var errors = getPropertyErrors(propertyAlias, culture, segment, fieldName, options); + items = items.filter(v => errors.indexOf(v) === -1); - //normalize culture to null - if (!culture) { - culture = "invariant"; - } - //normalize segment to null - if (!segment) { - segment = null; - } - - //remove the item - var count = items.length; - items = _.reject(items, function (item) { - return (item.propertyAlias === propertyAlias && item.culture === culture && item.segment === segment && (item.fieldName === fieldName || (fieldName === undefined || fieldName === ""))); - }); - - if (items.length !== count) { + if (errors.length > 0) { // removal was successful, re-notify all subscribers notifyCallbacks(); } 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 b4304747c9..b5907126f2 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 @@ -28,7 +28,7 @@ } }); - function BlockListController($scope, editorService, clipboardService, localizationService, overlayService, blockEditorService) { + function BlockListController($scope, editorService, clipboardService, localizationService, overlayService, blockEditorService, udiService, serverValidationManager) { var unsubscribe = []; var modelObject; @@ -231,10 +231,19 @@ function deleteBlock(block) { var layoutIndex = vm.layout.findIndex(entry => entry.udi === block.content.udi); - if(layoutIndex === -1) { + if (layoutIndex === -1) { throw new Error("Could not find layout entry of block with udi: "+block.content.udi) } - vm.layout.splice(layoutIndex, 1); + + setDirty(); + + var removed = vm.layout.splice(layoutIndex, 1); + removed.forEach(x => { + // remove any server validation errors associated + var guid = udiService.getKey(x.udi); + serverValidationManager.removePropertyError(guid, vm.umbProperty.property.culture, vm.umbProperty.property.segment, "", { matchType: "contains" }); + }); + modelObject.removeDataAndDestroyModel(block); } 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 bf77e22b1d..fe9725a7d8 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 @@ -65,7 +65,7 @@ } }); - function NestedContentController($scope, $interpolate, $filter, $timeout, contentResource, localizationService, iconHelper, clipboardService, eventsService, overlayService) { + function NestedContentController($scope, $interpolate, $filter, serverValidationManager, contentResource, localizationService, iconHelper, clipboardService, eventsService, overlayService) { var vm = this; var model = $scope.$parent.$parent.model; @@ -298,8 +298,15 @@ } function deleteNode(idx) { - vm.nodes.splice(idx, 1); + var removed = vm.nodes.splice(idx, 1); + setDirty(); + + removed.forEach(x => { + // remove any server validation errors associated + serverValidationManager.removePropertyError(x.key, vm.umbProperty.property.culture, vm.umbProperty.property.segment, "", { matchType: "contains" }); + }); + updateModel(); validate(); }; @@ -670,8 +677,8 @@ ]; this.$onInit = function () { - if (this.umbProperty) { - this.umbProperty.setPropertyActions(propertyActions); + if (vm.umbProperty) { + vm.umbProperty.setPropertyActions(propertyActions); } }; From 4d060946965bef2ff22553fa9967dd61b7341fc7 Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 17 Jul 2020 12:21:33 +1000 Subject: [PATCH 341/377] Ensures that FormController hierarchy is maintained with infinite editors --- .../components/editor/umbeditors.directive.js | 20 ++++++-- .../overlays/umboverlay.directive.js | 2 +- .../propertyeditors/blocklist/blocklist.html | 2 +- .../inlineblock/inlineblock.editor.html | 28 +++++------ .../labelblock/labelblock.editor.html | 18 ++++--- .../umb-block-list-property-editor.html | 47 ++----------------- .../blocklist/umb-block-list-row.html | 45 ++++++++++++++++++ .../umbBlockListPropertyEditor.component.js | 24 ++++++++-- ...nent.js => umbblocklistblock.component.js} | 4 +- .../blocklist/umbblocklistrow.component.js | 34 ++++++++++++++ ...s => umbblocklistscopedblock.component.js} | 4 +- 11 files changed, 146 insertions(+), 82 deletions(-) create mode 100644 src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/umb-block-list-row.html rename src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/{umb-block-list-block.component.js => umbblocklistblock.component.js} (92%) create mode 100644 src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/umbblocklistrow.component.js rename src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/{umb-block-list-scoped-block.component.js => umbblocklistscopedblock.component.js} (94%) diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbeditors.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbeditors.directive.js index e8e22518b3..20fba6eb6e 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbeditors.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/editor/umbeditors.directive.js @@ -137,7 +137,7 @@ // This directive allows for us to run a custom $compile for the view within the repeater which allows // us to maintain a $scope hierarchy with the rendered view based on the $scope that initiated the // infinite editing. The retain the $scope hiearchy a special $parentScope property is passed in to the model. - function EditorRepeaterDirective($http, $templateCache, $compile) { + function EditorRepeaterDirective($http, $templateCache, $compile, angularHelper) { function link(scope, el, attr, ctrl) { var editor = scope && scope.$parent ? scope.$parent.model : null; @@ -159,14 +159,24 @@ }); // NOTE: the 'model' name here directly affects the naming convention used in infinite editors, this why you access the model - // like $scope.model.If this is changed, everything breaks.This is because we are entirely reliant upon ng - include and inheriting $scopes. + // like $scope.model.If this is changed, everything breaks.This is because we are entirely reliant upon ng-include and inheriting $scopes. // by default without a $parentScope used for infinite editing the 'model' propety will be set because the view creates the scopes in // ng-repeat by ng-repeat="model in editors" templateScope.model = editor; - - element.html(response.data); + element.show(); - $compile(element.contents())(templateScope); + + // if a parentForm is supplied then we can link them but to do that we need to inject a top level form + if (editor.$parentForm) { + element.html("" + response.data + ""); + } + + $compile(element)(templateScope); + + // if a parentForm is supplied then we can link them + if (editor.$parentForm) { + editor.$parentForm.$addControl(templateScope.infiniteEditorForm); + } }); } diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/overlays/umboverlay.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/overlays/umboverlay.directive.js index f595a14664..d1bdf6f42f 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/overlays/umboverlay.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/overlays/umboverlay.directive.js @@ -280,7 +280,7 @@ Opens an overlay to show a custom YSOD.
        templateScope.model = scope.model; element.html(response.data); element.show(); - $compile(element.contents())(templateScope); + $compile(element)(templateScope); }); } } diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.html index efadc4dfd6..8c3bced573 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklist.html @@ -1 +1 @@ - + diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklistentryeditors/inlineblock/inlineblock.editor.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklistentryeditors/inlineblock/inlineblock.editor.html index a76894ac51..edf234455e 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklistentryeditors/inlineblock/inlineblock.editor.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklistentryeditors/inlineblock/inlineblock.editor.html @@ -1,16 +1,14 @@ - -
        - -
        - -
        +
        + +
        +
        - +
        diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklistentryeditors/labelblock/labelblock.editor.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklistentryeditors/labelblock/labelblock.editor.html index 6d3f139db2..712cd1eff8 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklistentryeditors/labelblock/labelblock.editor.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklistentryeditors/labelblock/labelblock.editor.html @@ -1,10 +1,8 @@ - - - + diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/umb-block-list-property-editor.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/umb-block-list-property-editor.html index 4f60b5d567..fb94a00631 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/umb-block-list-property-editor.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/umb-block-list-property-editor.html @@ -18,49 +18,10 @@
        +
        -
        - - - - - - -
        - - - -
        -
        + +
        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 new file mode 100644 index 0000000000..475c29b96a --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/umb-block-list-row.html @@ -0,0 +1,45 @@ + +
        + + + + + + +
        + + + +
        +
        +
        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 b5907126f2..da4fc9cff8 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 @@ -17,10 +17,10 @@ controller: BlockListController, controllerAs: "vm", bindings: { - model: "=", - propertyForm: "=" + model: "=" }, require: { + propertyForm: "^form", umbProperty: "?^umbProperty", umbVariantContent: '?^^umbVariantContent', umbVariantContentEditors: '?^^umbVariantContentEditors', @@ -28,7 +28,7 @@ } }); - function BlockListController($scope, editorService, clipboardService, localizationService, overlayService, blockEditorService, udiService, serverValidationManager) { + function BlockListController($scope, editorService, clipboardService, localizationService, overlayService, blockEditorService, udiService, serverValidationManager, angularHelper) { var unsubscribe = []; var modelObject; @@ -65,6 +65,16 @@ vm.$onInit = function() { + if (!vm.umbVariantContent) { + // not found, then fallback to searching the scope chain, this may be needed when DOM inheritance isn't maintained but scope + // inheritance is (i.e.infinite editing) + var found = angularHelper.traverseScopeChain($scope, s => s && s.vm && s.vm.constructor.name === "umbVariantContentController"); + vm.umbVariantContent = found ? found.vm : null; + if (!vm.umbVariantContent) { + throw "Could not find umbVariantContent in the $scope chain"; + } + } + // set the onValueChanged callback, this will tell us if the block list model changed on the server // once the data is submitted. If so we need to re-initialize vm.model.onValueChanged = onServerValueChanged; @@ -258,7 +268,7 @@ blockObject.active = true; } - function editBlock(blockObject, openSettings, blockIndex) { + function editBlock(blockObject, openSettings, blockIndex, parentForm) { // this must be set if (blockIndex === undefined) { @@ -289,6 +299,7 @@ var blockEditorModel = { $parentScope: $scope, // pass in a $parentScope, this maintains the scope inheritance in infinite editing + $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, liveEditing: liveEditing, @@ -345,6 +356,8 @@ var amountOfAvailableTypes = vm.availableBlockTypes.length; var blockPickerModel = { + $parentScope: $scope, // pass in a $parentScope, this maintains the scope inheritance in infinite editing + $parentForm: vm.propertyForm, // pass in a $parentForm, this maintains the FormController hierarchy with the infinite editing view (if it contains a form) availableItems: vm.availableBlockTypes, title: vm.labels.grid_addElement, orderBy: "$index", @@ -378,7 +391,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); + editBlock(vm.layout[createIndex].$block, false, createIndex, blockPickerModel.$parentForm); } } } @@ -521,6 +534,7 @@ }); } + // TODO: We'll need to pass in a parentForm here too function openSettingsForBlock(block, blockIndex) { editBlock(block, true, blockIndex); } diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/umb-block-list-block.component.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/umbblocklistblock.component.js similarity index 92% rename from src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/umb-block-list-block.component.js rename to src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/umbblocklistblock.component.js index d2d9e98cc7..afe389ef6f 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/umb-block-list-block.component.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/umbblocklistblock.component.js @@ -18,7 +18,8 @@ view: "@", block: "=", api: "<", - index: "<" + index: "<", + parentForm: "<" } } ); @@ -32,6 +33,7 @@ $scope.block = model.block; $scope.api = model.api; $scope.index = model.index; + $scope.parentForm = model.parentForm; }; // We need to watch for changes on primitive types and upate the $scope values. diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/umbblocklistrow.component.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/umbblocklistrow.component.js new file mode 100644 index 0000000000..96a3648192 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/umbblocklistrow.component.js @@ -0,0 +1,34 @@ +(function () { + "use strict"; + + /** + * @ngdoc directive + * @name umbraco.directives.directive:umbBlockListRow + * @description + * renders each row for the block list editor + */ + + angular + .module("umbraco") + .component("umbBlockListRow", { + templateUrl: 'views/propertyeditors/blocklist/umb-block-list-row.html', + controller: BlockListRowController, + controllerAs: "vm", + bindings: { + blockEditorApi: "<", + layout: "<", + index: "<" + } + } + ); + + function BlockListRowController($scope) { + + var vm = this; + + vm.$onInit = function () { + + }; + } + +})(); diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/umb-block-list-scoped-block.component.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/umbblocklistscopedblock.component.js similarity index 94% rename from src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/umb-block-list-scoped-block.component.js rename to src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/umbblocklistscopedblock.component.js index 314ce1fcaa..2a6a185c0d 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/umb-block-list-scoped-block.component.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/umbblocklistscopedblock.component.js @@ -20,7 +20,8 @@ view: "@", block: "=", api: "<", - index: "<" + index: "<", + parentForm: "<" } } ); @@ -34,6 +35,7 @@ $scope.block = model.block; $scope.api = model.api; $scope.index = model.index; + $scope.parentForm = model.parentForm; var shadowRoot = $element[0].attachShadow({mode:'open'}); shadowRoot.innerHTML = ` From b57545a9b0aed6df2c836cb292d71a6843e7b240 Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 17 Jul 2020 13:37:36 +1000 Subject: [PATCH 342/377] No need for 2x the same component, we just change how we $compile, fixes up showing the validation border --- .../validation/valformmanager.directive.js | 4 +- .../inlineblock/inlineblock.editor.html | 2 +- .../labelblock/labelblock.editor.html | 2 +- .../blocklist/umb-block-list-row.html | 11 +--- .../umbBlockListPropertyEditor.component.js | 3 +- .../blocklist/umbblocklistblock.component.js | 38 ++++++++++-- .../umbblocklistscopedblock.component.js | 59 ------------------- 7 files changed, 40 insertions(+), 79 deletions(-) delete mode 100644 src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/umbblocklistscopedblock.component.js diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/validation/valformmanager.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/validation/valformmanager.directive.js index d91234e583..47f1145600 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/validation/valformmanager.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/validation/valformmanager.directive.js @@ -42,7 +42,7 @@ function valFormManager(serverValidationManager, $rootScope, $timeout, $location })); }; - this.showValidation = $scope.showValidation === true; + this.isShowingValidation = () => $scope.showValidation === true; this.notify = function () { notify($scope); @@ -136,7 +136,7 @@ function valFormManager(serverValidationManager, $rootScope, $timeout, $location var isSavingNewItem = false; //we should show validation if there are any msgs in the server validation collection - if (serverValidationManager.items.length > 0 || (parentFormMgr && parentFormMgr.showValidation)) { + if (serverValidationManager.items.length > 0 || (parentFormMgr && parentFormMgr.isShowingValidation())) { element.addClass(SHOW_VALIDATION_CLASS_NAME); scope.showValidation = true; notifySubView(); diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklistentryeditors/inlineblock/inlineblock.editor.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklistentryeditors/inlineblock/inlineblock.editor.html index edf234455e..360eeed8c0 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklistentryeditors/inlineblock/inlineblock.editor.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklistentryeditors/inlineblock/inlineblock.editor.html @@ -1,6 +1,6 @@
        + ng-class="{ '--error': parentForm.$invalid && valFormManager.isShowingValidation() }">
        - + From f7a831f05486d36e540870820c8175573d259b97 Mon Sep 17 00:00:00 2001 From: Shannon Date: Thu, 23 Jul 2020 16:03:35 +1000 Subject: [PATCH 351/377] Fixes some JS issues with new data format, streamlines the blockeditor data format serialization in c#, implements To/FromEditor methods --- .../Models/Blocks/BlockEditorData.cs | 68 ++--- .../Models/Blocks/BlockEditorDataConverter.cs | 17 +- .../Models/Blocks/BlockItemData.cs | 56 ++++ .../Blocks/BlockListEditorDataConverter.cs | 2 +- src/Umbraco.Core/Models/Blocks/BlockValue.cs | 18 ++ src/Umbraco.Core/Umbraco.Core.csproj | 2 + .../components/content/edit.controller.js | 2 +- .../blockeditormodelobject.service.js | 125 ++++---- .../services/servervalidationmgr.service.js | 1 - .../umbBlockListPropertyEditor.component.js | 27 +- .../BlockEditorPropertyEditor.cs | 274 +++++++++++++----- .../NestedContentPropertyEditor.cs | 20 +- .../ValueConverters/BlockEditorConverter.cs | 2 +- .../BlockListPropertyValueConverter.cs | 8 +- 14 files changed, 415 insertions(+), 207 deletions(-) create mode 100644 src/Umbraco.Core/Models/Blocks/BlockItemData.cs create mode 100644 src/Umbraco.Core/Models/Blocks/BlockValue.cs diff --git a/src/Umbraco.Core/Models/Blocks/BlockEditorData.cs b/src/Umbraco.Core/Models/Blocks/BlockEditorData.cs index ba9f22d945..5ee609b148 100644 --- a/src/Umbraco.Core/Models/Blocks/BlockEditorData.cs +++ b/src/Umbraco.Core/Models/Blocks/BlockEditorData.cs @@ -1,74 +1,44 @@ -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; +using Newtonsoft.Json.Linq; using System; using System.Collections.Generic; -using Umbraco.Core.Serialization; namespace Umbraco.Core.Models.Blocks { /// - /// Converted block data from json + /// Convertable block data from json /// public class BlockEditorData { + private readonly string _propertyEditorAlias; + public static BlockEditorData Empty { get; } = new BlockEditorData(); private BlockEditorData() { } - public BlockEditorData(JToken layout, - IReadOnlyList references, - IReadOnlyList contentData, - IReadOnlyList settingsData) + public BlockEditorData(string propertyEditorAlias, + IEnumerable references, + BlockValue blockValue) { - Layout = layout ?? throw new ArgumentNullException(nameof(layout)); - References = references ?? throw new ArgumentNullException(nameof(references)); - ContentData = contentData ?? throw new ArgumentNullException(nameof(contentData)); - SettingsData = settingsData ?? throw new ArgumentNullException(nameof(settingsData)); - } - - public JToken Layout { get; } - public IReadOnlyList References { get; } = new List(); - public IReadOnlyList ContentData { get; } = new List(); - public IReadOnlyList SettingsData { get; } = new List(); - - internal class BlockValue - { - [JsonProperty("layout")] - public IDictionary Layout { get; set; } - - [JsonProperty("contentData")] - public IEnumerable ContentData { get; set; } = new List(); - - [JsonProperty("settingsData")] - public IEnumerable SettingsData { get; set; } = new List(); + if (string.IsNullOrWhiteSpace(propertyEditorAlias)) + throw new ArgumentException($"'{nameof(propertyEditorAlias)}' cannot be null or whitespace", nameof(propertyEditorAlias)); + _propertyEditorAlias = propertyEditorAlias; + BlockValue = blockValue ?? throw new ArgumentNullException(nameof(blockValue)); + References = references != null ? new List(references) : throw new ArgumentNullException(nameof(references)); } /// - /// Represents a single block's data in raw form + /// Returns the layout for this specific property editor /// - public class BlockItemData - { - [JsonProperty("contentTypeKey")] - public Guid ContentTypeKey { get; set; } + public JToken Layout => BlockValue.Layout.TryGetValue(_propertyEditorAlias, out var layout) ? layout : null; - [JsonProperty("udi")] - [JsonConverter(typeof(UdiJsonConverter))] - public Udi Udi { get; set; } + /// + /// Returns the reference to the original BlockValue + /// + public BlockValue BlockValue { get; } - /// - /// The remaining properties will be serialized to a dictionary - /// - /// - /// The JsonExtensionDataAttribute is used to put the non-typed properties into a bucket - /// http://www.newtonsoft.com/json/help/html/DeserializeExtensionData.htm - /// NestedContent serializes to string, int, whatever eg - /// "stringValue":"Some String","numericValue":125,"otherNumeric":null - /// - [JsonExtensionData] - public Dictionary RawPropertyValues { get; set; } = new Dictionary(); - } + public List References { get; } = new List(); } } diff --git a/src/Umbraco.Core/Models/Blocks/BlockEditorDataConverter.cs b/src/Umbraco.Core/Models/Blocks/BlockEditorDataConverter.cs index f045a00401..22e364c0f8 100644 --- a/src/Umbraco.Core/Models/Blocks/BlockEditorDataConverter.cs +++ b/src/Umbraco.Core/Models/Blocks/BlockEditorDataConverter.cs @@ -1,9 +1,7 @@ using Newtonsoft.Json; using Newtonsoft.Json.Linq; using System.Linq; -using System; using System.Collections.Generic; -using static Umbraco.Core.Models.Blocks.BlockEditorData; namespace Umbraco.Core.Models.Blocks { @@ -20,21 +18,18 @@ namespace Umbraco.Core.Models.Blocks _propertyEditorAlias = propertyEditorAlias; } - public BlockEditorData Convert(string json) + public BlockEditorData Deserialize(string json) { var value = JsonConvert.DeserializeObject(json); if (value.Layout == null) return BlockEditorData.Empty; - if (!value.Layout.TryGetValue(_propertyEditorAlias, out var layout)) - return BlockEditorData.Empty; + var references = value.Layout.TryGetValue(_propertyEditorAlias, out var layout) + ? GetBlockReferences(layout) + : Enumerable.Empty(); - var references = GetBlockReferences(layout); - var contentData = value.ContentData.ToList(); - var settingsData = value.SettingsData.ToList(); - - return new BlockEditorData(layout, references, contentData, settingsData); + return new BlockEditorData(_propertyEditorAlias, references, value); } /// @@ -42,7 +37,7 @@ namespace Umbraco.Core.Models.Blocks /// /// /// - protected abstract IReadOnlyList GetBlockReferences(JToken jsonLayout); + protected abstract IEnumerable GetBlockReferences(JToken jsonLayout); } } diff --git a/src/Umbraco.Core/Models/Blocks/BlockItemData.cs b/src/Umbraco.Core/Models/Blocks/BlockItemData.cs new file mode 100644 index 0000000000..12a636771e --- /dev/null +++ b/src/Umbraco.Core/Models/Blocks/BlockItemData.cs @@ -0,0 +1,56 @@ +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using Umbraco.Core.Serialization; + +namespace Umbraco.Core.Models.Blocks +{ + /// + /// Represents a single block's data in raw form + /// + public class BlockItemData + { + [JsonProperty("contentTypeKey")] + public Guid ContentTypeKey { get; set; } + + /// + /// not serialized, manually set and used during internally + /// + [JsonIgnore] + public string ContentTypeAlias { get; set; } + + [JsonProperty("udi")] + [JsonConverter(typeof(UdiJsonConverter))] + public Udi Udi { get; set; } + + [JsonIgnore] + public Guid Key => Udi != null ? ((GuidUdi)Udi).Guid : throw new InvalidOperationException("No Udi assigned"); + + /// + /// The remaining properties will be serialized to a dictionary + /// + /// + /// The JsonExtensionDataAttribute is used to put the non-typed properties into a bucket + /// http://www.newtonsoft.com/json/help/html/DeserializeExtensionData.htm + /// NestedContent serializes to string, int, whatever eg + /// "stringValue":"Some String","numericValue":125,"otherNumeric":null + /// + [JsonExtensionData] + public Dictionary RawPropertyValues { get; set; } = new Dictionary(); + + /// + /// Used during deserialization to convert the raw property data into data with a property type context + /// + [JsonIgnore] + public IDictionary PropertyValues { get; set; } = new Dictionary(); + + /// + /// Used during deserialization to populate the property value/property type of a block item content property + /// + public class BlockPropertyValue + { + public object Value { get; set; } + public PropertyType PropertyType { get; set; } + } + } +} diff --git a/src/Umbraco.Core/Models/Blocks/BlockListEditorDataConverter.cs b/src/Umbraco.Core/Models/Blocks/BlockListEditorDataConverter.cs index 0dec05bc54..23f69922d9 100644 --- a/src/Umbraco.Core/Models/Blocks/BlockListEditorDataConverter.cs +++ b/src/Umbraco.Core/Models/Blocks/BlockListEditorDataConverter.cs @@ -13,7 +13,7 @@ namespace Umbraco.Core.Models.Blocks { } - protected override IReadOnlyList GetBlockReferences(JToken jsonLayout) + protected override IEnumerable GetBlockReferences(JToken jsonLayout) { var blockListLayout = jsonLayout.ToObject>(); return blockListLayout.Select(x => new ContentAndSettingsReference(x.ContentUdi, x.SettingsUdi)).ToList(); diff --git a/src/Umbraco.Core/Models/Blocks/BlockValue.cs b/src/Umbraco.Core/Models/Blocks/BlockValue.cs new file mode 100644 index 0000000000..4700ddfd3b --- /dev/null +++ b/src/Umbraco.Core/Models/Blocks/BlockValue.cs @@ -0,0 +1,18 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using System.Collections.Generic; + +namespace Umbraco.Core.Models.Blocks +{ + public class BlockValue + { + [JsonProperty("layout")] + public IDictionary Layout { get; set; } + + [JsonProperty("contentData")] + public List ContentData { get; set; } = new List(); + + [JsonProperty("settingsData")] + public List SettingsData { get; set; } = new List(); + } +} diff --git a/src/Umbraco.Core/Umbraco.Core.csproj b/src/Umbraco.Core/Umbraco.Core.csproj index d7a1251f20..73af567cbc 100755 --- a/src/Umbraco.Core/Umbraco.Core.csproj +++ b/src/Umbraco.Core/Umbraco.Core.csproj @@ -134,8 +134,10 @@ + + diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/content/edit.controller.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/content/edit.controller.js index 68b19abf08..e3c2913e4e 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/content/edit.controller.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/content/edit.controller.js @@ -517,7 +517,7 @@ } function handleHttpException(err) { - if (!err.status) { + if (err && !err.status) { $exceptionHandler(err); } } 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 7496c15b52..e3e1e31a53 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 @@ -25,7 +25,7 @@ if (!elementModel || !elementModel.variants || !elementModel.variants.length) { return; } var variant = elementModel.variants[0]; - + for (var t = 0; t < variant.tabs.length; t++) { var tab = variant.tabs[t]; @@ -36,7 +36,7 @@ } } } - + } /** @@ -44,11 +44,11 @@ * needs to stay simple to avoid deep watching. */ function mapToPropertyModel(elementModel, dataModel) { - + if (!elementModel || !elementModel.variants || !elementModel.variants.length) { return; } var variant = elementModel.variants[0]; - + for (var t = 0; t < variant.tabs.length; t++) { var tab = variant.tabs[t]; @@ -59,7 +59,7 @@ } } } - + } /** @@ -98,13 +98,13 @@ } } - + /** * Generate label for Block, uses either the labelInterpolator or falls back to the contentTypeName. * @param {Object} blockObject BlockObject to recive data values from. */ function getBlockLabel(blockObject) { - if(blockObject.labelInterpolator !== undefined) { + if (blockObject.labelInterpolator !== undefined) { // We are just using the data model, since its a key/value object that is live synced. (if we need to add additional vars, we could make a shallow copy and apply those.) return blockObject.labelInterpolator(blockObject.data); } @@ -133,7 +133,7 @@ // But we like to sync non-primitive values as well! Yes, and this does happen, just not through this code, but through the nature of JavaScript. // Non-primitive values act as references to the same data and are therefor synced. blockObject.__watchers.push(isolatedScope.$watch("blockObjects._" + blockObject.key + "." + field + ".variants[0].tabs[" + t + "].properties[" + p + "].value", watcherCreator(blockObject, prop))); - + // We also like to watch our data model to be able to capture changes coming from other places. if (forSettings === true) { blockObject.__watchers.push(isolatedScope.$watch("blockObjects._" + blockObject.key + "." + "settingsData" + "." + prop.alias, createLayoutSettingsModelWatcher(blockObject, prop))); @@ -151,8 +151,8 @@ /** * Used to create a prop watcher for the data in the property editor data model. */ - function createDataModelWatcher(blockObject, prop) { - return function() { + function createDataModelWatcher(blockObject, prop) { + return function () { if (prop.value !== blockObject.data[prop.alias]) { // sync data: @@ -165,8 +165,8 @@ /** * Used to create a prop watcher for the settings in the property editor data model. */ - function createLayoutSettingsModelWatcher(blockObject, prop) { - return function() { + function createLayoutSettingsModelWatcher(blockObject, prop) { + return function () { if (prop.value !== blockObject.settingsData[prop.alias]) { // sync data: prop.value = blockObject.settingsData[prop.alias]; @@ -177,8 +177,8 @@ /** * Used to create a scoped watcher for a content property on a blockObject. */ - function createContentModelPropWatcher(blockObject, prop) { - return function() { + function createContentModelPropWatcher(blockObject, prop) { + return function () { if (blockObject.data[prop.alias] !== prop.value) { // sync data: blockObject.data[prop.alias] = prop.value; @@ -191,8 +191,8 @@ /** * Used to create a scoped watcher for a settings property on a blockObject. */ - function createSettingsModelPropWatcher(blockObject, prop) { - return function() { + function createSettingsModelPropWatcher(blockObject, prop) { + return function () { if (blockObject.settingsData[prop.alias] !== prop.value) { // sync data: blockObject.settingsData[prop.alias] = prop.value; @@ -255,17 +255,17 @@ this.isolatedScope = scopeOfExistance.$new(true); this.isolatedScope.blockObjects = {}; - + this.__watchers.push(this.isolatedScope.$on("$destroy", this.destroy.bind(this))); this.__watchers.push(propertyEditorScope.$on("postFormSubmitting", this.sync.bind(this))); }; - + BlockEditorModelObject.prototype = { update: function (propertyModelValue, propertyEditorScope) { // clear watchers - this.__watchers.forEach(w => { w(); }); + this.__watchers.forEach(w => { w(); }); delete this.__watchers; // clear block objects @@ -293,7 +293,7 @@ * @param {string} key contentTypeKey to recive the configuration model for. * @returns {Object | null} Configuration model for the that specific block. Or ´null´ if the contentTypeKey isnt available in the current block configurations. */ - getBlockConfiguration: function(key) { + getBlockConfiguration: function (key) { return this.blockConfigurations.find(bc => bc.contentTypeKey === key) || null; }, @@ -305,7 +305,7 @@ * @param {Object} blockObject BlockObject to receive data values from. * @returns {Promise} A Promise object which resolves when all scaffold models are loaded. */ - load: function() { + load: function () { var tasks = []; var scaffoldKeys = []; @@ -339,7 +339,7 @@ * @description Retrive a list of aliases that are available for content of blocks in this property editor, does not contain aliases of block settings. * @return {Array} array of strings representing alias. */ - getAvailableAliasesForBlockContent: function() { + getAvailableAliasesForBlockContent: function () { return this.blockConfigurations.map(blockConfiguration => this.getScaffoldFromKey(blockConfiguration.contentTypeKey).contentTypeAlias); }, @@ -351,13 +351,13 @@ * The purpose of this data is to provide it for the Block Picker. * @return {Array} array of objects representing available blocks, each object containing properties blockConfigModel and elementTypeModel. */ - getAvailableBlocksForBlockPicker: function() { + getAvailableBlocksForBlockPicker: function () { var blocks = []; this.blockConfigurations.forEach(blockConfiguration => { var scaffold = this.getScaffoldFromKey(blockConfiguration.contentTypeKey); - if(scaffold) { + if (scaffold) { blocks.push({ blockConfigModel: blockConfiguration, elementTypeModel: scaffold.documentType @@ -376,7 +376,7 @@ * @param {string} key contentTypeKey to recive the scaffold model for. * @returns {Object | null} Scaffold model for the that content type. Or null if the scaffolding model dosnt exist in this context. */ - getScaffoldFromKey: function(contentTypeKey) { + getScaffoldFromKey: function (contentTypeKey) { return this.scaffolds.find(o => o.contentTypeKey === contentTypeKey); }, @@ -388,7 +388,7 @@ * @param {string} alias contentTypeAlias to recive the scaffold model for. * @returns {Object | null} Scaffold model for the that content type. Or null if the scaffolding model dosnt exist in this context. */ - getScaffoldFromAlias: function(contentTypeAlias) { + getScaffoldFromAlias: function (contentTypeAlias) { return this.scaffolds.find(o => o.contentTypeAlias === contentTypeAlias); }, @@ -413,14 +413,14 @@ * @param {Object} layoutEntry the layout entry object to build the block model from. * @return {Object | null} The BlockObject for the given layout entry. Or null if data or configuration wasnt found for this block. */ - getBlockObject: function(layoutEntry) { + getBlockObject: function (layoutEntry) { - var udi = layoutEntry.udi; + var udi = layoutEntry.contentUdi; var dataModel = this._getDataByUdi(udi); if (dataModel === null) { - console.error("Couldnt find content model of " + udi) + console.error("Couldn't find content model of " + udi) return null; } @@ -428,11 +428,12 @@ var contentScaffold; if (blockConfiguration === null) { - console.error("The block entry of "+udi+" is not begin initialized cause its contentTypeKey is not allowed for this PropertyEditor"); - } else { + console.error("The block entry of " + udi + " is not being initialized because its contentTypeKey is not allowed for this PropertyEditor"); + } + else { contentScaffold = this.getScaffoldFromKey(blockConfiguration.contentTypeKey); - if(contentScaffold === null) { - console.error("The block entry of "+udi+" is not begin initialized cause its Element Type was not loaded."); + if (contentScaffold === null) { + console.error("The block entry of " + udi + " is not begin initialized cause its Element Type was not loaded."); } } @@ -443,12 +444,12 @@ unsupported: true }; contentScaffold = {}; - + } var blockObject = {}; // Set an angularJS cloneNode method, to avoid this object begin cloned. - blockObject.cloneNode = function() { + blockObject.cloneNode = function () { return null;// angularJS accept this as a cloned value as long as the } blockObject.key = String.CreateGuid().replace(/-/g, ""); @@ -465,7 +466,7 @@ this.__scope.$evalAsync(); } }.bind(blockObject) - , 10); + , 10); // make basics from scaffold blockObject.content = Utilities.copy(contentScaffold); @@ -505,7 +506,7 @@ } } - blockObject.retrieveValuesFrom = function(content, settings) { + blockObject.retrieveValuesFrom = function (content, settings) { if (this.content !== null) { mapElementValues(content, this.content); } @@ -545,7 +546,7 @@ delete this.settingsData; delete this.content; delete this.settings; - + // remove model from isolatedScope. delete this.__scope.blockObjects["_" + this.key]; // NOTE: It seems like we should call this.__scope.$destroy(); since that is the only way to remove a scope from it's parent, @@ -555,7 +556,7 @@ // removes this method, making it impossible to destroy again. delete this.destroy; - + // lets remove the key to make things blow up if this is still referenced: delete this.key; } @@ -580,7 +581,7 @@ } this.destroyBlockObject(blockObject); this.removeDataByUdi(udi); - if(settingsUdi) { + if (settingsUdi) { this.removeSettingsByUdi(settingsUdi); } }, @@ -592,7 +593,7 @@ * @description Destroys the Block Model, but all data is kept. * @param {Object} blockObject The BlockObject to be destroyed. */ - destroyBlockObject: function(blockObject) { + destroyBlockObject: function (blockObject) { blockObject.destroy(); }, @@ -604,13 +605,13 @@ * @param {object} defaultStructure if no data exist the layout of your poerty editor will be set to this object. * @return {Object} Layout object, structure depends on the model of your property editor. */ - getLayout: function(defaultStructure) { + getLayout: function (defaultStructure) { if (!this.value.layout[this.propertyEditorAlias]) { this.value.layout[this.propertyEditorAlias] = defaultStructure; } return this.value.layout[this.propertyEditorAlias]; }, - + /** * @ngdoc method * @name create @@ -619,21 +620,21 @@ * @param {string} contentTypeKey the contentTypeKey of the block you wish to create, if contentTypeKey is not avaiable in the block configuration then ´null´ will be returned. * @return {Object | null} Layout entry object, to be inserted at a decired location in the layout object. Or null if contentTypeKey is unavaiaible. */ - create: function(contentTypeKey) { - + create: function (contentTypeKey) { + var blockConfiguration = this.getBlockConfiguration(contentTypeKey); - if(blockConfiguration === null) { + if (blockConfiguration === null) { return null; } var entry = { - udi: this._createDataEntry(contentTypeKey) + contentUdi: this._createDataEntry(contentTypeKey) } if (blockConfiguration.settingsElementTypeKey != null) { entry.settingsUdi = this._createSettingsEntry(blockConfiguration.settingsElementTypeKey) } - + return entry; }, @@ -644,19 +645,19 @@ * @description Insert data from ElementType Model * @return {Object | null} Layout entry object, to be inserted at a decired location in the layout object. Or ´null´ if the given ElementType isnt supported by the block configuration. */ - createFromElementType: function(elementTypeDataModel) { + createFromElementType: function (elementTypeDataModel) { elementTypeDataModel = Utilities.copy(elementTypeDataModel); var contentTypeKey = elementTypeDataModel.contentTypeKey; var layoutEntry = this.create(contentTypeKey); - if(layoutEntry === null) { + if (layoutEntry === null) { return null; } var dataModel = this._getDataByUdi(layoutEntry.udi); - if(dataModel === null) { + if (dataModel === null) { return null; } @@ -672,7 +673,7 @@ * @methodOf umbraco.services.blockEditorModelObject * @description Force immidiate update of the blockobject models to the property model. */ - sync: function() { + sync: function () { for (const key in this.isolatedScope.blockObjects) { this.isolatedScope.blockObjects[key].sync(); } @@ -680,7 +681,7 @@ // private // TODO: Then this can just be a method in the outer scope - _createDataEntry: function(elementTypeKey) { + _createDataEntry: function (elementTypeKey) { var content = { contentTypeKey: elementTypeKey, udi: udiService.create("element") @@ -690,7 +691,7 @@ }, // private // TODO: Then this can just be a method in the outer scope - _getDataByUdi: function(udi) { + _getDataByUdi: function (udi) { return this.value.contentData.find(entry => entry.udi === udi) || null; }, @@ -699,10 +700,10 @@ * @name removeDataByUdi * @methodOf umbraco.services.blockEditorModelObject * @description Removes the content data of a given UDI. - * Notice this method does not remove the block from your layout, this will need to be handlede by the Property Editor since this services donst know about your layout structure. + * Notice this method does not remove the block from your layout, this will need to be handled by the Property Editor since this services don't know about your layout structure. * @param {string} udi The UDI of the content data to be removed. */ - removeDataByUdi: function(udi) { + removeDataByUdi: function (udi) { const index = this.value.contentData.findIndex(o => o.udi === udi); if (index !== -1) { this.value.contentData.splice(index, 1); @@ -711,7 +712,7 @@ // private // TODO: Then this can just be a method in the outer scope - _createSettingsEntry: function(elementTypeKey) { + _createSettingsEntry: function (elementTypeKey) { var settings = { contentTypeKey: elementTypeKey, udi: udiService.create("element") @@ -722,7 +723,7 @@ // private // TODO: Then this can just be a method in the outer scope - _getSettingsByUdi: function(udi) { + _getSettingsByUdi: function (udi) { return this.value.settingsData.find(entry => entry.udi === udi) || null; }, @@ -731,10 +732,10 @@ * @name removeSettingsByUdi * @methodOf umbraco.services.blockEditorModelObject * @description Removes the settings data of a given UDI. - * Notice this method does not remove the settingsUdi from your layout, this will need to be handlede by the Property Editor since this services donst know about your layout structure. + * Notice this method does not remove the settingsUdi from your layout, this will need to be handled by the Property Editor since this services don't know about your layout structure. * @param {string} udi The UDI of the settings data to be removed. */ - removeSettingsByUdi: function(udi) { + removeSettingsByUdi: function (udi) { const index = this.value.settingsData.findIndex(o => o.udi === udi); if (index !== -1) { this.value.settingsData.splice(index, 1); @@ -747,13 +748,13 @@ * @methodOf umbraco.services.blockEditorModelObject * @description Notice you should not need to destroy the BlockEditorModelObject since it will automaticly be destroyed when the scope of existance gets destroyed. */ - destroy: function() { + destroy: function () { this.__watchers.forEach(w => { w(); }); for (const key in this.isolatedScope.blockObjects) { this.destroyBlockObject(this.isolatedScope.blockObjects[key]); } - + delete this.__watchers; delete this.value; delete this.propertyEditorAlias; diff --git a/src/Umbraco.Web.UI.Client/src/common/services/servervalidationmgr.service.js b/src/Umbraco.Web.UI.Client/src/common/services/servervalidationmgr.service.js index b2fdf37aa4..7f8212f2c6 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/servervalidationmgr.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/servervalidationmgr.service.js @@ -504,7 +504,6 @@ function serverValidationManager($timeout) { // add a generic error for the property addPropertyError(propertyValidationKey, culture, htmlFieldReference, value && Array.isArray(value) && value.length > 0 ? value[0] : null, segment); - hasPropertyErrors = true; } else { 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 aa66472778..45ba6ddaa7 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 @@ -132,7 +132,13 @@ // Called when we save the value, the server may return an updated data and our value is re-synced // we need to deal with that here so that our model values are all in sync so we basically re-initialize. - function onServerValueChanged(newVal, oldVal) { + function onServerValueChanged(newVal, oldVal) { + + // We need to ensure that the property model value is an object, this is needed for modelObject to recive a reference and keep that updated. + if (typeof newVal !== 'object' || newVal === null) {// testing if we have null or undefined value or if the value is set to another type than Object. + newVal = {}; + } + modelObject.update(newVal, $scope); onLoaded(); } @@ -148,6 +154,8 @@ // Store a reference to the layout model, because we need to maintain this model. vm.layout = modelObject.getLayout([]); + var invalidLayoutItems = []; + // Append the blockObjects to our layout. vm.layout.forEach(entry => { // $block must have the data property to be a valid BlockObject, if not its considered as a destroyed blockObject. @@ -155,9 +163,22 @@ var block = getBlockObject(entry); // If this entry was not supported by our property-editor it would return 'null'. - if(block !== null) { + if (block !== null) { entry.$block = block; } + else { + // then we need to filter this out and also update the underlying model. This could happen if the data + // is invalid for some reason or the data structure has changed. + invalidLayoutItems.push(entry); + } + } + }); + + // remove the ones that are invalid + invalidLayoutItems.forEach(entry => { + var index = vm.layout.findIndex(x => x === entry); + if (index >= 0) { + vm.layout.splice(index, 1); } }); @@ -240,7 +261,7 @@ function deleteBlock(block) { - var layoutIndex = vm.layout.findIndex(entry => entry.udi === block.content.udi); + var layoutIndex = vm.layout.findIndex(entry => entry.contentUdi === block.content.udi); if (layoutIndex === -1) { throw new Error("Could not find layout entry of block with udi: "+block.content.udi) } diff --git a/src/Umbraco.Web/PropertyEditors/BlockEditorPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/BlockEditorPropertyEditor.cs index 866bd38e05..c20bd08204 100644 --- a/src/Umbraco.Web/PropertyEditors/BlockEditorPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/BlockEditorPropertyEditor.cs @@ -11,6 +11,7 @@ using Umbraco.Core.Models.Editors; using Umbraco.Core.PropertyEditors; using Umbraco.Core.Services; using static Umbraco.Core.Models.Blocks.BlockEditorData; +using static Umbraco.Core.Models.Blocks.BlockItemData; namespace Umbraco.Web.PropertyEditors { @@ -63,8 +64,12 @@ namespace Umbraco.Web.PropertyEditors var rawJson = value == null ? string.Empty : value is string str ? str : value.ToString(); var result = new List(); + var blockEditorData = _blockEditorValues.DeserializeAndClean(rawJson); + if (blockEditorData == null) + return Enumerable.Empty(); - foreach (var row in _blockEditorValues.GetPropertyValues(rawJson)) + // TODO: What about Settings? + foreach (var row in blockEditorData.BlockValue.ContentData) { foreach (var prop in row.PropertyValues) { @@ -83,6 +88,132 @@ namespace Umbraco.Web.PropertyEditors return result; } + + #region Convert database // editor + + // note: there is NO variant support here + + /// + /// Ensure that sub-editor values are translated through their ToEditor methods + /// + /// + /// + /// + /// + /// + public override object ToEditor(Property property, IDataTypeService dataTypeService, string culture = null, string segment = null) + { + var val = property.GetValue(culture, segment); + + BlockEditorData blockEditorData; + try + { + blockEditorData = _blockEditorValues.DeserializeAndClean(val); + } + catch (JsonSerializationException) + { + // if this occurs it means the data is invalid, shouldn't happen but has happened if we change the data format. + return string.Empty; + } + + if (blockEditorData == null || blockEditorData.BlockValue.ContentData.Count == 0) + return string.Empty; + + foreach (var row in blockEditorData.BlockValue.ContentData) + { + foreach (var prop in row.PropertyValues) + { + try + { + // create a temp property with the value + // - force it to be culture invariant as the block editor can't handle culture variant element properties + prop.Value.PropertyType.Variations = ContentVariation.Nothing; + var tempProp = new Property(prop.Value.PropertyType); + + tempProp.SetValue(prop.Value.Value); + + // convert that temp property, and store the converted value + var propEditor = _propertyEditors[prop.Value.PropertyType.PropertyEditorAlias]; + if (propEditor == null) + { + // NOTE: This logic was borrowed from Nested Content and I'm unsure why it exists. + // if the property editor doesn't exist I think everything will break anyways? + // update the raw value since this is what will get serialized out + row.RawPropertyValues[prop.Key] = tempProp.GetValue()?.ToString(); + continue; + } + + var tempConfig = dataTypeService.GetDataType(prop.Value.PropertyType.DataTypeId).Configuration; + var valEditor = propEditor.GetValueEditor(tempConfig); + var convValue = valEditor.ToEditor(tempProp, dataTypeService); + + // update the raw value since this is what will get serialized out + row.RawPropertyValues[prop.Key] = convValue; + } + catch (InvalidOperationException) + { + // deal with weird situations by ignoring them (no comment) + row.PropertyValues.Remove(prop.Key); + } + } + } + + // return json convertable object + return blockEditorData.BlockValue; + } + + /// + /// Ensure that sub-editor values are translated through their FromEditor methods + /// + /// + /// + /// + public override object FromEditor(ContentPropertyData editorValue, object currentValue) + { + if (editorValue.Value == null || string.IsNullOrWhiteSpace(editorValue.Value.ToString())) + return null; + + BlockEditorData blockEditorData; + try + { + blockEditorData = _blockEditorValues.DeserializeAndClean(editorValue.Value); + } + catch (JsonSerializationException) + { + // if this occurs it means the data is invalid, shouldn't happen but has happened if we change the data format. + return string.Empty; + } + + if (blockEditorData == null || blockEditorData.BlockValue.ContentData.Count == 0) + return string.Empty; + + foreach (var row in blockEditorData.BlockValue.ContentData) + { + foreach (var prop in row.PropertyValues) + { + // Fetch the property types prevalue + var propConfiguration = _dataTypeService.GetDataType(prop.Value.PropertyType.DataTypeId).Configuration; + + // Lookup the property editor + var propEditor = _propertyEditors[prop.Value.PropertyType.PropertyEditorAlias]; + if (propEditor == null) continue; + + // Create a fake content property data object + var contentPropData = new ContentPropertyData(prop.Value.Value, propConfiguration); + + // Get the property editor to do it's conversion + var newValue = propEditor.GetValueEditor().FromEditor(contentPropData, prop.Value.Value); + + // update the raw value since this is what will get serialized out + row.RawPropertyValues[prop.Key] = newValue; + } + } + + // return json + return JsonConvert.SerializeObject(blockEditorData.BlockValue); + } + + #endregion } internal class BlockEditorValidator : ComplexEditorValidator @@ -96,19 +227,26 @@ namespace Umbraco.Web.PropertyEditors protected override IEnumerable GetElementTypeValidation(object value) { - foreach (var row in _blockEditorValues.GetPropertyValues(value)) + var blockEditorData = _blockEditorValues.DeserializeAndClean(value); + if (blockEditorData != null) { - var elementValidation = new ElementTypeValidationModel(row.ContentTypeAlias, row.Id); - foreach (var prop in row.PropertyValues) + foreach (var row in blockEditorData.BlockValue.ContentData) { - elementValidation.AddPropertyTypeValidation( - new PropertyTypeValidationModel(prop.Value.PropertyType, prop.Value.Value)); + var elementValidation = new ElementTypeValidationModel(row.ContentTypeAlias, row.Key); + foreach (var prop in row.PropertyValues) + { + elementValidation.AddPropertyTypeValidation( + new PropertyTypeValidationModel(prop.Value.PropertyType, prop.Value.Value)); + } + yield return elementValidation; } - yield return elementValidation; } } } + /// + /// Used to deserialize json values and clean up any values based on the existence of element types and layout structure + /// internal class BlockEditorValues { private readonly Lazy> _contentTypes; @@ -126,81 +264,77 @@ namespace Umbraco.Web.PropertyEditors return contentType; } - public IReadOnlyList GetPropertyValues(object propertyValue) + public BlockEditorData DeserializeAndClean(object propertyValue) { if (propertyValue == null || string.IsNullOrWhiteSpace(propertyValue.ToString())) - return new List(); + return null; - var converted = _dataConverter.Convert(propertyValue.ToString()); + var blockEditorData = _dataConverter.Deserialize(propertyValue.ToString()); - if (converted.ContentData.Count == 0) - return new List(); - - var contentTypePropertyTypes = new Dictionary>(); - var result = new List(); - - foreach(var block in converted.ContentData) + if (blockEditorData.BlockValue.ContentData.Count == 0) { - var contentType = GetElementType(block); - if (contentType == null) - continue; - - // get the prop types for this content type but keep a dictionary of found ones so we don't have to keep re-looking and re-creating - // objects on each iteration. - if (!contentTypePropertyTypes.TryGetValue(contentType.Alias, out var propertyTypes)) - propertyTypes = contentTypePropertyTypes[contentType.Alias] = contentType.CompositionPropertyTypes.ToDictionary(x => x.Alias, x => x); - - var propValues = new Dictionary(); - - // find any keys that are not real property types and remove them - foreach (var prop in block.RawPropertyValues.ToList()) - { - // doesn't exist so remove it - if (!propertyTypes.TryGetValue(prop.Key, out var propType)) - { - block.RawPropertyValues.Remove(prop.Key); - } - else - { - // set the value to include the resolved property type - propValues[prop.Key] = new BlockPropertyValue - { - PropertyType = propType, - Value = prop.Value - }; - } - } - - result.Add(new BlockValue - { - ContentTypeAlias = contentType.Alias, - PropertyValues = propValues, - Id = ((GuidUdi)block.Udi).Guid - }); + // if there's no content ensure there's no settings too + blockEditorData.BlockValue.SettingsData.Clear(); + return null; } - return result; + var contentTypePropertyTypes = new Dictionary>(); + + // filter out any content that isn't referenced in the layout references + foreach(var block in blockEditorData.BlockValue.ContentData.Where(x => blockEditorData.References.Any(r => r.ContentUdi == x.Udi))) + { + ResolveBlockItemData(block, contentTypePropertyTypes); + } + // filter out any settings that isn't referenced in the layout references + foreach (var block in blockEditorData.BlockValue.SettingsData.Where(x => blockEditorData.References.Any(r => r.SettingsUdi == x.Udi))) + { + ResolveBlockItemData(block, contentTypePropertyTypes); + } + + // remove blocks that couldn't be resolved + blockEditorData.BlockValue.ContentData.RemoveAll(x => x.ContentTypeAlias.IsNullOrWhiteSpace()); + blockEditorData.BlockValue.SettingsData.RemoveAll(x => x.ContentTypeAlias.IsNullOrWhiteSpace()); + + return blockEditorData; } - /// - /// Used during deserialization to populate the property value/property type of a nested content row property - /// - internal class BlockPropertyValue + private bool ResolveBlockItemData(BlockItemData block, Dictionary> contentTypePropertyTypes) { - public object Value { get; set; } - public PropertyType PropertyType { get; set; } - } + var contentType = GetElementType(block); + if (contentType == null) + return false; - /// - /// Used during deserialization to populate the content type alias and property values of a block - /// - internal class BlockValue - { - public Guid Id { get; set; } - public string ContentTypeAlias { get; set; } - public IDictionary PropertyValues { get; set; } = new Dictionary(); - } + // get the prop types for this content type but keep a dictionary of found ones so we don't have to keep re-looking and re-creating + // objects on each iteration. + if (!contentTypePropertyTypes.TryGetValue(contentType.Alias, out var propertyTypes)) + propertyTypes = contentTypePropertyTypes[contentType.Alias] = contentType.CompositionPropertyTypes.ToDictionary(x => x.Alias, x => x); + var propValues = new Dictionary(); + + // find any keys that are not real property types and remove them + foreach (var prop in block.RawPropertyValues.ToList()) + { + // doesn't exist so remove it + if (!propertyTypes.TryGetValue(prop.Key, out var propType)) + { + block.RawPropertyValues.Remove(prop.Key); + } + else + { + // set the value to include the resolved property type + propValues[prop.Key] = new BlockPropertyValue + { + PropertyType = propType, + Value = prop.Value + }; + } + } + + block.ContentTypeAlias = contentType.Alias; + block.PropertyValues = propValues; + + return true; + } } #endregion diff --git a/src/Umbraco.Web/PropertyEditors/NestedContentPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/NestedContentPropertyEditor.cs index 4767dc19cd..ac9e0624bb 100644 --- a/src/Umbraco.Web/PropertyEditors/NestedContentPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/NestedContentPropertyEditor.cs @@ -133,13 +133,20 @@ namespace Umbraco.Web.PropertyEditors #endregion - + #region Convert database // editor // note: there is NO variant support here - // TODO: What does this do? + /// + /// Ensure that sub-editor values are translated through their ToEditor methods + /// + /// + /// + /// + /// + /// public override object ToEditor(Property property, IDataTypeService dataTypeService, string culture = null, string segment = null) { var val = property.GetValue(culture, segment); @@ -186,11 +193,16 @@ namespace Umbraco.Web.PropertyEditors } } - // return json + // return the object, there's a native json converter for this so it will serialize correctly return rows; } - // TODO: What does this do? + /// + /// Ensure that sub-editor values are translated through their FromEditor methods + /// + /// + /// + /// public override object FromEditor(ContentPropertyData editorValue, object currentValue) { if (editorValue.Value == null || string.IsNullOrWhiteSpace(editorValue.Value.ToString())) diff --git a/src/Umbraco.Web/PropertyEditors/ValueConverters/BlockEditorConverter.cs b/src/Umbraco.Web/PropertyEditors/ValueConverters/BlockEditorConverter.cs index 83c612e9f7..f043c8e66e 100644 --- a/src/Umbraco.Web/PropertyEditors/ValueConverters/BlockEditorConverter.cs +++ b/src/Umbraco.Web/PropertyEditors/ValueConverters/BlockEditorConverter.cs @@ -24,7 +24,7 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters } public IPublishedElement ConvertToElement( - BlockEditorData.BlockItemData data, + BlockItemData data, PropertyCacheLevel referenceCacheLevel, bool preview) { // hack! we need to cast, we have no choice beacuse we cannot make breaking changes. diff --git a/src/Umbraco.Web/PropertyEditors/ValueConverters/BlockListPropertyValueConverter.cs b/src/Umbraco.Web/PropertyEditors/ValueConverters/BlockListPropertyValueConverter.cs index 2b04106288..4d972f7a33 100644 --- a/src/Umbraco.Web/PropertyEditors/ValueConverters/BlockListPropertyValueConverter.cs +++ b/src/Umbraco.Web/PropertyEditors/ValueConverters/BlockListPropertyValueConverter.cs @@ -62,20 +62,20 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters if (string.IsNullOrWhiteSpace(value)) return BlockListModel.Empty; var converter = new BlockListEditorDataConverter(); - var converted = converter.Convert(value); - if (converted.ContentData.Count == 0) return BlockListModel.Empty; + var converted = converter.Deserialize(value); + if (converted.BlockValue.ContentData.Count == 0) return BlockListModel.Empty; var blockListLayout = converted.Layout.ToObject>(); // convert the content data - foreach (var data in converted.ContentData) + foreach (var data in converted.BlockValue.ContentData) { var element = _blockConverter.ConvertToElement(data, referenceCacheLevel, preview); if (element == null) continue; contentPublishedElements[element.Key] = element; } // convert the settings data - foreach (var data in converted.SettingsData) + foreach (var data in converted.BlockValue.SettingsData) { var element = _blockConverter.ConvertToElement(data, referenceCacheLevel, preview); if (element == null) continue; From f77961ca6ecb147682ab8348a3783cd7736a8874 Mon Sep 17 00:00:00 2001 From: Shannon Date: Thu, 23 Jul 2020 16:42:53 +1000 Subject: [PATCH 352/377] Fixes tests --- src/Umbraco.Tests/Published/NestedContentTests.cs | 2 +- src/Umbraco.Web/Editors/Binders/ContentItemBinder.cs | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Umbraco.Tests/Published/NestedContentTests.cs b/src/Umbraco.Tests/Published/NestedContentTests.cs index 7f499d479b..c5cee8843f 100644 --- a/src/Umbraco.Tests/Published/NestedContentTests.cs +++ b/src/Umbraco.Tests/Published/NestedContentTests.cs @@ -34,7 +34,7 @@ namespace Umbraco.Tests.Published var proflog = new ProfilingLogger(logger, profiler); PropertyEditorCollection editors = null; - var editor = new NestedContentPropertyEditor(logger, new Lazy(() => editors), Mock.Of(), Mock.Of()); + var editor = new NestedContentPropertyEditor(logger, new Lazy(() => editors), Mock.Of(), Mock.Of(), Mock.Of()); editors = new PropertyEditorCollection(new DataEditorCollection(new DataEditor[] { editor })); var dataType1 = new DataType(editor) diff --git a/src/Umbraco.Web/Editors/Binders/ContentItemBinder.cs b/src/Umbraco.Web/Editors/Binders/ContentItemBinder.cs index 609974bef7..378865218c 100644 --- a/src/Umbraco.Web/Editors/Binders/ContentItemBinder.cs +++ b/src/Umbraco.Web/Editors/Binders/ContentItemBinder.cs @@ -47,7 +47,6 @@ namespace Umbraco.Web.Editors.Binders internal static void BindModel(ContentItemSave model, IContent persistedContent) { if (model is null) throw new ArgumentNullException(nameof(model)); - if (persistedContent is null) throw new ArgumentNullException(nameof(persistedContent)); model.PersistedContent = persistedContent; From 41dc30227f89655c13177ba95ce7423f6525e087 Mon Sep 17 00:00:00 2001 From: Shannon Date: Thu, 23 Jul 2020 19:04:42 +1000 Subject: [PATCH 353/377] Fixes tests --- .../src/common/mocks/resources/_utils.js | 14 ++- .../services/servervalidationmgr.service.js | 1 - .../services/block-editor-service.spec.js | 97 +++++++++++-------- 3 files changed, 68 insertions(+), 44 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/mocks/resources/_utils.js b/src/Umbraco.Web.UI.Client/src/common/mocks/resources/_utils.js index 619e1b77c3..efa7046569 100644 --- a/src/Umbraco.Web.UI.Client/src/common/mocks/resources/_utils.js +++ b/src/Umbraco.Web.UI.Client/src/common/mocks/resources/_utils.js @@ -1,5 +1,5 @@ angular.module('umbraco.mocks'). - factory('mocksUtils', ['$cookies', function ($cookies) { + factory('mocksUtils', ['$cookies', 'udiService', function ($cookies, udiService) { 'use strict'; //by default we will perform authorization @@ -40,13 +40,17 @@ angular.module('umbraco.mocks'). }, /** Creats a mock content object */ - getMockContent: function(id) { + getMockContent: function (id, key, udi) { + key = key || String.CreateGuid(); + var udi = udi || udiService.build("content", key); var node = { name: "My content with id: " + id, updateDate: new Date().toIsoDateTimeString(), publishDate: new Date().toIsoDateTimeString(), createDate: new Date().toIsoDateTimeString(), id: id, + key: key, + udi: udi, parentId: 1234, icon: "icon-umb-content", owner: { name: "Administrator", id: 0 }, @@ -282,13 +286,17 @@ angular.module('umbraco.mocks'). /** Creats a mock variant content object */ - getMockVariantContent: function(id) { + getMockVariantContent: function(id, key, udi) { + key = key || String.CreateGuid(); + var udi = udi || udiService.build("content", key); var node = { name: "My content with id: " + id, updateDate: new Date().toIsoDateTimeString(), publishDate: new Date().toIsoDateTimeString(), createDate: new Date().toIsoDateTimeString(), id: id, + key: key, + udi: udi, parentId: 1234, icon: "icon-umb-content", owner: { name: "Administrator", id: 0 }, diff --git a/src/Umbraco.Web.UI.Client/src/common/services/servervalidationmgr.service.js b/src/Umbraco.Web.UI.Client/src/common/services/servervalidationmgr.service.js index b2fdf37aa4..7f8212f2c6 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/servervalidationmgr.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/servervalidationmgr.service.js @@ -504,7 +504,6 @@ function serverValidationManager($timeout) { // add a generic error for the property addPropertyError(propertyValidationKey, culture, htmlFieldReference, value && Array.isArray(value) && value.length > 0 ? value[0] : null, segment); - hasPropertyErrors = true; } else { diff --git a/src/Umbraco.Web.UI.Client/test/unit/common/services/block-editor-service.spec.js b/src/Umbraco.Web.UI.Client/test/unit/common/services/block-editor-service.spec.js index 7f69f0594e..07967dbb60 100644 --- a/src/Umbraco.Web.UI.Client/test/unit/common/services/block-editor-service.spec.js +++ b/src/Umbraco.Web.UI.Client/test/unit/common/services/block-editor-service.spec.js @@ -1,5 +1,8 @@ describe('blockEditorService tests', function () { + var key = "6A1F5BDD-67EF-4173-B061-D6348ED07094"; + var udi = "umb://element/6A1F5BDD67EF4173B061D6348ED07094"; + var blockEditorService, contentResource, $rootScope, $scope; beforeEach(module('umbraco.services')); @@ -17,7 +20,7 @@ contentResource = $injector.get("contentResource"); spyOn(contentResource, "getScaffoldByKey").and.callFake( function () { - return Promise.resolve(mocksUtils.getMockVariantContent(1234)) + return Promise.resolve(mocksUtils.getMockVariantContent(1234, key, udi)) } ); @@ -32,13 +35,13 @@ layout: { "Umbraco.TestBlockEditor": [ { - udi: 1234 + udi: udi } ] }, data: [ { - udi: 1234, + udi: udi, contentTypeKey: "7C5B74D1-E2F9-45A3-AE4B-FC7A829BF8AB", testproperty: "myTestValue" } @@ -105,15 +108,19 @@ modelObject.load().then(() => { - var layout = modelObject.getLayout(); + try { + var layout = modelObject.getLayout(); - var blockObject = modelObject.getBlockObject(layout[0]); + var blockObject = modelObject.getBlockObject(layout[0]); - expect(blockObject).not.toBeUndefined(); - expect(blockObject.data.udi).toBe(propertyModelMock.data[0].udi); - expect(blockObject.content.variants[0].tabs[0].properties[0].value).toBe(propertyModelMock.data[0].testproperty); + expect(blockObject).not.toBeUndefined(); + expect(blockObject.data.udi).toBe(propertyModelMock.data[0].udi); + expect(blockObject.content.variants[0].tabs[0].properties[0].value).toBe(propertyModelMock.data[0].testproperty); - done(); + done(); + } catch (e) { + done.fail(e); + } }); }); @@ -127,21 +134,23 @@ modelObject.load().then(() => { - var layout = modelObject.getLayout(); + try { + var layout = modelObject.getLayout(); - var blockObject = modelObject.getBlockObject(layout[0]); + var blockObject = modelObject.getBlockObject(layout[0]); - blockObject.content.variants[0].tabs[0].properties[0].value = "anotherTestValue"; + blockObject.content.variants[0].tabs[0].properties[0].value = "anotherTestValue"; - $rootScope.$digest();// invoke angularJS Store. + $rootScope.$digest();// invoke angularJS Store. - expect(blockObject.data).toBe(propertyModel.data[0]); - expect(blockObject.data.testproperty).toBe("anotherTestValue"); - expect(propertyModel.data[0].testproperty).toBe("anotherTestValue"); + expect(blockObject.data).toBe(propertyModel.data[0]); + expect(blockObject.data.testproperty).toBe("anotherTestValue"); + expect(propertyModel.data[0].testproperty).toBe("anotherTestValue"); - // - - done(); + done(); + } catch (e) { + done.fail(e); + } }); }); @@ -159,19 +168,23 @@ modelObject.load().then(() => { - var layout = modelObject.getLayout(); + try { + var layout = modelObject.getLayout(); - var blockObject = modelObject.getBlockObject(layout[0]); + var blockObject = modelObject.getBlockObject(layout[0]); - blockObject.content.variants[0].tabs[0].properties[0].value.list[0] = "AA"; - blockObject.content.variants[0].tabs[0].properties[0].value.list.push("D"); + blockObject.content.variants[0].tabs[0].properties[0].value.list[0] = "AA"; + blockObject.content.variants[0].tabs[0].properties[0].value.list.push("D"); - $rootScope.$digest();// invoke angularJS Store. + $rootScope.$digest();// invoke angularJS Store. - expect(propertyModel.data[0].testproperty.list[0]).toBe("AA"); - expect(propertyModel.data[0].testproperty.list.length).toBe(4); + expect(propertyModel.data[0].testproperty.list[0]).toBe("AA"); + expect(propertyModel.data[0].testproperty.list.length).toBe(4); - done(); + done(); + } catch (e) { + done.fail(e); + } }); }); @@ -205,25 +218,29 @@ modelObject.load().then(() => { - var layout = modelObject.getLayout(); + try { + var layout = modelObject.getLayout(); - var blockObject = modelObject.getBlockObject(layout[0]); + var blockObject = modelObject.getBlockObject(layout[0]); - expect(blockObject).not.toBeUndefined(); - expect(blockObject).not.toBe(null); + expect(blockObject).not.toBeUndefined(); + expect(blockObject).not.toBe(null); - // remove from layout; - layout.splice(0, 1); + // remove from layout; + layout.splice(0, 1); - // remove from data; - modelObject.removeDataAndDestroyModel(blockObject); + // remove from data; + modelObject.removeDataAndDestroyModel(blockObject); - expect(propertyModel.data.length).toBe(0); - expect(propertyModel.data[0]).toBeUndefined(); - expect(propertyModel.layout["Umbraco.TestBlockEditor"].length).toBe(0); - expect(propertyModel.layout["Umbraco.TestBlockEditor"][0]).toBeUndefined(); + expect(propertyModel.data.length).toBe(0); + expect(propertyModel.data[0]).toBeUndefined(); + expect(propertyModel.layout["Umbraco.TestBlockEditor"].length).toBe(0); + expect(propertyModel.layout["Umbraco.TestBlockEditor"][0]).toBeUndefined(); - done(); + done(); + } catch (e) { + done.fail(e); + } }); }); From e01f36514b70c7bf4132ded5568834eafe025a75 Mon Sep 17 00:00:00 2001 From: Shannon Date: Thu, 23 Jul 2020 22:14:06 +1000 Subject: [PATCH 354/377] fixes tests --- src/Umbraco.Core/Models/Blocks/BlockEditorData.cs | 1 + .../ValueConverters/BlockListPropertyValueConverter.cs | 5 +++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Core/Models/Blocks/BlockEditorData.cs b/src/Umbraco.Core/Models/Blocks/BlockEditorData.cs index 5ee609b148..d8186f0dfa 100644 --- a/src/Umbraco.Core/Models/Blocks/BlockEditorData.cs +++ b/src/Umbraco.Core/Models/Blocks/BlockEditorData.cs @@ -16,6 +16,7 @@ namespace Umbraco.Core.Models.Blocks private BlockEditorData() { + BlockValue = new BlockValue(); } public BlockEditorData(string propertyEditorAlias, diff --git a/src/Umbraco.Web/PropertyEditors/ValueConverters/BlockListPropertyValueConverter.cs b/src/Umbraco.Web/PropertyEditors/ValueConverters/BlockListPropertyValueConverter.cs index 4d972f7a33..b04ef2d444 100644 --- a/src/Umbraco.Web/PropertyEditors/ValueConverters/BlockListPropertyValueConverter.cs +++ b/src/Umbraco.Web/PropertyEditors/ValueConverters/BlockListPropertyValueConverter.cs @@ -18,11 +18,13 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters { private readonly IProfilingLogger _proflog; private readonly BlockEditorConverter _blockConverter; + private readonly BlockListEditorDataConverter _blockListEditorDataConverter; public BlockListPropertyValueConverter(IProfilingLogger proflog, BlockEditorConverter blockConverter) { _proflog = proflog; _blockConverter = blockConverter; + _blockListEditorDataConverter = new BlockListEditorDataConverter(); } /// @@ -61,8 +63,7 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters var value = (string)inter; if (string.IsNullOrWhiteSpace(value)) return BlockListModel.Empty; - var converter = new BlockListEditorDataConverter(); - var converted = converter.Deserialize(value); + var converted = _blockListEditorDataConverter.Deserialize(value); if (converted.BlockValue.ContentData.Count == 0) return BlockListModel.Empty; var blockListLayout = converted.Layout.ToObject>(); From 06afab98935493eacdfa63b4baff96ea9894a219 Mon Sep 17 00:00:00 2001 From: Shannon Date: Thu, 23 Jul 2020 23:04:11 +1000 Subject: [PATCH 355/377] Adds some warning logs for when data is removed --- src/Umbraco.Web.UI/Umbraco.Web.UI.csproj | 9 +-- .../BlockEditorPropertyEditor.cs | 67 +++++++++++-------- .../NestedContentPropertyEditor.cs | 20 ++++-- 3 files changed, 54 insertions(+), 42 deletions(-) diff --git a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj index 87b47a55c3..477fb942be 100644 --- a/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj +++ b/src/Umbraco.Web.UI/Umbraco.Web.UI.csproj @@ -126,10 +126,6 @@ {52ac0ba8-a60e-4e36-897b-e8b97a54ed1c} Umbraco.ModelsBuilder.Embedded - - {fb5676ed-7a69-492c-b802-e7b24144c0fc} - Umbraco.TestData - {651e1350-91b6-44b7-bd60-7207006d7003} Umbraco.Web @@ -352,10 +348,7 @@ True 8700 / - http://localhost:8700 - 8640 - / - http://localhost:8640 + http://localhost:8700 False False diff --git a/src/Umbraco.Web/PropertyEditors/BlockEditorPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/BlockEditorPropertyEditor.cs index c20bd08204..8bf45e5313 100644 --- a/src/Umbraco.Web/PropertyEditors/BlockEditorPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/BlockEditorPropertyEditor.cs @@ -42,20 +42,22 @@ namespace Umbraco.Web.PropertyEditors #region Value Editor - protected override IDataValueEditor CreateValueEditor() => new BlockEditorPropertyValueEditor(Attribute, PropertyEditors, _dataTypeService, _contentTypeService, _localizedTextService); + protected override IDataValueEditor CreateValueEditor() => new BlockEditorPropertyValueEditor(Attribute, PropertyEditors, _dataTypeService, _contentTypeService, _localizedTextService, Logger); internal class BlockEditorPropertyValueEditor : DataValueEditor, IDataValueReference { private readonly PropertyEditorCollection _propertyEditors; private readonly IDataTypeService _dataTypeService; // TODO: Not used yet but we'll need it to fill in the FromEditor/ToEditor + private readonly ILogger _logger; private readonly BlockEditorValues _blockEditorValues; - public BlockEditorPropertyValueEditor(DataEditorAttribute attribute, PropertyEditorCollection propertyEditors, IDataTypeService dataTypeService, IContentTypeService contentTypeService, ILocalizedTextService textService) + public BlockEditorPropertyValueEditor(DataEditorAttribute attribute, PropertyEditorCollection propertyEditors, IDataTypeService dataTypeService, IContentTypeService contentTypeService, ILocalizedTextService textService, ILogger logger) : base(attribute) { _propertyEditors = propertyEditors; _dataTypeService = dataTypeService; - _blockEditorValues = new BlockEditorValues(new BlockListEditorDataConverter(), contentTypeService); + _logger = logger; + _blockEditorValues = new BlockEditorValues(new BlockListEditorDataConverter(), contentTypeService, _logger); Validators.Add(new BlockEditorValidator(_blockEditorValues, propertyEditors, dataTypeService, textService)); } @@ -123,38 +125,41 @@ namespace Umbraco.Web.PropertyEditors { foreach (var prop in row.PropertyValues) { - try + // create a temp property with the value + // - force it to be culture invariant as the block editor can't handle culture variant element properties + prop.Value.PropertyType.Variations = ContentVariation.Nothing; + var tempProp = new Property(prop.Value.PropertyType); + + tempProp.SetValue(prop.Value.Value); + + // convert that temp property, and store the converted value + var propEditor = _propertyEditors[prop.Value.PropertyType.PropertyEditorAlias]; + if (propEditor == null) { - // create a temp property with the value - // - force it to be culture invariant as the block editor can't handle culture variant element properties - prop.Value.PropertyType.Variations = ContentVariation.Nothing; - var tempProp = new Property(prop.Value.PropertyType); - - tempProp.SetValue(prop.Value.Value); - - // convert that temp property, and store the converted value - var propEditor = _propertyEditors[prop.Value.PropertyType.PropertyEditorAlias]; - if (propEditor == null) - { - // NOTE: This logic was borrowed from Nested Content and I'm unsure why it exists. - // if the property editor doesn't exist I think everything will break anyways? - // update the raw value since this is what will get serialized out - row.RawPropertyValues[prop.Key] = tempProp.GetValue()?.ToString(); - continue; - } - - var tempConfig = dataTypeService.GetDataType(prop.Value.PropertyType.DataTypeId).Configuration; - var valEditor = propEditor.GetValueEditor(tempConfig); - var convValue = valEditor.ToEditor(tempProp, dataTypeService); - + // NOTE: This logic was borrowed from Nested Content and I'm unsure why it exists. + // if the property editor doesn't exist I think everything will break anyways? // update the raw value since this is what will get serialized out - row.RawPropertyValues[prop.Key] = convValue; + row.RawPropertyValues[prop.Key] = tempProp.GetValue()?.ToString(); + continue; } - catch (InvalidOperationException) + + var dataType = dataTypeService.GetDataType(prop.Value.PropertyType.DataTypeId); + if (dataType == null) { // deal with weird situations by ignoring them (no comment) row.PropertyValues.Remove(prop.Key); + _logger.Warn( + "ToEditor removed property value {PropertyKey} in row {RowId} for property type {PropertyTypeAlias}", + prop.Key, row.Key, property.PropertyType.Alias); + continue; } + + var tempConfig = dataType.Configuration; + var valEditor = propEditor.GetValueEditor(tempConfig); + var convValue = valEditor.ToEditor(tempProp, dataTypeService); + + // update the raw value since this is what will get serialized out + row.RawPropertyValues[prop.Key] = convValue; } } @@ -251,11 +256,13 @@ namespace Umbraco.Web.PropertyEditors { private readonly Lazy> _contentTypes; private readonly BlockEditorDataConverter _dataConverter; + private readonly ILogger _logger; - public BlockEditorValues(BlockEditorDataConverter dataConverter, IContentTypeService contentTypeService) + public BlockEditorValues(BlockEditorDataConverter dataConverter, IContentTypeService contentTypeService, ILogger logger) { _contentTypes = new Lazy>(() => contentTypeService.GetAll().ToDictionary(c => c.Key)); _dataConverter = dataConverter; + _logger = logger; } private IContentType GetElementType(BlockItemData item) @@ -318,6 +325,8 @@ namespace Umbraco.Web.PropertyEditors if (!propertyTypes.TryGetValue(prop.Key, out var propType)) { block.RawPropertyValues.Remove(prop.Key); + _logger.Warn("The property {PropertyKey} for block {BlockKey} was removed because the property type {PropertyTypeAlias} was not found on {ContentTypeAlias}", + prop.Key, block.Key, prop.Key, contentType.Alias); } else { diff --git a/src/Umbraco.Web/PropertyEditors/NestedContentPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/NestedContentPropertyEditor.cs index ac9e0624bb..97a2948626 100644 --- a/src/Umbraco.Web/PropertyEditors/NestedContentPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/NestedContentPropertyEditor.cs @@ -25,7 +25,7 @@ namespace Umbraco.Web.PropertyEditors [DataEditor( Constants.PropertyEditors.Aliases.NestedContent, "Nested Content", - "nestedcontent", + "nestedcontent", ValueType = ValueTypes.Json, Group = Constants.PropertyEditors.Groups.Lists, Icon = "icon-thumbnail-list")] @@ -61,19 +61,21 @@ namespace Umbraco.Web.PropertyEditors #region Value Editor - protected override IDataValueEditor CreateValueEditor() => new NestedContentPropertyValueEditor(Attribute, PropertyEditors, _dataTypeService, _contentTypeService, _localizedTextService); + protected override IDataValueEditor CreateValueEditor() => new NestedContentPropertyValueEditor(Attribute, PropertyEditors, _dataTypeService, _contentTypeService, _localizedTextService, Logger); internal class NestedContentPropertyValueEditor : DataValueEditor, IDataValueReference { private readonly PropertyEditorCollection _propertyEditors; private readonly IDataTypeService _dataTypeService; + private readonly ILogger _logger; private readonly NestedContentValues _nestedContentValues; - public NestedContentPropertyValueEditor(DataEditorAttribute attribute, PropertyEditorCollection propertyEditors, IDataTypeService dataTypeService, IContentTypeService contentTypeService, ILocalizedTextService textService) + public NestedContentPropertyValueEditor(DataEditorAttribute attribute, PropertyEditorCollection propertyEditors, IDataTypeService dataTypeService, IContentTypeService contentTypeService, ILocalizedTextService textService, ILogger logger) : base(attribute) { _propertyEditors = propertyEditors; _dataTypeService = dataTypeService; + _logger = logger; _nestedContentValues = new NestedContentValues(contentTypeService); Validators.Add(new NestedContentValidator(_nestedContentValues, propertyEditors, dataTypeService, textService)); } @@ -120,10 +122,14 @@ namespace Umbraco.Web.PropertyEditors // update the raw value since this is what will get serialized out row.RawPropertyValues[prop.Key] = convValue; } - catch (InvalidOperationException) + catch (InvalidOperationException ex) { // deal with weird situations by ignoring them (no comment) row.RawPropertyValues.Remove(prop.Key); + _logger.Warn( + ex, + "ConvertDbToString removed property value {PropertyKey} in row {RowId} for property type {PropertyTypeAlias}", + prop.Key, row.Id, propertyType.Alias); } } } @@ -185,10 +191,14 @@ namespace Umbraco.Web.PropertyEditors // update the raw value since this is what will get serialized out row.RawPropertyValues[prop.Key] = convValue == null ? null : JToken.FromObject(convValue); } - catch (InvalidOperationException) + catch (InvalidOperationException ex) { // deal with weird situations by ignoring them (no comment) row.RawPropertyValues.Remove(prop.Key); + _logger.Warn( + ex, + "ToEditor removed property value {PropertyKey} in row {RowId} for property type {PropertyTypeAlias}", + prop.Key, row.Id, property.PropertyType.Alias); } } } From ad9d0fad4f94fb52e026b6f52cd33bcbde5c6495 Mon Sep 17 00:00:00 2001 From: Shannon Date: Thu, 23 Jul 2020 23:06:58 +1000 Subject: [PATCH 356/377] Adds notes with links back to PR about server side validation --- .../src/common/services/servervalidationmgr.service.js | 4 ++++ .../Validation/ComplexEditorElementTypeValidationResult.cs | 4 ++++ .../Validation/ComplexEditorPropertyTypeValidationResult.cs | 4 ++++ .../Validation/ComplexEditorValidationResult.cs | 5 ++++- .../Validation/ContentPropertyValidationResult.cs | 5 ++++- .../PropertyEditors/Validation/ValidationResultConverter.cs | 3 +++ 6 files changed, 23 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/services/servervalidationmgr.service.js b/src/Umbraco.Web.UI.Client/src/common/services/servervalidationmgr.service.js index 7f8212f2c6..2a889d676d 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/servervalidationmgr.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/servervalidationmgr.service.js @@ -7,6 +7,10 @@ * Used to handle server side validation and wires up the UI with the messages. There are 2 types of validation messages, one * is for user defined properties (called Properties) and the other is for field properties which are attached to the native * model objects (not user defined). The methods below are named according to these rules: Properties vs Fields. + * + * For a more indepth explanation of how server side validation works with the angular app, see this GitHub PR: + * https://github.com/umbraco/Umbraco-CMS/pull/8339 + * */ function serverValidationManager($timeout) { diff --git a/src/Umbraco.Web/PropertyEditors/Validation/ComplexEditorElementTypeValidationResult.cs b/src/Umbraco.Web/PropertyEditors/Validation/ComplexEditorElementTypeValidationResult.cs index 17a1433db5..9c68deb7b5 100644 --- a/src/Umbraco.Web/PropertyEditors/Validation/ComplexEditorElementTypeValidationResult.cs +++ b/src/Umbraco.Web/PropertyEditors/Validation/ComplexEditorElementTypeValidationResult.cs @@ -7,6 +7,10 @@ namespace Umbraco.Web.PropertyEditors.Validation /// /// A collection of for an element type within complex editor represented by an Element Type /// + /// + /// For a more indepth explanation of how server side validation works with the angular app, see this GitHub PR: + /// https://github.com/umbraco/Umbraco-CMS/pull/8339 + /// public class ComplexEditorElementTypeValidationResult : ValidationResult { public ComplexEditorElementTypeValidationResult(string elementTypeAlias, Guid blockId) diff --git a/src/Umbraco.Web/PropertyEditors/Validation/ComplexEditorPropertyTypeValidationResult.cs b/src/Umbraco.Web/PropertyEditors/Validation/ComplexEditorPropertyTypeValidationResult.cs index 1872648f54..3036529fb7 100644 --- a/src/Umbraco.Web/PropertyEditors/Validation/ComplexEditorPropertyTypeValidationResult.cs +++ b/src/Umbraco.Web/PropertyEditors/Validation/ComplexEditorPropertyTypeValidationResult.cs @@ -8,6 +8,10 @@ namespace Umbraco.Web.PropertyEditors.Validation /// /// A collection of for a property type within a complex editor represented by an Element Type /// + /// + /// For a more indepth explanation of how server side validation works with the angular app, see this GitHub PR: + /// https://github.com/umbraco/Umbraco-CMS/pull/8339 + /// public class ComplexEditorPropertyTypeValidationResult : ValidationResult { public ComplexEditorPropertyTypeValidationResult(string propertyTypeAlias) diff --git a/src/Umbraco.Web/PropertyEditors/Validation/ComplexEditorValidationResult.cs b/src/Umbraco.Web/PropertyEditors/Validation/ComplexEditorValidationResult.cs index eb1efbc64f..178e1ec1f4 100644 --- a/src/Umbraco.Web/PropertyEditors/Validation/ComplexEditorValidationResult.cs +++ b/src/Umbraco.Web/PropertyEditors/Validation/ComplexEditorValidationResult.cs @@ -8,7 +8,10 @@ namespace Umbraco.Web.PropertyEditors.Validation /// A collection of for a complex editor represented by an Element Type /// /// - /// For example, each represents validation results for a row in Nested Content + /// For example, each represents validation results for a row in Nested Content. + /// + /// For a more indepth explanation of how server side validation works with the angular app, see this GitHub PR: + /// https://github.com/umbraco/Umbraco-CMS/pull/8339 /// public class ComplexEditorValidationResult : ValidationResult { diff --git a/src/Umbraco.Web/PropertyEditors/Validation/ContentPropertyValidationResult.cs b/src/Umbraco.Web/PropertyEditors/Validation/ContentPropertyValidationResult.cs index e161e277ac..0f161c5628 100644 --- a/src/Umbraco.Web/PropertyEditors/Validation/ContentPropertyValidationResult.cs +++ b/src/Umbraco.Web/PropertyEditors/Validation/ContentPropertyValidationResult.cs @@ -7,7 +7,10 @@ namespace Umbraco.Web.PropertyEditors.Validation /// Custom for content properties /// /// - /// This clones the original result and then ensures the nested result if it's the correct type + /// This clones the original result and then ensures the nested result if it's the correct type. + /// + /// For a more indepth explanation of how server side validation works with the angular app, see this GitHub PR: + /// https://github.com/umbraco/Umbraco-CMS/pull/8339 /// public class ContentPropertyValidationResult : ValidationResult { diff --git a/src/Umbraco.Web/PropertyEditors/Validation/ValidationResultConverter.cs b/src/Umbraco.Web/PropertyEditors/Validation/ValidationResultConverter.cs index 85f2c5f81e..2fb000e2ca 100644 --- a/src/Umbraco.Web/PropertyEditors/Validation/ValidationResultConverter.cs +++ b/src/Umbraco.Web/PropertyEditors/Validation/ValidationResultConverter.cs @@ -15,6 +15,9 @@ namespace Umbraco.Web.PropertyEditors.Validation /// /// This converter is specifically used to convert validation results for content in order to be able to have nested /// validation results for complex editors. + /// + /// For a more indepth explanation of how server side validation works with the angular app, see this GitHub PR: + /// https://github.com/umbraco/Umbraco-CMS/pull/8339 /// internal class ValidationResultConverter : JsonConverter { From 78455756e12b6c72c359198c70ef7e4919cc3389 Mon Sep 17 00:00:00 2001 From: Shannon Date: Thu, 23 Jul 2020 23:20:07 +1000 Subject: [PATCH 357/377] Removes _private methods that don't need to exist --- .../blockeditormodelobject.service.js | 58 ++++++------------- 1 file changed, 19 insertions(+), 39 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 e3e1e31a53..f1b7234a12 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 @@ -162,6 +162,7 @@ } } } + /** * Used to create a prop watcher for the settings in the property editor data model. */ @@ -200,6 +201,18 @@ } } + function createDataEntry(elementTypeKey, dataItems) { + var data = { + contentTypeKey: elementTypeKey, + udi: udiService.create("element") + }; + dataItems.push(data); + return data.udi; + } + + function getDataByUdi(udi, dataItems) { + return dataItems.find(entry => entry.udi === udi) || null; + } /** * Used to highlight unsupported properties for the user, changes unsupported properties into a unsupported-property. @@ -417,7 +430,7 @@ var udi = layoutEntry.contentUdi; - var dataModel = this._getDataByUdi(udi); + var dataModel = getDataByUdi(udi, this.value.contentData); if (dataModel === null) { console.error("Couldn't find content model of " + udi) @@ -486,12 +499,12 @@ if (!layoutEntry.settingsUdi) { // if this block does not have settings data, then create it. This could happen because settings model has been added later than this content was created. - layoutEntry.settingsUdi = this._createSettingsEntry(blockConfiguration.settingsElementTypeKey); + layoutEntry.settingsUdi = createDataEntry(blockConfiguration.settingsElementTypeKey, this.value.settingsData); } var settingsUdi = layoutEntry.settingsUdi; - var settingsData = this._getSettingsByUdi(settingsUdi); + var settingsData = getDataByUdi(settingsUdi, this.value.settingsData); if (settingsData === null) { console.error("Couldnt find content settings data of " + settingsUdi) return null; @@ -628,11 +641,11 @@ } var entry = { - contentUdi: this._createDataEntry(contentTypeKey) + contentUdi: createDataEntry(contentTypeKey, this.value.contentData) } if (blockConfiguration.settingsElementTypeKey != null) { - entry.settingsUdi = this._createSettingsEntry(blockConfiguration.settingsElementTypeKey) + entry.settingsUdi = createDataEntry(blockConfiguration.settingsElementTypeKey, this.value.settingsData) } return entry; @@ -656,7 +669,7 @@ return null; } - var dataModel = this._getDataByUdi(layoutEntry.udi); + var dataModel = getDataByUdi(layoutEntry.udi, this.value.contentData); if (dataModel === null) { return null; } @@ -679,22 +692,6 @@ } }, - // private - // TODO: Then this can just be a method in the outer scope - _createDataEntry: function (elementTypeKey) { - var content = { - contentTypeKey: elementTypeKey, - udi: udiService.create("element") - }; - this.value.contentData.push(content); - return content.udi; - }, - // private - // TODO: Then this can just be a method in the outer scope - _getDataByUdi: function (udi) { - return this.value.contentData.find(entry => entry.udi === udi) || null; - }, - /** * @ngdoc method * @name removeDataByUdi @@ -710,23 +707,6 @@ } }, - // private - // TODO: Then this can just be a method in the outer scope - _createSettingsEntry: function (elementTypeKey) { - var settings = { - contentTypeKey: elementTypeKey, - udi: udiService.create("element") - }; - this.value.settingsData.push(settings); - return settings.udi; - }, - - // private - // TODO: Then this can just be a method in the outer scope - _getSettingsByUdi: function (udi) { - return this.value.settingsData.find(entry => entry.udi === udi) || null; - }, - /** * @ngdoc method * @name removeSettingsByUdi From 84dddd18691e64c81c6bfc7e679668dfc77484a5 Mon Sep 17 00:00:00 2001 From: Shannon Date: Thu, 23 Jul 2020 23:23:39 +1000 Subject: [PATCH 358/377] fixes tests --- .../common/services/block-editor-service.spec.js | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/test/unit/common/services/block-editor-service.spec.js b/src/Umbraco.Web.UI.Client/test/unit/common/services/block-editor-service.spec.js index 577cb1b3e8..33341c7cec 100644 --- a/src/Umbraco.Web.UI.Client/test/unit/common/services/block-editor-service.spec.js +++ b/src/Umbraco.Web.UI.Client/test/unit/common/services/block-editor-service.spec.js @@ -264,16 +264,12 @@ expect(propertyModel.layout["Umbraco.TestBlockEditor"][0]).toBeUndefined(); done(); - }); - + } catch (e) { + done.fail(e); + } + }); }); - - - - - - it('getBlockObject of block with settings has values', function (done) { var propertyModel = angular.copy(propertyModelWithSettingsMock); From 692b8b819f57d6b082cc357382878765bab826d2 Mon Sep 17 00:00:00 2001 From: Shannon Date: Thu, 23 Jul 2020 23:31:55 +1000 Subject: [PATCH 359/377] fixes tests --- .../services/block-editor-service.spec.js | 57 ++++++++++--------- 1 file changed, 30 insertions(+), 27 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/test/unit/common/services/block-editor-service.spec.js b/src/Umbraco.Web.UI.Client/test/unit/common/services/block-editor-service.spec.js index 33341c7cec..e4e1133bd6 100644 --- a/src/Umbraco.Web.UI.Client/test/unit/common/services/block-editor-service.spec.js +++ b/src/Umbraco.Web.UI.Client/test/unit/common/services/block-editor-service.spec.js @@ -1,7 +1,9 @@ describe('blockEditorService tests', function () { - var key = "6A1F5BDD-67EF-4173-B061-D6348ED07094"; - var udi = "umb://element/6A1F5BDD67EF4173B061D6348ED07094"; + var contentKey = "6A1F5BDD-67EF-4173-B061-D6348ED07094"; + var contentUdi = "umb://element/6A1F5BDD67EF4173B061D6348ED07094"; + var settingsKey = "2AF42343-C8A2-400D-BA43-4818C2B3CDC5"; + var settingsUdi = "umb://element/2AF42343C8A2400DBA434818C2B3CDC5"; var blockEditorService, contentResource, $rootScope, $scope; @@ -20,7 +22,7 @@ contentResource = $injector.get("contentResource"); spyOn(contentResource, "getScaffoldByKey").and.callFake( function () { - return Promise.resolve(mocksUtils.getMockVariantContent(1234, key, udi)) + return Promise.resolve(mocksUtils.getMockVariantContent(1234, contentKey, contentUdi)) } ); @@ -35,13 +37,13 @@ layout: { "Umbraco.TestBlockEditor": [ { - udi: udi + contentUdi: contentUdi } ] }, contentData: [ { - udi: 1234, + udi: contentUdi, contentTypeKey: "7C5B74D1-E2F9-45A3-AE4B-FC7A829BF8AB", testproperty: "myTestValue" } @@ -53,21 +55,21 @@ layout: { "Umbraco.TestBlockEditor": [ { - udi: 1234, - settingsUdi: 4567 + contentUdi: contentUdi, + settingsUdi: settingsUdi } ] }, contentData: [ { - udi: udi, + udi: contentUdi, contentTypeKey: "7C5B74D1-E2F9-45A3-AE4B-FC7A829BF8AB", testproperty: "myTestValue" } ], settingsData: [ { - udi: 4567, + udi: settingsUdi, contentTypeKey: "7C5B74D1-E2F9-45A3-AE4B-FC7A829BF8AB", testproperty: "myTestValueForSettings" } @@ -339,33 +341,34 @@ modelObject.load().then(() => { - var layout = modelObject.getLayout(); + try { + var layout = modelObject.getLayout(); - var blockObject = modelObject.getBlockObject(layout[0]); + var blockObject = modelObject.getBlockObject(layout[0]); - blockObject.content.variants[0].tabs[0].properties[0].value.list[0] = "AA"; - blockObject.content.variants[0].tabs[0].properties[0].value.list.push("D"); + blockObject.content.variants[0].tabs[0].properties[0].value.list[0] = "AA"; + blockObject.content.variants[0].tabs[0].properties[0].value.list.push("D"); - blockObject.settings.variants[0].tabs[0].properties[0].value.list[0] = "settingsValue"; - blockObject.settings.variants[0].tabs[0].properties[0].value.list.push("settingsNewValue"); + blockObject.settings.variants[0].tabs[0].properties[0].value.list[0] = "settingsValue"; + blockObject.settings.variants[0].tabs[0].properties[0].value.list.push("settingsNewValue"); - $rootScope.$digest();// invoke angularJS Store. + $rootScope.$digest();// invoke angularJS Store. - expect(propertyModel.contentData[0].testproperty.list[0]).toBe("AA"); - expect(propertyModel.contentData[0].testproperty.list.length).toBe(4); + expect(propertyModel.contentData[0].testproperty.list[0]).toBe("AA"); + expect(propertyModel.contentData[0].testproperty.list.length).toBe(4); - expect(propertyModel.settingsData[0].testproperty.list[0]).toBe("settingsValue"); - expect(propertyModel.settingsData[0].testproperty.list.length).toBe(4); + expect(propertyModel.settingsData[0].testproperty.list[0]).toBe("settingsValue"); + expect(propertyModel.settingsData[0].testproperty.list.length).toBe(4); + + done(); + } catch (e) { + done.fail(e); + } + }); - done(); - } catch (e) { - done.fail(e); - } }); + }); - -}); - }); From e52cd3e3597249510237d897e59b713e5f190f07 Mon Sep 17 00:00:00 2001 From: Shannon Date: Thu, 23 Jul 2020 23:53:58 +1000 Subject: [PATCH 360/377] fixes block removal with new data structure --- .../blocklist/umbBlockListPropertyEditor.component.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 45ba6ddaa7..3ad18d87c1 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 @@ -271,7 +271,7 @@ var removed = vm.layout.splice(layoutIndex, 1); removed.forEach(x => { // remove any server validation errors associated - var guid = udiService.getKey(x.udi); + var guid = udiService.getKey(x.contentUdi); serverValidationManager.removePropertyError(guid, vm.umbProperty.property.culture, vm.umbProperty.property.segment, "", { matchType: "contains" }); }); From 9e9f8863c2186c5279a1a88840943af6c16e1aa5 Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 24 Jul 2020 00:42:37 +1000 Subject: [PATCH 361/377] Ensures validation works for settings data, now to test other scenarios. --- .../validation/valservermatch.directive.js | 23 ++- .../blockeditormodelobject.service.js | 19 ++- .../blockeditor/blockeditor.controller.js | 146 +++++++++--------- .../blocklist/umb-block-list-row.html | 4 +- .../umbBlockListPropertyEditor.component.js | 5 +- .../BlockEditorPropertyEditor.cs | 6 +- 6 files changed, 108 insertions(+), 95 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/validation/valservermatch.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/validation/valservermatch.directive.js index b8e1b5cac8..17a32be8f8 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/validation/valservermatch.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/validation/valservermatch.directive.js @@ -57,15 +57,22 @@ function valServerMatch(serverValidationManager) { throw "valServerMatch dictionary keys must be one of " + allowedKeys.join(); } - unsubscribe.push(serverValidationManager.subscribe( - scope.valServerMatch[k], - currentCulture, - "", - serverValidationManagerCallback, - currentSegment, - { matchType: k } // specify the match type - )); + var matchVal = scope.valServerMatch[k]; + if (Utilities.isString(matchVal)) { + matchVal = [matchVal]; // change to an array since the value can also natively be an array + } + // match for each string in the array + matchVal.forEach(m => { + unsubscribe.push(serverValidationManager.subscribe( + m, + currentCulture, + "", + serverValidationManagerCallback, + currentSegment, + { matchType: k } // specify the match type + )); + }) }); } else if (Utilities.isString(scope.valServerMatch)) { 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 f1b7234a12..2c6ff25845 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 @@ -428,12 +428,12 @@ */ getBlockObject: function (layoutEntry) { - var udi = layoutEntry.contentUdi; + var contentUdi = layoutEntry.contentUdi; - var dataModel = getDataByUdi(udi, this.value.contentData); + var dataModel = getDataByUdi(contentUdi, this.value.contentData); if (dataModel === null) { - console.error("Couldn't find content model of " + udi) + console.error("Couldn't find content model of " + contentUdi) return null; } @@ -441,12 +441,12 @@ var contentScaffold; if (blockConfiguration === null) { - console.error("The block entry of " + udi + " is not being initialized because its contentTypeKey is not allowed for this PropertyEditor"); + console.error("The block entry of " + contentUdi + " is not being initialized because its contentTypeKey is not allowed for this PropertyEditor"); } else { contentScaffold = this.getScaffoldFromKey(blockConfiguration.contentTypeKey); if (contentScaffold === null) { - console.error("The block entry of " + udi + " is not begin initialized cause its Element Type was not loaded."); + console.error("The block entry of " + contentUdi + " is not begin initialized cause its Element Type was not loaded."); } } @@ -483,9 +483,9 @@ // make basics from scaffold blockObject.content = Utilities.copy(contentScaffold); - blockObject.content.udi = udi; - // Change the content.key to the GUID part of the udi, else it's just random which we don't want, it should be consistent - blockObject.content.key = udiService.getKey(udi); + blockObject.content.udi = contentUdi; + // Change the content.key to the GUID part of the udi, else it's just random which we don't want, it must be consistent + blockObject.content.key = udiService.getKey(contentUdi); mapToElementModel(blockObject.content, dataModel); @@ -515,6 +515,9 @@ // make basics from scaffold blockObject.settings = Utilities.copy(settingsScaffold); blockObject.settings.udi = settingsUdi; + // Change the settings.key to the GUID part of the udi, else it's just random which we don't want, it must be consistent + blockObject.settings.key = udiService.getKey(settingsUdi); + mapToElementModel(blockObject.settings, settingsData); } } 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 daded4d9a9..3a70c34170 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 @@ -1,86 +1,90 @@ angular.module("umbraco") -.controller("Umbraco.Editors.BlockEditorController", - function ($scope, localizationService, formHelper) { - var vm = this; + .controller("Umbraco.Editors.BlockEditorController", + function ($scope, localizationService, formHelper) { + var vm = this; - vm.model = $scope.model; - vm.model = $scope.model; - vm.tabs = []; - localizationService.localizeMany([ - vm.model.liveEditing ? "prompt_discardChanges" : "general_close", - vm.model.liveEditing ? "buttons_confirmActionConfirm" : "buttons_submitChanges" - ]).then(function (data) { - vm.closeLabel = data[0]; - vm.submitLabel = data[1]; - }); + vm.model = $scope.model; + vm.model = $scope.model; + vm.tabs = []; + localizationService.localizeMany([ + vm.model.liveEditing ? "prompt_discardChanges" : "general_close", + vm.model.liveEditing ? "buttons_confirmActionConfirm" : "buttons_submitChanges" + ]).then(function (data) { + vm.closeLabel = data[0]; + vm.submitLabel = data[1]; + }); - if ($scope.model.content && $scope.model.content.variants) { + if ($scope.model.content && $scope.model.content.variants) { - var apps = $scope.model.content.apps; + var apps = $scope.model.content.apps; - vm.tabs = apps; + vm.tabs = apps; - // replace view of content app. - var contentApp = apps.find(entry => entry.alias === "umbContent"); - if (contentApp) { - contentApp.view = "views/common/infiniteeditors/blockeditor/blockeditor.content.html"; - if(vm.model.hideContent) { - apps.splice(apps.indexOf(contentApp), 1); - } else if (vm.model.openSettings !== true) { - contentApp.active = true; - } - } - - // remove info app: - var infoAppIndex = apps.findIndex(entry => entry.alias === "umbInfo"); - apps.splice(infoAppIndex, 1); - - } - - if (vm.model.settings && vm.model.settings.variants) { - localizationService.localize("blockEditor_tabBlockSettings").then( - function (settingsName) { - var settingsTab = { - "name": settingsName, - "alias": "settings", - "icon": "icon-settings", - "view": "views/common/infiniteeditors/blockeditor/blockeditor.settings.html" - }; - vm.tabs.push(settingsTab); - if (vm.model.openSettings) { - settingsTab.active = true; + // replace view of content app. + var contentApp = apps.find(entry => entry.alias === "umbContent"); + if (contentApp) { + contentApp.view = "views/common/infiniteeditors/blockeditor/blockeditor.content.html"; + if (vm.model.hideContent) { + apps.splice(apps.indexOf(contentApp), 1); + } else if (vm.model.openSettings !== true) { + contentApp.active = true; } } - ); - } - vm.submitAndClose = function () { - if (vm.model && vm.model.submit) { - // always keep server validations since this will be a nested editor and server validations are global - if (formHelper.submitForm({ scope: $scope, formCtrl: vm.blockForm, keepServerValidation: true })) { - vm.model.submit(vm.model); + // remove info app: + var infoAppIndex = apps.findIndex(entry => entry.alias === "umbInfo"); + apps.splice(infoAppIndex, 1); + + } + + if (vm.model.settings && vm.model.settings.variants) { + localizationService.localize("blockEditor_tabBlockSettings").then( + function (settingsName) { + var settingsTab = { + "name": settingsName, + "alias": "settings", + "icon": "icon-settings", + "view": "views/common/infiniteeditors/blockeditor/blockeditor.settings.html" + }; + vm.tabs.push(settingsTab); + if (vm.model.openSettings) { + settingsTab.active = true; + } + } + ); + } + + vm.submitAndClose = function () { + if (vm.model && vm.model.submit) { + // always keep server validations since this will be a nested editor and server validations are global + if (formHelper.submitForm({ + scope: $scope, + formCtrl: vm.blockForm, + keepServerValidation: true + })) { + vm.model.submit(vm.model); + } } } - } - vm.close = function () { - if (vm.model && vm.model.close) { - // TODO: At this stage there could very well have been server errors that have been cleared - // but if we 'close' we are basically cancelling the value changes which means we'd want to cancel - // all of the server errors just cleared. It would be possible to do that but also quite annoying. - // The rudimentary way would be to: - // * Track all cleared server errors here by subscribing to the prefix validation of controls contained here - // * If this is closed, re-add all of those server validation errors - // A more robust way to do this would be to: - // * Add functionality to the serverValidationManager whereby we can remove validation errors and it will - // maintain a copy of the original errors - // * 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 + vm.close = function () { + if (vm.model && vm.model.close) { + // TODO: At this stage there could very well have been server errors that have been cleared + // but if we 'close' we are basically cancelling the value changes which means we'd want to cancel + // all of the server errors just cleared. It would be possible to do that but also quite annoying. + // The rudimentary way would be to: + // * Track all cleared server errors here by subscribing to the prefix validation of controls contained here + // * If this is closed, re-add all of those server validation errors + // A more robust way to do this would be to: + // * Add functionality to the serverValidationManager whereby we can remove validation errors and it will + // maintain a copy of the original errors + // * 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 - // TODO: check if content/settings has changed and ask user if they are sure. - 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/umb-block-list-row.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/umb-block-list-row.html index 62f4627aad..388a6f78ca 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,4 +1,4 @@ - +
        - - @@ -26,28 +24,29 @@
        - - - + + +
        Minimum %0% entries, needs %1% more.
        + >
        -
        -
        +
        +
        Maximum %0% entries, %1% too many.
        +
        diff --git a/src/Umbraco.Web/PropertyEditors/BlockEditorPropertyEditor.cs b/src/Umbraco.Web/PropertyEditors/BlockEditorPropertyEditor.cs index 9536d08439..a9dcd6b68b 100644 --- a/src/Umbraco.Web/PropertyEditors/BlockEditorPropertyEditor.cs +++ b/src/Umbraco.Web/PropertyEditors/BlockEditorPropertyEditor.cs @@ -2,7 +2,9 @@ using Newtonsoft.Json.Linq; using System; using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; using System.Linq; +using System.Web.Razor.Parser.SyntaxTree; using Umbraco.Core; using Umbraco.Core.Logging; using Umbraco.Core.Models; @@ -59,6 +61,7 @@ namespace Umbraco.Web.PropertyEditors _logger = logger; _blockEditorValues = new BlockEditorValues(new BlockListEditorDataConverter(), contentTypeService, _logger); Validators.Add(new BlockEditorValidator(_blockEditorValues, propertyEditors, dataTypeService, textService)); + Validators.Add(new MinMaxValidator(_blockEditorValues, textService)); } public IEnumerable GetReferences(object value) @@ -221,6 +224,41 @@ namespace Umbraco.Web.PropertyEditors #endregion } + /// + /// Validates the min/max of the block editor + /// + private class MinMaxValidator : IValueValidator + { + private readonly BlockEditorValues _blockEditorValues; + private readonly ILocalizedTextService _textService; + + public MinMaxValidator(BlockEditorValues blockEditorValues, ILocalizedTextService textService) + { + _blockEditorValues = blockEditorValues; + _textService = textService; + } + + public IEnumerable Validate(object value, string valueType, object dataTypeConfiguration) + { + var blockConfig = (BlockListConfiguration)dataTypeConfiguration; + var blockEditorData = _blockEditorValues.DeserializeAndClean(value); + if ((blockEditorData == null && blockConfig?.ValidationLimit?.Min > 0) + || (blockEditorData != null && blockEditorData.Layout.Count() < blockConfig?.ValidationLimit?.Min)) + { + yield return new ValidationResult( + _textService.Localize("validation/entriesShort", new[] { blockConfig.ValidationLimit.Min.ToString(), (blockConfig.ValidationLimit.Min - blockEditorData.Layout.Count()).ToString() }), + new[] { "minCount" }); + } + + if (blockEditorData != null && blockEditorData.Layout.Count() > blockConfig?.ValidationLimit?.Max) + { + yield return new ValidationResult( + _textService.Localize("validation/entriesExceed", new[] { blockConfig.ValidationLimit.Max.ToString(), (blockEditorData.Layout.Count() - blockConfig.ValidationLimit.Max).ToString() }), + new[] { "maxCount" }); + } + } + } + internal class BlockEditorValidator : ComplexEditorValidator { private readonly BlockEditorValues _blockEditorValues; From ca4f7bf41da058ac5a73f234f38dc9ce979327de Mon Sep 17 00:00:00 2001 From: Bjarne Fyrstenborg Date: Mon, 27 Jul 2020 21:12:55 +0200 Subject: [PATCH 368/377] Ensure replacement of angular.copy still works where second parameter is used for destination object. (cherry picked from commit eff630d3a02fe6c5ae67f48d6b80d3cbaa815aac) --- src/Umbraco.Web.UI.Client/src/utilities.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/utilities.js b/src/Umbraco.Web.UI.Client/src/utilities.js index 9121ba0e25..64884b589b 100644 --- a/src/Umbraco.Web.UI.Client/src/utilities.js +++ b/src/Umbraco.Web.UI.Client/src/utilities.js @@ -18,7 +18,7 @@ /** * Facade to angular.copy */ - const copy = val => angular.copy(val); + const copy = (src, dst) => angular.copy(src, dst); /** * Equivalent to angular.isArray From 58331bbdef196e9e99a85f49f42ddefe863a2621 Mon Sep 17 00:00:00 2001 From: Shannon Date: Wed, 29 Jul 2020 18:46:01 +1000 Subject: [PATCH 369/377] Fixes tests and issues causing test failures. --- src/Umbraco.Web.UI.Client/package-lock.json | 646 +++++++++--------- src/Umbraco.Web.UI.Client/package.json | 7 +- .../src/common/mocks/resources/_utils.js | 1 + .../blockeditormodelobject.service.js | 35 +- .../common/services/localization.service.js | 5 +- .../test/config/karma.conf.js | 1 - .../services/block-editor-service.spec.js | 132 ++-- 7 files changed, 434 insertions(+), 393 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/package-lock.json b/src/Umbraco.Web.UI.Client/package-lock.json index e3a03fd12b..afe9f05200 100644 --- a/src/Umbraco.Web.UI.Client/package-lock.json +++ b/src/Umbraco.Web.UI.Client/package-lock.json @@ -1099,9 +1099,9 @@ "dev": true }, "angular": { - "version": "1.7.9", - "resolved": "https://registry.npmjs.org/angular/-/angular-1.7.9.tgz", - "integrity": "sha512-5se7ZpcOtu0MBFlzGv5dsM1quQDoDeUTwZrWjGtTNA7O88cD8TEk5IEKCTDa3uECV9XnvKREVUr7du1ACiWGFQ==" + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/angular/-/angular-1.8.0.tgz", + "integrity": "sha512-VdaMx+Qk0Skla7B5gw77a8hzlcOakwF8mjlW13DpIWIDlfqwAbSSLfd8N/qZnzEmQF4jC4iofInd3gE7vL8ZZg==" }, "angular-animate": { "version": "1.7.5", @@ -1526,7 +1526,7 @@ "array-slice": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/array-slice/-/array-slice-1.1.0.tgz", - "integrity": "sha1-42jqFfibxwaff/uJrsOmx9SsItQ=", + "integrity": "sha512-B1qMD3RBP7O8o0H2KbrXDyB0IccejMF15+87Lvlor12ONPRHP6gTjXMNkt/d3ZuOGbAe66hFmaCfECI24Ufp6w==", "dev": true }, "array-sort": { @@ -1569,7 +1569,7 @@ "arraybuffer.slice": { "version": "0.0.7", "resolved": "https://registry.npmjs.org/arraybuffer.slice/-/arraybuffer.slice-0.0.7.tgz", - "integrity": "sha1-O7xCdd1YTMGxCAm4nU6LY6aednU=", + "integrity": "sha512-wGUIVQXuehL5TCqQun8OW81jGzAWycqzFF8lFp+GOM5BXLYj3bKNsYC4daB7n6XjCqxQA/qgTJ+8ANR3acjrog==", "dev": true }, "asap": { @@ -2093,7 +2093,7 @@ }, "bl": { "version": "1.2.2", - "resolved": "http://registry.npmjs.org/bl/-/bl-1.2.2.tgz", + "resolved": "https://registry.npmjs.org/bl/-/bl-1.2.2.tgz", "integrity": "sha512-e8tQYnZodmebYDWGH7KMRvtzKXaJHx3BbilrgZCfvyLUYdKpK1t5PSPmpkny/SgiTSCnjfLW7v5rlONXVFkQEA==", "dev": true, "optional": true, @@ -2111,7 +2111,7 @@ }, "readable-stream": { "version": "2.3.6", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", "dev": true, "optional": true, @@ -2127,7 +2127,7 @@ }, "string_decoder": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "resolved": "http://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", "dev": true, "optional": true, @@ -2424,7 +2424,7 @@ }, "callsites": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-2.0.0.tgz", + "resolved": "http://registry.npmjs.org/callsites/-/callsites-2.0.0.tgz", "integrity": "sha1-BuuE8A7qQT2oav/vrL/7Ngk7PFA=", "dev": true }, @@ -2467,9 +2467,9 @@ } }, "caniuse-lite": { - "version": "1.0.30001002", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001002.tgz", - "integrity": "sha512-pRuxPE8wdrWmVPKcDmJJiGBxr6lFJq4ivdSeo9FTmGj5Rb8NX3Mby2pARG57MXF15hYAhZ0nHV5XxT2ig4bz3g==", + "version": "1.0.30001107", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001107.tgz", + "integrity": "sha512-86rCH+G8onCmdN4VZzJet5uPELII59cUzDphko3thQFgAQG1RNa+sVLDoALIhRYmflo5iSIzWY3vu1XTWtNMQQ==", "dev": true }, "caseless": { @@ -2519,9 +2519,9 @@ "dev": true }, "chart.js": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-2.8.0.tgz", - "integrity": "sha512-Di3wUL4BFvqI5FB5K26aQ+hvWh8wnP9A3DWGvXHVkO13D3DSnaSsdZx29cXlEsYKVkn1E2az+ZYFS4t0zi8x0w==", + "version": "2.9.3", + "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-2.9.3.tgz", + "integrity": "sha512-+2jlOobSk52c1VU6fzkh3UwqHMdSlgH1xFv9FKMqHiNCpXsGPQa/+81AFa+i3jZ253Mq9aAycPwDjnn1XbRNNw==", "requires": { "chartjs-color": "^2.1.0", "moment": "^2.10.2" @@ -2739,7 +2739,7 @@ }, "readable-stream": { "version": "2.3.6", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", "dev": true, "requires": { @@ -2754,7 +2754,7 @@ }, "string_decoder": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "resolved": "http://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", "dev": true, "requires": { @@ -2966,7 +2966,7 @@ }, "readable-stream": { "version": "2.3.6", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", "dev": true, "requires": { @@ -3076,7 +3076,7 @@ "content-type": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", - "integrity": "sha1-4TjMdeBAxyexlm/l5fjJruJW/js=", + "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==", "dev": true }, "convert-source-map": { @@ -3922,7 +3922,7 @@ }, "readable-stream": { "version": "2.3.6", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", "dev": true, "requires": { @@ -3937,7 +3937,7 @@ }, "string_decoder": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "resolved": "http://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", "dev": true, "requires": { @@ -4042,7 +4042,7 @@ "debug": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", - "integrity": "sha1-W7WgZyYotkFJVmuhaBnmFRjGcmE=", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", "dev": true, "requires": { "ms": "2.0.0" @@ -4084,7 +4084,7 @@ "debug": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", - "integrity": "sha1-W7WgZyYotkFJVmuhaBnmFRjGcmE=", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", "dev": true, "requires": { "ms": "2.0.0" @@ -4414,7 +4414,7 @@ "esquery": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.0.1.tgz", - "integrity": "sha1-QGxRZYsfWZGl+bYrHcJbAOPlxwg=", + "integrity": "sha512-SmiyZ5zIWH9VM+SRUReLS5Q8a7GxtRdxEBVZpm98rJM7Sb+A9DVCndXfkeFUd3byderg+EbDkfnevfCwynWaNA==", "dev": true, "requires": { "estraverse": "^4.0.0" @@ -4423,7 +4423,7 @@ "esrecurse": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.2.1.tgz", - "integrity": "sha1-AHo7n9vCs7uH5IeeoZyS/b05Qs8=", + "integrity": "sha512-64RBB++fIOAXPw3P9cy89qfMlvZEXZkqqJkjqqXIvzP5ezRZjW+lPWjw35UX/3EhUPFYbg5ER4JYgDw4007/DQ==", "dev": true, "requires": { "estraverse": "^4.1.0" @@ -4600,7 +4600,7 @@ "fill-range": { "version": "2.2.4", "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-2.2.4.tgz", - "integrity": "sha512-cnrcCbj01+j2gTG921VZPnHbjmdAf8oQV/iGeV2kZxGSyfYjjTyY79ErsK1WJWMpw6DaApEX72binqJE+/d+5Q==", + "integrity": "sha1-6x53OrsFbc2N8r/favWbizqTZWU=", "dev": true, "requires": { "is-number": "^2.1.0", @@ -5091,15 +5091,26 @@ } }, "findup-sync": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-2.0.0.tgz", - "integrity": "sha1-kyaxSIwi0aYIhlCoaQGy2akKLLw=", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-3.0.0.tgz", + "integrity": "sha512-YbffarhcicEhOrm4CtrwdKBdCuz576RLdhJDsIfvNtxUuhdRet1qZcsMjqbePtAseKdAnDyM/IyXbu7PRPRLYg==", "dev": true, "requires": { "detect-file": "^1.0.0", - "is-glob": "^3.1.0", + "is-glob": "^4.0.0", "micromatch": "^3.0.4", "resolve-dir": "^1.0.1" + }, + "dependencies": { + "is-glob": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", + "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", + "dev": true, + "requires": { + "is-extglob": "^2.1.1" + } + } } }, "fined": { @@ -6191,7 +6202,7 @@ "global-modules": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-1.0.0.tgz", - "integrity": "sha1-bXcPDrUjrHgWTXK15xqIdyZcw+o=", + "integrity": "sha512-sKzpEkf11GpOFuw0Zzjzmt4B4UZwjOcG757PPvrfhxcLFbq0wpsgpOqxpxtxFiCG4DtG93M6XRVbF2oGdev7bg==", "dev": true, "requires": { "global-prefix": "^1.0.1", @@ -6410,9 +6421,9 @@ } }, "gulp-cli": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/gulp-cli/-/gulp-cli-2.2.0.tgz", - "integrity": "sha512-rGs3bVYHdyJpLqR0TUBnlcZ1O5O++Zs4bA0ajm+zr3WFCfiSLjGwoCBqFs18wzN+ZxahT9DkOK5nDf26iDsWjA==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/gulp-cli/-/gulp-cli-2.3.0.tgz", + "integrity": "sha512-zzGBl5fHo0EKSXsHzjspp3y5CONegCm8ErO5Qh0UzFzk2y4tMvzLWhoDokADbarfZRL2pGpRp7yt6gfJX4ph7A==", "dev": true, "requires": { "ansi-colors": "^1.0.1", @@ -6423,7 +6434,7 @@ "copy-props": "^2.0.1", "fancy-log": "^1.3.2", "gulplog": "^1.0.0", - "interpret": "^1.1.0", + "interpret": "^1.4.0", "isobject": "^3.0.1", "liftoff": "^3.1.0", "matchdep": "^2.0.0", @@ -6431,56 +6442,8 @@ "pretty-hrtime": "^1.0.0", "replace-homedir": "^1.0.0", "semver-greatest-satisfied-range": "^1.1.0", - "v8flags": "^3.0.1", + "v8flags": "^3.2.0", "yargs": "^7.1.0" - }, - "dependencies": { - "findup-sync": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-3.0.0.tgz", - "integrity": "sha512-YbffarhcicEhOrm4CtrwdKBdCuz576RLdhJDsIfvNtxUuhdRet1qZcsMjqbePtAseKdAnDyM/IyXbu7PRPRLYg==", - "dev": true, - "requires": { - "detect-file": "^1.0.0", - "is-glob": "^4.0.0", - "micromatch": "^3.0.4", - "resolve-dir": "^1.0.1" - } - }, - "is-glob": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", - "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", - "dev": true, - "requires": { - "is-extglob": "^2.1.1" - } - }, - "liftoff": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/liftoff/-/liftoff-3.1.0.tgz", - "integrity": "sha512-DlIPlJUkCV0Ips2zf2pJP0unEoT1kwYhiiPUGF3s/jtxTCjziNLoiVVh+jqWOWeFi6mmwQ5fNxvAUyPad4Dfog==", - "dev": true, - "requires": { - "extend": "^3.0.0", - "findup-sync": "^3.0.0", - "fined": "^1.0.1", - "flagged-respawn": "^1.0.0", - "is-plain-object": "^2.0.4", - "object.map": "^1.0.0", - "rechoir": "^0.6.2", - "resolve": "^1.1.7" - } - }, - "v8flags": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/v8flags/-/v8flags-3.1.3.tgz", - "integrity": "sha512-amh9CCg3ZxkzQ48Mhcb8iX7xpAfYJgePHxWMQCBWECpOSqJUXgY26ncA61UTV0BkPqfhcy6mzwCIoP4ygxpW8w==", - "dev": true, - "requires": { - "homedir-polyfill": "^1.0.1" - } - } } }, "gulp-concat": { @@ -6606,7 +6569,7 @@ }, "kind-of": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-1.1.0.tgz", + "resolved": "http://registry.npmjs.org/kind-of/-/kind-of-1.1.0.tgz", "integrity": "sha1-FAo9LUGjbS78+pN3tiwk+ElaXEQ=", "dev": true }, @@ -6818,7 +6781,7 @@ }, "chalk": { "version": "1.1.3", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "resolved": "http://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", "dev": true, "requires": { @@ -6894,7 +6857,7 @@ }, "readable-stream": { "version": "2.3.6", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", "dev": true, "requires": { @@ -6915,7 +6878,7 @@ }, "string_decoder": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "resolved": "http://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", "dev": true, "requires": { @@ -7079,7 +7042,7 @@ "has-binary2": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/has-binary2/-/has-binary2-1.0.3.tgz", - "integrity": "sha1-d3asYn8+p3JQz8My2rfd9eT10R0=", + "integrity": "sha512-G1LWKhDSvhGeAQ8mPVQlqNcOB2sJdwATtZKl2pDKKHfpf/rYj24lkinxf69blJbnsvtqqNU+L3SL50vzZhXOnw==", "dev": true, "requires": { "isarray": "2.0.1" @@ -7583,9 +7546,9 @@ } }, "interpret": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.2.0.tgz", - "integrity": "sha512-mT34yGKMNceBQUoVn7iCDKDntA7SC6gycMAWzGx1z/CMCTV7b2AAtXlo3nRyHZ1FelRkQbQjprHSYGwzLtkVbw==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.4.0.tgz", + "integrity": "sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==", "dev": true }, "into-stream": { @@ -7629,7 +7592,7 @@ "is-absolute": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-absolute/-/is-absolute-1.0.0.tgz", - "integrity": "sha1-OV4a6EsR8mrReV5zwXN45IowFXY=", + "integrity": "sha512-dOWoqflvcydARa360Gvv18DZ/gRuHKi2NU/wU5X1ZFzdYfH29nkiNZsF3mp4OJ3H4yo9Mx8A/uAGNzpzPN3yBA==", "dev": true, "requires": { "is-relative": "^1.0.0", @@ -7934,7 +7897,7 @@ "is-relative": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-relative/-/is-relative-1.0.0.tgz", - "integrity": "sha1-obtpNc6MXboei5dUubLcwCDiJg0=", + "integrity": "sha512-Kw/ReK0iqwKeu0MITLFuj0jbPAmEiOsIwyIXvvbfa6QfmN9pkD1M+8pdk7Rl/dTKbH34/XBFMbgD4iMJhLQbGA==", "dev": true, "requires": { "is-unc-path": "^1.0.0" @@ -7943,7 +7906,7 @@ "is-resolvable": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/is-resolvable/-/is-resolvable-1.1.0.tgz", - "integrity": "sha1-+xj4fOH+uSUWnJpAfBkxijIG7Yg=", + "integrity": "sha512-qgDYXFSR5WvEfuS5dMj6oTMEbrrSaM0CrFk2Yiq/gXnBvD9pMa2jGXxyhGLfvhZpuMZe18CJpFxAt3CRs42NMg==", "dev": true }, "is-retry-allowed": { @@ -7986,7 +7949,7 @@ "is-unc-path": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-unc-path/-/is-unc-path-1.0.0.tgz", - "integrity": "sha1-1zHoiY7QkKEsNSrS6u1Qla0yLJ0=", + "integrity": "sha512-mrGpVd0fs7WWLfVsStvgF6iEJnbjDFZh9/emhRDcGWTduTfNHd9CHeUwH3gYIjdbwo4On6hunkztwOaAw0yllQ==", "dev": true, "requires": { "unc-path-regex": "^0.1.2" @@ -8066,12 +8029,6 @@ "integrity": "sha512-nCeAiw37MIMA9w9IXso7bRaLl+c/ef3wnxsoSAlYrzS+Ot0zTG6nU8G/cIfGkqpkjX2wNaIW9RFG0TwIFnG6bA==", "dev": true }, - "jasmine-promise-matchers": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/jasmine-promise-matchers/-/jasmine-promise-matchers-2.6.0.tgz", - "integrity": "sha1-J1ASqFEeXoh9g11TWKutIMAmz2M=", - "dev": true - }, "jpegtran-bin": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/jpegtran-bin/-/jpegtran-bin-4.0.0.tgz", @@ -8085,9 +8042,9 @@ } }, "jquery": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.4.1.tgz", - "integrity": "sha512-36+AdBzCL+y6qjw5Tx7HgzeGCzC81MDDgaUP8ld2zhx58HdqXGoBd+tHdrBMiyjGQs0Hxs/MLZTu/eHNJJuWPw==" + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.5.1.tgz", + "integrity": "sha512-XwIBPqcMn57FxfT+Go5pzySnm4KWkT1Tv7gjrpT1srtf8Weynl6R273VJ5GjkRb51IzMp5nbaPjJXMWeju2MKg==" }, "jquery-ui-dist": { "version": "1.12.1", @@ -8155,7 +8112,7 @@ "json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha1-afaofZUTq4u4/mO9sJecRI5oRmA=", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", "dev": true }, "json-stable-stringify-without-jsonify": { @@ -8517,7 +8474,7 @@ }, "readable-stream": { "version": "2.3.6", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", "dev": true, "requires": { @@ -8532,7 +8489,7 @@ }, "string_decoder": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "resolved": "http://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", "dev": true, "requires": { @@ -8601,6 +8558,22 @@ "type-check": "~0.3.2" } }, + "liftoff": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/liftoff/-/liftoff-3.1.0.tgz", + "integrity": "sha512-DlIPlJUkCV0Ips2zf2pJP0unEoT1kwYhiiPUGF3s/jtxTCjziNLoiVVh+jqWOWeFi6mmwQ5fNxvAUyPad4Dfog==", + "dev": true, + "requires": { + "extend": "^3.0.0", + "findup-sync": "^3.0.0", + "fined": "^1.0.1", + "flagged-respawn": "^1.0.0", + "is-plain-object": "^2.0.4", + "object.map": "^1.0.0", + "rechoir": "^0.6.2", + "resolve": "^1.1.7" + } + }, "load-json-file": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz", @@ -8942,7 +8915,7 @@ "make-iterator": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/make-iterator/-/make-iterator-1.0.1.tgz", - "integrity": "sha1-KbM/MSqo9UfEpeSQ9Wr87JkTOtY=", + "integrity": "sha512-pxiuXh0iVEq7VM7KMIhs5gxsfxCux2URptUQaXo4iZZJxBAzTPOLE2BumO5dbfVYq/hBJFBR/a1mFDmOx5AGmw==", "dev": true, "requires": { "kind-of": "^6.0.2" @@ -8986,6 +8959,20 @@ "micromatch": "^3.0.4", "resolve": "^1.4.0", "stack-trace": "0.0.10" + }, + "dependencies": { + "findup-sync": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-2.0.0.tgz", + "integrity": "sha1-kyaxSIwi0aYIhlCoaQGy2akKLLw=", + "dev": true, + "requires": { + "detect-file": "^1.0.0", + "is-glob": "^3.1.0", + "micromatch": "^3.0.4", + "resolve-dir": "^1.0.1" + } + } } }, "math-random": { @@ -9108,7 +9095,7 @@ "mimic-fn": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz", - "integrity": "sha1-ggyGo5M0ZA6ZUWkovQP8qIBX0CI=", + "integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==", "dev": true }, "mimic-response": { @@ -9339,9 +9326,9 @@ "dev": true }, "nouislider": { - "version": "14.1.1", - "resolved": "https://registry.npmjs.org/nouislider/-/nouislider-14.1.1.tgz", - "integrity": "sha512-3/+Z/pTBoWoJf2YXSEWRmS27LW2XxOBmGEzkPyRzB/J6QvL+0mS3QwcQp0SmWhgO5CMzbSxPmb1lDDD4HP12bg==" + "version": "14.4.0", + "resolved": "https://registry.npmjs.org/nouislider/-/nouislider-14.4.0.tgz", + "integrity": "sha512-D1aYsT73yWrSNcRfqcovE//htpfFqQwd+m+9UCIVSsRupwD7kodSj6j/DTJur5mqnv5HckSJvjHekyVZKLi6dA==" }, "now-and-later": { "version": "2.0.1", @@ -9353,9 +9340,9 @@ } }, "npm": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/npm/-/npm-6.14.0.tgz", - "integrity": "sha512-OgfdLadz7j6dikbpaimmLzMxwLKbXthQXHiJwtegorwtBVnhecfUeYkHopwd5ICaiClQnqlYQCHERXDiYK3Jcw==", + "version": "6.14.7", + "resolved": "https://registry.npmjs.org/npm/-/npm-6.14.7.tgz", + "integrity": "sha512-swhsdpNpyXg4GbM6LpOQ6qaloQuIKizZ+Zh6JPXJQc59ka49100Js0WvZx594iaKSoFgkFq2s8uXFHS3/Xy2WQ==", "requires": { "JSONStream": "^1.3.5", "abbrev": "~1.1.1", @@ -9363,7 +9350,7 @@ "ansistyles": "~0.1.3", "aproba": "^2.0.0", "archy": "~1.0.0", - "bin-links": "^1.1.7", + "bin-links": "^1.1.8", "bluebird": "^3.5.5", "byte-size": "^5.0.1", "cacache": "^12.0.3", @@ -9384,11 +9371,11 @@ "find-npm-prefix": "^1.0.2", "fs-vacuum": "~1.2.10", "fs-write-stream-atomic": "~1.0.10", - "gentle-fs": "^2.3.0", - "glob": "^7.1.4", - "graceful-fs": "^4.2.3", + "gentle-fs": "^2.3.1", + "glob": "^7.1.6", + "graceful-fs": "^4.2.4", "has-unicode": "~2.0.1", - "hosted-git-info": "^2.8.6", + "hosted-git-info": "^2.8.8", "iferr": "^1.0.2", "imurmurhash": "*", "infer-owner": "^1.0.4", @@ -9399,14 +9386,14 @@ "is-cidr": "^3.0.0", "json-parse-better-errors": "^1.0.2", "lazy-property": "~1.0.0", - "libcipm": "^4.0.7", + "libcipm": "^4.0.8", "libnpm": "^3.0.1", "libnpmaccess": "^3.0.2", "libnpmhook": "^5.0.3", "libnpmorg": "^1.0.1", "libnpmsearch": "^2.0.2", "libnpmteam": "^1.0.2", - "libnpx": "^10.2.2", + "libnpx": "^10.2.4", "lock-verify": "^2.1.0", "lockfile": "^1.0.4", "lodash._baseindexof": "*", @@ -9423,20 +9410,20 @@ "lru-cache": "^5.1.1", "meant": "~1.0.1", "mississippi": "^3.0.0", - "mkdirp": "~0.5.1", + "mkdirp": "^0.5.5", "move-concurrently": "^1.0.1", - "node-gyp": "^5.0.7", - "nopt": "~4.0.1", + "node-gyp": "^5.1.0", + "nopt": "^4.0.3", "normalize-package-data": "^2.5.0", - "npm-audit-report": "^1.3.2", + "npm-audit-report": "^1.3.3", "npm-cache-filename": "~1.0.2", "npm-install-checks": "^3.0.2", - "npm-lifecycle": "^3.1.4", + "npm-lifecycle": "^3.1.5", "npm-package-arg": "^6.1.1", "npm-packlist": "^1.4.8", "npm-pick-manifest": "^3.0.2", - "npm-profile": "^4.0.2", - "npm-registry-fetch": "^4.0.3", + "npm-profile": "^4.0.4", + "npm-registry-fetch": "^4.0.5", "npm-user-validate": "~1.0.0", "npmlog": "~4.1.2", "once": "~1.4.0", @@ -9457,7 +9444,7 @@ "readdir-scoped-modules": "^1.1.0", "request": "^2.88.0", "retry": "^0.12.0", - "rimraf": "^2.6.3", + "rimraf": "^2.7.1", "safe-buffer": "^5.1.2", "semver": "^5.7.1", "sha": "^3.0.0", @@ -9622,7 +9609,7 @@ } }, "bin-links": { - "version": "1.1.7", + "version": "1.1.8", "bundled": true, "requires": { "bluebird": "^3.5.3", @@ -9757,23 +9744,36 @@ } }, "cliui": { - "version": "4.1.0", + "version": "5.0.0", "bundled": true, "requires": { - "string-width": "^2.1.1", - "strip-ansi": "^4.0.0", - "wrap-ansi": "^2.0.0" + "string-width": "^3.1.0", + "strip-ansi": "^5.2.0", + "wrap-ansi": "^5.1.0" }, "dependencies": { "ansi-regex": { - "version": "3.0.0", + "version": "4.1.0", "bundled": true }, - "strip-ansi": { - "version": "4.0.0", + "is-fullwidth-code-point": { + "version": "2.0.0", + "bundled": true + }, + "string-width": { + "version": "3.1.0", "bundled": true, "requires": { - "ansi-regex": "^3.0.0" + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + } + }, + "strip-ansi": { + "version": "5.2.0", + "bundled": true, + "requires": { + "ansi-regex": "^4.1.0" } } } @@ -9986,7 +9986,7 @@ "bundled": true }, "deep-extend": { - "version": "0.5.1", + "version": "0.6.0", "bundled": true }, "defaults": { @@ -10087,6 +10087,10 @@ "version": "1.0.0", "bundled": true }, + "emoji-regex": { + "version": "7.0.3", + "bundled": true + }, "encoding": { "version": "0.1.12", "bundled": true, @@ -10194,13 +10198,6 @@ "version": "1.0.2", "bundled": true }, - "find-up": { - "version": "2.1.0", - "bundled": true, - "requires": { - "locate-path": "^2.0.0" - } - }, "flush-write-stream": { "version": "1.0.3", "bundled": true, @@ -10378,7 +10375,7 @@ "bundled": true }, "gentle-fs": { - "version": "2.3.0", + "version": "2.3.1", "bundled": true, "requires": { "aproba": "^1.1.2", @@ -10405,7 +10402,7 @@ } }, "get-caller-file": { - "version": "1.0.3", + "version": "2.0.5", "bundled": true }, "get-stream": { @@ -10423,7 +10420,7 @@ } }, "glob": { - "version": "7.1.4", + "version": "7.1.6", "bundled": true, "requires": { "fs.realpath": "^1.0.0", @@ -10465,7 +10462,7 @@ } }, "graceful-fs": { - "version": "4.2.3", + "version": "4.2.4", "bundled": true }, "har-schema": { @@ -10500,7 +10497,7 @@ "bundled": true }, "hosted-git-info": { - "version": "2.8.6", + "version": "2.8.8", "bundled": true }, "http-cache-semantics": { @@ -10599,10 +10596,6 @@ "validate-npm-package-name": "^3.0.0" } }, - "invert-kv": { - "version": "2.0.0", - "bundled": true - }, "ip": { "version": "1.1.5", "bundled": true @@ -10616,10 +10609,10 @@ "bundled": true }, "is-ci": { - "version": "1.1.0", + "version": "1.2.1", "bundled": true, "requires": { - "ci-info": "^1.0.0" + "ci-info": "^1.5.0" }, "dependencies": { "ci-info": { @@ -10681,7 +10674,7 @@ } }, "is-retry-allowed": { - "version": "1.1.0", + "version": "1.2.0", "bundled": true }, "is-stream": { @@ -10757,15 +10750,8 @@ "version": "1.0.0", "bundled": true }, - "lcid": { - "version": "2.0.0", - "bundled": true, - "requires": { - "invert-kv": "^2.0.0" - } - }, "libcipm": { - "version": "4.0.7", + "version": "4.0.8", "bundled": true, "requires": { "bin-links": "^1.1.2", @@ -10774,7 +10760,7 @@ "find-npm-prefix": "^1.0.2", "graceful-fs": "^4.1.11", "ini": "^1.3.5", - "lock-verify": "^2.0.2", + "lock-verify": "^2.1.0", "mkdirp": "^0.5.1", "npm-lifecycle": "^3.0.0", "npm-logical-tree": "^1.2.1", @@ -10920,7 +10906,7 @@ } }, "libnpx": { - "version": "10.2.2", + "version": "10.2.4", "bundled": true, "requires": { "dotenv": "^5.0.1", @@ -10930,15 +10916,7 @@ "update-notifier": "^2.3.0", "which": "^1.3.0", "y18n": "^4.0.0", - "yargs": "^11.0.0" - } - }, - "locate-path": { - "version": "2.0.0", - "bundled": true, - "requires": { - "p-locate": "^2.0.0", - "path-exists": "^3.0.0" + "yargs": "^14.2.3" } }, "lock-verify": { @@ -11050,32 +11028,10 @@ "ssri": "^6.0.0" } }, - "map-age-cleaner": { - "version": "0.1.3", - "bundled": true, - "requires": { - "p-defer": "^1.0.0" - } - }, "meant": { "version": "1.0.1", "bundled": true }, - "mem": { - "version": "4.3.0", - "bundled": true, - "requires": { - "map-age-cleaner": "^0.1.1", - "mimic-fn": "^2.0.0", - "p-is-promise": "^2.0.0" - }, - "dependencies": { - "mimic-fn": { - "version": "2.1.0", - "bundled": true - } - } - }, "mime-db": { "version": "1.35.0", "bundled": true @@ -11094,10 +11050,6 @@ "brace-expansion": "^1.1.7" } }, - "minimist": { - "version": "0.0.8", - "bundled": true - }, "minizlib": { "version": "1.3.3", "bundled": true, @@ -11132,10 +11084,16 @@ } }, "mkdirp": { - "version": "0.5.1", + "version": "0.5.5", "bundled": true, "requires": { - "minimist": "0.0.8" + "minimist": "^1.2.5" + }, + "dependencies": { + "minimist": { + "version": "1.2.5", + "bundled": true + } } }, "move-concurrently": { @@ -11164,10 +11122,6 @@ "version": "0.0.7", "bundled": true }, - "nice-try": { - "version": "1.0.5", - "bundled": true - }, "node-fetch-npm": { "version": "2.0.2", "bundled": true, @@ -11178,7 +11132,7 @@ } }, "node-gyp": { - "version": "5.0.7", + "version": "5.1.0", "bundled": true, "requires": { "env-paths": "^2.2.0", @@ -11195,7 +11149,7 @@ } }, "nopt": { - "version": "4.0.1", + "version": "4.0.3", "bundled": true, "requires": { "abbrev": "1", @@ -11222,7 +11176,7 @@ } }, "npm-audit-report": { - "version": "1.3.2", + "version": "1.3.3", "bundled": true, "requires": { "cli-table3": "^0.5.0", @@ -11248,7 +11202,7 @@ } }, "npm-lifecycle": { - "version": "3.1.4", + "version": "3.1.5", "bundled": true, "requires": { "byline": "^5.0.0", @@ -11298,7 +11252,7 @@ } }, "npm-profile": { - "version": "4.0.2", + "version": "4.0.4", "bundled": true, "requires": { "aproba": "^1.1.2 || 2", @@ -11307,7 +11261,7 @@ } }, "npm-registry-fetch": { - "version": "4.0.3", + "version": "4.0.5", "bundled": true, "requires": { "JSONStream": "^1.3.4", @@ -11320,7 +11274,7 @@ }, "dependencies": { "safe-buffer": { - "version": "5.2.0", + "version": "5.2.1", "bundled": true } } @@ -11385,41 +11339,6 @@ "version": "1.0.2", "bundled": true }, - "os-locale": { - "version": "3.1.0", - "bundled": true, - "requires": { - "execa": "^1.0.0", - "lcid": "^2.0.0", - "mem": "^4.0.0" - }, - "dependencies": { - "cross-spawn": { - "version": "6.0.5", - "bundled": true, - "requires": { - "nice-try": "^1.0.4", - "path-key": "^2.0.1", - "semver": "^5.5.0", - "shebang-command": "^1.2.0", - "which": "^1.2.9" - } - }, - "execa": { - "version": "1.0.0", - "bundled": true, - "requires": { - "cross-spawn": "^6.0.0", - "get-stream": "^4.0.0", - "is-stream": "^1.1.0", - "npm-run-path": "^2.0.0", - "p-finally": "^1.0.0", - "signal-exit": "^3.0.0", - "strip-eof": "^1.0.0" - } - } - } - }, "os-tmpdir": { "version": "1.0.2", "bundled": true @@ -11432,36 +11351,10 @@ "os-tmpdir": "^1.0.0" } }, - "p-defer": { - "version": "1.0.0", - "bundled": true - }, "p-finally": { "version": "1.0.0", "bundled": true }, - "p-is-promise": { - "version": "2.1.0", - "bundled": true - }, - "p-limit": { - "version": "1.2.0", - "bundled": true, - "requires": { - "p-try": "^1.0.0" - } - }, - "p-locate": { - "version": "2.0.0", - "bundled": true, - "requires": { - "p-limit": "^1.1.0" - } - }, - "p-try": { - "version": "1.0.0", - "bundled": true - }, "package-json": { "version": "4.0.1", "bundled": true, @@ -11686,17 +11579,17 @@ "bundled": true }, "rc": { - "version": "1.2.7", + "version": "1.2.8", "bundled": true, "requires": { - "deep-extend": "^0.5.1", + "deep-extend": "^0.6.0", "ini": "~1.3.0", "minimist": "^1.2.0", "strip-json-comments": "~2.0.1" }, "dependencies": { "minimist": { - "version": "1.2.0", + "version": "1.2.5", "bundled": true } } @@ -11768,7 +11661,7 @@ } }, "registry-auth-token": { - "version": "3.3.2", + "version": "3.4.0", "bundled": true, "requires": { "rc": "^1.1.6", @@ -11813,7 +11706,7 @@ "bundled": true }, "require-main-filename": { - "version": "1.0.1", + "version": "2.0.0", "bundled": true }, "resolve-from": { @@ -11825,7 +11718,7 @@ "bundled": true }, "rimraf": { - "version": "2.6.3", + "version": "2.7.1", "bundled": true, "requires": { "glob": "^7.1.3" @@ -11983,7 +11876,7 @@ } }, "spdx-license-ids": { - "version": "3.0.3", + "version": "3.0.5", "bundled": true }, "split-on-first": { @@ -12362,7 +12255,7 @@ } }, "widest-line": { - "version": "2.0.0", + "version": "2.0.1", "bundled": true, "requires": { "string-width": "^2.1.1" @@ -12376,20 +12269,36 @@ } }, "wrap-ansi": { - "version": "2.1.0", + "version": "5.1.0", "bundled": true, "requires": { - "string-width": "^1.0.1", - "strip-ansi": "^3.0.1" + "ansi-styles": "^3.2.0", + "string-width": "^3.0.0", + "strip-ansi": "^5.0.0" }, "dependencies": { + "ansi-regex": { + "version": "4.1.0", + "bundled": true + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "bundled": true + }, "string-width": { - "version": "1.0.2", + "version": "3.1.0", "bundled": true, "requires": { - "code-point-at": "^1.0.0", - "is-fullwidth-code-point": "^1.0.0", - "strip-ansi": "^3.0.0" + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + } + }, + "strip-ansi": { + "version": "5.2.0", + "bundled": true, + "requires": { + "ansi-regex": "^4.1.0" } } } @@ -12424,34 +12333,93 @@ "bundled": true }, "yargs": { - "version": "11.1.1", + "version": "14.2.3", "bundled": true, "requires": { - "cliui": "^4.0.0", - "decamelize": "^1.1.1", - "find-up": "^2.1.0", - "get-caller-file": "^1.0.1", - "os-locale": "^3.1.0", + "cliui": "^5.0.0", + "decamelize": "^1.2.0", + "find-up": "^3.0.0", + "get-caller-file": "^2.0.1", "require-directory": "^2.1.1", - "require-main-filename": "^1.0.1", + "require-main-filename": "^2.0.0", "set-blocking": "^2.0.0", - "string-width": "^2.0.0", + "string-width": "^3.0.0", "which-module": "^2.0.0", - "y18n": "^3.2.1", - "yargs-parser": "^9.0.2" + "y18n": "^4.0.0", + "yargs-parser": "^15.0.1" }, "dependencies": { - "y18n": { - "version": "3.2.1", + "ansi-regex": { + "version": "4.1.0", "bundled": true + }, + "find-up": { + "version": "3.0.0", + "bundled": true, + "requires": { + "locate-path": "^3.0.0" + } + }, + "is-fullwidth-code-point": { + "version": "2.0.0", + "bundled": true + }, + "locate-path": { + "version": "3.0.0", + "bundled": true, + "requires": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + } + }, + "p-limit": { + "version": "2.3.0", + "bundled": true, + "requires": { + "p-try": "^2.0.0" + } + }, + "p-locate": { + "version": "3.0.0", + "bundled": true, + "requires": { + "p-limit": "^2.0.0" + } + }, + "p-try": { + "version": "2.2.0", + "bundled": true + }, + "string-width": { + "version": "3.1.0", + "bundled": true, + "requires": { + "emoji-regex": "^7.0.1", + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^5.1.0" + } + }, + "strip-ansi": { + "version": "5.2.0", + "bundled": true, + "requires": { + "ansi-regex": "^4.1.0" + } } } }, "yargs-parser": { - "version": "9.0.2", + "version": "15.0.1", "bundled": true, "requires": { - "camelcase": "^4.1.0" + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + }, + "dependencies": { + "camelcase": { + "version": "5.3.1", + "bundled": true + } } } } @@ -13107,7 +13075,7 @@ }, "pify": { "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "resolved": "http://registry.npmjs.org/pify/-/pify-2.3.0.tgz", "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", "dev": true }, @@ -13657,7 +13625,7 @@ "qjobs": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/qjobs/-/qjobs-1.2.0.tgz", - "integrity": "sha1-xF6cYYAL0IfviNfiVkI73Unl0HE=", + "integrity": "sha512-8YOJEHtxpySA3fFDyCRxA+UUV+fA+rTWnuWvylOK/NCjhY+b4ocCtmu8TtsWb+mYeU+GCHf/S66KZF/AsteKHg==", "dev": true }, "qs": { @@ -13782,7 +13750,7 @@ }, "readable-stream": { "version": "2.3.6", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", "dev": true, "requires": { @@ -14208,7 +14176,7 @@ }, "chalk": { "version": "1.1.3", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "resolved": "http://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", "dev": true, "requires": { @@ -14230,7 +14198,7 @@ }, "kind-of": { "version": "1.1.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-1.1.0.tgz", + "resolved": "http://registry.npmjs.org/kind-of/-/kind-of-1.1.0.tgz", "integrity": "sha1-FAo9LUGjbS78+pN3tiwk+ElaXEQ=", "dev": true }, @@ -14282,7 +14250,7 @@ "safer-buffer": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha1-RPoWGwGHuVSd2Eu5GAL5vYOFzWo=", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "dev": true }, "sax": { @@ -14674,7 +14642,7 @@ "debug": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/debug/-/debug-3.1.0.tgz", - "integrity": "sha1-W7WgZyYotkFJVmuhaBnmFRjGcmE=", + "integrity": "sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g==", "dev": true, "requires": { "ms": "2.0.0" @@ -14818,7 +14786,7 @@ }, "chalk": { "version": "1.1.3", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "resolved": "http://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", "dev": true, "optional": true, @@ -15207,7 +15175,7 @@ }, "readable-stream": { "version": "2.3.6", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", "dev": true, "optional": true, @@ -15293,7 +15261,7 @@ }, "readable-stream": { "version": "2.3.6", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", + "resolved": "http://registry.npmjs.org/readable-stream/-/readable-stream-2.3.6.tgz", "integrity": "sha512-tQtKA9WIAhBF3+VLAseyMqZeBjW0AHJoxOtYqSUZNJxauErmLbVm2FW1y+J/YA9dUrAC39ITejlZWhVIwawkKw==", "dev": true, "requires": { @@ -15308,7 +15276,7 @@ }, "string_decoder": { "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "resolved": "http://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", "dev": true, "requires": { @@ -15371,14 +15339,14 @@ "integrity": "sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q==" }, "tinymce": { - "version": "4.9.7", - "resolved": "https://registry.npmjs.org/tinymce/-/tinymce-4.9.7.tgz", - "integrity": "sha512-cj0HvUuniTuIjOAJdRt5BUfeQqM5yHjbA2NOub9HUHXlCrT9OwD9WBPU6tGlaPC2l2I4eGoOnT8llosZRdQU5Q==" + "version": "4.9.10", + "resolved": "https://registry.npmjs.org/tinymce/-/tinymce-4.9.10.tgz", + "integrity": "sha512-vyzGG04Q44Y7zWIKA4c+G7MxMCsed6JkrhU+k0TaDs9XKAiS+e+D3Fzz5OIJ7p5keF7lbRK5czgI8T1JtouZqw==" }, "tmp": { "version": "0.0.33", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", - "integrity": "sha1-bTQzWIl2jSGyvNoKonfO07G/rfk=", + "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", "dev": true, "requires": { "os-tmpdir": "~1.0.2" @@ -15630,7 +15598,7 @@ "ultron": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/ultron/-/ultron-1.1.1.tgz", - "integrity": "sha1-n+FTahCmZKZSZqHjzPhf02MCvJw=", + "integrity": "sha512-UIEXBNeYmKptWH6z8ZnqTeS8fV74zG0/eRU9VGkpzz+LIJNs8W/zM/L+7ctCkRrgbNnnR0xxw4bKOr0cW0N0Og==", "dev": true }, "unbzip2-stream": { @@ -15813,7 +15781,7 @@ "uri-js": { "version": "4.2.2", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.2.2.tgz", - "integrity": "sha1-lMVA4f93KVbiKZUHwBCupsiDjrA=", + "integrity": "sha512-KY9Frmirql91X2Qgjry0Wd4Y+YTdrdZheS8TFwvkbLWf/G5KNJDCh6pKL5OZctEW4+0Baa5idK2ZQuELRwPznQ==", "dev": true, "requires": { "punycode": "^2.1.0" @@ -15892,6 +15860,15 @@ "integrity": "sha512-usZBT3PW+LOjM25wbqIlZwPeJV+3OSz3M1k1Ws8snlW39dZyYL9lOGC5FgPVHfk0jKmjiDV8Z0mIbVQPiwFs7g==", "dev": true }, + "v8flags": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/v8flags/-/v8flags-3.2.0.tgz", + "integrity": "sha512-mH8etigqMfiGWdeXpaaqGfs6BndypxusHHcv2qSHyZkGEznCd/qAXCWWRzeowtL54147cktFOC4P5y+kl8d8Jg==", + "dev": true, + "requires": { + "homedir-polyfill": "^1.0.1" + } + }, "validate-npm-package-license": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", @@ -16186,7 +16163,7 @@ "ws": { "version": "3.3.3", "resolved": "https://registry.npmjs.org/ws/-/ws-3.3.3.tgz", - "integrity": "sha1-8c+E/i1ekB686U767OeF8YeiKPI=", + "integrity": "sha512-nnWLa/NwZSt4KQJu51MYlCcSQ5g7INpOrOMt4XV8j4dqTXdmlUmSHQ8/oLC069ckre0fRsgfvsKwbTdtKLCDkA==", "dev": true, "requires": { "async-limiter": "~1.0.0", @@ -16225,9 +16202,9 @@ "dev": true }, "yargs": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-7.1.0.tgz", - "integrity": "sha1-a6MY6xaWFyf10oT46gA+jWFU0Mg=", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-7.1.1.tgz", + "integrity": "sha512-huO4Fr1f9PmiJJdll5kwoS2e4GqzGSsMT3PPMpOwoVkOK8ckqAewMTZyA6LXVQWflleb/Z8oPBEvNsMft0XE+g==", "dev": true, "requires": { "camelcase": "^3.0.0", @@ -16242,16 +16219,17 @@ "string-width": "^1.0.2", "which-module": "^1.0.0", "y18n": "^3.2.1", - "yargs-parser": "^5.0.0" + "yargs-parser": "5.0.0-security.0" } }, "yargs-parser": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-5.0.0.tgz", - "integrity": "sha1-J17PDX/+Bcd+ZOfIbkzZS/DhIoo=", + "version": "5.0.0-security.0", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-5.0.0-security.0.tgz", + "integrity": "sha512-T69y4Ps64LNesYxeYGYPvfoMTt/7y1XtfpIslUeK4um+9Hu7hlGoRtaDLvdXb7+/tfq4opVa2HRY5xGip022rQ==", "dev": true, "requires": { - "camelcase": "^3.0.0" + "camelcase": "^3.0.0", + "object.assign": "^4.1.0" } }, "yauzl": { diff --git a/src/Umbraco.Web.UI.Client/package.json b/src/Umbraco.Web.UI.Client/package.json index 0d6db9118f..21445e3cb3 100644 --- a/src/Umbraco.Web.UI.Client/package.json +++ b/src/Umbraco.Web.UI.Client/package.json @@ -27,7 +27,7 @@ "angular-ui-sortable": "0.19.0", "animejs": "2.2.0", "bootstrap-social": "5.1.1", - "chart.js": "^2.8.0", + "chart.js": "^2.9.3", "clipboard": "2.0.4", "diff": "3.5.0", "flatpickr": "4.5.2", @@ -39,7 +39,7 @@ "moment": "2.22.2", "ng-file-upload": "12.2.13", "nouislider": "14.4.0", - "npm": "^6.14.0", + "npm": "^6.14.7", "signalr": "2.4.0", "spectrum-colorpicker": "1.8.0", "tinymce": "4.9.10", @@ -57,7 +57,7 @@ "gulp-angular-embed-templates": "^2.3.0", "gulp-babel": "8.0.0", "gulp-clean-css": "4.2.0", - "gulp-cli": "^2.2.0", + "gulp-cli": "^2.3.0", "gulp-concat": "2.6.1", "gulp-eslint": "6.0.0", "gulp-imagemin": "6.1.1", @@ -71,7 +71,6 @@ "gulp-wrap": "0.15.0", "gulp-wrap-js": "0.4.1", "jasmine-core": "3.5.0", - "jasmine-promise-matchers": "^2.6.0", "karma": "4.4.1", "karma-chrome-launcher": "^3.1.0", "karma-jasmine": "2.0.1", diff --git a/src/Umbraco.Web.UI.Client/src/common/mocks/resources/_utils.js b/src/Umbraco.Web.UI.Client/src/common/mocks/resources/_utils.js index efa7046569..8293443dd4 100644 --- a/src/Umbraco.Web.UI.Client/src/common/mocks/resources/_utils.js +++ b/src/Umbraco.Web.UI.Client/src/common/mocks/resources/_utils.js @@ -305,6 +305,7 @@ angular.module('umbraco.mocks'). allowedActions: ["U", "H", "A"], contentTypeAlias: "testAlias", contentTypeKey: "7C5B74D1-E2F9-45A3-AE4B-FC7A829BF8AB", + apps: [], variants: [ { name: "", 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 e23afbabfe..fe57534ffb 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 @@ -14,7 +14,7 @@ 'use strict'; - function blockEditorModelObjectFactory($interpolate, udiService, contentResource, localizationService) { + function blockEditorModelObjectFactory($interpolate, $q, udiService, contentResource, localizationService) { /** * Simple mapping from property model content entry to editing model, @@ -219,7 +219,7 @@ * @param {any} contentData * @param {any} udi */ - function ensureUdiAndKey(contentData, udi) { + function ensureUdiAndKey(contentData, udi) { contentData.udi = udi; // Change the content.key to the GUID part of the udi, else it's just random which we don't want, it must be consistent contentData.key = udiService.getKey(udi); @@ -236,7 +236,7 @@ /** - * Formats the content apps and ensures unsupported property's have the notsupported view + * Formats the content apps and ensures unsupported property's have the notsupported view (returns a promise) * @param {any} scaffold */ function formatScaffoldData(scaffold) { @@ -252,10 +252,17 @@ }); }); + // could be empty in tests + if (!scaffold.apps) { + console.warn("No content apps found in scaffold"); + return $q.resolve(scaffold); + } + // replace view of content app + var contentApp = scaffold.apps.find(entry => entry.alias === "umbContent"); if (contentApp) { - contentApp.view = "views/common/infiniteeditors/blockeditor/blockeditor.content.html"; + contentApp.view = "views/common/infiniteeditors/blockeditor/blockeditor.content.html"; } // remove info app @@ -267,6 +274,7 @@ // add the settings app return localizationService.localize("blockEditor_tabBlockSettings").then( function (settingsName) { + var settingsTab = { "name": settingsName, "alias": "settings", @@ -379,19 +387,24 @@ // removing duplicates. scaffoldKeys = scaffoldKeys.filter((value, index, self) => self.indexOf(value) === index); - scaffoldKeys.forEach((contentTypeKey => { + var self = this; + + scaffoldKeys.forEach(contentTypeKey => { tasks.push(contentResource.getScaffoldByKey(-20, contentTypeKey).then(scaffold => { - // this.scaffolds might not exists anymore, this happens if this instance has been destroyed before the load is complete. - if (this.scaffolds) { - return formatScaffoldData(scaffold).then(s => this.scaffolds.push(s)); + // self.scaffolds might not exists anymore, this happens if this instance has been destroyed before the load is complete. + if (self.scaffolds) { + return formatScaffoldData(scaffold).then(s => { + self.scaffolds.push(s); + return s; + }); } else { - return Promise.resolve(); + return $q.resolve(scaffold); } })); - })); + }); - return Promise.all(tasks); + return $q.all(tasks); }, /** diff --git a/src/Umbraco.Web.UI.Client/src/common/services/localization.service.js b/src/Umbraco.Web.UI.Client/src/common/services/localization.service.js index 6081cbd9ad..f8493ab39d 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/localization.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/localization.service.js @@ -67,6 +67,9 @@ angular.module('umbraco.services') // loads the language resource file from the server initLocalizedResources: function () { + + // TODO: This promise handling is super ugly, we should just be returnning the promise from $http and returning inner values. + var deferred = $q.defer(); if (resourceFileLoadStatus === "loaded") { @@ -179,7 +182,7 @@ angular.module('umbraco.services') */ localize: function (value, tokens, fallbackValue) { return service.initLocalizedResources().then(function (dic) { - return _lookup(value, tokens, dic, fallbackValue); + return _lookup(value, tokens, dic, fallbackValue); }); }, 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 5883f59be8..d0f59c110f 100644 --- a/src/Umbraco.Web.UI.Client/test/config/karma.conf.js +++ b/src/Umbraco.Web.UI.Client/test/config/karma.conf.js @@ -11,7 +11,6 @@ module.exports = function (config) { files: [ // Jasmine plugins - 'node_modules/jasmine-promise-matchers/dist/jasmine-promise-matchers.js', //libraries 'node_modules/jquery/dist/jquery.min.js', diff --git a/src/Umbraco.Web.UI.Client/test/unit/common/services/block-editor-service.spec.js b/src/Umbraco.Web.UI.Client/test/unit/common/services/block-editor-service.spec.js index e4e1133bd6..71404b5e85 100644 --- a/src/Umbraco.Web.UI.Client/test/unit/common/services/block-editor-service.spec.js +++ b/src/Umbraco.Web.UI.Client/test/unit/common/services/block-editor-service.spec.js @@ -5,24 +5,34 @@ var settingsKey = "2AF42343-C8A2-400D-BA43-4818C2B3CDC5"; var settingsUdi = "umb://element/2AF42343C8A2400DBA434818C2B3CDC5"; - var blockEditorService, contentResource, $rootScope, $scope; + var blockEditorService, contentResource, $rootScope, $scope, $q, localizationService, $timeout; beforeEach(module('umbraco.services')); beforeEach(module('umbraco.resources')); beforeEach(module('umbraco.mocks')); beforeEach(module('umbraco')); - - beforeEach(inject(function ($injector, mocksUtils, _$rootScope_) { + + beforeEach(inject(function ($injector, mocksUtils, _$rootScope_, _$q_, _$timeout_) { mocksUtils.disableAuth(); $rootScope = _$rootScope_; $scope = $rootScope.$new(); + $q = _$q_; + $timeout = _$timeout_; contentResource = $injector.get("contentResource"); spyOn(contentResource, "getScaffoldByKey").and.callFake( function () { - return Promise.resolve(mocksUtils.getMockVariantContent(1234, contentKey, contentUdi)) + var scaffold = mocksUtils.getMockVariantContent(1234, contentKey, contentUdi); + return $q.resolve(scaffold); + } + ); + // this seems to be required because of the poor promise implementation in localizationService (see TODO in that service) + localizationService = $injector.get("localizationService"); + spyOn(localizationService, "localize").and.callFake( + function () { + return $q.resolve("Localized test text"); } ); @@ -101,13 +111,19 @@ it('load provides data for itemPicker', function (done) { var modelObject = blockEditorService.createModelObject({}, "Umbraco.TestBlockEditor", [blockConfigurationMock], $scope, $scope); - modelObject.load().then(() => { - var itemPickerOptions = modelObject.getAvailableBlocksForBlockPicker(); - expect(itemPickerOptions.length).toBe(1); - expect(itemPickerOptions[0].blockConfigModel.contentTypeKey).toBe(blockConfigurationMock.contentTypeKey); - done(); + modelObject.load().then(() => { + try { + var itemPickerOptions = modelObject.getAvailableBlocksForBlockPicker(); + expect(itemPickerOptions.length).toBe(1); + expect(itemPickerOptions[0].blockConfigModel.contentTypeKey).toBe(blockConfigurationMock.contentTypeKey); + done(); + } catch (e) { + done.fail(e); + } }); + $rootScope.$digest(); + $timeout.flush(); }); it('getLayoutEntry has values', function (done) { @@ -117,16 +133,22 @@ modelObject.load().then(() => { - var layout = modelObject.getLayout(); + try { + var layout = modelObject.getLayout(); - expect(layout).not.toBeUndefined(); - expect(layout.length).toBe(1); - expect(layout[0]).toBe(propertyModelMock.layout["Umbraco.TestBlockEditor"][0]); - expect(layout[0].udi).toBe(propertyModelMock.layout["Umbraco.TestBlockEditor"][0].udi); + expect(layout).not.toBeUndefined(); + expect(layout.length).toBe(1); + expect(layout[0]).toBe(propertyModelMock.layout["Umbraco.TestBlockEditor"][0]); + expect(layout[0].udi).toBe(propertyModelMock.layout["Umbraco.TestBlockEditor"][0].udi); - done(); + done(); + } catch (e) { + done.fail(e); + } }); + $rootScope.$digest(); + $timeout.flush(); }); it('getBlockObject has values', function (done) { @@ -151,6 +173,8 @@ } }); + $rootScope.$digest(); + $timeout.flush(); }); @@ -169,18 +193,23 @@ blockObject.content.variants[0].tabs[0].properties[0].value = "anotherTestValue"; - $rootScope.$digest();// invoke angularJS Store. + // invoke angularJS Store. + $timeout(function () { + expect(blockObject.data).toEqual(propertyModel.contentData[0]); + expect(blockObject.data.testproperty).toBe("anotherTestValue"); + expect(propertyModel.contentData[0].testproperty).toBe("anotherTestValue"); - expect(blockObject.data).toEqual(propertyModel.contentData[0]); - expect(blockObject.data.testproperty).toBe("anotherTestValue"); - expect(propertyModel.contentData[0].testproperty).toBe("anotherTestValue"); + done(); + }); - done(); } catch (e) { done.fail(e); } }); + $rootScope.$digest(); + $timeout.flush(); + }); @@ -204,17 +233,22 @@ blockObject.content.variants[0].tabs[0].properties[0].value.list[0] = "AA"; blockObject.content.variants[0].tabs[0].properties[0].value.list.push("D"); - $rootScope.$digest();// invoke angularJS Store. + // invoke angularJS Store. + $timeout(function () { + expect(propertyModel.contentData[0].testproperty.list[0]).toBe("AA"); + expect(propertyModel.contentData[0].testproperty.list.length).toBe(4); - expect(propertyModel.contentData[0].testproperty.list[0]).toBe("AA"); - expect(propertyModel.contentData[0].testproperty.list.length).toBe(4); + done(); + }); - done(); + } catch (e) { done.fail(e); - } + } }); + $rootScope.$digest(); + $timeout.flush(); }); it('layout is referencing layout of propertyModel', function (done) { @@ -236,6 +270,8 @@ done(); }); + $rootScope.$digest(); + $timeout.flush(); }); it('removeDataAndDestroyModel removes data', function (done) { @@ -270,6 +306,9 @@ done.fail(e); } }); + + $rootScope.$digest(); + $timeout.flush(); }); it('getBlockObject of block with settings has values', function (done) { @@ -291,6 +330,8 @@ done(); }); + $rootScope.$digest(); + $timeout.flush(); }); @@ -309,21 +350,23 @@ blockObject.content.variants[0].tabs[0].properties[0].value = "anotherTestValue"; blockObject.settings.variants[0].tabs[0].properties[0].value = "anotherTestValueForSettings"; - $rootScope.$digest();// invoke angularJS Store. + // invoke angularJS Store. + $timeout(function () { + expect(blockObject.data).toEqual(propertyModel.contentData[0]); + expect(blockObject.data.testproperty).toBe("anotherTestValue"); + expect(propertyModel.contentData[0].testproperty).toBe("anotherTestValue"); - expect(blockObject.data).toEqual(propertyModel.contentData[0]); - expect(blockObject.data.testproperty).toBe("anotherTestValue"); - expect(propertyModel.contentData[0].testproperty).toBe("anotherTestValue"); + expect(blockObject.settingsData).toEqual(propertyModel.settingsData[0]); + expect(blockObject.settingsData.testproperty).toBe("anotherTestValueForSettings"); + expect(propertyModel.settingsData[0].testproperty).toBe("anotherTestValueForSettings"); - expect(blockObject.settingsData).toEqual(propertyModel.settingsData[0]); - expect(blockObject.settingsData.testproperty).toBe("anotherTestValueForSettings"); - expect(propertyModel.settingsData[0].testproperty).toBe("anotherTestValueForSettings"); - - // - - done(); + done(); + }); + }); + $rootScope.$digest(); + $timeout.flush(); }); @@ -352,20 +395,25 @@ blockObject.settings.variants[0].tabs[0].properties[0].value.list[0] = "settingsValue"; blockObject.settings.variants[0].tabs[0].properties[0].value.list.push("settingsNewValue"); - $rootScope.$digest();// invoke angularJS Store. + // invoke angularJS Store. + $timeout(function () { + expect(propertyModel.contentData[0].testproperty.list[0]).toBe("AA"); + expect(propertyModel.contentData[0].testproperty.list.length).toBe(4); - expect(propertyModel.contentData[0].testproperty.list[0]).toBe("AA"); - expect(propertyModel.contentData[0].testproperty.list.length).toBe(4); + expect(propertyModel.settingsData[0].testproperty.list[0]).toBe("settingsValue"); + expect(propertyModel.settingsData[0].testproperty.list.length).toBe(4); - expect(propertyModel.settingsData[0].testproperty.list[0]).toBe("settingsValue"); - expect(propertyModel.settingsData[0].testproperty.list.length).toBe(4); + done(); + }); - done(); + } catch (e) { done.fail(e); } }); + $rootScope.$digest(); + $timeout.flush(); }); From 99fc6ef587625ac14accb391d3ad3aa9d178e7a9 Mon Sep 17 00:00:00 2001 From: Shannon Date: Thu, 30 Jul 2020 15:47:14 +1000 Subject: [PATCH 370/377] cleanup TODOs --- .editorconfig | 79 ++++++++++--------- .../validation/valpropertymsg.directive.js | 1 - .../validation/valservermatch.directive.js | 2 +- .../services/contenteditinghelper.service.js | 1 - 4 files changed, 42 insertions(+), 41 deletions(-) diff --git a/.editorconfig b/.editorconfig index 5a35b71ce6..ca85827de8 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,40 +1,43 @@ -# editorconfig.org - -# top-most EditorConfig file -root = true - -# Default settings: -# A newline ending every file -# Use 4 spaces as indentation -[*] -insert_final_newline = true -end_of_line = crlf -indent_style = space -indent_size = 4 - -# Trim trailing whitespace, limited support. -# https://github.com/editorconfig/editorconfig/wiki/Property-research:-Trim-trailing-spaces -trim_trailing_whitespace = true - -[*.{cs,vb}] -dotnet_style_predefined_type_for_locals_parameters_members = true:error - -dotnet_naming_rule.private_members_with_underscore.symbols = private_fields -dotnet_naming_rule.private_members_with_underscore.style = prefix_underscore -dotnet_naming_rule.private_members_with_underscore.severity = suggestion - -dotnet_naming_symbols.private_fields.applicable_kinds = field -dotnet_naming_symbols.private_fields.applicable_accessibilities = private - -dotnet_naming_style.prefix_underscore.capitalization = camel_case -dotnet_naming_style.prefix_underscore.required_prefix = _ - -# https://github.com/MicrosoftDocs/visualstudio-docs/blob/master/docs/ide/editorconfig-code-style-settings-reference.md -[*.cs] -csharp_style_var_for_built_in_types = true:suggestion -csharp_style_var_when_type_is_apparent = true:suggestion -csharp_style_var_elsewhere = true:suggestion +# editorconfig.org + +# top-most EditorConfig file +root = true + +# Default settings: +# A newline ending every file +# Use 4 spaces as indentation +[*] +insert_final_newline = true +end_of_line = crlf +indent_style = space +indent_size = 4 + +# Trim trailing whitespace, limited support. +# https://github.com/editorconfig/editorconfig/wiki/Property-research:-Trim-trailing-spaces +trim_trailing_whitespace = true + +[*.{cs,vb}] +dotnet_style_predefined_type_for_locals_parameters_members = true:error + +dotnet_naming_rule.private_members_with_underscore.symbols = private_fields +dotnet_naming_rule.private_members_with_underscore.style = prefix_underscore +dotnet_naming_rule.private_members_with_underscore.severity = suggestion + +dotnet_naming_symbols.private_fields.applicable_kinds = field +dotnet_naming_symbols.private_fields.applicable_accessibilities = private + +dotnet_naming_style.prefix_underscore.capitalization = camel_case +dotnet_naming_style.prefix_underscore.required_prefix = _ + +# https://github.com/MicrosoftDocs/visualstudio-docs/blob/master/docs/ide/editorconfig-code-style-settings-reference.md +[*.cs] +csharp_style_var_for_built_in_types = true:suggestion +csharp_style_var_when_type_is_apparent = true:suggestion +csharp_style_var_elsewhere = true:suggestion csharp_prefer_braces = false : none - -[*.{js,less}] + +[*.js] +trim_trailing_whitespace = true + +[*.less] trim_trailing_whitespace = false diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/validation/valpropertymsg.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/validation/valpropertymsg.directive.js index cbbd78bfe1..979ac23bb3 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/validation/valpropertymsg.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/validation/valpropertymsg.directive.js @@ -171,7 +171,6 @@ function valPropertyMsg(serverValidationManager, localizationService, angularHel // we find all ngModel controls recursively on this form (but stop recursing before we get to the next) // umbProperty form). Then for each ngModelController we assign a new $validator. This $validator // will execute whenever the value is changed which allows us to check and reset the server validator - // TODO: Is this even needed? function startWatch() { if (!watcher) { diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/validation/valservermatch.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/validation/valservermatch.directive.js index c3f10ae673..1b4c593735 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/validation/valservermatch.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/validation/valservermatch.directive.js @@ -49,7 +49,7 @@ function valServerMatch(serverValidationManager) { // if we have reached this part, and there is no culture, then lets fallback to invariant. To get the validation feedback for invariant language. currentCulture = currentCulture || "invariant"; - + var unsubscribe = []; function bindCallback(validationKey, matchVal, matchType) { diff --git a/src/Umbraco.Web.UI.Client/src/common/services/contenteditinghelper.service.js b/src/Umbraco.Web.UI.Client/src/common/services/contenteditinghelper.service.js index c73b6e1848..cda10ee6df 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/contenteditinghelper.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/contenteditinghelper.service.js @@ -634,7 +634,6 @@ function contentEditingHelper(fileManager, $q, $location, $routeParams, editorSt formHelper.handleServerValidation(args.err.data.ModelState); //add model state errors to notifications - // TODO: Need to ignore complex messages if (args.showNotifications) { showNotificationsForModelsState(args.err.data.ModelState); } From f0cc7a493d68eb8171b6f32b6f39556369074e1e Mon Sep 17 00:00:00 2001 From: Shannon Date: Thu, 30 Jul 2020 16:03:31 +1000 Subject: [PATCH 371/377] Cleanup servervalidationmgr booleans and moves to more readable functions with comments --- .../services/servervalidationmgr.service.js | 107 +++++++++++------- 1 file changed, 67 insertions(+), 40 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/services/servervalidationmgr.service.js b/src/Umbraco.Web.UI.Client/src/common/services/servervalidationmgr.service.js index 2a889d676d..731cc5ed36 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/servervalidationmgr.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/servervalidationmgr.service.js @@ -31,7 +31,7 @@ function serverValidationManager($timeout) { // - segment // The object also contains: // - errorMsg - var items = []; + var errorMsgItems = []; var defaultMatchOptions = { matchType: null @@ -43,7 +43,7 @@ function serverValidationManager($timeout) { callback.apply(instance, [ isValid, // pass in a value indicating it is invalid errorsForCallback, // pass in the errors for this item - items, // pass in all errors in total + errorMsgItems, // pass in all errors in total culture, // pass the culture that we are listing for. segment // pass the segment that we are listing for. ] @@ -62,8 +62,8 @@ function serverValidationManager($timeout) { function notify() { $timeout(function () { - for (var i = 0; i < items.length; i++) { - var item = items[i]; + for (var i = 0; i < errorMsgItems.length; i++) { + var item = errorMsgItems[i]; } notifyCallbacks(); }); @@ -75,7 +75,7 @@ function serverValidationManager($timeout) { } //find errors for this field name - return _.filter(items, function (item) { + return _.filter(errorMsgItems, function (item) { return (item.propertyAlias === null && item.culture === "invariant" && item.fieldName === fieldName); }); } @@ -100,30 +100,44 @@ function serverValidationManager($timeout) { } //find all errors for this property - return _.filter(items, function (item) { + return _.filter(errorMsgItems, function (errMsgItem) { - if (!item.propertyAlias) { + if (!errMsgItem.propertyAlias) { return false; } - var matchProp = item.propertyAlias === propertyAlias - ? true - : options.matchType === "prefix" - ? item.propertyAlias.startsWith(propertyAlias + '/') - : options.matchType === "suffix" - ? item.propertyAlias.endsWith('/' + propertyAlias) - : options.matchType === "contains" - ? item.propertyAlias.includes('/' + propertyAlias + '/') - : false; + var matchProp = matchErrMsgItemProperty(errMsgItem, propertyAlias, options); return matchProp - && item.culture === culture - && item.segment === segment + && errMsgItem.culture === culture + && errMsgItem.segment === segment // ignore field matching if match options are used - && (options.matchType || (item.fieldName === fieldName || (fieldName === undefined || fieldName === ""))); + && (options.matchType || (errMsgItem.fieldName === fieldName || (fieldName === undefined || fieldName === ""))); }); } + /** + * Returns true if the error message item's data matches the property validation key with a match type provided by the options + * @param {any} errMsgItem The error message item + * @param {any} propertyValidationKey The property validation key) + * @param {any} options The match type options + */ + function matchErrMsgItemProperty(errMsgItem, propertyValidationKey, options) { + if (errMsgItem.propertyAlias === propertyValidationKey) { + return true; + } + if (options.matchType === "prefix" && errMsgItem.propertyAlias.startsWith(propertyValidationKey + '/')) { + return true; + } + if (options.matchType === "suffix" && errMsgItem.propertyAlias.endsWith('/' + propertyValidationKey)) { + return true; + } + if (options.matchType === "contains" && errMsgItem.propertyAlias.includes('/' + propertyValidationKey + '/')) { + return true; + } + return false; + } + function getVariantErrors(culture, segment) { if (!culture) { @@ -134,7 +148,7 @@ function serverValidationManager($timeout) { } //find all errors for this property - return _.filter(items, function (item) { + return _.filter(errorMsgItems, function (item) { return (item.culture === culture && item.segment === segment); }); } @@ -164,7 +178,7 @@ function serverValidationManager($timeout) { function notifyCallbacks() { // nothing to call - if (items.length === 0) { + if (errorMsgItems.length === 0) { return; } @@ -243,15 +257,7 @@ function serverValidationManager($timeout) { cb.options = defaultMatchOptions; } - var matchProp = cb.propertyAlias === propertyAlias - ? true - : cb.options.matchType === "prefix" - ? propertyAlias.startsWith(cb.propertyAlias + '/') - : cb.options.matchType === "suffix" - ? propertyAlias.endsWith('/' + cb.propertyAlias) - : cb.options.matchType === "contains" - ? propertyAlias.includes('/' + cb.propertyAlias + '/') - : false; + var matchProp = matchCallbackItemProperty(cb, propertyAlias); //returns any callback that have been registered directly against the field and for only the property return matchProp @@ -263,6 +269,27 @@ function serverValidationManager($timeout) { return found; } + /** + * Returns true if the callback item's data and match options matches the property validation key + * @param {any} cb + * @param {any} propertyValidationKey + */ + function matchCallbackItemProperty(cb, propertyValidationKey) { + if (cb.propertyAlias === propertyValidationKey) { + return true; + } + if (cb.options.matchType === "prefix" && propertyValidationKey.startsWith(cb.propertyAlias + '/')) { + return true; + } + if (cb.options.matchType === "suffix" && propertyValidationKey.endsWith('/' + cb.propertyAlias)) { + return true; + } + if (cb.options.matchType === "contains" && propertyValidationKey.includes('/' + cb.propertyAlias + '/')) { + return true; + } + return false; + } + /** * @ngdoc function * @name getFieldCallbacks @@ -313,7 +340,7 @@ function serverValidationManager($timeout) { //only add the item if it doesn't exist if (!hasFieldError(fieldName)) { - items.push({ + errorMsgItems.push({ propertyAlias: null, culture: "invariant", segment: null, @@ -379,7 +406,7 @@ function serverValidationManager($timeout) { //only add the item if it doesn't exist if (!hasPropertyError(propertyAlias, culture, fieldName, segment)) { - items.push({ + errorMsgItems.push({ propertyAlias: propertyAlias, culture: culture, segment: segment, @@ -409,7 +436,7 @@ function serverValidationManager($timeout) { segment = null; } - var err = _.find(items, function (item) { + var err = _.find(errorMsgItems, function (item) { //return true if the property alias matches and if an empty field name is specified or the field name matches return (item.propertyAlias === propertyAlias && item.culture === culture && item.segment === segment && (item.fieldName === fieldName || (fieldName === undefined || fieldName === ""))); }); @@ -426,7 +453,7 @@ function serverValidationManager($timeout) { * Checks if a content field has an error */ function hasFieldError(fieldName) { - var err = _.find(items, function (item) { + var err = _.find(errorMsgItems, function (item) { //return true if the property alias matches and if an empty field name is specified or the field name matches return (item.propertyAlias === null && item.culture === "invariant" && item.segment === null && item.fieldName === fieldName); }); @@ -559,7 +586,7 @@ function serverValidationManager($timeout) { * Clears all errors */ function clear() { - items = []; + errorMsgItems = []; } var instance = { @@ -743,7 +770,7 @@ function serverValidationManager($timeout) { removePropertyError: function (propertyAlias, culture, fieldName, segment, options) { var errors = getPropertyErrors(propertyAlias, culture, segment, fieldName, options); - items = items.filter(v => errors.indexOf(v) === -1); + errorMsgItems = errorMsgItems.filter(v => errors.indexOf(v) === -1); if (errors.length > 0) { // removal was successful, re-notify all subscribers @@ -785,7 +812,7 @@ function serverValidationManager($timeout) { * Gets the error message for a content field */ getFieldError: function (fieldName) { - var err = _.find(items, function (item) { + var err = _.find(errorMsgItems, function (item) { //return true if the property alias matches and if an empty field name is specified or the field name matches return (item.propertyAlias === null && item.culture === "invariant" && item.segment === null && item.fieldName === fieldName); }); @@ -811,7 +838,7 @@ function serverValidationManager($timeout) { culture = "invariant"; } - var err = _.find(items, function (item) { + var err = _.find(errorMsgItems, function (item) { return (item.culture === culture && item.segment === null); }); return err ? true : false; @@ -837,7 +864,7 @@ function serverValidationManager($timeout) { segment = null; } - var err = _.find(items, function (item) { + var err = _.find(errorMsgItems, function (item) { return (item.culture === culture && item.segment === segment); }); return err ? true : false; @@ -848,7 +875,7 @@ function serverValidationManager($timeout) { // Used to return the 'items' array as a reference/getter Object.defineProperty(instance, "items", { get: function () { - return items; + return errorMsgItems; }, set: function (value) { throw "Cannot set the items array"; From e387c3b27f928ae7dca0a04ec87dd8718a46e121 Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Wed, 8 Jan 2020 10:39:18 +0100 Subject: [PATCH 372/377] Fix JS error when trying to insert a link in an empty RTE --- .../src/common/resources/entity.resource.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/resources/entity.resource.js b/src/Umbraco.Web.UI.Client/src/common/resources/entity.resource.js index 61d646afc0..838e8f1b80 100644 --- a/src/Umbraco.Web.UI.Client/src/common/resources/entity.resource.js +++ b/src/Umbraco.Web.UI.Client/src/common/resources/entity.resource.js @@ -207,7 +207,7 @@ function entityResource($q, $http, umbRequestHelper) { getAnchors: function (rteContent) { if (!rteContent || rteContent.length === 0) { - return []; + return $q.when([]); } return umbRequestHelper.resourcePromise( From a93cc88756eb0bb190895c613ede64c556011029 Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Wed, 8 Jan 2020 10:01:20 +0100 Subject: [PATCH 373/377] Don't show empty search results on top of the search input field in the linkpicker --- .../components/tree/umbtreesearchresults.directive.js | 5 +++-- .../views/common/infiniteeditors/linkpicker/linkpicker.html | 3 ++- .../src/views/components/tree/umb-tree-search-results.html | 2 +- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/tree/umbtreesearchresults.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/tree/umbtreesearchresults.directive.js index bc74cbb13b..b006f75e6b 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/tree/umbtreesearchresults.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/tree/umbtreesearchresults.directive.js @@ -9,13 +9,14 @@ function treeSearchResults() { return { scope: { results: "=", - selectResultCallback: "=" + selectResultCallback: "=", + emptySearchResultPosition: '@' }, restrict: "E", // restrict to an element replace: true, // replace the html element with the template templateUrl: 'views/components/tree/umb-tree-search-results.html', link: function (scope, element, attrs, ctrl) { - + scope.emptySearchResultPosition = scope.emptySearchResultPosition || "center"; } }; } diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/linkpicker/linkpicker.html b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/linkpicker/linkpicker.html index ad0aaab57c..65148d5cc6 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/linkpicker/linkpicker.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/linkpicker/linkpicker.html @@ -76,7 +76,8 @@ + select-result-callback="selectResult" + empty-search-result-position="default">
        diff --git a/src/Umbraco.Web.UI.Client/src/views/components/tree/umb-tree-search-results.html b/src/Umbraco.Web.UI.Client/src/views/components/tree/umb-tree-search-results.html index b5eedaa24f..2a476c0500 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/tree/umb-tree-search-results.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/tree/umb-tree-search-results.html @@ -1,5 +1,5 @@
        - + From e8332a16ae49e42d4ebc55d3726910f447a4890c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Thu, 30 Jul 2020 14:44:13 +0200 Subject: [PATCH 374/377] make inline editor bar white --- .../blocklistentryeditors/inlineblock/inlineblock.editor.less | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklistentryeditors/inlineblock/inlineblock.editor.less b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklistentryeditors/inlineblock/inlineblock.editor.less index a31ffc172c..2be20946b8 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklistentryeditors/inlineblock/inlineblock.editor.less +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklistentryeditors/inlineblock/inlineblock.editor.less @@ -19,6 +19,7 @@ padding-left: 10px; padding-bottom: 2px; user-select: none; + background-color: white; .caret { transform: rotate(-90deg); From d5c0e79c40a371358890ef0ef25c07a639697cf8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Thu, 30 Jul 2020 14:47:16 +0200 Subject: [PATCH 375/377] make label bar white --- .../blocklistentryeditors/labelblock/labelblock.editor.less | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklistentryeditors/labelblock/labelblock.editor.less b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklistentryeditors/labelblock/labelblock.editor.less index f9fde1da97..51fb7242ef 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklistentryeditors/labelblock/labelblock.editor.less +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/blocklist/blocklistentryeditors/labelblock/labelblock.editor.less @@ -9,6 +9,7 @@ border-radius: @baseBorderRadius; cursor: pointer; color: @ui-action-discreet-type; + background-color: white; text-align: left; padding-left: 20px; padding-bottom: 2px; From a0034c51f9f4696cd4f8fd56efa2adfe53dfa145 Mon Sep 17 00:00:00 2001 From: Ian Houghton Date: Wed, 6 May 2020 12:58:00 +0100 Subject: [PATCH 376/377] fix #7842 - Render a larger textarea for editing dictionary items --- .../components/umbtextarea.directive.js | 59 +++++++++++++++++++ .../src/views/dictionary/edit.html | 2 +- 2 files changed, 60 insertions(+), 1 deletion(-) create mode 100644 src/Umbraco.Web.UI.Client/src/common/directives/components/umbtextarea.directive.js diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbtextarea.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbtextarea.directive.js new file mode 100644 index 0000000000..8b584b873c --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbtextarea.directive.js @@ -0,0 +1,59 @@ +(function () { + 'use strict'; + + function umbTextarea($document) { + + function autogrow(scope, element, attributes) { + if (!element.hasClass("autogrow")) { + // no autogrow for you today + return; + } + + // get possible minimum height style + var minHeight = parseInt(window.getComputedStyle(element[0]).getPropertyValue("min-height")) || 0; + + // prevent newlines in textbox + element.on("keydown", function (evt) { + if (evt.which === 13) { + //evt.preventDefault(); + } + }); + + element.on("input", function (evt) { + element.css({ + height: 'auto', + minHeight: 0 + }); + + var contentHeight = this.scrollHeight; + var borderHeight = 1; + var paddingHeight = 4; + + element.css({ + minHeight: null, // remove property + height: contentHeight + borderHeight + paddingHeight + "px" // because we're using border-box + }); + }); + + // watch model changes from the outside to adjust height + scope.$watch(attributes.ngModel, trigger); + + // set initial size + trigger(); + + function trigger() { + setTimeout(element.triggerHandler.bind(element, "input"), 1); + } + } + + var directive = { + restrict: 'E', + link: autogrow + }; + + return directive; + } + + angular.module('umbraco.directives').directive('textarea', umbTextarea); + +})(); diff --git a/src/Umbraco.Web.UI.Client/src/views/dictionary/edit.html b/src/Umbraco.Web.UI.Client/src/views/dictionary/edit.html index c2b0adec6f..e467588219 100644 --- a/src/Umbraco.Web.UI.Client/src/views/dictionary/edit.html +++ b/src/Umbraco.Web.UI.Client/src/views/dictionary/edit.html @@ -23,7 +23,7 @@

        - +
        From 87383db814b463c67a2036e9512a3313042ff508 Mon Sep 17 00:00:00 2001 From: "Nikolaj E. Lauridsen" Date: Mon, 3 Aug 2020 12:04:22 +0200 Subject: [PATCH 377/377] Fix button selectors --- .../cypress/integration/Settings/documentTypes.ts | 2 +- .../cypress/integration/Settings/mediaTypes.ts | 2 +- .../cypress/integration/Settings/memberTypes.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Settings/documentTypes.ts b/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Settings/documentTypes.ts index 5a5ba0b3e0..e0a651731a 100644 --- a/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Settings/documentTypes.ts +++ b/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Settings/documentTypes.ts @@ -34,7 +34,7 @@ context('Document Types', () => { cy.get('.umb-search-field').type('Textstring'); // Choose first item - cy.get('ul.umb-card-grid li a[title="Textstring"]').closest("li").click(); + cy.get('ul.umb-card-grid li [title="Textstring"]').closest("li").click(); // Save property cy.get('.btn-success').last().click(); diff --git a/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Settings/mediaTypes.ts b/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Settings/mediaTypes.ts index a963da754f..646654bbab 100644 --- a/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Settings/mediaTypes.ts +++ b/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Settings/mediaTypes.ts @@ -34,7 +34,7 @@ context('Media Types', () => { cy.get('.umb-search-field').type('Textstring'); // Choose first item - cy.get('ul.umb-card-grid li a[title="Textstring"]').closest("li").click(); + cy.get('ul.umb-card-grid li [title="Textstring"]').closest("li").click(); // Save property cy.get('.btn-success').last().click(); diff --git a/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Settings/memberTypes.ts b/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Settings/memberTypes.ts index 53823f2a69..fe2d88d64f 100644 --- a/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Settings/memberTypes.ts +++ b/src/Umbraco.Tests.AcceptanceTest/cypress/integration/Settings/memberTypes.ts @@ -32,7 +32,7 @@ context('Member Types', () => { cy.get('.umb-search-field').type('Textstring'); // Choose first item - cy.get('ul.umb-card-grid li a[title="Textstring"]').closest("li").click(); + cy.get('ul.umb-card-grid li [title="Textstring"]').closest("li").click(); // Save property cy.get('.btn-success').last().click();