From a77ebf4374e29e93844e96a4f0f9c46406eed86a Mon Sep 17 00:00:00 2001 From: akfakmot Date: Thu, 7 Nov 2024 12:31:11 +0100 Subject: [PATCH 01/13] Add custom widget migrations Add custom widget migrations --- .gitattributes | 2 +- .../Services/AssetFacade.cs | 4 +- .../Services/PageBuilderPatcher.cs | 184 ++++++++++-------- .../SampleWidgetMigration.cs | 34 ++++ .../DefaultMigrations/WidgetNoOpMigration.cs | 13 ++ Migration.Tool.Extensions/README.md | 26 ++- .../Services/CmsClass/ICustomMigration.cs | 9 + .../Services/CmsClass/IWidgetMigration.cs | 19 +- .../CmsClass/IWidgetPropertyMigration.cs | 13 ++ .../CmsClass/WidgetMigrationService.cs | 21 +- 10 files changed, 223 insertions(+), 102 deletions(-) create mode 100644 Migration.Tool.Extensions/CommunityMigrations/SampleWidgetMigration.cs create mode 100644 Migration.Tool.Extensions/DefaultMigrations/WidgetNoOpMigration.cs create mode 100644 Migration.Tool.KXP.Api/Services/CmsClass/ICustomMigration.cs create mode 100644 Migration.Tool.KXP.Api/Services/CmsClass/IWidgetPropertyMigration.cs diff --git a/.gitattributes b/.gitattributes index 6c63a529..c9873176 100644 --- a/.gitattributes +++ b/.gitattributes @@ -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 diff --git a/KVA/Migration.Tool.Source/Services/AssetFacade.cs b/KVA/Migration.Tool.Source/Services/AssetFacade.cs index eb36e9c1..b0a1d4ff 100644 --- a/KVA/Migration.Tool.Source/Services/AssetFacade.cs +++ b/KVA/Migration.Tool.Source/Services/AssetFacade.cs @@ -111,7 +111,7 @@ public async Task 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 @@ -164,7 +164,7 @@ public async Task FromAttachment(ICmsAttachment atta var contentLanguageData = new ContentItemLanguageData { LanguageName = contentLanguageName, - DisplayName = $"{attachment.AttachmentName}", + DisplayName = attachment.AttachmentName, UserGuid = null, VersionStatus = VersionStatus.Published, ContentItemData = new Dictionary diff --git a/KVA/Migration.Tool.Source/Services/PageBuilderPatcher.cs b/KVA/Migration.Tool.Source/Services/PageBuilderPatcher.cs index 628da52e..5f918e91 100644 --- a/KVA/Migration.Tool.Source/Services/PageBuilderPatcher.cs +++ b/KVA/Migration.Tool.Source/Services/PageBuilderPatcher.cs @@ -41,7 +41,7 @@ public async Task PatchJsonDefinitions(int sourceSiteId, sourceInstanceContext.GetPageTemplateFormComponents(sourceSiteId, pageTemplateConfigurationObj.Identifier); if (pageTemplateConfigurationObj.Properties is { Count: > 0 }) { - bool ndp = await MigrateProperties(sourceSiteId, pageTemplateConfigurationObj.Properties, pageTemplateConfigurationFcs); + bool ndp = await MigrateProperties(sourceSiteId, pageTemplateConfigurationObj.Properties, pageTemplateConfigurationFcs, new Dictionary()); needsDeferredPatch = ndp || needsDeferredPatch; } @@ -92,7 +92,7 @@ private async Task WalkSections(int siteId, List sec logger.LogTrace("Walk section {TypeIdentifier}|{Identifier}", section.TypeIdentifier, section.Identifier); var sectionFcs = sourceInstanceContext.GetSectionFormComponents(siteId, section.TypeIdentifier); - bool ndp1 = await MigrateProperties(siteId, section.Properties, sectionFcs); + bool ndp1 = await MigrateProperties(siteId, section.Properties, sectionFcs, new Dictionary()); needsDeferredPatch = ndp1 || needsDeferredPatch; if (section.Zones is { Count: > 0 }) @@ -128,19 +128,32 @@ private async Task WalkWidgets(int siteId, List widge foreach (var widget in widgets) { logger.LogTrace("Walk widget {TypeIdentifier}|{Identifier}", widget.TypeIdentifier, widget.Identifier); + var widgetCompos = sourceInstanceContext.GetWidgetPropertyFormComponents(siteId, widget.TypeIdentifier); + var context = new WidgetMigrationContext(siteId); + var identifier = new WidgetIdentifier(widget.TypeIdentifier, widget.Identifier); + var migration = widgetMigrationService.GetWidgetMigration(context, identifier); + IReadOnlyDictionary propertyMigrations = new Dictionary(); + + if (migration is not null) + { + (var migratedValue, var propertyMigrationTypes, bool ndp) = await migration.MigrateWidget(identifier, JObject.FromObject(widget), context); + propertyMigrations = propertyMigrationTypes.ToDictionary(x => x.Key, x => widgetMigrationService.ResolveWidgetPropertyMigration(x.Value)); + needsDeferredPatch = ndp || needsDeferredPatch; + + widget.Variants.Clear(); + using var migratedValueReader = migratedValue.CreateReader(); + JsonSerializer.CreateDefault().Populate(migratedValueReader, widget); + } + foreach (var variant in widget.Variants) { logger.LogTrace("Migrating widget variant {Name}|{Identifier}", variant.Name, variant.Identifier); if (variant.Properties is { Count: > 0 } properties) { - foreach ((string key, var value) in properties) - { - logger.LogTrace("Migrating widget property {Name}|{Identifier}", key, value?.ToString()); - await MigrateProperties(siteId, properties, widgetCompos); - } + await MigrateProperties(siteId, properties, widgetCompos, propertyMigrations); } } } @@ -148,109 +161,118 @@ private async Task WalkWidgets(int siteId, List widge return needsDeferredPatch; } - private async Task MigrateProperties(int siteId, JObject properties, List? formControlModels) + private async Task MigrateProperties(int siteId, JObject properties, List? formControlModels, IReadOnlyDictionary explicitMigrations) { bool needsDeferredPatch = false; foreach ((string key, var value) in properties) { - logger.LogTrace("Walk property {Name}|{Identifier}", key, value?.ToString()); + logger.LogTrace("Migrating widget property {Name}|{Identifier}", key, value?.ToString()); var editingFcm = formControlModels?.FirstOrDefault(x => x.PropertyName.Equals(key, StringComparison.InvariantCultureIgnoreCase)); - if (editingFcm != null) + + IWidgetPropertyMigration? propertyMigration = null; + WidgetPropertyMigrationContext? context = null; + bool customMigrationApplied = false; + if (explicitMigrations.ContainsKey(key)) { - var context = new WidgetPropertyMigrationContext(siteId, editingFcm); - var widgetPropertyMigration = widgetMigrationService.GetWidgetPropertyMigrations(context, key); - bool allowDefaultMigrations = true; - bool customMigrationApplied = false; - if (widgetPropertyMigration != null) - { - (var migratedValue, bool ndp, allowDefaultMigrations) = await widgetPropertyMigration.MigrateWidgetProperty(key, value, context); - needsDeferredPatch = ndp || needsDeferredPatch; - properties[key] = migratedValue; - customMigrationApplied = true; - logger.LogTrace("Migration {Migration} applied to {Value}, resulting in {Result}", widgetPropertyMigration.GetType().FullName, value?.ToString() ?? "", migratedValue?.ToString() ?? ""); - } + context = new WidgetPropertyMigrationContext(siteId, null); + propertyMigration = explicitMigrations[key]; + } + else if (editingFcm is not null) + { + context = new WidgetPropertyMigrationContext(siteId, editingFcm); + propertyMigration = widgetMigrationService.GetWidgetPropertyMigration(context, key); + } - if (allowDefaultMigrations) + bool allowDefaultMigrations = true; + if (propertyMigration is not null) + { + (var migratedValue, bool ndp, allowDefaultMigrations) = await propertyMigration.MigrateWidgetProperty(key, value, context!); + needsDeferredPatch = ndp || needsDeferredPatch; + properties[key] = migratedValue; + customMigrationApplied = true; + logger.LogTrace("Migration {Migration} applied to {Value}, resulting in {Result}", propertyMigration.GetType().FullName, value?.ToString() ?? "", migratedValue?.ToString() ?? ""); + } + + if (allowDefaultMigrations && editingFcm is not null) + { + if (FieldMappingInstance.BuiltInModel.NotSupportedInKxpLegacyMode + .SingleOrDefault(x => x.OldFormComponent == editingFcm.FormComponentIdentifier) is var (oldFormComponent, newFormComponent)) { - if (FieldMappingInstance.BuiltInModel.NotSupportedInKxpLegacyMode - .SingleOrDefault(x => x.OldFormComponent == editingFcm.FormComponentIdentifier) is var (oldFormComponent, newFormComponent)) - { - logger.LogTrace("Editing form component found {FormComponentName} => no longer supported {Replacement}", editingFcm.FormComponentIdentifier, newFormComponent); + logger.LogTrace("Editing form component found {FormComponentName} => no longer supported {Replacement}", editingFcm.FormComponentIdentifier, newFormComponent); - switch (oldFormComponent) + switch (oldFormComponent) + { + case Kx13FormComponents.Kentico_AttachmentSelector when newFormComponent == FormComponents.AdminAssetSelectorComponent: { - case Kx13FormComponents.Kentico_AttachmentSelector when newFormComponent == FormComponents.AdminAssetSelectorComponent: + if (value?.ToObject>() is { Count: > 0 } items) { - if (value?.ToObject>() is { Count: > 0 } items) + var nv = new List(); + foreach (var asi in items) { - var nv = new List(); - foreach (var asi in items) + var attachment = modelFacade.SelectWhere("AttachmentSiteID = @attachmentSiteId AND AttachmentGUID = @attachmentGUID", + new SqlParameter("attachmentSiteID", siteId), + new SqlParameter("attachmentGUID", asi.FileGuid) + ) + .FirstOrDefault(); + if (attachment != null) { - var attachment = modelFacade.SelectWhere("AttachmentSiteID = @attachmentSiteId AND AttachmentGUID = @attachmentGUID", - new SqlParameter("attachmentSiteID", siteId), - new SqlParameter("attachmentGUID", asi.FileGuid) - ) - .FirstOrDefault(); - if (attachment != null) + switch (attachmentMigrator.MigrateAttachment(attachment).GetAwaiter().GetResult()) { - switch (attachmentMigrator.MigrateAttachment(attachment).GetAwaiter().GetResult()) + case MigrateAttachmentResultMediaFile { Success: true, MediaFileInfo: { } x }: { - case MigrateAttachmentResultMediaFile { Success: true, MediaFileInfo: { } x }: - { - nv.Add(new AssetRelatedItem { Identifier = x.FileGUID, Dimensions = new AssetDimensions { Height = x.FileImageHeight, Width = x.FileImageWidth }, Name = x.FileName, Size = x.FileSize }); - break; - } - case MigrateAttachmentResultContentItem { Success: true, ContentItemGuid: { } contentItemGuid }: - { - nv.Add(new ContentItemReference { Identifier = contentItemGuid }); - break; - } - default: - { - logger.LogWarning("Attachment '{AttachmentGUID}' failed to migrate", asi.FileGuid); - break; - } + nv.Add(new AssetRelatedItem { Identifier = x.FileGUID, Dimensions = new AssetDimensions { Height = x.FileImageHeight, Width = x.FileImageWidth }, Name = x.FileName, Size = x.FileSize }); + break; + } + case MigrateAttachmentResultContentItem { Success: true, ContentItemGuid: { } contentItemGuid }: + { + nv.Add(new ContentItemReference { Identifier = contentItemGuid }); + break; + } + default: + { + logger.LogWarning("Attachment '{AttachmentGUID}' failed to migrate", asi.FileGuid); + break; } - } - else - { - logger.LogWarning("Attachment '{AttachmentGUID}' not found", asi.FileGuid); } } - - properties[key] = JToken.FromObject(nv); + else + { + logger.LogWarning("Attachment '{AttachmentGUID}' not found", asi.FileGuid); + } } - logger.LogTrace("Value migrated from {Old} model to {New} model", oldFormComponent, newFormComponent); - break; + properties[key] = JToken.FromObject(nv); } - default: - break; + logger.LogTrace("Value migrated from {Old} model to {New} model", oldFormComponent, newFormComponent); + break; } + + default: + break; + } + } + else if (!customMigrationApplied) + { + if (FieldMappingInstance.BuiltInModel.SupportedInKxpLegacyMode.Contains(editingFcm.FormComponentIdentifier)) + { + // OK + logger.LogTrace("Editing form component found {FormComponentName} => supported in legacy mode", editingFcm.FormComponentIdentifier); } - else if (!customMigrationApplied) + else { - if (FieldMappingInstance.BuiltInModel.SupportedInKxpLegacyMode.Contains(editingFcm.FormComponentIdentifier)) - { - // OK - logger.LogTrace("Editing form component found {FormComponentName} => supported in legacy mode", editingFcm.FormComponentIdentifier); - } - else - { - // unknown control, probably custom - logger.LogTrace("Editing form component found {FormComponentName} => custom or inlined component, don't forget to migrate code accordingly", editingFcm.FormComponentIdentifier); - } + // unknown control, probably custom + logger.LogTrace("Editing form component found {FormComponentName} => custom or inlined component, don't forget to migrate code accordingly", editingFcm.FormComponentIdentifier); } + } - if ("NodeAliasPath".Equals(key, StringComparison.InvariantCultureIgnoreCase)) - { - needsDeferredPatch = true; - properties["TreePath"] = value; - properties.Remove(key); - } + if ("NodeAliasPath".Equals(key, StringComparison.InvariantCultureIgnoreCase)) + { + needsDeferredPatch = true; + properties["TreePath"] = value; + properties.Remove(key); } } } diff --git a/Migration.Tool.Extensions/CommunityMigrations/SampleWidgetMigration.cs b/Migration.Tool.Extensions/CommunityMigrations/SampleWidgetMigration.cs new file mode 100644 index 00000000..d9a30a2c --- /dev/null +++ b/Migration.Tool.Extensions/CommunityMigrations/SampleWidgetMigration.cs @@ -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 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 + { + ["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); +} diff --git a/Migration.Tool.Extensions/DefaultMigrations/WidgetNoOpMigration.cs b/Migration.Tool.Extensions/DefaultMigrations/WidgetNoOpMigration.cs new file mode 100644 index 00000000..2a039d3c --- /dev/null +++ b/Migration.Tool.Extensions/DefaultMigrations/WidgetNoOpMigration.cs @@ -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 MigrateWidgetProperty(string key, JToken? value, WidgetPropertyMigrationContext context) => Task.FromResult(new WidgetPropertyMigrationResult(value)); +} diff --git a/Migration.Tool.Extensions/README.md b/Migration.Tool.Extensions/README.md index a71cbf6a..c6a4b3c9 100644 --- a/Migration.Tool.Extensions/README.md +++ b/Migration.Tool.Extensions/README.md @@ -80,14 +80,36 @@ serviceCollection.AddSingleton(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()` + +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()` diff --git a/Migration.Tool.KXP.Api/Services/CmsClass/ICustomMigration.cs b/Migration.Tool.KXP.Api/Services/CmsClass/ICustomMigration.cs new file mode 100644 index 00000000..e1a073aa --- /dev/null +++ b/Migration.Tool.KXP.Api/Services/CmsClass/ICustomMigration.cs @@ -0,0 +1,9 @@ +namespace Migration.Tool.KXP.Api.Services.CmsClass; + +public interface ICustomMigration +{ + /// + /// 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 + /// + int Rank { get; } +} diff --git a/Migration.Tool.KXP.Api/Services/CmsClass/IWidgetMigration.cs b/Migration.Tool.KXP.Api/Services/CmsClass/IWidgetMigration.cs index b0cdcf3a..15421d28 100644 --- a/Migration.Tool.KXP.Api/Services/CmsClass/IWidgetMigration.cs +++ b/Migration.Tool.KXP.Api/Services/CmsClass/IWidgetMigration.cs @@ -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 PropertyMigrations, bool NeedsDeferredPatch = false); -public interface IWidgetPropertyMigration +public interface IWidgetMigration : ICustomMigration { - /// - /// 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 - /// - int Rank { get; } - - bool ShallMigrate(WidgetPropertyMigrationContext context, string propertyName); - Task MigrateWidgetProperty(string key, JToken? value, WidgetPropertyMigrationContext context); + bool ShallMigrate(WidgetMigrationContext context, WidgetIdentifier identifier); + Task MigrateWidget(WidgetIdentifier identifier, JToken? value, WidgetMigrationContext context); } diff --git a/Migration.Tool.KXP.Api/Services/CmsClass/IWidgetPropertyMigration.cs b/Migration.Tool.KXP.Api/Services/CmsClass/IWidgetPropertyMigration.cs new file mode 100644 index 00000000..590277d0 --- /dev/null +++ b/Migration.Tool.KXP.Api/Services/CmsClass/IWidgetPropertyMigration.cs @@ -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 MigrateWidgetProperty(string key, JToken? value, WidgetPropertyMigrationContext context); +} diff --git a/Migration.Tool.KXP.Api/Services/CmsClass/WidgetMigrationService.cs b/Migration.Tool.KXP.Api/Services/CmsClass/WidgetMigrationService.cs index e07c6c6d..1a30ef4f 100644 --- a/Migration.Tool.KXP.Api/Services/CmsClass/WidgetMigrationService.cs +++ b/Migration.Tool.KXP.Api/Services/CmsClass/WidgetMigrationService.cs @@ -5,15 +5,28 @@ namespace Migration.Tool.KXP.Api.Services.CmsClass; public class WidgetMigrationService { private readonly List widgetPropertyMigrations; + private readonly List widgetMigrations; public WidgetMigrationService(IServiceProvider serviceProvider) { - var migrations = serviceProvider.GetService>(); - widgetPropertyMigrations = migrations == null + widgetPropertyMigrations = LoadRegisteredMigrations(serviceProvider); + widgetMigrations = LoadRegisteredMigrations(serviceProvider); + } + + private List LoadRegisteredMigrations(IServiceProvider serviceProvider) where T : ICustomMigration + { + var registeredMigrations = serviceProvider.GetService>(); + 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)); } From 3bb880c796c80272d9c61cd4b8b12ec4a7ca69f3 Mon Sep 17 00:00:00 2001 From: akfakmot Date: Thu, 7 Nov 2024 15:42:18 +0100 Subject: [PATCH 02/13] Fix creating of default culture Fix: Fix creating of default culture --- Migration.Tool.Core.KX13/Handlers/MigrateSitesCommandHandler.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Migration.Tool.Core.KX13/Handlers/MigrateSitesCommandHandler.cs b/Migration.Tool.Core.KX13/Handlers/MigrateSitesCommandHandler.cs index 323bedd1..656064b0 100644 --- a/Migration.Tool.Core.KX13/Handlers/MigrateSitesCommandHandler.cs +++ b/Migration.Tool.Core.KX13/Handlers/MigrateSitesCommandHandler.cs @@ -68,7 +68,7 @@ public async Task Handle(MigrateSitesCommand request, Cancellatio ContentLanguageGUID = cmsCulture.CultureGuid, ContentLanguageDisplayName = cmsCulture.CultureName, ContentLanguageName = cmsCulture.CultureCode, - ContentLanguageIsDefault = true, + ContentLanguageIsDefault = string.Equals(cmsCulture.CultureCode, defaultCultureCode, StringComparison.InvariantCultureIgnoreCase), ContentLanguageFallbackContentLanguageGuid = null, ContentLanguageCultureFormat = cmsCulture.CultureCode }); From 3d3be1c85937ef0b414b67bad61de1eade1b8c3d Mon Sep 17 00:00:00 2001 From: akfakmot Date: Thu, 7 Nov 2024 17:49:30 +0100 Subject: [PATCH 03/13] Allow trailing slash in source instance site's domain Modify: Allow trailing slash in source instance site's domain --- Migration.Tool.Core.KX13/Handlers/MigrateSitesCommandHandler.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Migration.Tool.Core.KX13/Handlers/MigrateSitesCommandHandler.cs b/Migration.Tool.Core.KX13/Handlers/MigrateSitesCommandHandler.cs index 656064b0..c18ddbd4 100644 --- a/Migration.Tool.Core.KX13/Handlers/MigrateSitesCommandHandler.cs +++ b/Migration.Tool.Core.KX13/Handlers/MigrateSitesCommandHandler.cs @@ -101,7 +101,7 @@ public async Task Handle(MigrateSitesCommand request, Cancellatio { WebsiteChannelGUID = kx13CmsSite.SiteGuid, WebsiteChannelChannelGuid = kx13CmsSite.SiteGuid, - WebsiteChannelDomain = kx13CmsSite.SiteDomainName, + WebsiteChannelDomain = kx13CmsSite.SiteDomainName.Trim('/'), WebsiteChannelHomePage = homePagePath, WebsiteChannelPrimaryContentLanguageGuid = migratedCultureCodes[defaultCultureCode].ContentLanguageGUID, WebsiteChannelDefaultCookieLevel = cookieLevel, From 08073b2cf2db7624cb8bdc0b50dcfd177b94cec0 Mon Sep 17 00:00:00 2001 From: Anthony Marquez Date: Thu, 24 Oct 2024 10:30:30 -0400 Subject: [PATCH 04/13] Added content item conversion --- .../Handlers/MigratePagesCommandHandler.cs | 167 ++++++++++-------- .../Mappers/ContentItemMapper.cs | 125 ++++++------- .../Migration.Tool.CLI.csproj.user | 9 + Migration.Tool.CLI/appsettings.json | 3 +- Migration.Tool.Common/ConfigurationNames.cs | 1 + Migration.Tool.Common/ToolConfiguration.cs | 9 + .../ClassMappings/ClassMappingSample.cs | 55 ++++++ .../ServiceCollectionExtensions.cs | 2 + 8 files changed, 234 insertions(+), 137 deletions(-) create mode 100644 Migration.Tool.CLI/Migration.Tool.CLI.csproj.user diff --git a/KVA/Migration.Tool.Source/Handlers/MigratePagesCommandHandler.cs b/KVA/Migration.Tool.Source/Handlers/MigratePagesCommandHandler.cs index 66f3b4b8..9d8c51d1 100644 --- a/KVA/Migration.Tool.Source/Handlers/MigratePagesCommandHandler.cs +++ b/KVA/Migration.Tool.Source/Handlers/MigratePagesCommandHandler.cs @@ -1,6 +1,3 @@ -using System.Collections.Concurrent; -using System.Diagnostics; - using CMS.ContentEngine; using CMS.ContentEngine.Internal; using CMS.Core; @@ -12,12 +9,9 @@ using CMS.Websites.Routing.Internal; using Kentico.Xperience.UMT.Model; using Kentico.Xperience.UMT.Services; - using MediatR; - using Microsoft.Data.SqlClient; using Microsoft.Extensions.Logging; - using Migration.Tool.Common; using Migration.Tool.Common.Abstractions; using Migration.Tool.Common.Helpers; @@ -30,9 +24,10 @@ using Migration.Tool.Source.Model; using Migration.Tool.Source.Providers; using Migration.Tool.Source.Services; - using Newtonsoft.Json; using Newtonsoft.Json.Linq; +using System.Collections.Concurrent; +using System.Diagnostics; namespace Migration.Tool.Source.Handlers; // ReSharper disable once UnusedType.Global @@ -218,8 +213,26 @@ public async Task Handle(MigratePagesCommand request, Cancellatio var commonDataInfos = new List(); foreach (var umtModel in results) { - var result = await importer.ImportAsync(umtModel); - if (result is { Success: false }) + var isReusable = toolConfiguration.ClassNamesConvertToContentHub.Contains(targetClass?.ClassName) || targetClass?.ClassContentTypeType is ClassContentTypeType.REUSABLE; + + + var skipWebPageItem = umtModel is WebPageItemModel && isReusable; + + IImportResult result = new ImportResult { Success = true }; + if (skipWebPageItem) + { + if (targetClass is { } && targetClass.ClassContentTypeType == ClassContentTypeType.WEBSITE) + { + targetClass.ClassContentTypeType = ClassContentTypeType.REUSABLE; + targetClass.ClassWebPageHasUrl = false; + targetClass.Update(); + } + } + else + { + result = await importer.ImportAsync(umtModel); + } + if (result is { Success: false } && !skipWebPageItem) { logger.LogError("Failed to import: {Exception}, {ValidationResults}", result.Exception, JsonConvert.SerializeObject(result.ModelValidationResults)); } @@ -227,21 +240,21 @@ public async Task Handle(MigratePagesCommand request, Cancellatio switch (result) { case { Success: true, Imported: ContentItemCommonDataInfo ccid }: - { - commonDataInfos.Add(ccid); - Debug.Assert(ccid.ContentItemCommonDataContentLanguageID != 0, "ccid.ContentItemCommonDataContentLanguageID != 0"); - break; - } + { + commonDataInfos.Add(ccid); + Debug.Assert(ccid.ContentItemCommonDataContentLanguageID != 0, "ccid.ContentItemCommonDataContentLanguageID != 0"); + break; + } case { Success: true, Imported: ContentItemLanguageMetadataInfo cclm }: - { - Debug.Assert(cclm.ContentItemLanguageMetadataContentLanguageID != 0, "ccid.ContentItemCommonDataContentLanguageID != 0"); - break; - } + { + Debug.Assert(cclm.ContentItemLanguageMetadataContentLanguageID != 0, "ccid.ContentItemCommonDataContentLanguageID != 0"); + break; + } case { Success: true, Imported: WebPageItemInfo wp }: - { - webPageItemInfo = wp; - break; - } + { + webPageItemInfo = wp; + break; + } default: break; @@ -541,24 +554,24 @@ private void LogImportResult(IImportResult importResult) switch (importResult) { case { Success: true, Imported: WebPageUrlPathInfo imported }: - { - logger.LogInformation("Page url path imported '{Path}' '{Guid}'", imported.WebPageUrlPath, imported.WebPageUrlPathGUID); - break; - } + { + logger.LogInformation("Page url path imported '{Path}' '{Guid}'", imported.WebPageUrlPath, imported.WebPageUrlPathGUID); + break; + } case { Success: false, Exception: { } exception }: - { - logger.LogError("Failed to import page url path: {Error}", exception.ToString()); - break; - } - case { Success: false, ModelValidationResults: { } validation }: - { - foreach (var validationResult in validation) { - logger.LogError("Failed to import page url path {Members}: {Error}", string.Join(",", validationResult.MemberNames), validationResult.ErrorMessage); + logger.LogError("Failed to import page url path: {Error}", exception.ToString()); + break; } + case { Success: false, ModelValidationResults: { } validation }: + { + foreach (var validationResult in validation) + { + logger.LogError("Failed to import page url path {Members}: {Error}", string.Join(",", validationResult.MemberNames), validationResult.ErrorMessage); + } - break; - } + break; + } default: break; @@ -583,53 +596,53 @@ private void MigrateFormerUrls(ICmsTree ksNode, WebPageItemInfo targetPage) { case CmsPageFormerUrlPathK11: case CmsPageFormerUrlPathK12: - { - logger.LogError("Unexpected type '{Type}'", cmsPageFormerUrlPath.GetType().FullName); - break; - } + { + logger.LogError("Unexpected type '{Type}'", cmsPageFormerUrlPath.GetType().FullName); + break; + } case CmsPageFormerUrlPathK13 pfup: - { - try { - var languageInfo = languages.GetOrAdd( - pfup.PageFormerUrlPathCulture, - s => ContentLanguageInfoProvider.ProviderObject.Get().WhereEquals(nameof(ContentLanguageInfo.ContentLanguageName), s).SingleOrDefault() ?? throw new InvalidOperationException($"Missing content language '{s}'") - ); - - var ktPath = WebPageFormerUrlPathInfo.Provider.Get() - .WhereEquals(nameof(WebPageFormerUrlPathInfo.WebPageFormerUrlPathHash), GetWebPageUrlPathHashQueryExpression(pfup.PageFormerUrlPathUrlPath)) - .WhereEquals(nameof(WebPageFormerUrlPathInfo.WebPageFormerUrlPathWebsiteChannelID), targetPage.WebPageItemWebsiteChannelID) - .WhereEquals(nameof(WebPageFormerUrlPathInfo.WebPageFormerUrlPathContentLanguageID), languageInfo.ContentLanguageID) - .SingleOrDefault(); - - if (ktPath != null) + try { - protocol.FetchedTarget(ktPath); - } + var languageInfo = languages.GetOrAdd( + pfup.PageFormerUrlPathCulture, + s => ContentLanguageInfoProvider.ProviderObject.Get().WhereEquals(nameof(ContentLanguageInfo.ContentLanguageName), s).SingleOrDefault() ?? throw new InvalidOperationException($"Missing content language '{s}'") + ); + + var ktPath = WebPageFormerUrlPathInfo.Provider.Get() + .WhereEquals(nameof(WebPageFormerUrlPathInfo.WebPageFormerUrlPathHash), GetWebPageUrlPathHashQueryExpression(pfup.PageFormerUrlPathUrlPath)) + .WhereEquals(nameof(WebPageFormerUrlPathInfo.WebPageFormerUrlPathWebsiteChannelID), targetPage.WebPageItemWebsiteChannelID) + .WhereEquals(nameof(WebPageFormerUrlPathInfo.WebPageFormerUrlPathContentLanguageID), languageInfo.ContentLanguageID) + .SingleOrDefault(); + + if (ktPath != null) + { + protocol.FetchedTarget(ktPath); + } - var webPageFormerUrlPathInfo = ktPath ?? new WebPageFormerUrlPathInfo(); - webPageFormerUrlPathInfo.WebPageFormerUrlPath = pfup.PageFormerUrlPathUrlPath; - webPageFormerUrlPathInfo.WebPageFormerUrlPathHash = modelFacade.HashPath(pfup.PageFormerUrlPathUrlPath); - webPageFormerUrlPathInfo.WebPageFormerUrlPathWebPageItemID = targetPage.WebPageItemID; - webPageFormerUrlPathInfo.WebPageFormerUrlPathWebsiteChannelID = targetPage.WebPageItemWebsiteChannelID; - webPageFormerUrlPathInfo.WebPageFormerUrlPathContentLanguageID = languageInfo.ContentLanguageID; - webPageFormerUrlPathInfo.WebPageFormerUrlPathLastModified = pfup.PageFormerUrlPathLastModified; + var webPageFormerUrlPathInfo = ktPath ?? new WebPageFormerUrlPathInfo(); + webPageFormerUrlPathInfo.WebPageFormerUrlPath = pfup.PageFormerUrlPathUrlPath; + webPageFormerUrlPathInfo.WebPageFormerUrlPathHash = modelFacade.HashPath(pfup.PageFormerUrlPathUrlPath); + webPageFormerUrlPathInfo.WebPageFormerUrlPathWebPageItemID = targetPage.WebPageItemID; + webPageFormerUrlPathInfo.WebPageFormerUrlPathWebsiteChannelID = targetPage.WebPageItemWebsiteChannelID; + webPageFormerUrlPathInfo.WebPageFormerUrlPathContentLanguageID = languageInfo.ContentLanguageID; + webPageFormerUrlPathInfo.WebPageFormerUrlPathLastModified = pfup.PageFormerUrlPathLastModified; - WebPageFormerUrlPathInfo.Provider.Set(webPageFormerUrlPathInfo); - logger.LogInformation("Former page url path imported '{Path}'", webPageFormerUrlPathInfo.WebPageFormerUrlPath); - } - catch (Exception ex) - { - protocol.Append(HandbookReferences - .ErrorCreatingTargetInstance(ex) - .NeedsManualAction() - .WithIdentityPrint(pfup) - ); - logger.LogError("Failed to import page former url path: {Exception}", ex); - } + WebPageFormerUrlPathInfo.Provider.Set(webPageFormerUrlPathInfo); + logger.LogInformation("Former page url path imported '{Path}'", webPageFormerUrlPathInfo.WebPageFormerUrlPath); + } + catch (Exception ex) + { + protocol.Append(HandbookReferences + .ErrorCreatingTargetInstance(ex) + .NeedsManualAction() + .WithIdentityPrint(pfup) + ); + logger.LogError("Failed to import page former url path: {Exception}", ex); + } - break; - } + break; + } default: throw new ArgumentOutOfRangeException(nameof(cmsPageFormerUrlPath)); } diff --git a/KVA/Migration.Tool.Source/Mappers/ContentItemMapper.cs b/KVA/Migration.Tool.Source/Mappers/ContentItemMapper.cs index 399ac595..c69dd7b3 100644 --- a/KVA/Migration.Tool.Source/Mappers/ContentItemMapper.cs +++ b/KVA/Migration.Tool.Source/Mappers/ContentItemMapper.cs @@ -1,4 +1,3 @@ -using System.Diagnostics; using CMS.ContentEngine; using CMS.ContentEngine.Internal; using CMS.Core; @@ -23,6 +22,7 @@ using Migration.Tool.Source.Providers; using Migration.Tool.Source.Services; using Newtonsoft.Json.Linq; +using System.Diagnostics; namespace Migration.Tool.Source.Mappers; @@ -66,19 +66,26 @@ protected override IEnumerable MapInternal(CmsTreeMapperSource source var sourceNodeClass = modelFacade.SelectById(cmsTree.NodeClassID) ?? throw new InvalidOperationException($"Fatal: node class is missing, class id '{cmsTree.NodeClassID}'"); var mapping = classMappingProvider.GetMapping(sourceNodeClass.ClassName); var targetClassGuid = sourceNodeClass.ClassGUID; + DataClassInfo targetClassInfo = null; if (mapping != null) { - targetClassGuid = DataClassInfoProvider.ProviderObject.Get(mapping.TargetClassName)?.ClassGUID ?? throw new InvalidOperationException($"Unable to find target class '{mapping.TargetClassName}'"); + targetClassInfo = DataClassInfoProvider.ProviderObject.Get(mapping.TargetClassName) ?? throw new InvalidOperationException($"Unable to find target class '{mapping.TargetClassName}'"); + targetClassGuid = targetClassInfo.ClassGUID; } bool migratedAsContentFolder = sourceNodeClass.ClassName.Equals("cms.folder", StringComparison.InvariantCultureIgnoreCase) && !configuration.UseDeprecatedFolderPageType.GetValueOrDefault(false); var contentItemGuid = spoiledGuidContext.EnsureNodeGuid(cmsTree.NodeGUID, cmsTree.NodeSiteID, cmsTree.NodeID); + + string className = targetClassInfo?.ClassName ?? sourceNodeClass.ClassName; + bool isMappedTypeReusable = targetClassInfo?.ClassContentTypeType is ClassContentTypeType.REUSABLE; + var isReusable = configuration.ClassNamesConvertToContentHub.Contains(className) || isMappedTypeReusable; + yield return new ContentItemModel { ContentItemGUID = contentItemGuid, ContentItemName = safeNodeName, - ContentItemIsReusable = false, // page is not reusable + ContentItemIsReusable = isReusable, ContentItemIsSecured = cmsTree.IsSecuredNode ?? false, ContentItemDataClassGuid = migratedAsContentFolder ? null : targetClassGuid, ContentItemChannelGuid = siteGuid @@ -190,21 +197,21 @@ protected override IEnumerable MapInternal(CmsTreeMapperSource source switch (cmsDocument) { case CmsDocumentK11: - { - break; - } + { + break; + } case CmsDocumentK12 doc: - { - contentItemCommonDataPageBuilderWidgets = doc.DocumentPageBuilderWidgets; - contentItemCommonDataPageTemplateConfiguration = doc.DocumentPageTemplateConfiguration; - break; - } + { + contentItemCommonDataPageBuilderWidgets = doc.DocumentPageBuilderWidgets; + contentItemCommonDataPageTemplateConfiguration = doc.DocumentPageTemplateConfiguration; + break; + } case CmsDocumentK13 doc: - { - contentItemCommonDataPageBuilderWidgets = doc.DocumentPageBuilderWidgets; - contentItemCommonDataPageTemplateConfiguration = doc.DocumentPageTemplateConfiguration; - break; - } + { + contentItemCommonDataPageBuilderWidgets = doc.DocumentPageBuilderWidgets; + contentItemCommonDataPageTemplateConfiguration = doc.DocumentPageTemplateConfiguration; + break; + } default: break; @@ -520,23 +527,23 @@ IClassMapping mapping switch (mapping?.GetMapping(targetColumnName, sourceNodeClass.ClassName)) { case FieldMappingWithConversion fieldMappingWithConversion: - { - targetFieldName = fieldMappingWithConversion.TargetFieldName; - valueConvertor = fieldMappingWithConversion.Converter; - break; - } + { + targetFieldName = fieldMappingWithConversion.TargetFieldName; + valueConvertor = fieldMappingWithConversion.Converter; + break; + } case FieldMapping fieldMapping: - { - targetFieldName = fieldMapping.TargetFieldName; - valueConvertor = sourceValue => sourceValue; - break; - } + { + targetFieldName = fieldMapping.TargetFieldName; + valueConvertor = sourceValue => sourceValue; + break; + } case null: - { - targetFieldName = targetColumnName; - valueConvertor = sourceValue => sourceValue; - break; - } + { + targetFieldName = targetColumnName; + valueConvertor = sourceValue => sourceValue; + break; + } default: break; @@ -630,15 +637,15 @@ IClassMapping mapping switch (await fmb.MigrateValue(sourceValue, fvmc)) { case { Success: true } result: - { - target[targetFieldName] = valueConvertor.Invoke(result.MigratedValue); - break; - } + { + target[targetFieldName] = valueConvertor.Invoke(result.MigratedValue); + break; + } case { Success: false }: - { - logger.LogError("Error while migrating field '{Field}' value {Value}", targetFieldName, sourceValue); - break; - } + { + logger.LogError("Error while migrating field '{Field}' value {Value}", targetFieldName, sourceValue); + break; + } default: break; @@ -670,33 +677,33 @@ IClassMapping mapping switch (result) { case { LinkKind: MediaLinkKind.Guid or MediaLinkKind.DirectMediaPath, MediaKind: MediaKind.MediaFile }: - { - var mediaFile = MediaHelper.GetMediaFile(result, modelFacade); - if (mediaFile is null) { - return original; - } + var mediaFile = MediaHelper.GetMediaFile(result, modelFacade); + if (mediaFile is null) + { + return original; + } - return assetFacade.GetAssetUri(mediaFile); - } + return assetFacade.GetAssetUri(mediaFile); + } case { LinkKind: MediaLinkKind.Guid, MediaKind: MediaKind.Attachment, MediaGuid: { } mediaGuid, LinkSiteId: var linkSiteId }: - { - var attachment = MediaHelper.GetAttachment(result, modelFacade); - if (attachment is null) { - return original; - } + var attachment = MediaHelper.GetAttachment(result, modelFacade); + if (attachment is null) + { + return original; + } - await attachmentMigrator.MigrateAttachment(attachment); + await attachmentMigrator.MigrateAttachment(attachment); - string? culture = null; - if (attachment.AttachmentDocumentID is { } attachmentDocumentId) - { - culture = modelFacade.SelectById(attachmentDocumentId)?.DocumentCulture; - } + string? culture = null; + if (attachment.AttachmentDocumentID is { } attachmentDocumentId) + { + culture = modelFacade.SelectById(attachmentDocumentId)?.DocumentCulture; + } - return assetFacade.GetAssetUri(attachment, culture); - } + return assetFacade.GetAssetUri(attachment, culture); + } default: break; diff --git a/Migration.Tool.CLI/Migration.Tool.CLI.csproj.user b/Migration.Tool.CLI/Migration.Tool.CLI.csproj.user new file mode 100644 index 00000000..854e66c6 --- /dev/null +++ b/Migration.Tool.CLI/Migration.Tool.CLI.csproj.user @@ -0,0 +1,9 @@ + + + + ProjectDebugger + + + Migration + + \ No newline at end of file diff --git a/Migration.Tool.CLI/appsettings.json b/Migration.Tool.CLI/appsettings.json index 24fbbacf..79371700 100644 --- a/Migration.Tool.CLI/appsettings.json +++ b/Migration.Tool.CLI/appsettings.json @@ -19,7 +19,7 @@ "MigrationProtocolPath": "C:\\Logs\\protocol.txt", "KxConnectionString": "[TODO]", "KxCmsDirPath": "[TODO]", - "XbKDirPath": "[TODO]", + "XbKDirPath": "[TODO]", "XbKApiSettings": { "ConnectionStrings": { "CMSConnectionString": "[TODO]" @@ -29,6 +29,7 @@ "MigrateMediaToMediaLibrary": false, "UseDeprecatedFolderPageType": false, "CreateReusableFieldSchemaForClasses": "", + "ConvertClassesToContentHub": "", "OptInFeatures": { "QuerySourceInstanceApi": { "Enabled": false, diff --git a/Migration.Tool.Common/ConfigurationNames.cs b/Migration.Tool.Common/ConfigurationNames.cs index b1d10b0e..11e67997 100644 --- a/Migration.Tool.Common/ConfigurationNames.cs +++ b/Migration.Tool.Common/ConfigurationNames.cs @@ -22,6 +22,7 @@ public class ConfigurationNames public const string UseDeprecatedFolderPageType = "UseDeprecatedFolderPageType"; public const string ExcludeCodeNames = "ExcludeCodeNames"; + public const string ConvertClassesToContentHub = "ConvertClassesToContentHub"; public const string ExplicitPrimaryKeyMapping = "ExplicitPrimaryKeyMapping"; public const string SiteName = "SiteName"; diff --git a/Migration.Tool.Common/ToolConfiguration.cs b/Migration.Tool.Common/ToolConfiguration.cs index bf01b952..fe50454e 100644 --- a/Migration.Tool.Common/ToolConfiguration.cs +++ b/Migration.Tool.Common/ToolConfiguration.cs @@ -49,12 +49,20 @@ public class ToolConfiguration [ConfigurationKeyName(ConfigurationNames.CreateReusableFieldSchemaForClasses)] public string? CreateReusableFieldSchemaForClasses { get; set; } + [ConfigurationKeyName(ConfigurationNames.ConvertClassesToContentHub)] + public string? ConvertClassesToContentHub { get; set; } + public IReadOnlySet ClassNamesCreateReusableSchema => classNamesCreateReusableSchema ??= new HashSet( (CreateReusableFieldSchemaForClasses?.Split(new[] { ',', ';' }, StringSplitOptions.RemoveEmptyEntries) ?? []).Select(x => x.Trim()), StringComparer.InvariantCultureIgnoreCase ); + public IReadOnlySet ClassNamesConvertToContentHub => classNamesConvertToContentHub ??= new HashSet( + (ConvertClassesToContentHub?.Split(new[] { ',', ';' }, StringSplitOptions.RemoveEmptyEntries) ?? []).Select(x => x.Trim()), + StringComparer.InvariantCultureIgnoreCase + ); + #region Opt-in features [ConfigurationKeyName(ConfigurationNames.OptInFeatures)] @@ -97,6 +105,7 @@ public void SetXbKConnectionStringIfNotEmpty(string? connectionString) #region Path to root directory of target instance private HashSet? classNamesCreateReusableSchema; + private HashSet? classNamesConvertToContentHub; private string? xbKConnectionString; [ConfigurationKeyName(ConfigurationNames.XbKDirPath)] diff --git a/Migration.Tool.Extensions/ClassMappings/ClassMappingSample.cs b/Migration.Tool.Extensions/ClassMappings/ClassMappingSample.cs index 19e6c03a..bdaa8cea 100644 --- a/Migration.Tool.Extensions/ClassMappings/ClassMappingSample.cs +++ b/Migration.Tool.Extensions/ClassMappings/ClassMappingSample.cs @@ -10,6 +10,61 @@ namespace Migration.Tool.Extensions.ClassMappings; public static class ClassMappingSample { + public static IServiceCollection AddReusableRemodelingSample(this IServiceCollection serviceCollection) + { + const string targetClassName = "DancingGoatCore.CoffeeRemodeled"; + // declare target class + var m = new MultiClassMapping(targetClassName, target => + { + target.ClassName = targetClassName; + target.ClassTableName = "DancingGoatCore_CoffeeRemodeled"; + target.ClassDisplayName = "Coffee remodeled"; + target.ClassType = ClassType.CONTENT_TYPE; + target.ClassContentTypeType = ClassContentTypeType.REUSABLE; + target.ClassWebPageHasUrl = false; + }); + + // set new primary key + m.BuildField("CoffeeRemodeledID").AsPrimaryKey(); + + // change fields according to new requirements + const string sourceClassName = "DancingGoatCore.Coffee"; + m + .BuildField("FarmRM") + .SetFrom(sourceClassName, "CoffeeFarm", true) + .WithFieldPatch(f => f.SetPropertyValue(FormFieldPropertyEnum.FieldCaption, "Farm RM")); + + m + .BuildField("CoffeeCountryRM") + .WithFieldPatch(f => f.Caption = "Country RM") + .SetFrom(sourceClassName, "CoffeeCountry", true); + + m + .BuildField("CoffeeVarietyRM") + .SetFrom(sourceClassName, "CoffeeVariety", true) + .WithFieldPatch(f => f.SetPropertyValue(FormFieldPropertyEnum.FieldCaption, "Variety RM")); + + m + .BuildField("CoffeeProcessingRM") + .SetFrom(sourceClassName, "CoffeeProcessing", true) + .WithFieldPatch(f => f.SetPropertyValue(FormFieldPropertyEnum.FieldCaption, "Processing RM")); + + m + .BuildField("CoffeeAltitudeRM") + .SetFrom(sourceClassName, "CoffeeAltitude", true) + .WithFieldPatch(f => f.SetPropertyValue(FormFieldPropertyEnum.FieldCaption, "Altitude RM")); + + m + .BuildField("CoffeeIsDecafRM") + .SetFrom(sourceClassName, "CoffeeIsDecaf", true) + .WithFieldPatch(f => f.SetPropertyValue(FormFieldPropertyEnum.FieldCaption, "IsDecaf RM")); + + // register class mapping + serviceCollection.AddSingleton(m); + + return serviceCollection; + } + public static IServiceCollection AddSimpleRemodelingSample(this IServiceCollection serviceCollection) { const string targetClassName = "DancingGoatCore.CoffeeRemodeled"; diff --git a/Migration.Tool.Extensions/ServiceCollectionExtensions.cs b/Migration.Tool.Extensions/ServiceCollectionExtensions.cs index 61c751d3..c0d440c2 100644 --- a/Migration.Tool.Extensions/ServiceCollectionExtensions.cs +++ b/Migration.Tool.Extensions/ServiceCollectionExtensions.cs @@ -16,8 +16,10 @@ public static IServiceCollection UseCustomizations(this IServiceCollection servi services.AddTransient(); services.AddTransient(); + // services.AddClassMergeExample(); // services.AddSimpleRemodelingSample(); + //services.AddReusableRemodelingSample(); // services.AddReusableSchemaIntegrationSample(); return services; } From 0bb3860c72a682fccb8074d383f5abf893fc5e66 Mon Sep 17 00:00:00 2001 From: Anthony Marquez Date: Tue, 29 Oct 2024 09:41:37 -0400 Subject: [PATCH 05/13] Fixed code formatting --- .../Handlers/MigratePagesCommandHandler.cs | 144 +++++++++--------- .../Mappers/ContentItemMapper.cs | 116 +++++++------- 2 files changed, 130 insertions(+), 130 deletions(-) diff --git a/KVA/Migration.Tool.Source/Handlers/MigratePagesCommandHandler.cs b/KVA/Migration.Tool.Source/Handlers/MigratePagesCommandHandler.cs index 9d8c51d1..05b95f63 100644 --- a/KVA/Migration.Tool.Source/Handlers/MigratePagesCommandHandler.cs +++ b/KVA/Migration.Tool.Source/Handlers/MigratePagesCommandHandler.cs @@ -1,3 +1,5 @@ +using System.Collections.Concurrent; +using System.Diagnostics; using CMS.ContentEngine; using CMS.ContentEngine.Internal; using CMS.Core; @@ -26,8 +28,6 @@ using Migration.Tool.Source.Services; using Newtonsoft.Json; using Newtonsoft.Json.Linq; -using System.Collections.Concurrent; -using System.Diagnostics; namespace Migration.Tool.Source.Handlers; // ReSharper disable once UnusedType.Global @@ -213,10 +213,10 @@ public async Task Handle(MigratePagesCommand request, Cancellatio var commonDataInfos = new List(); foreach (var umtModel in results) { - var isReusable = toolConfiguration.ClassNamesConvertToContentHub.Contains(targetClass?.ClassName) || targetClass?.ClassContentTypeType is ClassContentTypeType.REUSABLE; + bool isReusable = toolConfiguration.ClassNamesConvertToContentHub.Contains(targetClass?.ClassName) || targetClass?.ClassContentTypeType is ClassContentTypeType.REUSABLE; - var skipWebPageItem = umtModel is WebPageItemModel && isReusable; + bool skipWebPageItem = umtModel is WebPageItemModel && isReusable; IImportResult result = new ImportResult { Success = true }; if (skipWebPageItem) @@ -240,21 +240,21 @@ public async Task Handle(MigratePagesCommand request, Cancellatio switch (result) { case { Success: true, Imported: ContentItemCommonDataInfo ccid }: - { - commonDataInfos.Add(ccid); - Debug.Assert(ccid.ContentItemCommonDataContentLanguageID != 0, "ccid.ContentItemCommonDataContentLanguageID != 0"); - break; - } + { + commonDataInfos.Add(ccid); + Debug.Assert(ccid.ContentItemCommonDataContentLanguageID != 0, "ccid.ContentItemCommonDataContentLanguageID != 0"); + break; + } case { Success: true, Imported: ContentItemLanguageMetadataInfo cclm }: - { - Debug.Assert(cclm.ContentItemLanguageMetadataContentLanguageID != 0, "ccid.ContentItemCommonDataContentLanguageID != 0"); - break; - } + { + Debug.Assert(cclm.ContentItemLanguageMetadataContentLanguageID != 0, "ccid.ContentItemCommonDataContentLanguageID != 0"); + break; + } case { Success: true, Imported: WebPageItemInfo wp }: - { - webPageItemInfo = wp; - break; - } + { + webPageItemInfo = wp; + break; + } default: break; @@ -554,25 +554,25 @@ private void LogImportResult(IImportResult importResult) switch (importResult) { case { Success: true, Imported: WebPageUrlPathInfo imported }: - { - logger.LogInformation("Page url path imported '{Path}' '{Guid}'", imported.WebPageUrlPath, imported.WebPageUrlPathGUID); - break; - } + { + logger.LogInformation("Page url path imported '{Path}' '{Guid}'", imported.WebPageUrlPath, imported.WebPageUrlPathGUID); + break; + } case { Success: false, Exception: { } exception }: - { - logger.LogError("Failed to import page url path: {Error}", exception.ToString()); - break; - } + { + logger.LogError("Failed to import page url path: {Error}", exception.ToString()); + break; + } case { Success: false, ModelValidationResults: { } validation }: + { + foreach (var validationResult in validation) { - foreach (var validationResult in validation) - { - logger.LogError("Failed to import page url path {Members}: {Error}", string.Join(",", validationResult.MemberNames), validationResult.ErrorMessage); - } - - break; + logger.LogError("Failed to import page url path {Members}: {Error}", string.Join(",", validationResult.MemberNames), validationResult.ErrorMessage); } + break; + } + default: break; } @@ -596,53 +596,53 @@ private void MigrateFormerUrls(ICmsTree ksNode, WebPageItemInfo targetPage) { case CmsPageFormerUrlPathK11: case CmsPageFormerUrlPathK12: - { - logger.LogError("Unexpected type '{Type}'", cmsPageFormerUrlPath.GetType().FullName); - break; - } + { + logger.LogError("Unexpected type '{Type}'", cmsPageFormerUrlPath.GetType().FullName); + break; + } case CmsPageFormerUrlPathK13 pfup: + { + try { - try + var languageInfo = languages.GetOrAdd( + pfup.PageFormerUrlPathCulture, + s => ContentLanguageInfoProvider.ProviderObject.Get().WhereEquals(nameof(ContentLanguageInfo.ContentLanguageName), s).SingleOrDefault() ?? throw new InvalidOperationException($"Missing content language '{s}'") + ); + + var ktPath = WebPageFormerUrlPathInfo.Provider.Get() + .WhereEquals(nameof(WebPageFormerUrlPathInfo.WebPageFormerUrlPathHash), GetWebPageUrlPathHashQueryExpression(pfup.PageFormerUrlPathUrlPath)) + .WhereEquals(nameof(WebPageFormerUrlPathInfo.WebPageFormerUrlPathWebsiteChannelID), targetPage.WebPageItemWebsiteChannelID) + .WhereEquals(nameof(WebPageFormerUrlPathInfo.WebPageFormerUrlPathContentLanguageID), languageInfo.ContentLanguageID) + .SingleOrDefault(); + + if (ktPath != null) { - var languageInfo = languages.GetOrAdd( - pfup.PageFormerUrlPathCulture, - s => ContentLanguageInfoProvider.ProviderObject.Get().WhereEquals(nameof(ContentLanguageInfo.ContentLanguageName), s).SingleOrDefault() ?? throw new InvalidOperationException($"Missing content language '{s}'") - ); - - var ktPath = WebPageFormerUrlPathInfo.Provider.Get() - .WhereEquals(nameof(WebPageFormerUrlPathInfo.WebPageFormerUrlPathHash), GetWebPageUrlPathHashQueryExpression(pfup.PageFormerUrlPathUrlPath)) - .WhereEquals(nameof(WebPageFormerUrlPathInfo.WebPageFormerUrlPathWebsiteChannelID), targetPage.WebPageItemWebsiteChannelID) - .WhereEquals(nameof(WebPageFormerUrlPathInfo.WebPageFormerUrlPathContentLanguageID), languageInfo.ContentLanguageID) - .SingleOrDefault(); - - if (ktPath != null) - { - protocol.FetchedTarget(ktPath); - } - - var webPageFormerUrlPathInfo = ktPath ?? new WebPageFormerUrlPathInfo(); - webPageFormerUrlPathInfo.WebPageFormerUrlPath = pfup.PageFormerUrlPathUrlPath; - webPageFormerUrlPathInfo.WebPageFormerUrlPathHash = modelFacade.HashPath(pfup.PageFormerUrlPathUrlPath); - webPageFormerUrlPathInfo.WebPageFormerUrlPathWebPageItemID = targetPage.WebPageItemID; - webPageFormerUrlPathInfo.WebPageFormerUrlPathWebsiteChannelID = targetPage.WebPageItemWebsiteChannelID; - webPageFormerUrlPathInfo.WebPageFormerUrlPathContentLanguageID = languageInfo.ContentLanguageID; - webPageFormerUrlPathInfo.WebPageFormerUrlPathLastModified = pfup.PageFormerUrlPathLastModified; - - WebPageFormerUrlPathInfo.Provider.Set(webPageFormerUrlPathInfo); - logger.LogInformation("Former page url path imported '{Path}'", webPageFormerUrlPathInfo.WebPageFormerUrlPath); - } - catch (Exception ex) - { - protocol.Append(HandbookReferences - .ErrorCreatingTargetInstance(ex) - .NeedsManualAction() - .WithIdentityPrint(pfup) - ); - logger.LogError("Failed to import page former url path: {Exception}", ex); + protocol.FetchedTarget(ktPath); } - break; + var webPageFormerUrlPathInfo = ktPath ?? new WebPageFormerUrlPathInfo(); + webPageFormerUrlPathInfo.WebPageFormerUrlPath = pfup.PageFormerUrlPathUrlPath; + webPageFormerUrlPathInfo.WebPageFormerUrlPathHash = modelFacade.HashPath(pfup.PageFormerUrlPathUrlPath); + webPageFormerUrlPathInfo.WebPageFormerUrlPathWebPageItemID = targetPage.WebPageItemID; + webPageFormerUrlPathInfo.WebPageFormerUrlPathWebsiteChannelID = targetPage.WebPageItemWebsiteChannelID; + webPageFormerUrlPathInfo.WebPageFormerUrlPathContentLanguageID = languageInfo.ContentLanguageID; + webPageFormerUrlPathInfo.WebPageFormerUrlPathLastModified = pfup.PageFormerUrlPathLastModified; + + WebPageFormerUrlPathInfo.Provider.Set(webPageFormerUrlPathInfo); + logger.LogInformation("Former page url path imported '{Path}'", webPageFormerUrlPathInfo.WebPageFormerUrlPath); + } + catch (Exception ex) + { + protocol.Append(HandbookReferences + .ErrorCreatingTargetInstance(ex) + .NeedsManualAction() + .WithIdentityPrint(pfup) + ); + logger.LogError("Failed to import page former url path: {Exception}", ex); } + + break; + } default: throw new ArgumentOutOfRangeException(nameof(cmsPageFormerUrlPath)); } diff --git a/KVA/Migration.Tool.Source/Mappers/ContentItemMapper.cs b/KVA/Migration.Tool.Source/Mappers/ContentItemMapper.cs index c69dd7b3..9df6a391 100644 --- a/KVA/Migration.Tool.Source/Mappers/ContentItemMapper.cs +++ b/KVA/Migration.Tool.Source/Mappers/ContentItemMapper.cs @@ -1,3 +1,4 @@ +using System.Diagnostics; using CMS.ContentEngine; using CMS.ContentEngine.Internal; using CMS.Core; @@ -22,7 +23,6 @@ using Migration.Tool.Source.Providers; using Migration.Tool.Source.Services; using Newtonsoft.Json.Linq; -using System.Diagnostics; namespace Migration.Tool.Source.Mappers; @@ -79,7 +79,7 @@ protected override IEnumerable MapInternal(CmsTreeMapperSource source string className = targetClassInfo?.ClassName ?? sourceNodeClass.ClassName; bool isMappedTypeReusable = targetClassInfo?.ClassContentTypeType is ClassContentTypeType.REUSABLE; - var isReusable = configuration.ClassNamesConvertToContentHub.Contains(className) || isMappedTypeReusable; + bool isReusable = configuration.ClassNamesConvertToContentHub.Contains(className) || isMappedTypeReusable; yield return new ContentItemModel { @@ -197,21 +197,21 @@ protected override IEnumerable MapInternal(CmsTreeMapperSource source switch (cmsDocument) { case CmsDocumentK11: - { - break; - } + { + break; + } case CmsDocumentK12 doc: - { - contentItemCommonDataPageBuilderWidgets = doc.DocumentPageBuilderWidgets; - contentItemCommonDataPageTemplateConfiguration = doc.DocumentPageTemplateConfiguration; - break; - } + { + contentItemCommonDataPageBuilderWidgets = doc.DocumentPageBuilderWidgets; + contentItemCommonDataPageTemplateConfiguration = doc.DocumentPageTemplateConfiguration; + break; + } case CmsDocumentK13 doc: - { - contentItemCommonDataPageBuilderWidgets = doc.DocumentPageBuilderWidgets; - contentItemCommonDataPageTemplateConfiguration = doc.DocumentPageTemplateConfiguration; - break; - } + { + contentItemCommonDataPageBuilderWidgets = doc.DocumentPageBuilderWidgets; + contentItemCommonDataPageTemplateConfiguration = doc.DocumentPageTemplateConfiguration; + break; + } default: break; @@ -527,23 +527,23 @@ IClassMapping mapping switch (mapping?.GetMapping(targetColumnName, sourceNodeClass.ClassName)) { case FieldMappingWithConversion fieldMappingWithConversion: - { - targetFieldName = fieldMappingWithConversion.TargetFieldName; - valueConvertor = fieldMappingWithConversion.Converter; - break; - } + { + targetFieldName = fieldMappingWithConversion.TargetFieldName; + valueConvertor = fieldMappingWithConversion.Converter; + break; + } case FieldMapping fieldMapping: - { - targetFieldName = fieldMapping.TargetFieldName; - valueConvertor = sourceValue => sourceValue; - break; - } + { + targetFieldName = fieldMapping.TargetFieldName; + valueConvertor = sourceValue => sourceValue; + break; + } case null: - { - targetFieldName = targetColumnName; - valueConvertor = sourceValue => sourceValue; - break; - } + { + targetFieldName = targetColumnName; + valueConvertor = sourceValue => sourceValue; + break; + } default: break; @@ -637,15 +637,15 @@ IClassMapping mapping switch (await fmb.MigrateValue(sourceValue, fvmc)) { case { Success: true } result: - { - target[targetFieldName] = valueConvertor.Invoke(result.MigratedValue); - break; - } + { + target[targetFieldName] = valueConvertor.Invoke(result.MigratedValue); + break; + } case { Success: false }: - { - logger.LogError("Error while migrating field '{Field}' value {Value}", targetFieldName, sourceValue); - break; - } + { + logger.LogError("Error while migrating field '{Field}' value {Value}", targetFieldName, sourceValue); + break; + } default: break; @@ -677,34 +677,34 @@ IClassMapping mapping switch (result) { case { LinkKind: MediaLinkKind.Guid or MediaLinkKind.DirectMediaPath, MediaKind: MediaKind.MediaFile }: + { + var mediaFile = MediaHelper.GetMediaFile(result, modelFacade); + if (mediaFile is null) { - var mediaFile = MediaHelper.GetMediaFile(result, modelFacade); - if (mediaFile is null) - { - return original; - } - - return assetFacade.GetAssetUri(mediaFile); + return original; } + + return assetFacade.GetAssetUri(mediaFile); + } case { LinkKind: MediaLinkKind.Guid, MediaKind: MediaKind.Attachment, MediaGuid: { } mediaGuid, LinkSiteId: var linkSiteId }: + { + var attachment = MediaHelper.GetAttachment(result, modelFacade); + if (attachment is null) { - var attachment = MediaHelper.GetAttachment(result, modelFacade); - if (attachment is null) - { - return original; - } - - await attachmentMigrator.MigrateAttachment(attachment); + return original; + } - string? culture = null; - if (attachment.AttachmentDocumentID is { } attachmentDocumentId) - { - culture = modelFacade.SelectById(attachmentDocumentId)?.DocumentCulture; - } + await attachmentMigrator.MigrateAttachment(attachment); - return assetFacade.GetAssetUri(attachment, culture); + string? culture = null; + if (attachment.AttachmentDocumentID is { } attachmentDocumentId) + { + culture = modelFacade.SelectById(attachmentDocumentId)?.DocumentCulture; } + return assetFacade.GetAssetUri(attachment, culture); + } + default: break; } From 687605ab3f887430b11410f06a7853f4be94f4b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Krch?= Date: Thu, 7 Nov 2024 00:54:28 +0100 Subject: [PATCH 06/13] updating ability to convert web pages to content hub --- .../MigratePageTypesCommandHandler.cs | 5 ++++ .../Handlers/MigratePagesCommandHandler.cs | 26 +----------------- .../Mappers/ContentItemMapper.cs | 27 +++++++++---------- Migration.Tool.CLI/appsettings.json | 1 - Migration.Tool.Common/ConfigurationNames.cs | 1 - Migration.Tool.Common/ToolConfiguration.cs | 10 ------- .../ServiceCollectionExtensions.cs | 2 +- 7 files changed, 20 insertions(+), 52 deletions(-) diff --git a/KVA/Migration.Tool.Source/Handlers/MigratePageTypesCommandHandler.cs b/KVA/Migration.Tool.Source/Handlers/MigratePageTypesCommandHandler.cs index 82fbb401..34e17aae 100644 --- a/KVA/Migration.Tool.Source/Handlers/MigratePageTypesCommandHandler.cs +++ b/KVA/Migration.Tool.Source/Handlers/MigratePageTypesCommandHandler.cs @@ -151,6 +151,11 @@ public async Task Handle(MigratePageTypesCommand request, Cancell foreach (string sourceClassName in classMapping.SourceClassNames) { + if (newDt.ClassContentTypeType is ClassContentTypeType.REUSABLE) + { + continue; + } + var sourceClass = cmsClasses.First(c => c.ClassName.Equals(sourceClassName, StringComparison.InvariantCultureIgnoreCase)); foreach (var cmsClassSite in modelFacade.SelectWhere("ClassId = @classId", new SqlParameter("classId", sourceClass.ClassID))) { diff --git a/KVA/Migration.Tool.Source/Handlers/MigratePagesCommandHandler.cs b/KVA/Migration.Tool.Source/Handlers/MigratePagesCommandHandler.cs index 05b95f63..c694cc28 100644 --- a/KVA/Migration.Tool.Source/Handlers/MigratePagesCommandHandler.cs +++ b/KVA/Migration.Tool.Source/Handlers/MigratePagesCommandHandler.cs @@ -213,31 +213,7 @@ public async Task Handle(MigratePagesCommand request, Cancellatio var commonDataInfos = new List(); foreach (var umtModel in results) { - bool isReusable = toolConfiguration.ClassNamesConvertToContentHub.Contains(targetClass?.ClassName) || targetClass?.ClassContentTypeType is ClassContentTypeType.REUSABLE; - - - bool skipWebPageItem = umtModel is WebPageItemModel && isReusable; - - IImportResult result = new ImportResult { Success = true }; - if (skipWebPageItem) - { - if (targetClass is { } && targetClass.ClassContentTypeType == ClassContentTypeType.WEBSITE) - { - targetClass.ClassContentTypeType = ClassContentTypeType.REUSABLE; - targetClass.ClassWebPageHasUrl = false; - targetClass.Update(); - } - } - else - { - result = await importer.ImportAsync(umtModel); - } - if (result is { Success: false } && !skipWebPageItem) - { - logger.LogError("Failed to import: {Exception}, {ValidationResults}", result.Exception, JsonConvert.SerializeObject(result.ModelValidationResults)); - } - - switch (result) + switch (await importer.ImportAsync(umtModel)) { case { Success: true, Imported: ContentItemCommonDataInfo ccid }: { diff --git a/KVA/Migration.Tool.Source/Mappers/ContentItemMapper.cs b/KVA/Migration.Tool.Source/Mappers/ContentItemMapper.cs index 9df6a391..c1ddb754 100644 --- a/KVA/Migration.Tool.Source/Mappers/ContentItemMapper.cs +++ b/KVA/Migration.Tool.Source/Mappers/ContentItemMapper.cs @@ -76,16 +76,12 @@ protected override IEnumerable MapInternal(CmsTreeMapperSource source bool migratedAsContentFolder = sourceNodeClass.ClassName.Equals("cms.folder", StringComparison.InvariantCultureIgnoreCase) && !configuration.UseDeprecatedFolderPageType.GetValueOrDefault(false); var contentItemGuid = spoiledGuidContext.EnsureNodeGuid(cmsTree.NodeGUID, cmsTree.NodeSiteID, cmsTree.NodeID); - - string className = targetClassInfo?.ClassName ?? sourceNodeClass.ClassName; bool isMappedTypeReusable = targetClassInfo?.ClassContentTypeType is ClassContentTypeType.REUSABLE; - bool isReusable = configuration.ClassNamesConvertToContentHub.Contains(className) || isMappedTypeReusable; - yield return new ContentItemModel { ContentItemGUID = contentItemGuid, ContentItemName = safeNodeName, - ContentItemIsReusable = isReusable, + ContentItemIsReusable = isMappedTypeReusable, ContentItemIsSecured = cmsTree.IsSecuredNode ?? false, ContentItemDataClassGuid = migratedAsContentFolder ? null : targetClassGuid, ContentItemChannelGuid = siteGuid @@ -357,16 +353,19 @@ protected override IEnumerable MapInternal(CmsTreeMapperSource source Debug.Assert(cmsTree.NodeLinkedNodeID == null, "cmsTree.NodeLinkedNodeId == null"); Debug.Assert(cmsTree.NodeLinkedNodeSiteID == null, "cmsTree.NodeLinkedNodeSiteId == null"); - yield return new WebPageItemModel + if (!isMappedTypeReusable) { - WebPageItemParentGuid = nodeParentGuid, // NULL => under root - WebPageItemGUID = contentItemGuid, - WebPageItemName = safeNodeName, - WebPageItemTreePath = treePath, - WebPageItemWebsiteChannelGuid = siteGuid, - WebPageItemContentItemGuid = contentItemGuid, - WebPageItemOrder = cmsTree.NodeOrder ?? 0 // 0 is nullish value - }; + yield return new WebPageItemModel + { + WebPageItemParentGuid = nodeParentGuid, // NULL => under root + WebPageItemGUID = contentItemGuid, + WebPageItemName = safeNodeName, + WebPageItemTreePath = treePath, + WebPageItemWebsiteChannelGuid = siteGuid, + WebPageItemContentItemGuid = contentItemGuid, + WebPageItemOrder = cmsTree.NodeOrder ?? 0 // 0 is nullish value + }; + } } private IEnumerable MigrateDraft(ICmsVersionHistory checkoutVersion, ICmsTree cmsTree, string sourceFormClassDefinition, string targetFormDefinition, Guid contentItemGuid, diff --git a/Migration.Tool.CLI/appsettings.json b/Migration.Tool.CLI/appsettings.json index 79371700..a7f377ef 100644 --- a/Migration.Tool.CLI/appsettings.json +++ b/Migration.Tool.CLI/appsettings.json @@ -29,7 +29,6 @@ "MigrateMediaToMediaLibrary": false, "UseDeprecatedFolderPageType": false, "CreateReusableFieldSchemaForClasses": "", - "ConvertClassesToContentHub": "", "OptInFeatures": { "QuerySourceInstanceApi": { "Enabled": false, diff --git a/Migration.Tool.Common/ConfigurationNames.cs b/Migration.Tool.Common/ConfigurationNames.cs index 11e67997..b1d10b0e 100644 --- a/Migration.Tool.Common/ConfigurationNames.cs +++ b/Migration.Tool.Common/ConfigurationNames.cs @@ -22,7 +22,6 @@ public class ConfigurationNames public const string UseDeprecatedFolderPageType = "UseDeprecatedFolderPageType"; public const string ExcludeCodeNames = "ExcludeCodeNames"; - public const string ConvertClassesToContentHub = "ConvertClassesToContentHub"; public const string ExplicitPrimaryKeyMapping = "ExplicitPrimaryKeyMapping"; public const string SiteName = "SiteName"; diff --git a/Migration.Tool.Common/ToolConfiguration.cs b/Migration.Tool.Common/ToolConfiguration.cs index fe50454e..98644dfe 100644 --- a/Migration.Tool.Common/ToolConfiguration.cs +++ b/Migration.Tool.Common/ToolConfiguration.cs @@ -49,20 +49,11 @@ public class ToolConfiguration [ConfigurationKeyName(ConfigurationNames.CreateReusableFieldSchemaForClasses)] public string? CreateReusableFieldSchemaForClasses { get; set; } - [ConfigurationKeyName(ConfigurationNames.ConvertClassesToContentHub)] - public string? ConvertClassesToContentHub { get; set; } - - public IReadOnlySet ClassNamesCreateReusableSchema => classNamesCreateReusableSchema ??= new HashSet( (CreateReusableFieldSchemaForClasses?.Split(new[] { ',', ';' }, StringSplitOptions.RemoveEmptyEntries) ?? []).Select(x => x.Trim()), StringComparer.InvariantCultureIgnoreCase ); - public IReadOnlySet ClassNamesConvertToContentHub => classNamesConvertToContentHub ??= new HashSet( - (ConvertClassesToContentHub?.Split(new[] { ',', ';' }, StringSplitOptions.RemoveEmptyEntries) ?? []).Select(x => x.Trim()), - StringComparer.InvariantCultureIgnoreCase - ); - #region Opt-in features [ConfigurationKeyName(ConfigurationNames.OptInFeatures)] @@ -105,7 +96,6 @@ public void SetXbKConnectionStringIfNotEmpty(string? connectionString) #region Path to root directory of target instance private HashSet? classNamesCreateReusableSchema; - private HashSet? classNamesConvertToContentHub; private string? xbKConnectionString; [ConfigurationKeyName(ConfigurationNames.XbKDirPath)] diff --git a/Migration.Tool.Extensions/ServiceCollectionExtensions.cs b/Migration.Tool.Extensions/ServiceCollectionExtensions.cs index c0d440c2..569d89d6 100644 --- a/Migration.Tool.Extensions/ServiceCollectionExtensions.cs +++ b/Migration.Tool.Extensions/ServiceCollectionExtensions.cs @@ -19,7 +19,7 @@ public static IServiceCollection UseCustomizations(this IServiceCollection servi // services.AddClassMergeExample(); // services.AddSimpleRemodelingSample(); - //services.AddReusableRemodelingSample(); + // services.AddReusableRemodelingSample(); // services.AddReusableSchemaIntegrationSample(); return services; } From a9cb03a59dfad899a52e9690baaa231eff2c1717 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20Krch?= Date: Thu, 7 Nov 2024 22:09:57 +0100 Subject: [PATCH 07/13] re-introduction of simple conversion to reusable content item --- .../MigratePageTypesCommandHandler.cs | 66 ++++++++++--------- .../Handlers/MigratePagesCommandHandler.cs | 5 ++ .../Mappers/CmsClassMapper.cs | 7 +- .../Mappers/ContentItemMapper.cs | 2 +- Migration.Tool.Common/ConfigurationNames.cs | 1 + Migration.Tool.Common/ToolConfiguration.cs | 9 +++ .../ClassMappings/ClassMappingSample.cs | 6 ++ Migration.Tool.Extensions/README.md | 4 ++ 8 files changed, 67 insertions(+), 33 deletions(-) diff --git a/KVA/Migration.Tool.Source/Handlers/MigratePageTypesCommandHandler.cs b/KVA/Migration.Tool.Source/Handlers/MigratePageTypesCommandHandler.cs index 34e17aae..c791d10b 100644 --- a/KVA/Migration.Tool.Source/Handlers/MigratePageTypesCommandHandler.cs +++ b/KVA/Migration.Tool.Source/Handlers/MigratePageTypesCommandHandler.cs @@ -103,27 +103,30 @@ public async Task Handle(MigratePageTypesCommand request, Cancell bool hasFieldsAlready = true; foreach (var cmml in classMapping.Mappings.Where(m => m.IsTemplate).ToLookup(x => x.SourceFieldName)) { - var cmm = cmml.FirstOrDefault() ?? throw new InvalidOperationException(); - if (fieldInReusableSchemas.ContainsKey(cmm.TargetFieldName)) + foreach (var cmm in cmml) { - // part of reusable schema - continue; - } + if (fieldInReusableSchemas.ContainsKey(cmm.TargetFieldName)) + { + // part of reusable schema + continue; + } - var sc = cmsClasses.FirstOrDefault(sc => sc.ClassName.Equals(cmm.SourceClassName, StringComparison.InvariantCultureIgnoreCase)) - ?? throw new NullReferenceException($"The source class '{cmm.SourceClassName}' does not exist - wrong mapping {classMapping}"); + var sc = cmsClasses.FirstOrDefault(sc => sc.ClassName.Equals(cmm.SourceClassName, StringComparison.InvariantCultureIgnoreCase)) + ?? throw new NullReferenceException($"The source class '{cmm.SourceClassName}' does not exist - wrong mapping {classMapping}"); - var fi = new FormInfo(sc.ClassFormDefinition); - if (nfi.GetFormField(cmm.TargetFieldName) is { }) - { - } - else - { - var src = fi.GetFormField(cmm.SourceFieldName); - src.Name = cmm.TargetFieldName; - nfi.AddFormItem(src); - hasFieldsAlready = false; + var fi = new FormInfo(sc.ClassFormDefinition); + if (nfi.GetFormField(cmm.TargetFieldName) is { }) + { + } + else + { + var src = fi.GetFormField(cmm.SourceFieldName); + src.Name = cmm.TargetFieldName; + nfi.AddFormItem(src); + hasFieldsAlready = false; + } } + //var cmm = cmml.FirstOrDefault() ?? throw new InvalidOperationException(); } if (!hasFieldsAlready) @@ -242,26 +245,29 @@ public async Task Handle(MigratePageTypesCommand request, Cancell var kxoDataClass = kxpClassFacade.GetClass(ksClass.ClassGUID); protocol.FetchedTarget(kxoDataClass); - if (SaveUsingKxoApi(ksClass, kxoDataClass) is { } targetClassId) + if (SaveUsingKxoApi(ksClass, kxoDataClass) is { } targetClass) { - foreach (var cmsClassSite in modelFacade.SelectWhere("ClassID = @classId", new SqlParameter("classId", ksClass.ClassID))) + if (targetClass.ClassContentTypeType is ClassContentTypeType.WEBSITE) { - if (modelFacade.SelectById(cmsClassSite.SiteID) is { SiteGUID: var siteGuid }) + foreach (var cmsClassSite in modelFacade.SelectWhere("ClassID = @classId", new SqlParameter("classId", ksClass.ClassID))) { - if (ChannelInfoProvider.ProviderObject.Get(siteGuid) is { ChannelID: var channelId }) + if (modelFacade.SelectById(cmsClassSite.SiteID) is { SiteGUID: var siteGuid }) { - var info = new ContentTypeChannelInfo { ContentTypeChannelChannelID = channelId, ContentTypeChannelContentTypeID = targetClassId }; - ContentTypeChannelInfoProvider.ProviderObject.Set(info); + if (ChannelInfoProvider.ProviderObject.Get(siteGuid) is { ChannelID: var channelId }) + { + var info = new ContentTypeChannelInfo { ContentTypeChannelChannelID = channelId, ContentTypeChannelContentTypeID = targetClass.ClassID }; + ContentTypeChannelInfoProvider.ProviderObject.Set(info); + } + else + { + logger.LogWarning("Channel for site with SiteGUID '{SiteGuid}' not found", siteGuid); + } } else { - logger.LogWarning("Channel for site with SiteGUID '{SiteGuid}' not found", siteGuid); + logger.LogWarning("Source site with SiteID '{SiteId}' not found", cmsClassSite.SiteID); } } - else - { - logger.LogWarning("Source site with SiteID '{SiteId}' not found", cmsClassSite.SiteID); - } } } } @@ -337,7 +343,7 @@ private async Task MigratePageTemplateConfigurations() } } - private int? SaveUsingKxoApi(ICmsClass ksClass, DataClassInfo kxoDataClass) + private DataClassInfo? SaveUsingKxoApi(ICmsClass ksClass, DataClassInfo kxoDataClass) { var mapped = dataClassMapper.Map(ksClass, kxoDataClass); protocol.MappedTarget(mapped); @@ -365,7 +371,7 @@ private async Task MigratePageTemplateConfigurations() dataClassInfo.ClassID ); - return dataClassInfo.ClassID; + return dataClassInfo; } } catch (Exception ex) diff --git a/KVA/Migration.Tool.Source/Handlers/MigratePagesCommandHandler.cs b/KVA/Migration.Tool.Source/Handlers/MigratePagesCommandHandler.cs index c694cc28..aa5608ba 100644 --- a/KVA/Migration.Tool.Source/Handlers/MigratePagesCommandHandler.cs +++ b/KVA/Migration.Tool.Source/Handlers/MigratePagesCommandHandler.cs @@ -215,6 +215,11 @@ public async Task Handle(MigratePagesCommand request, Cancellatio { switch (await importer.ImportAsync(umtModel)) { + case { Success: false } result: + { + logger.LogError("Failed to import: {Exception}, {ValidationResults}", result.Exception, JsonConvert.SerializeObject(result.ModelValidationResults)); + break; + } case { Success: true, Imported: ContentItemCommonDataInfo ccid }: { commonDataInfos.Add(ccid); diff --git a/KVA/Migration.Tool.Source/Mappers/CmsClassMapper.cs b/KVA/Migration.Tool.Source/Mappers/CmsClassMapper.cs index 0413f96e..33d9ac95 100644 --- a/KVA/Migration.Tool.Source/Mappers/CmsClassMapper.cs +++ b/KVA/Migration.Tool.Source/Mappers/CmsClassMapper.cs @@ -25,7 +25,8 @@ public class CmsClassMapper( PrimaryKeyMappingContext primaryKeyMappingContext, IProtocol protocol, FieldMigrationService fieldMigrationService, - ModelFacade modelFacade + ModelFacade modelFacade, + ToolConfiguration configuration ) : EntityMapperBase(logger, primaryKeyMappingContext, protocol) @@ -188,7 +189,9 @@ protected override DataClassInfo MapInternal(ICmsClass source, DataClassInfo tar }: { target.ClassType = ClassType.CONTENT_TYPE; - target.ClassContentTypeType = ClassContentTypeType.WEBSITE; + target.ClassContentTypeType = configuration.ClassNamesConvertToContentHub.Contains(target.ClassName) + ? ClassContentTypeType.REUSABLE + : ClassContentTypeType.WEBSITE; target = PatchDataClassInfo(target, out string? oldPrimaryKeyName, out string? documentNameField); break; diff --git a/KVA/Migration.Tool.Source/Mappers/ContentItemMapper.cs b/KVA/Migration.Tool.Source/Mappers/ContentItemMapper.cs index c1ddb754..231259b6 100644 --- a/KVA/Migration.Tool.Source/Mappers/ContentItemMapper.cs +++ b/KVA/Migration.Tool.Source/Mappers/ContentItemMapper.cs @@ -76,7 +76,7 @@ protected override IEnumerable MapInternal(CmsTreeMapperSource source bool migratedAsContentFolder = sourceNodeClass.ClassName.Equals("cms.folder", StringComparison.InvariantCultureIgnoreCase) && !configuration.UseDeprecatedFolderPageType.GetValueOrDefault(false); var contentItemGuid = spoiledGuidContext.EnsureNodeGuid(cmsTree.NodeGUID, cmsTree.NodeSiteID, cmsTree.NodeID); - bool isMappedTypeReusable = targetClassInfo?.ClassContentTypeType is ClassContentTypeType.REUSABLE; + bool isMappedTypeReusable = (targetClassInfo?.ClassContentTypeType is ClassContentTypeType.REUSABLE) || configuration.ClassNamesConvertToContentHub.Contains(sourceNodeClass.ClassName); yield return new ContentItemModel { ContentItemGUID = contentItemGuid, diff --git a/Migration.Tool.Common/ConfigurationNames.cs b/Migration.Tool.Common/ConfigurationNames.cs index b1d10b0e..11e67997 100644 --- a/Migration.Tool.Common/ConfigurationNames.cs +++ b/Migration.Tool.Common/ConfigurationNames.cs @@ -22,6 +22,7 @@ public class ConfigurationNames public const string UseDeprecatedFolderPageType = "UseDeprecatedFolderPageType"; public const string ExcludeCodeNames = "ExcludeCodeNames"; + public const string ConvertClassesToContentHub = "ConvertClassesToContentHub"; public const string ExplicitPrimaryKeyMapping = "ExplicitPrimaryKeyMapping"; public const string SiteName = "SiteName"; diff --git a/Migration.Tool.Common/ToolConfiguration.cs b/Migration.Tool.Common/ToolConfiguration.cs index 98644dfe..15249bb6 100644 --- a/Migration.Tool.Common/ToolConfiguration.cs +++ b/Migration.Tool.Common/ToolConfiguration.cs @@ -49,11 +49,19 @@ public class ToolConfiguration [ConfigurationKeyName(ConfigurationNames.CreateReusableFieldSchemaForClasses)] public string? CreateReusableFieldSchemaForClasses { get; set; } + [ConfigurationKeyName(ConfigurationNames.ConvertClassesToContentHub)] + public string? ConvertClassesToContentHub { get; set; } + public IReadOnlySet ClassNamesCreateReusableSchema => classNamesCreateReusableSchema ??= new HashSet( (CreateReusableFieldSchemaForClasses?.Split(new[] { ',', ';' }, StringSplitOptions.RemoveEmptyEntries) ?? []).Select(x => x.Trim()), StringComparer.InvariantCultureIgnoreCase ); + public IReadOnlySet ClassNamesConvertToContentHub => classNamesConvertToContentHub ??= new HashSet( + (ConvertClassesToContentHub?.Split(new[] { ',', ';' }, StringSplitOptions.RemoveEmptyEntries) ?? []).Select(x => x.Trim()), + StringComparer.InvariantCultureIgnoreCase + ); + #region Opt-in features [ConfigurationKeyName(ConfigurationNames.OptInFeatures)] @@ -95,6 +103,7 @@ public void SetXbKConnectionStringIfNotEmpty(string? connectionString) #region Path to root directory of target instance + private HashSet? classNamesConvertToContentHub; private HashSet? classNamesCreateReusableSchema; private string? xbKConnectionString; diff --git a/Migration.Tool.Extensions/ClassMappings/ClassMappingSample.cs b/Migration.Tool.Extensions/ClassMappings/ClassMappingSample.cs index bdaa8cea..fa0c0026 100644 --- a/Migration.Tool.Extensions/ClassMappings/ClassMappingSample.cs +++ b/Migration.Tool.Extensions/ClassMappings/ClassMappingSample.cs @@ -34,6 +34,12 @@ public static IServiceCollection AddReusableRemodelingSample(this IServiceCollec .SetFrom(sourceClassName, "CoffeeFarm", true) .WithFieldPatch(f => f.SetPropertyValue(FormFieldPropertyEnum.FieldCaption, "Farm RM")); + // field clone sample + m + .BuildField("FarmRM_Clone") + .SetFrom(sourceClassName, "CoffeeFarm", true) + .WithFieldPatch(f => f.SetPropertyValue(FormFieldPropertyEnum.FieldCaption, "Farm RM Clone")); + m .BuildField("CoffeeCountryRM") .WithFieldPatch(f => f.Caption = "Country RM") diff --git a/Migration.Tool.Extensions/README.md b/Migration.Tool.Extensions/README.md index c6a4b3c9..6b2cf2b7 100644 --- a/Migration.Tool.Extensions/README.md +++ b/Migration.Tool.Extensions/README.md @@ -103,6 +103,10 @@ To create custom widget migration: Samples: - [Sample BannerWidget migration](./CommunityMigrations/SampleWidgetMigration.cs) +### Convert page type to reusable content item (content hub) + +demonstrated in method `AddReusableRemodelingSample`. Please note, that all information unique to page will be lost + ## Custom widget property migrations To create custom widget property migration: From 7d32e1904ad7a59847b3e832a6fc4416b7509002 Mon Sep 17 00:00:00 2001 From: akfakmot Date: Mon, 11 Nov 2024 11:48:39 +0100 Subject: [PATCH 08/13] Enhance logging Enhance logging --- .../Mappers/MediaFileInfoMapper.cs | 5 +++-- .../Services/AttachmentMigratorToContentItem.cs | 7 ++++++- .../Services/AttachmentMigratorToMediaLibrary.cs | 7 ++++++- .../MigrationProtocol/HandbookReference.cs | 12 +++++++++++- .../Handlers/MigrateUsersCommandHandler.cs | 4 ++++ .../Handlers/MigrateUsersCommandHandler.cs | 4 ++++ .../Handlers/MigrateUsersCommandHandler.cs | 4 ++++ 7 files changed, 38 insertions(+), 5 deletions(-) diff --git a/KVA/Migration.Tool.Source/Mappers/MediaFileInfoMapper.cs b/KVA/Migration.Tool.Source/Mappers/MediaFileInfoMapper.cs index 563469cb..e6f72265 100644 --- a/KVA/Migration.Tool.Source/Mappers/MediaFileInfoMapper.cs +++ b/KVA/Migration.Tool.Source/Mappers/MediaFileInfoMapper.cs @@ -47,9 +47,9 @@ ToolConfiguration toolConfiguration return new MediaFileInfo(); } - protected override MediaFileInfo MapInternal(MediaFileInfoMapperSource args, MediaFileInfo target, bool newInstance, MappingHelper mappingHelper, AddFailure addFailure) + protected override MediaFileInfo MapInternal(MediaFileInfoMapperSource source, MediaFileInfo target, bool newInstance, MappingHelper mappingHelper, AddFailure addFailure) { - (string fullMediaFilePath, var mediaFile, int targetLibraryId, var file, _, bool migrateOnlyMediaFileInfo, var safeMediaFileGuid) = args; + (string fullMediaFilePath, var mediaFile, int targetLibraryId, var file, _, bool migrateOnlyMediaFileInfo, var safeMediaFileGuid) = source; target.FileName = mediaFile.FileName; target.FileTitle = mediaFile.FileTitle; @@ -100,6 +100,7 @@ protected override MediaFileInfo MapInternal(MediaFileInfoMapperSource args, Med addFailure(HandbookReferences.MediaFileIsMissingOnSourceFilesystem .WithId(nameof(mediaFile.FileID), mediaFile.FileID) .WithData(new { mediaFile.FilePath, mediaFile.FileGUID, mediaFile.FileLibraryID, mediaFile.FileSiteID, SearchedPath = fullMediaFilePath }) + .WithSuggestion($"If you have a backup, copy it to the filesystem on path {source.FullMediaFilePath}. Otherwise, delete the media file from the source instance using admin web interface.") .AsFailure() ); } diff --git a/KVA/Migration.Tool.Source/Services/AttachmentMigratorToContentItem.cs b/KVA/Migration.Tool.Source/Services/AttachmentMigratorToContentItem.cs index be94568e..ae841add 100644 --- a/KVA/Migration.Tool.Source/Services/AttachmentMigratorToContentItem.cs +++ b/KVA/Migration.Tool.Source/Services/AttachmentMigratorToContentItem.cs @@ -131,7 +131,12 @@ public async Task MigrateAttachment(ICmsAttachment ksA if (ksAttachment.AttachmentBinary is null) { - logger.LogError("Binary data is null, cannot migrate attachment: {Attachment}", ksAttachment); + logger.LogError("Attachment binary data is null {Attachment} " + + "Option 1: Via admin web interface of your source instance navigate to the attachment and update the data. " + + "Option 2: Update the database directly - table CMS_Attachment, column AttachmentBinary. " + + "Option 3: Via admin web interface of your source instance remove all attachment references, then remove the attachment", + new { ksAttachment.AttachmentName, ksAttachment.AttachmentSiteID, ksAttachment.AttachmentID }); + throw new InvalidOperationException("Attachment data is null!"); } diff --git a/KVA/Migration.Tool.Source/Services/AttachmentMigratorToMediaLibrary.cs b/KVA/Migration.Tool.Source/Services/AttachmentMigratorToMediaLibrary.cs index cdbe85c8..d03c645c 100644 --- a/KVA/Migration.Tool.Source/Services/AttachmentMigratorToMediaLibrary.cs +++ b/KVA/Migration.Tool.Source/Services/AttachmentMigratorToMediaLibrary.cs @@ -214,7 +214,12 @@ public async Task MigrateAttachment(ICmsAttachment ksA return DummyUploadedFile.FromStream(ms, attachment.AttachmentMimeType, attachment.AttachmentSize, attachment.AttachmentName); } - logger.LogWarning("Attachment binary is null! {Attachment}", new { attachment.AttachmentName, attachment.AttachmentSiteID, attachment.AttachmentID }); + logger.LogError("Attachment binary data is null {Attachment} " + + "Option 1: Via admin web interface of your source instance navigate to the attachment and update the data. " + + "Option 2: Update the database directly - table CMS_Attachment, column AttachmentBinary. " + + "Option 3: Via admin web interface of your source instance remove all attachment references, then remove the attachment", + new { attachment.AttachmentName, attachment.AttachmentSiteID, attachment.AttachmentID }); + return null; } diff --git a/Migration.Tool.Common/MigrationProtocol/HandbookReference.cs b/Migration.Tool.Common/MigrationProtocol/HandbookReference.cs index ae30b7d9..0d2888ce 100644 --- a/Migration.Tool.Common/MigrationProtocol/HandbookReference.cs +++ b/Migration.Tool.Common/MigrationProtocol/HandbookReference.cs @@ -26,6 +26,7 @@ public HandbookReference(string referenceName, string? additionalInfo = null) public string ReferenceName { get; } public string? AdditionalInfo { get; } public Dictionary? Data { get; private set; } + public string Suggestion { get; private set; } public override string ToString() { @@ -47,10 +48,19 @@ public override string ToString() sb.Append(", "); } } - + sb.AppendLine(); + if (!string.IsNullOrEmpty(Suggestion)) + { + sb.AppendLine($"Suggestion: {Suggestion}"); + } return sb.ToString(); } + public HandbookReference WithSuggestion(string suggestion) + { + Suggestion = suggestion; + return this; + } /// /// Related ID of data, specify if possible /// diff --git a/Migration.Tool.Core.K11/Handlers/MigrateUsersCommandHandler.cs b/Migration.Tool.Core.K11/Handlers/MigrateUsersCommandHandler.cs index 378a4501..5eb1f71d 100644 --- a/Migration.Tool.Core.K11/Handlers/MigrateUsersCommandHandler.cs +++ b/Migration.Tool.Core.K11/Handlers/MigrateUsersCommandHandler.cs @@ -89,6 +89,10 @@ private bool SaveUserUsingKenticoApi(IModelMappingResult mapped, CmsUs try { + if (string.IsNullOrEmpty(userInfo.Email)) + { + logger.LogError($"User {userInfo.UserName} does not have an email set. Email is required. You can set it via admin web interface of your source instance or directly in CMS_User database table."); + } UserInfoProvider.ProviderObject.Set(userInfo); protocol.Success(k11User, userInfo, mapped); diff --git a/Migration.Tool.Core.KX12/Handlers/MigrateUsersCommandHandler.cs b/Migration.Tool.Core.KX12/Handlers/MigrateUsersCommandHandler.cs index 42c482be..95989d6b 100644 --- a/Migration.Tool.Core.KX12/Handlers/MigrateUsersCommandHandler.cs +++ b/Migration.Tool.Core.KX12/Handlers/MigrateUsersCommandHandler.cs @@ -89,6 +89,10 @@ private void SaveUserUsingKenticoApi(IModelMappingResult mapped, KX12M try { + if (string.IsNullOrEmpty(userInfo.Email)) + { + logger.LogError($"User {userInfo.UserName} does not have an email set. Email is required. You can set it via admin web interface of your source instance or directly in CMS_User database table."); + } UserInfoProvider.ProviderObject.Set(userInfo); protocol.Success(k12User, userInfo, mapped); diff --git a/Migration.Tool.Core.KX13/Handlers/MigrateUsersCommandHandler.cs b/Migration.Tool.Core.KX13/Handlers/MigrateUsersCommandHandler.cs index f13f1d09..0c90eae6 100644 --- a/Migration.Tool.Core.KX13/Handlers/MigrateUsersCommandHandler.cs +++ b/Migration.Tool.Core.KX13/Handlers/MigrateUsersCommandHandler.cs @@ -89,6 +89,10 @@ private Task SaveUserUsingKenticoApi(IModelMappingResult mapped, KX13M try { + if (string.IsNullOrEmpty(userInfo.Email)) + { + logger.LogError($"User {userInfo.UserName} does not have an email set. Email is required. You can set it via admin web interface of your source instance or directly in CMS_User database table."); + } UserInfoProvider.ProviderObject.Set(userInfo); protocol.Success(kx13User, userInfo, mapped); From 620c23bb102de52b0891a13fb1cdf65d94c6ff0d Mon Sep 17 00:00:00 2001 From: akfakmot Date: Tue, 12 Nov 2024 17:14:04 +0100 Subject: [PATCH 09/13] Fix null exception when section properties is null Fix: Fix null exception when section properties is null --- KVA/Migration.Tool.Source/Services/PageBuilderPatcher.cs | 8 ++++++-- Migration.Tool.Common/Model/EditableAreasConfiguration.cs | 2 +- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/KVA/Migration.Tool.Source/Services/PageBuilderPatcher.cs b/KVA/Migration.Tool.Source/Services/PageBuilderPatcher.cs index 5f918e91..43f604b4 100644 --- a/KVA/Migration.Tool.Source/Services/PageBuilderPatcher.cs +++ b/KVA/Migration.Tool.Source/Services/PageBuilderPatcher.cs @@ -92,8 +92,12 @@ private async Task WalkSections(int siteId, List sec logger.LogTrace("Walk section {TypeIdentifier}|{Identifier}", section.TypeIdentifier, section.Identifier); var sectionFcs = sourceInstanceContext.GetSectionFormComponents(siteId, section.TypeIdentifier); - bool ndp1 = await MigrateProperties(siteId, section.Properties, sectionFcs, new Dictionary()); - needsDeferredPatch = ndp1 || needsDeferredPatch; + + if (section.Properties is { Count: > 0 } properties) + { + bool ndp1 = await MigrateProperties(siteId, properties, sectionFcs, new Dictionary()); + needsDeferredPatch = ndp1 || needsDeferredPatch; + } if (section.Zones is { Count: > 0 }) { diff --git a/Migration.Tool.Common/Model/EditableAreasConfiguration.cs b/Migration.Tool.Common/Model/EditableAreasConfiguration.cs index b00f281e..11a0304f 100644 --- a/Migration.Tool.Common/Model/EditableAreasConfiguration.cs +++ b/Migration.Tool.Common/Model/EditableAreasConfiguration.cs @@ -89,7 +89,7 @@ public sealed class SectionConfiguration [DataMember] [JsonProperty("properties")] // public ISectionProperties Properties { get; set; } - public JObject Properties { get; set; } + public JObject? Properties { get; set; } /// Zones within the section. [DataMember] From 11b54e33271146da9e121dd7aa5a6de7ff5ac8f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20H=C3=BCbelbauer?= Date: Thu, 7 Nov 2024 16:10:21 +0100 Subject: [PATCH 10/13] Fix broken link I think this is the right link. The original link is a 404. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index c37ffdce..0894eef0 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ ## Description -This repository is part of the [Xperience by Kentico Migration Tool](https://github.com/Kentico/xperience-by-kentico-migration-tool). +This repository is part of the [Xperience by Kentico Migration Tool](https://github.com/Kentico/xperience-by-kentico-kentico-migration-tool). The Kentico Migration Tool transfers content and other data from **Kentico Xperience 13**, **Kentico 12** or **Kentico 11** to **Xperience by Kentico**. From f03ec5107f14264c83735b9b5af48f14a13bf199 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20H=C3=BCbelbauer?= Date: Thu, 7 Nov 2024 19:32:09 +0100 Subject: [PATCH 11/13] Use the suggested new link text Co-authored-by: Sean G. Wright --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 0894eef0..87833e4f 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ ## Description -This repository is part of the [Xperience by Kentico Migration Tool](https://github.com/Kentico/xperience-by-kentico-kentico-migration-tool). +This repository is part of the [Xperience by Kentico Migration Toolkit](https://github.com/Kentico/xperience-by-kentico-migration-toolkit). The Kentico Migration Tool transfers content and other data from **Kentico Xperience 13**, **Kentico 12** or **Kentico 11** to **Xperience by Kentico**. From 08f301e3c65ee118f49ee63bce16aabc5ce53b47 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20H=C3=BCbelbauer?= Date: Fri, 8 Nov 2024 23:39:27 +0100 Subject: [PATCH 12/13] Use the suggestion from the maintainer MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Tomáš Krch --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 87833e4f..0894eef0 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ ## Description -This repository is part of the [Xperience by Kentico Migration Toolkit](https://github.com/Kentico/xperience-by-kentico-migration-toolkit). +This repository is part of the [Xperience by Kentico Migration Tool](https://github.com/Kentico/xperience-by-kentico-kentico-migration-tool). The Kentico Migration Tool transfers content and other data from **Kentico Xperience 13**, **Kentico 12** or **Kentico 11** to **Xperience by Kentico**. From 1b23515118e54dd5a534ceaf907b6cd7d9a8d8df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Tom=C3=A1=C5=A1=20H=C3=BCbelbauer?= Date: Wed, 13 Nov 2024 10:11:10 +0100 Subject: [PATCH 13/13] Revert to the correct suggestion --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 0894eef0..87833e4f 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ ## Description -This repository is part of the [Xperience by Kentico Migration Tool](https://github.com/Kentico/xperience-by-kentico-kentico-migration-tool). +This repository is part of the [Xperience by Kentico Migration Toolkit](https://github.com/Kentico/xperience-by-kentico-migration-toolkit). The Kentico Migration Tool transfers content and other data from **Kentico Xperience 13**, **Kentico 12** or **Kentico 11** to **Xperience by Kentico**.