Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/custom widget migrations 2 #286

Merged
merged 1 commit into from
Nov 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .gitattributes
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# Auto detect text files and perform LF normalization
* text=auto
* text=CRLF

*.cs text=CRLF diff=csharp
*.html text diff=html
Expand Down
4 changes: 2 additions & 2 deletions KVA/Migration.Tool.Source/Services/AssetFacade.cs
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ public async Task<ContentItemSimplifiedModel> FromMediaFile(IMediaFile mediaFile
languageData.AddRange(contentLanguageNames.Select(contentLanguageName => new ContentItemLanguageData
{
LanguageName = contentLanguageName,
DisplayName = $"{mediaFile.FileName}",
DisplayName = mediaFile.FileName,
UserGuid = createdByUser?.UserGUID,
VersionStatus = VersionStatus.Published,
ContentItemData = new Dictionary<string, object?>
Expand Down Expand Up @@ -164,7 +164,7 @@ public async Task<ContentItemSimplifiedModel> FromAttachment(ICmsAttachment atta
var contentLanguageData = new ContentItemLanguageData
{
LanguageName = contentLanguageName,
DisplayName = $"{attachment.AttachmentName}",
DisplayName = attachment.AttachmentName,
UserGuid = null,
VersionStatus = VersionStatus.Published,
ContentItemData = new Dictionary<string, object?>
Expand Down
184 changes: 103 additions & 81 deletions KVA/Migration.Tool.Source/Services/PageBuilderPatcher.cs

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
using Migration.Tool.Extensions.DefaultMigrations;
using Migration.Tool.KXP.Api.Services.CmsClass;
using Newtonsoft.Json.Linq;

namespace Migration.Tool.Extensions.CommunityMigrations;
public class SampleWidgetMigration : IWidgetMigration
{
public int Rank => 1;

public async Task<WidgetMigrationResult> MigrateWidget(WidgetIdentifier identifier, JToken? value, WidgetMigrationContext context)
{
value!["type"] = "DancingGoat.HeroWidget"; //Migrate to different type of widget

//Recombine the properties
var variants = (JArray)value!["variants"]!;
var singleVariant = variants[0];
singleVariant["properties"] = new JObject
{
["teaser"] = singleVariant["properties"]!["image"],
["text"] = singleVariant["properties"]!["text"]
};

//For new properties, we must explicitly define property migration classes
var propertyMigrations = new Dictionary<string, Type>
{
["teaser"] = typeof(WidgetFileMigration)
//["text"] ... this is an unchanged property from the original widget => default widget property migrations will handle it
};

return new WidgetMigrationResult(value, propertyMigrations);
}

public bool ShallMigrate(WidgetMigrationContext context, WidgetIdentifier identifier) => string.Equals("DancingGoat.HomePage.BannerWidget", identifier.TypeIdentifier, StringComparison.InvariantCultureIgnoreCase);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using Migration.Tool.KXP.Api.Services.CmsClass;
using Newtonsoft.Json.Linq;

namespace Migration.Tool.Extensions.DefaultMigrations;

public class WidgetNoOpMigration : IWidgetPropertyMigration
{
public int Rank => 1_000_000;

public bool ShallMigrate(WidgetPropertyMigrationContext context, string propertyName) => false; // used only when explicitly stated in custom widget migration, ShallMigrate isn't used

public Task<WidgetPropertyMigrationResult> MigrateWidgetProperty(string key, JToken? value, WidgetPropertyMigrationContext context) => Task.FromResult(new WidgetPropertyMigrationResult(value));
}
26 changes: 24 additions & 2 deletions Migration.Tool.Extensions/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,14 +80,36 @@ serviceCollection.AddSingleton<IClassMapping>(m);

demonstrated in method `AddReusableSchemaIntegrationSample`, goal is to take single data class and assign reusable schema.

## Custom widget migrations

Custom widget migration allows you to remodel the original widget as a new widget type. The prominent operations are
changing the target widget type and recombining the original properties.

To create custom widget migration:
- create new file in `Migration.Tool.Extensions/CommunityMigrations` (directory if you need more files for single migration)
- implement interface `Migration.Tool.KXP.Api.Services.CmsClass.IWidgetMigration`
- implement property `Rank`, set number bellow 100 000 - for example 5000. Rank determines the order by which the migrations are tested to be eligible via the `ShallMigrate` method
- implement method `ShallMigrate`. If method returns true, migration will be used. This method receives a context, by which you can decide - typically by the original widget's type
- implement `MigrateWidget`, where objective is to convert old JToken representing the widget's JSON to new converted JToken value
- Widget property migration will still be applied after your custom widget migration
- In the following cases, you must explicitly specify the property migration to be used, via `PropertyMigrations` in returned value (because it can't be infered from the original widget)
- If you add a new property. That includes renaming an original property.
- In the special case when you introduce a new property whose name overlaps with original property. Otherwise the migration infered from the original property would be used
- If your new property is not supposed to be subject to property migrations and the original one was, explicitly specify `WidgetNoOpMigration` for this property
- You can also override the property migration of an original property if that suits your case

- finally register in `Migration.Tool.Extensions/ServiceCollectionExtensions.cs` as `Transient` dependency into service collection. For example `services.AddTransient<IWidgetMigration, YourMigrationClass>()`

Samples:
- [Sample BannerWidget migration](./CommunityMigrations/SampleWidgetMigration.cs)

## Custom widget property migrations

To create custom widget property migration:
- create new file in `Migration.Tool.Extensions/CommunityMigrations` (directory if you need more files for single migration)
- implement interface `Migration.Tool.KXP.Api.Services.CmsClass.IWidgetPropertyMigration`
- implement property rank, set number bellow 100 000 - for example 5000
- implement method shall migrate (if method returns true, migration will be used)
- implement property `Rank`, set number bellow 100 000 - for example 5000. Rank determines the order by which the migrations are tested to be eligible via the `ShallMigrate` method
- implement method `ShallMigrate` (if method returns true, migration will be used)
- implement `MigrateWidgetProperty`, where objective is to convert old JToken representing json value to new converted JToken value
- finally register in `Migration.Tool.Extensions/ServiceCollectionExtensions.cs` as `Transient` dependency into service collection. For example `services.AddTransient<IWidgetPropertyMigration, WidgetPathSelectorMigration>()`

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
namespace Migration.Tool.KXP.Api.Services.CmsClass;

public interface ICustomMigration
{
/// <summary>
/// custom migrations are sorted by this number, first encountered migration wins. Values higher than 100 000 are set to default migrations, set number bellow 100 000 for custom migrations
/// </summary>
int Rank { get; }
}
19 changes: 7 additions & 12 deletions Migration.Tool.KXP.Api/Services/CmsClass/IWidgetMigration.cs
Original file line number Diff line number Diff line change
@@ -1,18 +1,13 @@
using Migration.Tool.Common.Services.Ipc;
using Newtonsoft.Json.Linq;
using Newtonsoft.Json.Linq;

namespace Migration.Tool.KXP.Api.Services.CmsClass;

public record WidgetPropertyMigrationContext(int SiteId, EditingFormControlModel? EditingFormControlModel);
public record WidgetPropertyMigrationResult(JToken? Value, bool NeedsDeferredPatch = false, bool AllowDefaultMigrations = true);
public record WidgetIdentifier(string TypeIdentifier, Guid InstanceIdentifier);
public record WidgetMigrationContext(int SiteId);
public record WidgetMigrationResult(JToken? Value, IReadOnlyDictionary<string, Type> PropertyMigrations, bool NeedsDeferredPatch = false);

public interface IWidgetPropertyMigration
public interface IWidgetMigration : ICustomMigration
{
/// <summary>
/// custom migrations are sorted by this number, first encountered migration wins. Values higher than 100 000 are set to default migrations, set number bellow 100 000 for custom migrations
/// </summary>
int Rank { get; }

bool ShallMigrate(WidgetPropertyMigrationContext context, string propertyName);
Task<WidgetPropertyMigrationResult> MigrateWidgetProperty(string key, JToken? value, WidgetPropertyMigrationContext context);
bool ShallMigrate(WidgetMigrationContext context, WidgetIdentifier identifier);
Task<WidgetMigrationResult> MigrateWidget(WidgetIdentifier identifier, JToken? value, WidgetMigrationContext context);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using Migration.Tool.Common.Services.Ipc;
using Newtonsoft.Json.Linq;

namespace Migration.Tool.KXP.Api.Services.CmsClass;

public record WidgetPropertyMigrationContext(int SiteId, EditingFormControlModel? EditingFormControlModel);
public record WidgetPropertyMigrationResult(JToken? Value, bool NeedsDeferredPatch = false, bool AllowDefaultMigrations = true);

public interface IWidgetPropertyMigration : ICustomMigration
{
bool ShallMigrate(WidgetPropertyMigrationContext context, string propertyName);
Task<WidgetPropertyMigrationResult> MigrateWidgetProperty(string key, JToken? value, WidgetPropertyMigrationContext context);
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,28 @@ namespace Migration.Tool.KXP.Api.Services.CmsClass;
public class WidgetMigrationService
{
private readonly List<IWidgetPropertyMigration> widgetPropertyMigrations;
private readonly List<IWidgetMigration> widgetMigrations;

public WidgetMigrationService(IServiceProvider serviceProvider)
{
var migrations = serviceProvider.GetService<IEnumerable<IWidgetPropertyMigration>>();
widgetPropertyMigrations = migrations == null
widgetPropertyMigrations = LoadRegisteredMigrations<IWidgetPropertyMigration>(serviceProvider);
widgetMigrations = LoadRegisteredMigrations<IWidgetMigration>(serviceProvider);
}

private List<T> LoadRegisteredMigrations<T>(IServiceProvider serviceProvider) where T : ICustomMigration
{
var registeredMigrations = serviceProvider.GetService<IEnumerable<T>>();
return registeredMigrations == null
? []
: migrations.OrderBy(wpm => wpm.Rank).ToList();
: registeredMigrations.OrderBy(wpm => wpm.Rank).ToList();
}

public IWidgetPropertyMigration? GetWidgetPropertyMigrations(WidgetPropertyMigrationContext context, string key)
public IWidgetPropertyMigration? GetWidgetPropertyMigration(WidgetPropertyMigrationContext context, string key)
=> widgetPropertyMigrations.FirstOrDefault(wpm => wpm.ShallMigrate(context, key));

public IWidgetPropertyMigration ResolveWidgetPropertyMigration(Type type)
=> widgetPropertyMigrations.FirstOrDefault(x => x.GetType() == type) ?? throw new ArgumentException($"No migration of type {type} registered", nameof(type));

public IWidgetMigration? GetWidgetMigration(WidgetMigrationContext context, WidgetIdentifier identifier)
=> widgetMigrations.FirstOrDefault(wpm => wpm.ShallMigrate(context, identifier));
}
Loading