Gets copying logic working for the block editor, needs more tests though

This commit is contained in:
Shannon
2020-09-08 02:07:02 +10:00
parent 40d13bfb2f
commit a2c24bcc76
14 changed files with 511 additions and 49 deletions

View File

@@ -0,0 +1,187 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Umbraco.Core;
using Umbraco.Core.Composing;
using Umbraco.Core.Events;
using Umbraco.Core.Models;
using Umbraco.Core.Models.Blocks;
using Umbraco.Core.PropertyEditors;
using Umbraco.Core.Services;
using Umbraco.Core.Services.Implement;
using Umbraco.Web.PropertyEditors;
namespace Umbraco.Web.Compose
{
/// <summary>
/// A component for Block editors used to bind to events
/// </summary>
public class BlockEditorComponent : IComponent
{
private ComplexPropertyEditorContentEventHandler _handler;
private readonly BlockListEditorDataConverter _converter = new BlockListEditorDataConverter();
public void Initialize()
{
_handler = new ComplexPropertyEditorContentEventHandler(
Constants.PropertyEditors.Aliases.BlockList,
CreateNestedContentKeys);
}
public void Terminate() => _handler?.Dispose();
private string CreateNestedContentKeys(string rawJson, bool onlyMissingKeys) => CreateNestedContentKeys(rawJson, onlyMissingKeys, null);
// internal for tests
internal string CreateNestedContentKeys(string rawJson, bool onlyMissingKeys, Func<Guid> createGuid = null, JsonSerializerSettings serializerSettings = null)
{
// used so we can test nicely
if (createGuid == null)
createGuid = () => Guid.NewGuid();
if (string.IsNullOrWhiteSpace(rawJson) || !rawJson.DetectIsJson())
return rawJson;
// Parse JSON
var blockListValue = _converter.Deserialize(rawJson);
UpdateBlockListRecursively(blockListValue, onlyMissingKeys, createGuid, serializerSettings);
return JsonConvert.SerializeObject(blockListValue.BlockValue, serializerSettings);
}
private void UpdateBlockListRecursively(BlockEditorData blockListData, bool onlyMissingKeys, Func<Guid> createGuid, JsonSerializerSettings serializerSettings)
{
var oldToNew = new Dictionary<Udi, Udi>();
MapOldToNewUdis(oldToNew, blockListData.BlockValue.ContentData, onlyMissingKeys, createGuid);
MapOldToNewUdis(oldToNew, blockListData.BlockValue.SettingsData, onlyMissingKeys, createGuid);
for (var i = 0; i < blockListData.References.Count; i++)
{
var reference = blockListData.References[i];
var hasContentMap = oldToNew.TryGetValue(reference.ContentUdi, out var contentMap);
Udi settingsMap = null;
var hasSettingsMap = reference.SettingsUdi != null && oldToNew.TryGetValue(reference.SettingsUdi, out settingsMap);
if (hasContentMap)
{
// replace the reference
blockListData.References.RemoveAt(i);
blockListData.References.Insert(i, new ContentAndSettingsReference(contentMap, hasSettingsMap ? settingsMap : null));
}
}
// build the layout with the new UDIs
var layout = (JArray)blockListData.Layout;
layout.Clear();
foreach (var reference in blockListData.References)
{
layout.Add(JObject.FromObject(new BlockListLayoutItem
{
ContentUdi = reference.ContentUdi,
SettingsUdi = reference.SettingsUdi
}));
}
RecursePropertyValues(blockListData.BlockValue.ContentData, onlyMissingKeys, createGuid, serializerSettings);
RecursePropertyValues(blockListData.BlockValue.SettingsData, onlyMissingKeys, createGuid, serializerSettings);
}
private void RecursePropertyValues(IEnumerable<BlockItemData> blockData, bool onlyMissingKeys, Func<Guid> createGuid, JsonSerializerSettings serializerSettings)
{
foreach (var data in blockData)
{
// check if we need to recurse (make a copy of the dictionary since it will be modified)
foreach (var propertyAliasToBlockItemData in new Dictionary<string, object>(data.RawPropertyValues))
{
var asString = propertyAliasToBlockItemData.Value?.ToString();
//// if this is a nested block list
//if (propertyAliasToBlockItemData.Value.PropertyType.PropertyEditorAlias == Constants.PropertyEditors.Aliases.BlockList)
//{
// // recurse
// var blockListValue = _converter.Deserialize(asString);
// UpdateBlockListRecursively(blockListValue, onlyMissingKeys, createGuid);
// // set new value
// data.RawPropertyValues[propertyAliasToBlockItemData.Key] = JsonConvert.SerializeObject(blockListValue.BlockValue);
//}
if (asString != null && asString.DetectIsJson())
{
// this gets a little ugly because there could be some other complex editor that contains another block editor
// and since we would have no idea how to parse that, all we can do is try JSON Path to find another block editor
// of our type
var json = JToken.Parse(asString);
// select all tokens (flatten)
var allProperties = json.SelectTokens("$..*").Select(x => x.Parent as JProperty).WhereNotNull().ToList();
foreach (var prop in allProperties)
{
if (prop.Name == Constants.PropertyEditors.Aliases.BlockList)
{
// get it's parent 'layout' and it's parent's container
var layout = prop.Parent?.Parent as JProperty;
if (layout != null && layout.Parent is JObject layoutJson)
{
// recurse
var blockListValue = _converter.ConvertFrom(layoutJson);
UpdateBlockListRecursively(blockListValue, onlyMissingKeys, createGuid, serializerSettings);
// set new value
if (layoutJson.Parent != null)
{
// we can replace the sub string
layoutJson.Replace(JsonConvert.SerializeObject(blockListValue.BlockValue, serializerSettings));
}
else
{
// this was the root string
data.RawPropertyValues[propertyAliasToBlockItemData.Key] = JsonConvert.SerializeObject(blockListValue.BlockValue, serializerSettings);
}
}
}
else if (prop.Name != "layout" && prop.Name != "contentData" && prop.Name != "settingsData" && prop.Name != "contentTypeKey")
{
// this is an arbitrary property that could contain a nested complex editor
var propVal = prop.Value?.ToString();
// check if this might contain a nested Block Editor
if (!propVal.IsNullOrWhiteSpace() && propVal.DetectIsJson() && propVal.InvariantContains(Constants.PropertyEditors.Aliases.BlockList))
{
if (_converter.TryDeserialize(propVal, out var nestedBlockData))
{
// recurse
UpdateBlockListRecursively(nestedBlockData, onlyMissingKeys, createGuid, serializerSettings);
// set the value to the updated one
prop.Value = JsonConvert.SerializeObject(nestedBlockData.BlockValue, serializerSettings);
}
}
}
}
}
}
}
}
private void MapOldToNewUdis(Dictionary<Udi, Udi> oldToNew, IEnumerable<BlockItemData> blockData, bool onlyMissingKeys, Func<Guid> createGuid)
{
foreach (var data in blockData)
{
if (data.Udi == null)
throw new InvalidOperationException("Block data cannot contain a null UDI");
// replace the UDIs
if (!onlyMissingKeys)
{
var newUdi = GuidUdi.Create(Constants.UdiEntityType.Element, createGuid());
oldToNew[data.Udi] = newUdi;
data.Udi = newUdi;
}
}
}
}
}

