From e434b01386e52650d17cab4fe51aa2213716c287 Mon Sep 17 00:00:00 2001 From: Warren Buckley Date: Tue, 26 May 2020 14:43:57 +0100 Subject: [PATCH 1/7] Adds in NC Property Component to deal with creating/adding missing keys --- .../Compose/NestedContentPropertyComponent.cs | 174 ++++++++++++++++++ .../Compose/NestedContentPropertyComposer.cs | 9 + src/Umbraco.Web/Umbraco.Web.csproj | 2 + 3 files changed, 185 insertions(+) create mode 100644 src/Umbraco.Web/Compose/NestedContentPropertyComponent.cs create mode 100644 src/Umbraco.Web/Compose/NestedContentPropertyComposer.cs diff --git a/src/Umbraco.Web/Compose/NestedContentPropertyComponent.cs b/src/Umbraco.Web/Compose/NestedContentPropertyComponent.cs new file mode 100644 index 0000000000..28c2a288f8 --- /dev/null +++ b/src/Umbraco.Web/Compose/NestedContentPropertyComponent.cs @@ -0,0 +1,174 @@ +using System; +using System.Linq; +using Newtonsoft.Json.Linq; +using Umbraco.Core; +using Umbraco.Core.Composing; +using Umbraco.Core.Events; +using Umbraco.Core.Models; +using Umbraco.Core.Services; +using Umbraco.Core.Services.Implement; +using Umbraco.Web.PropertyEditors; + +namespace Umbraco.Web.Compose +{ + public class NestedContentPropertyComponent : IComponent + { + public void Initialize() + { + ContentService.Copying += ContentService_Copying; + ContentService.Saving += ContentService_Saving; + ContentService.Publishing += ContentService_Publishing; + } + + private void ContentService_Copying(IContentService sender, CopyEventArgs e) + { + // When a content node contains nested content property + // Check if the copied node contains a nested content + var nestedContentProps = e.Copy.Properties.Where(x => x.PropertyType.PropertyEditorAlias == Constants.PropertyEditors.Aliases.NestedContent); + + // Each NC Property on a doctype + foreach (var nestedContentProp in nestedContentProps) + { + // A NC Prop may have one or more values due to cultures + var propVals = nestedContentProp.Values; + foreach (var cultureVal in propVals) + { + // Remove keys from published value & any nested NC's + var updatedPublishedVal = CreateNewNestedContentKeys(cultureVal.PublishedValue.ToString()); + cultureVal.PublishedValue = updatedPublishedVal; + + // Remove keys from edited/draft value & any nested NC's + var updatedEditedVal = CreateNewNestedContentKeys(cultureVal.EditedValue.ToString()); + cultureVal.EditedValue = updatedEditedVal; + } + } + } + + private void ContentService_Saving(IContentService sender, ContentSavingEventArgs e) + { + // One or more content nodes could be saved in a bulk publish + foreach(var entity in e.SavedEntities) + { + // When a content node contains nested content property + // Check if the copied node contains a nested content + var nestedContentProps = entity.Properties.Where(x => x.PropertyType.PropertyEditorAlias == Constants.PropertyEditors.Aliases.NestedContent); + + // Each NC Property on a doctype + foreach (var nestedContentProp in nestedContentProps) + { + // A NC Prop may have one or more values due to cultures + var propVals = nestedContentProp.Values; + foreach (var cultureVal in propVals) + { + // Remove keys from published value & any nested NC's + var updatedPublishedVal = CreateMissingNestedContentKeys(cultureVal.PublishedValue.ToString()); + cultureVal.PublishedValue = updatedPublishedVal; + + // Remove keys from edited/draft value & any nested NC's + var updatedEditedVal = CreateMissingNestedContentKeys(cultureVal.EditedValue.ToString()); + cultureVal.EditedValue = updatedEditedVal; + } + } + } + } + + private void ContentService_Publishing(IContentService sender, ContentPublishingEventArgs e) + { + // One or more content nodes could be saved in a bulk publish + foreach (var entity in e.PublishedEntities) + { + // When a content node contains nested content property + // Check if the copied node contains a nested content + var nestedContentProps = entity.Properties.Where(x => x.PropertyType.PropertyEditorAlias == Constants.PropertyEditors.Aliases.NestedContent); + + // Each NC Property on a doctype + foreach (var nestedContentProp in nestedContentProps) + { + // A NC Prop may have one or more values due to cultures + var propVals = nestedContentProp.Values; + foreach (var cultureVal in propVals) + { + // Remove keys from published value & any nested NC's + var updatedPublishedVal = CreateMissingNestedContentKeys(cultureVal.PublishedValue.ToString()); + cultureVal.PublishedValue = updatedPublishedVal; + + // Remove keys from edited/draft value & any nested NC's + var updatedEditedVal = CreateMissingNestedContentKeys(cultureVal.EditedValue.ToString()); + cultureVal.EditedValue = updatedEditedVal; + } + } + } + } + + public void Terminate() + { + ContentService.Copying -= ContentService_Copying; + ContentService.Saving -= ContentService_Saving; + ContentService.Publishing -= ContentService_Publishing; + } + + private string CreateNewNestedContentKeys(string ncJson) + { + // Try & convert JSON to JArray (two props we will know should exist are key & ncContentTypeAlias) + var ncItems = JArray.Parse(ncJson); + + // NC prop contains one or more items/rows of things + foreach (var nestedContentItem in ncItems.Children()) + { + foreach (var ncItemProp in nestedContentItem.Properties()) + { + if(ncItemProp.Name.ToLowerInvariant() == "key") + ncItemProp.Value = Guid.NewGuid().ToString(); + + // No need to check this property for JSON - as this is a JSON prop we know + // That onyl contains the string of the doctype alias used as the NC item + if (ncItemProp.Name == NestedContentPropertyEditor.ContentTypeAliasPropertyKey) + continue; + + // As we don't know what properties in the JSON may contain the nested NC + // We are detecting if its value stores JSON to help filter the list AND that in its JSON it has ncContentTypeAlias prop + if (ncItemProp.Value.ToString().DetectIsJson() && ncItemProp.Value.ToString().Contains(NestedContentPropertyEditor.ContentTypeAliasPropertyKey)) + { + // Recurse & update this JSON property + ncItemProp.Value = CreateNewNestedContentKeys(ncItemProp.Value.ToString()); + } + } + } + + return ncItems.ToString(); + } + + private string CreateMissingNestedContentKeys(string ncJson) + { + // Try & convert JSON to JArray (two props we will know should exist are key & ncContentTypeAlias) + var ncItems = JArray.Parse(ncJson); + + // NC prop contains one or more items/rows of things + foreach (var nestedContentItem in ncItems.Children()) + { + var ncKeyProp = nestedContentItem.Properties().SingleOrDefault(x => x.Name.ToLowerInvariant() == "key"); + if(ncKeyProp == null) + { + nestedContentItem.Properties().Append(new JProperty("key", Guid.NewGuid().ToString())); + } + + foreach (var ncItemProp in nestedContentItem.Properties()) + { + // No need to check this property for JSON (Its the key OR ncContentTypeAlias) which has no JSON + if (ncItemProp.Name == NestedContentPropertyEditor.ContentTypeAliasPropertyKey || ncItemProp.Name.ToLowerInvariant() == "key") + continue; + + // As we don't know what properties in the JSON may contain the nested NC + // We are detecting if its value stores JSON to help filter the list AND that in its JSON it has ncContentTypeAlias prop + if (ncItemProp.Value.ToString().DetectIsJson() && ncItemProp.Value.ToString().Contains(NestedContentPropertyEditor.ContentTypeAliasPropertyKey)) + { + // Recurse & update this JSON property + ncItemProp.Value = CreateMissingNestedContentKeys(ncItemProp.Value.ToString()); + } + } + } + + return ncItems.ToString(); + } + } +} diff --git a/src/Umbraco.Web/Compose/NestedContentPropertyComposer.cs b/src/Umbraco.Web/Compose/NestedContentPropertyComposer.cs new file mode 100644 index 0000000000..4c9d9dee1c --- /dev/null +++ b/src/Umbraco.Web/Compose/NestedContentPropertyComposer.cs @@ -0,0 +1,9 @@ +using Umbraco.Core; +using Umbraco.Core.Composing; + +namespace Umbraco.Web.Compose +{ + [RuntimeLevel(MinLevel = RuntimeLevel.Run)] + public class NestedContentPropertyComposer : ComponentComposer, ICoreComposer + { } +} diff --git a/src/Umbraco.Web/Umbraco.Web.csproj b/src/Umbraco.Web/Umbraco.Web.csproj index e39687bed8..c06ec574c5 100755 --- a/src/Umbraco.Web/Umbraco.Web.csproj +++ b/src/Umbraco.Web/Umbraco.Web.csproj @@ -129,6 +129,7 @@ + @@ -237,6 +238,7 @@ + From ff30a2769733db85e9f48dfcf8ee7523adf06e5a Mon Sep 17 00:00:00 2001 From: Claus Date: Thu, 28 May 2020 08:41:32 +0200 Subject: [PATCH 2/7] adding missing nullchecks. --- .../Compose/NestedContentPropertyComponent.cs | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/src/Umbraco.Web/Compose/NestedContentPropertyComponent.cs b/src/Umbraco.Web/Compose/NestedContentPropertyComponent.cs index 28c2a288f8..bff821358d 100644 --- a/src/Umbraco.Web/Compose/NestedContentPropertyComponent.cs +++ b/src/Umbraco.Web/Compose/NestedContentPropertyComponent.cs @@ -34,11 +34,11 @@ namespace Umbraco.Web.Compose foreach (var cultureVal in propVals) { // Remove keys from published value & any nested NC's - var updatedPublishedVal = CreateNewNestedContentKeys(cultureVal.PublishedValue.ToString()); + var updatedPublishedVal = CreateNewNestedContentKeys(cultureVal.PublishedValue?.ToString()); cultureVal.PublishedValue = updatedPublishedVal; // Remove keys from edited/draft value & any nested NC's - var updatedEditedVal = CreateNewNestedContentKeys(cultureVal.EditedValue.ToString()); + var updatedEditedVal = CreateNewNestedContentKeys(cultureVal.EditedValue?.ToString()); cultureVal.EditedValue = updatedEditedVal; } } @@ -61,11 +61,11 @@ namespace Umbraco.Web.Compose foreach (var cultureVal in propVals) { // Remove keys from published value & any nested NC's - var updatedPublishedVal = CreateMissingNestedContentKeys(cultureVal.PublishedValue.ToString()); + var updatedPublishedVal = CreateMissingNestedContentKeys(cultureVal.PublishedValue?.ToString()); cultureVal.PublishedValue = updatedPublishedVal; // Remove keys from edited/draft value & any nested NC's - var updatedEditedVal = CreateMissingNestedContentKeys(cultureVal.EditedValue.ToString()); + var updatedEditedVal = CreateMissingNestedContentKeys(cultureVal.EditedValue?.ToString()); cultureVal.EditedValue = updatedEditedVal; } } @@ -89,11 +89,11 @@ namespace Umbraco.Web.Compose foreach (var cultureVal in propVals) { // Remove keys from published value & any nested NC's - var updatedPublishedVal = CreateMissingNestedContentKeys(cultureVal.PublishedValue.ToString()); + var updatedPublishedVal = CreateMissingNestedContentKeys(cultureVal.PublishedValue?.ToString()); cultureVal.PublishedValue = updatedPublishedVal; // Remove keys from edited/draft value & any nested NC's - var updatedEditedVal = CreateMissingNestedContentKeys(cultureVal.EditedValue.ToString()); + var updatedEditedVal = CreateMissingNestedContentKeys(cultureVal.EditedValue?.ToString()); cultureVal.EditedValue = updatedEditedVal; } } @@ -109,6 +109,9 @@ namespace Umbraco.Web.Compose private string CreateNewNestedContentKeys(string ncJson) { + if (string.IsNullOrWhiteSpace(ncJson)) + return ncJson; + // Try & convert JSON to JArray (two props we will know should exist are key & ncContentTypeAlias) var ncItems = JArray.Parse(ncJson); @@ -140,6 +143,9 @@ namespace Umbraco.Web.Compose private string CreateMissingNestedContentKeys(string ncJson) { + if (string.IsNullOrWhiteSpace(ncJson)) + return ncJson; + // Try & convert JSON to JArray (two props we will know should exist are key & ncContentTypeAlias) var ncItems = JArray.Parse(ncJson); From c9ab2ca9ef8b19d3a0f1d408b2601193504cc9cb Mon Sep 17 00:00:00 2001 From: Warren Buckley Date: Thu, 28 May 2020 11:45:43 +0100 Subject: [PATCH 3/7] Use InvariantEquals --- src/Umbraco.Web/Compose/NestedContentPropertyComponent.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Umbraco.Web/Compose/NestedContentPropertyComponent.cs b/src/Umbraco.Web/Compose/NestedContentPropertyComponent.cs index bff821358d..e41c14d94b 100644 --- a/src/Umbraco.Web/Compose/NestedContentPropertyComponent.cs +++ b/src/Umbraco.Web/Compose/NestedContentPropertyComponent.cs @@ -120,7 +120,7 @@ namespace Umbraco.Web.Compose { foreach (var ncItemProp in nestedContentItem.Properties()) { - if(ncItemProp.Name.ToLowerInvariant() == "key") + if(ncItemProp.Name.InvariantEquals("key")) ncItemProp.Value = Guid.NewGuid().ToString(); // No need to check this property for JSON - as this is a JSON prop we know From c81268995ceaa352acbe55359783925d2be19f23 Mon Sep 17 00:00:00 2001 From: Warren Buckley Date: Thu, 28 May 2020 12:06:04 +0100 Subject: [PATCH 4/7] Remove code duplication --- .../Compose/NestedContentPropertyComponent.cs | 77 +++++++------------ 1 file changed, 29 insertions(+), 48 deletions(-) diff --git a/src/Umbraco.Web/Compose/NestedContentPropertyComponent.cs b/src/Umbraco.Web/Compose/NestedContentPropertyComponent.cs index e41c14d94b..28b503fc9b 100644 --- a/src/Umbraco.Web/Compose/NestedContentPropertyComponent.cs +++ b/src/Umbraco.Web/Compose/NestedContentPropertyComponent.cs @@ -34,11 +34,11 @@ namespace Umbraco.Web.Compose foreach (var cultureVal in propVals) { // Remove keys from published value & any nested NC's - var updatedPublishedVal = CreateNewNestedContentKeys(cultureVal.PublishedValue?.ToString()); + var updatedPublishedVal = CreateNestedContentKeys(cultureVal.PublishedValue?.ToString(), false); cultureVal.PublishedValue = updatedPublishedVal; // Remove keys from edited/draft value & any nested NC's - var updatedEditedVal = CreateNewNestedContentKeys(cultureVal.EditedValue?.ToString()); + var updatedEditedVal = CreateNestedContentKeys(cultureVal.EditedValue?.ToString(), false); cultureVal.EditedValue = updatedEditedVal; } } @@ -61,11 +61,11 @@ namespace Umbraco.Web.Compose foreach (var cultureVal in propVals) { // Remove keys from published value & any nested NC's - var updatedPublishedVal = CreateMissingNestedContentKeys(cultureVal.PublishedValue?.ToString()); + var updatedPublishedVal = CreateNestedContentKeys(cultureVal.PublishedValue?.ToString(), true); cultureVal.PublishedValue = updatedPublishedVal; // Remove keys from edited/draft value & any nested NC's - var updatedEditedVal = CreateMissingNestedContentKeys(cultureVal.EditedValue?.ToString()); + var updatedEditedVal = CreateNestedContentKeys(cultureVal.EditedValue?.ToString(), true); cultureVal.EditedValue = updatedEditedVal; } } @@ -89,11 +89,11 @@ namespace Umbraco.Web.Compose foreach (var cultureVal in propVals) { // Remove keys from published value & any nested NC's - var updatedPublishedVal = CreateMissingNestedContentKeys(cultureVal.PublishedValue?.ToString()); + var updatedPublishedVal = CreateNestedContentKeys(cultureVal.PublishedValue?.ToString(), true); cultureVal.PublishedValue = updatedPublishedVal; // Remove keys from edited/draft value & any nested NC's - var updatedEditedVal = CreateMissingNestedContentKeys(cultureVal.EditedValue?.ToString()); + var updatedEditedVal = CreateNestedContentKeys(cultureVal.EditedValue?.ToString(), true); cultureVal.EditedValue = updatedEditedVal; } } @@ -107,21 +107,36 @@ namespace Umbraco.Web.Compose ContentService.Publishing -= ContentService_Publishing; } - private string CreateNewNestedContentKeys(string ncJson) + private string CreateNestedContentKeys(string ncJson, bool onlyMissingKeys) { if (string.IsNullOrWhiteSpace(ncJson)) return ncJson; - // Try & convert JSON to JArray (two props we will know should exist are key & ncContentTypeAlias) + // Convert JSON to JArray (two props we will know should exist are key & ncContentTypeAlias) var ncItems = JArray.Parse(ncJson); // NC prop contains one or more items/rows of things foreach (var nestedContentItem in ncItems.Children()) { + // If saving/publishing - we only generate keys for NC items that are missing + if (onlyMissingKeys) + { + var ncKeyProp = nestedContentItem.Properties().SingleOrDefault(x => x.Name.ToLowerInvariant() == "key"); + if (ncKeyProp == null) + { + nestedContentItem.Properties().Append(new JProperty("key", Guid.NewGuid().ToString())); + } + } + + foreach (var ncItemProp in nestedContentItem.Properties()) { - if(ncItemProp.Name.InvariantEquals("key")) - ncItemProp.Value = Guid.NewGuid().ToString(); + if(onlyMissingKeys == false) + { + // Only when copying a node - we generate new keys for all NC items + if (ncItemProp.Name.InvariantEquals("key")) + ncItemProp.Value = Guid.NewGuid().ToString(); + } // No need to check this property for JSON - as this is a JSON prop we know // That onyl contains the string of the doctype alias used as the NC item @@ -130,46 +145,12 @@ namespace Umbraco.Web.Compose // As we don't know what properties in the JSON may contain the nested NC // We are detecting if its value stores JSON to help filter the list AND that in its JSON it has ncContentTypeAlias prop - if (ncItemProp.Value.ToString().DetectIsJson() && ncItemProp.Value.ToString().Contains(NestedContentPropertyEditor.ContentTypeAliasPropertyKey)) + var ncItemPropVal = ncItemProp.Value?.ToString(); + + if (ncItemPropVal.DetectIsJson() && ncItemPropVal.Contains(NestedContentPropertyEditor.ContentTypeAliasPropertyKey)) { // Recurse & update this JSON property - ncItemProp.Value = CreateNewNestedContentKeys(ncItemProp.Value.ToString()); - } - } - } - - return ncItems.ToString(); - } - - private string CreateMissingNestedContentKeys(string ncJson) - { - if (string.IsNullOrWhiteSpace(ncJson)) - return ncJson; - - // Try & convert JSON to JArray (two props we will know should exist are key & ncContentTypeAlias) - var ncItems = JArray.Parse(ncJson); - - // NC prop contains one or more items/rows of things - foreach (var nestedContentItem in ncItems.Children()) - { - var ncKeyProp = nestedContentItem.Properties().SingleOrDefault(x => x.Name.ToLowerInvariant() == "key"); - if(ncKeyProp == null) - { - nestedContentItem.Properties().Append(new JProperty("key", Guid.NewGuid().ToString())); - } - - foreach (var ncItemProp in nestedContentItem.Properties()) - { - // No need to check this property for JSON (Its the key OR ncContentTypeAlias) which has no JSON - if (ncItemProp.Name == NestedContentPropertyEditor.ContentTypeAliasPropertyKey || ncItemProp.Name.ToLowerInvariant() == "key") - continue; - - // As we don't know what properties in the JSON may contain the nested NC - // We are detecting if its value stores JSON to help filter the list AND that in its JSON it has ncContentTypeAlias prop - if (ncItemProp.Value.ToString().DetectIsJson() && ncItemProp.Value.ToString().Contains(NestedContentPropertyEditor.ContentTypeAliasPropertyKey)) - { - // Recurse & update this JSON property - ncItemProp.Value = CreateMissingNestedContentKeys(ncItemProp.Value.ToString()); + ncItemProp.Value = CreateNestedContentKeys(ncItemPropVal, onlyMissingKeys); } } } From 732d574ec2170a1c53801827dc7271b1ee6029f5 Mon Sep 17 00:00:00 2001 From: Warren Buckley Date: Thu, 28 May 2020 21:35:39 +0100 Subject: [PATCH 5/7] Tidy up & better checking for NC JSON with JSONPath & SelectTokens() Less code duplication. No need for Publishing event as Saving event is called before publishing --- .../Compose/NestedContentPropertyComponent.cs | 129 +++++++----------- 1 file changed, 48 insertions(+), 81 deletions(-) diff --git a/src/Umbraco.Web/Compose/NestedContentPropertyComponent.cs b/src/Umbraco.Web/Compose/NestedContentPropertyComponent.cs index 28b503fc9b..6e4fdb386e 100644 --- a/src/Umbraco.Web/Compose/NestedContentPropertyComponent.cs +++ b/src/Umbraco.Web/Compose/NestedContentPropertyComponent.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Linq; using Newtonsoft.Json.Linq; using Umbraco.Core; @@ -17,7 +18,6 @@ namespace Umbraco.Web.Compose { ContentService.Copying += ContentService_Copying; ContentService.Saving += ContentService_Saving; - ContentService.Publishing += ContentService_Publishing; } private void ContentService_Copying(IContentService sender, CopyEventArgs e) @@ -25,7 +25,29 @@ namespace Umbraco.Web.Compose // When a content node contains nested content property // Check if the copied node contains a nested content var nestedContentProps = e.Copy.Properties.Where(x => x.PropertyType.PropertyEditorAlias == Constants.PropertyEditors.Aliases.NestedContent); + UpdateNestedContentProperties(nestedContentProps, false); + } + private void ContentService_Saving(IContentService sender, ContentSavingEventArgs e) + { + // One or more content nodes could be saved in a bulk publish + foreach (var entity in e.SavedEntities) + { + // When a content node contains nested content property + // Check if the copied node contains a nested content + var nestedContentProps = entity.Properties.Where(x => x.PropertyType.PropertyEditorAlias == Constants.PropertyEditors.Aliases.NestedContent); + UpdateNestedContentProperties(nestedContentProps, true); + } + } + + public void Terminate() + { + ContentService.Copying -= ContentService_Copying; + ContentService.Saving -= ContentService_Saving; + } + + private void UpdateNestedContentProperties(IEnumerable nestedContentProps, bool onlyMissingKeys) + { // Each NC Property on a doctype foreach (var nestedContentProp in nestedContentProps) { @@ -34,104 +56,40 @@ namespace Umbraco.Web.Compose foreach (var cultureVal in propVals) { // Remove keys from published value & any nested NC's - var updatedPublishedVal = CreateNestedContentKeys(cultureVal.PublishedValue?.ToString(), false); + var updatedPublishedVal = CreateNestedContentKeys(cultureVal.PublishedValue?.ToString(), onlyMissingKeys); cultureVal.PublishedValue = updatedPublishedVal; // Remove keys from edited/draft value & any nested NC's - var updatedEditedVal = CreateNestedContentKeys(cultureVal.EditedValue?.ToString(), false); + var updatedEditedVal = CreateNestedContentKeys(cultureVal.EditedValue?.ToString(), onlyMissingKeys); cultureVal.EditedValue = updatedEditedVal; } } } - private void ContentService_Saving(IContentService sender, ContentSavingEventArgs e) + private string CreateNestedContentKeys(string rawJson, bool onlyMissingKeys) { - // One or more content nodes could be saved in a bulk publish - foreach(var entity in e.SavedEntities) - { - // When a content node contains nested content property - // Check if the copied node contains a nested content - var nestedContentProps = entity.Properties.Where(x => x.PropertyType.PropertyEditorAlias == Constants.PropertyEditors.Aliases.NestedContent); + if (string.IsNullOrWhiteSpace(rawJson)) + return rawJson; - // Each NC Property on a doctype - foreach (var nestedContentProp in nestedContentProps) - { - // A NC Prop may have one or more values due to cultures - var propVals = nestedContentProp.Values; - foreach (var cultureVal in propVals) - { - // Remove keys from published value & any nested NC's - var updatedPublishedVal = CreateNestedContentKeys(cultureVal.PublishedValue?.ToString(), true); - cultureVal.PublishedValue = updatedPublishedVal; - - // Remove keys from edited/draft value & any nested NC's - var updatedEditedVal = CreateNestedContentKeys(cultureVal.EditedValue?.ToString(), true); - cultureVal.EditedValue = updatedEditedVal; - } - } - } - } - - private void ContentService_Publishing(IContentService sender, ContentPublishingEventArgs e) - { - // One or more content nodes could be saved in a bulk publish - foreach (var entity in e.PublishedEntities) - { - // When a content node contains nested content property - // Check if the copied node contains a nested content - var nestedContentProps = entity.Properties.Where(x => x.PropertyType.PropertyEditorAlias == Constants.PropertyEditors.Aliases.NestedContent); - - // Each NC Property on a doctype - foreach (var nestedContentProp in nestedContentProps) - { - // A NC Prop may have one or more values due to cultures - var propVals = nestedContentProp.Values; - foreach (var cultureVal in propVals) - { - // Remove keys from published value & any nested NC's - var updatedPublishedVal = CreateNestedContentKeys(cultureVal.PublishedValue?.ToString(), true); - cultureVal.PublishedValue = updatedPublishedVal; - - // Remove keys from edited/draft value & any nested NC's - var updatedEditedVal = CreateNestedContentKeys(cultureVal.EditedValue?.ToString(), true); - cultureVal.EditedValue = updatedEditedVal; - } - } - } - } - - public void Terminate() - { - ContentService.Copying -= ContentService_Copying; - ContentService.Saving -= ContentService_Saving; - ContentService.Publishing -= ContentService_Publishing; - } - - private string CreateNestedContentKeys(string ncJson, bool onlyMissingKeys) - { - if (string.IsNullOrWhiteSpace(ncJson)) - return ncJson; - - // Convert JSON to JArray (two props we will know should exist are key & ncContentTypeAlias) - var ncItems = JArray.Parse(ncJson); + // Parse JSON + var ncJson = JToken.Parse(rawJson); // NC prop contains one or more items/rows of things - foreach (var nestedContentItem in ncItems.Children()) + foreach (var nestedContentItem in ncJson.Children()) { // If saving/publishing - we only generate keys for NC items that are missing if (onlyMissingKeys) { - var ncKeyProp = nestedContentItem.Properties().SingleOrDefault(x => x.Name.ToLowerInvariant() == "key"); + var ncKeyProp = nestedContentItem.Properties().SingleOrDefault(x => x.Name.InvariantEquals("key")); if (ncKeyProp == null) { nestedContentItem.Properties().Append(new JProperty("key", Guid.NewGuid().ToString())); } } - foreach (var ncItemProp in nestedContentItem.Properties()) { - if(onlyMissingKeys == false) + if (onlyMissingKeys == false) { // Only when copying a node - we generate new keys for all NC items if (ncItemProp.Name.InvariantEquals("key")) @@ -139,23 +97,32 @@ namespace Umbraco.Web.Compose } // No need to check this property for JSON - as this is a JSON prop we know - // That onyl contains the string of the doctype alias used as the NC item + // That only contains the string of the doctype alias used as the NC item if (ncItemProp.Name == NestedContentPropertyEditor.ContentTypeAliasPropertyKey) continue; - // As we don't know what properties in the JSON may contain the nested NC + // As we don't know what properties in the JSON may contain other complex editors or deep nested NC // We are detecting if its value stores JSON to help filter the list AND that in its JSON it has ncContentTypeAlias prop var ncItemPropVal = ncItemProp.Value?.ToString(); - if (ncItemPropVal.DetectIsJson() && ncItemPropVal.Contains(NestedContentPropertyEditor.ContentTypeAliasPropertyKey)) + if (ncItemPropVal.DetectIsJson()) { - // Recurse & update this JSON property - ncItemProp.Value = CreateNestedContentKeys(ncItemPropVal, onlyMissingKeys); + // Parse the nested JSON (complex editor) + var complexEditorJson = JToken.Parse(ncItemPropVal); + + // Verify the complex editor is nested content (Will have one or more ncContentTypeAlias) + // One for each NC item/row + var hasNestedContentJsonProp = complexEditorJson.SelectTokens($"$..['{NestedContentPropertyEditor.ContentTypeAliasPropertyKey}']", false); + if (hasNestedContentJsonProp.Count() > 0) + { + // Recurse & update this JSON property + ncItemProp.Value = CreateNestedContentKeys(ncItemPropVal, onlyMissingKeys); + } } } } - return ncItems.ToString(); + return ncJson.ToString(); } } } From 2565a626cb60cf108e3b254e50c2d02e1935dea1 Mon Sep 17 00:00:00 2001 From: Shannon Date: Fri, 29 May 2020 13:01:56 +1000 Subject: [PATCH 6/7] Fixes NC key replacement and adds tests --- src/Umbraco.Tests/Umbraco.Tests.csproj | 1 + .../Compose/NestedContentPropertyComponent.cs | 86 +++++++++---------- 2 files changed, 43 insertions(+), 44 deletions(-) diff --git a/src/Umbraco.Tests/Umbraco.Tests.csproj b/src/Umbraco.Tests/Umbraco.Tests.csproj index 3c359cdde8..731dc05363 100644 --- a/src/Umbraco.Tests/Umbraco.Tests.csproj +++ b/src/Umbraco.Tests/Umbraco.Tests.csproj @@ -148,6 +148,7 @@ + diff --git a/src/Umbraco.Web/Compose/NestedContentPropertyComponent.cs b/src/Umbraco.Web/Compose/NestedContentPropertyComponent.cs index 6e4fdb386e..5794a2734e 100644 --- a/src/Umbraco.Web/Compose/NestedContentPropertyComponent.cs +++ b/src/Umbraco.Web/Compose/NestedContentPropertyComponent.cs @@ -66,63 +66,61 @@ namespace Umbraco.Web.Compose } } - private string CreateNestedContentKeys(string rawJson, bool onlyMissingKeys) + + // internal for tests + internal string CreateNestedContentKeys(string rawJson, bool onlyMissingKeys, Func createGuid = null) { - if (string.IsNullOrWhiteSpace(rawJson)) + // used so we can test nicely + if (createGuid == null) + createGuid = () => Guid.NewGuid(); + + if (string.IsNullOrWhiteSpace(rawJson) || !rawJson.DetectIsJson()) return rawJson; - // Parse JSON - var ncJson = JToken.Parse(rawJson); + // Parse JSON + var complexEditorValue = JToken.Parse(rawJson); - // NC prop contains one or more items/rows of things - foreach (var nestedContentItem in ncJson.Children()) + UpdateNestedContentKeysRecursively(complexEditorValue, onlyMissingKeys, createGuid); + + return complexEditorValue.ToString(); + } + + private void UpdateNestedContentKeysRecursively(JToken json, bool onlyMissingKeys, Func createGuid) + { + // check if this is NC + var isNestedContent = json.SelectTokens($"$..['{NestedContentPropertyEditor.ContentTypeAliasPropertyKey}']", false).Any(); + + // select all values (flatten) + var allProperties = json.SelectTokens("$..*").OfType().Select(x => x.Parent as JProperty).WhereNotNull().ToList(); + foreach (var prop in allProperties) { - // If saving/publishing - we only generate keys for NC items that are missing - if (onlyMissingKeys) + if (prop.Name == NestedContentPropertyEditor.ContentTypeAliasPropertyKey) { - var ncKeyProp = nestedContentItem.Properties().SingleOrDefault(x => x.Name.InvariantEquals("key")); - if (ncKeyProp == null) + // get it's sibling 'key' property + var ncKeyVal = prop.Parent["key"] as JValue; + // TODO: This bool seems odd, if the key is null, shouldn't we fill it in regardless of onlyMissingKeys? + if ((onlyMissingKeys && ncKeyVal == null) || (!onlyMissingKeys && ncKeyVal != null)) { - nestedContentItem.Properties().Append(new JProperty("key", Guid.NewGuid().ToString())); - } + // create or replace + prop.Parent["key"] = createGuid().ToString(); + } } - - foreach (var ncItemProp in nestedContentItem.Properties()) + else if (!isNestedContent || prop.Name != "key") { - if (onlyMissingKeys == false) + // this is an arbitrary property that could contain a nested complex editor + var propVal = prop.Value?.ToString(); + // check if this might contain a nested NC + if (!propVal.IsNullOrWhiteSpace() && propVal.DetectIsJson() && propVal.InvariantContains(NestedContentPropertyEditor.ContentTypeAliasPropertyKey)) { - // Only when copying a node - we generate new keys for all NC items - if (ncItemProp.Name.InvariantEquals("key")) - ncItemProp.Value = Guid.NewGuid().ToString(); - } - - // No need to check this property for JSON - as this is a JSON prop we know - // That only contains the string of the doctype alias used as the NC item - if (ncItemProp.Name == NestedContentPropertyEditor.ContentTypeAliasPropertyKey) - continue; - - // As we don't know what properties in the JSON may contain other complex editors or deep nested NC - // We are detecting if its value stores JSON to help filter the list AND that in its JSON it has ncContentTypeAlias prop - var ncItemPropVal = ncItemProp.Value?.ToString(); - - if (ncItemPropVal.DetectIsJson()) - { - // Parse the nested JSON (complex editor) - var complexEditorJson = JToken.Parse(ncItemPropVal); - - // Verify the complex editor is nested content (Will have one or more ncContentTypeAlias) - // One for each NC item/row - var hasNestedContentJsonProp = complexEditorJson.SelectTokens($"$..['{NestedContentPropertyEditor.ContentTypeAliasPropertyKey}']", false); - if (hasNestedContentJsonProp.Count() > 0) - { - // Recurse & update this JSON property - ncItemProp.Value = CreateNestedContentKeys(ncItemPropVal, onlyMissingKeys); - } + // recurse + var parsed = JToken.Parse(propVal); + UpdateNestedContentKeysRecursively(parsed, onlyMissingKeys, createGuid); + // set the value to the updated one + prop.Value = parsed.ToString(); } } } - - return ncJson.ToString(); } + } } From b1d0cbf1d0a99479cdf7a0ce0c75d5442869155e Mon Sep 17 00:00:00 2001 From: Warren Buckley Date: Fri, 29 May 2020 09:30:34 +0100 Subject: [PATCH 7/7] Remove the CSProj reference to missing test file in repo - until we get a chance to add it back in #8199 --- src/Umbraco.Tests/Umbraco.Tests.csproj | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Umbraco.Tests/Umbraco.Tests.csproj b/src/Umbraco.Tests/Umbraco.Tests.csproj index 731dc05363..3c359cdde8 100644 --- a/src/Umbraco.Tests/Umbraco.Tests.csproj +++ b/src/Umbraco.Tests/Umbraco.Tests.csproj @@ -148,7 +148,6 @@ -