From 9df6e6555267599db6902a1f354f0011c110f751 Mon Sep 17 00:00:00 2001 From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> Date: Thu, 18 Jan 2024 13:19:36 +0100 Subject: [PATCH 01/15] V13: Rich text editor does not show its toolbar on grid layout rte's (#15595) * synchronize normal rte with grid-rte * restore pinToolbar and unpinToolbar from v10 and update to tinymce v6 and apply to grid-rte * linting * Reverting `pinToolbar` from v8 * remove unused variable --------- Co-authored-by: leekelleher --- .../components/grid/grid.rte.directive.js | 141 ++++----- .../src/common/services/tinymce.service.js | 5 + .../propertyeditors/grid/editors/rte.html | 4 +- .../propertyeditors/rte/rte.component.js | 273 +++++++++--------- 4 files changed, 214 insertions(+), 209 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 812fec6e9c..7821c00e69 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 @@ -12,22 +12,18 @@ angular.module("umbraco.directives") replace: true, link: function (scope, element, attrs) { - // TODO: A lot of the code below should be shared between the grid rte and the normal rte - scope.isLoading = true; - var promises = []; - //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(); - var editorConfig = scope.configuration ? scope.configuration : null; + let editorConfig = scope.configuration ? scope.configuration : null; if (!editorConfig || Utilities.isString(editorConfig)) { editorConfig = tinyMceService.defaultPrevalues(); //for the grid by default, we don't want to include the macro or the block-picker toolbar - editorConfig.toolbar = _.without(editorConfig, "umbmacro", "umbblockpicker"); + editorConfig.toolbar = _.without(editorConfig.toolbar, "umbmacro", "umbblockpicker"); } //ensure the grid's global config is being passed up to the RTE, these 2 properties need to be in this format @@ -39,46 +35,50 @@ angular.module("umbraco.directives") scope.dataTypeKey = scope.datatypeKey; //Yes - this casing is rediculous, but it's because the var starts with `data` so it can't be `data-type-id` :/ //stores a reference to the editor - var tinyMceEditor = null; + let tinyMceEditor = null; + + const assetPromises = []; //queue file loading tinyMceAssets.forEach(function (tinyJsAsset) { - promises.push(assetsService.loadJs(tinyJsAsset, scope)); + assetPromises.push(assetsService.loadJs(tinyJsAsset, scope)); }); - promises.push(tinyMceService.getTinyMceEditorConfig({ - htmlId: scope.textAreaHtmlId, - stylesheets: editorConfig.stylesheets, - toolbar: editorConfig.toolbar, - mode: editorConfig.mode - })); - $q.all(promises).then(function (result) { + //wait for assets to load before proceeding + $q.all(assetPromises) + .then(function () { + return tinyMceService.getTinyMceEditorConfig({ + htmlId: scope.textAreaHtmlId, + stylesheets: editorConfig.stylesheets, + toolbar: editorConfig.toolbar, + mode: editorConfig.mode + }) + }) - var standardConfig = result[promises.length - 1]; + // Handle additional assets loading depending on the configuration before initializing the editor + .then(function (tinyMceConfig) { + // Load the plugins.min.js file from the TinyMCE Cloud if a Cloud Api Key is specified + if (tinyMceConfig.cloudApiKey) { + return assetsService.loadJs(`https://cdn.tiny.cloud/1/${tinyMceConfig.cloudApiKey}/tinymce/${tinymce.majorVersion}.${tinymce.minorVersion}/plugins.min.js`) + .then(() => tinyMceConfig); + } + + return tinyMceConfig; + }) + + //wait for config to be ready after assets have loaded + .then(function (standardConfig) { //create a baseline Config to extend upon - var baseLineConfigObj = { - maxImageSize: editorConfig.maxImageSize, - toolbar_sticky: true + let baseLineConfigObj = { + maxImageSize: editorConfig.maxImageSize }; - Utilities.extend(baseLineConfigObj, standardConfig); - baseLineConfigObj.setup = function (editor) { //set the reference tinyMceEditor = editor; - //initialize the standard editor functionality for Umbraco - tinyMceService.initializeEditor({ - editor: editor, - toolbar: editorConfig.toolbar, - model: scope, - // Form is found in the scope of the grid controller above us, not in our isolated scope - // https://github.com/umbraco/Umbraco-CMS/issues/7461 - currentForm: angularHelper.getCurrentForm(scope.$parent) - }); - //custom initialization for this editor within the grid editor.on('init', function (e) { @@ -96,49 +96,52 @@ angular.module("umbraco.directives") }, 400); }); + + //initialize the standard editor functionality for Umbraco + tinyMceService.initializeEditor({ + editor: editor, + toolbar: editorConfig.toolbar, + model: scope, + // Form is found in the scope of the grid controller above us, not in our isolated scope + // https://github.com/umbraco/Umbraco-CMS/issues/7461 + currentForm: angularHelper.getCurrentForm(scope.$parent) + }); }; - /** Loads in the editor */ - function loadTinyMce() { - - //we need to add a timeout here, to force a redraw so TinyMCE can find - //the elements needed - $timeout(function () { - tinymce.init(baseLineConfigObj); - }, 150, false); - } - - loadTinyMce(); - - // TODO: This should probably be in place for all RTE, not just for the grid, which means - // this code can live in tinyMceService.initializeEditor - var tabShownListener = eventsService.on("app.tabChange", function (e, args) { - - var tabId = args.id; - var myTabId = element.closest(".umb-tab-pane").attr("rel"); - - if (String(tabId) === myTabId) { - //the tab has been shown, trigger the mceAutoResize (as it could have timed out before the tab was shown) - if (tinyMceEditor !== undefined && tinyMceEditor != null) { - tinyMceEditor.execCommand('mceAutoResize', false, null, 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 () { - eventsService.unsubscribe(tabShownListener); - //ensure we unbind this in case the blur doesn't fire above - if (tinyMceEditor !== undefined && tinyMceEditor != null) { - tinyMceEditor.destroy() - } - }); + Utilities.extend(baseLineConfigObj, standardConfig); + //we need to add a timeout here, to force a redraw so TinyMCE can find + //the elements needed + $timeout(function () { + tinymce.init(baseLineConfigObj); + }, 150); }); + const tabShownListener = eventsService.on("app.tabChange", function (e, args) { + + const tabId = String(args.id); + const myTabId = element.closest(".umb-tab-pane").attr("rel"); + + if (tabId === myTabId) { + //the tab has been shown, trigger the mceAutoResize (as it could have timed out before the tab was shown) + if (tinyMceEditor !== undefined && tinyMceEditor != null) { + tinyMceEditor.execCommand('mceAutoResize', false, null, 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 () { + eventsService.unsubscribe(tabShownListener); + + //ensure we unbind this in case the blur doesn't fire above + if (tinyMceEditor) { + tinyMceEditor.destroy(); + tinyMceEditor = null; + } + }); } }; }); diff --git a/src/Umbraco.Web.UI.Client/src/common/services/tinymce.service.js b/src/Umbraco.Web.UI.Client/src/common/services/tinymce.service.js index cfea1d5594..ebf759c20c 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/tinymce.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/tinymce.service.js @@ -797,6 +797,11 @@ function tinyMceService($rootScope, $q, imageHelper, $locale, $http, $timeout, s }); }); + // Do not add any further controls if the block editor is not available + if (!blockEditorApi) { + return; + } + editor.ui.registry.addToggleButton('umbblockpicker', { icon: 'visualblocks', tooltip: 'Insert Block', diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/rte.html b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/rte.html index fb6f3b2a4b..2fbd0b2fe3 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/rte.html +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/grid/editors/rte.html @@ -4,8 +4,8 @@ configuration="model.config.rte" value="control.value" unique-id="control.$uniqueId" - datatype-key="{{model.dataTypeKey}}" - ignore-user-start-nodes="{{model.config.ignoreUserStartNodes}}"> + datatype-key="{{model.dataTypeKey}}" + ignore-user-start-nodes="{{model.config.ignoreUserStartNodes}}"> diff --git a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.component.js b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.component.js index 4e9cf7d014..7f06215148 100644 --- a/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.component.js +++ b/src/Umbraco.Web.UI.Client/src/views/propertyeditors/rte/rte.component.js @@ -195,162 +195,148 @@ assetPromises.push(assetsService.loadJs(tinyJsAsset, $scope)); }); - const tinyMceConfigDeferred = $q.defer(); - //wait for assets to load before proceeding - $q.all(assetPromises).then(function () { - - tinyMceService.getTinyMceEditorConfig({ - htmlId: vm.textAreaHtmlId, - stylesheets: editorConfig.stylesheets, - toolbar: editorConfig.toolbar, - mode: editorConfig.mode + $q.all(assetPromises) + .then(function () { + return tinyMceService.getTinyMceEditorConfig({ + htmlId: vm.textAreaHtmlId, + stylesheets: editorConfig.stylesheets, + toolbar: editorConfig.toolbar, + mode: editorConfig.mode + }) }) - .then(function (tinyMceConfig) { - // Load the plugins.min.js file from the TinyMCE Cloud if a Cloud Api Key is specified - if (tinyMceConfig.cloudApiKey) { - return assetsService.loadJs(`https://cdn.tiny.cloud/1/${tinyMceConfig.cloudApiKey}/tinymce/${tinymce.majorVersion}.${tinymce.minorVersion}/plugins.min.js`) - .then(() => tinyMceConfig); + + // Handle additional assets loading depending on the configuration before initializing the editor + .then(function (tinyMceConfig) { + // Load the plugins.min.js file from the TinyMCE Cloud if a Cloud Api Key is specified + if (tinyMceConfig.cloudApiKey) { + return assetsService.loadJs(`https://cdn.tiny.cloud/1/${tinyMceConfig.cloudApiKey}/tinymce/${tinymce.majorVersion}.${tinymce.minorVersion}/plugins.min.js`) + .then(() => tinyMceConfig); + } + + return tinyMceConfig; + }) + + //wait for config to be ready after assets have loaded + .then(function (standardConfig) { + + if (height !== null) { + standardConfig.plugins.splice(standardConfig.plugins.indexOf("autoresize"), 1); } - return tinyMceConfig; - }) - .then(function (tinyMceConfig) { - tinyMceConfigDeferred.resolve(tinyMceConfig); - }); - }); + //create a baseline Config to extend upon + let baseLineConfigObj = { + maxImageSize: editorConfig.maxImageSize, + width: width, + height: height + }; - //wait for config to be ready after assets have loaded - tinyMceConfigDeferred.promise.then(function (standardConfig) { + baseLineConfigObj.setup = function (editor) { - if (height !== null) { - standardConfig.plugins.splice(standardConfig.plugins.indexOf("autoresize"), 1); - } + //set the reference + vm.tinyMceEditor = editor; - //create a baseline Config to extend upon - var baseLineConfigObj = { - maxImageSize: editorConfig.maxImageSize, - width: width, - height: height - }; - - baseLineConfigObj.setup = function (editor) { - - //set the reference - vm.tinyMceEditor = editor; - - vm.tinyMceEditor.on('init', function (e) { - $timeout(function () { - vm.rteLoading = false; - vm.updateLoading(); - }); - }); - vm.tinyMceEditor.on("focus", function () { - $element[0].dispatchEvent(new CustomEvent('umb-rte-focus', {composed: true, bubbles: true})); - }); - vm.tinyMceEditor.on("blur", function () { - $element[0].dispatchEvent(new CustomEvent('umb-rte-blur', {composed: true, bubbles: true})); - }); - - //initialize the standard editor functionality for Umbraco - tinyMceService.initializeEditor({ - //scope: $scope, - editor: editor, - toolbar: editorConfig.toolbar, - model: vm.model, - getValue: function () { - return vm.model.value.markup; - }, - setValue: function (newVal) { - vm.model.value.markup = newVal; - $scope.$evalAsync(); - }, - culture: vm.umbProperty?.culture ?? null, - segment: vm.umbProperty?.segment ?? null, - blockEditorApi: vm.noBlocksMode ? undefined : vm.blockEditorApi, - parentForm: vm.propertyForm, - valFormManager: vm.valFormManager, - currentFormInput: $scope.rteForm.modelValue - }); - - }; - - Utilities.extend(baseLineConfigObj, standardConfig); - - // Readonly mode - baseLineConfigObj.toolbar = vm.readonly ? false : baseLineConfigObj.toolbar; - baseLineConfigObj.readonly = vm.readonly ? 1 : baseLineConfigObj.readonly; - - // We need to wait for DOM to have rendered before we can find the element by ID. - $timeout(function () { - tinymce.init(baseLineConfigObj); - }, 50); - - //listen for formSubmitting event (the result is callback used to remove the event subscription) - unsubscribe.push($scope.$on("formSubmitting", function () { - if (vm.tinyMceEditor != null && !vm.rteLoading) { - - // Remove unused Blocks of Blocks Layout. Leaving only the Blocks that are present in Markup. - var blockElements = vm.tinyMceEditor.dom.select(`umb-rte-block, umb-rte-block-inline`); - const usedContentUdis = blockElements.map(blockElement => blockElement.getAttribute('data-content-udi')); - - const unusedBlocks = vm.layout.filter(x => usedContentUdis.indexOf(x.contentUdi) === -1); - unusedBlocks.forEach(blockLayout => { - deleteBlock(blockLayout.$block); - }); - - - // Remove Angular Classes from markup: - var parser = new DOMParser(); - var doc = parser.parseFromString(vm.model.value.markup, 'text/html'); - - // Get all elements in the parsed document - var elements = doc.querySelectorAll('*[class]'); - elements.forEach(element => { - var classAttribute = element.getAttribute("class"); - if (classAttribute) { - // Split the class attribute by spaces and remove "ng-scope" and "ng-isolate-scope" - var classes = classAttribute.split(" "); - var newClasses = classes.filter(function (className) { - return className !== "ng-scope" && className !== "ng-isolate-scope"; + vm.tinyMceEditor.on('init', function (e) { + $timeout(function () { + vm.rteLoading = false; + vm.updateLoading(); }); - - // Update the class attribute with the remaining classes - if (newClasses.length > 0) { - element.setAttribute('class', newClasses.join(' ')); - } else { - // If no remaining classes, remove the class attribute - element.removeAttribute('class'); - } - } + }); + vm.tinyMceEditor.on("focus", function () { + $element[0].dispatchEvent(new CustomEvent('umb-rte-focus', {composed: true, bubbles: true})); + }); + vm.tinyMceEditor.on("blur", function () { + $element[0].dispatchEvent(new CustomEvent('umb-rte-blur', {composed: true, bubbles: true})); }); - vm.model.value.markup = doc.body.innerHTML; + //initialize the standard editor functionality for Umbraco + tinyMceService.initializeEditor({ + //scope: $scope, + editor: editor, + toolbar: editorConfig.toolbar, + model: vm.model, + getValue: function () { + return vm.model.value.markup; + }, + setValue: function (newVal) { + vm.model.value.markup = newVal; + $scope.$evalAsync(); + }, + culture: vm.umbProperty?.culture ?? null, + segment: vm.umbProperty?.segment ?? null, + blockEditorApi: vm.noBlocksMode ? undefined : vm.blockEditorApi, + parentForm: vm.propertyForm, + valFormManager: vm.valFormManager, + currentFormInput: $scope.rteForm.modelValue + }); - } - })); + }; - vm.focusRTE = function () { - vm.tinyMceEditor.focus(); - } + Utilities.extend(baseLineConfigObj, standardConfig); + + // Readonly mode + baseLineConfigObj.toolbar = vm.readonly ? false : baseLineConfigObj.toolbar; + baseLineConfigObj.readonly = vm.readonly ? 1 : baseLineConfigObj.readonly; + + // We need to wait for DOM to have rendered before we can find the element by ID. + $timeout(function () { + tinymce.init(baseLineConfigObj); + }, 50); + + //listen for formSubmitting event (the result is callback used to remove the event subscription) + unsubscribe.push($scope.$on("formSubmitting", function () { + if (vm.tinyMceEditor != null && !vm.rteLoading) { + + // Remove unused Blocks of Blocks Layout. Leaving only the Blocks that are present in Markup. + var blockElements = vm.tinyMceEditor.dom.select(`umb-rte-block, umb-rte-block-inline`); + const usedContentUdis = blockElements.map(blockElement => blockElement.getAttribute('data-content-udi')); + + const unusedBlocks = vm.layout.filter(x => usedContentUdis.indexOf(x.contentUdi) === -1); + unusedBlocks.forEach(blockLayout => { + deleteBlock(blockLayout.$block); + }); + + + // Remove Angular Classes from markup: + var parser = new DOMParser(); + var doc = parser.parseFromString(vm.model.value.markup, 'text/html'); + + // Get all elements in the parsed document + var elements = doc.querySelectorAll('*[class]'); + elements.forEach(element => { + var classAttribute = element.getAttribute("class"); + if (classAttribute) { + // Split the class attribute by spaces and remove "ng-scope" and "ng-isolate-scope" + var classes = classAttribute.split(" "); + var newClasses = classes.filter(function (className) { + return className !== "ng-scope" && className !== "ng-isolate-scope"; + }); + + // Update the class attribute with the remaining classes + if (newClasses.length > 0) { + element.setAttribute('class', newClasses.join(' ')); + } else { + // If no remaining classes, remove the class attribute + element.removeAttribute('class'); + } + } + }); + + vm.model.value.markup = doc.body.innerHTML; - // 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 () { - if (vm.tinyMceEditor != null) { - if($element) { - $element[0]?.dispatchEvent(new CustomEvent('blur', {composed: true, bubbles: true})); } - vm.tinyMceEditor.destroy(); - vm.tinyMceEditor = null; - } - }); + })); - }); + }); }; + vm.focusRTE = function () { + if (vm.tinyMceEditor) { + vm.tinyMceEditor.focus(); + } + } + // Called when we save the value, the server may return an updated data and our value is re-synced // we need to deal with that here so that our model values are all in sync so we basically re-initialize. function onServerValueChanged(newVal, oldVal) { @@ -978,6 +964,17 @@ for (const subscription of unsubscribe) { subscription(); } + + // 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. + if (vm.tinyMceEditor != null) { + if($element) { + $element[0]?.dispatchEvent(new CustomEvent('blur', {composed: true, bubbles: true})); + } + vm.tinyMceEditor.destroy(); + vm.tinyMceEditor = null; + } }); } From f3ed9352134bfec213cadde81fe21274bcae2c3d Mon Sep 17 00:00:00 2001 From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> Date: Mon, 22 Jan 2024 13:37:15 +0100 Subject: [PATCH 02/15] allow the blockeditormodelobject.service.js to fall back to -20 for currentPageId if we are not in an editor mode (#15615) we detect the editor mode by checking for `contentTypeKey` which only exists on content contexts --- .../common/services/blockeditormodelobject.service.js | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/Umbraco.Web.UI.Client/src/common/services/blockeditormodelobject.service.js b/src/Umbraco.Web.UI.Client/src/common/services/blockeditormodelobject.service.js index 1367d58151..ab6c176c85 100644 --- a/src/Umbraco.Web.UI.Client/src/common/services/blockeditormodelobject.service.js +++ b/src/Umbraco.Web.UI.Client/src/common/services/blockeditormodelobject.service.js @@ -367,7 +367,6 @@ * @name load * @methodOf umbraco.services.blockEditorModelObject * @description Load the scaffolding models for the given configuration, these are needed to provide useful models for each block. - * @param {Object} blockObject BlockObject to receive data values from. * @returns {Promise} A Promise object which resolves when all scaffold models are loaded. */ load: function () { @@ -398,9 +397,15 @@ scaffoldKeys = scaffoldKeys.filter((value, index, self) => self.indexOf(value) === index); if(scaffoldKeys.length > 0) { - var currentPage = editorState.getCurrent(); - var currentPageId = currentPage ? (currentPage.id > 0 ? currentPage.id : currentPage.parentId) : null || -20; + // We need to know if we are in the document type editor or content editor. + // If we are in the document type editor, we need to use -20 as the current page id. + // If we are in the content editor, we need to use the current page id or parent id if the current page is new. + // We can recognize a content editor context by checking if the current editor state has a contentTypeKey. + const currentEditorState = editorState.getCurrent(); + const currentPageId = currentEditorState.contentTypeKey ? currentEditorState.id || currentEditorState.parentId || -20 : -20; + // Load all scaffolds for the block types. + // The currentPageId is used to determine the access level for the current user. tasks.push(contentResource.getScaffoldByKeys(currentPageId, scaffoldKeys).then(scaffolds => { Object.values(scaffolds).forEach(scaffold => { // self.scaffolds might not exists anymore, this happens if this instance has been destroyed before the load is complete. From 5940a545851fe71851ff24acafeccd7ba43a8b5f Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Mon, 22 Jan 2024 15:40:26 +0100 Subject: [PATCH 03/15] Updated minor and patch dependencies. (#15619) --- Directory.Packages.props | 32 ++++++++++++++++---------------- tests/Directory.Packages.props | 4 ++-- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 7279fa21c0..59b0f032a1 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -12,24 +12,24 @@ - - + + - - - - + + + + - + - - + + - + @@ -44,9 +44,9 @@ - - - + + + @@ -61,7 +61,7 @@ - + @@ -72,7 +72,7 @@ - + @@ -89,4 +89,4 @@ - + \ No newline at end of file diff --git a/tests/Directory.Packages.props b/tests/Directory.Packages.props index 1ecbee80a5..586469c2c8 100644 --- a/tests/Directory.Packages.props +++ b/tests/Directory.Packages.props @@ -4,8 +4,8 @@ - - + + From 0821fb64bbbba0b1c1c3e05cfdbd991da8c1198d Mon Sep 17 00:00:00 2001 From: Sven Geusens Date: Wed, 24 Jan 2024 10:47:06 +0100 Subject: [PATCH 04/15] version bump --- version.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.json b/version.json index 15469ada83..2822aad6ef 100644 --- a/version.json +++ b/version.json @@ -1,6 +1,6 @@ { "$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/main/src/NerdBank.GitVersioning/version.schema.json", - "version": "13.1.0-rc2", + "version": "13.1.0", "assemblyVersion": { "precision": "build" }, From 802e5a8c27b332565b8b4d53af6652978ea960de Mon Sep 17 00:00:00 2001 From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> Date: Wed, 24 Jan 2024 16:29:00 +0100 Subject: [PATCH 05/15] update apidocs ui section to Umbraco 13 --- src/Umbraco.Web.UI.Docs/.nvmrc | 1 + src/Umbraco.Web.UI.Docs/README.md | 8 +++++--- src/Umbraco.Web.UI.Docs/gulpfile.js | 2 +- 3 files changed, 7 insertions(+), 4 deletions(-) create mode 100644 src/Umbraco.Web.UI.Docs/.nvmrc diff --git a/src/Umbraco.Web.UI.Docs/.nvmrc b/src/Umbraco.Web.UI.Docs/.nvmrc new file mode 100644 index 0000000000..ec3053c8dc --- /dev/null +++ b/src/Umbraco.Web.UI.Docs/.nvmrc @@ -0,0 +1 @@ +10.15 diff --git a/src/Umbraco.Web.UI.Docs/README.md b/src/Umbraco.Web.UI.Docs/README.md index c6f3893e55..6a42c38915 100644 --- a/src/Umbraco.Web.UI.Docs/README.md +++ b/src/Umbraco.Web.UI.Docs/README.md @@ -1,14 +1,16 @@ # Umbraco Backoffice UI API Documentation -This project builds the documentation for the UI of the Umbraco backoffice, it is published on Our Umbraco in the "Reference" section of the documentation. +This project builds the documentation for the UI of the Umbraco backoffice, it is published on the Umbraco Docs in the "Reference" section of the documentation. + +All versions can be accessed through the https://apidocs.umbraco.com/vXX/ui/ url where XX is the major version number of Umbraco. In order to build the documentation, please follow the following two steps: ``` npm ci -npx gulp docs +npm start ``` After this, you should have an `api` directory which contains index.html. -In order to check if the documentation works properly, you would need to run the `api` directory in a webserver. On Windows, this can be accomplished by opening the `api` directory in [Visual Studio Code](https://code.visualstudio.com/) and running it with the [IIS Express plugin](https://marketplace.visualstudio.com/items?itemName=warren-buckley.iis-express). \ No newline at end of file +In order to check if the documentation works properly, you would need to run the `api` directory in a webserver. On Windows, this can be accomplished by opening the `api` directory in [Visual Studio Code](https://code.visualstudio.com/) and running it with the [IIS Express plugin](https://marketplace.visualstudio.com/items?itemName=warren-buckley.iis-express). diff --git a/src/Umbraco.Web.UI.Docs/gulpfile.js b/src/Umbraco.Web.UI.Docs/gulpfile.js index e595eabe5d..53f7f2cf8d 100644 --- a/src/Umbraco.Web.UI.Docs/gulpfile.js +++ b/src/Umbraco.Web.UI.Docs/gulpfile.js @@ -18,7 +18,7 @@ gulp.task('docs', [], function (cb) { var options = { html5Mode: false, startPage: '/api', - title: "Umbraco 12 Backoffice UI API Documentation", + title: "Umbraco 13 Backoffice UI API Documentation", dest: './api', styles: ['./umb-docs.css'], image: "https://our.umbraco.com/assets/images/logo.svg" From b22c7b5350ed145ad89689f276aba76d2c8c8e44 Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Tue, 30 Jan 2024 15:51:11 +0100 Subject: [PATCH 06/15] Bump version --- version.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.json b/version.json index 2822aad6ef..bad154e29f 100644 --- a/version.json +++ b/version.json @@ -1,6 +1,6 @@ { "$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/main/src/NerdBank.GitVersioning/version.schema.json", - "version": "13.1.0", + "version": "13.1.1", "assemblyVersion": { "precision": "build" }, From ec25c3a61d939240541de812f2d5982a10c2f463 Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Tue, 30 Jan 2024 15:52:25 +0100 Subject: [PATCH 07/15] Bump version --- version.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/version.json b/version.json index 1d168fd91b..90f6e87fbf 100644 --- a/version.json +++ b/version.json @@ -1,6 +1,6 @@ { "$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/main/src/NerdBank.GitVersioning/version.schema.json", - "version": "12.3.6", + "version": "12.3.7", "assemblyVersion": { "precision": "build" }, From 5b102e3b8efaa6ac1b930940634a7e9fbea7f83d Mon Sep 17 00:00:00 2001 From: Aleksander Date: Thu, 4 Aug 2022 14:29:20 +0200 Subject: [PATCH 08/15] Pass cache level to properties when creating published content in nucache (cherry picked from commit d9d2b66e8580bc0cbdd42739a92cf9df16b4e96e) # Conflicts: # src/Umbraco.Web/PublishedCache/NuCache/PublishedContent.cs (cherry picked from commit 040495f359f8577197c6281dd0afb84b9e7debdc) --- src/Umbraco.PublishedCache.NuCache/PublishedContent.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.PublishedCache.NuCache/PublishedContent.cs b/src/Umbraco.PublishedCache.NuCache/PublishedContent.cs index 3544ab35bc..f84df0644d 100644 --- a/src/Umbraco.PublishedCache.NuCache/PublishedContent.cs +++ b/src/Umbraco.PublishedCache.NuCache/PublishedContent.cs @@ -56,7 +56,7 @@ internal class PublishedContent : PublishedContentBase // add one property per property type - this is required, for the indexing to work // if contentData supplies pdatas, use them, else use null contentData.Properties.TryGetValue(propertyType.Alias, out PropertyData[]? pdatas); // else will be null - properties[i++] = new Property(propertyType, this, pdatas, _publishedSnapshotAccessor); + properties[i++] = new Property(propertyType, this, pdatas, _publishedSnapshotAccessor, propertyType.CacheLevel); } PropertiesArray = properties; From 7379a752f1e412e6c3b79c61d8a9d9079bec0665 Mon Sep 17 00:00:00 2001 From: Mole Date: Wed, 31 Jan 2024 12:10:07 +0100 Subject: [PATCH 09/15] Obsolete old migrations (#15650) --- .../Migrations/Upgrade/V_10_0_0/AddMemberPropertiesAsColumns.cs | 1 + .../Upgrade/V_10_2_0/AddHasAccessToAllLanguagesColumn.cs | 1 + .../Migrations/Upgrade/V_10_2_0/AddUserGroup2LanguageTable.cs | 1 + .../Migrations/Upgrade/V_10_3_0/AddBlockGridPartialViews.cs | 1 + .../Migrations/Upgrade/V_10_4_0/AddBlockGridPartialViews.cs | 1 + .../AddPrimaryKeyConstrainToContentVersionCleanupDtos.cs | 1 + .../Upgrade/V_10_7_0/MigrateTagsFromNVarcharToNText.cs | 1 + .../Migrations/Upgrade/V_11_3_0/AddDomainSortOrder.cs | 1 + .../Migrations/Upgrade/V_11_4_0/AlterKeyValueDataType.cs | 1 + .../Migrations/Upgrade/V_12_0_0/ResetCache.cs | 1 + .../Migrations/Upgrade/V_12_0_0/UseNvarcharInsteadOfNText.cs | 1 + .../Migrations/Upgrade/V_12_1_0/AddOpenIddict.cs | 1 + .../Migrations/Upgrade/V_12_1_0/TablesIndexesImprovement.cs | 1 + 13 files changed, 13 insertions(+) diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_10_0_0/AddMemberPropertiesAsColumns.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_10_0_0/AddMemberPropertiesAsColumns.cs index 1a3cda316d..40eab7cd18 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_10_0_0/AddMemberPropertiesAsColumns.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_10_0_0/AddMemberPropertiesAsColumns.cs @@ -7,6 +7,7 @@ using Umbraco.Extensions; namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_10_0_0; +[Obsolete("This is no longer used and will be removed in V14.")] public class AddMemberPropertiesAsColumns : MigrationBase { public AddMemberPropertiesAsColumns(IMigrationContext context) diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_10_2_0/AddHasAccessToAllLanguagesColumn.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_10_2_0/AddHasAccessToAllLanguagesColumn.cs index 6078beb815..874831d269 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_10_2_0/AddHasAccessToAllLanguagesColumn.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_10_2_0/AddHasAccessToAllLanguagesColumn.cs @@ -2,6 +2,7 @@ namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_10_2_0; +[Obsolete("This is no longer used and will be removed in V14.")] public class AddHasAccessToAllLanguagesColumn : MigrationBase { public AddHasAccessToAllLanguagesColumn(IMigrationContext context) : base(context) diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_10_2_0/AddUserGroup2LanguageTable.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_10_2_0/AddUserGroup2LanguageTable.cs index b6a4814e7d..955d95d501 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_10_2_0/AddUserGroup2LanguageTable.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_10_2_0/AddUserGroup2LanguageTable.cs @@ -3,6 +3,7 @@ using Umbraco.Extensions; namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_10_2_0; +[Obsolete("This is no longer used and will be removed in V14.")] public class AddUserGroup2LanguageTable : MigrationBase { public AddUserGroup2LanguageTable(IMigrationContext context) : base(context) diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_10_3_0/AddBlockGridPartialViews.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_10_3_0/AddBlockGridPartialViews.cs index c99f5cfe81..e7e9148e59 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_10_3_0/AddBlockGridPartialViews.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_10_3_0/AddBlockGridPartialViews.cs @@ -2,6 +2,7 @@ namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_10_3_0; +[Obsolete("This is no longer used and will be removed in V14.")] public class AddBlockGridPartialViews : MigrationBase { private readonly IPartialViewPopulator _partialViewPopulator; diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_10_4_0/AddBlockGridPartialViews.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_10_4_0/AddBlockGridPartialViews.cs index fafd5b1a6f..b760323f99 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_10_4_0/AddBlockGridPartialViews.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_10_4_0/AddBlockGridPartialViews.cs @@ -2,6 +2,7 @@ namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_10_4_0; +[Obsolete("This is no longer used and will be removed in V14.")] public class AddBlockGridPartialViews : MigrationBase { private readonly IPartialViewPopulator _partialViewPopulator; diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_10_5_0/AddPrimaryKeyConstrainToContentVersionCleanupDtos.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_10_5_0/AddPrimaryKeyConstrainToContentVersionCleanupDtos.cs index d1185bec46..714e214030 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_10_5_0/AddPrimaryKeyConstrainToContentVersionCleanupDtos.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_10_5_0/AddPrimaryKeyConstrainToContentVersionCleanupDtos.cs @@ -2,6 +2,7 @@ namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_10_5_0; +[Obsolete("This is no longer used and will be removed in V14.")] public class AddPrimaryKeyConstrainToContentVersionCleanupDtos : MigrationBase { public AddPrimaryKeyConstrainToContentVersionCleanupDtos(IMigrationContext context) : base(context) diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_10_7_0/MigrateTagsFromNVarcharToNText.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_10_7_0/MigrateTagsFromNVarcharToNText.cs index 462e3772fa..9bc9856fe7 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_10_7_0/MigrateTagsFromNVarcharToNText.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_10_7_0/MigrateTagsFromNVarcharToNText.cs @@ -7,6 +7,7 @@ using Umbraco.Extensions; namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_10_7_0; +[Obsolete("This is no longer used and will be removed in V14.")] public class MigrateTagsFromNVarcharToNText : MigrationBase { public MigrateTagsFromNVarcharToNText(IMigrationContext context) diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_11_3_0/AddDomainSortOrder.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_11_3_0/AddDomainSortOrder.cs index b8668de31f..19f20c0f6c 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_11_3_0/AddDomainSortOrder.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_11_3_0/AddDomainSortOrder.cs @@ -2,6 +2,7 @@ using Umbraco.Cms.Infrastructure.Persistence.Dtos; namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_11_3_0; +[Obsolete("This is no longer used and will be removed in V14.")] public class AddDomainSortOrder : MigrationBase { public AddDomainSortOrder(IMigrationContext context) diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_11_4_0/AlterKeyValueDataType.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_11_4_0/AlterKeyValueDataType.cs index 413711ad1d..9c7852620d 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_11_4_0/AlterKeyValueDataType.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_11_4_0/AlterKeyValueDataType.cs @@ -5,6 +5,7 @@ using Umbraco.Cms.Infrastructure.Persistence.Dtos; namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_11_4_0; +[Obsolete("This is no longer used and will be removed in V14.")] public class AlterKeyValueDataType : MigrationBase { private readonly IMigrationContext _context; diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_12_0_0/ResetCache.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_12_0_0/ResetCache.cs index 0e41ad89ca..1a28ea28ed 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_12_0_0/ResetCache.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_12_0_0/ResetCache.cs @@ -5,6 +5,7 @@ using IHostingEnvironment = Umbraco.Cms.Core.Hosting.IHostingEnvironment; namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_12_0_0; +[Obsolete("This is no longer used and will be removed in V14.")] public class ResetCache : MigrationBase { private readonly IHostingEnvironment _hostingEnvironment; diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_12_0_0/UseNvarcharInsteadOfNText.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_12_0_0/UseNvarcharInsteadOfNText.cs index 4c3a0c889a..e761ccf8c4 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_12_0_0/UseNvarcharInsteadOfNText.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_12_0_0/UseNvarcharInsteadOfNText.cs @@ -8,6 +8,7 @@ using Umbraco.Cms.Infrastructure.Persistence.Dtos; namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_12_0_0; +[Obsolete("This is no longer used and will be removed in V14.")] public class UseNvarcharInsteadOfNText : MigrationBase { public UseNvarcharInsteadOfNText(IMigrationContext context) diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_12_1_0/AddOpenIddict.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_12_1_0/AddOpenIddict.cs index a32334ece4..06dc7256cc 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_12_1_0/AddOpenIddict.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_12_1_0/AddOpenIddict.cs @@ -2,6 +2,7 @@ using Umbraco.Cms.Persistence.EFCore.Migrations; namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_12_1_0; +[Obsolete("This is no longer used and will be removed in V14.")] public class AddOpenIddict : UnscopedMigrationBase { private readonly IEFCoreMigrationExecutor _iefCoreMigrationExecutor; diff --git a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_12_1_0/TablesIndexesImprovement.cs b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_12_1_0/TablesIndexesImprovement.cs index 5483f25593..22eeb8e36b 100644 --- a/src/Umbraco.Infrastructure/Migrations/Upgrade/V_12_1_0/TablesIndexesImprovement.cs +++ b/src/Umbraco.Infrastructure/Migrations/Upgrade/V_12_1_0/TablesIndexesImprovement.cs @@ -2,6 +2,7 @@ namespace Umbraco.Cms.Infrastructure.Migrations.Upgrade.V_12_1_0; +[Obsolete("This is no longer used and will be removed in V14.")] public class TablesIndexesImprovement : MigrationBase { public TablesIndexesImprovement(IMigrationContext context) : base(context) From 5146f05625d53d11c64f75459b7003ce11bcb078 Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Wed, 31 Jan 2024 15:09:12 +0100 Subject: [PATCH 10/15] Do not dispose EF Core contexts manually (#15649) * Attempt to fix #15090. Save connection string in private variable after setup. * Do not dispose ef core contexts when if the connectionstring changes. A Ef core context is protected from creations when no connection string is present. If it changes that single instance will continue with the connection string it was initialized with. * Clean up * Review Equals methods --- .../UmbracoDbContext.cs | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/src/Umbraco.Cms.Persistence.EFCore/UmbracoDbContext.cs b/src/Umbraco.Cms.Persistence.EFCore/UmbracoDbContext.cs index 3df757ee15..419ecd6a28 100644 --- a/src/Umbraco.Cms.Persistence.EFCore/UmbracoDbContext.cs +++ b/src/Umbraco.Cms.Persistence.EFCore/UmbracoDbContext.cs @@ -36,19 +36,14 @@ public class UmbracoDbContext : DbContext /// /// public UmbracoDbContext(DbContextOptions options) - : base(ConfigureOptions(options, out IOptionsMonitor? connectionStringsOptionsMonitor)) + : base(ConfigureOptions(options)) { - connectionStringsOptionsMonitor.OnChange(c => - { - ILogger logger = StaticServiceProvider.Instance.GetRequiredService>(); - logger.LogWarning("Connection string changed, disposing context"); - Dispose(); - }); + } - private static DbContextOptions ConfigureOptions(DbContextOptions options, out IOptionsMonitor connectionStringsOptionsMonitor) + private static DbContextOptions ConfigureOptions(DbContextOptions options) { - connectionStringsOptionsMonitor = StaticServiceProvider.Instance.GetRequiredService>(); + IOptionsMonitor connectionStringsOptionsMonitor = StaticServiceProvider.Instance.GetRequiredService>(); ConnectionStrings connectionStrings = connectionStringsOptionsMonitor.CurrentValue; From 94b8c11f0f8071c731791439c0ebdf4c73aa62f4 Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Wed, 31 Jan 2024 15:09:12 +0100 Subject: [PATCH 11/15] Do not dispose EF Core contexts manually (#15649) * Attempt to fix #15090. Save connection string in private variable after setup. * Do not dispose ef core contexts when if the connectionstring changes. A Ef core context is protected from creations when no connection string is present. If it changes that single instance will continue with the connection string it was initialized with. * Clean up * Review Equals methods (cherry picked from commit 5146f05625d53d11c64f75459b7003ce11bcb078) --- .../UmbracoDbContext.cs | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/src/Umbraco.Cms.Persistence.EFCore/UmbracoDbContext.cs b/src/Umbraco.Cms.Persistence.EFCore/UmbracoDbContext.cs index 3df757ee15..419ecd6a28 100644 --- a/src/Umbraco.Cms.Persistence.EFCore/UmbracoDbContext.cs +++ b/src/Umbraco.Cms.Persistence.EFCore/UmbracoDbContext.cs @@ -36,19 +36,14 @@ public class UmbracoDbContext : DbContext /// /// public UmbracoDbContext(DbContextOptions options) - : base(ConfigureOptions(options, out IOptionsMonitor? connectionStringsOptionsMonitor)) + : base(ConfigureOptions(options)) { - connectionStringsOptionsMonitor.OnChange(c => - { - ILogger logger = StaticServiceProvider.Instance.GetRequiredService>(); - logger.LogWarning("Connection string changed, disposing context"); - Dispose(); - }); + } - private static DbContextOptions ConfigureOptions(DbContextOptions options, out IOptionsMonitor connectionStringsOptionsMonitor) + private static DbContextOptions ConfigureOptions(DbContextOptions options) { - connectionStringsOptionsMonitor = StaticServiceProvider.Instance.GetRequiredService>(); + IOptionsMonitor connectionStringsOptionsMonitor = StaticServiceProvider.Instance.GetRequiredService>(); ConnectionStrings connectionStrings = connectionStringsOptionsMonitor.CurrentValue; From ac02e97e0c888440bba512e4bf51e4056a4fdcf9 Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Thu, 1 Feb 2024 09:58:42 +0100 Subject: [PATCH 12/15] V10+ version of https://github.com/umbraco/Umbraco-CMS/pull/15638 (#15664) --- .../Filters/ContentSaveValidationAttribute.cs | 2 +- src/Umbraco.Web.BackOffice/ModelBinders/BlueprintItemBinder.cs | 2 +- src/Umbraco.Web.BackOffice/ModelBinders/ContentItemBinder.cs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Umbraco.Web.BackOffice/Filters/ContentSaveValidationAttribute.cs b/src/Umbraco.Web.BackOffice/Filters/ContentSaveValidationAttribute.cs index f7be9d129a..c75bbd5a80 100644 --- a/src/Umbraco.Web.BackOffice/Filters/ContentSaveValidationAttribute.cs +++ b/src/Umbraco.Web.BackOffice/Filters/ContentSaveValidationAttribute.cs @@ -18,7 +18,7 @@ namespace Umbraco.Cms.Web.BackOffice.Filters; /// Validates the incoming model along with if the user is allowed to perform the /// operation /// -internal sealed class ContentSaveValidationAttribute : TypeFilterAttribute +public sealed class ContentSaveValidationAttribute : TypeFilterAttribute { public ContentSaveValidationAttribute(bool skipUserAccessValidation = false) : base(typeof(ContentSaveValidationFilter)) diff --git a/src/Umbraco.Web.BackOffice/ModelBinders/BlueprintItemBinder.cs b/src/Umbraco.Web.BackOffice/ModelBinders/BlueprintItemBinder.cs index bc07497fcd..bf8c7372bc 100644 --- a/src/Umbraco.Web.BackOffice/ModelBinders/BlueprintItemBinder.cs +++ b/src/Umbraco.Web.BackOffice/ModelBinders/BlueprintItemBinder.cs @@ -7,7 +7,7 @@ using Umbraco.Cms.Core.Services; namespace Umbraco.Cms.Web.BackOffice.ModelBinders; -internal class BlueprintItemBinder : ContentItemBinder +public class BlueprintItemBinder : ContentItemBinder { private readonly IContentService _contentService; diff --git a/src/Umbraco.Web.BackOffice/ModelBinders/ContentItemBinder.cs b/src/Umbraco.Web.BackOffice/ModelBinders/ContentItemBinder.cs index 0842ca2051..c73a45f904 100644 --- a/src/Umbraco.Web.BackOffice/ModelBinders/ContentItemBinder.cs +++ b/src/Umbraco.Web.BackOffice/ModelBinders/ContentItemBinder.cs @@ -13,7 +13,7 @@ namespace Umbraco.Cms.Web.BackOffice.ModelBinders; /// /// The model binder for /// -internal class ContentItemBinder : IModelBinder +public class ContentItemBinder : IModelBinder { private readonly IContentService _contentService; private readonly IContentTypeService _contentTypeService; From 4aed6a1034e182fe533f5d2aa8a06b0c1cf73619 Mon Sep 17 00:00:00 2001 From: Ronald Barendse Date: Thu, 1 Feb 2024 09:55:09 +0100 Subject: [PATCH 13/15] Skip cache refresher operations for content blueprints (#15633) * Skip cache refresher operations for content blueprints * Fix JsonPayload deserialization error by adding a default constructor and property initializers * Obsolete JsonPayload constructor and update usages --- .../Cache/DistributedCacheExtensions.cs | 24 ++- .../Implement/ContentCacheRefresher.cs | 23 ++- .../Implement/LanguageCacheRefresher.cs | 10 +- .../IndexingNotificationHandler.Content.cs | 12 +- .../PublishedSnapshotService.cs | 17 +- .../Umbraco.Core/Cache/RefresherTests.cs | 24 ++- ...PublishedSnapshotServiceCollectionTests.cs | 179 +++++++++++++++--- 7 files changed, 239 insertions(+), 50 deletions(-) diff --git a/src/Umbraco.Core/Cache/DistributedCacheExtensions.cs b/src/Umbraco.Core/Cache/DistributedCacheExtensions.cs index 8d792a5ef7..438c66a2c1 100644 --- a/src/Umbraco.Core/Cache/DistributedCacheExtensions.cs +++ b/src/Umbraco.Core/Cache/DistributedCacheExtensions.cs @@ -118,15 +118,35 @@ public static class DistributedCacheExtensions #region ContentCacheRefresher public static void RefreshAllContentCache(this DistributedCache dc) + { + ContentCacheRefresher.JsonPayload[] payloads = new[] + { + new ContentCacheRefresher.JsonPayload() + { + ChangeTypes = TreeChangeTypes.RefreshAll + } + }; + // note: refresh all content cache does refresh content types too - => dc.RefreshByPayload(ContentCacheRefresher.UniqueId, new ContentCacheRefresher.JsonPayload(0, null, TreeChangeTypes.RefreshAll).Yield()); + dc.RefreshByPayload(ContentCacheRefresher.UniqueId, payloads); + } [Obsolete("Use the overload accepting IEnumerable instead to avoid allocating arrays. This overload will be removed in Umbraco 13.")] public static void RefreshContentCache(this DistributedCache dc, TreeChange[] changes) => dc.RefreshContentCache(changes.AsEnumerable()); public static void RefreshContentCache(this DistributedCache dc, IEnumerable> changes) - => dc.RefreshByPayload(ContentCacheRefresher.UniqueId, changes.DistinctBy(x => (x.Item.Id, x.Item.Key, x.ChangeTypes)).Select(x => new ContentCacheRefresher.JsonPayload(x.Item.Id, x.Item.Key, x.ChangeTypes))); + { + IEnumerable payloads = changes.Select(x => new ContentCacheRefresher.JsonPayload() + { + Id = x.Item.Id, + Key = x.Item.Key, + ChangeTypes = x.ChangeTypes, + Blueprint = x.Item.Blueprint + }); + + dc.RefreshByPayload(ContentCacheRefresher.UniqueId, payloads); + } #endregion diff --git a/src/Umbraco.Core/Cache/Refreshers/Implement/ContentCacheRefresher.cs b/src/Umbraco.Core/Cache/Refreshers/Implement/ContentCacheRefresher.cs index a515d5c5d1..779b22fe68 100644 --- a/src/Umbraco.Core/Cache/Refreshers/Implement/ContentCacheRefresher.cs +++ b/src/Umbraco.Core/Cache/Refreshers/Implement/ContentCacheRefresher.cs @@ -84,8 +84,8 @@ public sealed class ContentCacheRefresher : PayloadCacheRefresherBase((k, v) => v.Path?.Contains(pathid) ?? false); } - // if the item is being completely removed, we need to refresh the domains cache if any domain was assigned to the content - if (payload.ChangeTypes.HasTypesAny(TreeChangeTypes.Remove)) + // if the item is not a blueprint and is being completely removed, we need to refresh the domains cache if any domain was assigned to the content + if (payload.Blueprint is false && payload.ChangeTypes.HasTypesAny(TreeChangeTypes.Remove)) { idsRemoved.Add(payload.Id); } @@ -120,7 +120,11 @@ public sealed class ContentCacheRefresher : PayloadCacheRefresherBase x.Blueprint is false)) + { + // Only notify if the payload contains actual (non-blueprint) contents + NotifyPublishedSnapshotService(_publishedSnapshotService, AppCaches, payloads); + } base.Refresh(payloads); } @@ -157,8 +161,13 @@ public sealed class ContentCacheRefresher : PayloadCacheRefresherBase(); - } - + deleteBatch ??= new HashSet(); deleteBatch.Add(payload.Id); } else if (payload.ChangeTypes.HasType(TreeChangeTypes.RefreshAll)) diff --git a/src/Umbraco.PublishedCache.NuCache/PublishedSnapshotService.cs b/src/Umbraco.PublishedCache.NuCache/PublishedSnapshotService.cs index a7f8c42823..286335fe6e 100644 --- a/src/Umbraco.PublishedCache.NuCache/PublishedSnapshotService.cs +++ b/src/Umbraco.PublishedCache.NuCache/PublishedSnapshotService.cs @@ -215,7 +215,16 @@ internal class PublishedSnapshotService : IPublishedSnapshotService // they require. using (_contentStore.GetScopedWriteLock(_scopeProvider)) { - NotifyLocked(new[] { new ContentCacheRefresher.JsonPayload(0, null, TreeChangeTypes.RefreshAll) }, out _, out _); + NotifyLocked( + new[] + { + new ContentCacheRefresher.JsonPayload() + { + ChangeTypes = TreeChangeTypes.RefreshAll + } + }, + out _, + out _); } using (_mediaStore.GetScopedWriteLock(_scopeProvider)) @@ -891,6 +900,12 @@ internal class PublishedSnapshotService : IPublishedSnapshotService _logger.LogDebug("Notified {ChangeTypes} for content {ContentId}", payload.ChangeTypes, payload.Id); } + if (payload.Blueprint) + { + // Skip blueprints + continue; + } + if (payload.ChangeTypes.HasType(TreeChangeTypes.RefreshAll)) { using (IScope scope = _scopeProvider.CreateScope()) diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Cache/RefresherTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Cache/RefresherTests.cs index 92ef2b5c9f..e509742cb9 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Cache/RefresherTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Cache/RefresherTests.cs @@ -18,8 +18,10 @@ public class RefresherTests { new MediaCacheRefresher.JsonPayload(1234, Guid.NewGuid(), TreeChangeTypes.None), }; + var json = JsonConvert.SerializeObject(source); var payload = JsonConvert.DeserializeObject(json); + Assert.AreEqual(source[0].Id, payload[0].Id); Assert.AreEqual(source[0].Key, payload[0].Key); Assert.AreEqual(source[0].ChangeTypes, payload[0].ChangeTypes); @@ -30,13 +32,21 @@ public class RefresherTests { ContentCacheRefresher.JsonPayload[] source = { - new ContentCacheRefresher.JsonPayload(1234, Guid.NewGuid(), TreeChangeTypes.None), + new ContentCacheRefresher.JsonPayload() + { + Id = 1234, + Key = Guid.NewGuid(), + ChangeTypes = TreeChangeTypes.None + } }; + var json = JsonConvert.SerializeObject(source); var payload = JsonConvert.DeserializeObject(json); + Assert.AreEqual(source[0].Id, payload[0].Id); Assert.AreEqual(source[0].Key, payload[0].Key); Assert.AreEqual(source[0].ChangeTypes, payload[0].ChangeTypes); + Assert.AreEqual(source[0].Blueprint, payload[0].Blueprint); } [Test] @@ -46,8 +56,10 @@ public class RefresherTests { new ContentTypeCacheRefresher.JsonPayload("xxx", 1234, ContentTypeChangeTypes.None), }; + var json = JsonConvert.SerializeObject(source); var payload = JsonConvert.DeserializeObject(json); + Assert.AreEqual(source[0].ItemType, payload[0].ItemType); Assert.AreEqual(source[0].Id, payload[0].Id); Assert.AreEqual(source[0].ChangeTypes, payload[0].ChangeTypes); @@ -60,8 +72,10 @@ public class RefresherTests { new DataTypeCacheRefresher.JsonPayload(1234, Guid.NewGuid(), true), }; + var json = JsonConvert.SerializeObject(source); var payload = JsonConvert.DeserializeObject(json); + Assert.AreEqual(source[0].Id, payload[0].Id); Assert.AreEqual(source[0].Key, payload[0].Key); Assert.AreEqual(source[0].Removed, payload[0].Removed); @@ -70,10 +84,14 @@ public class RefresherTests [Test] public void DomainCacheRefresherCanDeserializeJsonPayload() { - DomainCacheRefresher.JsonPayload[] - source = { new DomainCacheRefresher.JsonPayload(1234, DomainChangeTypes.None) }; + DomainCacheRefresher.JsonPayload[] source = + { + new DomainCacheRefresher.JsonPayload(1234, DomainChangeTypes.None) + }; + var json = JsonConvert.SerializeObject(source); var payload = JsonConvert.DeserializeObject(json); + Assert.AreEqual(source[0].Id, payload[0].Id); Assert.AreEqual(source[0].ChangeType, payload[0].ChangeType); } diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/PublishedCache/PublishedSnapshotServiceCollectionTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/PublishedCache/PublishedSnapshotServiceCollectionTests.cs index a007079ca9..b516b4fffb 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/PublishedCache/PublishedSnapshotServiceCollectionTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/PublishedCache/PublishedSnapshotServiceCollectionTests.cs @@ -358,7 +358,16 @@ public class PublishedSnapshotServiceCollectionTests : PublishedSnapshotServiceT // notify SnapshotService.Notify( - new[] { new ContentCacheRefresher.JsonPayload(10, Guid.Empty, TreeChangeTypes.RefreshBranch) }, out _, out _); + new[] + { + new ContentCacheRefresher.JsonPayload() + { + Id = 10, + ChangeTypes = TreeChangeTypes.RefreshBranch + } + }, + out _, + out _); // changes that *I* make are immediately visible on the current snapshot var documents = snapshot.Content.GetAtRoot().ToArray(); @@ -392,7 +401,16 @@ public class PublishedSnapshotServiceCollectionTests : PublishedSnapshotServiceT // notify SnapshotService.Notify( - new[] { new ContentCacheRefresher.JsonPayload(1, Guid.Empty, TreeChangeTypes.RefreshBranch) }, out _, out _); + new[] + { + new ContentCacheRefresher.JsonPayload() + { + Id = 1, + ChangeTypes = TreeChangeTypes.RefreshBranch + } + }, + out _, + out _); // changes that *I* make are immediately visible on the current snapshot var documents = snapshot.Content.GetAtRoot().ToArray(); @@ -450,7 +468,11 @@ public class PublishedSnapshotServiceCollectionTests : PublishedSnapshotServiceT SnapshotService.Notify( new[] { - new ContentCacheRefresher.JsonPayload(kit.Node.ParentContentId, Guid.Empty, TreeChangeTypes.RefreshBranch), + new ContentCacheRefresher.JsonPayload() + { + Id = kit.Node.ParentContentId, + ChangeTypes = TreeChangeTypes.RefreshBranch + } }, out _, out _); @@ -516,11 +538,19 @@ public class PublishedSnapshotServiceCollectionTests : PublishedSnapshotServiceT // notify SnapshotService.Notify( new[] - { - // removal must come first - new ContentCacheRefresher.JsonPayload(2, Guid.Empty, TreeChangeTypes.RefreshBranch), - new ContentCacheRefresher.JsonPayload(1, Guid.Empty, TreeChangeTypes.RefreshBranch), - }, + { + // removal must come first + new ContentCacheRefresher.JsonPayload() + { + Id = 2, + ChangeTypes = TreeChangeTypes.RefreshBranch + }, + new ContentCacheRefresher.JsonPayload() + { + Id = 1, + ChangeTypes = TreeChangeTypes.RefreshBranch + } + }, out _, out _); @@ -571,7 +601,16 @@ public class PublishedSnapshotServiceCollectionTests : PublishedSnapshotServiceT // notify - which ensures there are 2 generations in the cache meaning each LinkedNode has a Next value. SnapshotService.Notify( - new[] { new ContentCacheRefresher.JsonPayload(4, Guid.Empty, TreeChangeTypes.RefreshBranch) }, out _, out _); + new[] + { + new ContentCacheRefresher.JsonPayload() + { + Id = 4, + ChangeTypes = TreeChangeTypes.RefreshBranch + } + }, + out _, + out _); // refresh the branch again, this used to show the issue where a null ref exception would occur // because in the ClearBranchLocked logic, when SetValueLocked was called within a recursive call @@ -579,7 +618,16 @@ public class PublishedSnapshotServiceCollectionTests : PublishedSnapshotServiceT // this value before recursing. Assert.DoesNotThrow(() => SnapshotService.Notify( - new[] { new ContentCacheRefresher.JsonPayload(4, Guid.Empty, TreeChangeTypes.RefreshBranch) }, out _, out _)); + new[] + { + new ContentCacheRefresher.JsonPayload() + { + Id = 4, + ChangeTypes = TreeChangeTypes.RefreshBranch + } + }, + out _, + out _)); } [Test] @@ -759,11 +807,23 @@ public class PublishedSnapshotServiceCollectionTests : PublishedSnapshotServiceT // notify SnapshotService.Notify( new[] - { - new ContentCacheRefresher.JsonPayload(3, Guid.Empty, TreeChangeTypes.Remove), // remove last - new ContentCacheRefresher.JsonPayload(5, Guid.Empty, TreeChangeTypes.Remove), // remove middle - new ContentCacheRefresher.JsonPayload(9, Guid.Empty, TreeChangeTypes.Remove), // remove first - }, + { + new ContentCacheRefresher.JsonPayload() // remove last + { + Id = 3, + ChangeTypes = TreeChangeTypes.Remove + }, + new ContentCacheRefresher.JsonPayload() // remove middle + { + Id = 5, + ChangeTypes = TreeChangeTypes.Remove + }, + new ContentCacheRefresher.JsonPayload() // remove first + { + Id = 9, + ChangeTypes = TreeChangeTypes.Remove + } + }, out _, out _); @@ -779,11 +839,23 @@ public class PublishedSnapshotServiceCollectionTests : PublishedSnapshotServiceT // notify SnapshotService.Notify( new[] - { - new ContentCacheRefresher.JsonPayload(1, Guid.Empty, TreeChangeTypes.Remove), // remove first - new ContentCacheRefresher.JsonPayload(8, Guid.Empty, TreeChangeTypes.Remove), // remove - new ContentCacheRefresher.JsonPayload(7, Guid.Empty, TreeChangeTypes.Remove), // remove - }, + { + new ContentCacheRefresher.JsonPayload() // remove first + { + Id = 1, + ChangeTypes = TreeChangeTypes.Remove + }, + new ContentCacheRefresher.JsonPayload() // remove + { + Id = 8, + ChangeTypes = TreeChangeTypes.Remove + }, + new ContentCacheRefresher.JsonPayload() // remove + { + Id = 7, + ChangeTypes = TreeChangeTypes.Remove + } + }, out _, out _); @@ -823,8 +895,16 @@ public class PublishedSnapshotServiceCollectionTests : PublishedSnapshotServiceT SnapshotService.Notify( new[] { - new ContentCacheRefresher.JsonPayload(1, Guid.Empty, TreeChangeTypes.RefreshBranch), - new ContentCacheRefresher.JsonPayload(2, Guid.Empty, TreeChangeTypes.RefreshNode), + new ContentCacheRefresher.JsonPayload() + { + Id = 1, + ChangeTypes = TreeChangeTypes.RefreshBranch + }, + new ContentCacheRefresher.JsonPayload() + { + Id = 2, + ChangeTypes = TreeChangeTypes.RefreshNode + } }, out _, out _); @@ -887,7 +967,17 @@ public class PublishedSnapshotServiceCollectionTests : PublishedSnapshotServiceT var parentNode = parentNodes[0]; AssertLinkedNode(parentNode.contentNode, -1, -1, -1, 2, 2); - SnapshotService.Notify(new[] { new ContentCacheRefresher.JsonPayload(2, Guid.Empty, TreeChangeTypes.Remove) }, out _, out _); + SnapshotService.Notify( + new[] + { + new ContentCacheRefresher.JsonPayload() + { + Id = 2, + ChangeTypes = TreeChangeTypes.Remove + } + }, + out _, + out _); parentNodes = contentStore.Test.GetValues(1); parentNode = parentNodes[0]; @@ -944,9 +1034,13 @@ public class PublishedSnapshotServiceCollectionTests : PublishedSnapshotServiceT SnapshotService.Notify( new[] - { - new ContentCacheRefresher.JsonPayload(3, Guid.Empty, TreeChangeTypes.Remove), // remove middle child - }, + { + new ContentCacheRefresher.JsonPayload() // remove middle child + { + Id = 3, + ChangeTypes = TreeChangeTypes.Remove + } + }, out _, out _); @@ -1013,7 +1107,16 @@ public class PublishedSnapshotServiceCollectionTests : PublishedSnapshotServiceT Assert.IsFalse(contentStore.Test.NextGen); SnapshotService.Notify( - new[] { new ContentCacheRefresher.JsonPayload(1, Guid.Empty, TreeChangeTypes.RefreshNode) }, out _, out _); + new[] + { + new ContentCacheRefresher.JsonPayload() + { + Id = 1, + ChangeTypes = TreeChangeTypes.RefreshNode + } + }, + out _, + out _); Assert.AreEqual(2, contentStore.Test.LiveGen); Assert.IsTrue(contentStore.Test.NextGen); @@ -1084,7 +1187,17 @@ public class PublishedSnapshotServiceCollectionTests : PublishedSnapshotServiceT published ? rootKit.PublishedData : null); NuCacheContentService.ContentKits[1] = kit; - SnapshotService.Notify(new[] { new ContentCacheRefresher.JsonPayload(1, Guid.Empty, changeType) }, out _, out _); + SnapshotService.Notify( + new[] + { + new ContentCacheRefresher.JsonPayload() + { + Id = 1, + ChangeTypes = changeType + } + }, + out _, + out _); Assert.AreEqual(assertGen, contentStore.Test.LiveGen); Assert.IsTrue(contentStore.Test.NextGen); @@ -1162,9 +1275,13 @@ public class PublishedSnapshotServiceCollectionTests : PublishedSnapshotServiceT SnapshotService.Notify( new[] - { - new ContentCacheRefresher.JsonPayload(3, Guid.Empty, TreeChangeTypes.RefreshBranch), // remove middle child - }, + { + new ContentCacheRefresher.JsonPayload() // remove middle child + { + Id = 3, + ChangeTypes = TreeChangeTypes.RefreshBranch + } + }, out _, out _); From e7c53d4768682613bab74570374fac3c0b195629 Mon Sep 17 00:00:00 2001 From: Bjarke Berg Date: Thu, 1 Feb 2024 09:58:42 +0100 Subject: [PATCH 14/15] V10+ version of https://github.com/umbraco/Umbraco-CMS/pull/15638 (#15664) --- .../Filters/ContentSaveValidationAttribute.cs | 2 +- src/Umbraco.Web.BackOffice/ModelBinders/BlueprintItemBinder.cs | 2 +- src/Umbraco.Web.BackOffice/ModelBinders/ContentItemBinder.cs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Umbraco.Web.BackOffice/Filters/ContentSaveValidationAttribute.cs b/src/Umbraco.Web.BackOffice/Filters/ContentSaveValidationAttribute.cs index f7be9d129a..c75bbd5a80 100644 --- a/src/Umbraco.Web.BackOffice/Filters/ContentSaveValidationAttribute.cs +++ b/src/Umbraco.Web.BackOffice/Filters/ContentSaveValidationAttribute.cs @@ -18,7 +18,7 @@ namespace Umbraco.Cms.Web.BackOffice.Filters; /// Validates the incoming model along with if the user is allowed to perform the /// operation /// -internal sealed class ContentSaveValidationAttribute : TypeFilterAttribute +public sealed class ContentSaveValidationAttribute : TypeFilterAttribute { public ContentSaveValidationAttribute(bool skipUserAccessValidation = false) : base(typeof(ContentSaveValidationFilter)) diff --git a/src/Umbraco.Web.BackOffice/ModelBinders/BlueprintItemBinder.cs b/src/Umbraco.Web.BackOffice/ModelBinders/BlueprintItemBinder.cs index bc07497fcd..bf8c7372bc 100644 --- a/src/Umbraco.Web.BackOffice/ModelBinders/BlueprintItemBinder.cs +++ b/src/Umbraco.Web.BackOffice/ModelBinders/BlueprintItemBinder.cs @@ -7,7 +7,7 @@ using Umbraco.Cms.Core.Services; namespace Umbraco.Cms.Web.BackOffice.ModelBinders; -internal class BlueprintItemBinder : ContentItemBinder +public class BlueprintItemBinder : ContentItemBinder { private readonly IContentService _contentService; diff --git a/src/Umbraco.Web.BackOffice/ModelBinders/ContentItemBinder.cs b/src/Umbraco.Web.BackOffice/ModelBinders/ContentItemBinder.cs index 0842ca2051..c73a45f904 100644 --- a/src/Umbraco.Web.BackOffice/ModelBinders/ContentItemBinder.cs +++ b/src/Umbraco.Web.BackOffice/ModelBinders/ContentItemBinder.cs @@ -13,7 +13,7 @@ namespace Umbraco.Cms.Web.BackOffice.ModelBinders; /// /// The model binder for /// -internal class ContentItemBinder : IModelBinder +public class ContentItemBinder : IModelBinder { private readonly IContentService _contentService; private readonly IContentTypeService _contentTypeService; From 25139dabe0ed70d799a0593cd5338b1107c035ed Mon Sep 17 00:00:00 2001 From: Ronald Barendse Date: Thu, 1 Feb 2024 09:55:09 +0100 Subject: [PATCH 15/15] Skip cache refresher operations for content blueprints (#15633) * Skip cache refresher operations for content blueprints * Fix JsonPayload deserialization error by adding a default constructor and property initializers * Obsolete JsonPayload constructor and update usages --- .../Cache/DistributedCacheExtensions.cs | 24 ++- .../Implement/ContentCacheRefresher.cs | 23 ++- .../Implement/LanguageCacheRefresher.cs | 10 +- .../IndexingNotificationHandler.Content.cs | 12 +- .../PublishedSnapshotService.cs | 17 +- .../Umbraco.Core/Cache/RefresherTests.cs | 24 ++- ...PublishedSnapshotServiceCollectionTests.cs | 179 +++++++++++++++--- 7 files changed, 239 insertions(+), 50 deletions(-) diff --git a/src/Umbraco.Core/Cache/DistributedCacheExtensions.cs b/src/Umbraco.Core/Cache/DistributedCacheExtensions.cs index 8d792a5ef7..438c66a2c1 100644 --- a/src/Umbraco.Core/Cache/DistributedCacheExtensions.cs +++ b/src/Umbraco.Core/Cache/DistributedCacheExtensions.cs @@ -118,15 +118,35 @@ public static class DistributedCacheExtensions #region ContentCacheRefresher public static void RefreshAllContentCache(this DistributedCache dc) + { + ContentCacheRefresher.JsonPayload[] payloads = new[] + { + new ContentCacheRefresher.JsonPayload() + { + ChangeTypes = TreeChangeTypes.RefreshAll + } + }; + // note: refresh all content cache does refresh content types too - => dc.RefreshByPayload(ContentCacheRefresher.UniqueId, new ContentCacheRefresher.JsonPayload(0, null, TreeChangeTypes.RefreshAll).Yield()); + dc.RefreshByPayload(ContentCacheRefresher.UniqueId, payloads); + } [Obsolete("Use the overload accepting IEnumerable instead to avoid allocating arrays. This overload will be removed in Umbraco 13.")] public static void RefreshContentCache(this DistributedCache dc, TreeChange[] changes) => dc.RefreshContentCache(changes.AsEnumerable()); public static void RefreshContentCache(this DistributedCache dc, IEnumerable> changes) - => dc.RefreshByPayload(ContentCacheRefresher.UniqueId, changes.DistinctBy(x => (x.Item.Id, x.Item.Key, x.ChangeTypes)).Select(x => new ContentCacheRefresher.JsonPayload(x.Item.Id, x.Item.Key, x.ChangeTypes))); + { + IEnumerable payloads = changes.Select(x => new ContentCacheRefresher.JsonPayload() + { + Id = x.Item.Id, + Key = x.Item.Key, + ChangeTypes = x.ChangeTypes, + Blueprint = x.Item.Blueprint + }); + + dc.RefreshByPayload(ContentCacheRefresher.UniqueId, payloads); + } #endregion diff --git a/src/Umbraco.Core/Cache/Refreshers/Implement/ContentCacheRefresher.cs b/src/Umbraco.Core/Cache/Refreshers/Implement/ContentCacheRefresher.cs index a515d5c5d1..779b22fe68 100644 --- a/src/Umbraco.Core/Cache/Refreshers/Implement/ContentCacheRefresher.cs +++ b/src/Umbraco.Core/Cache/Refreshers/Implement/ContentCacheRefresher.cs @@ -84,8 +84,8 @@ public sealed class ContentCacheRefresher : PayloadCacheRefresherBase((k, v) => v.Path?.Contains(pathid) ?? false); } - // if the item is being completely removed, we need to refresh the domains cache if any domain was assigned to the content - if (payload.ChangeTypes.HasTypesAny(TreeChangeTypes.Remove)) + // if the item is not a blueprint and is being completely removed, we need to refresh the domains cache if any domain was assigned to the content + if (payload.Blueprint is false && payload.ChangeTypes.HasTypesAny(TreeChangeTypes.Remove)) { idsRemoved.Add(payload.Id); } @@ -120,7 +120,11 @@ public sealed class ContentCacheRefresher : PayloadCacheRefresherBase x.Blueprint is false)) + { + // Only notify if the payload contains actual (non-blueprint) contents + NotifyPublishedSnapshotService(_publishedSnapshotService, AppCaches, payloads); + } base.Refresh(payloads); } @@ -157,8 +161,13 @@ public sealed class ContentCacheRefresher : PayloadCacheRefresherBase(); - } - + deleteBatch ??= new HashSet(); deleteBatch.Add(payload.Id); } else if (payload.ChangeTypes.HasType(TreeChangeTypes.RefreshAll)) diff --git a/src/Umbraco.PublishedCache.NuCache/PublishedSnapshotService.cs b/src/Umbraco.PublishedCache.NuCache/PublishedSnapshotService.cs index 175cf766c5..8aa012d11f 100644 --- a/src/Umbraco.PublishedCache.NuCache/PublishedSnapshotService.cs +++ b/src/Umbraco.PublishedCache.NuCache/PublishedSnapshotService.cs @@ -220,7 +220,16 @@ internal class PublishedSnapshotService : IPublishedSnapshotService // they require. using (_contentStore.GetScopedWriteLock(_scopeProvider)) { - NotifyLocked(new[] { new ContentCacheRefresher.JsonPayload(0, null, TreeChangeTypes.RefreshAll) }, out _, out _); + NotifyLocked( + new[] + { + new ContentCacheRefresher.JsonPayload() + { + ChangeTypes = TreeChangeTypes.RefreshAll + } + }, + out _, + out _); } using (_mediaStore.GetScopedWriteLock(_scopeProvider)) @@ -885,6 +894,12 @@ internal class PublishedSnapshotService : IPublishedSnapshotService _logger.LogDebug("Notified {ChangeTypes} for content {ContentId}", payload.ChangeTypes, payload.Id); } + if (payload.Blueprint) + { + // Skip blueprints + continue; + } + if (payload.ChangeTypes.HasType(TreeChangeTypes.RefreshAll)) { using (IScope scope = _scopeProvider.CreateScope()) diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Cache/RefresherTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Cache/RefresherTests.cs index 92ef2b5c9f..e509742cb9 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Cache/RefresherTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Core/Cache/RefresherTests.cs @@ -18,8 +18,10 @@ public class RefresherTests { new MediaCacheRefresher.JsonPayload(1234, Guid.NewGuid(), TreeChangeTypes.None), }; + var json = JsonConvert.SerializeObject(source); var payload = JsonConvert.DeserializeObject(json); + Assert.AreEqual(source[0].Id, payload[0].Id); Assert.AreEqual(source[0].Key, payload[0].Key); Assert.AreEqual(source[0].ChangeTypes, payload[0].ChangeTypes); @@ -30,13 +32,21 @@ public class RefresherTests { ContentCacheRefresher.JsonPayload[] source = { - new ContentCacheRefresher.JsonPayload(1234, Guid.NewGuid(), TreeChangeTypes.None), + new ContentCacheRefresher.JsonPayload() + { + Id = 1234, + Key = Guid.NewGuid(), + ChangeTypes = TreeChangeTypes.None + } }; + var json = JsonConvert.SerializeObject(source); var payload = JsonConvert.DeserializeObject(json); + Assert.AreEqual(source[0].Id, payload[0].Id); Assert.AreEqual(source[0].Key, payload[0].Key); Assert.AreEqual(source[0].ChangeTypes, payload[0].ChangeTypes); + Assert.AreEqual(source[0].Blueprint, payload[0].Blueprint); } [Test] @@ -46,8 +56,10 @@ public class RefresherTests { new ContentTypeCacheRefresher.JsonPayload("xxx", 1234, ContentTypeChangeTypes.None), }; + var json = JsonConvert.SerializeObject(source); var payload = JsonConvert.DeserializeObject(json); + Assert.AreEqual(source[0].ItemType, payload[0].ItemType); Assert.AreEqual(source[0].Id, payload[0].Id); Assert.AreEqual(source[0].ChangeTypes, payload[0].ChangeTypes); @@ -60,8 +72,10 @@ public class RefresherTests { new DataTypeCacheRefresher.JsonPayload(1234, Guid.NewGuid(), true), }; + var json = JsonConvert.SerializeObject(source); var payload = JsonConvert.DeserializeObject(json); + Assert.AreEqual(source[0].Id, payload[0].Id); Assert.AreEqual(source[0].Key, payload[0].Key); Assert.AreEqual(source[0].Removed, payload[0].Removed); @@ -70,10 +84,14 @@ public class RefresherTests [Test] public void DomainCacheRefresherCanDeserializeJsonPayload() { - DomainCacheRefresher.JsonPayload[] - source = { new DomainCacheRefresher.JsonPayload(1234, DomainChangeTypes.None) }; + DomainCacheRefresher.JsonPayload[] source = + { + new DomainCacheRefresher.JsonPayload(1234, DomainChangeTypes.None) + }; + var json = JsonConvert.SerializeObject(source); var payload = JsonConvert.DeserializeObject(json); + Assert.AreEqual(source[0].Id, payload[0].Id); Assert.AreEqual(source[0].ChangeType, payload[0].ChangeType); } diff --git a/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/PublishedCache/PublishedSnapshotServiceCollectionTests.cs b/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/PublishedCache/PublishedSnapshotServiceCollectionTests.cs index a007079ca9..b516b4fffb 100644 --- a/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/PublishedCache/PublishedSnapshotServiceCollectionTests.cs +++ b/tests/Umbraco.Tests.UnitTests/Umbraco.Infrastructure/PublishedCache/PublishedSnapshotServiceCollectionTests.cs @@ -358,7 +358,16 @@ public class PublishedSnapshotServiceCollectionTests : PublishedSnapshotServiceT // notify SnapshotService.Notify( - new[] { new ContentCacheRefresher.JsonPayload(10, Guid.Empty, TreeChangeTypes.RefreshBranch) }, out _, out _); + new[] + { + new ContentCacheRefresher.JsonPayload() + { + Id = 10, + ChangeTypes = TreeChangeTypes.RefreshBranch + } + }, + out _, + out _); // changes that *I* make are immediately visible on the current snapshot var documents = snapshot.Content.GetAtRoot().ToArray(); @@ -392,7 +401,16 @@ public class PublishedSnapshotServiceCollectionTests : PublishedSnapshotServiceT // notify SnapshotService.Notify( - new[] { new ContentCacheRefresher.JsonPayload(1, Guid.Empty, TreeChangeTypes.RefreshBranch) }, out _, out _); + new[] + { + new ContentCacheRefresher.JsonPayload() + { + Id = 1, + ChangeTypes = TreeChangeTypes.RefreshBranch + } + }, + out _, + out _); // changes that *I* make are immediately visible on the current snapshot var documents = snapshot.Content.GetAtRoot().ToArray(); @@ -450,7 +468,11 @@ public class PublishedSnapshotServiceCollectionTests : PublishedSnapshotServiceT SnapshotService.Notify( new[] { - new ContentCacheRefresher.JsonPayload(kit.Node.ParentContentId, Guid.Empty, TreeChangeTypes.RefreshBranch), + new ContentCacheRefresher.JsonPayload() + { + Id = kit.Node.ParentContentId, + ChangeTypes = TreeChangeTypes.RefreshBranch + } }, out _, out _); @@ -516,11 +538,19 @@ public class PublishedSnapshotServiceCollectionTests : PublishedSnapshotServiceT // notify SnapshotService.Notify( new[] - { - // removal must come first - new ContentCacheRefresher.JsonPayload(2, Guid.Empty, TreeChangeTypes.RefreshBranch), - new ContentCacheRefresher.JsonPayload(1, Guid.Empty, TreeChangeTypes.RefreshBranch), - }, + { + // removal must come first + new ContentCacheRefresher.JsonPayload() + { + Id = 2, + ChangeTypes = TreeChangeTypes.RefreshBranch + }, + new ContentCacheRefresher.JsonPayload() + { + Id = 1, + ChangeTypes = TreeChangeTypes.RefreshBranch + } + }, out _, out _); @@ -571,7 +601,16 @@ public class PublishedSnapshotServiceCollectionTests : PublishedSnapshotServiceT // notify - which ensures there are 2 generations in the cache meaning each LinkedNode has a Next value. SnapshotService.Notify( - new[] { new ContentCacheRefresher.JsonPayload(4, Guid.Empty, TreeChangeTypes.RefreshBranch) }, out _, out _); + new[] + { + new ContentCacheRefresher.JsonPayload() + { + Id = 4, + ChangeTypes = TreeChangeTypes.RefreshBranch + } + }, + out _, + out _); // refresh the branch again, this used to show the issue where a null ref exception would occur // because in the ClearBranchLocked logic, when SetValueLocked was called within a recursive call @@ -579,7 +618,16 @@ public class PublishedSnapshotServiceCollectionTests : PublishedSnapshotServiceT // this value before recursing. Assert.DoesNotThrow(() => SnapshotService.Notify( - new[] { new ContentCacheRefresher.JsonPayload(4, Guid.Empty, TreeChangeTypes.RefreshBranch) }, out _, out _)); + new[] + { + new ContentCacheRefresher.JsonPayload() + { + Id = 4, + ChangeTypes = TreeChangeTypes.RefreshBranch + } + }, + out _, + out _)); } [Test] @@ -759,11 +807,23 @@ public class PublishedSnapshotServiceCollectionTests : PublishedSnapshotServiceT // notify SnapshotService.Notify( new[] - { - new ContentCacheRefresher.JsonPayload(3, Guid.Empty, TreeChangeTypes.Remove), // remove last - new ContentCacheRefresher.JsonPayload(5, Guid.Empty, TreeChangeTypes.Remove), // remove middle - new ContentCacheRefresher.JsonPayload(9, Guid.Empty, TreeChangeTypes.Remove), // remove first - }, + { + new ContentCacheRefresher.JsonPayload() // remove last + { + Id = 3, + ChangeTypes = TreeChangeTypes.Remove + }, + new ContentCacheRefresher.JsonPayload() // remove middle + { + Id = 5, + ChangeTypes = TreeChangeTypes.Remove + }, + new ContentCacheRefresher.JsonPayload() // remove first + { + Id = 9, + ChangeTypes = TreeChangeTypes.Remove + } + }, out _, out _); @@ -779,11 +839,23 @@ public class PublishedSnapshotServiceCollectionTests : PublishedSnapshotServiceT // notify SnapshotService.Notify( new[] - { - new ContentCacheRefresher.JsonPayload(1, Guid.Empty, TreeChangeTypes.Remove), // remove first - new ContentCacheRefresher.JsonPayload(8, Guid.Empty, TreeChangeTypes.Remove), // remove - new ContentCacheRefresher.JsonPayload(7, Guid.Empty, TreeChangeTypes.Remove), // remove - }, + { + new ContentCacheRefresher.JsonPayload() // remove first + { + Id = 1, + ChangeTypes = TreeChangeTypes.Remove + }, + new ContentCacheRefresher.JsonPayload() // remove + { + Id = 8, + ChangeTypes = TreeChangeTypes.Remove + }, + new ContentCacheRefresher.JsonPayload() // remove + { + Id = 7, + ChangeTypes = TreeChangeTypes.Remove + } + }, out _, out _); @@ -823,8 +895,16 @@ public class PublishedSnapshotServiceCollectionTests : PublishedSnapshotServiceT SnapshotService.Notify( new[] { - new ContentCacheRefresher.JsonPayload(1, Guid.Empty, TreeChangeTypes.RefreshBranch), - new ContentCacheRefresher.JsonPayload(2, Guid.Empty, TreeChangeTypes.RefreshNode), + new ContentCacheRefresher.JsonPayload() + { + Id = 1, + ChangeTypes = TreeChangeTypes.RefreshBranch + }, + new ContentCacheRefresher.JsonPayload() + { + Id = 2, + ChangeTypes = TreeChangeTypes.RefreshNode + } }, out _, out _); @@ -887,7 +967,17 @@ public class PublishedSnapshotServiceCollectionTests : PublishedSnapshotServiceT var parentNode = parentNodes[0]; AssertLinkedNode(parentNode.contentNode, -1, -1, -1, 2, 2); - SnapshotService.Notify(new[] { new ContentCacheRefresher.JsonPayload(2, Guid.Empty, TreeChangeTypes.Remove) }, out _, out _); + SnapshotService.Notify( + new[] + { + new ContentCacheRefresher.JsonPayload() + { + Id = 2, + ChangeTypes = TreeChangeTypes.Remove + } + }, + out _, + out _); parentNodes = contentStore.Test.GetValues(1); parentNode = parentNodes[0]; @@ -944,9 +1034,13 @@ public class PublishedSnapshotServiceCollectionTests : PublishedSnapshotServiceT SnapshotService.Notify( new[] - { - new ContentCacheRefresher.JsonPayload(3, Guid.Empty, TreeChangeTypes.Remove), // remove middle child - }, + { + new ContentCacheRefresher.JsonPayload() // remove middle child + { + Id = 3, + ChangeTypes = TreeChangeTypes.Remove + } + }, out _, out _); @@ -1013,7 +1107,16 @@ public class PublishedSnapshotServiceCollectionTests : PublishedSnapshotServiceT Assert.IsFalse(contentStore.Test.NextGen); SnapshotService.Notify( - new[] { new ContentCacheRefresher.JsonPayload(1, Guid.Empty, TreeChangeTypes.RefreshNode) }, out _, out _); + new[] + { + new ContentCacheRefresher.JsonPayload() + { + Id = 1, + ChangeTypes = TreeChangeTypes.RefreshNode + } + }, + out _, + out _); Assert.AreEqual(2, contentStore.Test.LiveGen); Assert.IsTrue(contentStore.Test.NextGen); @@ -1084,7 +1187,17 @@ public class PublishedSnapshotServiceCollectionTests : PublishedSnapshotServiceT published ? rootKit.PublishedData : null); NuCacheContentService.ContentKits[1] = kit; - SnapshotService.Notify(new[] { new ContentCacheRefresher.JsonPayload(1, Guid.Empty, changeType) }, out _, out _); + SnapshotService.Notify( + new[] + { + new ContentCacheRefresher.JsonPayload() + { + Id = 1, + ChangeTypes = changeType + } + }, + out _, + out _); Assert.AreEqual(assertGen, contentStore.Test.LiveGen); Assert.IsTrue(contentStore.Test.NextGen); @@ -1162,9 +1275,13 @@ public class PublishedSnapshotServiceCollectionTests : PublishedSnapshotServiceT SnapshotService.Notify( new[] - { - new ContentCacheRefresher.JsonPayload(3, Guid.Empty, TreeChangeTypes.RefreshBranch), // remove middle child - }, + { + new ContentCacheRefresher.JsonPayload() // remove middle child + { + Id = 3, + ChangeTypes = TreeChangeTypes.RefreshBranch + } + }, out _, out _);