View File

@@ -0,0 +1,12 @@
using Umbraco.Core;
using Umbraco.Core.Composing;
namespace Umbraco.Web.Compose
{
/// <summary>
/// A composer for Block editors to run a component
/// </summary>
[RuntimeLevel(MinLevel = RuntimeLevel.Run)]
public class BlockEditorComposer : ComponentComposer<NestedContentPropertyComponent>, ICoreComposer
{ }
}

View File

@@ -6,67 +6,32 @@ using Umbraco.Core;
using Umbraco.Core.Composing;
using Umbraco.Core.Events;
using Umbraco.Core.Models;
using Umbraco.Core.PropertyEditors;
using Umbraco.Core.Services;
using Umbraco.Core.Services.Implement;
using Umbraco.Web.PropertyEditors;
namespace Umbraco.Web.Compose
{
/// <summary>
/// A component for NestedContent used to bind to events
/// </summary>
public class NestedContentPropertyComponent : IComponent
{
private ComplexPropertyEditorContentEventHandler _handler;
public void Initialize()
{
ContentService.Copying += ContentService_Copying;
ContentService.Saving += ContentService_Saving;
_handler = new ComplexPropertyEditorContentEventHandler(
Constants.PropertyEditors.Aliases.NestedContent,
CreateNestedContentKeys);
}
private void ContentService_Copying(IContentService sender, CopyEventArgs<IContent> e)
{
// When a content node contains nested content property
// Check if the copied node contains a nested content
var nestedContentProps = e.Copy.Properties.Where(x => x.PropertyType.PropertyEditorAlias == Constants.PropertyEditors.Aliases.NestedContent);
UpdateNestedContentProperties(nestedContentProps, false);
}
public void Terminate() => _handler?.Dispose();
private void ContentService_Saving(IContentService sender, ContentSavingEventArgs e)
{
// One or more content nodes could be saved in a bulk publish
foreach (var entity in e.SavedEntities)
{
// When a content node contains nested content property
// Check if the copied node contains a nested content
var nestedContentProps = entity.Properties.Where(x => x.PropertyType.PropertyEditorAlias == Constants.PropertyEditors.Aliases.NestedContent);
UpdateNestedContentProperties(nestedContentProps, true);
}
}
private string CreateNestedContentKeys(string rawJson, bool onlyMissingKeys) => CreateNestedContentKeys(rawJson, onlyMissingKeys, null);
public void Terminate()
{
ContentService.Copying -= ContentService_Copying;
ContentService.Saving -= ContentService_Saving;
}
private void UpdateNestedContentProperties(IEnumerable<Property> nestedContentProps, bool onlyMissingKeys)
{
// Each NC Property on a doctype
foreach (var nestedContentProp in nestedContentProps)
{
// A NC Prop may have one or more values due to cultures
var propVals = nestedContentProp.Values;
foreach (var cultureVal in propVals)
{
// Remove keys from published value & any nested NC's
var updatedPublishedVal = CreateNestedContentKeys(cultureVal.PublishedValue?.ToString(), onlyMissingKeys);
cultureVal.PublishedValue = updatedPublishedVal;
// Remove keys from edited/draft value & any nested NC's
var updatedEditedVal = CreateNestedContentKeys(cultureVal.EditedValue?.ToString(), onlyMissingKeys);
cultureVal.EditedValue = updatedEditedVal;
}
}
}
// internal for tests
internal string CreateNestedContentKeys(string rawJson, bool onlyMissingKeys, Func<Guid> createGuid = null)
{
@@ -98,7 +63,6 @@ namespace Umbraco.Web.Compose
{
// get it's sibling 'key' property
var ncKeyVal = prop.Parent["key"] as JValue;
// TODO: This bool seems odd, if the key is null, shouldn't we fill it in regardless of onlyMissingKeys?
if ((onlyMissingKeys && ncKeyVal == null) || (!onlyMissingKeys && ncKeyVal != null))
{
// create or replace

View File

@@ -3,6 +3,9 @@ using Umbraco.Core.Composing;
namespace Umbraco.Web.Compose
{
/// <summary>
/// A composer for nested content to run a component
/// </summary>
[RuntimeLevel(MinLevel = RuntimeLevel.Run)]
public class NestedContentPropertyComposer : ComponentComposer<NestedContentPropertyComponent>, ICoreComposer
{ }

View File

@@ -130,6 +130,8 @@
<Compile Include="Cache\UserGroupCacheRefresher.cs" />
<Compile Include="Cache\UserGroupPermissionsCacheRefresher.cs" />
<Compile Include="Compose\BackOfficeUserAuditEventsComposer.cs" />
<Compile Include="Compose\BlockEditorComponent.cs" />
<Compile Include="Compose\BlockEditorComposer.cs" />
<Compile Include="Compose\NestedContentPropertyComponent.cs" />
<Compile Include="Compose\NotificationsComposer.cs" />
<Compile Include="Compose\PublicAccessComposer.cs" />