From 2fb07a0b9b3d3efec604509f2acc5133c169fd2f Mon Sep 17 00:00:00 2001 From: akfakmot Date: Sun, 4 May 2025 17:00:53 +0200 Subject: [PATCH 1/2] Preserve parent-child relationship for pages converted to reusable content items Add: Preserve parent-child relationship for pages converted to reusable content items --- .../Handlers/MigratePagesCommandHandler.cs | 137 ++++++++++++++++++ .../Mappers/ContentItemMapper.cs | 12 +- .../ContentItemActionProvider.cs | 10 +- .../ContentItemDirectiveBase.cs | 11 +- .../ContentItemSource.cs | 2 +- .../IContentItemActionProvider.cs | 12 +- .../Mappers/NodeMapResult.cs | 10 ++ Migration.Tool.CLI/README.md | 4 +- Migration.Tool.Common/Helpers/GuidHelper.cs | 2 + .../SampleChildLinkDirector.cs | 30 ++++ Migration.Tool.Extensions/README.md | 12 ++ .../ServiceCollectionExtensions.cs | 1 + .../Migration.Tool.KXP.Extensions.csproj | 2 +- 13 files changed, 235 insertions(+), 10 deletions(-) create mode 100644 KVA/Migration.Tool.Source/Mappers/NodeMapResult.cs create mode 100644 Migration.Tool.Extensions/CommunityMigrations/SampleChildLinkDirector.cs diff --git a/KVA/Migration.Tool.Source/Handlers/MigratePagesCommandHandler.cs b/KVA/Migration.Tool.Source/Handlers/MigratePagesCommandHandler.cs index c08b261f..c85f407a 100644 --- a/KVA/Migration.Tool.Source/Handlers/MigratePagesCommandHandler.cs +++ b/KVA/Migration.Tool.Source/Handlers/MigratePagesCommandHandler.cs @@ -5,7 +5,9 @@ using CMS.Core; using CMS.Core.Internal; using CMS.DataEngine; +using CMS.DataEngine.Internal; using CMS.DataEngine.Query; +using CMS.FormEngine; using CMS.Websites; using CMS.Websites.Internal; using CMS.Websites.Routing.Internal; @@ -20,6 +22,8 @@ using Migration.Tool.Common.Helpers; using Migration.Tool.Common.MigrationProtocol; using Migration.Tool.Common.Model; +using Migration.Tool.KXP.Api.Auxiliary; +using Migration.Tool.KXP.Api.Services.CmsClass; using Migration.Tool.Source.Contexts; using Migration.Tool.Source.Helpers; using Migration.Tool.Source.Mappers; @@ -29,6 +33,7 @@ using Migration.Tool.Source.Services; using Newtonsoft.Json; using Newtonsoft.Json.Linq; +using Org.BouncyCastle.Asn1.X509; namespace Migration.Tool.Source.Handlers; // ReSharper disable once UnusedType.Global @@ -206,6 +211,10 @@ private async Task MigratePages() new SqlParameter("siteId", ksSite.SiteID) ); + // Map from original node GUID to mapped result. Using original node GUID is possible only within context of one site + // as only then is the GUID guaranteed by legacy versions to be unique + Dictionary mappedSiteNodes = []; + for (int pass = 0; pass < 2; pass++) { deferredTreeNodesService.Clear(); @@ -353,6 +362,7 @@ private async Task MigratePages() if (umtModel is ContentItemDirectiveBase yieldedDirective) { contentItemDirective = yieldedDirective; + mappedSiteNodes[contentItemDirective!.Node!.NodeGUID] = new(contentItemDirective!.Node!, contentItemDirective.ContentItemGuid, [], contentItemDirective.TargetClassInfo!, contentItemDirective.ChildLinks); } else { @@ -387,6 +397,11 @@ private async Task MigratePages() contentItemInfo = cii; break; } + case { Success: true, Imported: ContentItemDataInfo cidi }: + { + mappedSiteNodes[ksNode.NodeGUID].ContentItemDataGuids.Add(cidi.ContentItemDataGUID); + break; + } default: break; @@ -449,6 +464,128 @@ await MigratePageUrlPaths(ksSite.SiteGUID, } ksTrees = deferredTreeNodesService.GetNodes(); } + + await LinkChildren(mappedSiteNodes); + } + } + + private async Task LinkChildren(Dictionary mappedSiteNodes) + { + var reusableItems = mappedSiteNodes.Values.Where(x => x.TargetClassInfo.IsReusableContentType()); + var nonReusableItemsWithChildLinks = mappedSiteNodes.Values.Where(x => !x.TargetClassInfo.IsReusableContentType() && x.ChildLinks.Count != 0); + foreach (var item in nonReusableItemsWithChildLinks) + { + logger.LogError("Content item {ContentItemGuid} (original node {OriginalNodeGuid}) is not reusable, but specifies linked children. Add the content type to appsettings ConvertClassesToContentHub collection", item.ContentItemGuid, item.Node.NodeGUID); + } + + // [content type name] -> { [field name] -> allowed referenced types } + Dictionary allowedTypes)>> referenceFields = []; + + // Gather necessary reference fields and/or allowed referenced types + foreach (var parentItem in reusableItems) + { + if (!referenceFields.ContainsKey(parentItem.TargetClassInfo.ClassName)) + { + referenceFields[parentItem.TargetClassInfo.ClassName] = []; + } + var childCollections = parentItem.ChildLinks.GroupBy(x => x.fieldName); + foreach (var collection in childCollections) + { + if (!referenceFields[parentItem.TargetClassInfo.ClassName].ContainsKey(collection.Key)) + { + referenceFields[parentItem.TargetClassInfo.ClassName][collection.Key] = (null, []); + } + foreach (var (fieldName, node) in collection) + { + var childTargetType = mappedSiteNodes[node.NodeGUID].TargetClassInfo.ClassGUID; + referenceFields[parentItem.TargetClassInfo.ClassName][collection.Key].allowedTypes.Add(childTargetType); + } + } + } + + // Merge required allowed referenced types with already existing ones + foreach (var classInfo in reusableItems.Select(x => x.TargetClassInfo).GroupBy(x => x.ClassName).Select(x => x.First())) + { + if (referenceFields.ContainsKey(classInfo.ClassName)) + { + foreach (var field in new FormInfo(classInfo.ClassFormDefinition).GetFields().Where(x => x.DataType == "contentitemreference")) + { + if (referenceFields[classInfo.ClassName].ContainsKey(field.Name)) + { + string existingSerialized = (string?)field.Settings[FormDefinitionPatcher.AllowedContentItemTypeIdentifiers] ?? "[]"; + var existingAllowedTypes = JsonConvert.DeserializeObject(existingSerialized)!.ToHashSet(); + foreach (var existingAllowedType in existingAllowedTypes) + { + referenceFields[classInfo.ClassName][field.Name].allowedTypes.Add(existingAllowedType); + } + } + } + } + } + + foreach (var classRefFields in referenceFields) + { + var classInfo = DataClassInfoProvider.ProviderObject.Get(classRefFields.Key); + var fi = new FormInfo(classInfo.ClassFormDefinition); + foreach (var field in classRefFields.Value) + { + var ffi = fi.GetFormField(field.Key); + if (ffi is null) + { + ffi = new FormFieldInfo + { + Guid = GuidHelper.CreateFieldGuid($"{field.Key.ToLower()}|{classInfo.ClassName}"), + AllowEmpty = true, + Caption = field.Key, + DataType = "contentitemreference", + Name = field.Key, + Enabled = true, + Visible = true, + System = false, + DefaultValue = null, + Settings = { + { FormDefinitionPatcher.SettingsElemControlname, FormComponents.AdminContentItemSelectorComponent }, + } + }; + fi.AddFormItem(ffi); + } + classRefFields.Value[field.Key] = (ffi.Guid, field.Value.allowedTypes); + ffi.Settings[FormDefinitionPatcher.AllowedContentItemTypeIdentifiers] = JsonConvert.SerializeObject(field.Value.allowedTypes.ToArray()); + } + classInfo.ClassFormDefinition = fi.GetXmlDefinition(); + DataClassInfoProvider.ProviderObject.Set(classInfo); + } + + foreach (var parentItem in reusableItems) + { + var parentMappedNode = mappedSiteNodes[parentItem.Node.NodeGUID]; + foreach (var contentItemDataGuid in parentItem.ContentItemDataGuids) + { + var dataModel = new ContentItemDataModel + { + ContentItemDataGUID = contentItemDataGuid, + ContentItemDataCommonDataGuid = contentItemDataGuid, + ContentItemContentTypeName = parentMappedNode.TargetClassInfo.ClassName, + CustomProperties = [] + }; + + foreach (var childCollection in parentItem.ChildLinks.GroupBy(x => x.fieldName)) + { + var guidArray = childCollection.Select(x => new { Identifier = mappedSiteNodes[x.node.NodeGUID].ContentItemGuid }).ToArray(); + string serializedValue = JsonConvert.SerializeObject(guidArray); + dataModel.CustomProperties[childCollection.Key] = serializedValue; + } + + var importResult = await importer.ImportAsync(dataModel); + if (importResult is { Success: true }) + { + logger.LogInformation("Imported linked children of content item {ContentItemGuid}", parentItem.ContentItemGuid); + } + else + { + logger.LogError("Failed to import linked children of content item {ContentItemGuid}: {Exception}", parentItem.ContentItemGuid, importResult.Exception); + } + } } } diff --git a/KVA/Migration.Tool.Source/Mappers/ContentItemMapper.cs b/KVA/Migration.Tool.Source/Mappers/ContentItemMapper.cs index 055c39d7..b08f5e7f 100644 --- a/KVA/Migration.Tool.Source/Mappers/ContentItemMapper.cs +++ b/KVA/Migration.Tool.Source/Mappers/ContentItemMapper.cs @@ -80,9 +80,10 @@ ContentFolderService contentFolderService protected override IEnumerable MapInternal(CmsTreeMapperSource source) { (var cmsTree, string safeNodeName, var siteGuid, var nodeParentGuid, var cultureToLanguageGuid, string? targetFormDefinition, string sourceFormDefinition, var migratedDocuments, var sourceSite, bool deferred) = source; - logger.LogTrace("Mapping {Value}", new { cmsTree.NodeAliasPath, cmsTree.NodeName, cmsTree.NodeGUID, cmsTree.NodeSiteID }); + var childNodes = modelFacade.Select("NodeParentID = @nodeID", "NodeOrder", new SqlParameter("nodeID", cmsTree.NodeID))!.ToArray(); + 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; @@ -99,9 +100,14 @@ protected override IEnumerable MapInternal(CmsTreeMapperSource source yield break; } + var contentItemGuid = spoiledGuidContext.EnsureNodeGuid(cmsTree.NodeGUID, cmsTree.NodeSiteID, cmsTree.NodeID); + var formerUrlPaths = GetFormerUrlPaths(cmsTree); - var directive = GetDirective(new ContentItemSource(cmsTree, sourceNodeClass.ClassName, mapping?.TargetClassName ?? sourceNodeClass.ClassName, sourceSite, formerUrlPaths)); + var directive = GetDirective(new ContentItemSource(cmsTree, sourceNodeClass.ClassName, mapping?.TargetClassName ?? sourceNodeClass.ClassName, sourceSite, formerUrlPaths, childNodes)); + directive.ContentItemGuid = contentItemGuid; + directive.TargetClassInfo = targetClassInfo; + directive.Node = cmsTree; yield return directive; if (directive is DropDirective) @@ -117,7 +123,6 @@ 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 || configuration.ClassNamesConvertToContentHub.Contains(sourceNodeClass.ClassName); if (isMappedTypeReusable) { @@ -438,7 +443,6 @@ protected override IEnumerable MapInternal(CmsTreeMapperSource source : JsonConvert.DeserializeObject(hostPageCommonDataInfo.ContentItemCommonDataVisualBuilderWidgets)!; // fill widget properties - var childNodes = modelFacade.Select("NodeParentID = @nodeID", "NodeOrder", new SqlParameter("nodeID", cmsTree.NodeID)); var childItemGUIDs = childNodes.Select(x => ContentItemInfo.Provider.Get(x.NodeGUID)?.ContentItemGUID).Where(x => x is not null).Select(x => x!.Value).ToList(); // don't presume that ContentItem matching the Node exists. Director might have dropped it var widgetProperties = widgetDirective.ItemToWidgetPropertiesMapping?.Invoke(commonDataModel.CustomProperties.Concat(dataModel.CustomProperties).ToDictionary(), storeContentItem ? contentItemModel.ContentItemGUID : null, childItemGUIDs) ?? []; diff --git a/KVA/Migration.Tool.Source/Mappers/ContentItemMapperDirectives/ContentItemActionProvider.cs b/KVA/Migration.Tool.Source/Mappers/ContentItemMapperDirectives/ContentItemActionProvider.cs index 23fc7a41..b93ac011 100644 --- a/KVA/Migration.Tool.Source/Mappers/ContentItemMapperDirectives/ContentItemActionProvider.cs +++ b/KVA/Migration.Tool.Source/Mappers/ContentItemMapperDirectives/ContentItemActionProvider.cs @@ -1,4 +1,5 @@ -using Newtonsoft.Json.Linq; +using Migration.Tool.Source.Model; +using Newtonsoft.Json.Linq; namespace Migration.Tool.Source.Mappers.ContentItemMapperDirectives; internal partial class ContentItemActionProvider : IContentItemActionProvider @@ -20,4 +21,11 @@ public void OverridePageTemplate(string templateIdentifier, JObject? templatePro public void OverrideContentFolder(string displayNamePath) => Directive.ContentFolderOptions = new ContentFolderOptions(DisplayNamePath: displayNamePath); public void RegenerateUrlPath() => Directive.RegenerateUrlPath = true; public void OverrideFormerUrlPaths(IEnumerable formerPaths) => Directive.FormerUrlPaths = formerPaths; + public void LinkChildren(string fieldName, IEnumerable children) + { + foreach (var child in children) + { + Directive.ChildLinks.Add((fieldName, child)); + } + } } diff --git a/KVA/Migration.Tool.Source/Mappers/ContentItemMapperDirectives/ContentItemDirectiveBase.cs b/KVA/Migration.Tool.Source/Mappers/ContentItemMapperDirectives/ContentItemDirectiveBase.cs index dc31c199..3d060032 100644 --- a/KVA/Migration.Tool.Source/Mappers/ContentItemMapperDirectives/ContentItemDirectiveBase.cs +++ b/KVA/Migration.Tool.Source/Mappers/ContentItemMapperDirectives/ContentItemDirectiveBase.cs @@ -1,4 +1,6 @@ -using Kentico.Xperience.UMT.Model; +using CMS.DataEngine; +using Kentico.Xperience.UMT.Model; +using Migration.Tool.Source.Model; using Newtonsoft.Json.Linq; namespace Migration.Tool.Source.Mappers.ContentItemMapperDirectives; @@ -9,6 +11,13 @@ internal abstract class ContentItemDirectiveBase : IUmtModel public ContentFolderOptions? ContentFolderOptions { get; set; } public bool RegenerateUrlPath { get; set; } = false; public IEnumerable? FormerUrlPaths { get; set; } + public List<(string fieldName, ICmsTree)> ChildLinks { get; set; } = []; + + #region Mapping results, used for postprocessing + public Guid ContentItemGuid { get; set; } + public DataClassInfo? TargetClassInfo { get; set; } + public ICmsTree? Node { get; set; } + #endregion #region IUmtModel // This interface is implemented only as means to allow yielding the directive out of content item mapper. diff --git a/KVA/Migration.Tool.Source/Mappers/ContentItemMapperDirectives/ContentItemSource.cs b/KVA/Migration.Tool.Source/Mappers/ContentItemMapperDirectives/ContentItemSource.cs index 34aa11c6..7c1d94e7 100644 --- a/KVA/Migration.Tool.Source/Mappers/ContentItemMapperDirectives/ContentItemSource.cs +++ b/KVA/Migration.Tool.Source/Mappers/ContentItemMapperDirectives/ContentItemSource.cs @@ -1,4 +1,4 @@ using Migration.Tool.Source.Model; namespace Migration.Tool.Source.Mappers.ContentItemMapperDirectives; -public record ContentItemSource(ICmsTree SourceNode, string SourceClassName, string TargetClassName, ICmsSite SourceSite, IEnumerable FormerUrlPaths); +public record ContentItemSource(ICmsTree SourceNode, string SourceClassName, string TargetClassName, ICmsSite SourceSite, IEnumerable FormerUrlPaths, IEnumerable ChildNodes); diff --git a/KVA/Migration.Tool.Source/Mappers/ContentItemMapperDirectives/IContentItemActionProvider.cs b/KVA/Migration.Tool.Source/Mappers/ContentItemMapperDirectives/IContentItemActionProvider.cs index b3b1e16a..7543b360 100644 --- a/KVA/Migration.Tool.Source/Mappers/ContentItemMapperDirectives/IContentItemActionProvider.cs +++ b/KVA/Migration.Tool.Source/Mappers/ContentItemMapperDirectives/IContentItemActionProvider.cs @@ -1,4 +1,5 @@ -using Newtonsoft.Json.Linq; +using Migration.Tool.Source.Model; +using Newtonsoft.Json.Linq; namespace Migration.Tool.Source.Mappers.ContentItemMapperDirectives; public interface IContentItemActionProvider @@ -18,6 +19,15 @@ public interface IContentItemActionProvider /// void RegenerateUrlPath(); void OverrideFormerUrlPaths(IEnumerable formerPaths); + + /// + /// Add references to child content items to a field. If the field doesn't exist, it will be created. + /// Child content item's content types will be added to the field's allowed content types as necessary. + /// If the field exists and is not of content item reference type, the migration will fail. + /// + /// Name of the field to add the child content item references to + /// One or more child objects passed by + void LinkChildren(string fieldName, IEnumerable children); } public record FormerPageUrlPath(string LanguageName, string Path, DateTime? LastModified = null); diff --git a/KVA/Migration.Tool.Source/Mappers/NodeMapResult.cs b/KVA/Migration.Tool.Source/Mappers/NodeMapResult.cs new file mode 100644 index 00000000..16a7518e --- /dev/null +++ b/KVA/Migration.Tool.Source/Mappers/NodeMapResult.cs @@ -0,0 +1,10 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using CMS.DataEngine; +using Migration.Tool.Source.Model; + +namespace Migration.Tool.Source.Mappers; +public record NodeMapResult(ICmsTree Node, Guid ContentItemGuid, List ContentItemDataGuids, DataClassInfo TargetClassInfo, List<(string fieldName, ICmsTree node)> ChildLinks); diff --git a/Migration.Tool.CLI/README.md b/Migration.Tool.CLI/README.md index 3507cb78..bf2c8909 100644 --- a/Migration.Tool.CLI/README.md +++ b/Migration.Tool.CLI/README.md @@ -420,7 +420,7 @@ Add the options under the `Settings` section in the configuration file. | XbyKDirPath | The absolute file system path of the root of the target Xperience by Kentico project. Required to migrate media library and page attachment files. | | XbyKApiSettings | Configuration options set for the API when creating migrated objects in the target application.

