From 988be21990ef56c6b452fb4a83b28cfa67a08b0c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Wed, 13 Feb 2019 13:20:01 +0100 Subject: [PATCH 01/50] making the grid work in splitview --- .../src/less/components/umb-grid.less | 37 +++++----- .../propertyeditors/grid/grid.controller.js | 68 +++++++++++-------- .../src/views/propertyeditors/grid/grid.html | 40 +++++------ 3 files changed, 81 insertions(+), 64 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-grid.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-grid.less index e7f24d08fa..6bf8cd612a 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-grid.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-grid.less @@ -128,9 +128,10 @@ position: relative; margin-bottom: 40px; padding-top: 10px; + border: 1px solid @grayLighter; &:hover { - background-color: @grayLighter; + border-color: @grayLight; } &[ng-click], @@ -161,15 +162,16 @@ } .umb-grid .umb-row .umb-cell-placeholder { - min-height: 130px; - background-color: @gray-10; - border-width: 2px; + min-height: 88px; + border-width: 1px; border-style: dashed; - border-color: @gray-8; + border-color: @ui-action-disgrete-border; + color: @ui-action-disgrete-type; transition: border-color 100ms linear; &:hover { - border-color: @blueMid; + border-color: @ui-action-disgrete-border-hover; + color: @ui-action-disgrete-type-hover; cursor: pointer; } } @@ -207,9 +209,9 @@ } .umb-grid .cell-tools-add { - color: @ui-action-type; + color: @ui-action-disgrete-type; &:focus, &:hover { - color: @ui-action-type-hover; + color: @ui-action-disgrete-type-hover; text-decoration: none; } } @@ -219,16 +221,18 @@ top: 50%; left: 50%; transform: translate(-50%, -50%); - color: @ui-action-type; + color: @ui-action-disgrete-type; } .umb-grid .cell-tools-add.-bar { display: block; - background: @gray-10; text-align: center; padding: 5px; - border: 1px dashed @gray-7; + border: 1px dashed @ui-action-disgrete-border; margin: 10px; + &:focus, &:hover { + border-color: @ui-action-disgrete-border-hover; + } } @@ -249,7 +253,6 @@ .umb-grid .cell-tools-edit { display: inline-block; - color: @white; } .umb-grid .drop-overlay { @@ -412,12 +415,12 @@ // Row states .umb-grid .umb-row.-active { - background-color: @ui-action-type; + border-color: @ui-action-type; .umb-row-title-bar { cursor: move; } - + /* .row-tool, .umb-row-title { color: @white; @@ -436,12 +439,13 @@ .umb-cell .umb-cell-content { border-color: transparent; } + */ } .umb-grid .umb-row.-active-child { background-color: @gray-10; - + .umb-row-title-bar { cursor: inherit; } @@ -449,7 +453,7 @@ .umb-row-title { color: @gray-3; } - + /* .row-tool { color: fade(@black, 23); } @@ -465,6 +469,7 @@ border-color: fade(@gray, 44); } } + */ } diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/grid.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/grid.controller.js index b40305f5f5..0ea5842612 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/grid.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/grid.controller.js @@ -14,11 +14,16 @@ angular.module("umbraco") // Grid status variables var placeHolder = ""; var currentForm = angularHelper.getCurrentForm($scope); - + + $scope.currentRowWithActiveChild = null; + $scope.currentCellWithActiveChild = null; + $scope.active = null; + $scope.currentRow = null; $scope.currentCell = null; $scope.currentToolsControl = null; $scope.currentControl = null; + $scope.openRTEToolbarId = null; $scope.hasSettings = false; $scope.showRowConfigurations = true; @@ -235,10 +240,16 @@ angular.module("umbraco") }, 500, false); $scope.$apply(function () { - + + console.log("$apply function called...") + var cell = $(e.target).scope().area; - cell.hasActiveChild = hasActiveChild(cell, cell.controls); - cell.active = false; + + if(hasActiveChild(cell, cell.controls)) { + $scope.currentCellWithActiveChild = cell; + } + $scope.active = cell; + }); } @@ -307,12 +318,13 @@ angular.module("umbraco") // Row management function // ********************************************* - $scope.clickRow = function(index, rows) { - rows[index].active = true; - }; - - $scope.clickOutsideRow = function(index, rows) { - rows[index].active = false; + $scope.clickRow = function(index, rows, $event) { + console.log("clickRow") + //rows[index].active = true; + $scope.currentRowWithActiveChild = null; + $scope.active = rows[index]; + + $event.stopPropagation(); }; function getAllowedLayouts(section) { @@ -359,6 +371,7 @@ angular.module("umbraco") if (section.rows.length > 0) { section.rows.splice($index, 1); $scope.currentRow = null; + $scope.currentRowWithActiveChild = null; $scope.openRTEToolbarId = null; currentForm.$setDirty(); } @@ -513,14 +526,13 @@ angular.module("umbraco") // Area management functions // ********************************************* - $scope.clickCell = function(index, cells, row) { - cells[index].active = true; - row.hasActiveChild = true; - }; - - $scope.clickOutsideCell = function(index, cells, row) { - cells[index].active = false; - row.hasActiveChild = hasActiveChild(row, cells); + $scope.clickCell = function(index, cells, row, $event) { + + $scope.currentCellWithActiveChild = null; + + $scope.active = cells[index]; + $scope.currentRowWithActiveChild = row; + $event.stopPropagation(); }; $scope.cellPreview = function (cell) { @@ -536,14 +548,14 @@ angular.module("umbraco") // ********************************************* // Control management functions // ********************************************* - $scope.clickControl = function (index, controls, cell) { - controls[index].active = true; - cell.hasActiveChild = true; - }; - - $scope.clickOutsideControl = function (index, controls, cell) { - controls[index].active = false; - cell.hasActiveChild = hasActiveChild(cell, controls); + $scope.clickControl = function (index, controls, cell, $event) { + + console.log("clickControl"); + + $scope.active = controls[index]; + $scope.currentCellWithActiveChild = cell; + + $event.stopPropagation(); }; function hasActiveChild(item, children) { @@ -594,8 +606,8 @@ angular.module("umbraco") if (index === undefined) { index = cell.controls.length; } - - newControl.active = true; + + $scope.active = newControl; //populate control $scope.initControl(newControl, index + 1); diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/grid.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/grid.html index 22e72daff9..eeb70718f9 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/grid.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/grid.html @@ -69,13 +69,13 @@
@@ -89,7 +89,7 @@
-
+
@@ -123,17 +123,17 @@ ng-style="{width: area.$percentage + '%'}" ng-class="{ '-has-config': area.hasConfig, - '-active': area.active, - '-active-child': area.hasActiveChild}" + '-active': area === active, + '-active-child': area === currentCellWithActiveChild}" ng-model="area.controls" - ng-click="clickCell($index, row.areas, row)" - on-outside-click="clickOutsideCell($index, row.areas, row)" - bind-click-on="{{area.active}}"> + ng-click="clickCell($index, row.areas, row, $event)" + + bind-click-on="{{area === active}}">
@@ -153,7 +153,7 @@
-
+
@@ -172,14 +172,14 @@
-
+
{{control.editor.name}} @@ -189,11 +189,11 @@
-
+
{{control.editor.name}}
-
+
From 60fa931f3f6c64a97f5350421461f14b99bca507 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Wed, 13 Feb 2019 14:38:25 +0100 Subject: [PATCH 02/50] Grid-RTE got a unique ID, which fixes TineMCE code, which is dependent on a unique ID. #4011 --- .../components/grid/grid.rte.directive.js | 14 ++++++-------- .../src/views/components/grid/grid-rte.html | 2 +- 2 files changed, 7 insertions(+), 9 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/grid/grid.rte.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/grid/grid.rte.directive.js index 6472dd3d38..dbbb0e98a2 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/grid/grid.rte.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/grid/grid.rte.directive.js @@ -13,6 +13,10 @@ angular.module("umbraco.directives") // TODO: A lot of the code below should be shared between the grid rte and the normal rte var promises = []; + + var d = new Date(); + var n = d.getTime(); + scope.textAreaHtmlId = scope.uniqueId + "_" + n + "_rte"; //queue file loading if (typeof (tinymce) === "undefined") { @@ -28,7 +32,7 @@ angular.module("umbraco.directives") var tinyMceEditor = null; promises.push(tinyMceService.getTinyMceEditorConfig({ - htmlId: scope.uniqueId, + htmlId: scope.textAreaHtmlId, stylesheets: scope.configuration ? scope.configuration.stylesheets : null, toolbar: toolbar, mode: scope.configuration.mode @@ -132,17 +136,11 @@ angular.module("umbraco.directives") } }); - - //listen for formSubmitting event (the result is callback used to remove the event subscription) - var formSubmittingListener = scope.$on("formSubmitting", function () { - scope.value = tinyMceEditor ? tinyMceEditor.getContent() : null; - }); - + //when the element is disposed we need to unsubscribe! // NOTE: this is very important otherwise if this is part of a modal, the listener still exists because the dom // element might still be there even after the modal has been hidden. scope.$on('$destroy', function () { - formSubmittingListener(); eventsService.unsubscribe(tabShownListener); //ensure we unbind this in case the blur doesn't fire above $('.umb-panel-body').off('scroll', pinToolbar); diff --git a/src/Umbraco.Web.UI.Client/src/views/components/grid/grid-rte.html b/src/Umbraco.Web.UI.Client/src/views/components/grid/grid-rte.html index f3b854a062..afa9869676 100644 --- a/src/Umbraco.Web.UI.Client/src/views/components/grid/grid-rte.html +++ b/src/Umbraco.Web.UI.Client/src/views/components/grid/grid-rte.html @@ -1,3 +1,3 @@ 
-
+
From 13a326b4673a89fa5694ed81de0ff06470110ece Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Mon, 18 Feb 2019 10:58:59 +0100 Subject: [PATCH 03/50] remove logs --- .../src/views/propertyeditors/grid/grid.controller.js | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/grid.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/grid.controller.js index 0ea5842612..650717b7c0 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/grid.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/grid.controller.js @@ -240,9 +240,6 @@ angular.module("umbraco") }, 500, false); $scope.$apply(function () { - - console.log("$apply function called...") - var cell = $(e.target).scope().area; if(hasActiveChild(cell, cell.controls)) { @@ -319,7 +316,6 @@ angular.module("umbraco") // ********************************************* $scope.clickRow = function(index, rows, $event) { - console.log("clickRow") //rows[index].active = true; $scope.currentRowWithActiveChild = null; $scope.active = rows[index]; @@ -550,8 +546,6 @@ angular.module("umbraco") // ********************************************* $scope.clickControl = function (index, controls, cell, $event) { - console.log("clickControl"); - $scope.active = controls[index]; $scope.currentCellWithActiveChild = cell; From 2e4091145066435bf8eba2d9e7bce5d1399fbe36 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Mon, 18 Feb 2019 10:59:30 +0100 Subject: [PATCH 04/50] get uniq ID by using String.CreateGuid() --- .../directives/components/grid/grid.rte.directive.js | 9 +++++---- .../src/views/propertyeditors/rte/rte.controller.js | 4 +--- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/grid/grid.rte.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/grid/grid.rte.directive.js index dbbb0e98a2..0f933a4ccc 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/grid/grid.rte.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/grid/grid.rte.directive.js @@ -14,10 +14,11 @@ angular.module("umbraco.directives") var promises = []; - var d = new Date(); - var n = d.getTime(); - scope.textAreaHtmlId = scope.uniqueId + "_" + n + "_rte"; - + //To id the html textarea we need to use the datetime ticks because we can have multiple rte's per a single property alias + // because now we have to support having 2x (maybe more at some stage) content editors being displayed at once. This is because + // we have this mini content editor panel that can be launched with MNTP. + scope.textAreaHtmlId = scope.uniqueId + "_" + String.CreateGuid(); + //queue file loading if (typeof (tinymce) === "undefined") { promises.push(assetsService.loadJs("lib/tinymce/tinymce.min.js", scope)); diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.controller.js index 6099c5dad4..43da0cb66f 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.controller.js @@ -9,9 +9,7 @@ angular.module("umbraco") //To id the html textarea we need to use the datetime ticks because we can have multiple rte's per a single property alias // because now we have to support having 2x (maybe more at some stage) content editors being displayed at once. This is because // we have this mini content editor panel that can be launched with MNTP. - var d = new Date(); - var n = d.getTime(); - $scope.textAreaHtmlId = $scope.model.alias + "_" + n + "_rte"; + $scope.textAreaHtmlId = $scope.model.alias + "_" + String.CreateGuid(); var editorConfig = $scope.model.config ? $scope.model.config.editor : null; if (!editorConfig || angular.isString(editorConfig)) { From 44b1a738bf5a4d9ca04b105399d61990ac469bd2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Mon, 18 Feb 2019 10:59:40 +0100 Subject: [PATCH 05/50] clean up css for grid --- .../src/less/components/umb-grid.less | 38 +------------------ 1 file changed, 1 insertion(+), 37 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-grid.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-grid.less index 6bf8cd612a..5283e259c7 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-grid.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-grid.less @@ -420,26 +420,6 @@ .umb-row-title-bar { cursor: move; } - /* - .row-tool, - .umb-row-title { - color: @white; - } - - .umb-grid-has-config { - color: fade(@white, 66); - } - - .umb-cell { - .umb-grid-has-config { - color: fade(@black, 44); - } - } - - .umb-cell .umb-cell-content { - border-color: transparent; - } - */ } @@ -453,23 +433,7 @@ .umb-row-title { color: @gray-3; } - /* - .row-tool { - color: fade(@black, 23); - } - - .umb-grid-has-config { - color: fade(@black, 44); - } - - .umb-cell-content.-placeholder { - border-color: @gray-8; - - &:hover { - border-color: fade(@gray, 44); - } - } - */ + } From 3f13baa8e773d9435ae9eab239c7d883881bd193 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Mon, 18 Feb 2019 11:00:47 +0100 Subject: [PATCH 06/50] make Embed and Media work for Grid in SplitView when grid is not allowed to vary. --- .../grid/editors/embed.controller.js | 21 +++++------------- .../propertyeditors/grid/editors/embed.html | 4 ++-- .../grid/editors/media.controller.js | 22 ++++++------------- .../propertyeditors/grid/editors/media.html | 4 ++-- 4 files changed, 17 insertions(+), 34 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/embed.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/embed.controller.js index beb8edab20..17916299bc 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/embed.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/embed.controller.js @@ -2,24 +2,16 @@ angular.module("umbraco") .controller("Umbraco.PropertyEditors.Grid.EmbedController", function ($scope, $timeout, $sce, editorService) { - function onInit() { - $scope.trustedValue = null; - $scope.trustedValue = $sce.trustAsHtml($scope.control.value); - - if(!$scope.control.value) { - $timeout(function(){ - if($scope.control.$initializing){ - $scope.setEmbed(); - } - }, 200); - } + $scope.hasEmbed = function(){ + return $scope.control.value !== null; + } + $scope.getEmbed = function(){ + return $sce.trustAsHtml($scope.control.value); } - $scope.setEmbed = function(){ var embed = { submit: function(model) { $scope.control.value = model.embed.preview; - $scope.trustedValue = $sce.trustAsHtml(model.embed.preview); editorService.close(); }, close: function() { @@ -28,6 +20,5 @@ angular.module("umbraco") }; editorService.embed(embed); }; - - onInit(); + }); diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/embed.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/embed.html index 87109e1eb9..32ebfd3257 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/embed.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/embed.html @@ -1,10 +1,10 @@
-
+
Click to embed
-
+
diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/media.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/media.controller.js index e267133cf4..c290b8ae7d 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/media.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/media.controller.js @@ -8,13 +8,7 @@ angular.module("umbraco") $scope.model.config.startNodeIsVirtual = userData.startMediaIds.length !== 1; }); } - - function onInit() { - if($scope.control.value){ - $scope.setUrl(); - } - } - + $scope.setImage = function(){ var startNodeId = $scope.model.config && $scope.model.config.startNodeId ? $scope.model.config.startNodeId : undefined; var startNodeIsVirtual = startNodeId ? $scope.model.config.startNodeIsVirtual : undefined; @@ -34,10 +28,8 @@ angular.module("umbraco") id: selectedImage.id, udi: selectedImage.udi, image: selectedImage.image, - altText: selectedImage.altText - }; - - $scope.setUrl(); + caption: selectedImage.altText + }; editorService.close(); }, @@ -49,7 +41,7 @@ angular.module("umbraco") editorService.mediaPicker(mediaPicker); }; - $scope.setUrl = function(){ + $scope.getThumbnailUrl = function(){ if($scope.control.value.image){ var url = $scope.control.value.image; @@ -70,10 +62,10 @@ angular.module("umbraco") { url += "?width=800&upscale=false&animationprocessmode=false" } - $scope.url = url; + return url; } + + return ""; }; - onInit(); - }); diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/media.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/media.html index 7ffb26d831..1a91fe52b8 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/media.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/media.html @@ -7,9 +7,9 @@
From 6f673184324a90f10109a18e87e60e2b65f76ee8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Tue, 19 Feb 2019 10:18:01 +0100 Subject: [PATCH 07/50] corrected color variables --- .../src/less/components/umb-grid.less | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-grid.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-grid.less index 3aab0340a8..5a14294ebf 100644 --- a/src/Umbraco.Web.UI.Client/src/less/components/umb-grid.less +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-grid.less @@ -542,14 +542,13 @@ display: inline-block; cursor: pointer; border-radius: 200px; - background: @gray-10; - border: 1px solid @gray-7; + border: 1px solid @ui-action-disgrete-border; margin: 2px; &:hover, &:hover * { - background: @ui-action-type-hover !important; + background: @ui-action-disgrete-type-hover !important; color: @white !important; - border-color: @ui-action-type-hover !important; + border-color: @ui-action-disgrete-border-hover !important; text-decoration: none; } } From 592d8db3e72194eeafa7a52229287ce538e7974c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Tue, 19 Feb 2019 10:18:13 +0100 Subject: [PATCH 08/50] removed comment --- .../src/views/propertyeditors/grid/grid.controller.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/grid.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/grid.controller.js index 650717b7c0..ff01c8cc71 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/grid.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/grid.controller.js @@ -316,7 +316,7 @@ angular.module("umbraco") // ********************************************* $scope.clickRow = function(index, rows, $event) { - //rows[index].active = true; + $scope.currentRowWithActiveChild = null; $scope.active = rows[index]; From 564117e62edd23f5e786c9619f02a9f72ad60455 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Tue, 19 Feb 2019 10:19:02 +0100 Subject: [PATCH 09/50] Grid editor to use $watch for values that needs to transform for presentation --- .../grid/editors/embed.controller.js | 24 ++++++++++++++----- .../propertyeditors/grid/editors/embed.html | 2 +- .../grid/editors/media.controller.js | 22 +++++++++++++---- .../propertyeditors/grid/editors/media.html | 5 ++-- 4 files changed, 38 insertions(+), 15 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/embed.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/embed.controller.js index 17916299bc..8e9ea06a84 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/embed.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/embed.controller.js @@ -1,14 +1,26 @@ angular.module("umbraco") .controller("Umbraco.PropertyEditors.Grid.EmbedController", function ($scope, $timeout, $sce, editorService) { - - $scope.hasEmbed = function(){ - return $scope.control.value !== null; - } - $scope.getEmbed = function(){ + + + + function getEmbed() { return $sce.trustAsHtml($scope.control.value); } - $scope.setEmbed = function(){ + + + $scope.embedHtml = getEmbed(); + $scope.$watch('control.value', function(newValue, oldValue) { + if(angular.equals(newValue, oldValue)){ + return; // simply skip that + } + + $scope.embedHtml = getEmbed(); + }, false); + $scope.hasEmbed = function() { + return $scope.control.value !== null; + } + $scope.setEmbed = function() { var embed = { submit: function(model) { $scope.control.value = model.embed.preview; diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/embed.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/embed.html index 32ebfd3257..0178c3b3ec 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/embed.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/embed.html @@ -5,6 +5,6 @@
Click to embed
-
+
diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/media.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/media.controller.js index c290b8ae7d..eb1032a9c7 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/media.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/media.controller.js @@ -1,7 +1,11 @@ angular.module("umbraco") .controller("Umbraco.PropertyEditors.Grid.MediaController", function ($scope, $timeout, userService, editorService) { - + + + $scope.thumbnailUrl = getThumbnailUrl(); + + if (!$scope.model.config.startNodeId) { userService.getCurrentUser().then(function (userData) { $scope.model.config.startNodeId = userData.startMediaIds.length !== 1 ? -1 : userData.startMediaIds[0]; @@ -40,10 +44,18 @@ angular.module("umbraco") editorService.mediaPicker(mediaPicker); }; + + $scope.$watch('control.value', function(newValue, oldValue) { + if(angular.equals(newValue, oldValue)){ + return; // simply skip that + } + + $scope.thumbnailUrl = getThumbnailUrl(); + }, true); + + function getThumbnailUrl() { - $scope.getThumbnailUrl = function(){ - - if($scope.control.value.image){ + if($scope.control.value && $scope.control.value.image) { var url = $scope.control.value.image; if($scope.control.editor.config && $scope.control.editor.config.size){ @@ -65,7 +77,7 @@ angular.module("umbraco") return url; } - return ""; + return null; }; }); diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/media.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/media.html index 1a91fe52b8..184f707ebf 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/media.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/media.html @@ -5,11 +5,10 @@
Click to insert image
-
+
From 1f7172e98dc4306c28dc2f2a5f4b9349d646d3ea Mon Sep 17 00:00:00 2001 From: Poornima Nayar Date: Tue, 12 Mar 2019 21:54:13 +0000 Subject: [PATCH 10/50] Cleaned up class names in logviewer overview to use new logviewer classes and made a logviewer specific less stylesheet. Also left aligned the saved searches to make them look better --- src/Umbraco.Web.UI.Client/src/less/belle.less | 1 + .../src/less/components/umb-logviewer.less | 42 +++++++++++++++++++ .../src/views/logviewer/overview.html | 10 ++--- 3 files changed, 48 insertions(+), 5 deletions(-) create mode 100644 src/Umbraco.Web.UI.Client/src/less/components/umb-logviewer.less diff --git a/src/Umbraco.Web.UI.Client/src/less/belle.less b/src/Umbraco.Web.UI.Client/src/less/belle.less index 88988485fe..82fe416319 100644 --- a/src/Umbraco.Web.UI.Client/src/less/belle.less +++ b/src/Umbraco.Web.UI.Client/src/less/belle.less @@ -137,6 +137,7 @@ @import "components/umb-iconpicker.less"; @import "components/umb-insert-code-box.less"; @import "components/umb-packages.less"; +@import "components/umb-logviewer.less"; @import "components/umb-package-local-install.less"; @import "components/umb-panel-group.less"; @import "components/umb-lightbox.less"; diff --git a/src/Umbraco.Web.UI.Client/src/less/components/umb-logviewer.less b/src/Umbraco.Web.UI.Client/src/less/components/umb-logviewer.less new file mode 100644 index 0000000000..f7aa0e4558 --- /dev/null +++ b/src/Umbraco.Web.UI.Client/src/less/components/umb-logviewer.less @@ -0,0 +1,42 @@ +/* PACKAGE DETAILS */ + +.umb-logviewer { + display: flex; + flex-flow: row wrap; +} + +@sidebarwidth: 350px; // Width of sidebar. Ugly hack because of old version of Less + +.umb-logviewer__main-content { + flex: 1 1 auto; + margin-right: 20px; + width: calc(~'100%' - ~'@{sidebarwidth}' - ~'20px'); // Make sure that the main content area doesn't gets affected by inline styling + min-width: 500px; + + .btn-link { + text-align: left; + } +} + +.umb-logviewer__sidebar { + flex: 0 0 @sidebarwidth; +} + +@media (max-width: 768px) { + + .umb-logviewer { + flex-direction: column; + } + + .umb-logviewer__main-content { + flex: 1 1 auto; + width: 100%; + margin-bottom: 30px; + margin-right: 0; + } + + .umb-logviewer__sidebar { + flex: 1 1 auto; + width: 100%; + } +} diff --git a/src/Umbraco.Web.UI.Client/src/views/logviewer/overview.html b/src/Umbraco.Web.UI.Client/src/views/logviewer/overview.html index 67d89e5ee7..a46853f97e 100644 --- a/src/Umbraco.Web.UI.Client/src/views/logviewer/overview.html +++ b/src/Umbraco.Web.UI.Client/src/views/logviewer/overview.html @@ -1,4 +1,4 @@ -
+
@@ -24,8 +24,8 @@
-
-
+
+
@@ -68,7 +68,7 @@
-
+
@@ -95,4 +95,4 @@
-
\ No newline at end of file +
From 7f334540029f76761525b494c7d2ac40c9c6b98b Mon Sep 17 00:00:00 2001 From: Stephan Date: Mon, 18 Mar 2019 17:51:04 +0100 Subject: [PATCH 11/50] Better ServerMessenger configuration --- src/Umbraco.Core/CompositionExtensions.cs | 22 +++++ .../BatchedDatabaseServerMessenger.cs | 5 +- ...aseServerRegistrarAndMessengerComponent.cs | 80 ++++++++----------- 3 files changed, 59 insertions(+), 48 deletions(-) diff --git a/src/Umbraco.Core/CompositionExtensions.cs b/src/Umbraco.Core/CompositionExtensions.cs index e1c7ad4467..828a577c34 100644 --- a/src/Umbraco.Core/CompositionExtensions.cs +++ b/src/Umbraco.Core/CompositionExtensions.cs @@ -203,6 +203,28 @@ namespace Umbraco.Core composition.RegisterUnique(_ => registrar); } + /// + /// Sets the database server messenger options. + /// + /// The composition. + /// A function creating the options. + /// Use DatabaseServerRegistrarAndMessengerComposer.GetDefaultOptions to get the options that Umbraco would use by default. + public static void SetDatabaseServerMessengerOptions(this Composition composition, Func factory) + { + composition.RegisterUnique(factory); + } + + /// + /// Sets the database server messenger options. + /// + /// The composition. + /// Options. + /// Use DatabaseServerRegistrarAndMessengerComposer.GetDefaultOptions to get the options that Umbraco would use by default. + public static void SetDatabaseServerMessengerOptions(this Composition composition, DatabaseServerMessengerOptions options) + { + composition.RegisterUnique(_ => options); + } + /// /// Sets the short string helper. /// diff --git a/src/Umbraco.Web/BatchedDatabaseServerMessenger.cs b/src/Umbraco.Web/BatchedDatabaseServerMessenger.cs index 76d7565862..818e8ecf77 100644 --- a/src/Umbraco.Web/BatchedDatabaseServerMessenger.cs +++ b/src/Umbraco.Web/BatchedDatabaseServerMessenger.cs @@ -27,9 +27,8 @@ namespace Umbraco.Web private readonly IUmbracoDatabaseFactory _databaseFactory; public BatchedDatabaseServerMessenger( - IRuntimeState runtime, IUmbracoDatabaseFactory databaseFactory, IScopeProvider scopeProvider, ISqlContext sqlContext, IProfilingLogger proflog, IGlobalSettings globalSettings, - bool enableDistCalls, DatabaseServerMessengerOptions options) - : base(runtime, scopeProvider, sqlContext, proflog, globalSettings, enableDistCalls, options) + IRuntimeState runtime, IUmbracoDatabaseFactory databaseFactory, IScopeProvider scopeProvider, ISqlContext sqlContext, IProfilingLogger proflog, IGlobalSettings globalSettings, DatabaseServerMessengerOptions options) + : base(runtime, scopeProvider, sqlContext, proflog, globalSettings, true, options) { _databaseFactory = databaseFactory; } diff --git a/src/Umbraco.Web/Compose/DatabaseServerRegistrarAndMessengerComponent.cs b/src/Umbraco.Web/Compose/DatabaseServerRegistrarAndMessengerComponent.cs index 086aa9b197..a68e137665 100644 --- a/src/Umbraco.Web/Compose/DatabaseServerRegistrarAndMessengerComponent.cs +++ b/src/Umbraco.Web/Compose/DatabaseServerRegistrarAndMessengerComponent.cs @@ -38,54 +38,44 @@ namespace Umbraco.Web.Compose public sealed class DatabaseServerRegistrarAndMessengerComposer : ComponentComposer, ICoreComposer { + public static DatabaseServerMessengerOptions GetDefaultOptions(IFactory factory) + { + var logger = factory.GetInstance(); + var indexRebuilder = factory.GetInstance(); + + return new DatabaseServerMessengerOptions + { + //These callbacks will be executed if the server has not been synced + // (i.e. it is a new server or the lastsynced.txt file has been removed) + InitializingCallbacks = new Action[] + { + //rebuild the xml cache file if the server is not synced + () => + { + // rebuild the published snapshot caches entirely, if the server is not synced + // this is equivalent to DistributedCache RefreshAll... but local only + // (we really should have a way to reuse RefreshAll... locally) + // note: refresh all content & media caches does refresh content types too + var svc = Current.PublishedSnapshotService; + svc.Notify(new[] { new DomainCacheRefresher.JsonPayload(0, DomainChangeTypes.RefreshAll) }); + svc.Notify(new[] { new ContentCacheRefresher.JsonPayload(0, TreeChangeTypes.RefreshAll) }, out _, out _); + svc.Notify(new[] { new MediaCacheRefresher.JsonPayload(0, TreeChangeTypes.RefreshAll) }, out _); + }, + + //rebuild indexes if the server is not synced + // NOTE: This will rebuild ALL indexes including the members, if developers want to target specific + // indexes then they can adjust this logic themselves. + () => { ExamineComponent.RebuildIndexes(indexRebuilder, logger, false, 5000); } + } + }; + } + public override void Compose(Composition composition) { base.Compose(composition); - composition.SetServerMessenger(factory => - { - var runtime = factory.GetInstance(); - var databaseFactory = factory.GetInstance(); - var globalSettings = factory.GetInstance(); - var proflog = factory.GetInstance(); - var scopeProvider = factory.GetInstance(); - var sqlContext = factory.GetInstance(); - var logger = factory.GetInstance(); - var indexRebuilder = factory.GetInstance(); - - return new BatchedDatabaseServerMessenger( - runtime, databaseFactory, scopeProvider, sqlContext, proflog, globalSettings, - true, - //Default options for web including the required callbacks to build caches - new DatabaseServerMessengerOptions - { - //These callbacks will be executed if the server has not been synced - // (i.e. it is a new server or the lastsynced.txt file has been removed) - InitializingCallbacks = new Action[] - { - //rebuild the xml cache file if the server is not synced - () => - { - // rebuild the published snapshot caches entirely, if the server is not synced - // this is equivalent to DistributedCache RefreshAll... but local only - // (we really should have a way to reuse RefreshAll... locally) - // note: refresh all content & media caches does refresh content types too - var svc = Current.PublishedSnapshotService; - svc.Notify(new[] { new DomainCacheRefresher.JsonPayload(0, DomainChangeTypes.RefreshAll) }); - svc.Notify(new[] { new ContentCacheRefresher.JsonPayload(0, TreeChangeTypes.RefreshAll) }, out _, out _); - svc.Notify(new[] { new MediaCacheRefresher.JsonPayload(0, TreeChangeTypes.RefreshAll) }, out _); - }, - - //rebuild indexes if the server is not synced - // NOTE: This will rebuild ALL indexes including the members, if developers want to target specific - // indexes then they can adjust this logic themselves. - () => - { - ExamineComponent.RebuildIndexes(indexRebuilder, logger, false, 5000); - } - } - }); - }); + composition.SetDatabaseServerMessengerOptions(GetDefaultOptions); + composition.SetServerMessenger(); } } @@ -128,7 +118,7 @@ namespace Umbraco.Web.Compose } public void Initialize() - { + { //We will start the whole process when a successful request is made if (_registrar != null || _messenger != null) UmbracoModule.RouteAttempt += RegisterBackgroundTasksOnce; From 019ac523098f79361e5ecd93d585212903912760 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Fri, 22 Mar 2019 16:06:07 +0100 Subject: [PATCH 12/50] emit "editors.dataTypeSettings.saved"-event --- .../datatypesettings.controller.js | 41 +++++++++++-------- 1 file changed, 24 insertions(+), 17 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/datatypesettings/datatypesettings.controller.js b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/datatypesettings/datatypesettings.controller.js index 099439fa4b..46a4238c0c 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/datatypesettings/datatypesettings.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/common/infiniteeditors/datatypesettings/datatypesettings.controller.js @@ -10,7 +10,8 @@ (function () { "use strict"; - function DataTypeSettingsController($scope, dataTypeResource, dataTypeHelper, localizationService, notificationsService, overlayService, formHelper) { + function DataTypeSettingsController($scope, dataTypeResource, dataTypeHelper, + localizationService, notificationsService, overlayService, formHelper, eventsService) { var vm = this; @@ -103,27 +104,33 @@ var preValues = dataTypeHelper.createPreValueProps(vm.dataType.preValues); - dataTypeResource.save(vm.dataType, preValues, $scope.model.create).then(function(newDataType) { - $scope.model.dataType = newDataType; - vm.saveButtonState = "success"; + dataTypeResource.save(vm.dataType, preValues, $scope.model.create).then( + function(newDataType) { + $scope.model.dataType = newDataType; + + var args = { dataType: newDataType }; + eventsService.emit("editors.dataTypeSettings.saved", args); + + vm.saveButtonState = "success"; - if ($scope.model && $scope.model.submit) { - $scope.model.submit($scope.model); - } - }, function(err) { - vm.saveButtonState = "error"; - - if(err.status === 400) { - if (err.data && (err.data.ModelState)) { - - formHelper.handleServerValidation(err.data.ModelState); + if ($scope.model && $scope.model.submit) { + $scope.model.submit($scope.model); + } + }, function(err) { + vm.saveButtonState = "error"; + + if(err.status === 400) { + if (err.data && (err.data.ModelState)) { + + formHelper.handleServerValidation(err.data.ModelState); - for (var e in err.data.ModelState) { - notificationsService.error("Validation", err.data.ModelState[e][0]); + for (var e in err.data.ModelState) { + notificationsService.error("Validation", err.data.ModelState[e][0]); + } } } } - }); + ); } From af4dc97b10505848e519535f32562a31de3acf23 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Fri, 22 Mar 2019 16:06:39 +0100 Subject: [PATCH 13/50] changed ' to " --- .../components/content/umbcontentnodeinfo.directive.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/content/umbcontentnodeinfo.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/content/umbcontentnodeinfo.directive.js index d4b617f94b..e58ff14e21 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/content/umbcontentnodeinfo.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/content/umbcontentnodeinfo.directive.js @@ -147,7 +147,7 @@ id: documentType.id, submit: function (model) { const args = { node: scope.node }; - eventsService.emit('editors.content.reload', args); + eventsService.emit("editors.content.reload", args); editorService.close(); }, close: function () { From da503cc9b5730cb01611dbe16761956215b3bb51 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Fri, 22 Mar 2019 16:19:56 +0100 Subject: [PATCH 14/50] listen for "editors.dataTypeSettings.saved" --- .../components/umbgroupsbuilder.directive.js | 54 ++++++++++++++----- 1 file changed, 42 insertions(+), 12 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbgroupsbuilder.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbgroupsbuilder.directive.js index 15f5a1d46a..da51528bd2 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/umbgroupsbuilder.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/umbgroupsbuilder.directive.js @@ -1,13 +1,17 @@ (function() { 'use strict'; - function GroupsBuilderDirective(contentTypeHelper, contentTypeResource, mediaTypeResource, dataTypeHelper, dataTypeResource, $filter, iconHelper, $q, $timeout, notificationsService, localizationService, editorService) { + function GroupsBuilderDirective(contentTypeHelper, contentTypeResource, mediaTypeResource, + dataTypeHelper, dataTypeResource, $filter, iconHelper, $q, $timeout, notificationsService, + localizationService, editorService, eventsService) { function link(scope, el, attr, ctrl) { - + + var eventBindings = []; var validationTranslated = ""; var tabNoSortOrderTranslated = ""; + scope.dataTypeHasChanged = false; scope.sortingMode = false; scope.toolbar = []; scope.sortableOptionsGroup = {}; @@ -613,18 +617,44 @@ }); }); } - - - var unbindModelWatcher = scope.$watch('model', function(newValue, oldValue) { - if (newValue !== undefined && newValue.groups !== undefined) { - activate(); + + function hasPropertyOfDataTypeId(dataTypeId) { + + // look at each property + var result = _.filter(scope.model.groups, function(group) { + return _.filter(group.properties, function(property) { + return (property.dataTypeId === dataTypeId); + }); + }); + + return (result.length > 0); } - }); - // clean up - scope.$on('$destroy', function(){ - unbindModelWatcher(); - }); + + eventBindings.push(scope.$watch('model', function(newValue, oldValue) { + if (newValue !== undefined && newValue.groups !== undefined) { + activate(); + } + })); + + // clean up + eventBindings.push(eventsService.on("editors.dataTypeSettings.saved", function (name, args) { + if(hasPropertyOfDataTypeId(args.dataType.id)) { + scope.dataTypeHasChanged = true; + } + })); + + // clean up + eventBindings.push(scope.$on('$destroy', function() { + for(var e in eventBindings) { + eventBindings[e](); + } + // if a dataType has changed, we want to notify which properties that are affected by this dataTypeSettings change + if(scope.dataTypeHasChanged === true) { + var args = {documentType: scope.model}; + eventsService.emit("editors.documentType.saved", args); + } + })); } From d806855698d374378bae65878cb4c006d76eb22c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Fri, 22 Mar 2019 16:22:36 +0100 Subject: [PATCH 15/50] listen for "editors.documentType.saved" instead of "editors.content.reload" --- .../components/content/edit.controller.js | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 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 80b5f4ed39..a7ce013bc3 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 @@ -116,6 +116,13 @@ function isContentCultureVariant() { return $scope.content.variants.length > 1; } + + function reload() { + $scope.page.loading = true; + loadContent().then(function() { + $scope.page.loading = false; + }); + } function bindEvents() { //bindEvents can be called more than once and we don't want to have multiple bound events @@ -123,13 +130,10 @@ eventsService.unsubscribe(evts[e]); } - evts.push(eventsService.on("editors.content.reload", function (name, args) { + evts.push(eventsService.on("editors.documentType.saved", function (name, args) { // if this content item uses the updated doc type we need to reload the content item - if(args && args.node && args.node.key === $scope.content.key) { - $scope.page.loading = true; - loadContent().then(function() { - $scope.page.loading = false; - }); + if(args && args.documentType && $scope.content.documentType.id === args.documentType.id) { + reload(); } })); From 15d5c67cd3192ba65ff72ec4416ff7eeb8c3d22c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Mon, 25 Mar 2019 13:42:05 +0100 Subject: [PATCH 16/50] global text-color variables --- src/Umbraco.Web.UI.Client/src/less/variables.less | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/less/variables.less b/src/Umbraco.Web.UI.Client/src/less/variables.less index 6400ae0321..55be161cf8 100644 --- a/src/Umbraco.Web.UI.Client/src/less/variables.less +++ b/src/Umbraco.Web.UI.Client/src/less/variables.less @@ -112,7 +112,7 @@ @blueExtraDark: #1b264f;// added 2019 @blueLight: #ADD8E6; @blueNight: #162335;// added 2019 -@orange: #f79c37;// updated 2019 +//@orange: #f79c37;// updated 2019 @pink: #D93F4C;// #C3325F;// update 2019 @pinkLight: #f5c1bc;// added 2019 @pinkRedLight: #ff8a89;// added 2019 @@ -199,6 +199,16 @@ .turquoise{color: @turquoise;} .turquoise-d1{color: @turquoise-d1;} +.text-warning { + color: @orange; +} +.text-error { + color: @red; +} +.text-success { + color: @green; +} + //icon colors for tree icons .color-red, .color-red i{color: @red-d1 !important;} From 0cc6e51b7a9ae3999a9ddfdaa089cc20e7186821 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Mon, 25 Mar 2019 13:43:03 +0100 Subject: [PATCH 17/50] simplify controller + only show text when near limit + different text when exceeding limit --- .../textarea/textarea.controller.js | 24 +++----------- .../propertyeditors/textarea/textarea.html | 9 ++++-- .../textbox/textbox.controller.js | 31 +++---------------- .../propertyeditors/textbox/textbox.html | 9 ++++-- src/Umbraco.Web.UI/Umbraco/config/lang/da.xml | 5 +-- src/Umbraco.Web.UI/Umbraco/config/lang/en.xml | 5 +-- .../Umbraco/config/lang/en_us.xml | 5 +-- 7 files changed, 31 insertions(+), 57 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/textarea/textarea.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/textarea/textarea.controller.js index 60d762738f..4934adb5dd 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/textarea/textarea.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/textarea/textarea.controller.js @@ -13,25 +13,11 @@ function textAreaController($scope) { $scope.model.maxlength = false; if ($scope.model.config && $scope.model.config.maxChars) { $scope.model.maxlength = true; - if($scope.model.value == undefined) { - $scope.model.count = ($scope.model.config.maxChars * 1); - } else { - $scope.model.count = ($scope.model.config.maxChars * 1) - $scope.model.value.length; - } } - - $scope.model.change = function() { - if ($scope.model.config && $scope.model.config.maxChars) { - if($scope.model.value == undefined) { - $scope.model.count = ($scope.model.config.maxChars * 1); - } else { - $scope.model.count = ($scope.model.config.maxChars * 1) - $scope.model.value.length; - } - if($scope.model.count < 0) { - $scope.model.value = $scope.model.value.substring(0, ($scope.model.config.maxChars * 1)); - $scope.model.count = 0; - } - } + + $scope.model.change = function () { + $scope.model.count = $scope.model.value.length; } + $scope.model.change(); } -angular.module('umbraco').controller("Umbraco.PropertyEditors.textAreaController", textAreaController); \ No newline at end of file +angular.module('umbraco').controller("Umbraco.PropertyEditors.textAreaController", textAreaController); diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/textarea/textarea.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/textarea/textarea.html index 2fdca5d654..80da759fba 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/textarea/textarea.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/textarea/textarea.html @@ -7,9 +7,12 @@ {{textareaFieldForm.textarea.errorMsg}} -
- {{model.count}} - characters left +
+ characters left
+
+ Maximum {0} characters ({1} too many) +
+
diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/textbox/textbox.controller.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/textbox/textbox.controller.js index 734903e46c..4ab827db59 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/textbox/textbox.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/textbox/textbox.controller.js @@ -4,39 +4,18 @@ function textboxController($scope) { if (!$scope.model.config) { $scope.model.config = {}; } - - $scope.model.maxlength = false; - if ($scope.model.config && $scope.model.config.maxChars) { - $scope.model.maxlength = true; - } - + if (!$scope.model.config.maxChars) { // 500 is the maximum number that can be stored // in the database, so set it to the max, even // if no max is specified in the config $scope.model.config.maxChars = 500; } - - if ($scope.model.maxlength) { - if ($scope.model.value === undefined) { - $scope.model.count = ($scope.model.config.maxChars * 1); - } else { - $scope.model.count = ($scope.model.config.maxChars * 1) - $scope.model.value.length; - } - } - + $scope.model.change = function () { - if ($scope.model.config && $scope.model.config.maxChars) { - if ($scope.model.value === undefined) { - $scope.model.count = ($scope.model.config.maxChars * 1); - } else { - $scope.model.count = ($scope.model.config.maxChars * 1) - $scope.model.value.length; - } - if ($scope.model.count < 0) { - $scope.model.value = $scope.model.value.substring(0, ($scope.model.config.maxChars * 1)); - $scope.model.count = 0; - } - } + $scope.model.count = $scope.model.value.length; } + $scope.model.change(); + } angular.module('umbraco').controller("Umbraco.PropertyEditors.textboxController", textboxController); diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/textbox/textbox.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/textbox/textbox.html index 6f6ed6bdf6..0e041e7f34 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/textbox/textbox.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/textbox/textbox.html @@ -12,9 +12,12 @@ Required -
- {{model.count}} - characters left +
+ characters left
+
+ Maximum {0} characters ({1} too many) +
+
diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/da.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/da.xml index 875948d4d2..11dcd6ac57 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/da.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/da.xml @@ -1568,8 +1568,9 @@ Mange hilsner fra Umbraco robotten Ingen ordbog elementer at vælge imellem - - Karakterer tilbage + + >{0} tegn tilbage.]]> + {1} for mange.]]> Slettet indhold med Id: {0} Relateret til original "parent" med id: {1} diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml index fa6f689845..d10bc6ea0c 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml @@ -2078,8 +2078,9 @@ To manage your website, simply open the Umbraco back office and start adding con No Dictionary items to choose from - - characters left + + >{0} characters left.]]> + {1} too many.]]> Trashed content with Id: {0} related to original parent content with Id: {1} 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 6ff4d32f08..c04f639c3a 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml @@ -2090,8 +2090,9 @@ To manage your website, simply open the Umbraco back office and start adding con No Dictionary items to choose from - - characters left + + >{0} characters left.]]> + {1} too many.]]> Trashed content with Id: {0} related to original parent content with Id: {1} From 286978647eb57371bddee1b58a4a0026da8bce2a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Mon, 25 Mar 2019 14:04:31 +0100 Subject: [PATCH 18/50] fix type mistake, plus correcting math. --- .../src/views/propertyeditors/textarea/textarea.html | 4 ++-- .../src/views/propertyeditors/textbox/textbox.html | 4 ++-- src/Umbraco.Web.UI/Umbraco/config/lang/da.xml | 2 +- src/Umbraco.Web.UI/Umbraco/config/lang/en.xml | 2 +- src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml | 2 +- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/textarea/textarea.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/textarea/textarea.html index 80da759fba..e08668fb20 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/textarea/textarea.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/textarea/textarea.html @@ -8,10 +8,10 @@
- characters left + characters left
- Maximum {0} characters ({1} too many) + Maximum {0} characters ({1} too many)
diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/textbox/textbox.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/textbox/textbox.html index 0e041e7f34..8385c951b1 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/textbox/textbox.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/textbox/textbox.html @@ -13,10 +13,10 @@
- characters left + characters left
- Maximum {0} characters ({1} too many) + Maximum {0} characters ({1} too many)
diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/da.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/da.xml index 11dcd6ac57..52be26e2fc 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/da.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/da.xml @@ -1569,7 +1569,7 @@ Mange hilsner fra Umbraco robotten Ingen ordbog elementer at vælge imellem - >{0} tegn tilbage.]]> + {0} tegn tilbage.]]> {1} for mange.]]> diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml index d10bc6ea0c..218812270d 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml @@ -2079,7 +2079,7 @@ To manage your website, simply open the Umbraco back office and start adding con No Dictionary items to choose from - >{0} characters left.]]> + {0} characters left.]]> {1} too many.]]> 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 c04f639c3a..9f578da1b6 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml @@ -2091,7 +2091,7 @@ To manage your website, simply open the Umbraco back office and start adding con No Dictionary items to choose from - >{0} characters left.]]> + {0} characters left.]]> {1} too many.]]> From 50ee60e8c35eb4b8f4ed7fcf666950c369eb1ce7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Mon, 25 Mar 2019 14:09:29 +0100 Subject: [PATCH 19/50] correcting translate tokens to use % --- .../src/views/propertyeditors/textarea/textarea.html | 4 ++-- .../src/views/propertyeditors/textbox/textbox.html | 4 ++-- src/Umbraco.Web.UI/Umbraco/config/lang/da.xml | 4 ++-- src/Umbraco.Web.UI/Umbraco/config/lang/en.xml | 4 ++-- src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml | 4 ++-- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/textarea/textarea.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/textarea/textarea.html index e08668fb20..94935423e8 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/textarea/textarea.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/textarea/textarea.html @@ -8,10 +8,10 @@
- characters left + %0% characters left.
- Maximum {0} characters ({1} too many) + Maximum %0% characters, %1% too many.
diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/textbox/textbox.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/textbox/textbox.html index 8385c951b1..6e8543de26 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/textbox/textbox.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/textbox/textbox.html @@ -13,10 +13,10 @@
- characters left + %0% characters left.
- Maximum {0} characters ({1} too many) + Maximum %0% characters, %1% too many.
diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/da.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/da.xml index 52be26e2fc..b7a8196e92 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/da.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/da.xml @@ -1569,8 +1569,8 @@ Mange hilsner fra Umbraco robotten Ingen ordbog elementer at vælge imellem - {0} tegn tilbage.]]> - {1} for mange.]]> + %0% tegn tilbage.]]> + %1% for mange.]]> Slettet indhold med Id: {0} Relateret til original "parent" med id: {1} diff --git a/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml b/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml index 218812270d..ab3a281114 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/en.xml @@ -2079,8 +2079,8 @@ To manage your website, simply open the Umbraco back office and start adding con No Dictionary items to choose from - {0} characters left.]]> - {1} too many.]]> + %0% characters left.]]> + %1% too many.]]> Trashed content with Id: {0} related to original parent content with Id: {1} 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 9f578da1b6..9c66d1bef8 100644 --- a/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml +++ b/src/Umbraco.Web.UI/Umbraco/config/lang/en_us.xml @@ -2091,8 +2091,8 @@ To manage your website, simply open the Umbraco back office and start adding con No Dictionary items to choose from - {0} characters left.]]> - {1} too many.]]> + %0% characters left.]]> + %1% too many.]]> Trashed content with Id: {0} related to original parent content with Id: {1} From 118f6c7e50c1619bfb6e1f24b5832829754fbe4f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Mon, 25 Mar 2019 14:46:42 +0100 Subject: [PATCH 20/50] added the ability to watch tokens in localization service --- .../localization/localize.directive.js | 19 +++++++--- .../common/services/localization.service.js | 36 +++++++++++++------ .../propertyeditors/textarea/textarea.html | 4 +-- .../propertyeditors/textbox/textbox.html | 4 +-- 4 files changed, 45 insertions(+), 18 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/directives/components/localization/localize.directive.js b/src/Umbraco.Web.UI.Client/src/common/directives/components/localization/localize.directive.js index c3093eee9e..df3770056e 100644 --- a/src/Umbraco.Web.UI.Client/src/common/directives/components/localization/localize.directive.js +++ b/src/Umbraco.Web.UI.Client/src/common/directives/components/localization/localize.directive.js @@ -32,16 +32,27 @@ angular.module("umbraco.directives") restrict: 'E', scope:{ key: '@', - tokens: '=' + tokens: '=', + watchTokens: '@' }, replace: true, link: function (scope, element, attrs) { var key = scope.key; - var tokens = scope.tokens ? scope.tokens : null; - localizationService.localize(key, tokens).then(function(value){ - element.html(value); + scope.text = ""; + + // A render function to be able to update tokens as values update. + function render() { + element.html(localizationService.tokenReplace(scope.text, scope.tokens || null)); + } + + localizationService.localize(key).then(function(value){ + scope.text = value; + render(); }); + if (scope.watchTokens === 'true') { + scope.$watch("tokens", render, true); + } } }; }) 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 2a9afdfa94..ea2ad73263 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 @@ -43,16 +43,11 @@ angular.module('umbraco.services') var entry = dictionary[value]; if (entry) { - if (tokens) { - for (var i = 0; i < tokens.length; i++) { - entry = entry.replace("%" + i + "%", tokens[i]); - } - } - return entry; + return service.tokenReplace(entry, tokens); } return "[" + value + "]"; } - + var service = { // array to hold the localized resource string entries dictionary: [], @@ -127,7 +122,29 @@ angular.module('umbraco.services') } return value; }, - + + + /** + * @ngdoc method + * @name umbraco.services.localizationService#tokenReplace + * @methodOf umbraco.services.localizationService + * + * @description + * Helper to replace tokens + * @param {String} value the text-string to manipulate + * @param {Array} tekens An array of tokens values + * @returns {String} Replaced test-string + */ + tokenReplace: function (text, tokens) { + if (tokens) { + for (var i = 0; i < tokens.length; i++) { + text = text.replace("%" + i + "%", tokens[i]); + } + } + return text; + }, + + /** * @ngdoc method * @name umbraco.services.localizationService#localize @@ -146,8 +163,7 @@ angular.module('umbraco.services') */ localize: function (value, tokens) { return service.initLocalizedResources().then(function (dic) { - var val = _lookup(value, tokens, dic); - return val; + return _lookup(value, tokens, dic); }); }, diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/textarea/textarea.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/textarea/textarea.html index 94935423e8..1be37d92d6 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/textarea/textarea.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/textarea/textarea.html @@ -8,10 +8,10 @@
- %0% characters left. + %0% characters left.
- Maximum %0% characters, %1% too many. + Maximum %0% characters, %1% too many.
diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/textbox/textbox.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/textbox/textbox.html index 6e8543de26..92f02b9f5b 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/textbox/textbox.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/textbox/textbox.html @@ -13,10 +13,10 @@
- %0% characters left. + %0% characters left.
- Maximum %0% characters, %1% too many. + Maximum %0% characters, %1% too many.
From 2bfa81c7079d28625a7f4b65bc5e6dbaa50d3b96 Mon Sep 17 00:00:00 2001 From: Shannon Date: Tue, 26 Mar 2019 20:07:42 +1100 Subject: [PATCH 21/50] fixes 4627 + makes front-end validations appear as soon as they are invalid again. --- .../directives/validation/valpropertymsg.directive.js | 11 +++++++---- 1 file changed, 7 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 9ee83dc2ba..c027e0778e 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 @@ -62,8 +62,8 @@ function valPropertyMsg(serverValidationManager) { if (!watcher) { watcher = scope.$watch("currentProperty.value", function (newValue, oldValue) { - - if (!newValue || angular.equals(newValue, oldValue)) { + + if (angular.equals(newValue, oldValue)) { return; } @@ -78,10 +78,12 @@ function valPropertyMsg(serverValidationManager) { // 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 === 1 && angular.isArray(formCtrl.$error.valPropertyMsg)) || (formCtrl.$invalid && angular.isArray(formCtrl.$error.valServer))) { + if (errCount === 0 || (errCount === 1 && angular.isArray(formCtrl.$error.valPropertyMsg)) || (formCtrl.$invalid && angular.isArray(formCtrl.$error.valServer))) { scope.errorMsg = ""; formCtrl.$setValidity('valPropertyMsg', true); - stopWatch(); + } else if (showValidation && scope.errorMsg === "") { + formCtrl.$setValidity('valPropertyMsg', false); + scope.errorMsg = getErrorMsg(); } }, true); } @@ -152,6 +154,7 @@ function valPropertyMsg(serverValidationManager) { showValidation = true; if (hasError && scope.errorMsg === "") { scope.errorMsg = getErrorMsg(); + startWatch(); } else if (!hasError) { scope.errorMsg = ""; From 080ace90b208b29ecca5e34bd0046fc7019d56ce Mon Sep 17 00:00:00 2001 From: Shannon Date: Tue, 26 Mar 2019 20:09:12 +1100 Subject: [PATCH 22/50] Bugfix: https://github.com/umbraco/Umbraco-CMS/issues/4827 - Fixed MediaPickerValueConverter when config is Only Images --- .../MediaPickerValueConverter.cs | 58 +++++++++++++------ 1 file changed, 39 insertions(+), 19 deletions(-) diff --git a/src/Umbraco.Web/PropertyEditors/ValueConverters/MediaPickerValueConverter.cs b/src/Umbraco.Web/PropertyEditors/ValueConverters/MediaPickerValueConverter.cs index 802f42ed9e..fb3134c9a3 100644 --- a/src/Umbraco.Web/PropertyEditors/ValueConverters/MediaPickerValueConverter.cs +++ b/src/Umbraco.Web/PropertyEditors/ValueConverters/MediaPickerValueConverter.cs @@ -1,4 +1,5 @@ using System; +using System.Collections; using System.Collections.Generic; using System.Linq; using Umbraco.Core; @@ -14,11 +15,18 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters [DefaultPropertyValueConverter] public class MediaPickerValueConverter : PropertyValueConverterBase { + // hard-coding "image" here but that's how it works at UI level too + private const string ImageTypeAlias = "image"; + + private readonly IPublishedModelFactory _publishedModelFactory; private readonly IPublishedSnapshotAccessor _publishedSnapshotAccessor; - public MediaPickerValueConverter(IPublishedSnapshotAccessor publishedSnapshotAccessor) + public MediaPickerValueConverter(IPublishedSnapshotAccessor publishedSnapshotAccessor, + IPublishedModelFactory publishedModelFactory) { - _publishedSnapshotAccessor = publishedSnapshotAccessor ?? throw new ArgumentNullException(nameof(publishedSnapshotAccessor)); + _publishedSnapshotAccessor = publishedSnapshotAccessor ?? + throw new ArgumentNullException(nameof(publishedSnapshotAccessor)); + _publishedModelFactory = publishedModelFactory; } public override bool IsConverter(PublishedPropertyType propertyType) @@ -31,15 +39,19 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters var isMultiple = IsMultipleDataType(propertyType.DataType); var isOnlyImages = IsOnlyImagesDataType(propertyType.DataType); - // hard-coding "image" here but that's how it works at UI level too - return isMultiple - ? (isOnlyImages ? typeof(IEnumerable<>).MakeGenericType(ModelType.For("image")) : typeof(IEnumerable)) - : (isOnlyImages ? ModelType.For("image") : typeof(IPublishedContent)); + ? isOnlyImages + ? typeof(IEnumerable<>).MakeGenericType(ModelType.For(ImageTypeAlias)) + : typeof(IEnumerable) + : isOnlyImages + ? ModelType.For(ImageTypeAlias) + : typeof(IPublishedContent); } public override PropertyCacheLevel GetPropertyCacheLevel(PublishedPropertyType propertyType) - => PropertyCacheLevel.Snapshot; + { + return PropertyCacheLevel.Snapshot; + } private bool IsMultipleDataType(PublishedDataType dataType) { @@ -53,26 +65,31 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters return config.OnlyImages; } - public override object ConvertSourceToIntermediate(IPublishedElement owner, PublishedPropertyType propertyType, object source, bool preview) + public override object ConvertSourceToIntermediate(IPublishedElement owner, PublishedPropertyType propertyType, + object source, bool preview) { if (source == null) return null; var nodeIds = source.ToString() - .Split(new[] { "," }, StringSplitOptions.RemoveEmptyEntries) + .Split(new[] {","}, StringSplitOptions.RemoveEmptyEntries) .Select(Udi.Parse) .ToArray(); return nodeIds; } - public override object ConvertIntermediateToObject(IPublishedElement owner, PublishedPropertyType propertyType, PropertyCacheLevel cacheLevel, object source, bool preview) + public override object ConvertIntermediateToObject(IPublishedElement owner, PublishedPropertyType propertyType, + PropertyCacheLevel cacheLevel, object source, bool preview) { - if (source == null) - { - return null; - } + var isMultiple = IsMultipleDataType(propertyType.DataType); + var isOnlyImages = IsOnlyImagesDataType(propertyType.DataType); + + var udis = (Udi[]) source; + var mediaItems = isOnlyImages + ? _publishedModelFactory.CreateModelList(ImageTypeAlias) + : new List(); + + if (source == null) return isMultiple ? mediaItems : null; - var udis = (Udi[])source; - var mediaItems = new List(); if (udis.Any()) { foreach (var udi in udis) @@ -84,12 +101,15 @@ namespace Umbraco.Web.PropertyEditors.ValueConverters mediaItems.Add(item); } - if (IsMultipleDataType(propertyType.DataType)) - return mediaItems; - return mediaItems.FirstOrDefault(); + return isMultiple ? mediaItems : FirstOrDefault(mediaItems); } return source; } + + private object FirstOrDefault(IList mediaItems) + { + return mediaItems.Count == 0 ? null : mediaItems[0]; + } } } From 4fcf2ce7bdac14b49ca318021335d4fdd89ea252 Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Tue, 12 Mar 2019 08:17:09 +0100 Subject: [PATCH 23/50] Fixed issue when creating variant content templates, but the properties is invariant. Before this change an exception was thrown --- src/Umbraco.Core/Services/Implement/ContentService.cs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/Umbraco.Core/Services/Implement/ContentService.cs b/src/Umbraco.Core/Services/Implement/ContentService.cs index 5bdc0959da..82ebd92f0b 100644 --- a/src/Umbraco.Core/Services/Implement/ContentService.cs +++ b/src/Umbraco.Core/Services/Implement/ContentService.cs @@ -2875,7 +2875,14 @@ namespace Umbraco.Core.Services.Implement { foreach (var property in blueprint.Properties) { - content.SetValue(property.Alias, property.GetValue(culture), culture); + if (property.PropertyType.VariesByCulture()) + { + content.SetValue(property.Alias, property.GetValue(culture), culture); + } + else + { + content.SetValue(property.Alias, property.GetValue()); + } } content.Name = blueprint.Name; From 8a56b3db163620a6c0e04bba6f0f1ca015eb06ee Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Sun, 17 Mar 2019 20:51:42 +0100 Subject: [PATCH 24/50] Fix the "Language" dropdown in the "Culture and Hostnames" dialog --- .../src/views/content/content.assigndomain.controller.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/content/content.assigndomain.controller.js b/src/Umbraco.Web.UI.Client/src/views/content/content.assigndomain.controller.js index 0f27f3046c..5fa1eebd0b 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/content.assigndomain.controller.js +++ b/src/Umbraco.Web.UI.Client/src/views/content/content.assigndomain.controller.js @@ -46,8 +46,7 @@ if (data.language !== "undefined") { var lang = vm.languages.filter(function (l) { - return matchLanguageById(l, data.language.Id); - + return matchLanguageById(l, data.language); }); if (lang.length > 0) { vm.language = lang[0]; From a70cb51559a225f6e13c38be3893759867c4e3e2 Mon Sep 17 00:00:00 2001 From: Stephan Date: Fri, 22 Feb 2019 15:27:22 +0100 Subject: [PATCH 25/50] NuCache: cleanup scoped objects --- .../Scoping/ScopeContextualBase.cs | 33 +++++++++++++++-- .../NuCache/PublishedSnapshotService.cs | 36 +++++++++---------- 2 files changed, 48 insertions(+), 21 deletions(-) diff --git a/src/Umbraco.Core/Scoping/ScopeContextualBase.cs b/src/Umbraco.Core/Scoping/ScopeContextualBase.cs index 7461142234..1f2b6155e6 100644 --- a/src/Umbraco.Core/Scoping/ScopeContextualBase.cs +++ b/src/Umbraco.Core/Scoping/ScopeContextualBase.cs @@ -2,24 +2,43 @@ namespace Umbraco.Core.Scoping { - // base class for an object that will be enlisted in scope context, if any. it - // must be used in a 'using' block, and if not scoped, released when disposed, - // else when scope context runs enlisted actions + /// + /// Provides a base class for scope contextual objects. + /// + /// + /// A scope contextual object is enlisted in the current scope context, + /// if any, and released when the context exists. It must be used in a 'using' + /// block, and will be released when disposed, if not part of a scope. + /// public abstract class ScopeContextualBase : IDisposable { private bool _using, _scoped; + /// + /// Gets a contextual object. + /// + /// The type of the object. + /// A scope provider. + /// A context key for the object. + /// A function producing the contextual object. + /// The contextual object. + /// + /// + /// public static T Get(IScopeProvider scopeProvider, string key, Func ctor) where T : ScopeContextualBase { + // no scope context = create a non-scoped object var scopeContext = scopeProvider.Context; if (scopeContext == null) return ctor(false); + // create & enlist the scoped object var w = scopeContext.Enlist("ScopeContextualBase_" + key, () => ctor(true), (completed, item) => { item.Release(completed); }); + // the object can be 'used' only once at a time if (w._using) throw new InvalidOperationException("panic: used."); w._using = true; w._scoped = true; @@ -27,6 +46,10 @@ namespace Umbraco.Core.Scoping return w; } + /// + /// + /// If not scoped, then this releases the contextual object. + /// public void Dispose() { _using = false; @@ -35,6 +58,10 @@ namespace Umbraco.Core.Scoping Release(true); } + /// + /// Releases the contextual object. + /// + /// A value indicating whether the scoped operation completed. public abstract void Release(bool completed); } } diff --git a/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs b/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs index e19531a25b..541ff2ea23 100755 --- a/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs @@ -330,14 +330,15 @@ namespace Umbraco.Web.PublishedCache.NuCache private void LockAndLoadContent(Action action) { + // first get a writer, then a scope + // if there already is a scope, the writer will attach to it + // otherwise, it will only exist here - cheap using (_contentStore.GetWriter(_scopeProvider)) + using (var scope = _scopeProvider.CreateScope()) { - using (var scope = _scopeProvider.CreateScope()) - { - scope.ReadLock(Constants.Locks.ContentTree); - action(scope); - scope.Complete(); - } + scope.ReadLock(Constants.Locks.ContentTree); + action(scope); + scope.Complete(); } } @@ -399,14 +400,13 @@ namespace Umbraco.Web.PublishedCache.NuCache private void LockAndLoadMedia(Action action) { + // see note in LockAndLoadContent using (_mediaStore.GetWriter(_scopeProvider)) + using (var scope = _scopeProvider.CreateScope()) { - using (var scope = _scopeProvider.CreateScope()) - { - scope.ReadLock(Constants.Locks.MediaTree); - action(scope); - scope.Complete(); - } + scope.ReadLock(Constants.Locks.MediaTree); + action(scope); + scope.Complete(); } } @@ -528,14 +528,13 @@ namespace Umbraco.Web.PublishedCache.NuCache private void LockAndLoadDomains() { + // see note in LockAndLoadContent using (_domainStore.GetWriter(_scopeProvider)) + using (var scope = _scopeProvider.CreateScope()) { - using (var scope = _scopeProvider.CreateScope()) - { - scope.ReadLock(Constants.Locks.Domains); - LoadDomainsLocked(); - scope.Complete(); - } + scope.ReadLock(Constants.Locks.Domains); + LoadDomainsLocked(); + scope.Complete(); } } @@ -858,6 +857,7 @@ namespace Umbraco.Web.PublishedCache.NuCache if (_isReady == false) return; + // see note in LockAndLoadContent using (_domainStore.GetWriter(_scopeProvider)) { foreach (var payload in payloads) From 13a8548d3a19e6de90ef180a9f87115c80abf1c3 Mon Sep 17 00:00:00 2001 From: Stephan Date: Fri, 22 Feb 2019 15:30:55 +0100 Subject: [PATCH 26/50] NuCache: rename things --- .../Cache/SnapDictionaryTests.cs | 10 ++-- .../PublishedCache/NuCache/ContentStore.cs | 56 ++++++++--------- .../PublishedCache/NuCache/SnapDictionary.cs | 60 +++++++++---------- 3 files changed, 63 insertions(+), 63 deletions(-) diff --git a/src/Umbraco.Tests/Cache/SnapDictionaryTests.cs b/src/Umbraco.Tests/Cache/SnapDictionaryTests.cs index 013dbadbb8..0ee0d180c3 100644 --- a/src/Umbraco.Tests/Cache/SnapDictionaryTests.cs +++ b/src/Umbraco.Tests/Cache/SnapDictionaryTests.cs @@ -388,8 +388,8 @@ namespace Umbraco.Tests.Cache // collect liveGen GC.Collect(); - SnapDictionary.GenerationObject genObj; - Assert.IsTrue(d.Test.GenerationObjects.TryPeek(out genObj)); + SnapDictionary.GenObj genObj; + Assert.IsTrue(d.Test.GenObjs.TryPeek(out genObj)); genObj = null; // in Release mode, it works, but in Debug mode, the weak reference is still alive @@ -399,14 +399,14 @@ namespace Umbraco.Tests.Cache GC.Collect(); #endif - Assert.IsTrue(d.Test.GenerationObjects.TryPeek(out genObj)); - Assert.IsFalse(genObj.WeakReference.IsAlive); // snapshot is gone, along with its reference + Assert.IsTrue(d.Test.GenObjs.TryPeek(out genObj)); + Assert.IsFalse(genObj.WeakGenRef.IsAlive); // snapshot is gone, along with its reference await d.CollectAsync(); Assert.AreEqual(0, d.Test.GetValues(1).Length); // null value is gone Assert.AreEqual(0, d.Count); // item is gone - Assert.AreEqual(0, d.Test.GenerationObjects.Count); + Assert.AreEqual(0, d.Test.GenObjs.Count); Assert.AreEqual(0, d.SnapCount); // snapshot is gone Assert.AreEqual(0, d.GenCount); // and generation has been dequeued } diff --git a/src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs b/src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs index b3996050a6..e78c4d419e 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs @@ -29,8 +29,8 @@ namespace Umbraco.Web.PublishedCache.NuCache private readonly ILogger _logger; private BPlusTree _localDb; - private readonly ConcurrentQueue _genRefRefs; - private GenRefRef _genRefRef; + private readonly ConcurrentQueue _genObjs; + private GenObj _genObj; private readonly object _wlocko = new object(); private readonly object _rlocko = new object(); private long _liveGen, _floorGen; @@ -64,8 +64,8 @@ namespace Umbraco.Web.PublishedCache.NuCache _contentTypesByAlias = new ConcurrentDictionary>(StringComparer.InvariantCultureIgnoreCase); _xmap = new ConcurrentDictionary(); - _genRefRefs = new ConcurrentQueue(); - _genRefRef = null; // no initial gen exists + _genObjs = new ConcurrentQueue(); + _genObj = null; // no initial gen exists _liveGen = _floorGen = 0; _nextGen = false; // first time, must create a snapshot _collectAuto = true; // collect automatically by default @@ -836,8 +836,8 @@ namespace Umbraco.Web.PublishedCache.NuCache // if no next generation is required, and we already have one, // use it and create a new snapshot - if (_nextGen == false && _genRefRef != null) - return new Snapshot(this, _genRefRef.GetGenRef() + if (_nextGen == false && _genObj != null) + return new Snapshot(this, _genObj.GetGenRef() #if DEBUG , _logger #endif @@ -852,15 +852,15 @@ namespace Umbraco.Web.PublishedCache.NuCache var snapGen = _nextGen ? _liveGen - 1 : _liveGen; // create a new gen ref unless we already have it - if (_genRefRef == null) - _genRefRefs.Enqueue(_genRefRef = new GenRefRef(snapGen)); - else if (_genRefRef.Gen != snapGen) + if (_genObj == null) + _genObjs.Enqueue(_genObj = new GenObj(snapGen)); + else if (_genObj.Gen != snapGen) throw new Exception("panic"); } else { // not write-locked, can use latest gen, create a new gen ref - _genRefRefs.Enqueue(_genRefRef = new GenRefRef(_liveGen)); + _genObjs.Enqueue(_genObj = new GenObj(_liveGen)); _nextGen = false; // this is the ONLY thing that triggers a _liveGen++ } @@ -873,7 +873,7 @@ namespace Umbraco.Web.PublishedCache.NuCache // - the genRefRef weak ref is dead because all snapshots have been collected // in both cases, we will dequeue and collect - var snapshot = new Snapshot(this, _genRefRef.GetGenRef() + var snapshot = new Snapshot(this, _genObj.GetGenRef() #if DEBUG , _logger #endif @@ -930,10 +930,10 @@ namespace Umbraco.Web.PublishedCache.NuCache #if DEBUG _logger.Debug("Collect."); #endif - while (_genRefRefs.TryPeek(out GenRefRef genRefRef) && (genRefRef.Count == 0 || genRefRef.WGenRef.IsAlive == false)) + while (_genObjs.TryPeek(out var genObj) && (genObj.Count == 0 || genObj.WeakGenRef.IsAlive == false)) { - _genRefRefs.TryDequeue(out genRefRef); // cannot fail since TryPeek has succeeded - _floorGen = genRefRef.Gen; + _genObjs.TryDequeue(out genObj); // cannot fail since TryPeek has succeeded + _floorGen = genObj.Gen; #if DEBUG //_logger.Debug("_floorGen=" + _floorGen + ", _liveGen=" + _liveGen); #endif @@ -1009,9 +1009,9 @@ namespace Umbraco.Web.PublishedCache.NuCache await task; } - public long GenCount => _genRefRefs.Count; + public long GenCount => _genObjs.Count; - public long SnapCount => _genRefRefs.Sum(x => x.Count); + public long SnapCount => _genObjs.Sum(x => x.Count); #endregion @@ -1100,7 +1100,7 @@ namespace Umbraco.Web.PublishedCache.NuCache _store = store; _genRef = genRef; _gen = genRef.Gen; - Interlocked.Increment(ref genRef.GenRefRef.Count); + Interlocked.Increment(ref genRef.GenObj.Count); //_thisCount = _count++; #if DEBUG @@ -1201,46 +1201,46 @@ namespace Umbraco.Web.PublishedCache.NuCache { if (_gen < 0) return; #if DEBUG - _logger.Debug("Dispose snapshot ({Snapshot})", _genRef?.GenRefRef.Count.ToString() ?? "live"); + _logger.Debug("Dispose snapshot ({Snapshot})", _genRef?.GenObj.Count.ToString() ?? "live"); #endif _gen = -1; if (_genRef != null) - Interlocked.Decrement(ref _genRef.GenRefRef.Count); + Interlocked.Decrement(ref _genRef.GenObj.Count); GC.SuppressFinalize(this); } } - internal class GenRefRef + internal class GenObj { - public GenRefRef(long gen) + public GenObj(long gen) { Gen = gen; - WGenRef = new WeakReference(null); + WeakGenRef = new WeakReference(null); } public GenRef GetGenRef() { // not thread-safe but always invoked from within a lock - var genRef = (GenRef) WGenRef.Target; + var genRef = (GenRef) WeakGenRef.Target; if (genRef == null) - WGenRef.Target = genRef = new GenRef(this, Gen); + WeakGenRef.Target = genRef = new GenRef(this, Gen); return genRef; } public readonly long Gen; - public readonly WeakReference WGenRef; + public readonly WeakReference WeakGenRef; public int Count; } internal class GenRef { - public GenRef(GenRefRef genRefRef, long gen) + public GenRef(GenObj genObj, long gen) { - GenRefRef = genRefRef; + GenObj = genObj; Gen = gen; } - public readonly GenRefRef GenRefRef; + public readonly GenObj GenObj; public readonly long Gen; } diff --git a/src/Umbraco.Web/PublishedCache/NuCache/SnapDictionary.cs b/src/Umbraco.Web/PublishedCache/NuCache/SnapDictionary.cs index 30f6e7e638..eebbe3122f 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/SnapDictionary.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/SnapDictionary.cs @@ -21,8 +21,8 @@ namespace Umbraco.Web.PublishedCache.NuCache // Readers are lock-free private readonly ConcurrentDictionary _items; - private readonly ConcurrentQueue _generationObjects; - private GenerationObject _generationObject; + private readonly ConcurrentQueue _genObjs; + private GenObj _genObj; private readonly object _wlocko = new object(); private readonly object _rlocko = new object(); private long _liveGen, _floorGen; @@ -41,8 +41,8 @@ namespace Umbraco.Web.PublishedCache.NuCache public SnapDictionary() { _items = new ConcurrentDictionary(); - _generationObjects = new ConcurrentQueue(); - _generationObject = null; // no initial gen exists + _genObjs = new ConcurrentQueue(); + _genObj = null; // no initial gen exists _liveGen = _floorGen = 0; _nextGen = false; // first time, must create a snapshot _collectAuto = true; // collect automatically by default @@ -339,8 +339,8 @@ namespace Umbraco.Web.PublishedCache.NuCache // if no next generation is required, and we already have one, // use it and create a new snapshot - if (_nextGen == false && _generationObject != null) - return new Snapshot(this, _generationObject.GetReference()); + if (_nextGen == false && _genObj != null) + return new Snapshot(this, _genObj.GetGenRef()); // else we need to try to create a new gen ref // whether we are wlocked or not, noone can rlock while we do, @@ -351,15 +351,15 @@ namespace Umbraco.Web.PublishedCache.NuCache var snapGen = _nextGen ? _liveGen - 1 : _liveGen; // create a new gen ref unless we already have it - if (_generationObject == null) - _generationObjects.Enqueue(_generationObject = new GenerationObject(snapGen)); - else if (_generationObject.Gen != snapGen) + if (_genObj == null) + _genObjs.Enqueue(_genObj = new GenObj(snapGen)); + else if (_genObj.Gen != snapGen) throw new Exception("panic"); } else { // not write-locked, can use latest gen, create a new gen ref - _generationObjects.Enqueue(_generationObject = new GenerationObject(_liveGen)); + _genObjs.Enqueue(_genObj = new GenObj(_liveGen)); _nextGen = false; // this is the ONLY thing that triggers a _liveGen++ } @@ -372,7 +372,7 @@ namespace Umbraco.Web.PublishedCache.NuCache // - the genRefRef weak ref is dead because all snapshots have been collected // in both cases, we will dequeue and collect - var snapshot = new Snapshot(this, _generationObject.GetReference()); + var snapshot = new Snapshot(this, _genObj.GetGenRef()); // reading _floorGen is safe if _collectTask is null if (_collectTask == null && _collectAuto && _liveGen - _floorGen > CollectMinGenDelta) @@ -416,10 +416,10 @@ namespace Umbraco.Web.PublishedCache.NuCache private void Collect() { // see notes in CreateSnapshot - while (_generationObjects.TryPeek(out GenerationObject generationObject) && (generationObject.Count == 0 || generationObject.WeakReference.IsAlive == false)) + while (_genObjs.TryPeek(out var genObj) && (genObj.Count == 0 || genObj.WeakGenRef.IsAlive == false)) { - _generationObjects.TryDequeue(out generationObject); // cannot fail since TryPeek has succeeded - _floorGen = generationObject.Gen; + _genObjs.TryDequeue(out genObj); // cannot fail since TryPeek has succeeded + _floorGen = genObj.Gen; } Collect(_items); @@ -490,9 +490,9 @@ namespace Umbraco.Web.PublishedCache.NuCache // await task; } - public long GenCount => _generationObjects.Count; + public long GenCount => _genObjs.Count; - public long SnapCount => _generationObjects.Sum(x => x.Count); + public long SnapCount => _genObjs.Sum(x => x.Count); #endregion @@ -520,7 +520,7 @@ namespace Umbraco.Web.PublishedCache.NuCache set => _dict._collectAuto = value; } - public ConcurrentQueue GenerationObjects => _dict._generationObjects; + public ConcurrentQueue GenObjs => _dict._genObjs; public Snapshot LiveSnapshot => new Snapshot(_dict, _dict._liveGen); @@ -586,8 +586,8 @@ namespace Umbraco.Web.PublishedCache.NuCache { _store = store; _generationReference = generationReference; - _gen = generationReference.GenerationObject.Gen; - _generationReference.GenerationObject.Reference(); + _gen = generationReference.GenObj.Gen; + _generationReference.GenObj.Reference(); } internal Snapshot(SnapDictionary store, long gen) @@ -634,30 +634,30 @@ namespace Umbraco.Web.PublishedCache.NuCache { if (_gen < 0) return; _gen = -1; - _generationReference?.GenerationObject.Release(); + _generationReference?.GenObj.Release(); GC.SuppressFinalize(this); } } - internal class GenerationObject + internal class GenObj { - public GenerationObject(long gen) + public GenObj(long gen) { Gen = gen; - WeakReference = new WeakReference(null); + WeakGenRef = new WeakReference(null); } - public GenerationReference GetReference() + public GenerationReference GetGenRef() { // not thread-safe but always invoked from within a lock - var generationReference = (GenerationReference) WeakReference.Target; + var generationReference = (GenerationReference) WeakGenRef.Target; if (generationReference == null) - WeakReference.Target = generationReference = new GenerationReference(this); + WeakGenRef.Target = generationReference = new GenerationReference(this); return generationReference; } public readonly long Gen; - public readonly WeakReference WeakReference; + public readonly WeakReference WeakGenRef; public int Count; public void Reference() @@ -673,12 +673,12 @@ namespace Umbraco.Web.PublishedCache.NuCache internal class GenerationReference { - public GenerationReference(GenerationObject generationObject) + public GenerationReference(GenObj genObj) { - GenerationObject = generationObject; + GenObj = genObj; } - public readonly GenerationObject GenerationObject; + public readonly GenObj GenObj; } #endregion From c56b8863eec4eb64504da614828a35bf4ec53e65 Mon Sep 17 00:00:00 2001 From: Stephan Date: Fri, 22 Feb 2019 16:03:39 +0100 Subject: [PATCH 27/50] NuCache: reorg some classes --- .../Cache/SnapDictionaryTests.cs | 4 +- .../PublishedCache/NuCache/ContentStore.cs | 53 +----------- .../PublishedCache/NuCache/Snap/GenObj.cs | 37 +++++++++ .../PublishedCache/NuCache/Snap/GenRef.cs | 13 +++ .../PublishedCache/NuCache/Snap/LinkedNode.cs | 20 +++++ .../PublishedCache/NuCache/SnapDictionary.cs | 80 +++---------------- src/Umbraco.Web/Umbraco.Web.csproj | 3 + 7 files changed, 87 insertions(+), 123 deletions(-) create mode 100644 src/Umbraco.Web/PublishedCache/NuCache/Snap/GenObj.cs create mode 100644 src/Umbraco.Web/PublishedCache/NuCache/Snap/GenRef.cs create mode 100644 src/Umbraco.Web/PublishedCache/NuCache/Snap/LinkedNode.cs diff --git a/src/Umbraco.Tests/Cache/SnapDictionaryTests.cs b/src/Umbraco.Tests/Cache/SnapDictionaryTests.cs index 0ee0d180c3..82c27ebc38 100644 --- a/src/Umbraco.Tests/Cache/SnapDictionaryTests.cs +++ b/src/Umbraco.Tests/Cache/SnapDictionaryTests.cs @@ -5,6 +5,7 @@ using Moq; using NUnit.Framework; using Umbraco.Core.Scoping; using Umbraco.Web.PublishedCache.NuCache; +using Umbraco.Web.PublishedCache.NuCache.Snap; namespace Umbraco.Tests.Cache { @@ -388,8 +389,7 @@ namespace Umbraco.Tests.Cache // collect liveGen GC.Collect(); - SnapDictionary.GenObj genObj; - Assert.IsTrue(d.Test.GenObjs.TryPeek(out genObj)); + Assert.IsTrue(d.Test.GenObjs.TryPeek(out var genObj)); genObj = null; // in Release mode, it works, but in Debug mode, the weak reference is still alive diff --git a/src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs b/src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs index e78c4d419e..dd5805daa1 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs @@ -8,6 +8,7 @@ using CSharpTest.Net.Collections; using Umbraco.Core.Logging; using Umbraco.Core.Models.PublishedContent; using Umbraco.Core.Scoping; +using Umbraco.Web.PublishedCache.NuCache.Snap; namespace Umbraco.Web.PublishedCache.NuCache { @@ -1061,24 +1062,6 @@ namespace Umbraco.Web.PublishedCache.NuCache #region Classes - private class LinkedNode - where TValue: class - { - public LinkedNode(TValue value, long gen, LinkedNode next = null) - { - Value = value; - Gen = gen; - Next = next; - } - - internal readonly long Gen; - - // reading & writing references is thread-safe on all .NET platforms - // mark as volatile to ensure we always read the correct value - internal volatile TValue Value; - internal volatile LinkedNode Next; - } - public class Snapshot : IDisposable { private readonly ContentStore _store; @@ -1210,40 +1193,6 @@ namespace Umbraco.Web.PublishedCache.NuCache } } - internal class GenObj - { - public GenObj(long gen) - { - Gen = gen; - WeakGenRef = new WeakReference(null); - } - - public GenRef GetGenRef() - { - // not thread-safe but always invoked from within a lock - var genRef = (GenRef) WeakGenRef.Target; - if (genRef == null) - WeakGenRef.Target = genRef = new GenRef(this, Gen); - return genRef; - } - - public readonly long Gen; - public readonly WeakReference WeakGenRef; - public int Count; - } - - internal class GenRef - { - public GenRef(GenObj genObj, long gen) - { - GenObj = genObj; - Gen = gen; - } - - public readonly GenObj GenObj; - public readonly long Gen; - } - #endregion } } diff --git a/src/Umbraco.Web/PublishedCache/NuCache/Snap/GenObj.cs b/src/Umbraco.Web/PublishedCache/NuCache/Snap/GenObj.cs new file mode 100644 index 0000000000..b69dab7dac --- /dev/null +++ b/src/Umbraco.Web/PublishedCache/NuCache/Snap/GenObj.cs @@ -0,0 +1,37 @@ +using System; +using System.Threading; + +namespace Umbraco.Web.PublishedCache.NuCache.Snap +{ + internal class GenObj + { + public GenObj(long gen) + { + Gen = gen; + WeakGenRef = new WeakReference(null); + } + + public GenRef GetGenRef() + { + // not thread-safe but always invoked from within a lock + var genRef = (GenRef)WeakGenRef.Target; + if (genRef == null) + WeakGenRef.Target = genRef = new GenRef(this); + return genRef; + } + + public readonly long Gen; + public readonly WeakReference WeakGenRef; + public int Count; + + public void Reference() + { + Interlocked.Increment(ref Count); + } + + public void Release() + { + Interlocked.Decrement(ref Count); + } + } +} diff --git a/src/Umbraco.Web/PublishedCache/NuCache/Snap/GenRef.cs b/src/Umbraco.Web/PublishedCache/NuCache/Snap/GenRef.cs new file mode 100644 index 0000000000..ade0251b8d --- /dev/null +++ b/src/Umbraco.Web/PublishedCache/NuCache/Snap/GenRef.cs @@ -0,0 +1,13 @@ +namespace Umbraco.Web.PublishedCache.NuCache.Snap +{ + internal class GenRef + { + public GenRef(GenObj genObj) + { + GenObj = genObj; + } + + public readonly GenObj GenObj; + public long Gen => GenObj.Gen; + } +} diff --git a/src/Umbraco.Web/PublishedCache/NuCache/Snap/LinkedNode.cs b/src/Umbraco.Web/PublishedCache/NuCache/Snap/LinkedNode.cs new file mode 100644 index 0000000000..20d7e7ddcd --- /dev/null +++ b/src/Umbraco.Web/PublishedCache/NuCache/Snap/LinkedNode.cs @@ -0,0 +1,20 @@ +namespace Umbraco.Web.PublishedCache.NuCache.Snap +{ + internal class LinkedNode + where TValue : class + { + public LinkedNode(TValue value, long gen, LinkedNode next = null) + { + Value = value; + Gen = gen; + Next = next; + } + + public readonly long Gen; + + // reading & writing references is thread-safe on all .NET platforms + // mark as volatile to ensure we always read the correct value + public volatile TValue Value; + public volatile LinkedNode Next; + } +} \ No newline at end of file diff --git a/src/Umbraco.Web/PublishedCache/NuCache/SnapDictionary.cs b/src/Umbraco.Web/PublishedCache/NuCache/SnapDictionary.cs index eebbe3122f..f117a395b5 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/SnapDictionary.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/SnapDictionary.cs @@ -5,6 +5,7 @@ using System.Linq; using System.Threading; using System.Threading.Tasks; using Umbraco.Core.Scoping; +using Umbraco.Web.PublishedCache.NuCache.Snap; namespace Umbraco.Web.PublishedCache.NuCache { @@ -20,7 +21,7 @@ namespace Umbraco.Web.PublishedCache.NuCache // This class is optimized for many readers, few writers // Readers are lock-free - private readonly ConcurrentDictionary _items; + private readonly ConcurrentDictionary> _items; private readonly ConcurrentQueue _genObjs; private GenObj _genObj; private readonly object _wlocko = new object(); @@ -40,7 +41,7 @@ namespace Umbraco.Web.PublishedCache.NuCache public SnapDictionary() { - _items = new ConcurrentDictionary(); + _items = new ConcurrentDictionary>(); _genObjs = new ConcurrentQueue(); _genObj = null; // no initial gen exists _liveGen = _floorGen = 0; @@ -198,9 +199,9 @@ namespace Umbraco.Web.PublishedCache.NuCache public int Count => _items.Count; - private LinkedNode GetHead(TKey key) + private LinkedNode GetHead(TKey key) { - _items.TryGetValue(key, out LinkedNode link); // else null + _items.TryGetValue(key, out var link); // else null return link; } @@ -221,7 +222,7 @@ namespace Umbraco.Web.PublishedCache.NuCache // for an older gen - if value is different then insert a new // link for the new gen, with the new value if (link.Value != value) - _items.TryUpdate(key, new LinkedNode(value, _liveGen, link), link); + _items.TryUpdate(key, new LinkedNode(value, _liveGen, link), link); } else { @@ -235,7 +236,7 @@ namespace Umbraco.Web.PublishedCache.NuCache } else { - _items.TryAdd(key, new LinkedNode(value, _liveGen)); + _items.TryAdd(key, new LinkedNode(value, _liveGen)); } } finally @@ -261,7 +262,7 @@ namespace Umbraco.Web.PublishedCache.NuCache { if (kvp.Value.Gen < _liveGen) { - var link = new LinkedNode(null, _liveGen, kvp.Value); + var link = new LinkedNode(null, _liveGen, kvp.Value); _items.TryUpdate(kvp.Key, link, kvp.Value); } else @@ -425,7 +426,7 @@ namespace Umbraco.Web.PublishedCache.NuCache Collect(_items); } - private void Collect(ConcurrentDictionary dict) + private void Collect(ConcurrentDictionary> dict) { // it is OK to enumerate a concurrent dictionary and it does not lock // it - and here it's not an issue if we skip some items, they will be @@ -460,7 +461,7 @@ namespace Umbraco.Web.PublishedCache.NuCache // not live, null value, no next link = remove that one -- but only if // the dict has not been updated, have to do it via ICollection<> (thanks // Mr Toub) -- and if the dict has been updated there is nothing to collect - var idict = dict as ICollection>; + var idict = dict as ICollection>>; /*var removed =*/ idict.Remove(kvp); //Console.WriteLine("remove (" + (removed ? "true" : "false") + ")"); continue; @@ -526,7 +527,7 @@ namespace Umbraco.Web.PublishedCache.NuCache public GenVal[] GetValues(TKey key) { - _dict._items.TryGetValue(key, out LinkedNode link); // else null + _dict._items.TryGetValue(key, out var link); // else null if (link == null) return new GenVal[0]; @@ -559,23 +560,6 @@ namespace Umbraco.Web.PublishedCache.NuCache #region Classes - private class LinkedNode - { - public LinkedNode(TValue value, long gen, LinkedNode next = null) - { - Value = value; - Gen = gen; - Next = next; - } - - internal readonly long Gen; - - // reading & writing references is thread-safe on all .NET platforms - // mark as volatile to ensure we always read the correct value - internal volatile TValue Value; - internal volatile LinkedNode Next; - } - public class Snapshot : IDisposable { private readonly SnapDictionary _store; @@ -639,48 +623,6 @@ namespace Umbraco.Web.PublishedCache.NuCache } } - internal class GenObj - { - public GenObj(long gen) - { - Gen = gen; - WeakGenRef = new WeakReference(null); - } - - public GenerationReference GetGenRef() - { - // not thread-safe but always invoked from within a lock - var generationReference = (GenerationReference) WeakGenRef.Target; - if (generationReference == null) - WeakGenRef.Target = generationReference = new GenerationReference(this); - return generationReference; - } - - public readonly long Gen; - public readonly WeakReference WeakGenRef; - public int Count; - - public void Reference() - { - Interlocked.Increment(ref Count); - } - - public void Release() - { - Interlocked.Decrement(ref Count); - } - } - - internal class GenerationReference - { - public GenerationReference(GenObj genObj) - { - GenObj = genObj; - } - - public readonly GenObj GenObj; - } - #endregion } } diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index 09cc7d856a..c6cbc7cbaa 100755 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -205,6 +205,9 @@ + + + From 7e413ed4060b5aab7c4197be79214a77e8e0f050 Mon Sep 17 00:00:00 2001 From: Stephan Date: Fri, 22 Feb 2019 15:43:37 +0100 Subject: [PATCH 28/50] NuCache: troubleshooting --- .../Cache/SnapDictionaryTests.cs | 174 ++++++++++++++++-- .../PublishedCache/NuCache/ContentStore.cs | 7 +- .../PublishedCache/NuCache/SnapDictionary.cs | 105 +++++++---- 3 files changed, 233 insertions(+), 53 deletions(-) diff --git a/src/Umbraco.Tests/Cache/SnapDictionaryTests.cs b/src/Umbraco.Tests/Cache/SnapDictionaryTests.cs index 82c27ebc38..eb034eec26 100644 --- a/src/Umbraco.Tests/Cache/SnapDictionaryTests.cs +++ b/src/Umbraco.Tests/Cache/SnapDictionaryTests.cs @@ -712,16 +712,102 @@ namespace Umbraco.Tests.Cache } [Test] - public void NestedWriteLocking() + public void NestedWriteLocking1() + { + var d = new SnapDictionary(); + var t = d.Test; + t.CollectAuto = false; + + Assert.AreEqual(0, d.CreateSnapshot().Gen); + + // no scope context: writers nest, last one to be disposed commits + + var scopeProvider = GetScopeProvider(); + + using (var w1 = d.GetWriter(scopeProvider)) + { + Assert.AreEqual(1, t.LiveGen); + Assert.AreEqual(1, t.WLocked); + Assert.IsTrue(t.NextGen); + + using (var w2 = d.GetWriter(scopeProvider)) + { + Assert.AreEqual(1, t.LiveGen); + Assert.AreEqual(2, t.WLocked); + Assert.IsTrue(t.NextGen); + + Assert.AreNotSame(w1, w2); // get a new writer each time + + d.Set(1, "one"); + + Assert.AreEqual(0, d.CreateSnapshot().Gen); + } + + Assert.AreEqual(1, t.LiveGen); + Assert.AreEqual(1, t.WLocked); + Assert.IsTrue(t.NextGen); + + Assert.AreEqual(0, d.CreateSnapshot().Gen); + } + + Assert.AreEqual(1, t.LiveGen); + Assert.AreEqual(0, t.WLocked); + Assert.IsTrue(t.NextGen); + + Assert.AreEqual(1, d.CreateSnapshot().Gen); + } + + [Test] + public void NestedWriteLocking2() { var d = new SnapDictionary(); d.Test.CollectAuto = false; - var scopeProvider = GetScopeProvider(); - using (d.GetWriter(scopeProvider)) + Assert.AreEqual(0, d.CreateSnapshot().Gen); + + // scope context: writers enlist + + var scopeContext = new ScopeContext(); + var scopeProvider = GetScopeProvider(scopeContext); + + using (var w1 = d.GetWriter(scopeProvider)) { - using (d.GetWriter(scopeProvider)) + using (var w2 = d.GetWriter(scopeProvider)) { + Assert.AreSame(w1, w2); + + d.Set(1, "one"); + } + } + } + + [Test] + public void NestedWriteLocking3() + { + var d = new SnapDictionary(); + var t = d.Test; + t.CollectAuto = false; + + Assert.AreEqual(0, d.CreateSnapshot().Gen); + + var scopeContext = new ScopeContext(); + var scopeProvider1 = GetScopeProvider(); + var scopeProvider2 = GetScopeProvider(scopeContext); + + using (var w1 = d.GetWriter(scopeProvider1)) + { + Assert.AreEqual(1, t.LiveGen); + Assert.AreEqual(1, t.WLocked); + Assert.IsTrue(t.NextGen); + + using (var w2 = d.GetWriter(scopeProvider2)) + { + Assert.AreEqual(1, t.LiveGen); + Assert.AreEqual(2, t.WLocked); + Assert.IsTrue(t.NextGen); + + Assert.AreNotSame(w1, w2); + d.Set(1, "one"); } } @@ -846,7 +932,8 @@ namespace Umbraco.Tests.Cache Assert.AreEqual(2, s2.Gen); Assert.AreEqual("uno", s2.Get(1)); - var scopeProvider = GetScopeProvider(true); + var scopeContext = new ScopeContext(); + var scopeProvider = GetScopeProvider(scopeContext); using (d.GetWriter(scopeProvider)) { @@ -867,7 +954,7 @@ namespace Umbraco.Tests.Cache Assert.AreEqual(2, s4.Gen); Assert.AreEqual("uno", s4.Get(1)); - ((ScopeContext) scopeProvider.Context).ScopeExit(true); + scopeContext.ScopeExit(true); var s5 = d.CreateSnapshot(); Assert.AreEqual(3, s5.Gen); @@ -878,7 +965,8 @@ namespace Umbraco.Tests.Cache public void ScopeLocking2() { var d = new SnapDictionary(); - d.Test.CollectAuto = false; + var t = d.Test; + t.CollectAuto = false; // gen 1 d.Set(1, "one"); @@ -891,10 +979,11 @@ namespace Umbraco.Tests.Cache Assert.AreEqual(2, s2.Gen); Assert.AreEqual("uno", s2.Get(1)); - var scopeProviderMock = new Mock(); + Assert.AreEqual(2, t.LiveGen); + Assert.IsFalse(t.NextGen); + var scopeContext = new ScopeContext(); - scopeProviderMock.Setup(x => x.Context).Returns(scopeContext); - var scopeProvider = scopeProviderMock.Object; + var scopeProvider = GetScopeProvider(scopeContext); using (d.GetWriter(scopeProvider)) { @@ -905,18 +994,72 @@ namespace Umbraco.Tests.Cache Assert.AreEqual(2, s3.Gen); Assert.AreEqual("uno", s3.Get(1)); + // we made some changes, so a next gen is required + Assert.AreEqual(3, t.LiveGen); + Assert.IsTrue(t.NextGen); + Assert.AreEqual(1, t.WLocked); + // but live snapshot contains changes - var ls = d.Test.LiveSnapshot; + var ls = t.LiveSnapshot; Assert.AreEqual("ein", ls.Get(1)); Assert.AreEqual(3, ls.Gen); } + // nothing is committed until scope exits + Assert.AreEqual(3, t.LiveGen); + Assert.IsTrue(t.NextGen); + Assert.AreEqual(1, t.WLocked); + + // no changes until exit var s4 = d.CreateSnapshot(); Assert.AreEqual(2, s4.Gen); Assert.AreEqual("uno", s4.Get(1)); + // fixme - remove debugging code + /* + Exception caught = null; + var genFlip = 0; + var lckFlip = 0; + var thread = new System.Threading.Thread(() => + { + try + { + for (var i = 0; i < 20; i++) + { + if (t.LiveGen == 2 && genFlip == 0) genFlip = i; // flips at 1 + if (t.WLocked == 0 && lckFlip == 0) lckFlip = i; // flips at 10 ie 5s, as expected + d.CreateSnapshot(); + System.Threading.Thread.Sleep(500); + } + } + catch (Exception e) + { + caught = e; + } + }); + thread.Start(); + */ + scopeContext.ScopeExit(false); + // fixme - remove debugging code + /* + thread.Join(); + + Assert.IsNull(caught); // but then how can it be not null? + + Console.WriteLine(genFlip); + Console.WriteLine(lckFlip); + Assert.AreEqual(1, genFlip); + Assert.AreEqual(10, lckFlip); + */ + + // now things have changed + Assert.AreEqual(2, t.LiveGen); + Assert.IsFalse(t.NextGen); + Assert.AreEqual(0, t.WLocked); + + // no changes since not completed var s5 = d.CreateSnapshot(); Assert.AreEqual(2, s5.Gen); Assert.AreEqual("uno", s5.Get(1)); @@ -955,12 +1098,11 @@ namespace Umbraco.Tests.Cache Assert.AreEqual("four", all[3]); } - private IScopeProvider GetScopeProvider(bool withContext = false) + private IScopeProvider GetScopeProvider(ScopeContext scopeContext = null) { - var scopeProviderMock = new Mock(); - var scopeContext = withContext ? new ScopeContext() : null; - scopeProviderMock.Setup(x => x.Context).Returns(scopeContext); - var scopeProvider = scopeProviderMock.Object; + var scopeProvider = Mock.Of(); + Mock.Get(scopeProvider) + .Setup(x => x.Context).Returns(scopeContext); return scopeProvider; } } diff --git a/src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs b/src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs index dd5805daa1..2548046e23 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs @@ -95,7 +95,8 @@ namespace Umbraco.Web.PublishedCache.NuCache private class ContentStoreWriter : ScopeContextualBase { private readonly WriteLockInfo _lockinfo = new WriteLockInfo(); - private ContentStore _store; + private readonly ContentStore _store; + private int _released; public ContentStoreWriter(ContentStore store, bool scoped) { @@ -105,9 +106,9 @@ namespace Umbraco.Web.PublishedCache.NuCache public override void Release(bool completed) { - if (_store== null) return; + if (Interlocked.CompareExchange(ref _released, 1, 0) != 0) + return; _store.Release(_lockinfo, completed); - _store = null; } } diff --git a/src/Umbraco.Web/PublishedCache/NuCache/SnapDictionary.cs b/src/Umbraco.Web/PublishedCache/NuCache/SnapDictionary.cs index f117a395b5..1d462f1b76 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/SnapDictionary.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/SnapDictionary.cs @@ -90,7 +90,8 @@ namespace Umbraco.Web.PublishedCache.NuCache private class SnapDictionaryWriter : ScopeContextualBase { private readonly WriteLockInfo _lockinfo = new WriteLockInfo(); - private SnapDictionary _dictionary; + private readonly SnapDictionary _dictionary; + private int _released; public SnapDictionaryWriter(SnapDictionary dictionary, bool scoped) { @@ -100,14 +101,18 @@ namespace Umbraco.Web.PublishedCache.NuCache public override void Release(bool completed) { - if (_dictionary == null) return; + if (Interlocked.CompareExchange(ref _released, 1, 0) != 0) + return; _dictionary.Release(_lockinfo, completed); - _dictionary = null; } } // gets a scope contextual representing a locked writer to the dictionary - // GetScopedWriter? should the dict have a ref onto the scope provider? + // fixme GetScopedWriter? should the dict have a ref onto the scope provider? + // fixme this is not a "writer" but a "write lock" => rename GetWriteLock + // the dict is write-locked until the write-lock is released + // which happens when it is disposed (non-scoped) + // or when the scope context exits (scoped) public IDisposable GetWriter(IScopeProvider scopeProvider) { return ScopeContextualBase.Get(scopeProvider, _instanceId, scoped => new SnapDictionaryWriter(this, scoped)); @@ -130,13 +135,22 @@ namespace Umbraco.Web.PublishedCache.NuCache //RuntimeHelpers.PrepareConstrainedRegions(); try { } finally { + // increment the lock count, and register that this lock is counting _wlocked++; lockInfo.Count = true; + + // fixme - this comment: "ok to have holes in generation objects" is annoying + // 'cos if you have a hole, then doing _liveGen-1 in some places would be wrong + // besides, forceGen is used only when getting a scoped write lock, which is not reentrant? + // + // but... if _wlocked ==1 and we just incremented it, it means it was 0, so it wasnt locked, so wtf? + // this is the only place where _nextGen would turn true so ... this makes no sense at all! + if (_nextGen == false || (forceGen && _wlocked == 1)) // if true already... ok to have "holes" in generation objects { // because we are changing things, a new generation // is created, which will trigger a new snapshot - _nextGen = true; + _nextGen = true; // this is the ONLY place where _nextGen becomes true _liveGen += 1; } } @@ -154,6 +168,10 @@ namespace Umbraco.Web.PublishedCache.NuCache private void Release(WriteLockInfo lockInfo, bool commit = true) { + // if the lock wasn't taken in the first place, do nothing + if (!lockInfo.Taken) + return; + if (commit == false) { var rtaken = false; @@ -162,6 +180,7 @@ namespace Umbraco.Web.PublishedCache.NuCache Monitor.Enter(_rlocko, ref rtaken); try { } finally { + // forget about the temp. liveGen _nextGen = false; _liveGen -= 1; } @@ -184,8 +203,12 @@ namespace Umbraco.Web.PublishedCache.NuCache } } + // fixme - pretend we need to do something that takes time + //System.Threading.Thread.Sleep(TimeSpan.FromSeconds(5)); + + // decrement the lock count, if counting, then exit the lock if (lockInfo.Count) _wlocked--; - if (lockInfo.Taken) Monitor.Exit(_wlocko); + Monitor.Exit(_wlocko); } private void Release(ReadLockInfo lockInfo) @@ -338,12 +361,12 @@ namespace Umbraco.Web.PublishedCache.NuCache { Lock(lockInfo); - // if no next generation is required, and we already have one, - // use it and create a new snapshot + // if no next generation is required, and we already have a gen object, + // use it to create a new snapshot if (_nextGen == false && _genObj != null) return new Snapshot(this, _genObj.GetGenRef()); - // else we need to try to create a new gen ref + // else we need to try to create a new gen object // whether we are wlocked or not, noone can rlock while we do, // so _liveGen and _nextGen are safe if (_wlocked > 0) // volatile, cannot ++ but could -- @@ -351,26 +374,32 @@ namespace Umbraco.Web.PublishedCache.NuCache // write-locked, cannot use latest gen (at least 1) so use previous var snapGen = _nextGen ? _liveGen - 1 : _liveGen; - // create a new gen ref unless we already have it + // create a new gen object if we don't already have one + // (happens the first time a snapshot is created) if (_genObj == null) _genObjs.Enqueue(_genObj = new GenObj(snapGen)); + + // fixme - getting a panic exception here + // means _genObj != null, means _nextGen == true + + // if we have one already, ensure it's consistent else if (_genObj.Gen != snapGen) throw new Exception("panic"); } else { - // not write-locked, can use latest gen, create a new gen ref + // not write-locked, can use latest gen (_liveGen), create a corresponding new gen object _genObjs.Enqueue(_genObj = new GenObj(_liveGen)); _nextGen = false; // this is the ONLY thing that triggers a _liveGen++ } // so... - // the genRefRef has a weak ref to the genRef, and is queued - // the snapshot has a ref to the genRef, which has a ref to the genRefRef - // when the snapshot is disposed, it decreases genRefRef counter + // the genObj has a weak ref to the genRef, and is queued + // the snapshot has a ref to the genRef, which has a ref to the genObj + // when the snapshot is disposed, it decreases genObj counter // so after a while, one of these conditions is going to be true: - // - the genRefRef counter is zero because all snapshots have properly been disposed - // - the genRefRef weak ref is dead because all snapshots have been collected + // - genObj.Count is zero because all snapshots have properly been disposed + // - genObj.WeakGenRef is dead because all snapshots have been collected // in both cases, we will dequeue and collect var snapshot = new Snapshot(this, _genObj.GetGenRef()); @@ -486,7 +515,7 @@ namespace Umbraco.Web.PublishedCache.NuCache { task = _collectTask; } - return task ?? Task.FromResult(0); + return task ?? Task.CompletedTask; //if (task != null) // await task; } @@ -514,6 +543,7 @@ namespace Umbraco.Web.PublishedCache.NuCache public long LiveGen => _dict._liveGen; public long FloorGen => _dict._floorGen; public bool NextGen => _dict._nextGen; + public int WLocked => _dict._wlocked; public bool CollectAuto { @@ -563,15 +593,20 @@ namespace Umbraco.Web.PublishedCache.NuCache public class Snapshot : IDisposable { private readonly SnapDictionary _store; - private readonly GenerationReference _generationReference; - private long _gen; // copied for perfs + private readonly GenRef _genRef; + private readonly long _gen; // copied for perfs + private int _disposed; - internal Snapshot(SnapDictionary store, GenerationReference generationReference) + //private static int _count; + //private readonly int _thisCount; + + internal Snapshot(SnapDictionary store, GenRef genRef) { _store = store; - _generationReference = generationReference; - _gen = generationReference.GenObj.Gen; - _generationReference.GenObj.Reference(); + _genRef = genRef; + _gen = genRef.GenObj.Gen; + _genRef.GenObj.Reference(); + //_thisCount = _count++; } internal Snapshot(SnapDictionary store, long gen) @@ -580,17 +615,21 @@ namespace Umbraco.Web.PublishedCache.NuCache _gen = gen; } + private void EnsureNotDisposed() + { + if (_disposed > 0) + throw new ObjectDisposedException("snapshot" /*+ " (" + _thisCount + ")"*/); + } + public TValue Get(TKey key) { - if (_gen < 0) - throw new ObjectDisposedException("snapshot" /*+ " (" + _thisCount + ")"*/); + EnsureNotDisposed(); return _store.Get(key, _gen); } public IEnumerable GetAll() { - if (_gen < 0) - throw new ObjectDisposedException("snapshot" /*+ " (" + _thisCount + ")"*/); + EnsureNotDisposed(); return _store.GetAll(_gen); } @@ -598,8 +637,7 @@ namespace Umbraco.Web.PublishedCache.NuCache { get { - if (_gen < 0) - throw new ObjectDisposedException("snapshot" /*+ " (" + _thisCount + ")"*/); + EnsureNotDisposed(); return _store.IsEmpty(_gen); } } @@ -608,17 +646,16 @@ namespace Umbraco.Web.PublishedCache.NuCache { get { - if (_gen < 0) - throw new ObjectDisposedException("snapshot" /*+ " (" + _thisCount + ")"*/); + EnsureNotDisposed(); return _gen; } } public void Dispose() { - if (_gen < 0) return; - _gen = -1; - _generationReference?.GenObj.Release(); + if (Interlocked.CompareExchange(ref _disposed, 1, 0) != 0) + return; + _genRef?.GenObj.Release(); GC.SuppressFinalize(this); } } From 851c844c8b3268909b0939bb3ec8a5202ea554c5 Mon Sep 17 00:00:00 2001 From: Stephan Date: Thu, 14 Mar 2019 18:56:23 +0100 Subject: [PATCH 29/50] NuCache: fix panic exception --- .../Cache/SnapDictionaryTests.cs | 81 +++++++++++++++++++ .../PublishedCache/NuCache/ContentStore.cs | 4 +- .../PublishedCache/NuCache/SnapDictionary.cs | 15 +--- 3 files changed, 87 insertions(+), 13 deletions(-) diff --git a/src/Umbraco.Tests/Cache/SnapDictionaryTests.cs b/src/Umbraco.Tests/Cache/SnapDictionaryTests.cs index eb034eec26..e4ca35fbd3 100644 --- a/src/Umbraco.Tests/Cache/SnapDictionaryTests.cs +++ b/src/Umbraco.Tests/Cache/SnapDictionaryTests.cs @@ -1098,6 +1098,87 @@ namespace Umbraco.Tests.Cache Assert.AreEqual("four", all[3]); } + [Test] + public void DontPanic() + { + var d = new SnapDictionary(); + d.Test.CollectAuto = false; + + Assert.IsNull(d.Test.GenObj); // set with first snapshot or first lock, then never null + + // gen 1 + d.Set(1, "one"); + Assert.IsTrue(d.Test.NextGen); + Assert.AreEqual(1, d.Test.LiveGen); + Assert.IsNotNull(d.Test.GenObj); // set with lock + + var s1 = d.CreateSnapshot(); + Assert.IsFalse(d.Test.NextGen); + Assert.AreEqual(1, d.Test.LiveGen); + Assert.IsNotNull(d.Test.GenObj); + Assert.AreEqual(1, d.Test.GenObj.Gen); + + Assert.AreEqual(1, s1.Gen); + Assert.AreEqual("one", s1.Get(1)); + + d.Set(1, "uno"); + Assert.IsTrue(d.Test.NextGen); + Assert.AreEqual(2, d.Test.LiveGen); + Assert.IsNotNull(d.Test.GenObj); + Assert.AreEqual(1, d.Test.GenObj.Gen); + + var scopeContext = new ScopeContext(); + var scopeProvider = GetScopeProvider(scopeContext); + + // scopeProvider.Context == scopeContext -> writer is scoped + // writer is scope contextual and scoped + // when disposed, nothing happens + // when the context exists, the writer is released + using (d.GetWriter(scopeProvider)) + { + d.Set(1, "ein"); + Assert.IsTrue(d.Test.NextGen); + Assert.AreEqual(3, d.Test.LiveGen); + Assert.IsNotNull(d.Test.GenObj); + Assert.AreEqual(2, d.Test.GenObj.Gen); + } + + // writer has not released + Assert.AreEqual(1, d.Test.WLocked); + Assert.IsNotNull(d.Test.GenObj); + Assert.AreEqual(2, d.Test.GenObj.Gen); + + // nothing changed + Assert.IsTrue(d.Test.NextGen); + Assert.AreEqual(3, d.Test.LiveGen); + + // panic! + var s2 = d.CreateSnapshot(); + + Assert.AreEqual(1, d.Test.WLocked); + Assert.IsNotNull(d.Test.GenObj); + Assert.AreEqual(2, d.Test.GenObj.Gen); + Assert.AreEqual(3, d.Test.LiveGen); + Assert.IsTrue(d.Test.NextGen); + + // release writer + scopeContext.ScopeExit(true); + + Assert.AreEqual(0, d.Test.WLocked); + Assert.IsNotNull(d.Test.GenObj); + Assert.AreEqual(2, d.Test.GenObj.Gen); + Assert.AreEqual(3, d.Test.LiveGen); + Assert.IsTrue(d.Test.NextGen); + + var s3 = d.CreateSnapshot(); + + Assert.AreEqual(0, d.Test.WLocked); + Assert.IsNotNull(d.Test.GenObj); + Assert.AreEqual(3, d.Test.GenObj.Gen); + Assert.AreEqual(3, d.Test.LiveGen); + Assert.IsFalse(d.Test.NextGen); + } + private IScopeProvider GetScopeProvider(ScopeContext scopeContext = null) { var scopeProvider = Mock.Of(); diff --git a/src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs b/src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs index 2548046e23..2b9c9bee4d 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs @@ -133,11 +133,12 @@ namespace Umbraco.Web.PublishedCache.NuCache { _wlocked++; lockInfo.Count = true; - if (_nextGen == false || (forceGen && _wlocked == 1)) // if true already... ok to have "holes" in generation objects + if (_nextGen == false || (forceGen && _wlocked == 1)) { // because we are changing things, a new generation // is created, which will trigger a new snapshot _nextGen = true; + _genObjs.Enqueue(_genObj = new GenObj(_liveGen)); _liveGen += 1; } } @@ -214,7 +215,6 @@ namespace Umbraco.Web.PublishedCache.NuCache else dictionary.TryUpdate(key, link.Next, link); } - } #endregion diff --git a/src/Umbraco.Web/PublishedCache/NuCache/SnapDictionary.cs b/src/Umbraco.Web/PublishedCache/NuCache/SnapDictionary.cs index 1d462f1b76..73904d1452 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/SnapDictionary.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/SnapDictionary.cs @@ -139,18 +139,12 @@ namespace Umbraco.Web.PublishedCache.NuCache _wlocked++; lockInfo.Count = true; - // fixme - this comment: "ok to have holes in generation objects" is annoying - // 'cos if you have a hole, then doing _liveGen-1 in some places would be wrong - // besides, forceGen is used only when getting a scoped write lock, which is not reentrant? - // - // but... if _wlocked ==1 and we just incremented it, it means it was 0, so it wasnt locked, so wtf? - // this is the only place where _nextGen would turn true so ... this makes no sense at all! - - if (_nextGen == false || (forceGen && _wlocked == 1)) // if true already... ok to have "holes" in generation objects + if (_nextGen == false || (forceGen && _wlocked == 1)) { // because we are changing things, a new generation // is created, which will trigger a new snapshot _nextGen = true; // this is the ONLY place where _nextGen becomes true + _genObjs.Enqueue(_genObj = new GenObj(_liveGen)); _liveGen += 1; } } @@ -379,9 +373,6 @@ namespace Umbraco.Web.PublishedCache.NuCache if (_genObj == null) _genObjs.Enqueue(_genObj = new GenObj(snapGen)); - // fixme - getting a panic exception here - // means _genObj != null, means _nextGen == true - // if we have one already, ensure it's consistent else if (_genObj.Gen != snapGen) throw new Exception("panic"); @@ -551,6 +542,8 @@ namespace Umbraco.Web.PublishedCache.NuCache set => _dict._collectAuto = value; } + public GenObj GenObj => _dict._genObj; + public ConcurrentQueue GenObjs => _dict._genObjs; public Snapshot LiveSnapshot => new Snapshot(_dict, _dict._liveGen); From dfe6f2029ac5d65aa8bac1c91f27bc0da5ca5253 Mon Sep 17 00:00:00 2001 From: Stephan Date: Thu, 14 Mar 2019 19:48:44 +0100 Subject: [PATCH 30/50] NuCache: better fixing, cleanup --- .../Scoping/ScopeContextualBase.cs | 7 +- .../Cache/SnapDictionaryTests.cs | 67 +++++-------------- .../PublishedCache/NuCache/ContentStore.cs | 13 ++-- .../NuCache/PublishedSnapshotService.cs | 18 ++--- .../PublishedCache/NuCache/SnapDictionary.cs | 18 ++--- 5 files changed, 39 insertions(+), 84 deletions(-) diff --git a/src/Umbraco.Core/Scoping/ScopeContextualBase.cs b/src/Umbraco.Core/Scoping/ScopeContextualBase.cs index 1f2b6155e6..25f176d471 100644 --- a/src/Umbraco.Core/Scoping/ScopeContextualBase.cs +++ b/src/Umbraco.Core/Scoping/ScopeContextualBase.cs @@ -12,7 +12,7 @@ namespace Umbraco.Core.Scoping /// public abstract class ScopeContextualBase : IDisposable { - private bool _using, _scoped; + private bool _scoped; /// /// Gets a contextual object. @@ -38,9 +38,6 @@ namespace Umbraco.Core.Scoping () => ctor(true), (completed, item) => { item.Release(completed); }); - // the object can be 'used' only once at a time - if (w._using) throw new InvalidOperationException("panic: used."); - w._using = true; w._scoped = true; return w; @@ -52,8 +49,6 @@ namespace Umbraco.Core.Scoping /// public void Dispose() { - _using = false; - if (_scoped == false) Release(true); } diff --git a/src/Umbraco.Tests/Cache/SnapDictionaryTests.cs b/src/Umbraco.Tests/Cache/SnapDictionaryTests.cs index e4ca35fbd3..b435af9e77 100644 --- a/src/Umbraco.Tests/Cache/SnapDictionaryTests.cs +++ b/src/Umbraco.Tests/Cache/SnapDictionaryTests.cs @@ -632,7 +632,7 @@ namespace Umbraco.Tests.Cache Assert.AreEqual(1, d.Test.LiveGen); Assert.IsTrue(d.Test.NextGen); - using (d.GetWriter(GetScopeProvider())) + using (d.GetScopedWriteLock(GetScopeProvider())) { var s1 = d.CreateSnapshot(); @@ -685,7 +685,7 @@ namespace Umbraco.Tests.Cache Assert.IsFalse(d.Test.NextGen); Assert.AreEqual("uno", s2.Get(1)); - using (d.GetWriter(GetScopeProvider())) + using (d.GetScopedWriteLock(GetScopeProvider())) { // gen 3 Assert.AreEqual(2, d.Test.GetValues(1).Length); @@ -724,13 +724,13 @@ namespace Umbraco.Tests.Cache var scopeProvider = GetScopeProvider(); - using (var w1 = d.GetWriter(scopeProvider)) + using (var w1 = d.GetScopedWriteLock(scopeProvider)) { Assert.AreEqual(1, t.LiveGen); Assert.AreEqual(1, t.WLocked); Assert.IsTrue(t.NextGen); - using (var w2 = d.GetWriter(scopeProvider)) + using (var w2 = d.GetScopedWriteLock(scopeProvider)) { Assert.AreEqual(1, t.LiveGen); Assert.AreEqual(2, t.WLocked); @@ -770,9 +770,9 @@ namespace Umbraco.Tests.Cache var scopeContext = new ScopeContext(); var scopeProvider = GetScopeProvider(scopeContext); - using (var w1 = d.GetWriter(scopeProvider)) + using (var w1 = d.GetScopedWriteLock(scopeProvider)) { - using (var w2 = d.GetWriter(scopeProvider)) + using (var w2 = d.GetScopedWriteLock(scopeProvider)) { Assert.AreSame(w1, w2); @@ -794,13 +794,13 @@ namespace Umbraco.Tests.Cache var scopeProvider1 = GetScopeProvider(); var scopeProvider2 = GetScopeProvider(scopeContext); - using (var w1 = d.GetWriter(scopeProvider1)) + using (var w1 = d.GetScopedWriteLock(scopeProvider1)) { Assert.AreEqual(1, t.LiveGen); Assert.AreEqual(1, t.WLocked); Assert.IsTrue(t.NextGen); - using (var w2 = d.GetWriter(scopeProvider2)) + using (var w2 = d.GetScopedWriteLock(scopeProvider2)) { Assert.AreEqual(1, t.LiveGen); Assert.AreEqual(2, t.WLocked); @@ -850,7 +850,7 @@ namespace Umbraco.Tests.Cache var scopeProvider = GetScopeProvider(); - using (d.GetWriter(scopeProvider)) + using (d.GetScopedWriteLock(scopeProvider)) { // gen 3 Assert.AreEqual(2, d.Test.GetValues(1).Length); @@ -895,7 +895,7 @@ namespace Umbraco.Tests.Cache var scopeProvider = GetScopeProvider(); - using (d.GetWriter(scopeProvider)) + using (d.GetScopedWriteLock(scopeProvider)) { // creating a snapshot in a write-lock does NOT return the "current" content // it uses the previous snapshot, so new snapshot created only on release @@ -935,7 +935,7 @@ namespace Umbraco.Tests.Cache var scopeContext = new ScopeContext(); var scopeProvider = GetScopeProvider(scopeContext); - using (d.GetWriter(scopeProvider)) + using (d.GetScopedWriteLock(scopeProvider)) { // creating a snapshot in a write-lock does NOT return the "current" content // it uses the previous snapshot, so new snapshot created only on release @@ -985,7 +985,7 @@ namespace Umbraco.Tests.Cache var scopeContext = new ScopeContext(); var scopeProvider = GetScopeProvider(scopeContext); - using (d.GetWriter(scopeProvider)) + using (d.GetScopedWriteLock(scopeProvider)) { // creating a snapshot in a write-lock does NOT return the "current" content // it uses the previous snapshot, so new snapshot created only on release @@ -1015,45 +1015,8 @@ namespace Umbraco.Tests.Cache Assert.AreEqual(2, s4.Gen); Assert.AreEqual("uno", s4.Get(1)); - // fixme - remove debugging code - /* - Exception caught = null; - var genFlip = 0; - var lckFlip = 0; - var thread = new System.Threading.Thread(() => - { - try - { - for (var i = 0; i < 20; i++) - { - if (t.LiveGen == 2 && genFlip == 0) genFlip = i; // flips at 1 - if (t.WLocked == 0 && lckFlip == 0) lckFlip = i; // flips at 10 ie 5s, as expected - d.CreateSnapshot(); - System.Threading.Thread.Sleep(500); - } - } - catch (Exception e) - { - caught = e; - } - }); - thread.Start(); - */ - scopeContext.ScopeExit(false); - // fixme - remove debugging code - /* - thread.Join(); - - Assert.IsNull(caught); // but then how can it be not null? - - Console.WriteLine(genFlip); - Console.WriteLine(lckFlip); - Assert.AreEqual(1, genFlip); - Assert.AreEqual(10, lckFlip); - */ - // now things have changed Assert.AreEqual(2, t.LiveGen); Assert.IsFalse(t.NextGen); @@ -1104,13 +1067,13 @@ namespace Umbraco.Tests.Cache var d = new SnapDictionary(); d.Test.CollectAuto = false; - Assert.IsNull(d.Test.GenObj); // set with first snapshot or first lock, then never null + Assert.IsNull(d.Test.GenObj); // gen 1 d.Set(1, "one"); Assert.IsTrue(d.Test.NextGen); Assert.AreEqual(1, d.Test.LiveGen); - Assert.IsNotNull(d.Test.GenObj); // set with lock + Assert.IsNull(d.Test.GenObj); var s1 = d.CreateSnapshot(); Assert.IsFalse(d.Test.NextGen); @@ -1134,7 +1097,7 @@ namespace Umbraco.Tests.Cache // writer is scope contextual and scoped // when disposed, nothing happens // when the context exists, the writer is released - using (d.GetWriter(scopeProvider)) + using (d.GetScopedWriteLock(scopeProvider)) { d.Set(1, "ein"); Assert.IsTrue(d.Test.NextGen); diff --git a/src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs b/src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs index 2b9c9bee4d..7ab4a64f31 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/ContentStore.cs @@ -92,13 +92,13 @@ namespace Umbraco.Web.PublishedCache.NuCache } // a scope contextual that represents a locked writer to the dictionary - private class ContentStoreWriter : ScopeContextualBase + private class ScopedWriteLock : ScopeContextualBase { private readonly WriteLockInfo _lockinfo = new WriteLockInfo(); private readonly ContentStore _store; private int _released; - public ContentStoreWriter(ContentStore store, bool scoped) + public ScopedWriteLock(ContentStore store, bool scoped) { _store = store; store.Lock(_lockinfo, scoped); @@ -114,9 +114,9 @@ namespace Umbraco.Web.PublishedCache.NuCache // gets a scope contextual representing a locked writer to the dictionary // TODO: GetScopedWriter? should the dict have a ref onto the scope provider? - public IDisposable GetWriter(IScopeProvider scopeProvider) + public IDisposable GetScopedWriteLock(IScopeProvider scopeProvider) { - return ScopeContextualBase.Get(scopeProvider, _instanceId, scoped => new ContentStoreWriter(this, scoped)); + return ScopeContextualBase.Get(scopeProvider, _instanceId, scoped => new ScopedWriteLock(this, scoped)); } private void Lock(WriteLockInfo lockInfo, bool forceGen = false) @@ -137,9 +137,10 @@ namespace Umbraco.Web.PublishedCache.NuCache { // because we are changing things, a new generation // is created, which will trigger a new snapshot - _nextGen = true; - _genObjs.Enqueue(_genObj = new GenObj(_liveGen)); + if (_nextGen) + _genObjs.Enqueue(_genObj = new GenObj(_liveGen)); _liveGen += 1; + _nextGen = true; } } } diff --git a/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs b/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs index 541ff2ea23..9c5587fbd5 100755 --- a/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/PublishedSnapshotService.cs @@ -333,7 +333,7 @@ namespace Umbraco.Web.PublishedCache.NuCache // first get a writer, then a scope // if there already is a scope, the writer will attach to it // otherwise, it will only exist here - cheap - using (_contentStore.GetWriter(_scopeProvider)) + using (_contentStore.GetScopedWriteLock(_scopeProvider)) using (var scope = _scopeProvider.CreateScope()) { scope.ReadLock(Constants.Locks.ContentTree); @@ -401,7 +401,7 @@ namespace Umbraco.Web.PublishedCache.NuCache private void LockAndLoadMedia(Action action) { // see note in LockAndLoadContent - using (_mediaStore.GetWriter(_scopeProvider)) + using (_mediaStore.GetScopedWriteLock(_scopeProvider)) using (var scope = _scopeProvider.CreateScope()) { scope.ReadLock(Constants.Locks.MediaTree); @@ -529,7 +529,7 @@ namespace Umbraco.Web.PublishedCache.NuCache private void LockAndLoadDomains() { // see note in LockAndLoadContent - using (_domainStore.GetWriter(_scopeProvider)) + using (_domainStore.GetScopedWriteLock(_scopeProvider)) using (var scope = _scopeProvider.CreateScope()) { scope.ReadLock(Constants.Locks.Domains); @@ -586,7 +586,7 @@ namespace Umbraco.Web.PublishedCache.NuCache return; } - using (_contentStore.GetWriter(_scopeProvider)) + using (_contentStore.GetScopedWriteLock(_scopeProvider)) { NotifyLocked(payloads, out bool draftChanged2, out bool publishedChanged2); draftChanged = draftChanged2; @@ -682,7 +682,7 @@ namespace Umbraco.Web.PublishedCache.NuCache return; } - using (_mediaStore.GetWriter(_scopeProvider)) + using (_mediaStore.GetScopedWriteLock(_scopeProvider)) { NotifyLocked(payloads, out bool anythingChanged2); anythingChanged = anythingChanged2; @@ -803,7 +803,7 @@ namespace Umbraco.Web.PublishedCache.NuCache if (removedIds.Count == 0 && refreshedIds.Count == 0 && otherIds.Count == 0 && newIds.Count == 0) return; - using (store.GetWriter(_scopeProvider)) + using (store.GetScopedWriteLock(_scopeProvider)) { // ReSharper disable AccessToModifiedClosure action(removedIds, refreshedIds, otherIds, newIds); @@ -824,8 +824,8 @@ namespace Umbraco.Web.PublishedCache.NuCache payload.Removed ? "Removed" : "Refreshed", payload.Id); - using (_contentStore.GetWriter(_scopeProvider)) - using (_mediaStore.GetWriter(_scopeProvider)) + using (_contentStore.GetScopedWriteLock(_scopeProvider)) + using (_mediaStore.GetScopedWriteLock(_scopeProvider)) { // TODO: need to add a datatype lock // this is triggering datatypes reload in the factory, and right after we create some @@ -858,7 +858,7 @@ namespace Umbraco.Web.PublishedCache.NuCache return; // see note in LockAndLoadContent - using (_domainStore.GetWriter(_scopeProvider)) + using (_domainStore.GetScopedWriteLock(_scopeProvider)) { foreach (var payload in payloads) { diff --git a/src/Umbraco.Web/PublishedCache/NuCache/SnapDictionary.cs b/src/Umbraco.Web/PublishedCache/NuCache/SnapDictionary.cs index 73904d1452..c5b1df1206 100644 --- a/src/Umbraco.Web/PublishedCache/NuCache/SnapDictionary.cs +++ b/src/Umbraco.Web/PublishedCache/NuCache/SnapDictionary.cs @@ -87,13 +87,13 @@ namespace Umbraco.Web.PublishedCache.NuCache } // a scope contextual that represents a locked writer to the dictionary - private class SnapDictionaryWriter : ScopeContextualBase + private class ScopedWriteLock : ScopeContextualBase { private readonly WriteLockInfo _lockinfo = new WriteLockInfo(); private readonly SnapDictionary _dictionary; private int _released; - public SnapDictionaryWriter(SnapDictionary dictionary, bool scoped) + public ScopedWriteLock(SnapDictionary dictionary, bool scoped) { _dictionary = dictionary; dictionary.Lock(_lockinfo, scoped); @@ -108,14 +108,12 @@ namespace Umbraco.Web.PublishedCache.NuCache } // gets a scope contextual representing a locked writer to the dictionary - // fixme GetScopedWriter? should the dict have a ref onto the scope provider? - // fixme this is not a "writer" but a "write lock" => rename GetWriteLock // the dict is write-locked until the write-lock is released // which happens when it is disposed (non-scoped) // or when the scope context exits (scoped) - public IDisposable GetWriter(IScopeProvider scopeProvider) + public IDisposable GetScopedWriteLock(IScopeProvider scopeProvider) { - return ScopeContextualBase.Get(scopeProvider, _instanceId, scoped => new SnapDictionaryWriter(this, scoped)); + return ScopeContextualBase.Get(scopeProvider, _instanceId, scoped => new ScopedWriteLock(this, scoped)); } private void Lock(WriteLockInfo lockInfo, bool forceGen = false) @@ -143,9 +141,10 @@ namespace Umbraco.Web.PublishedCache.NuCache { // because we are changing things, a new generation // is created, which will trigger a new snapshot - _nextGen = true; // this is the ONLY place where _nextGen becomes true - _genObjs.Enqueue(_genObj = new GenObj(_liveGen)); + if (_nextGen) + _genObjs.Enqueue(_genObj = new GenObj(_liveGen)); _liveGen += 1; + _nextGen = true; // this is the ONLY place where _nextGen becomes true } } } @@ -197,9 +196,6 @@ namespace Umbraco.Web.PublishedCache.NuCache } } - // fixme - pretend we need to do something that takes time - //System.Threading.Thread.Sleep(TimeSpan.FromSeconds(5)); - // decrement the lock count, if counting, then exit the lock if (lockInfo.Count) _wlocked--; Monitor.Exit(_wlocko); From b36288b987cd37122da42f4b261b1ab5a90bcfd9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Tue, 26 Mar 2019 14:11:01 +0100 Subject: [PATCH 31/50] infinity editing dim-layer for umb-modelcolumn --- src/Umbraco.Web.UI.Client/src/less/modals.less | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/Umbraco.Web.UI.Client/src/less/modals.less b/src/Umbraco.Web.UI.Client/src/less/modals.less index 771be1bc2a..8e88774613 100644 --- a/src/Umbraco.Web.UI.Client/src/less/modals.less +++ b/src/Umbraco.Web.UI.Client/src/less/modals.less @@ -55,6 +55,16 @@ position: absolute;; } +.--notInFront .umb-modalcolumn::after { + content: ''; + position: absolute; + top: 0; + bottom: 0; + right: 0; + left: 0; + background: rgba(0,0,0,.4); +} + /* re-align loader */ .umb-modal .umb-loader-wrapper, .umb-modalcolumn .umb-loader-wrapper, .umb-dialog .umb-loader-wrapper{ position:relative; From 458e51096151a1578e02241d9949ae547ebfa7ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Tue, 26 Mar 2019 14:57:13 +0100 Subject: [PATCH 32/50] remove double ;; --- src/Umbraco.Web.UI.Client/src/less/modals.less | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/less/modals.less b/src/Umbraco.Web.UI.Client/src/less/modals.less index 771be1bc2a..f922d7d95a 100644 --- a/src/Umbraco.Web.UI.Client/src/less/modals.less +++ b/src/Umbraco.Web.UI.Client/src/less/modals.less @@ -52,7 +52,7 @@ bottom: 0px; left: 0px; right: 0px; - position: absolute;; + position: absolute; } /* re-align loader */ From 4a14d45a64199673125e846d51f60b3e76a1216b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Tue, 26 Mar 2019 14:57:30 +0100 Subject: [PATCH 33/50] remove inline styling --- .../src/views/content/overlays/schedule.html | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/content/overlays/schedule.html b/src/Umbraco.Web.UI.Client/src/views/content/overlays/schedule.html index 2bf290b752..42f6ae7b98 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/overlays/schedule.html +++ b/src/Umbraco.Web.UI.Client/src/views/content/overlays/schedule.html @@ -27,7 +27,7 @@ {{vm.variants[0].releaseDateFormatted}} - + Set date
@@ -59,7 +59,7 @@ {{vm.variants[0].expireDateFormatted}} - + Set date
@@ -123,7 +123,7 @@ {{variant.releaseDateFormatted}} - + Set date
@@ -149,7 +149,7 @@ {{variant.expireDateFormatted}} - + Set date
From d6be02aaecd83823c8b0578a6314a807a86859af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Tue, 26 Mar 2019 14:57:45 +0100 Subject: [PATCH 34/50] style set date input --- .../src/less/properties.less | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/src/Umbraco.Web.UI.Client/src/less/properties.less b/src/Umbraco.Web.UI.Client/src/less/properties.less index 916f1b5a3a..d3e69a2d79 100644 --- a/src/Umbraco.Web.UI.Client/src/less/properties.less +++ b/src/Umbraco.Web.UI.Client/src/less/properties.less @@ -16,6 +16,31 @@ border-left: 1px solid @gray-10; } +.date-wrapper__date .flatpickr-input > a { + + display: flex; + align-items: center; + justify-content: center; + font-weight: 700; + padding: 5px 15px; + box-sizing: border-box; + min-width: 200px; + + color: @ui-action-discreet-type; + border: 1px dashed @ui-action-discreet-border; + border-radius: 3px; + + &:hover { + text-decoration: none; + color: @ui-action-discreet-type-hover; + border-color: @ui-action-discreet-border-hover; + + localize { + text-decoration: none; + } + } +} + //------------------- HISTORY ------------------ .history { From 719a4a8b6a50b829e7b8ce1166a5afaafdde436b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Wed, 27 Mar 2019 08:29:04 +0100 Subject: [PATCH 35/50] adjusted size --- src/Umbraco.Web.UI.Client/src/less/properties.less | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web.UI.Client/src/less/properties.less b/src/Umbraco.Web.UI.Client/src/less/properties.less index d3e69a2d79..d206b2a413 100644 --- a/src/Umbraco.Web.UI.Client/src/less/properties.less +++ b/src/Umbraco.Web.UI.Client/src/less/properties.less @@ -22,7 +22,7 @@ align-items: center; justify-content: center; font-weight: 700; - padding: 5px 15px; + padding: 4px 15px; box-sizing: border-box; min-width: 200px; From 8da9ecc9e4d0f967d9aea54674801b53ed13f85c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Niels=20Lyngs=C3=B8?= Date: Wed, 27 Mar 2019 12:49:03 +0100 Subject: [PATCH 36/50] date-wrapper styles for variants schedule publishing --- .../src/less/properties.less | 48 ++++++++++++++++++- .../src/views/content/overlays/schedule.html | 28 ++--------- 2 files changed, 51 insertions(+), 25 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/less/properties.less b/src/Umbraco.Web.UI.Client/src/less/properties.less index d206b2a413..e14bb5c0d6 100644 --- a/src/Umbraco.Web.UI.Client/src/less/properties.less +++ b/src/Umbraco.Web.UI.Client/src/less/properties.less @@ -30,7 +30,51 @@ border: 1px dashed @ui-action-discreet-border; border-radius: 3px; - &:hover { + &:hover, &:focus { + text-decoration: none; + color: @ui-action-discreet-type-hover; + border-color: @ui-action-discreet-border-hover; + + localize { + text-decoration: none; + } + } +} + +//----- VARIANTS SCHEDULED PUBLISH ------ + +.date-wrapper-mini { + display: flex; + flex-direction: row; +} + +.date-wrapper-mini__date { + display: flex; + + margin-left: 5px; + margin-top: 5px; + margin-bottom: 10px; + + &:first-of-type { + margin-left: 0; + } +} + +.date-wrapper-mini__date .flatpickr-input > a { + + display: flex; + align-items: center; + justify-content: center; + font-weight: 700; + padding: 1px 15px; + box-sizing: border-box; + min-width: 180px; + + color: @ui-action-discreet-type; + border: 1px dashed @ui-action-discreet-border; + border-radius: 3px; + + &:hover, &:focus { text-decoration: none; color: @ui-action-discreet-type-hover; border-color: @ui-action-discreet-border-hover; @@ -96,4 +140,4 @@ .history-line { display: none; } -} \ No newline at end of file +} diff --git a/src/Umbraco.Web.UI.Client/src/views/content/overlays/schedule.html b/src/Umbraco.Web.UI.Client/src/views/content/overlays/schedule.html index 42f6ae7b98..8cdc4a0abc 100644 --- a/src/Umbraco.Web.UI.Client/src/views/content/overlays/schedule.html +++ b/src/Umbraco.Web.UI.Client/src/views/content/overlays/schedule.html @@ -84,8 +84,8 @@
-
- +
+
-
+
+
-
Publish:  {{variant.releaseDateFormatted}}
@@ -134,7 +134,7 @@
-
+
Unpublish:  {{variant.expireDateFormatted}}
@@ -182,24 +182,6 @@
-
-
-

-
- -
-
-
- {{ variant.language.name }} -
-
- - - -
-
-
-
-
From d49d4bcdebe623af2ebff643ccc0a63d5b0258c3 Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Thu, 28 Mar 2019 17:10:41 +0100 Subject: [PATCH 37/50] Align the logviewer search icons to the right of the search field --- src/Umbraco.Web.UI.Client/src/views/logviewer/search.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/logviewer/search.html b/src/Umbraco.Web.UI.Client/src/views/logviewer/search.html index fad7698969..e22e8909d1 100644 --- a/src/Umbraco.Web.UI.Client/src/views/logviewer/search.html +++ b/src/Umbraco.Web.UI.Client/src/views/logviewer/search.html @@ -54,8 +54,8 @@ -   -   +   +   Example Searches From 6a69906fb6139b187b754371b5fd213c30d4ed20 Mon Sep 17 00:00:00 2001 From: Kenn Jacobsen Date: Thu, 28 Mar 2019 17:11:00 +0100 Subject: [PATCH 38/50] Make the "save search" dialog a little prettier --- .../common/overlays/logviewersearch/logviewersearch.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/views/common/overlays/logviewersearch/logviewersearch.html b/src/Umbraco.Web.UI.Client/src/views/common/overlays/logviewersearch/logviewersearch.html index 464a24517a..8ee9d1aa64 100644 --- a/src/Umbraco.Web.UI.Client/src/views/common/overlays/logviewersearch/logviewersearch.html +++ b/src/Umbraco.Web.UI.Client/src/views/common/overlays/logviewersearch/logviewersearch.html @@ -6,6 +6,6 @@

Name:
- +

-
\ No newline at end of file +
From cea8753ff28a3b6c695ffd10e183c9f4e6fb5e45 Mon Sep 17 00:00:00 2001 From: Stephan Date: Thu, 28 Mar 2019 12:10:59 +0100 Subject: [PATCH 39/50] Replace dynamic ViewBag with ViewData --- .../Umbraco/Install/Views/Index.cshtml | 6 +- .../Umbraco/Views/AuthorizeUpgrade.cshtml | 4 +- .../Umbraco/Views/Default.cshtml | 4 +- .../Editors/BackOfficeController.cs | 37 ++++------ .../Install/Controllers/InstallController.cs | 4 +- src/Umbraco.Web/Umbraco.Web.csproj | 1 + src/Umbraco.Web/ViewDataExtensions.cs | 71 +++++++++++++++++++ 7 files changed, 95 insertions(+), 32 deletions(-) create mode 100644 src/Umbraco.Web/ViewDataExtensions.cs diff --git a/src/Umbraco.Web.UI/Umbraco/Install/Views/Index.cshtml b/src/Umbraco.Web.UI/Umbraco/Install/Views/Index.cshtml index 1d397bfd01..e74dbea217 100644 --- a/src/Umbraco.Web.UI/Umbraco/Install/Views/Index.cshtml +++ b/src/Umbraco.Web.UI/Umbraco/Install/Views/Index.cshtml @@ -7,7 +7,7 @@ - + @@ -66,8 +66,8 @@ var Umbraco = {}; Umbraco.Sys = {}; Umbraco.Sys.ServerVariables = { - "installApiBaseUrl": "@ViewBag.InstallApiBaseUrl", - "umbracoBaseUrl": "@ViewBag.UmbracoBaseFolder" + "installApiBaseUrl": "@ViewData.GetInstallApiBaseUrl()", + "umbracoBaseUrl": "@ViewData.GetUmbracoBaseFolder()" }; diff --git a/src/Umbraco.Web.UI/Umbraco/Views/AuthorizeUpgrade.cshtml b/src/Umbraco.Web.UI/Umbraco/Views/AuthorizeUpgrade.cshtml index fba9a83789..b7c1e6540c 100644 --- a/src/Umbraco.Web.UI/Umbraco/Views/AuthorizeUpgrade.cshtml +++ b/src/Umbraco.Web.UI/Umbraco/Views/AuthorizeUpgrade.cshtml @@ -51,7 +51,7 @@ @{ var externalLoginUrl = Url.Action("ExternalLogin", "BackOffice", new { - area = ViewBag.UmbracoPath, + area = ViewData.GetUmbracoPath(), //Custom redirect URL since we don't want to just redirect to the back office since this is for authing upgrades redirectUrl = Url.Action("AuthorizeUpgrade", "BackOffice") }); @@ -61,7 +61,7 @@