Skip to content
Merged
Original file line number Diff line number Diff line change
@@ -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<IContentType>, 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);
}
Original file line number Diff line number Diff line change
@@ -1,22 +1,32 @@
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<IMediaType>, 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"];

/// <summary>
/// we migrate this one, from "Umbraco.MediaPicker" to "Umbraco.MediaPicker3"
/// </summary>
public string? GetEditorAlias()
=> Constants.PropertyEditors.Aliases.MediaPicker3;

public override IDictionary<string, object> GetConfigurationImport(IDictionary<string, object> configuration)
{
return base.GetConfigurationImport(configuration);
}
protected override IMediaType? GetByAlias(string alias)
=> _mediaTypeService.Get(alias);
}
68 changes: 68 additions & 0 deletions uSync.Core/DataTypes/PickerConfigurationSerializerBase.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
using Umbraco.Cms.Core;
using Umbraco.Cms.Core.Models;
using Umbraco.Extensions;

namespace uSync.Core.DataTypes;

internal abstract class PickerConfigurationSerializerBase<TContentType> : ConfigurationSerializerBase
where TContentType : IContentTypeBase
{
public override IDictionary<string, object> GetConfigurationImport(IDictionary<string, object> 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<string> 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);
}


/// <summary>
/// converts a string from UDI to Guid (if they are set)
/// </summary>
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();
}
}
1 change: 0 additions & 1 deletion uSync.Core/Mapping/Mappers/MediaPicker3Mapper.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
using Microsoft.Extensions.Logging;

using System.Text.Json.Nodes;

using Umbraco.Cms.Core;
Expand Down
222 changes: 222 additions & 0 deletions uSync.Tests/Migrations/ContentPickerMigrationTests.cs
Original file line number Diff line number Diff line change
@@ -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<IContentTypeService> _contentTypeServiceMock;

[SetUp]
public void Setup()
{
_contentTypeServiceMock = new Mock<IContentTypeService>();
_serializer = new ContentPickerConfigSerializer(_contentTypeServiceMock.Object);
}

[Test]
public void FilterMigrationFromAliasToGuid()
{
// Arrange
var contentTypeGuid = Guid.Parse("a1b2c3d4-e5f6-7890-abcd-ef1234567890");
var contentTypeMock = new Mock<IContentType>();
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<IContentType>();
contentType1Mock.Setup(x => x.Key).Returns(contentType1Guid);

var contentType2Mock = new Mock<IContentType>();
contentType2Mock.Setup(x => x.Key).Returns(contentType2Guid);

var contentType3Mock = new Mock<IContentType>();
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<IContentType>();
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<IContentType>();
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);
}
}
Loading