The `ConnectionStrings.CMSConnectionString`option is required - set the connection string to the target Xperience by Kentico database (the same value as obsolete `XbKConnectionString`). | | MigrationProtocolPath | The absolute file system path of the location where the [migration protocol file](./MIGRATION_PROTOCOL_REFERENCE.md) is generated.

For example: `"C:\\Logs\\Migration.Tool.Protocol.log"` | -| ConvertClassesToContentHub | Specifies which page types are migrated to [reusable content items](https://docs.kentico.com/x/barWCQ) instead of website channel pages. Enter page type code names, separated with either `;` or `,`. See [Convert pages to Content hub](#convert-pages-to-content-hub) for detailed information. | +| ConvertClassesToContentHub | Specifies which page types are migrated to [reusable content items](https://docs.kentico.com/x/barWCQ) instead of website channel pages. Enter page type code names, separated with either `;` or `,`. See [Convert pages to Content hub](#convert-pages-to-content-hub) for detailed information. | | MigrateOnlyMediaFileInfo | If set to `true`, only the database representations of media files are migrated, without the files in the media folder in the project's file system. For example, enable this option if your media library files are mapped to a shared directory or Cloud storage.

If `false`, media files are migrated based on the `KxCmsDirPath` location. | | MigrateMediaToMediaLibrary | Determines whether media library files and attachments from the source instance are migrated to the target instance as media libraries or as [content item assets](https://docs.kentico.com/x/barWCQ) in the content hub. The default value is `false` – media files and attachments are migrated as content item assets.

See [Convert attachments and media library files to media libraries instad of content item assets](#convert-attachments-and-media-library-files-to-media-libraries-instead-of-content-item-assets) | | LegacyFlatAssetTree | Use legacy behaviour of versions up to 2.3.0. Content folders for asset content items will be created in a flat structure (all under root folder) | @@ -657,6 +657,8 @@ The migration then converts the specified page types to content types for reusab For advanced scenarios, you can use the extensibility feature to implement [customizations](../Migration.Tool.Extensions/README.md#custom-class-mappings) that map specific page types or individual fields to reusable content types. For example, this allows you to migrate multiple page types to a single content type. +To preserve relationship between page converted to reusable content item and its children, you can use [Custom child links](../Migration.Tool.Extensions/README.md#custom-child-links). This feature allows you to map children of the original page to a content item/page selector field of the target reusable content item. + ## Convert page types to reusable field schemas It is not possible to migrate any page types that inherit fields from other page types. However, to make the manual diff --git a/Migration.Tool.Common/Helpers/GuidHelper.cs b/Migration.Tool.Common/Helpers/GuidHelper.cs index 771178d8..2b5437fa 100644 --- a/Migration.Tool.Common/Helpers/GuidHelper.cs +++ b/Migration.Tool.Common/Helpers/GuidHelper.cs @@ -15,6 +15,7 @@ public static class GuidHelper public static readonly Guid GuidNsContentItem = new("EEBBD8D5-BA56-492F-969E-58E77EE90055"); public static readonly Guid GuidNsContentItemCommonData = new("31BA319C-843F-482A-9841-87BC62062DC2"); public static readonly Guid GuidNsContentItemLanguageMetadata = new("AAC0C3A9-3DE7-436E-AFAB-49C1E29D5DE2"); + public static readonly Guid GuidNsContentItemReference = new("9FEDEA1C-C677-4026-B5E8-1A83EC501D06"); public static Guid CreateWebPageUrlPathGuid(string hash) => GuidV5.NewNameBased(GuidNsWebPageUrlPathInfo, hash); public static Guid CreateWebPageFormerUrlPathGuid(string hash) => GuidV5.NewNameBased(GuidNsWebPageFormerUrlPathInfo, hash); @@ -27,6 +28,7 @@ public static class GuidHelper public static Guid CreateFolderGuid(string path) => GuidV5.NewNameBased(GuidNsFolder, path); public static Guid CreateDataClassGuid(string key) => GuidV5.NewNameBased(GuidNsDataClass, key); public static Guid CreateContentItemGuid(string key) => GuidV5.NewNameBased(GuidNsContentItem, key); + public static Guid CreateContentItemReferenceGuid(string key) => GuidV5.NewNameBased(GuidNsContentItemReference, key); public static Guid CreateContentItemCommonDataGuid(string key) => GuidV5.NewNameBased(GuidNsContentItemCommonData, key); public static Guid CreateContentItemLanguageMetadataGuid(string key) => GuidV5.NewNameBased(GuidNsContentItemLanguageMetadata, key); diff --git a/Migration.Tool.Extensions/CommunityMigrations/SampleChildLinkDirector.cs b/Migration.Tool.Extensions/CommunityMigrations/SampleChildLinkDirector.cs new file mode 100644 index 00000000..57c88b31 --- /dev/null +++ b/Migration.Tool.Extensions/CommunityMigrations/SampleChildLinkDirector.cs @@ -0,0 +1,30 @@ +using Migration.Tool.Source.Mappers.ContentItemMapperDirectives; + +namespace Migration.Tool.Extensions.CommunityMigrations; + +public class SampleChildLinkDirector : ContentItemDirectorBase +{ + public override void Direct(ContentItemSource source, IContentItemActionProvider options) + { + // + // Note that linking will take effect only for reusable content items (i.e. types in appsettings ConvertClassesToContentHub). + // For the purpose of this sample, ConvertClassesToContentHub must contain DancingGoatCore.ProductSection. + // + // If the field to link the children in doesn't exist, it will be created automatically. + // If the field doesn't allow any of the linked child's content type, the type will be added to allowed types automatically. + + // You can link any subset of child pages in one or more content item reference field, based on any filtering criteria + if (source.SourceNode.NodeName == "Accessories") + { + int filterPackClassID = 5550; + int tablewareClassID = 5531; + options.LinkChildren("Filters", source.ChildNodes.Where(x => x.NodeClassID == filterPackClassID)); + options.LinkChildren("Tableware", source.ChildNodes.Where(x => x.NodeClassID == tablewareClassID)); + } + else + { + // or link all children in general + options.LinkChildren("Children", source.ChildNodes); + } + } +} diff --git a/Migration.Tool.Extensions/README.md b/Migration.Tool.Extensions/README.md index a4aa6651..aa2b0965 100644 --- a/Migration.Tool.Extensions/README.md +++ b/Migration.Tool.Extensions/README.md @@ -8,6 +8,7 @@ To create custom migrations: - [Widget property migrations](#customize-widget-property-migrations) - [Page to widget migrations](#migrate-pages-to-widgets) - [Custom class mappings](#custom-class-mappings) + - [Custom child links](#custom-child-links) 2. [Register the migration](#register-migrations) ## Customize field migrations @@ -239,3 +240,14 @@ You can find sample class mappings in the [ClassMappingSample.cs](/Migration.Too - `AddReusableRemodelingSample` showcases how to migrate a page type as reusable content +## Custom child links + +This feature allows you to link child pages as referenced content items of a page converted to reusable content item. + +This feature is available by means of content item director. + +You can apply a simple general rule to link child pages e.g. in `Children` field or you can apply more elaborate rules. You can see samples of both approaches in [SampleChildLinkDirector.cs](./CommunityMigrations/SampleChildLinkDirector.cs) + +After implementing the content item director, you need to [register the director](#register-migrations) in the system. + + diff --git a/Migration.Tool.Extensions/ServiceCollectionExtensions.cs b/Migration.Tool.Extensions/ServiceCollectionExtensions.cs index feb8f4a1..e3b77ba3 100644 --- a/Migration.Tool.Extensions/ServiceCollectionExtensions.cs +++ b/Migration.Tool.Extensions/ServiceCollectionExtensions.cs @@ -23,6 +23,7 @@ public static IServiceCollection UseCustomizations(this IServiceCollection servi // services.AddReusableRemodelingSample(); // services.AddReusableSchemaIntegrationSample(); // services.AddTransient(); + // services.AddTransient(); return services; } } diff --git a/Migration.Tool.KXP.Extensions/Migration.Tool.KXP.Extensions.csproj b/Migration.Tool.KXP.Extensions/Migration.Tool.KXP.Extensions.csproj index a7f6093c..4db00de9 100644 --- a/Migration.Tool.KXP.Extensions/Migration.Tool.KXP.Extensions.csproj +++ b/Migration.Tool.KXP.Extensions/Migration.Tool.KXP.Extensions.csproj @@ -7,7 +7,7 @@ - + From 47b97fbf2e58feb39cd4ae5a4d8eadad1a298df4 Mon Sep 17 00:00:00 2001 From: akfakmot Date: Tue, 6 May 2025 10:30:51 +0200 Subject: [PATCH 2/2] Fix formatting Chore: Fix formatting --- .../Handlers/MigratePagesCommandHandler.cs | 1 - KVA/Migration.Tool.Source/Mappers/NodeMapResult.cs | 7 +------ 2 files changed, 1 insertion(+), 7 deletions(-) diff --git a/KVA/Migration.Tool.Source/Handlers/MigratePagesCommandHandler.cs b/KVA/Migration.Tool.Source/Handlers/MigratePagesCommandHandler.cs index c85f407a..67ef44fb 100644 --- a/KVA/Migration.Tool.Source/Handlers/MigratePagesCommandHandler.cs +++ b/KVA/Migration.Tool.Source/Handlers/MigratePagesCommandHandler.cs @@ -33,7 +33,6 @@ using Migration.Tool.Source.Services; using Newtonsoft.Json; using Newtonsoft.Json.Linq; -using Org.BouncyCastle.Asn1.X509; namespace Migration.Tool.Source.Handlers; // ReSharper disable once UnusedType.Global diff --git a/KVA/Migration.Tool.Source/Mappers/NodeMapResult.cs b/KVA/Migration.Tool.Source/Mappers/NodeMapResult.cs index 16a7518e..cc1d8796 100644 --- a/KVA/Migration.Tool.Source/Mappers/NodeMapResult.cs +++ b/KVA/Migration.Tool.Source/Mappers/NodeMapResult.cs @@ -1,9 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using CMS.DataEngine; +using CMS.DataEngine; using Migration.Tool.Source.Model; namespace Migration.Tool.Source.Mappers;