diff --git a/uSync.Core/DataTypes/DataTypeSerializers/ContentPickerConfigSerializer.cs b/uSync.Core/DataTypes/DataTypeSerializers/ContentPickerConfigSerializer.cs new file mode 100644 index 000000000..28a2039b6 --- /dev/null +++ b/uSync.Core/DataTypes/DataTypeSerializers/ContentPickerConfigSerializer.cs @@ -0,0 +1,19 @@ +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Services; + +namespace uSync.Core.DataTypes.DataTypeSerializers; + +internal class ContentPickerConfigSerializer : PickerConfigurationSerializerBase, IConfigurationSerializer +{ + private readonly IContentTypeService _contentTypeService; + public ContentPickerConfigSerializer(IContentTypeService contentTypeService) + { + _contentTypeService = contentTypeService; + } + public string Name => nameof(ContentPickerConfigSerializer); + public string[] Editors => [Constants.PropertyEditors.Aliases.ContentPicker]; + + protected override IContentType? GetByAlias(string alias) + => _contentTypeService.Get(alias); +} \ No newline at end of file diff --git a/uSync.Core/DataTypes/DataTypeSerializers/MediaPickerConfigSerializer.cs b/uSync.Core/DataTypes/DataTypeSerializers/MediaPickerConfigSerializer.cs index 3b6f87060..831b4cb82 100644 --- a/uSync.Core/DataTypes/DataTypeSerializers/MediaPickerConfigSerializer.cs +++ b/uSync.Core/DataTypes/DataTypeSerializers/MediaPickerConfigSerializer.cs @@ -1,13 +1,25 @@ using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Services; +using Umbraco.Extensions; +using uSync.Core.Extensions; namespace uSync.Core.DataTypes.DataTypeSerializers; -internal class MediaPickerConfigSerializer : ConfigurationSerializerBase, IConfigurationSerializer +internal class MediaPickerConfigSerializer : PickerConfigurationSerializerBase, IConfigurationSerializer { + private readonly IMediaTypeService _mediaTypeService; + + public MediaPickerConfigSerializer(IMediaTypeService mediaTypeService) + { + _mediaTypeService = mediaTypeService; + } + public string Name => nameof(MediaPickerConfigSerializer); public string[] Editors => [ "Umbraco.MediaPicker", - "Umbraco.MediaPicker2"]; + "Umbraco.MediaPicker2", + "Umbraco.MediaPicker3"]; /// /// we migrate this one, from "Umbraco.MediaPicker" to "Umbraco.MediaPicker3" @@ -15,8 +27,6 @@ internal class MediaPickerConfigSerializer : ConfigurationSerializerBase, IConfi public string? GetEditorAlias() => Constants.PropertyEditors.Aliases.MediaPicker3; - public override IDictionary GetConfigurationImport(IDictionary configuration) - { - return base.GetConfigurationImport(configuration); - } + protected override IMediaType? GetByAlias(string alias) + => _mediaTypeService.Get(alias); } diff --git a/uSync.Core/DataTypes/PickerConfigurationSerializerBase.cs b/uSync.Core/DataTypes/PickerConfigurationSerializerBase.cs new file mode 100644 index 000000000..c1e61c76a --- /dev/null +++ b/uSync.Core/DataTypes/PickerConfigurationSerializerBase.cs @@ -0,0 +1,68 @@ +using Umbraco.Cms.Core; +using Umbraco.Cms.Core.Models; +using Umbraco.Extensions; + +namespace uSync.Core.DataTypes; + +internal abstract class PickerConfigurationSerializerBase : ConfigurationSerializerBase + where TContentType : IContentTypeBase +{ + public override IDictionary GetConfigurationImport(IDictionary configuration) + { + if (configuration.TryGetValue("filter", out var filterValue) is true) + configuration["filter"] = ConvertAliasesToGuidValues(filterValue) ?? filterValue; + + if (configuration.TryGetValue("startNodeId", out var startNodeValue) is true) + configuration["startNodeId"] = ConvertUdiToGuid(startNodeValue) ?? startNodeValue; + + return base.GetConfigurationImport(configuration); + } + + protected abstract TContentType? GetByAlias(string alias); + protected string? ConvertAliasesToGuidValues(object? filterValue) + { + var item = filterValue?.ToString(); + if (string.IsNullOrWhiteSpace(item) || item.Equals("null", System.StringComparison.OrdinalIgnoreCase)) return item; + + List result = []; + + foreach (var value in item.ToDelimitedList()) + { + // if this item is already a guid, we return it. + if (Guid.TryParse(value, out _) is true) + { + result.Add(value); + continue; + } + + // Lookup the content type by alias - return the guid. + var mediaType = GetByAlias(value); + if (mediaType != null) + { + result.Add(mediaType.Key.ToString()); + continue; + } + + result.Add(value); + } + + return string.Join(',', result); + } + + + /// + /// converts a string from UDI to Guid (if they are set) + /// + protected static string? ConvertUdiToGuid(object? idValue) + { + var id = idValue?.ToString(); + + if (string.IsNullOrWhiteSpace(id)) return id; + if (Guid.TryParse(id, out _) is true) return id; + + if (UdiParser.TryParse(id, out GuidUdi? guidUdi) is false || guidUdi is null) + return id; + + return guidUdi.Guid.ToString(); + } +} diff --git a/uSync.Core/Mapping/Mappers/MediaPicker3Mapper.cs b/uSync.Core/Mapping/Mappers/MediaPicker3Mapper.cs index eb8591d77..0c08a0ec2 100644 --- a/uSync.Core/Mapping/Mappers/MediaPicker3Mapper.cs +++ b/uSync.Core/Mapping/Mappers/MediaPicker3Mapper.cs @@ -1,5 +1,4 @@ using Microsoft.Extensions.Logging; - using System.Text.Json.Nodes; using Umbraco.Cms.Core; diff --git a/uSync.Tests/Migrations/ContentPickerMigrationTests.cs b/uSync.Tests/Migrations/ContentPickerMigrationTests.cs new file mode 100644 index 000000000..3360526dd --- /dev/null +++ b/uSync.Tests/Migrations/ContentPickerMigrationTests.cs @@ -0,0 +1,222 @@ +using System; + +using Moq; + +using NUnit.Framework; + +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Services; + +using uSync.Core.DataTypes.DataTypeSerializers; + +namespace uSync.Tests.Migrations; + +[TestFixture] +internal class ContentPickerMigrationTests : MigrationTestBase +{ + private ContentPickerConfigSerializer _serializer; + private Mock _contentTypeServiceMock; + + [SetUp] + public void Setup() + { + _contentTypeServiceMock = new Mock(); + _serializer = new ContentPickerConfigSerializer(_contentTypeServiceMock.Object); + } + + [Test] + public void FilterMigrationFromAliasToGuid() + { + // Arrange + var contentTypeGuid = Guid.Parse("a1b2c3d4-e5f6-7890-abcd-ef1234567890"); + var contentTypeMock = new Mock(); + contentTypeMock.Setup(x => x.Key).Returns(contentTypeGuid); + + _contentTypeServiceMock + .Setup(x => x.Get("myContentType")) + .Returns(contentTypeMock.Object); + + var source = @"{ + ""filter"": ""myContentType"", + ""ignoreUserStartNodes"": false +}"; + + var target = @"{ + ""filter"": ""a1b2c3d4-e5f6-7890-abcd-ef1234567890"", + ""ignoreUserStartNodes"": false +}"; + + // Act & Assert + TestSerializerPropertyMigration(_serializer, source, target); + } + + [Test] + public void FilterMigrationAlreadyGuid() + { + // Arrange - filter is already a GUID, should not change + var source = @"{ + ""filter"": ""a1b2c3d4-e5f6-7890-abcd-ef1234567890"", + ""ignoreUserStartNodes"": false +}"; + + var target = @"{ + ""filter"": ""a1b2c3d4-e5f6-7890-abcd-ef1234567890"", + ""ignoreUserStartNodes"": false +}"; + + // Act & Assert + TestSerializerPropertyMigration(_serializer, source, target); + } + + [Test] + public void FilterMigrationMultipleAliasesToGuids() + { + // Arrange + var contentType1Guid = Guid.Parse("11111111-1111-1111-1111-111111111111"); + var contentType2Guid = Guid.Parse("22222222-2222-2222-2222-222222222222"); + var contentType3Guid = Guid.Parse("33333333-3333-3333-3333-333333333333"); + + var contentType1Mock = new Mock(); + contentType1Mock.Setup(x => x.Key).Returns(contentType1Guid); + + var contentType2Mock = new Mock(); + contentType2Mock.Setup(x => x.Key).Returns(contentType2Guid); + + var contentType3Mock = new Mock(); + contentType3Mock.Setup(x => x.Key).Returns(contentType3Guid); + + _contentTypeServiceMock + .Setup(x => x.Get("contentTypeOne")) + .Returns(contentType1Mock.Object); + + _contentTypeServiceMock + .Setup(x => x.Get("contentTypeTwo")) + .Returns(contentType2Mock.Object); + + _contentTypeServiceMock + .Setup(x => x.Get("contentTypeThree")) + .Returns(contentType3Mock.Object); + + var source = @"{ + ""filter"": ""contentTypeOne,contentTypeTwo,contentTypeThree"", + ""ignoreUserStartNodes"": false +}"; + + var target = @"{ + ""filter"": ""11111111-1111-1111-1111-111111111111,22222222-2222-2222-2222-222222222222,33333333-3333-3333-3333-333333333333"", + ""ignoreUserStartNodes"": false +}"; + + // Act & Assert + TestSerializerPropertyMigration(_serializer, source, target); + } + + [Test] + public void FilterMigrationMixedAliasesAndGuids() + { + // Arrange - mix of aliases and GUIDs + var contentType1Guid = Guid.Parse("11111111-1111-1111-1111-111111111111"); + + var contentType1Mock = new Mock(); + contentType1Mock.Setup(x => x.Key).Returns(contentType1Guid); + + _contentTypeServiceMock + .Setup(x => x.Get("contentTypeOne")) + .Returns(contentType1Mock.Object); + + var source = @"{ + ""filter"": ""contentTypeOne,22222222-2222-2222-2222-222222222222"", + ""ignoreUserStartNodes"": false +}"; + + var target = @"{ + ""filter"": ""11111111-1111-1111-1111-111111111111,22222222-2222-2222-2222-222222222222"", + ""ignoreUserStartNodes"": false +}"; + + // Act & Assert + TestSerializerPropertyMigration(_serializer, source, target); + } + + [Test] + public void StartNodeIdMigrationFromUdiToGuid() + { + // Arrange + var source = @"{ + ""ignoreUserStartNodes"": false, + ""startNodeId"": ""umb://document/a1b2c3d4e5f67890abcdef1234567890"" +}"; + + var target = @"{ + ""ignoreUserStartNodes"": false, + ""startNodeId"": ""a1b2c3d4-e5f6-7890-abcd-ef1234567890"" +}"; + + // Act & Assert + TestSerializerPropertyMigration(_serializer, source, target); + } + + [Test] + public void StartNodeIdMigrationAlreadyGuid() + { + // Arrange - startNodeId is already a GUID, should not change + var source = @"{ + ""ignoreUserStartNodes"": false, + ""startNodeId"": ""a1b2c3d4-e5f6-7890-abcd-ef1234567890"" +}"; + + var target = @"{ + ""ignoreUserStartNodes"": false, + ""startNodeId"": ""a1b2c3d4-e5f6-7890-abcd-ef1234567890"" +}"; + + // Act & Assert + TestSerializerPropertyMigration(_serializer, source, target); + } + + [Test] + public void StartNodeIdMigrationEmpty() + { + // Arrange - empty startNodeId should remain empty + var source = @"{ + ""ignoreUserStartNodes"": false, + ""startNodeId"": """" +}"; + + var target = @"{ + ""ignoreUserStartNodes"": false, + ""startNodeId"": """" +}"; + + // Act & Assert + TestSerializerPropertyMigration(_serializer, source, target); + } + + [Test] + public void FilterAndStartNodeIdMigrationTogether() + { + // Arrange - test both filter and startNodeId migration in the same config + var contentTypeGuid = Guid.Parse("a1b2c3d4-e5f6-7890-abcd-ef1234567890"); + var contentTypeMock = new Mock(); + contentTypeMock.Setup(x => x.Key).Returns(contentTypeGuid); + + _contentTypeServiceMock + .Setup(x => x.Get("myContentType")) + .Returns(contentTypeMock.Object); + + var source = @"{ + ""filter"": ""myContentType"", + ""ignoreUserStartNodes"": false, + ""startNodeId"": ""umb://document/bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"" +}"; + + var target = @"{ + ""filter"": ""a1b2c3d4-e5f6-7890-abcd-ef1234567890"", + ""ignoreUserStartNodes"": false, + ""startNodeId"": ""bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb"" +}"; + + // Act & Assert + TestSerializerPropertyMigration(_serializer, source, target); + } +} diff --git a/uSync.Tests/Migrations/MediaPickerMigrationTests.cs b/uSync.Tests/Migrations/MediaPickerMigrationTests.cs new file mode 100644 index 000000000..cd31a48ab --- /dev/null +++ b/uSync.Tests/Migrations/MediaPickerMigrationTests.cs @@ -0,0 +1,141 @@ +using System; + +using Moq; + +using NUnit.Framework; + +using Umbraco.Cms.Core.Models; +using Umbraco.Cms.Core.Services; + +using uSync.Core.DataTypes.DataTypeSerializers; + +namespace uSync.Tests.Migrations; + +[TestFixture] +internal class MediaPickerMigrationTests : MigrationTestBase +{ + private MediaPickerConfigSerializer _serializer; + private Mock _mockMediaTypeService; + + [SetUp] + public void Setup() + { + _mockMediaTypeService = new Mock(); + + // Mock media types with their aliases and keys + var imageMediaType = new Mock(); + imageMediaType.Setup(x => x.Key).Returns(Guid.Parse("cc07b313-0843-4aa8-bbda-871c8da728c8")); + imageMediaType.Setup(x => x.Alias).Returns("Image"); + + var videoMediaType = new Mock(); + videoMediaType.Setup(x => x.Key).Returns(Guid.Parse("f6c515bb-653c-4bdc-821c-987729ebe327")); + videoMediaType.Setup(x => x.Alias).Returns("Video"); + + var fileMediaType = new Mock(); + fileMediaType.Setup(x => x.Key).Returns(Guid.Parse("4c52d8ab-54e6-40cd-999c-7a5f24903e4d")); + fileMediaType.Setup(x => x.Alias).Returns("File"); + + _mockMediaTypeService.Setup(x => x.Get("Image")).Returns(imageMediaType.Object); + _mockMediaTypeService.Setup(x => x.Get("Video")).Returns(videoMediaType.Object); + _mockMediaTypeService.Setup(x => x.Get("File")).Returns(fileMediaType.Object); + + _serializer = new MediaPickerConfigSerializer(_mockMediaTypeService.Object); + } + + // Test migrating filter from aliases to GUIDs + private static string FilterAliasSource = @"{ + ""filter"": ""Image,Video"", + ""multiple"": false, + ""validationLimit"": {} +}"; + + private static string FilterAliasTarget = @"{ + ""filter"": ""cc07b313-0843-4aa8-bbda-871c8da728c8,f6c515bb-653c-4bdc-821c-987729ebe327"", + ""multiple"": false, + ""validationLimit"": {} +}"; + + [Test] + public void FilterAliasMigrationTest() + => TestSerializerPropertyMigration(_serializer, FilterAliasSource, FilterAliasTarget); + + // Test that already migrated filter (GUIDs) remain unchanged + private static string FilterGuidSource = @"{ + ""filter"": ""cc07b313-0843-4aa8-bbda-871c8da728c8,f6c515bb-653c-4bdc-821c-987729ebe327"", + ""multiple"": false, + ""validationLimit"": {} +}"; + + [Test] + public void FilterAlreadyMigratedTest() + => TestSerializerPropertyMigration(_serializer, FilterGuidSource, FilterGuidSource); + + // Test migrating startNodeId from UDI to GUID + private static string StartNodeUdiSource = @"{ + ""startNodeId"": ""umb://media/71332aa78bea44f19aa600de961b66e8"", + ""multiple"": false, + ""validationLimit"": {} +}"; + + private static string StartNodeUdiTarget = @"{ + ""startNodeId"": ""71332aa7-8bea-44f1-9aa6-00de961b66e8"", + ""multiple"": false, + ""validationLimit"": {} +}"; + + [Test] + public void StartNodeUdiMigrationTest() + => TestSerializerPropertyMigration(_serializer, StartNodeUdiSource, StartNodeUdiTarget); + + // Test that already migrated startNodeId (GUID) remains unchanged + private static string StartNodeGuidSource = @"{ + ""startNodeId"": ""71332aa7-8bea-44f1-9aa6-00de961b66e8"", + ""multiple"": false, + ""validationLimit"": {} +}"; + + [Test] + public void StartNodeAlreadyMigratedTest() + => TestSerializerPropertyMigration(_serializer, StartNodeGuidSource, StartNodeGuidSource); + + // Test migrating both filter and startNodeId together + private static string BothSource = @"{ + ""filter"": ""Image,File"", + ""startNodeId"": ""umb://media/71332aa78bea44f19aa600de961b66e8"", + ""multiple"": true, + ""validationLimit"": {} +}"; + + private static string BothTarget = @"{ + ""filter"": ""cc07b313-0843-4aa8-bbda-871c8da728c8,4c52d8ab-54e6-40cd-999c-7a5f24903e4d"", + ""startNodeId"": ""71332aa7-8bea-44f1-9aa6-00de961b66e8"", + ""multiple"": true, + ""validationLimit"": {} +}"; + + [Test] + public void BothFilterAndStartNodeMigrationTest() + => TestSerializerPropertyMigration(_serializer, BothSource, BothTarget); + + // Test with null/empty filter + private static string NullFilterSource = @"{ + ""filter"": null, + ""multiple"": false, + ""validationLimit"": {} +}"; + + [Test] + public void NullFilterTest() + => TestSerializerPropertyMigration(_serializer, NullFilterSource, NullFilterSource); + + // Test with unknown alias (should preserve the alias) + private static string UnknownAliasSource = @"{ + ""filter"": ""UnknownMediaType"", + ""multiple"": false, + ""validationLimit"": {} +}"; + + [Test] + public void UnknownAliasPreservedTest() + => TestSerializerPropertyMigration(_serializer, UnknownAliasSource, UnknownAliasSource); +} diff --git a/uSync.Tests/Migrations/RichTextMigrationTests.cs b/uSync.Tests/Migrations/RichTextMigrationTests.cs index 490ab97b7..bcc2c3fe4 100644 --- a/uSync.Tests/Migrations/RichTextMigrationTests.cs +++ b/uSync.Tests/Migrations/RichTextMigrationTests.cs @@ -151,18 +151,36 @@ public void Setup() }, ""extensions"": [ ""Umb.Tiptap.RichTextEssentials"", + ""Umb.Tiptap.Anchor"", + ""Umb.Tiptap.Blockquote"", + ""Umb.Tiptap.Bold"", + ""Umb.Tiptap.BulletList"", + ""Umb.Tiptap.CodeBlock"", ""Umb.Tiptap.Embed"", ""Umb.Tiptap.Figure"", + ""Umb.Tiptap.Heading"", + ""Umb.Tiptap.HorizontalRule"", + ""Umb.Tiptap.HtmlAttributeClass"", + ""Umb.Tiptap.HtmlAttributeDataset"", + ""Umb.Tiptap.HtmlAttributeId"", + ""Umb.Tiptap.HtmlAttributeStyle"", + ""Umb.Tiptap.HtmlTagDiv"", + ""Umb.Tiptap.HtmlTagSpan"", ""Umb.Tiptap.Image"", + ""Umb.Tiptap.Italic"", ""Umb.Tiptap.Link"", ""Umb.Tiptap.MediaUpload"", + ""Umb.Tiptap.OrderedList"", + ""Umb.Tiptap.Strike"", ""Umb.Tiptap.Subscript"", ""Umb.Tiptap.Superscript"", ""Umb.Tiptap.Table"", ""Umb.Tiptap.TextAlign"", ""Umb.Tiptap.TextDirection"", ""Umb.Tiptap.TextIndent"", + ""Umb.Tiptap.TrailingNode"", ""Umb.Tiptap.Underline"", + ""Umb.Tiptap.WordCount"", ""Umb.Tiptap.Block"" ], ""ignoreUserStartNodes"": false,