From ea1be074b72fb1788de10015891f0cca0a066931 Mon Sep 17 00:00:00 2001 From: Kevin Jump Date: Thu, 27 Aug 2026 10:59:20 +0100 Subject: [PATCH] Revive uSync.Community.DataTypeSerializers with Try-pattern base and new serializers Convert SyncDataTypeSerializerBase's guid/path lookups (UdiToEntityPath, GuidToEntityPath, PathToUdi, PathToGuid, FindItem) to a Try* pattern instead of relying on empty-string/null sentinels for failure. Add/update editor config serializers that map node references to portable entity paths on export and back to guids on import: - ContentPickerConfigSerializer (startNodeId) - MediaPicker3ConfigSerializer (startNodeId) - MNTPickerConfigSerializer (startNode.id, startNode.dynamicRoot.originKey, the latter only mapped when present) - RichTextConfigSerializer (mediaParentId) - new, defensively copies the configuration dictionary before mutating since other serializers for the same editor (e.g. uSync.Core's RichTextEditorMigratingSerializer) can hand back a read-only ImmutableSortedDictionary. --- .../ContentPickerConfigSerializer.cs | 64 +++------- .../CoreTypes/MNTPickerConfigSerializer.cs | 117 ++++++++++-------- .../CoreTypes/MediaPicker3ConfigSerializer.cs | 69 ++++------- .../CoreTypes/RichTextConfigSerializer.cs | 56 +++++++++ .../SyncDataTypeSerializerBase.cs | 73 +++++++---- 5 files changed, 209 insertions(+), 170 deletions(-) create mode 100644 uSync.Community.DataTypeSerializers/CoreTypes/RichTextConfigSerializer.cs diff --git a/uSync.Community.DataTypeSerializers/CoreTypes/ContentPickerConfigSerializer.cs b/uSync.Community.DataTypeSerializers/CoreTypes/ContentPickerConfigSerializer.cs index 9f20e850c..1036b0a15 100644 --- a/uSync.Community.DataTypeSerializers/CoreTypes/ContentPickerConfigSerializer.cs +++ b/uSync.Community.DataTypeSerializers/CoreTypes/ContentPickerConfigSerializer.cs @@ -1,10 +1,12 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using Umbraco.Cms.Core.Services; +using uSync.Community.DataTypeSerializers; using uSync.Core.DataTypes; -namespace uSync8.Community.DataTypeSerializers.CoreTypes; +namespace uSync.Community.DataTypeSerializers.CoreTypes; public class ContentPickerConfigSerializer : SyncDataTypeSerializerBase, IConfigurationSerializer { @@ -13,57 +15,31 @@ public ContentPickerConfigSerializer(IEntityService entityService) { } public string Name => "ContentPickerNodeSerializer"; - public string[] Editors => ["Umbraco.ContentPicker"]; + private const string _startNodeIdKey = "startNodeId"; + public override IDictionary GetConfigurationExport(IDictionary configuration) { + if (configuration.TryGetValue(_startNodeIdKey, out var startNodeId) + && Guid.TryParse(startNodeId.ToString(), out var startNodeGuid) + && TryGuidToEntityPath(startNodeGuid, out var entityPath)) + { + configuration["startNodeId"] = entityPath; + } + return base.GetConfigurationExport(configuration); } public override IDictionary GetConfigurationImport(IDictionary configuration) { + if (configuration.TryGetValue(_startNodeIdKey, out var startNodeId) + && startNodeId is string startNodePath + && TryPathToGuid(startNodePath, out var startNodeGuid)) + { + configuration["startNodeId"] = startNodeGuid; + } + return base.GetConfigurationImport(configuration); } - - //public override string? SerializeConfig(object configuration) - //{ - - // if (configuration is ContentPickerConfiguration pickerConfig) - // { - // var contentPickerConfig = new MappedPathConfigBase(); - - // contentPickerConfig.Config = new ContentPickerConfiguration() - // { - // IgnoreUserStartNodes = pickerConfig.IgnoreUserStartNodes, - // //StartNodeId = null, - // //ShowOpenButton = pickerConfig.ShowOpenButton - // }; - - // //if (pickerConfig.StartNodeId != null) - // // contentPickerConfig.MappedPath = UdiToEntityPath(pickerConfig.StartNodeId); - - // return base.SerializeConfig(contentPickerConfig); - // } - - // return base.SerializeConfig(configuration); - //} - - - //public override object? DeserializeConfig(string config, Type configType) - //{ - // if (configType == typeof(ContentPickerConfiguration)) - // { - // var mappedConfig = config.DeserializeJson>(); - - // //if (!string.IsNullOrWhiteSpace(mappedConfig.MappedPath)) - // //{ - // // mappedConfig.Config.StartNodeId = PathToUdi(mappedConfig.MappedPath); - // //} - - // return mappedConfig?.Config; - // } - - // return base.DeserializeConfig(config, configType); - //} } diff --git a/uSync.Community.DataTypeSerializers/CoreTypes/MNTPickerConfigSerializer.cs b/uSync.Community.DataTypeSerializers/CoreTypes/MNTPickerConfigSerializer.cs index 2904abda7..d2a63f653 100644 --- a/uSync.Community.DataTypeSerializers/CoreTypes/MNTPickerConfigSerializer.cs +++ b/uSync.Community.DataTypeSerializers/CoreTypes/MNTPickerConfigSerializer.cs @@ -1,10 +1,13 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; +using System.Text.Json.Nodes; using Umbraco.Cms.Core.Services; using uSync.Core.DataTypes; +using uSync.Core.Extensions; -namespace uSync8.Community.DataTypeSerializers.CoreTypes; +namespace uSync.Community.DataTypeSerializers.CoreTypes; public class MNTPickerConfigSerializer : SyncDataTypeSerializerBase, IConfigurationSerializer { @@ -16,64 +19,74 @@ public MNTPickerConfigSerializer(IEntityService entityService) public string[] Editors => ["Umbraco.MultiNodeTreePicker"]; + private const string _startNodeKey = "startNode"; + private const string _idKey = "id"; + private const string _dynamicRootKey = "dynamicRoot"; + private const string _originKeyKey = "originKey"; + public override IDictionary GetConfigurationExport(IDictionary configuration) { + if (configuration.TryGetValue(_startNodeKey, out var startNodeValue) + && startNodeValue is not null + && startNodeValue.TryConvertToJsonObject(out var startNode)) + { + TryMapNodeValue(startNode, _idKey, TryGuidToEntityPath); + + if (startNode.TryGetPropertyAsObject(_dynamicRootKey, out var dynamicRoot)) + TryMapNodeValue(dynamicRoot, _originKeyKey, TryGuidToEntityPath); + + configuration[_startNodeKey] = startNode; + } + return base.GetConfigurationExport(configuration); } public override IDictionary GetConfigurationImport(IDictionary configuration) { + if (configuration.TryGetValue(_startNodeKey, out var startNodeValue) + && startNodeValue is not null + && startNodeValue.TryConvertToJsonObject(out var startNode)) + { + TryMapNodeValue(startNode, _idKey, TryPathToGuid); + + if (startNode.TryGetPropertyAsObject(_dynamicRootKey, out var dynamicRoot)) + TryMapNodeValue(dynamicRoot, _originKeyKey, TryPathToGuid); + + configuration[_startNodeKey] = startNode; + } + return base.GetConfigurationImport(configuration); } - //public override string SerializeConfig(object configuration) - //{ - // var MNTPMappedConfig = new MappedPathConfigBase(); - - // if (configuration is MultiNodePickerConfiguration pickerConfig) - // { - // MNTPMappedConfig.Config = new MultiNodePickerConfiguration() - // { - // IgnoreUserStartNodes = pickerConfig.IgnoreUserStartNodes, - // // Filter = pickerConfig.Filter, - // MaxNumber = pickerConfig.MaxNumber, - // MinNumber = pickerConfig.MinNumber, - // // ShowOpen = pickerConfig.ShowOpen, - // TreeSource = new MultiNodePickerConfigurationTreeSource() - // { - // ObjectType = pickerConfig.TreeSource.ObjectType, - // StartNodeId = pickerConfig.TreeSource.StartNodeId, - // StartNodeQuery = pickerConfig.TreeSource.StartNodeQuery - // } - - // }; - - // if (pickerConfig?.TreeSource?.StartNodeId != null) - // { - // MNTPMappedConfig.MappedPath = UdiToEntityPath(pickerConfig.TreeSource.StartNodeId); - // } - - // return base.SerializeConfig(MNTPMappedConfig); - // } - - // return base.SerializeConfig(configuration); - //} - - - //public override object DeserializeConfig(string config, Type configType) - //{ - // if (configType == typeof(MultiNodePickerConfiguration)) - // { - // var mappedConfig = config.DeserializeJson>(); - - // if (!string.IsNullOrWhiteSpace(mappedConfig.MappedPath)) - // { - // mappedConfig.Config.TreeSource.StartNodeId = PathToUdi(mappedConfig.MappedPath); - // } - - // return mappedConfig.Config; - // } - - // return base.DeserializeConfig(config, configType); - //} + private delegate bool TryConvert(Guid guid, out TResult result); + private delegate bool TryConvertBack(string value, out Guid guid); + + /// + /// maps a guid property value to its entity path, if the property is present and holds a guid. + /// + private static bool TryMapNodeValue(JsonObject node, string propertyName, TryConvert converter) + { + if (node.TryGetPropertyValue(propertyName, out var propertyValue) is false + || propertyValue is null) return false; + + if (Guid.TryParse(propertyValue.ToString(), out var guid) is false) return false; + if (converter(guid, out var path) is false) return false; + + node[propertyName] = path; + return true; + } + + /// + /// maps an entity path property value back to a guid, if the property is present and holds a path. + /// + private static bool TryMapNodeValue(JsonObject node, string propertyName, TryConvertBack converter) + { + if (node.TryGetPropertyValue(propertyName, out var propertyValue) is false + || propertyValue is null) return false; + + if (converter(propertyValue.ToString(), out var guid) is false) return false; + + node[propertyName] = guid; + return true; + } } diff --git a/uSync.Community.DataTypeSerializers/CoreTypes/MediaPicker3ConfigSerializer.cs b/uSync.Community.DataTypeSerializers/CoreTypes/MediaPicker3ConfigSerializer.cs index 81074fd4c..14b4e8d40 100644 --- a/uSync.Community.DataTypeSerializers/CoreTypes/MediaPicker3ConfigSerializer.cs +++ b/uSync.Community.DataTypeSerializers/CoreTypes/MediaPicker3ConfigSerializer.cs @@ -1,10 +1,12 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using Umbraco.Cms.Core.Services; +using uSync.Community.DataTypeSerializers; using uSync.Core.DataTypes; -namespace uSync8.Community.DataTypeSerializers.CoreTypes; +namespace uSync.Community.DataTypeSerializers.CoreTypes; public class MediaPicker3ConfigSerializer : SyncDataTypeSerializerBase, IConfigurationSerializer { @@ -16,56 +18,29 @@ public MediaPicker3ConfigSerializer(IEntityService entityService) public string[] Editors => ["Umbraco.MediaPicker3"]; - public override IDictionary GetConfigurationImport(IDictionary configuration) - { - return base.GetConfigurationImport(configuration); - } + private const string _startNodeIdKey = "startNodeId"; public override IDictionary GetConfigurationExport(IDictionary configuration) { + if (configuration.TryGetValue(_startNodeIdKey, out var startNodeId) + && Guid.TryParse(startNodeId?.ToString(), out var startNodeGuid) + && TryGuidToEntityPath(startNodeGuid, out var entityPath)) + { + configuration[_startNodeIdKey] = entityPath; + } + return base.GetConfigurationExport(configuration); } - // public override string SerializeConfig(object configuration) - // { - - // if (configuration is MediaPicker3Configuration pickerConfig) - // { - // var mediaPickerConfig = new MappedPathConfigBase(); - // mediaPickerConfig.Config = new MediaPicker3Configuration() - // { - // EnableLocalFocalPoint = pickerConfig.EnableLocalFocalPoint, - // Crops = pickerConfig.Crops, - // Filter = pickerConfig.Filter, - // IgnoreUserStartNodes = pickerConfig.IgnoreUserStartNodes, - // Multiple = pickerConfig.Multiple, - // ValidationLimit = pickerConfig.ValidationLimit - // }; - - // if (pickerConfig.StartNodeId != null) - // mediaPickerConfig.MappedPath = UdiToEntityPath(pickerConfig.StartNodeId); - // return base.SerializeConfig(mediaPickerConfig); - // } - - // return base.SerializeConfig(configuration); - - // } - - - // public override object DeserializeConfig(string config, Type configType) - // { - // if (configType == typeof(MediaPicker3Configuration)) - // { - // var mappedConfig = config.DeserializeJson>(); - - // if (!string.IsNullOrWhiteSpace(mappedConfig.MappedPath)) - // { - // mappedConfig.Config.StartNodeId = PathToUdi(mappedConfig.MappedPath); - // } - - // return mappedConfig.Config; - // } + public override IDictionary GetConfigurationImport(IDictionary configuration) + { + if (configuration.TryGetValue(_startNodeIdKey, out var startNodeId) + && startNodeId is string startNodePath + && TryPathToGuid(startNodePath, out var startNodeGuid)) + { + configuration[_startNodeIdKey] = startNodeGuid; + } - // return base.DeserializeConfig(config, configType); - // } + return base.GetConfigurationImport(configuration); + } } diff --git a/uSync.Community.DataTypeSerializers/CoreTypes/RichTextConfigSerializer.cs b/uSync.Community.DataTypeSerializers/CoreTypes/RichTextConfigSerializer.cs new file mode 100644 index 000000000..770386e65 --- /dev/null +++ b/uSync.Community.DataTypeSerializers/CoreTypes/RichTextConfigSerializer.cs @@ -0,0 +1,56 @@ +using System; +using System.Collections.Generic; + +using Umbraco.Cms.Core.Services; + +using uSync.Community.DataTypeSerializers; +using uSync.Core.DataTypes; + +namespace uSync.Community.DataTypeSerializers.CoreTypes; + +public class RichTextConfigSerializer : SyncDataTypeSerializerBase, IConfigurationSerializer +{ + public RichTextConfigSerializer(IEntityService entityService) + : base(entityService) + { } + + public string Name => "RichTextNodeSerializer"; + + public string[] Editors => ["Umbraco.RichText"]; + + private const string _mediaParentIdKey = "mediaParentId"; + + public override IDictionary GetConfigurationExport(IDictionary configuration) + { + if (configuration.TryGetValue(_mediaParentIdKey, out var mediaParentId) + && Guid.TryParse(mediaParentId?.ToString(), out var mediaParentGuid) + && TryGuidToEntityPath(mediaParentGuid, out var entityPath)) + { + // other serializers for this editor (e.g. uSync.Core's RichTextEditorMigratingSerializer) + // may hand us a read-only dictionary, so copy before mutating. + configuration = new Dictionary(configuration) + { + [_mediaParentIdKey] = entityPath + }; + } + + return base.GetConfigurationExport(configuration); + } + + public override IDictionary GetConfigurationImport(IDictionary configuration) + { + if (configuration.TryGetValue(_mediaParentIdKey, out var mediaParentId) + && mediaParentId is string mediaParentPath + && TryPathToGuid(mediaParentPath, out var mediaParentGuid)) + { + // other serializers for this editor (e.g. uSync.Core's RichTextEditorMigratingSerializer) + // may hand us a read-only dictionary, so copy before mutating. + configuration = new Dictionary(configuration) + { + [_mediaParentIdKey] = mediaParentGuid + }; + } + + return base.GetConfigurationImport(configuration); + } +} diff --git a/uSync.Community.DataTypeSerializers/SyncDataTypeSerializerBase.cs b/uSync.Community.DataTypeSerializers/SyncDataTypeSerializerBase.cs index 191f0a673..569776b08 100644 --- a/uSync.Community.DataTypeSerializers/SyncDataTypeSerializerBase.cs +++ b/uSync.Community.DataTypeSerializers/SyncDataTypeSerializerBase.cs @@ -1,4 +1,5 @@ -using System.Linq; +using System; +using System.Linq; using Umbraco.Cms.Core; using Umbraco.Cms.Core.Models; @@ -8,7 +9,7 @@ using uSync.Core.DataTypes; -namespace uSync8.Community.DataTypeSerializers; +namespace uSync.Community.DataTypeSerializers; public abstract class SyncDataTypeSerializerBase : ConfigurationSerializerBase { @@ -19,18 +20,25 @@ public SyncDataTypeSerializerBase(IEntityService entityService) this.entityService = entityService; } - protected virtual string UdiToEntityPath(Udi udi) + protected virtual bool TryUdiToEntityPath(Udi? udi, out string entityPath) { - if (udi != null && udi is GuidUdi guidUdi) - { - var item = entityService.Get(guidUdi.Guid); - if (item != null) - { - var type = Umbraco.Cms.Core.Models.ObjectTypes.GetUdiType(item.NodeObjectType); - return type + ":" + GetItemPath(item); - } - } - return string.Empty; + entityPath = string.Empty; + + if (udi is not GuidUdi guidUdi) return false; + + return TryGuidToEntityPath(guidUdi.Guid, out entityPath); + } + + protected virtual bool TryGuidToEntityPath(Guid guid, out string entityPath) + { + entityPath = string.Empty; + + var item = entityService.Get(guid); + if (item == null) return false; + + var type = ObjectTypes.GetUdiType(item.NodeObjectType); + entityPath = type + ":" + GetItemPath(item); + return true; } protected virtual string GetItemPath(IEntitySlim item) @@ -46,9 +54,24 @@ protected virtual string GetItemPath(IEntitySlim item) return path + "/" + item.Name; } - protected virtual Udi? PathToUdi(string entityPath) + protected virtual bool TryPathToUdi(string entityPath, out Udi? udi) { - if (!entityPath.Contains(':')) return null; + udi = null; + + if (!entityPath.Contains(':')) return false; + var entityType = entityPath.Substring(0, entityPath.IndexOf(':')); + + if (!TryPathToGuid(entityPath, out var key)) return false; + + udi = Udi.Create(entityType, key); + return true; + } + + protected virtual bool TryPathToGuid(string entityPath, out Guid guid) + { + guid = Guid.Empty; + + if (!entityPath.Contains(':')) return false; var entityType = entityPath.Substring(0, entityPath.IndexOf(':')); var objectType = UdiEntityTypeHelper.ToUmbracoObjectType(entityType); @@ -61,28 +84,24 @@ protected virtual string GetItemPath(IEntitySlim item) foreach (var name in names) { - next = FindItem(parentId, name, objectType); - if (next == null) return null; + if (!TryFindItem(parentId, name, objectType, out next)) return false; parentId = next.Id; } - if (next != null) - return Udi.Create(entityType, next.Key); - + if (next == null) return false; - return null; + guid = next.Key; + return true; } - protected IEntitySlim? FindItem(int parentId, string name, UmbracoObjectTypes objectType) + + protected bool TryFindItem(int parentId, string name, UmbracoObjectTypes objectType, [System.Diagnostics.CodeAnalysis.NotNullWhen(true)] out IEntitySlim? item) { var children = entityService.GetChildren(parentId, objectType); - if (children.Any()) - { - return children.FirstOrDefault(x => x.Name.InvariantEquals(name)); - } - return null; + item = children.FirstOrDefault(x => x.Name.InvariantEquals(name)); + return item != null; } }