From 8f6d0a7fe7773984c8bbcca141fd1e6f9d927459 Mon Sep 17 00:00:00 2001 From: Martin Foukal Date: Sun, 7 Jan 2024 17:10:58 +0100 Subject: [PATCH 01/23] leads finishing wip --- examples/DancingGoat/Program.cs | 4 +-- .../ServiceCollectionExtensions.cs | 2 +- .../Workers/FailedSyncItemsWorkerBase.cs | 2 +- ....cs => DynamicsIntegrationGlobalEvents.cs} | 21 +++++++----- .../DynamicsServiceCollectionExtensions.cs | 11 +++--- ...s => SalesForceIntegrationGlobalEvents.cs} | 21 +++++++----- .../SalesForceServiceCollectionsExtensions.cs | 34 +++++++++++-------- 7 files changed, 55 insertions(+), 40 deletions(-) rename src/Kentico.Xperience.CRM.Dynamics/{DynamicsBizFormGlobalEvents.cs => DynamicsIntegrationGlobalEvents.cs} (76%) rename src/Kentico.Xperience.CRM.SalesForce/{SalesForceBizFormGlobalEvents.cs => SalesForceIntegrationGlobalEvents.cs} (76%) diff --git a/examples/DancingGoat/Program.cs b/examples/DancingGoat/Program.cs index 636da6a..db9b56f 100644 --- a/examples/DancingGoat/Program.cs +++ b/examples/DancingGoat/Program.cs @@ -51,7 +51,7 @@ ConfigureMembershipServices(builder.Services); //CRM integration registration start -builder.Services.AddDynamicsCrmLeadsIntegration(builder => +builder.Services.AddDynamicsFormLeadsIntegration(builder => builder.AddForm(DancingGoatContactUsItem.CLASS_NAME, //form class name c => c .MapField("UserFirstName", "firstname") @@ -64,7 +64,7 @@ builder.Configuration.GetSection(DynamicsIntegrationSettings.ConfigKeyName)) //config section with settings .AddCustomFormLeadsValidationService(); //optional -builder.Services.AddSalesForceCrmLeadsIntegration(builder => +builder.Services.AddSalesForceFormLeadsIntegration(builder => builder.AddForm(DancingGoatContactUsItem.CLASS_NAME, //form class name c => c .MapField("UserFirstName", "FirstName") //option1: mapping based on source and target field names diff --git a/src/Kentico.Xperience.CRM.Common/ServiceCollectionExtensions.cs b/src/Kentico.Xperience.CRM.Common/ServiceCollectionExtensions.cs index 4bb6900..f1fa823 100644 --- a/src/Kentico.Xperience.CRM.Common/ServiceCollectionExtensions.cs +++ b/src/Kentico.Xperience.CRM.Common/ServiceCollectionExtensions.cs @@ -19,7 +19,7 @@ public static class ServiceCollectionExtensions /// /// /// - public static IServiceCollection AddKenticoCrmCommonIntegration( + public static IServiceCollection AddKenticoCrmCommonFormLeadsIntegration( this IServiceCollection services, Action formsMappingConfig) where TMappingConfiguration : BizFormsMappingConfiguration, new() { diff --git a/src/Kentico.Xperience.CRM.Common/Workers/FailedSyncItemsWorkerBase.cs b/src/Kentico.Xperience.CRM.Common/Workers/FailedSyncItemsWorkerBase.cs index 9b74089..b905622 100644 --- a/src/Kentico.Xperience.CRM.Common/Workers/FailedSyncItemsWorkerBase.cs +++ b/src/Kentico.Xperience.CRM.Common/Workers/FailedSyncItemsWorkerBase.cs @@ -38,7 +38,7 @@ protected override void Process() try { - var settings = Service.Resolve>().Value; + var settings = Service.Resolve>().CurrentValue; if (!settings.FormLeadsEnabled) return; var failedSyncItemsService = Service.Resolve(); diff --git a/src/Kentico.Xperience.CRM.Dynamics/DynamicsBizFormGlobalEvents.cs b/src/Kentico.Xperience.CRM.Dynamics/DynamicsIntegrationGlobalEvents.cs similarity index 76% rename from src/Kentico.Xperience.CRM.Dynamics/DynamicsBizFormGlobalEvents.cs rename to src/Kentico.Xperience.CRM.Dynamics/DynamicsIntegrationGlobalEvents.cs index 61ced1b..5c511c9 100644 --- a/src/Kentico.Xperience.CRM.Dynamics/DynamicsBizFormGlobalEvents.cs +++ b/src/Kentico.Xperience.CRM.Dynamics/DynamicsIntegrationGlobalEvents.cs @@ -14,20 +14,20 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -[assembly: RegisterModule(typeof(DynamicsBizFormGlobalEvents))] +[assembly: RegisterModule(typeof(DynamicsIntegrationGlobalEvents))] namespace Kentico.Xperience.CRM.Dynamics; /// -/// Module with bizformitem event handlers for Dynamics integration +/// Module with BizFormItem and ContactInfo event handlers for Dynamics integration /// -internal class DynamicsBizFormGlobalEvents : Module +internal class DynamicsIntegrationGlobalEvents : Module { - public DynamicsBizFormGlobalEvents() : base(nameof(DynamicsBizFormGlobalEvents)) + public DynamicsIntegrationGlobalEvents() : base(nameof(DynamicsIntegrationGlobalEvents)) { } - private ILogger logger = null!; + private ILogger logger = null!; protected override void OnInit() { @@ -35,9 +35,12 @@ protected override void OnInit() BizFormItemEvents.Insert.After += BizFormInserted; BizFormItemEvents.Update.After += BizFormUpdated; - logger = Service.Resolve>(); + logger = Service.Resolve>(); Service.Resolve().Install(); - ThreadWorker.Current.EnsureRunningThread(); + RequestEvents.RunEndRequestTasks.Execute += (_, _) => + { + FailedItemsWorker.Current.EnsureRunningThread(); + }; } private void BizFormInserted(object? sender, BizFormItemEventArgs e) @@ -45,7 +48,7 @@ private void BizFormInserted(object? sender, BizFormItemEventArgs e) var failedSyncItemsService = Service.Resolve(); try { - var settings = Service.Resolve>().Value; + var settings = Service.Resolve>().CurrentValue; if (!settings.FormLeadsEnabled) return; using (var serviceScope = Service.Resolve().CreateScope()) @@ -67,7 +70,7 @@ private void BizFormUpdated(object? sender, BizFormItemEventArgs e) { try { - var settings = Service.Resolve>().Value; + var settings = Service.Resolve>().CurrentValue; if (!settings.FormLeadsEnabled) return; var mappingConfig = Service.Resolve(); diff --git a/src/Kentico.Xperience.CRM.Dynamics/DynamicsServiceCollectionExtensions.cs b/src/Kentico.Xperience.CRM.Dynamics/DynamicsServiceCollectionExtensions.cs index ff76f96..1ef9f69 100644 --- a/src/Kentico.Xperience.CRM.Dynamics/DynamicsServiceCollectionExtensions.cs +++ b/src/Kentico.Xperience.CRM.Dynamics/DynamicsServiceCollectionExtensions.cs @@ -4,6 +4,7 @@ using Kentico.Xperience.CRM.Dynamics.Services; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Microsoft.PowerPlatform.Dataverse.Client; @@ -19,14 +20,14 @@ public static class DynamicsServiceCollectionExtensions /// /// /// - public static IServiceCollection AddDynamicsCrmLeadsIntegration(this IServiceCollection serviceCollection, + public static IServiceCollection AddDynamicsFormLeadsIntegration(this IServiceCollection serviceCollection, Action formsConfig, IConfiguration configuration) { - serviceCollection.AddKenticoCrmCommonIntegration(formsConfig); + serviceCollection.AddKenticoCrmCommonFormLeadsIntegration(formsConfig); serviceCollection.AddOptions().Bind(configuration); - serviceCollection.AddSingleton(GetCrmServiceClient); + serviceCollection.TryAddSingleton(GetCrmServiceClient); serviceCollection.AddScoped(); return serviceCollection; } @@ -39,8 +40,8 @@ public static IServiceCollection AddDynamicsCrmLeadsIntegration(this IServiceCol /// private static ServiceClient GetCrmServiceClient(IServiceProvider serviceProvider) { - var settings = serviceProvider.GetRequiredService>().Value; - var logger = serviceProvider.GetRequiredService>(); + var settings = serviceProvider.GetRequiredService>().CurrentValue; + var logger = serviceProvider.GetRequiredService>(); if (settings.ApiConfig?.IsValid() is not true) { diff --git a/src/Kentico.Xperience.CRM.SalesForce/SalesForceBizFormGlobalEvents.cs b/src/Kentico.Xperience.CRM.SalesForce/SalesForceIntegrationGlobalEvents.cs similarity index 76% rename from src/Kentico.Xperience.CRM.SalesForce/SalesForceBizFormGlobalEvents.cs rename to src/Kentico.Xperience.CRM.SalesForce/SalesForceIntegrationGlobalEvents.cs index 86b9ec6..7c76f56 100644 --- a/src/Kentico.Xperience.CRM.SalesForce/SalesForceBizFormGlobalEvents.cs +++ b/src/Kentico.Xperience.CRM.SalesForce/SalesForceIntegrationGlobalEvents.cs @@ -14,18 +14,18 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -[assembly: RegisterModule(typeof(SalesForceBizFormGlobalEvents))] +[assembly: RegisterModule(typeof(SalesForceIntegrationGlobalEvents))] namespace Kentico.Xperience.CRM.SalesForce; /// -/// Module with bizformitem event handlers for SalesForce Sales integration +/// Module with BizFormItem and ContactInfo event handlers for SalesForce integration /// -internal class SalesForceBizFormGlobalEvents : Module +internal class SalesForceIntegrationGlobalEvents : Module { - private ILogger logger = null!; + private ILogger logger = null!; - public SalesForceBizFormGlobalEvents() : base(nameof(SalesForceBizFormGlobalEvents)) + public SalesForceIntegrationGlobalEvents() : base(nameof(SalesForceIntegrationGlobalEvents)) { } @@ -35,9 +35,14 @@ protected override void OnInit() BizFormItemEvents.Insert.After += BizFormInserted; BizFormItemEvents.Update.After += BizFormUpdated; - logger = Service.Resolve>(); + logger = Service.Resolve>(); Service.Resolve().Install(); ThreadWorker.Current.EnsureRunningThread(); + + RequestEvents.RunEndRequestTasks.Execute += (_, _) => + { + FailedItemsWorker.Current.EnsureRunningThread(); + }; } private void BizFormInserted(object? sender, BizFormItemEventArgs e) @@ -45,7 +50,7 @@ private void BizFormInserted(object? sender, BizFormItemEventArgs e) var failedSyncItemsService = Service.Resolve(); try { - var settings = Service.Resolve>().Value; + var settings = Service.Resolve>().CurrentValue; if (!settings.FormLeadsEnabled) return; using (var serviceScope = Service.Resolve().CreateScope()) @@ -67,7 +72,7 @@ private void BizFormUpdated(object? sender, BizFormItemEventArgs e) { try { - var settings = Service.Resolve>().Value; + var settings = Service.Resolve>().CurrentValue; if (!settings.FormLeadsEnabled) return; var mappingConfig = Service.Resolve(); diff --git a/src/Kentico.Xperience.CRM.SalesForce/SalesForceServiceCollectionsExtensions.cs b/src/Kentico.Xperience.CRM.SalesForce/SalesForceServiceCollectionsExtensions.cs index 0e7452b..03958fc 100644 --- a/src/Kentico.Xperience.CRM.SalesForce/SalesForceServiceCollectionsExtensions.cs +++ b/src/Kentico.Xperience.CRM.SalesForce/SalesForceServiceCollectionsExtensions.cs @@ -7,6 +7,7 @@ using System.Globalization; namespace Kentico.Xperience.CRM.SalesForce; + public static class SalesForceServiceCollectionsExtensions { /// @@ -17,13 +18,22 @@ public static class SalesForceServiceCollectionsExtensions /// /// /// - public static IServiceCollection AddSalesForceCrmLeadsIntegration(this IServiceCollection serviceCollection, + public static IServiceCollection AddSalesForceFormLeadsIntegration(this IServiceCollection serviceCollection, Action formsConfig, IConfiguration configuration) { - serviceCollection.AddKenticoCrmCommonIntegration(formsConfig); + serviceCollection.AddKenticoCrmCommonFormLeadsIntegration(formsConfig); + serviceCollection.AddOptions().Bind(configuration); + AddSalesForceCommonIntegration(serviceCollection, configuration); + serviceCollection.AddScoped(); + return serviceCollection; + } + + private static void AddSalesForceCommonIntegration(IServiceCollection serviceCollection, + IConfiguration configuration) + { // default cache for token management serviceCollection.AddDistributedMemoryCache(); @@ -44,19 +54,15 @@ public static IServiceCollection AddSalesForceCrmLeadsIntegration(this IServiceC //add http client for salesforce api serviceCollection.AddHttpClient(client => - { - var apiConfig = configuration.Get()?.ApiConfig; - - if (apiConfig?.IsValid() is not true) - throw new InvalidOperationException("Missing API settings"); - - string apiVersion = apiConfig.ApiVersion.ToString("F1", CultureInfo.InvariantCulture); - client.BaseAddress = new Uri($"{apiConfig.SalesForceUrl?.TrimEnd('/')}/services/data/v{apiVersion}/"); - }) - .AddClientCredentialsTokenHandler("salesforce.api.client"); + { + var apiConfig = configuration.Get()?.ApiConfig; + if (apiConfig?.IsValid() is not true) + throw new InvalidOperationException("Missing API settings"); - serviceCollection.AddScoped(); - return serviceCollection; + string apiVersion = apiConfig.ApiVersion.ToString("F1", CultureInfo.InvariantCulture); + client.BaseAddress = new Uri($"{apiConfig.SalesForceUrl?.TrimEnd('/')}/services/data/v{apiVersion}/"); + }) + .AddClientCredentialsTokenHandler("salesforce.api.client"); } } \ No newline at end of file From 9884f7704ca42c7801ece8b921273f5efdb47367 Mon Sep 17 00:00:00 2001 From: Martin Foukal Date: Mon, 8 Jan 2024 19:04:08 +0100 Subject: [PATCH 02/23] form leads new pairing, ui settings, sync service --- examples/DancingGoat/appsettings.json | 6 +- .../Classes/CRMSyncItemInfo.generated.cs | 161 +++++++++ .../CRMSyncItemInfoProvider.generated.cs | 19 + .../ICRMSyncItemInfoProvider.generated.cs | 11 + .../Configuration/BizFormsMappingBuilder.cs | 1 - .../BizFormsMappingConfiguration.cs | 1 - .../CommonIntegrationSettings.cs | 2 + .../Constants/CRMType.cs | 4 +- .../Installers/CRMModuleInstaller.cs | 334 ++++++++++++++++++ .../Installers/CrmModuleInstaller.cs | 136 ------- .../Installers/ICRMModuleInstaller.cs | 9 + .../Installers/ICrmModuleInstaller.cs | 8 - .../ServiceCollectionExtensions.cs | 6 +- .../Services/ICRMSyncItemService.cs | 11 + .../Services/ILeadsIntegrationService.cs | 9 +- .../Implementations/CRMSyncItemService.cs | 22 ++ .../LeadsIntegrationServiceCommon.cs | 35 +- .../Workers/FailedSyncItemsWorkerBase.cs | 2 +- .../DynamicsIntegrationGlobalEvents.cs | 43 +-- .../DynamicsLeadsIntegrationService.cs | 130 ++++--- .../SalesForceIntegrationGlobalEvents.cs | 43 +-- .../SalesForceLeadsIntegrationService.cs | 12 +- 22 files changed, 682 insertions(+), 323 deletions(-) create mode 100644 src/Kentico.Xperience.CRM.Common/Classes/CRMSyncItemInfo.generated.cs create mode 100644 src/Kentico.Xperience.CRM.Common/Classes/CRMSyncItemInfoProvider.generated.cs create mode 100644 src/Kentico.Xperience.CRM.Common/Classes/ICRMSyncItemInfoProvider.generated.cs create mode 100644 src/Kentico.Xperience.CRM.Common/Installers/CRMModuleInstaller.cs delete mode 100644 src/Kentico.Xperience.CRM.Common/Installers/CrmModuleInstaller.cs create mode 100644 src/Kentico.Xperience.CRM.Common/Installers/ICRMModuleInstaller.cs delete mode 100644 src/Kentico.Xperience.CRM.Common/Installers/ICrmModuleInstaller.cs create mode 100644 src/Kentico.Xperience.CRM.Common/Services/ICRMSyncItemService.cs create mode 100644 src/Kentico.Xperience.CRM.Common/Services/Implementations/CRMSyncItemService.cs diff --git a/examples/DancingGoat/appsettings.json b/examples/DancingGoat/appsettings.json index 890fdb9..506e2d7 100644 --- a/examples/DancingGoat/appsettings.json +++ b/examples/DancingGoat/appsettings.json @@ -25,11 +25,13 @@ }, "CMSHashStringSalt": "", "CMSDynamicsCRMIntegration": { - "FormLeadsEnabled": true + "FormLeadsEnabled": true, + "IgnoreExistingRecords": false //"ApiConfig" add to secrets.json }, "CMSSalesForceCRMIntegration": { - "FormLeadsEnabled": true + "FormLeadsEnabled": true, + "IgnoreExistingRecords": false //"ApiConfig" add to secrets.json } } diff --git a/src/Kentico.Xperience.CRM.Common/Classes/CRMSyncItemInfo.generated.cs b/src/Kentico.Xperience.CRM.Common/Classes/CRMSyncItemInfo.generated.cs new file mode 100644 index 0000000..57f54d4 --- /dev/null +++ b/src/Kentico.Xperience.CRM.Common/Classes/CRMSyncItemInfo.generated.cs @@ -0,0 +1,161 @@ +using System; +using System.Data; +using System.Runtime.Serialization; + +using CMS; +using CMS.DataEngine; +using CMS.Helpers; +using Kentico.Xperience.CRM.Common.Classes; + +[assembly: RegisterObjectType(typeof(CRMSyncItemInfo), CRMSyncItemInfo.OBJECT_TYPE)] + +namespace Kentico.Xperience.CRM.Common.Classes +{ + /// + /// Data container class for . + /// + [Serializable] + public partial class CRMSyncItemInfo : AbstractInfo + { + /// + /// Object type. + /// + public const string OBJECT_TYPE = "kenticocrmcommon.crmsyncitem"; + + + /// + /// Type information. + /// +#warning "You will need to configure the type info." + public static readonly ObjectTypeInfo TYPEINFO = new ObjectTypeInfo(typeof(CRMSyncItemInfoProvider), OBJECT_TYPE, "kenticocrmcommon.crmsyncitem", "CRMSyncItemID", "CRMSyncItemLastModified", null, null, null, null, null, null) + { + ModuleName = "Kentic.Xperience.CRM.Common", + TouchCacheDependencies = true, + }; + + + /// + /// CRM sync item ID. + /// + [DatabaseField] + public virtual int CRMSyncItemID + { + get => ValidationHelper.GetInteger(GetValue(nameof(CRMSyncItemID)), 0); + set => SetValue(nameof(CRMSyncItemID), value); + } + + + /// + /// CRM sync item entity class. + /// + [DatabaseField] + public virtual string CRMSyncItemEntityClass + { + get => ValidationHelper.GetString(GetValue(nameof(CRMSyncItemEntityClass)), String.Empty); + set => SetValue(nameof(CRMSyncItemEntityClass), value); + } + + + /// + /// CRM sync item entity ID. + /// + [DatabaseField] + public virtual string CRMSyncItemEntityID + { + get => ValidationHelper.GetString(GetValue(nameof(CRMSyncItemEntityID)), String.Empty); + set => SetValue(nameof(CRMSyncItemEntityID), value); + } + + + /// + /// CRM sync item CRMID. + /// + [DatabaseField] + public virtual string CRMSyncItemCRMID + { + get => ValidationHelper.GetString(GetValue(nameof(CRMSyncItemCRMID)), String.Empty); + set => SetValue(nameof(CRMSyncItemCRMID), value); + } + + + /// + /// CRM sync item entity CRM. + /// + [DatabaseField] + public virtual string CRMSyncItemEntityCRM + { + get => ValidationHelper.GetString(GetValue(nameof(CRMSyncItemEntityCRM)), String.Empty); + set => SetValue(nameof(CRMSyncItemEntityCRM), value); + } + + + /// + /// CRM sync item created by kentico. + /// + [DatabaseField] + public virtual bool CRMSyncItemCreatedByKentico + { + get => ValidationHelper.GetBoolean(GetValue(nameof(CRMSyncItemCreatedByKentico)), false); + set => SetValue(nameof(CRMSyncItemCreatedByKentico), value); + } + + + /// + /// CRM sync item last modified. + /// + [DatabaseField] + public virtual DateTime CRMSyncItemLastModified + { + get => ValidationHelper.GetDateTime(GetValue(nameof(CRMSyncItemLastModified)), DateTimeHelper.ZERO_TIME); + set => SetValue(nameof(CRMSyncItemLastModified), value); + } + + + /// + /// Deletes the object using appropriate provider. + /// + protected override void DeleteObject() + { + Provider.Delete(this); + } + + + /// + /// Updates the object using appropriate provider. + /// + protected override void SetObject() + { + Provider.Set(this); + } + + + /// + /// Constructor for de-serialization. + /// + /// Serialization info. + /// Streaming context. + protected CRMSyncItemInfo(SerializationInfo info, StreamingContext context) + : base(info, context) + { + } + + + /// + /// Creates an empty instance of the class. + /// + public CRMSyncItemInfo() + : base(TYPEINFO) + { + } + + + /// + /// Creates a new instances of the class from the given . + /// + /// DataRow with the object data. + public CRMSyncItemInfo(DataRow dr) + : base(TYPEINFO, dr) + { + } + } +} \ No newline at end of file diff --git a/src/Kentico.Xperience.CRM.Common/Classes/CRMSyncItemInfoProvider.generated.cs b/src/Kentico.Xperience.CRM.Common/Classes/CRMSyncItemInfoProvider.generated.cs new file mode 100644 index 0000000..39fb2cd --- /dev/null +++ b/src/Kentico.Xperience.CRM.Common/Classes/CRMSyncItemInfoProvider.generated.cs @@ -0,0 +1,19 @@ +using CMS.DataEngine; + +namespace Kentico.Xperience.CRM.Common.Classes +{ + /// + /// Class providing management. + /// + [ProviderInterface(typeof(ICRMSyncItemInfoProvider))] + public partial class CRMSyncItemInfoProvider : AbstractInfoProvider, ICRMSyncItemInfoProvider + { + /// + /// Initializes a new instance of the class. + /// + public CRMSyncItemInfoProvider() + : base(CRMSyncItemInfo.TYPEINFO) + { + } + } +} \ No newline at end of file diff --git a/src/Kentico.Xperience.CRM.Common/Classes/ICRMSyncItemInfoProvider.generated.cs b/src/Kentico.Xperience.CRM.Common/Classes/ICRMSyncItemInfoProvider.generated.cs new file mode 100644 index 0000000..ccefc0a --- /dev/null +++ b/src/Kentico.Xperience.CRM.Common/Classes/ICRMSyncItemInfoProvider.generated.cs @@ -0,0 +1,11 @@ +using CMS.DataEngine; + +namespace Kentico.Xperience.CRM.Common.Classes +{ + /// + /// Declares members for management. + /// + public partial interface ICRMSyncItemInfoProvider : IInfoProvider, IInfoByIdProvider + { + } +} \ No newline at end of file diff --git a/src/Kentico.Xperience.CRM.Common/Configuration/BizFormsMappingBuilder.cs b/src/Kentico.Xperience.CRM.Common/Configuration/BizFormsMappingBuilder.cs index f455ba2..e2b696e 100644 --- a/src/Kentico.Xperience.CRM.Common/Configuration/BizFormsMappingBuilder.cs +++ b/src/Kentico.Xperience.CRM.Common/Configuration/BizFormsMappingBuilder.cs @@ -30,7 +30,6 @@ internal TBizFormsConfiguration Build() { FormsMappings = forms.Select(f => (f.Key, f.Value.Build())) .ToDictionary(r => r.Key, r => r.Item2), - ExternalIdFieldName = externalIdFieldName }; } } \ No newline at end of file diff --git a/src/Kentico.Xperience.CRM.Common/Configuration/BizFormsMappingConfiguration.cs b/src/Kentico.Xperience.CRM.Common/Configuration/BizFormsMappingConfiguration.cs index dd473d5..df4c37e 100644 --- a/src/Kentico.Xperience.CRM.Common/Configuration/BizFormsMappingConfiguration.cs +++ b/src/Kentico.Xperience.CRM.Common/Configuration/BizFormsMappingConfiguration.cs @@ -8,5 +8,4 @@ namespace Kentico.Xperience.CRM.Common.Configuration; public class BizFormsMappingConfiguration { public Dictionary> FormsMappings { get; internal init; } = new(); - public string? ExternalIdFieldName { get; internal init; } } \ No newline at end of file diff --git a/src/Kentico.Xperience.CRM.Common/Configuration/CommonIntegrationSettings.cs b/src/Kentico.Xperience.CRM.Common/Configuration/CommonIntegrationSettings.cs index a06303e..6e71255 100644 --- a/src/Kentico.Xperience.CRM.Common/Configuration/CommonIntegrationSettings.cs +++ b/src/Kentico.Xperience.CRM.Common/Configuration/CommonIntegrationSettings.cs @@ -9,6 +9,8 @@ public class CommonIntegrationSettings public bool FormLeadsEnabled { get; set; } // @TODO phase 2 public bool ContactsEnabled { get; set; } + + public bool IgnoreExistingRecords { get; set; } public TApiConfig? ApiConfig { get; set; } } \ No newline at end of file diff --git a/src/Kentico.Xperience.CRM.Common/Constants/CRMType.cs b/src/Kentico.Xperience.CRM.Common/Constants/CRMType.cs index 03acf70..e2d385f 100644 --- a/src/Kentico.Xperience.CRM.Common/Constants/CRMType.cs +++ b/src/Kentico.Xperience.CRM.Common/Constants/CRMType.cs @@ -2,6 +2,6 @@ public static class CRMType { - public const string Dynamics = "dynamics"; - public const string SalesForce = "salesforce"; + public const string Dynamics = "Dynamics"; + public const string SalesForce = "Salesforce"; } \ No newline at end of file diff --git a/src/Kentico.Xperience.CRM.Common/Installers/CRMModuleInstaller.cs b/src/Kentico.Xperience.CRM.Common/Installers/CRMModuleInstaller.cs new file mode 100644 index 0000000..a72f745 --- /dev/null +++ b/src/Kentico.Xperience.CRM.Common/Installers/CRMModuleInstaller.cs @@ -0,0 +1,334 @@ +using CMS.Base; +using CMS.Core; +using CMS.DataEngine; +using CMS.FormEngine; +using CMS.Modules; +using Kentico.Xperience.CRM.Common.Classes; +using Kentico.Xperience.CRM.Common.Constants; + +namespace Kentico.Xperience.CRM.Common.Installers; + +/// +/// This installer creates custom module for common crm functionality +/// Currently this module contains only custom class for failed synchronizations items +/// which is created when not exists on start. +/// +public class CRMModuleInstaller : ICRMModuleInstaller +{ + private readonly IResourceInfoProvider resourceInfoProvider; + + public CRMModuleInstaller(IResourceInfoProvider resourceInfoProvider) + { + this.resourceInfoProvider = resourceInfoProvider; + } + + public void Install(string crmtype) + { + using (new CMSActionContext { ContinuousIntegrationAllowObjectSerialization = false }) + { + var resourceInfo = InstallModule(); + InstallModuleClasses(resourceInfo); + InstallSettings(resourceInfo, crmtype); + } + } + + private ResourceInfo InstallModule() + { + var resourceInfo = resourceInfoProvider.Get(ResourceConstants.ResourceName) ?? new ResourceInfo(); + + resourceInfo.ResourceDisplayName = ResourceConstants.ResourceDisplayName; + resourceInfo.ResourceName = ResourceConstants.ResourceName; + resourceInfo.ResourceDescription = ResourceConstants.ResourceDescription; + resourceInfo.ResourceIsInDevelopment = ResourceConstants.ResourceIsInDevelopment; + if (resourceInfo.HasChanged) + { + resourceInfoProvider.Set(resourceInfo); + } + + return resourceInfo; + } + + private void InstallModuleClasses(ResourceInfo resourceInfo) + { + InstallSyncedItemClass(resourceInfo); + InstallFailedSyncItemClass(resourceInfo); + } + + private void InstallSyncedItemClass(ResourceInfo resourceInfo) + { + var failedSyncItemClass = DataClassInfoProvider.GetDataClassInfo("kenticocrmcommon.crmsyncitem"); + if (failedSyncItemClass is not null) + { + return; + } + + failedSyncItemClass = DataClassInfo.New("kenticocrmcommon.crmsyncitem"); + + failedSyncItemClass.ClassName = "kenticocrmcommon.crmsyncitem"; + failedSyncItemClass.ClassTableName = "kenticocrmcommon.crmsyncitem".Replace(".", "_"); + failedSyncItemClass.ClassDisplayName = "CRM sync item"; + failedSyncItemClass.ClassResourceID = resourceInfo.ResourceID; + failedSyncItemClass.ClassType = ClassType.OTHER; + + var formInfo = FormHelper.GetBasicFormDefinition("CRMSyncItemID"); + + var formItem = new FormFieldInfo + { + Name = "CRMSyncItemEntityClass", + Visible = false, + Precision = 0, + Size = 100, + DataType = "text", + Enabled = true + }; + formInfo.AddFormItem(formItem); + + formItem = new FormFieldInfo + { + Name = "CRMSyncItemEntityID", + Visible = false, + Precision = 0, + Size = 50, + DataType = "text", + Enabled = true + }; + formInfo.AddFormItem(formItem); + + formItem = new FormFieldInfo + { + Name = "CRMSyncItemCRMID", + Visible = false, + Precision = 0, + Size = 50, + DataType = "text", + Enabled = true + }; + formInfo.AddFormItem(formItem); + + formItem = new FormFieldInfo + { + Name = "CRMSyncItemEntityCRM", + Visible = false, + Precision = 0, + Size = 50, + DataType = "text", + Enabled = true + }; + formInfo.AddFormItem(formItem); + + formItem = new FormFieldInfo + { + Name = "CRMSyncItemCreatedByKentico", Visible = false, DataType = "boolean", Enabled = true + }; + formInfo.AddFormItem(formItem); + + formItem = new FormFieldInfo + { + Name = "CRMSyncItemLastModified", + Visible = false, + Precision = 0, + DataType = "datetime", + Enabled = true + }; + formInfo.AddFormItem(formItem); + + failedSyncItemClass.ClassFormDefinition = formInfo.GetXmlDefinition(); + + DataClassInfoProvider.SetDataClassInfo(failedSyncItemClass); + } + + private void InstallFailedSyncItemClass(ResourceInfo resourceInfo) + { + var failedSyncItemClass = DataClassInfoProvider.GetDataClassInfo(FailedSyncItemInfo.OBJECT_TYPE); + if (failedSyncItemClass is not null) + { + return; + } + + failedSyncItemClass = DataClassInfo.New(FailedSyncItemInfo.OBJECT_TYPE); + + failedSyncItemClass.ClassName = FailedSyncItemInfo.OBJECT_TYPE; + failedSyncItemClass.ClassTableName = FailedSyncItemInfo.OBJECT_TYPE.Replace(".", "_"); + failedSyncItemClass.ClassDisplayName = "Failed sync item"; + failedSyncItemClass.ClassResourceID = resourceInfo.ResourceID; + failedSyncItemClass.ClassType = ClassType.OTHER; + + var formInfo = FormHelper.GetBasicFormDefinition(nameof(FailedSyncItemInfo.FailedSyncItemID)); + + var formItem = new FormFieldInfo + { + Name = nameof(FailedSyncItemInfo.FailedSyncItemEntityClass), + Visible = false, + Precision = 0, + Size = 100, + DataType = "text", + Enabled = true + }; + formInfo.AddFormItem(formItem); + + formItem = new FormFieldInfo + { + Name = nameof(FailedSyncItemInfo.FailedSyncItemEntityID), + Visible = false, + DataType = "integer", + Enabled = true + }; + formInfo.AddFormItem(formItem); + + formItem = new FormFieldInfo + { + Name = nameof(FailedSyncItemInfo.FailedSyncItemEntityCRM), + Visible = false, + Precision = 0, + Size = 50, + DataType = "text", + Enabled = true + }; + formInfo.AddFormItem(formItem); + + formItem = new FormFieldInfo + { + Name = nameof(FailedSyncItemInfo.FailedSyncItemTryCount), + Visible = false, + DataType = "integer", + Enabled = true + }; + formInfo.AddFormItem(formItem); + + formItem = new FormFieldInfo + { + Name = nameof(FailedSyncItemInfo.FailedSyncItemNextTime), + Visible = false, + Precision = 0, + DataType = "datetime", + Enabled = true + }; + formInfo.AddFormItem(formItem); + + formItem = new FormFieldInfo + { + Name = nameof(FailedSyncItemInfo.FailedSyncItemLastModified), + Visible = false, + Precision = 0, + DataType = "datetime", + Enabled = true + }; + formInfo.AddFormItem(formItem); + + failedSyncItemClass.ClassFormDefinition = formInfo.GetXmlDefinition(); + + DataClassInfoProvider.SetDataClassInfo(failedSyncItemClass); + } + + private void InstallSettings(ResourceInfo resourceInfo, string crmType) + { + var crmIntegrations = SettingsCategoryInfo.Provider.Get("kenticocrmcommon.crmintegrations"); + if (crmIntegrations is null) + { + var rootSettings = SettingsCategoryInfo.Provider.Get("CMS.Settings"); + if (rootSettings is null) + throw new InvalidOperationException("Category 'CMS.Settings' root not found"); + + crmIntegrations = new SettingsCategoryInfo + { + CategoryName = "kenticocrmcommon.crmintegrations", + CategoryDisplayName = "CRM integrations", + CategoryParentID = rootSettings.CategoryID, + CategoryLevel = 1, + CategoryResourceID = resourceInfo.ResourceID, + CategoryIsCustom = true, + CategoryIsGroup = false, + CategoryOrder = SettingsCategoryInfo.Provider.Get() + .Where(c => c.CategoryLevel == 1) + .Max(c => c.CategoryOrder) + 1 + }; + + SettingsCategoryInfo.Provider.Set(crmIntegrations); + } + + var crmCategory = SettingsCategoryInfo.Provider.Get($"kenticocrmcommon.{crmType}"); + if (crmCategory is null) + { + crmCategory = new SettingsCategoryInfo + { + CategoryName = $"kenticocrmcommon.{crmType}", + CategoryDisplayName = $"{crmType} settings", + CategoryParentID = crmIntegrations.CategoryID, + CategoryLevel = 2, + CategoryResourceID = resourceInfo.ResourceID, + CategoryIsCustom = true, + CategoryIsGroup = true + }; + + SettingsCategoryInfo.Provider.Set(crmCategory); + } + + var settingFormsEnabled = SettingsKeyInfo.Provider.Get($"CMS{crmType}CRMIntegrationFormLeadsEnabled"); + if (settingFormsEnabled is null) + { + settingFormsEnabled = new SettingsKeyInfo + { + KeyName = $"CMS{crmType}CRMIntegrationFormLeadsEnabled", + KeyDisplayName = "Form leads enabled", + KeyDescription = "", + KeyType = "boolean", + KeyCategoryID = crmCategory.CategoryID, + KeyIsCustom = true, + KeyExplanationText = "", + }; + + SettingsKeyInfo.Provider.Set(settingFormsEnabled); + } + + var settingUrl = SettingsKeyInfo.Provider.Get($"CMS{crmType}CRMIntegration{crmType}Url"); + if (settingUrl is null) + { + settingUrl = new SettingsKeyInfo + { + KeyName = $"CMS{crmType}CRMIntegration{crmType}Url", + KeyDisplayName = $"{crmType} URL", + KeyDescription = "", + KeyType = "string", + KeyCategoryID = crmCategory.CategoryID, + KeyIsCustom = true, + KeyExplanationText = "", + }; + + SettingsKeyInfo.Provider.Set(settingUrl); + } + + var settingClientId = SettingsKeyInfo.Provider.Get($"CMS{crmType}CRMIntegrationClientId"); + if (settingClientId is null) + { + settingClientId = new SettingsKeyInfo + { + KeyName = $"CMS{crmType}CRMIntegrationClientId", + KeyDisplayName = "Client ID", + KeyDescription = "", + KeyType = "string", + KeyCategoryID = crmCategory.CategoryID, + KeyIsCustom = true, + KeyExplanationText = "", + }; + + SettingsKeyInfo.Provider.Set(settingClientId); + } + + var settingClientSecret = SettingsKeyInfo.Provider.Get($"CMS{crmType}CRMIntegration{crmType}ClientSecret"); + if (settingClientSecret is null) + { + settingClientSecret = new SettingsKeyInfo + { + KeyName = $"CMS{crmType}CRMIntegration{crmType}ClientSecret", + KeyDisplayName = "Client Secret", + KeyDescription = "", + KeyType = "string", + KeyCategoryID = crmCategory.CategoryID, + KeyIsCustom = true, + KeyExplanationText = "", + }; + + SettingsKeyInfo.Provider.Set(settingClientSecret); + } + } +} \ No newline at end of file diff --git a/src/Kentico.Xperience.CRM.Common/Installers/CrmModuleInstaller.cs b/src/Kentico.Xperience.CRM.Common/Installers/CrmModuleInstaller.cs deleted file mode 100644 index df609b2..0000000 --- a/src/Kentico.Xperience.CRM.Common/Installers/CrmModuleInstaller.cs +++ /dev/null @@ -1,136 +0,0 @@ -using CMS.Base; -using CMS.DataEngine; -using CMS.FormEngine; -using CMS.Modules; -using Kentico.Xperience.CRM.Common.Classes; -using Kentico.Xperience.CRM.Common.Constants; - -namespace Kentico.Xperience.CRM.Common.Installers; - -/// -/// This installer creates custom module for common crm functionality -/// Currently this module contains only custom class for failed synchronizations items -/// which is created when not exists on start. -/// -public class CrmModuleInstaller : ICrmModuleInstaller -{ - private readonly IResourceInfoProvider resourceInfoProvider; - - public CrmModuleInstaller(IResourceInfoProvider resourceInfoProvider) - { - this.resourceInfoProvider = resourceInfoProvider; - } - - public void Install() - { - using (new CMSActionContext { ContinuousIntegrationAllowObjectSerialization = false }) - { - var resourceInfo = InstallModule(); - InstallModuleClasses(resourceInfo); - } - } - - private ResourceInfo InstallModule() - { - var resourceInfo = resourceInfoProvider.Get(ResourceConstants.ResourceName) ?? new ResourceInfo(); - - resourceInfo.ResourceDisplayName = ResourceConstants.ResourceDisplayName; - resourceInfo.ResourceName = ResourceConstants.ResourceName; - resourceInfo.ResourceDescription = ResourceConstants.ResourceDescription; - resourceInfo.ResourceIsInDevelopment = ResourceConstants.ResourceIsInDevelopment; - if (resourceInfo.HasChanged) - { - resourceInfoProvider.Set(resourceInfo); - } - - return resourceInfo; - } - - private void InstallModuleClasses(ResourceInfo resourceInfo) - { - InstallFailedSyncItemClass(resourceInfo); - } - - private void InstallFailedSyncItemClass(ResourceInfo resourceInfo) - { - var failedSyncItemClass = DataClassInfoProvider.GetDataClassInfo(FailedSyncItemInfo.OBJECT_TYPE); - if (failedSyncItemClass is not null) - { - return; - } - - failedSyncItemClass = DataClassInfo.New(FailedSyncItemInfo.OBJECT_TYPE); - - failedSyncItemClass.ClassName = FailedSyncItemInfo.OBJECT_TYPE; - failedSyncItemClass.ClassTableName = FailedSyncItemInfo.OBJECT_TYPE.Replace(".", "_"); - failedSyncItemClass.ClassDisplayName = "Failed sync item"; - failedSyncItemClass.ClassResourceID = resourceInfo.ResourceID; - failedSyncItemClass.ClassType = ClassType.OTHER; - - var formInfo = FormHelper.GetBasicFormDefinition(nameof(FailedSyncItemInfo.FailedSyncItemID)); - - var formItem = new FormFieldInfo - { - Name = nameof(FailedSyncItemInfo.FailedSyncItemEntityClass), - Visible = false, - Precision = 0, - Size = 100, - DataType = "text", - Enabled = true - }; - formInfo.AddFormItem(formItem); - - formItem = new FormFieldInfo - { - Name = nameof(FailedSyncItemInfo.FailedSyncItemEntityID), - Visible = false, - DataType = "integer", - Enabled = true - }; - formInfo.AddFormItem(formItem); - - formItem = new FormFieldInfo - { - Name = nameof(FailedSyncItemInfo.FailedSyncItemEntityCRM), - Visible = false, - Precision = 0, - Size = 50, - DataType = "text", - Enabled = true - }; - formInfo.AddFormItem(formItem); - - formItem = new FormFieldInfo - { - Name = nameof(FailedSyncItemInfo.FailedSyncItemTryCount), - Visible = false, - DataType = "integer", - Enabled = true - }; - formInfo.AddFormItem(formItem); - - formItem = new FormFieldInfo - { - Name = nameof(FailedSyncItemInfo.FailedSyncItemNextTime), - Visible = false, - Precision = 0, - DataType = "datetime", - Enabled = true - }; - formInfo.AddFormItem(formItem); - - formItem = new FormFieldInfo - { - Name = nameof(FailedSyncItemInfo.FailedSyncItemLastModified), - Visible = false, - Precision = 0, - DataType = "datetime", - Enabled = true - }; - formInfo.AddFormItem(formItem); - - failedSyncItemClass.ClassFormDefinition = formInfo.GetXmlDefinition(); - - DataClassInfoProvider.SetDataClassInfo(failedSyncItemClass); - } -} \ No newline at end of file diff --git a/src/Kentico.Xperience.CRM.Common/Installers/ICRMModuleInstaller.cs b/src/Kentico.Xperience.CRM.Common/Installers/ICRMModuleInstaller.cs new file mode 100644 index 0000000..23c26b9 --- /dev/null +++ b/src/Kentico.Xperience.CRM.Common/Installers/ICRMModuleInstaller.cs @@ -0,0 +1,9 @@ +using CMS.Base; +using Kentico.Xperience.CRM.Common.Constants; + +namespace Kentico.Xperience.CRM.Common.Installers; + +public interface ICRMModuleInstaller +{ + void Install(string crmType); +} \ No newline at end of file diff --git a/src/Kentico.Xperience.CRM.Common/Installers/ICrmModuleInstaller.cs b/src/Kentico.Xperience.CRM.Common/Installers/ICrmModuleInstaller.cs deleted file mode 100644 index ad7866a..0000000 --- a/src/Kentico.Xperience.CRM.Common/Installers/ICrmModuleInstaller.cs +++ /dev/null @@ -1,8 +0,0 @@ -using CMS.Base; - -namespace Kentico.Xperience.CRM.Common.Installers; - -public interface ICrmModuleInstaller -{ - void Install(); -} \ No newline at end of file diff --git a/src/Kentico.Xperience.CRM.Common/ServiceCollectionExtensions.cs b/src/Kentico.Xperience.CRM.Common/ServiceCollectionExtensions.cs index f1fa823..0797003 100644 --- a/src/Kentico.Xperience.CRM.Common/ServiceCollectionExtensions.cs +++ b/src/Kentico.Xperience.CRM.Common/ServiceCollectionExtensions.cs @@ -1,4 +1,5 @@ -using Kentico.Xperience.CRM.Common.Configuration; +using Kentico.Xperience.CRM.Common.Classes; +using Kentico.Xperience.CRM.Common.Configuration; using Kentico.Xperience.CRM.Common.Installers; using Kentico.Xperience.CRM.Common.Services; using Kentico.Xperience.CRM.Common.Services.Implementations; @@ -33,8 +34,9 @@ public static IServiceCollection AddKenticoCrmCommonFormLeadsIntegration(); }); - services.TryAddSingleton(); + services.TryAddSingleton(); services.TryAddSingleton(); + services.TryAddSingleton(); return services; } diff --git a/src/Kentico.Xperience.CRM.Common/Services/ICRMSyncItemService.cs b/src/Kentico.Xperience.CRM.Common/Services/ICRMSyncItemService.cs new file mode 100644 index 0000000..5c93843 --- /dev/null +++ b/src/Kentico.Xperience.CRM.Common/Services/ICRMSyncItemService.cs @@ -0,0 +1,11 @@ +using CMS.OnlineForms; +using Kentico.Xperience.CRM.Common.Classes; + +namespace Kentico.Xperience.CRM.Common.Services; + +public interface ICRMSyncItemService +{ + void LogFormLeadCreateItem(BizFormItem bizFormItem, string crmId, string crmName); + void LogFormLeadUpdateItem(BizFormItem bizFormItem, string crmId, string crmName); + CRMSyncItemInfo GetFormLeadSyncItem(BizFormItem bizFormItem, string crmName); +} \ No newline at end of file diff --git a/src/Kentico.Xperience.CRM.Common/Services/ILeadsIntegrationService.cs b/src/Kentico.Xperience.CRM.Common/Services/ILeadsIntegrationService.cs index f696ebe..a917e4a 100644 --- a/src/Kentico.Xperience.CRM.Common/Services/ILeadsIntegrationService.cs +++ b/src/Kentico.Xperience.CRM.Common/Services/ILeadsIntegrationService.cs @@ -7,17 +7,10 @@ namespace Kentico.Xperience.CRM.Common.Services; /// public interface ILeadsIntegrationService { - /// - /// Creates lead in CRM from BizForm item - /// - /// - /// - Task CreateLeadAsync(BizFormItem bizFormItem); - /// /// Updates lead in CRM from BizForm item /// /// /// - Task UpdateLeadAsync(BizFormItem bizFormItem); + Task SynchronizeLeadAsync(BizFormItem bizFormItem); } \ No newline at end of file diff --git a/src/Kentico.Xperience.CRM.Common/Services/Implementations/CRMSyncItemService.cs b/src/Kentico.Xperience.CRM.Common/Services/Implementations/CRMSyncItemService.cs new file mode 100644 index 0000000..83eab24 --- /dev/null +++ b/src/Kentico.Xperience.CRM.Common/Services/Implementations/CRMSyncItemService.cs @@ -0,0 +1,22 @@ +using CMS.OnlineForms; +using Kentico.Xperience.CRM.Common.Classes; + +namespace Kentico.Xperience.CRM.Common.Services.Implementations; + +internal class CRMSyncItemService : ICRMSyncItemService +{ + public void LogFormLeadCreateItem(BizFormItem bizFormItem, string crmId, string crmName) + { + throw new NotImplementedException(); + } + + public void LogFormLeadUpdateItem(BizFormItem bizFormItem, string crmId, string crmName) + { + throw new NotImplementedException(); + } + + public CRMSyncItemInfo GetFormLeadSyncItem(BizFormItem bizFormItem, string crmName) + { + throw new NotImplementedException(); + } +} \ No newline at end of file diff --git a/src/Kentico.Xperience.CRM.Common/Services/Implementations/LeadsIntegrationServiceCommon.cs b/src/Kentico.Xperience.CRM.Common/Services/Implementations/LeadsIntegrationServiceCommon.cs index 2f21ccc..e839678 100644 --- a/src/Kentico.Xperience.CRM.Common/Services/Implementations/LeadsIntegrationServiceCommon.cs +++ b/src/Kentico.Xperience.CRM.Common/Services/Implementations/LeadsIntegrationServiceCommon.cs @@ -24,34 +24,13 @@ protected LeadsIntegrationServiceCommon( this.validationService = validationService; this.logger = logger; } - - /// - /// Validates BizForm item, then get specific mapping and finally specific implementation is called - /// from inherited service - /// - /// - public async Task CreateLeadAsync(BizFormItem bizFormItem) - { - if (!await validationService.ValidateFormItem(bizFormItem)) - { - logger.LogInformation("BizForm item {ItemID} for {BizFormDisplayName} refused by validation", - bizFormItem.ItemID, bizFormItem.BizFormInfo.FormDisplayName); - return; - } - - if (bizFormMappingConfig.FormsMappings.TryGetValue(bizFormItem.BizFormClassName.ToLowerInvariant(), - out var formMapping)) - { - await CreateLeadAsync(bizFormItem, formMapping); - } - } - + /// /// Validates BizForm item, then get specific mapping and finally specific implementation is called /// from inherited service /// /// - public async Task UpdateLeadAsync(BizFormItem bizFormItem) + public async Task SynchronizeLeadAsync(BizFormItem bizFormItem) { if (!await validationService.ValidateFormItem(bizFormItem)) { @@ -63,13 +42,9 @@ public async Task UpdateLeadAsync(BizFormItem bizFormItem) if (bizFormMappingConfig.FormsMappings.TryGetValue(bizFormItem.BizFormClassName.ToLowerInvariant(), out var formMapping)) { - await UpdateLeadAsync(bizFormItem, formMapping); + await SynchronizeLeadAsync(bizFormItem, formMapping); } } - - protected abstract Task CreateLeadAsync(BizFormItem bizFormItem, IEnumerable fieldMappings); - protected abstract Task UpdateLeadAsync(BizFormItem bizFormItem, IEnumerable fieldMappings); - - protected virtual string FormatExternalId(BizFormItem bizFormItem) => - $"{bizFormItem.BizFormClassName}-{bizFormItem.ItemID}"; + + protected abstract Task SynchronizeLeadAsync(BizFormItem bizFormItem, IEnumerable fieldMappings); } \ No newline at end of file diff --git a/src/Kentico.Xperience.CRM.Common/Workers/FailedSyncItemsWorkerBase.cs b/src/Kentico.Xperience.CRM.Common/Workers/FailedSyncItemsWorkerBase.cs index b905622..8974f0e 100644 --- a/src/Kentico.Xperience.CRM.Common/Workers/FailedSyncItemsWorkerBase.cs +++ b/src/Kentico.Xperience.CRM.Common/Workers/FailedSyncItemsWorkerBase.cs @@ -56,7 +56,7 @@ protected override void Process() continue; } - leadsIntegrationService.UpdateLeadAsync(bizFormItem).ConfigureAwait(false).GetAwaiter().GetResult(); + leadsIntegrationService.SynchronizeLeadAsync(bizFormItem).ConfigureAwait(false).GetAwaiter().GetResult(); } } catch (Exception e) diff --git a/src/Kentico.Xperience.CRM.Dynamics/DynamicsIntegrationGlobalEvents.cs b/src/Kentico.Xperience.CRM.Dynamics/DynamicsIntegrationGlobalEvents.cs index 5c511c9..68483bb 100644 --- a/src/Kentico.Xperience.CRM.Dynamics/DynamicsIntegrationGlobalEvents.cs +++ b/src/Kentico.Xperience.CRM.Dynamics/DynamicsIntegrationGlobalEvents.cs @@ -33,58 +33,29 @@ protected override void OnInit() { base.OnInit(); - BizFormItemEvents.Insert.After += BizFormInserted; - BizFormItemEvents.Update.After += BizFormUpdated; + BizFormItemEvents.Insert.After += SynchronizeBizFormLead; + BizFormItemEvents.Update.After += SynchronizeBizFormLead; logger = Service.Resolve>(); - Service.Resolve().Install(); + Service.Resolve().Install(CRMType.Dynamics); RequestEvents.RunEndRequestTasks.Execute += (_, _) => { FailedItemsWorker.Current.EnsureRunningThread(); }; } - - private void BizFormInserted(object? sender, BizFormItemEventArgs e) + + private void SynchronizeBizFormLead(object? sender, BizFormItemEventArgs e) { - var failedSyncItemsService = Service.Resolve(); try { var settings = Service.Resolve>().CurrentValue; if (!settings.FormLeadsEnabled) return; - - using (var serviceScope = Service.Resolve().CreateScope()) - { - var leadsIntegrationService = serviceScope.ServiceProvider - .GetRequiredService(); - - leadsIntegrationService.CreateLeadAsync(e.Item).ConfigureAwait(false).GetAwaiter().GetResult(); - } - } - catch (Exception exception) - { - logger.LogError(exception, "Error occured during inserting lead"); - failedSyncItemsService.LogFailedLeadItem(e.Item, CRMType.Dynamics); - } - } - - private void BizFormUpdated(object? sender, BizFormItemEventArgs e) - { - try - { - var settings = Service.Resolve>().CurrentValue; - if (!settings.FormLeadsEnabled) return; - - var mappingConfig = Service.Resolve(); - if (mappingConfig.ExternalIdFieldName is not { Length: > 0 }) - { - return; - } - + using (var serviceScope = Service.Resolve().CreateScope()) { var leadsIntegrationService = serviceScope.ServiceProvider .GetRequiredService(); - leadsIntegrationService.UpdateLeadAsync(e.Item).ConfigureAwait(false).GetAwaiter().GetResult(); + leadsIntegrationService.SynchronizeLeadAsync(e.Item).ConfigureAwait(false).GetAwaiter().GetResult(); } } catch (Exception exception) diff --git a/src/Kentico.Xperience.CRM.Dynamics/Services/DynamicsLeadsIntegrationService.cs b/src/Kentico.Xperience.CRM.Dynamics/Services/DynamicsLeadsIntegrationService.cs index 0cfa6e9..0c1765c 100644 --- a/src/Kentico.Xperience.CRM.Dynamics/Services/DynamicsLeadsIntegrationService.cs +++ b/src/Kentico.Xperience.CRM.Dynamics/Services/DynamicsLeadsIntegrationService.cs @@ -1,4 +1,5 @@ -using CMS.OnlineForms; +using CMS.Helpers; +using CMS.OnlineForms; using Kentico.Xperience.CRM.Common.Constants; using Kentico.Xperience.CRM.Common.Mapping.Implementations; using Kentico.Xperience.CRM.Common.Services; @@ -6,6 +7,7 @@ using Kentico.Xperience.CRM.Dynamics.Configuration; using Kentico.Xperience.CRM.Dynamics.Dataverse.Entities; using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; using Microsoft.PowerPlatform.Dataverse.Client; using Microsoft.Xrm.Sdk; using Microsoft.Xrm.Sdk.Query; @@ -21,102 +23,125 @@ internal class DynamicsLeadsIntegrationService : LeadsIntegrationServiceCommon, private readonly DynamicsBizFormsMappingConfiguration bizFormMappingConfig; private readonly ServiceClient serviceClient; private readonly ILogger logger; + private readonly ICRMSyncItemService syncItemService; private readonly IFailedSyncItemService failedSyncItemService; + private readonly IOptionsMonitor settings; public DynamicsLeadsIntegrationService( DynamicsBizFormsMappingConfiguration bizFormMappingConfig, ILeadsIntegrationValidationService validationService, ServiceClient serviceClient, ILogger logger, - IFailedSyncItemService failedSyncItemService) + ICRMSyncItemService syncItemService, + IFailedSyncItemService failedSyncItemService, + IOptionsMonitor settings) : base(bizFormMappingConfig, validationService, logger) { this.bizFormMappingConfig = bizFormMappingConfig; this.serviceClient = serviceClient; this.logger = logger; + this.syncItemService = syncItemService; this.failedSyncItemService = failedSyncItemService; + this.settings = settings; } - protected override async Task CreateLeadAsync(BizFormItem bizFormItem, + protected override async Task SynchronizeLeadAsync(BizFormItem bizFormItem, IEnumerable fieldMappings) { try { - var leadEntity = new Lead(); - MapLead(bizFormItem, leadEntity, fieldMappings); - - if (leadEntity.Subject is null) + var syncItem = syncItemService.GetFormLeadSyncItem(bizFormItem, CRMType.Dynamics); + + if (syncItem is null) { - leadEntity.Subject = $"Form {bizFormItem.BizFormInfo.FormDisplayName} - ID: {bizFormItem.ItemID}"; + await UpdateByEmailOrCreate(bizFormItem, fieldMappings); } - - if (bizFormMappingConfig.ExternalIdFieldName is { Length: > 0 } externalIdFieldName) + else { - leadEntity[externalIdFieldName] = FormatExternalId(bizFormItem); + var existingLead = await GetLeadById(Guid.Parse(syncItem.CRMSyncItemCRMID)); + if (existingLead is null) + { + await UpdateByEmailOrCreate(bizFormItem, fieldMappings); + } + else if (!settings.CurrentValue.IgnoreExistingRecords) + { + await UpdateLeadAsync(existingLead, bizFormItem, fieldMappings); + } } - - await serviceClient.CreateAsync(leadEntity); - return true; } catch (FaultException e) { - logger.LogError(e, "Create lead failed - api error: {ApiResult}", e.Detail); + logger.LogError(e, "Sync lead failed - api error: {ApiResult}", e.Detail); failedSyncItemService.LogFailedLeadItem(bizFormItem, CRMType.Dynamics); } catch (Exception e) when (e.InnerException is FaultException ie) { - logger.LogError(e, "Create lead failed - api error: {ApiResult}", ie.Detail); + logger.LogError(e, "Sync lead failed - api error: {ApiResult}", ie.Detail); failedSyncItemService.LogFailedLeadItem(bizFormItem, CRMType.Dynamics); } catch (Exception e) { - logger.LogError(e, "Create lead failed - unknown api error"); + logger.LogError(e, "Sync lead failed - unknown api error"); failedSyncItemService.LogFailedLeadItem(bizFormItem, CRMType.Dynamics); } - - return false; } - protected override async Task UpdateLeadAsync(BizFormItem bizFormItem, + private async Task UpdateByEmailOrCreate(BizFormItem bizFormItem, IEnumerable fieldMappings) { - try + Lead? existingLead = null; + var emailMapping = fieldMappings.FirstOrDefault(m => + m.CRMFieldMapping is CRMFieldNameMapping nm && nm.CrmFieldName == "emailaddress1"); + if (emailMapping is not null) { - var leadEntity = await GetLeadByExternalId(FormatExternalId(bizFormItem)); - if (leadEntity is not null) + var email = ValidationHelper.GetString(emailMapping.FormFieldMapping.MapFormField(bizFormItem), + string.Empty); + if (!string.IsNullOrWhiteSpace(email)) { - MapLead(bizFormItem, leadEntity, fieldMappings); - await serviceClient.UpdateAsync(leadEntity); - failedSyncItemService.DeleteFailedSyncItem(CRMType.Dynamics, bizFormItem.BizFormClassName, bizFormItem.ItemID); - return true; + existingLead = await GetLeadByEmail(email); } - else - { - if (await CreateLeadAsync(bizFormItem, fieldMappings)) - { - failedSyncItemService.DeleteFailedSyncItem(CRMType.Dynamics, bizFormItem.BizFormClassName, bizFormItem.ItemID); - return true; - } + } - return false; - } + if (existingLead is null) + { + await CreateLeadAsync(bizFormItem, fieldMappings); } - catch (FaultException e) + else if (!settings.CurrentValue.IgnoreExistingRecords) { - logger.LogError(e, "Update lead failed - api error: {ApiResult}", e.Detail); - failedSyncItemService.LogFailedLeadItem(bizFormItem, CRMType.Dynamics); + await UpdateLeadAsync(existingLead, bizFormItem, fieldMappings); } - catch (Exception e) when (e.InnerException is FaultException ie) + } + + private async Task CreateLeadAsync(BizFormItem bizFormItem, + IEnumerable fieldMappings) + { + var leadEntity = new Lead(); + MapLead(bizFormItem, leadEntity, fieldMappings); + + if (leadEntity.Subject is null) { - logger.LogError(e, "Update lead failed - api error: {ApiResult}", ie.Detail); - failedSyncItemService.LogFailedLeadItem(bizFormItem, CRMType.Dynamics); + leadEntity.Subject = $"Form {bizFormItem.BizFormInfo.FormDisplayName} - ID: {bizFormItem.ItemID}"; } - catch (Exception e) + + await serviceClient.CreateAsync(leadEntity); + + failedSyncItemService.DeleteFailedSyncItem(CRMType.Dynamics, bizFormItem.BizFormClassName, + bizFormItem.ItemID); + } + + private async Task UpdateLeadAsync(Lead leadEntity, BizFormItem bizFormItem, + IEnumerable fieldMappings) + { + MapLead(bizFormItem, leadEntity, fieldMappings); + + if (leadEntity.Subject is null) { - logger.LogError(e, "Update lead failed - unknown api error"); - failedSyncItemService.LogFailedLeadItem(bizFormItem, CRMType.Dynamics); + leadEntity.Subject = $"Form {bizFormItem.BizFormInfo.FormDisplayName} - ID: {bizFormItem.ItemID}"; } - return false; + await serviceClient.UpdateAsync(leadEntity); + + failedSyncItemService.DeleteFailedSyncItem(CRMType.Dynamics, bizFormItem.BizFormClassName, + bizFormItem.ItemID); } protected virtual void MapLead(BizFormItem bizFormItem, Lead leadEntity, @@ -129,18 +154,19 @@ protected virtual void MapLead(BizFormItem bizFormItem, Lead leadEntity, _ = fieldMapping.CRMFieldMapping switch { CRMFieldNameMapping m => leadEntity[m.CrmFieldName] = formFieldValue, - _ => throw new ArgumentOutOfRangeException(nameof(fieldMapping.CRMFieldMapping), fieldMapping.CRMFieldMapping.GetType(), "Unsupported mapping") + _ => throw new ArgumentOutOfRangeException(nameof(fieldMapping.CRMFieldMapping), + fieldMapping.CRMFieldMapping.GetType(), "Unsupported mapping") }; } } - private async Task GetLeadByExternalId(string externalId) - { - if (string.IsNullOrWhiteSpace(bizFormMappingConfig.ExternalIdFieldName)) - return null; + private async Task GetLeadById(Guid leadId) + => (await serviceClient.RetrieveAsync(Lead.EntityLogicalName, leadId, new ColumnSet(true)))?.ToEntity(); + private async Task GetLeadByEmail(string email) + { var query = new QueryExpression(Lead.EntityLogicalName) { ColumnSet = new ColumnSet(true), TopCount = 1 }; - query.Criteria.AddCondition(bizFormMappingConfig.ExternalIdFieldName, ConditionOperator.Equal, externalId); + query.Criteria.AddCondition("emailaddress1", ConditionOperator.Equal, email); return (await serviceClient.RetrieveMultipleAsync(query)).Entities.FirstOrDefault()?.ToEntity(); } diff --git a/src/Kentico.Xperience.CRM.SalesForce/SalesForceIntegrationGlobalEvents.cs b/src/Kentico.Xperience.CRM.SalesForce/SalesForceIntegrationGlobalEvents.cs index 7c76f56..459fc6f 100644 --- a/src/Kentico.Xperience.CRM.SalesForce/SalesForceIntegrationGlobalEvents.cs +++ b/src/Kentico.Xperience.CRM.SalesForce/SalesForceIntegrationGlobalEvents.cs @@ -33,10 +33,10 @@ protected override void OnInit() { base.OnInit(); - BizFormItemEvents.Insert.After += BizFormInserted; - BizFormItemEvents.Update.After += BizFormUpdated; + BizFormItemEvents.Insert.After += SynchronizeBizFormLead; + BizFormItemEvents.Update.After += SynchronizeBizFormLead; logger = Service.Resolve>(); - Service.Resolve().Install(); + Service.Resolve().Install(CRMType.SalesForce); ThreadWorker.Current.EnsureRunningThread(); RequestEvents.RunEndRequestTasks.Execute += (_, _) => @@ -44,54 +44,27 @@ protected override void OnInit() FailedItemsWorker.Current.EnsureRunningThread(); }; } - - private void BizFormInserted(object? sender, BizFormItemEventArgs e) + + private void SynchronizeBizFormLead(object? sender, BizFormItemEventArgs e) { var failedSyncItemsService = Service.Resolve(); try { var settings = Service.Resolve>().CurrentValue; if (!settings.FormLeadsEnabled) return; - + using (var serviceScope = Service.Resolve().CreateScope()) { var leadsIntegrationService = serviceScope.ServiceProvider .GetRequiredService(); - leadsIntegrationService.CreateLeadAsync(e.Item).ConfigureAwait(false).GetAwaiter().GetResult(); - } - } - catch (Exception exception) - { - logger.LogError(exception, "Error occured during inserting lead"); - failedSyncItemsService.LogFailedLeadItem(e.Item, CRMType.SalesForce); - } - } - - private void BizFormUpdated(object? sender, BizFormItemEventArgs e) - { - try - { - var settings = Service.Resolve>().CurrentValue; - if (!settings.FormLeadsEnabled) return; - - var mappingConfig = Service.Resolve(); - if (string.IsNullOrWhiteSpace(mappingConfig.ExternalIdFieldName)) - { - return; - } - - using (var serviceScope = Service.Resolve().CreateScope()) - { - var leadsIntegrationService = serviceScope.ServiceProvider - .GetRequiredService(); - - leadsIntegrationService.UpdateLeadAsync(e.Item).ConfigureAwait(false).GetAwaiter().GetResult(); + leadsIntegrationService.SynchronizeLeadAsync(e.Item).ConfigureAwait(false).GetAwaiter().GetResult(); } } catch (Exception exception) { logger.LogError(exception, "Error occured during updating lead"); + failedSyncItemsService.LogFailedLeadItem(e.Item, CRMType.SalesForce); } } } \ No newline at end of file diff --git a/src/Kentico.Xperience.CRM.SalesForce/Services/SalesForceLeadsIntegrationService.cs b/src/Kentico.Xperience.CRM.SalesForce/Services/SalesForceLeadsIntegrationService.cs index 3afdd18..350b511 100644 --- a/src/Kentico.Xperience.CRM.SalesForce/Services/SalesForceLeadsIntegrationService.cs +++ b/src/Kentico.Xperience.CRM.SalesForce/Services/SalesForceLeadsIntegrationService.cs @@ -32,7 +32,7 @@ public SalesForceLeadsIntegrationService( this.failedSyncItemService = failedSyncItemService; } - protected override async Task CreateLeadAsync(BizFormItem bizFormItem, + protected async Task CreateLeadAsync(BizFormItem bizFormItem, IEnumerable fieldMappings) { try @@ -42,10 +42,6 @@ protected override async Task CreateLeadAsync(BizFormItem bizFormItem, lead.LeadSource ??= $"Form {bizFormItem.BizFormInfo.FormDisplayName} - ID: {bizFormItem.ItemID}"; lead.Company ??= "undefined"; //required field - set to 'undefined' to prevent errors - if (bizFormMappingConfig.ExternalIdFieldName is { Length: > 0 } externalIdFieldName) - { - lead.AdditionalProperties[externalIdFieldName] = FormatExternalId(bizFormItem); - } await apiService.CreateLeadAsync(lead); return true; @@ -69,14 +65,12 @@ protected override async Task CreateLeadAsync(BizFormItem bizFormItem, return false; } - protected override async Task UpdateLeadAsync(BizFormItem bizFormItem, + protected override async Task SynchronizeLeadAsync(BizFormItem bizFormItem, IEnumerable fieldMappings) { try { - string? leadId = string.IsNullOrWhiteSpace(bizFormMappingConfig.ExternalIdFieldName) ? null : - await apiService.GetLeadIdByExternalId(bizFormMappingConfig.ExternalIdFieldName!, - FormatExternalId(bizFormItem)); + string? leadId = null;//@TODO if (leadId is not null) { From 1da2bad99415d150afbff9c361e7d22ae92a460c Mon Sep 17 00:00:00 2001 From: Martin Foukal Date: Mon, 8 Jan 2024 23:54:30 +0100 Subject: [PATCH 03/23] sync service, salesforce pairing wip --- .../Services/ICRMSyncItemService.cs | 2 +- .../Implementations/CRMSyncItemService.cs | 44 +++++++-- .../DynamicsLeadsIntegrationService.cs | 4 +- .../SalesForceLeadsIntegrationService.cs | 92 +++++++++---------- 4 files changed, 85 insertions(+), 57 deletions(-) diff --git a/src/Kentico.Xperience.CRM.Common/Services/ICRMSyncItemService.cs b/src/Kentico.Xperience.CRM.Common/Services/ICRMSyncItemService.cs index 5c93843..c553c49 100644 --- a/src/Kentico.Xperience.CRM.Common/Services/ICRMSyncItemService.cs +++ b/src/Kentico.Xperience.CRM.Common/Services/ICRMSyncItemService.cs @@ -7,5 +7,5 @@ public interface ICRMSyncItemService { void LogFormLeadCreateItem(BizFormItem bizFormItem, string crmId, string crmName); void LogFormLeadUpdateItem(BizFormItem bizFormItem, string crmId, string crmName); - CRMSyncItemInfo GetFormLeadSyncItem(BizFormItem bizFormItem, string crmName); + CRMSyncItemInfo? GetFormLeadSyncItem(BizFormItem bizFormItem, string crmName); } \ No newline at end of file diff --git a/src/Kentico.Xperience.CRM.Common/Services/Implementations/CRMSyncItemService.cs b/src/Kentico.Xperience.CRM.Common/Services/Implementations/CRMSyncItemService.cs index 83eab24..c47d87d 100644 --- a/src/Kentico.Xperience.CRM.Common/Services/Implementations/CRMSyncItemService.cs +++ b/src/Kentico.Xperience.CRM.Common/Services/Implementations/CRMSyncItemService.cs @@ -1,22 +1,52 @@ using CMS.OnlineForms; using Kentico.Xperience.CRM.Common.Classes; +using Kentico.Xperience.CRM.Common.Constants; namespace Kentico.Xperience.CRM.Common.Services.Implementations; internal class CRMSyncItemService : ICRMSyncItemService { - public void LogFormLeadCreateItem(BizFormItem bizFormItem, string crmId, string crmName) + private readonly ICRMSyncItemInfoProvider crmSyncItemInfoProvider; + + public CRMSyncItemService(ICRMSyncItemInfoProvider crmSyncItemInfoProvider) { - throw new NotImplementedException(); + this.crmSyncItemInfoProvider = crmSyncItemInfoProvider; } + public void LogFormLeadCreateItem(BizFormItem bizFormItem, string crmId, string crmName) + => LogFormLeadSyncItem(bizFormItem, crmId, crmName, false); + public void LogFormLeadUpdateItem(BizFormItem bizFormItem, string crmId, string crmName) - { - throw new NotImplementedException(); - } + => LogFormLeadSyncItem(bizFormItem, crmId, crmName, false); - public CRMSyncItemInfo GetFormLeadSyncItem(BizFormItem bizFormItem, string crmName) + private void LogFormLeadSyncItem(BizFormItem bizFormItem, string crmId, string crmName, bool createdByKentico) { - throw new NotImplementedException(); + var syncItem = GetFormLeadSyncItem(bizFormItem, crmName); + if (syncItem is null) + { + new CRMSyncItemInfo + { + CRMSyncItemEntityID = bizFormItem.ItemID.ToString(), + CRMSyncItemEntityClass = bizFormItem.BizFormClassName, + CRMSyncItemEntityCRM = crmName, + CRMSyncItemCRMID = crmId, + CRMSyncItemCreatedByKentico = createdByKentico + }.Insert(); + } + else + { + syncItem.CRMSyncItemCRMID = crmId; + syncItem.CRMSyncItemCreatedByKentico = createdByKentico; + syncItem.Update(); + } } + + public CRMSyncItemInfo? GetFormLeadSyncItem(BizFormItem bizFormItem, string crmName) + => crmSyncItemInfoProvider.Get() + .TopN(1) + .WhereEquals(nameof(CRMSyncItemInfo.CRMSyncItemEntityClass), bizFormItem.BizFormClassName) + .WhereEquals(nameof(CRMSyncItemInfo.CRMSyncItemEntityID), bizFormItem.ItemID) + .WhereEquals(nameof(CRMSyncItemInfo.CRMSyncItemEntityCRM), crmName) + .FirstOrDefault(); + } \ No newline at end of file diff --git a/src/Kentico.Xperience.CRM.Dynamics/Services/DynamicsLeadsIntegrationService.cs b/src/Kentico.Xperience.CRM.Dynamics/Services/DynamicsLeadsIntegrationService.cs index 0c1765c..27fa6e3 100644 --- a/src/Kentico.Xperience.CRM.Dynamics/Services/DynamicsLeadsIntegrationService.cs +++ b/src/Kentico.Xperience.CRM.Dynamics/Services/DynamicsLeadsIntegrationService.cs @@ -122,8 +122,9 @@ private async Task CreateLeadAsync(BizFormItem bizFormItem, leadEntity.Subject = $"Form {bizFormItem.BizFormInfo.FormDisplayName} - ID: {bizFormItem.ItemID}"; } - await serviceClient.CreateAsync(leadEntity); + var leadId = await serviceClient.CreateAsync(leadEntity); + syncItemService.LogFormLeadCreateItem(bizFormItem, leadId.ToString(), CRMType.Dynamics); failedSyncItemService.DeleteFailedSyncItem(CRMType.Dynamics, bizFormItem.BizFormClassName, bizFormItem.ItemID); } @@ -140,6 +141,7 @@ private async Task UpdateLeadAsync(Lead leadEntity, BizFormItem bizFormItem, await serviceClient.UpdateAsync(leadEntity); + syncItemService.LogFormLeadUpdateItem(bizFormItem, leadEntity.LeadId.ToString()!, CRMType.Dynamics); failedSyncItemService.DeleteFailedSyncItem(CRMType.Dynamics, bizFormItem.BizFormClassName, bizFormItem.ItemID); } diff --git a/src/Kentico.Xperience.CRM.SalesForce/Services/SalesForceLeadsIntegrationService.cs b/src/Kentico.Xperience.CRM.SalesForce/Services/SalesForceLeadsIntegrationService.cs index 350b511..8e0dcab 100644 --- a/src/Kentico.Xperience.CRM.SalesForce/Services/SalesForceLeadsIntegrationService.cs +++ b/src/Kentico.Xperience.CRM.SalesForce/Services/SalesForceLeadsIntegrationService.cs @@ -16,79 +16,50 @@ internal class SalesForceLeadsIntegrationService : LeadsIntegrationServiceCommon private readonly SalesForceBizFormsMappingConfiguration bizFormMappingConfig; private readonly ISalesForceApiService apiService; private readonly ILogger logger; + private readonly ICRMSyncItemService syncItemService; private readonly IFailedSyncItemService failedSyncItemService; + private readonly IOptionsMonitor settings; public SalesForceLeadsIntegrationService( SalesForceBizFormsMappingConfiguration bizFormMappingConfig, ILeadsIntegrationValidationService validationService, ISalesForceApiService apiService, ILogger logger, - IFailedSyncItemService failedSyncItemService) + ICRMSyncItemService syncItemService, + IFailedSyncItemService failedSyncItemService, + IOptionsMonitor settings) : base(bizFormMappingConfig, validationService, logger) { this.bizFormMappingConfig = bizFormMappingConfig; this.apiService = apiService; this.logger = logger; + this.syncItemService = syncItemService; this.failedSyncItemService = failedSyncItemService; + this.settings = settings; } - - protected async Task CreateLeadAsync(BizFormItem bizFormItem, - IEnumerable fieldMappings) - { - try - { - var lead = new LeadSObject(); - MapLead(bizFormItem, lead, fieldMappings); - - lead.LeadSource ??= $"Form {bizFormItem.BizFormInfo.FormDisplayName} - ID: {bizFormItem.ItemID}"; - lead.Company ??= "undefined"; //required field - set to 'undefined' to prevent errors - - await apiService.CreateLeadAsync(lead); - return true; - } - catch (ApiException> e) - { - logger.LogError(e, "Create lead failed - api error: {ApiResult}", JsonSerializer.Serialize(e.Result)); - failedSyncItemService.LogFailedLeadItem(bizFormItem, CRMType.SalesForce); - } - catch (ApiException> e) - { - logger.LogError(e, "Create lead failed - api error: {ApiResult}", JsonSerializer.Serialize(e.Result)); - failedSyncItemService.LogFailedLeadItem(bizFormItem, CRMType.SalesForce); - } - catch (ApiException e) - { - logger.LogError(e, "Create lead failed - unexpected api error"); - failedSyncItemService.LogFailedLeadItem(bizFormItem, CRMType.SalesForce); - } - - return false; - } - + protected override async Task SynchronizeLeadAsync(BizFormItem bizFormItem, IEnumerable fieldMappings) { try { - string? leadId = null;//@TODO - - if (leadId is not null) + var syncItem = syncItemService.GetFormLeadSyncItem(bizFormItem, CRMType.Dynamics); + + if (syncItem is null) { - var lead = new LeadSObject(); - MapLead(bizFormItem, lead, fieldMappings); - await apiService.UpdateLeadAsync(leadId, lead); - failedSyncItemService.DeleteFailedSyncItem(CRMType.SalesForce, bizFormItem.BizFormClassName, bizFormItem.ItemID); - return true; + await UpdateByEmailOrCreate(bizFormItem, fieldMappings); } else { - if (await CreateLeadAsync(bizFormItem, fieldMappings)) + var existingLead = await GetLeadById(Guid.Parse(syncItem.CRMSyncItemCRMID)); + if (existingLead is null) { - failedSyncItemService.DeleteFailedSyncItem(CRMType.SalesForce, bizFormItem.BizFormClassName, bizFormItem.ItemID); - return true; + await UpdateByEmailOrCreate(bizFormItem, fieldMappings); + } + else if (!settings.CurrentValue.IgnoreExistingRecords) + { + await UpdateLeadAsync(existingLead, bizFormItem, fieldMappings); } - - return false; } } catch (ApiException> e) @@ -110,6 +81,31 @@ protected override async Task SynchronizeLeadAsync(BizFormItem bizFormItem return false; } + private async Task CreateLeadAsync(BizFormItem bizFormItem, + IEnumerable fieldMappings) + { + var lead = new LeadSObject(); + MapLead(bizFormItem, lead, fieldMappings); + + lead.LeadSource ??= $"Form {bizFormItem.BizFormInfo.FormDisplayName} - ID: {bizFormItem.ItemID}"; + lead.Company ??= "undefined"; //required field - set to 'undefined' to prevent errors + + var result = await apiService.CreateLeadAsync(lead); + + syncItemService.LogFormLeadUpdateItem(bizFormItem, result.Id!, CRMType.SalesForce); + failedSyncItemService.DeleteFailedSyncItem(CRMType.SalesForce, bizFormItem.BizFormClassName, bizFormItem.ItemID); + } + + private async Task UpdateLeadAsync(string leadId, BizFormItem bizFormItem, + IEnumerable fieldMappings) + { + var lead = new LeadSObject(); + MapLead(bizFormItem, lead, fieldMappings); + + await apiService.UpdateLeadAsync(leadId, lead); + failedSyncItemService.DeleteFailedSyncItem(CRMType.SalesForce, bizFormItem.BizFormClassName, bizFormItem.ItemID); + } + protected virtual void MapLead(BizFormItem bizFormItem, LeadSObject lead, IEnumerable fieldMappings) { From 31febeba9abec809f343a635a4e19a0e3b865869 Mon Sep 17 00:00:00 2001 From: Martin Foukal Date: Tue, 9 Jan 2024 00:18:37 +0100 Subject: [PATCH 04/23] salesforce wip --- .../DynamicsLeadsIntegrationService.cs | 14 ++---- .../Services/ISalesForceApiService.cs | 14 ++++++ .../Services/SalesForceApiService.cs | 8 ++++ .../SalesForceLeadsIntegrationService.cs | 46 ++++++++++++++----- 4 files changed, 62 insertions(+), 20 deletions(-) diff --git a/src/Kentico.Xperience.CRM.Dynamics/Services/DynamicsLeadsIntegrationService.cs b/src/Kentico.Xperience.CRM.Dynamics/Services/DynamicsLeadsIntegrationService.cs index 27fa6e3..3a35602 100644 --- a/src/Kentico.Xperience.CRM.Dynamics/Services/DynamicsLeadsIntegrationService.cs +++ b/src/Kentico.Xperience.CRM.Dynamics/Services/DynamicsLeadsIntegrationService.cs @@ -89,16 +89,12 @@ private async Task UpdateByEmailOrCreate(BizFormItem bizFormItem, IEnumerable fieldMappings) { Lead? existingLead = null; - var emailMapping = fieldMappings.FirstOrDefault(m => - m.CRMFieldMapping is CRMFieldNameMapping nm && nm.CrmFieldName == "emailaddress1"); - if (emailMapping is not null) + var tmpLead = new Lead(); + MapLead(bizFormItem, tmpLead, fieldMappings); + + if (!string.IsNullOrWhiteSpace(tmpLead.EMailAddress1)) { - var email = ValidationHelper.GetString(emailMapping.FormFieldMapping.MapFormField(bizFormItem), - string.Empty); - if (!string.IsNullOrWhiteSpace(email)) - { - existingLead = await GetLeadByEmail(email); - } + existingLead = await GetLeadByEmail(tmpLead.EMailAddress1); } if (existingLead is null) diff --git a/src/Kentico.Xperience.CRM.SalesForce/Services/ISalesForceApiService.cs b/src/Kentico.Xperience.CRM.SalesForce/Services/ISalesForceApiService.cs index fb46de4..b9232da 100644 --- a/src/Kentico.Xperience.CRM.SalesForce/Services/ISalesForceApiService.cs +++ b/src/Kentico.Xperience.CRM.SalesForce/Services/ISalesForceApiService.cs @@ -29,4 +29,18 @@ public interface ISalesForceApiService /// External ID value /// Task GetLeadIdByExternalId(string fieldName, string externalId); + + /// + /// + /// + /// + /// + Task GetLeadById(string id); + + /// + /// + /// + /// + /// + Task GetLeadByEmail(string email); } \ No newline at end of file diff --git a/src/Kentico.Xperience.CRM.SalesForce/Services/SalesForceApiService.cs b/src/Kentico.Xperience.CRM.SalesForce/Services/SalesForceApiService.cs index 1469124..a19ff01 100644 --- a/src/Kentico.Xperience.CRM.SalesForce/Services/SalesForceApiService.cs +++ b/src/Kentico.Xperience.CRM.SalesForce/Services/SalesForceApiService.cs @@ -74,4 +74,12 @@ public async Task UpdateLeadAsync(string id, LeadSObject leadSObject) throw new ApiException("Unexpected response", (int)response.StatusCode, responseMessage, null!, null); } } + + public async Task GetLeadById(string id) + => await apiClient.LeadGET2Async(id); + + public Task GetLeadByEmail(string email) + { + throw new NotImplementedException(); + } } \ No newline at end of file diff --git a/src/Kentico.Xperience.CRM.SalesForce/Services/SalesForceLeadsIntegrationService.cs b/src/Kentico.Xperience.CRM.SalesForce/Services/SalesForceLeadsIntegrationService.cs index 8e0dcab..ef3f58a 100644 --- a/src/Kentico.Xperience.CRM.SalesForce/Services/SalesForceLeadsIntegrationService.cs +++ b/src/Kentico.Xperience.CRM.SalesForce/Services/SalesForceLeadsIntegrationService.cs @@ -37,28 +37,28 @@ public SalesForceLeadsIntegrationService( this.failedSyncItemService = failedSyncItemService; this.settings = settings; } - + protected override async Task SynchronizeLeadAsync(BizFormItem bizFormItem, IEnumerable fieldMappings) { try { var syncItem = syncItemService.GetFormLeadSyncItem(bizFormItem, CRMType.Dynamics); - + if (syncItem is null) { await UpdateByEmailOrCreate(bizFormItem, fieldMappings); } else { - var existingLead = await GetLeadById(Guid.Parse(syncItem.CRMSyncItemCRMID)); + var existingLead = await apiService.GetLeadById(syncItem.CRMSyncItemCRMID); if (existingLead is null) { await UpdateByEmailOrCreate(bizFormItem, fieldMappings); } else if (!settings.CurrentValue.IgnoreExistingRecords) { - await UpdateLeadAsync(existingLead, bizFormItem, fieldMappings); + await UpdateLeadAsync(existingLead.Id!, bizFormItem, fieldMappings); } } } @@ -81,8 +81,29 @@ protected override async Task SynchronizeLeadAsync(BizFormItem bizFormItem return false; } - private async Task CreateLeadAsync(BizFormItem bizFormItem, - IEnumerable fieldMappings) + private async Task UpdateByEmailOrCreate(BizFormItem bizFormItem, IEnumerable fieldMappings) + { + LeadSObject? existingLead = null; + + var tmpLead = new LeadSObject(); + MapLead(bizFormItem, new LeadSObject(), fieldMappings); + + if (!string.IsNullOrWhiteSpace(tmpLead.Email)) + { + existingLead = await apiService.GetLeadByEmail(tmpLead.Email); + } + + if (existingLead is null) + { + await CreateLeadAsync(bizFormItem, fieldMappings); + } + else if (!settings.CurrentValue.IgnoreExistingRecords) + { + await UpdateLeadAsync(existingLead.Id!, bizFormItem, fieldMappings); + } + } + + private async Task CreateLeadAsync(BizFormItem bizFormItem, IEnumerable fieldMappings) { var lead = new LeadSObject(); MapLead(bizFormItem, lead, fieldMappings); @@ -91,9 +112,10 @@ private async Task CreateLeadAsync(BizFormItem bizFormItem, lead.Company ??= "undefined"; //required field - set to 'undefined' to prevent errors var result = await apiService.CreateLeadAsync(lead); - + syncItemService.LogFormLeadUpdateItem(bizFormItem, result.Id!, CRMType.SalesForce); - failedSyncItemService.DeleteFailedSyncItem(CRMType.SalesForce, bizFormItem.BizFormClassName, bizFormItem.ItemID); + failedSyncItemService.DeleteFailedSyncItem(CRMType.SalesForce, bizFormItem.BizFormClassName, + bizFormItem.ItemID); } private async Task UpdateLeadAsync(string leadId, BizFormItem bizFormItem, @@ -101,9 +123,10 @@ private async Task UpdateLeadAsync(string leadId, BizFormItem bizFormItem, { var lead = new LeadSObject(); MapLead(bizFormItem, lead, fieldMappings); - + await apiService.UpdateLeadAsync(leadId, lead); - failedSyncItemService.DeleteFailedSyncItem(CRMType.SalesForce, bizFormItem.BizFormClassName, bizFormItem.ItemID); + failedSyncItemService.DeleteFailedSyncItem(CRMType.SalesForce, bizFormItem.BizFormClassName, + bizFormItem.ItemID); } protected virtual void MapLead(BizFormItem bizFormItem, LeadSObject lead, @@ -116,7 +139,8 @@ protected virtual void MapLead(BizFormItem bizFormItem, LeadSObject lead, { CRMFieldNameMapping m => lead.AdditionalProperties[m.CrmFieldName] = formFieldValue, CRMFieldMappingFunction m => m.MapCrmField(lead, formFieldValue), - _ => throw new ArgumentOutOfRangeException(nameof(fieldMapping.CRMFieldMapping), fieldMapping.CRMFieldMapping.GetType(), "Unsupported mapping") + _ => throw new ArgumentOutOfRangeException(nameof(fieldMapping.CRMFieldMapping), + fieldMapping.CRMFieldMapping.GetType(), "Unsupported mapping") }; } } From c4dd96ce69820e843d21813971aac6e9e2785bbc Mon Sep 17 00:00:00 2001 From: Martin Foukal Date: Tue, 9 Jan 2024 14:09:00 +0100 Subject: [PATCH 05/23] leads pairing logic finished --- .../DancingGoat/Controllers/TestController.cs | 17 ++++++++++++++ examples/DancingGoat/Program.cs | 2 -- examples/DancingGoat/appsettings.json | 2 +- .../Configuration/BizFormsMappingBuilder.cs | 9 +------- .../Implementations/CRMSyncItemService.cs | 2 +- .../Models/QueryResult.cs | 8 +++++++ .../Models/QueryResultBase.cs | 10 ++++++++ .../Services/ISalesForceApiService.cs | 2 +- .../Services/SalesForceApiService.cs | 23 ++++++++++++++++--- .../SalesForceLeadsIntegrationService.cs | 22 ++++++++++-------- 10 files changed, 71 insertions(+), 26 deletions(-) create mode 100644 examples/DancingGoat/Controllers/TestController.cs create mode 100644 src/Kentico.Xperience.CRM.SalesForce/Models/QueryResult.cs create mode 100644 src/Kentico.Xperience.CRM.SalesForce/Models/QueryResultBase.cs diff --git a/examples/DancingGoat/Controllers/TestController.cs b/examples/DancingGoat/Controllers/TestController.cs new file mode 100644 index 0000000..1a0165f --- /dev/null +++ b/examples/DancingGoat/Controllers/TestController.cs @@ -0,0 +1,17 @@ +using CMS.OnlineForms; +using CMS.OnlineForms.Types; +using Microsoft.AspNetCore.Mvc; + +namespace DancingGoat.Controllers; + +[Route("[controller]/[action]")] +public class TestController : Controller +{ + [HttpGet("{id:int}")] + public IActionResult FormLead(int id) + { + var item = BizFormItemProvider.GetItem(id, DancingGoatContactUsItem.CLASS_NAME); + item.Update(); + return Ok(); + } +} \ No newline at end of file diff --git a/examples/DancingGoat/Program.cs b/examples/DancingGoat/Program.cs index db9b56f..7955669 100644 --- a/examples/DancingGoat/Program.cs +++ b/examples/DancingGoat/Program.cs @@ -59,7 +59,6 @@ .MapField(c => c.UserEmail, e => e.EMailAddress1) //generated form class used .MapField(b => b.GetStringValue("UserMessage", ""), e => e.Description) //general BizFormItem used ) - .ExternalIdField("crf1c_kenticoid") //optional custom field when you want updates to work , builder.Configuration.GetSection(DynamicsIntegrationSettings.ConfigKeyName)) //config section with settings .AddCustomFormLeadsValidationService(); //optional @@ -74,7 +73,6 @@ .MapField(b => b.GetStringValue("UserMessage", ""), e => e.Description) //option 4: source mapping function general BizFormItem -> member expression to SObject ) - .ExternalIdField("KenticoID__c") //optional custom field when you want updates to work //.AddForm("formname") // add another forms definitions , builder.Configuration.GetSection(SalesForceIntegrationSettings.ConfigKeyName)); //config section with settings diff --git a/examples/DancingGoat/appsettings.json b/examples/DancingGoat/appsettings.json index 506e2d7..496d4ac 100644 --- a/examples/DancingGoat/appsettings.json +++ b/examples/DancingGoat/appsettings.json @@ -25,7 +25,7 @@ }, "CMSHashStringSalt": "", "CMSDynamicsCRMIntegration": { - "FormLeadsEnabled": true, + "FormLeadsEnabled": false, "IgnoreExistingRecords": false //"ApiConfig" add to secrets.json }, diff --git a/src/Kentico.Xperience.CRM.Common/Configuration/BizFormsMappingBuilder.cs b/src/Kentico.Xperience.CRM.Common/Configuration/BizFormsMappingBuilder.cs index e2b696e..f4e7bff 100644 --- a/src/Kentico.Xperience.CRM.Common/Configuration/BizFormsMappingBuilder.cs +++ b/src/Kentico.Xperience.CRM.Common/Configuration/BizFormsMappingBuilder.cs @@ -6,7 +6,6 @@ public class BizFormsMappingBuilder { private readonly Dictionary forms = new(); - private string? externalIdFieldName; public BizFormsMappingBuilder AddForm(string formCodeName, Func configureFields) @@ -16,13 +15,7 @@ public BizFormsMappingBuilder AddForm(string formCodeName, forms.Add(formCodeName.ToLowerInvariant(), configureFields(new BizFormFieldsMappingBuilder())); return this; } - - public BizFormsMappingBuilder ExternalIdField(string fieldName) - { - externalIdFieldName = fieldName; - return this; - } - + internal TBizFormsConfiguration Build() where TBizFormsConfiguration : BizFormsMappingConfiguration, new() { diff --git a/src/Kentico.Xperience.CRM.Common/Services/Implementations/CRMSyncItemService.cs b/src/Kentico.Xperience.CRM.Common/Services/Implementations/CRMSyncItemService.cs index c47d87d..f9d3deb 100644 --- a/src/Kentico.Xperience.CRM.Common/Services/Implementations/CRMSyncItemService.cs +++ b/src/Kentico.Xperience.CRM.Common/Services/Implementations/CRMSyncItemService.cs @@ -14,7 +14,7 @@ public CRMSyncItemService(ICRMSyncItemInfoProvider crmSyncItemInfoProvider) } public void LogFormLeadCreateItem(BizFormItem bizFormItem, string crmId, string crmName) - => LogFormLeadSyncItem(bizFormItem, crmId, crmName, false); + => LogFormLeadSyncItem(bizFormItem, crmId, crmName, true); public void LogFormLeadUpdateItem(BizFormItem bizFormItem, string crmId, string crmName) => LogFormLeadSyncItem(bizFormItem, crmId, crmName, false); diff --git a/src/Kentico.Xperience.CRM.SalesForce/Models/QueryResult.cs b/src/Kentico.Xperience.CRM.SalesForce/Models/QueryResult.cs new file mode 100644 index 0000000..0a1db51 --- /dev/null +++ b/src/Kentico.Xperience.CRM.SalesForce/Models/QueryResult.cs @@ -0,0 +1,8 @@ +using System.Text.Json.Serialization; + +namespace Kentico.Xperience.CRM.SalesForce.Models; + +public class QueryResult : QueryResultBase +{ + [JsonPropertyName("records")] public List Records { get; set; } = new(); +} \ No newline at end of file diff --git a/src/Kentico.Xperience.CRM.SalesForce/Models/QueryResultBase.cs b/src/Kentico.Xperience.CRM.SalesForce/Models/QueryResultBase.cs new file mode 100644 index 0000000..2e6b226 --- /dev/null +++ b/src/Kentico.Xperience.CRM.SalesForce/Models/QueryResultBase.cs @@ -0,0 +1,10 @@ +using System.Text.Json.Serialization; + +namespace Kentico.Xperience.CRM.SalesForce.Models; + +public class QueryResultBase +{ + [JsonPropertyName("totalSize")] public int TotalSize { get; set; } + + [JsonPropertyName("done")] public bool Done { get; set; } +} \ No newline at end of file diff --git a/src/Kentico.Xperience.CRM.SalesForce/Services/ISalesForceApiService.cs b/src/Kentico.Xperience.CRM.SalesForce/Services/ISalesForceApiService.cs index b9232da..444a197 100644 --- a/src/Kentico.Xperience.CRM.SalesForce/Services/ISalesForceApiService.cs +++ b/src/Kentico.Xperience.CRM.SalesForce/Services/ISalesForceApiService.cs @@ -42,5 +42,5 @@ public interface ISalesForceApiService /// /// /// - Task GetLeadByEmail(string email); + Task GetLeadByEmail(string email); } \ No newline at end of file diff --git a/src/Kentico.Xperience.CRM.SalesForce/Services/SalesForceApiService.cs b/src/Kentico.Xperience.CRM.SalesForce/Services/SalesForceApiService.cs index a19ff01..b92249b 100644 --- a/src/Kentico.Xperience.CRM.SalesForce/Services/SalesForceApiService.cs +++ b/src/Kentico.Xperience.CRM.SalesForce/Services/SalesForceApiService.cs @@ -1,4 +1,5 @@ using Kentico.Xperience.CRM.SalesForce.Configuration; +using Kentico.Xperience.CRM.SalesForce.Models; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using SalesForce.OpenApi; @@ -76,10 +77,26 @@ public async Task UpdateLeadAsync(string id, LeadSObject leadSObject) } public async Task GetLeadById(string id) - => await apiClient.LeadGET2Async(id); + => await apiClient.LeadGET2Async(id, nameof(LeadSObject.Id)); - public Task GetLeadByEmail(string email) + public async Task GetLeadByEmail(string email) { - throw new NotImplementedException(); + var apiVersion = (integrationSettings?.ApiConfig?.ApiVersion ?? SalesForceApiConfig.DefaultVersion) + .ToString("F1", CultureInfo.InvariantCulture); + using var request = + new HttpRequestMessage(HttpMethod.Get, $"/services/data/v{apiVersion}/query?q=SELECT+Id+FROM+Lead+WHERE+Email='{email}'+ORDER+BY+CreatedDate+DESC"); + request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(MediaTypeNames.Application.Json)); + var response = await httpClient.SendAsync(request); + + if (response.IsSuccessStatusCode) + { + var queryResult = await response.Content.ReadFromJsonAsync>(); + return queryResult?.Records.FirstOrDefault()?.Id; + } + else + { + string responseMessage = await response.Content.ReadAsStringAsync(); + throw new ApiException("Unexpected response", (int)response.StatusCode, responseMessage, null!, null); + } } } \ No newline at end of file diff --git a/src/Kentico.Xperience.CRM.SalesForce/Services/SalesForceLeadsIntegrationService.cs b/src/Kentico.Xperience.CRM.SalesForce/Services/SalesForceLeadsIntegrationService.cs index ef3f58a..b3637d9 100644 --- a/src/Kentico.Xperience.CRM.SalesForce/Services/SalesForceLeadsIntegrationService.cs +++ b/src/Kentico.Xperience.CRM.SalesForce/Services/SalesForceLeadsIntegrationService.cs @@ -38,12 +38,12 @@ public SalesForceLeadsIntegrationService( this.settings = settings; } - protected override async Task SynchronizeLeadAsync(BizFormItem bizFormItem, + protected override async Task SynchronizeLeadAsync(BizFormItem bizFormItem, IEnumerable fieldMappings) { try { - var syncItem = syncItemService.GetFormLeadSyncItem(bizFormItem, CRMType.Dynamics); + var syncItem = syncItemService.GetFormLeadSyncItem(bizFormItem, CRMType.SalesForce); if (syncItem is null) { @@ -77,29 +77,27 @@ protected override async Task SynchronizeLeadAsync(BizFormItem bizFormItem logger.LogError(e, "Update lead failed - unexpected api error"); failedSyncItemService.LogFailedLeadItem(bizFormItem, CRMType.SalesForce); } - - return false; } private async Task UpdateByEmailOrCreate(BizFormItem bizFormItem, IEnumerable fieldMappings) { - LeadSObject? existingLead = null; + string? existingLeadId = null; var tmpLead = new LeadSObject(); - MapLead(bizFormItem, new LeadSObject(), fieldMappings); + MapLead(bizFormItem, tmpLead, fieldMappings); if (!string.IsNullOrWhiteSpace(tmpLead.Email)) { - existingLead = await apiService.GetLeadByEmail(tmpLead.Email); + existingLeadId = await apiService.GetLeadByEmail(tmpLead.Email); } - if (existingLead is null) + if (existingLeadId is null) { await CreateLeadAsync(bizFormItem, fieldMappings); } else if (!settings.CurrentValue.IgnoreExistingRecords) { - await UpdateLeadAsync(existingLead.Id!, bizFormItem, fieldMappings); + await UpdateLeadAsync(existingLeadId, bizFormItem, fieldMappings); } } @@ -113,7 +111,7 @@ private async Task CreateLeadAsync(BizFormItem bizFormItem, IEnumerable Date: Tue, 9 Jan 2024 18:04:09 +0100 Subject: [PATCH 06/23] settings in CMS, contact form mapping start --- examples/DancingGoat/appsettings.json | 2 +- .../Configuration/BizFormsMappingBuilder.cs | 13 +++- .../CommonIntegrationSettings.cs | 7 +- .../Constants/SettingKeys.cs | 14 ++++ .../Workers/FailedSyncItemsWorkerBase.cs | 7 +- .../BizFormMappingBuilderExtensions.cs | 27 ++++++++ .../DynamicsIntegrationGlobalEvents.cs | 6 +- .../DynamicsServiceCollectionExtensions.cs | 47 +++++++++++--- .../DynamicsLeadsIntegrationService.cs | 8 +-- .../SalesForceIntegrationGlobalEvents.cs | 6 +- .../SalesForceServiceCollectionsExtensions.cs | 65 +++++++++++++++---- .../Services/ISalesForceApiService.cs | 2 +- .../Services/SalesForceApiService.cs | 24 +++---- .../SalesForceLeadsIntegrationService.cs | 10 +-- 14 files changed, 181 insertions(+), 57 deletions(-) create mode 100644 src/Kentico.Xperience.CRM.Common/Constants/SettingKeys.cs create mode 100644 src/Kentico.Xperience.CRM.Dynamics/Configuration/BizFormMappingBuilderExtensions.cs diff --git a/examples/DancingGoat/appsettings.json b/examples/DancingGoat/appsettings.json index 496d4ac..eaffa9f 100644 --- a/examples/DancingGoat/appsettings.json +++ b/examples/DancingGoat/appsettings.json @@ -30,7 +30,7 @@ //"ApiConfig" add to secrets.json }, "CMSSalesForceCRMIntegration": { - "FormLeadsEnabled": true, + "FormLeadsEnabled": false, "IgnoreExistingRecords": false //"ApiConfig" add to secrets.json } diff --git a/src/Kentico.Xperience.CRM.Common/Configuration/BizFormsMappingBuilder.cs b/src/Kentico.Xperience.CRM.Common/Configuration/BizFormsMappingBuilder.cs index f4e7bff..d088fad 100644 --- a/src/Kentico.Xperience.CRM.Common/Configuration/BizFormsMappingBuilder.cs +++ b/src/Kentico.Xperience.CRM.Common/Configuration/BizFormsMappingBuilder.cs @@ -15,9 +15,18 @@ public BizFormsMappingBuilder AddForm(string formCodeName, forms.Add(formCodeName.ToLowerInvariant(), configureFields(new BizFormFieldsMappingBuilder())); return this; } - + + public BizFormsMappingBuilder AddForm(string formCodeName, + BizFormFieldsMappingBuilder configuredBuilder) + { + if (formCodeName is null) throw new ArgumentNullException(nameof(formCodeName)); + + forms.Add(formCodeName.ToLowerInvariant(), configuredBuilder); + return this; + } + internal TBizFormsConfiguration Build() - where TBizFormsConfiguration : BizFormsMappingConfiguration, new() + where TBizFormsConfiguration : BizFormsMappingConfiguration, new() { return new TBizFormsConfiguration { diff --git a/src/Kentico.Xperience.CRM.Common/Configuration/CommonIntegrationSettings.cs b/src/Kentico.Xperience.CRM.Common/Configuration/CommonIntegrationSettings.cs index 6e71255..73180da 100644 --- a/src/Kentico.Xperience.CRM.Common/Configuration/CommonIntegrationSettings.cs +++ b/src/Kentico.Xperience.CRM.Common/Configuration/CommonIntegrationSettings.cs @@ -4,13 +4,14 @@ /// Common setting for Kentico-CRM integration /// /// -public class CommonIntegrationSettings +public class CommonIntegrationSettings where TApiConfig : new() { public bool FormLeadsEnabled { get; set; } + // @TODO phase 2 public bool ContactsEnabled { get; set; } - + public bool IgnoreExistingRecords { get; set; } - public TApiConfig? ApiConfig { get; set; } + public TApiConfig ApiConfig { get; set; } = new(); } \ No newline at end of file diff --git a/src/Kentico.Xperience.CRM.Common/Constants/SettingKeys.cs b/src/Kentico.Xperience.CRM.Common/Constants/SettingKeys.cs new file mode 100644 index 0000000..a8bb3df --- /dev/null +++ b/src/Kentico.Xperience.CRM.Common/Constants/SettingKeys.cs @@ -0,0 +1,14 @@ +namespace Kentico.Xperience.CRM.Common.Constants; + +public class SettingKeys +{ + public const string DynamicsFormLeadsEnabled = "CMSDynamicsCRMIntegrationFormLeadsEnabled"; + public const string DynamicsUrl = "CMSDynamicsCRMIntegrationDynamicsUrl"; + public const string DynamicsClientId = "CMSDynamicsCRMIntegrationClientId"; + public const string DynamicsClientSecret = "CMSDynamicsCRMIntegrationDynamicsClientSecret"; + + public const string SalesForceFormLeadsEnabled = "CMSSalesforceCRMIntegrationFormLeadsEnabled"; + public const string SalesForceUrl = "CMSSalesforceCRMIntegrationSalesforceUrl"; + public const string SalesForceClientId = "CMSSalesforceCRMIntegrationClientId"; + public const string SalesForceClientSecret = "CMSSalesforceCRMIntegrationSalesforceClientSecret"; +} \ No newline at end of file diff --git a/src/Kentico.Xperience.CRM.Common/Workers/FailedSyncItemsWorkerBase.cs b/src/Kentico.Xperience.CRM.Common/Workers/FailedSyncItemsWorkerBase.cs index 8974f0e..1462a08 100644 --- a/src/Kentico.Xperience.CRM.Common/Workers/FailedSyncItemsWorkerBase.cs +++ b/src/Kentico.Xperience.CRM.Common/Workers/FailedSyncItemsWorkerBase.cs @@ -21,6 +21,7 @@ public abstract class FailedSyncItemsWorkerBase, new() where TService : ILeadsIntegrationService where TSettings : CommonIntegrationSettings + where TApiConfig : new() { protected override int DefaultInterval => 60000; private ILogger logger = null!; @@ -38,13 +39,13 @@ protected override void Process() try { - var settings = Service.Resolve>().CurrentValue; + using var serviceScope = Service.Resolve().CreateScope(); + + var settings = serviceScope.ServiceProvider.GetRequiredService>().Value; if (!settings.FormLeadsEnabled) return; var failedSyncItemsService = Service.Resolve(); - using var serviceScope = Service.Resolve().CreateScope(); - var leadsIntegrationService = serviceScope.ServiceProvider .GetRequiredService(); diff --git a/src/Kentico.Xperience.CRM.Dynamics/Configuration/BizFormMappingBuilderExtensions.cs b/src/Kentico.Xperience.CRM.Dynamics/Configuration/BizFormMappingBuilderExtensions.cs new file mode 100644 index 0000000..7021e49 --- /dev/null +++ b/src/Kentico.Xperience.CRM.Dynamics/Configuration/BizFormMappingBuilderExtensions.cs @@ -0,0 +1,27 @@ +using CMS.Core; +using CMS.OnlineForms.Internal; +using Kentico.Xperience.CRM.Common.Configuration; + +namespace Kentico.Xperience.CRM.Dynamics.Configuration; + +public static class BizFormMappingBuilderExtensions +{ + public static BizFormsMappingBuilder AddFormWithContactMapping(this BizFormsMappingBuilder formsMappingBuilder, + string formCodeName) => AddFormWithContactMapping(formsMappingBuilder, formCodeName, b => b); + + public static BizFormsMappingBuilder AddFormWithContactMapping(this BizFormsMappingBuilder formsMappingBuilder, + string formCodeName, + Func configureFields) + { + if (formCodeName is null) throw new ArgumentNullException(nameof(formCodeName)); + //@TODO use Form contact mapping + var mappingBuilder = new BizFormFieldsMappingBuilder(); + + formsMappingBuilder.AddForm(formCodeName, mappingBuilder); + return formsMappingBuilder; + } + + private static void ConfigureMappingFromContactMapping(string formClassName, BizFormFieldsMappingBuilder mappingBuilder) + { + } +} \ No newline at end of file diff --git a/src/Kentico.Xperience.CRM.Dynamics/DynamicsIntegrationGlobalEvents.cs b/src/Kentico.Xperience.CRM.Dynamics/DynamicsIntegrationGlobalEvents.cs index 68483bb..3266786 100644 --- a/src/Kentico.Xperience.CRM.Dynamics/DynamicsIntegrationGlobalEvents.cs +++ b/src/Kentico.Xperience.CRM.Dynamics/DynamicsIntegrationGlobalEvents.cs @@ -47,11 +47,11 @@ private void SynchronizeBizFormLead(object? sender, BizFormItemEventArgs e) { try { - var settings = Service.Resolve>().CurrentValue; - if (!settings.FormLeadsEnabled) return; - using (var serviceScope = Service.Resolve().CreateScope()) { + var settings = serviceScope.ServiceProvider.GetRequiredService>().Value; + if (!settings.FormLeadsEnabled) return; + var leadsIntegrationService = serviceScope.ServiceProvider .GetRequiredService(); diff --git a/src/Kentico.Xperience.CRM.Dynamics/DynamicsServiceCollectionExtensions.cs b/src/Kentico.Xperience.CRM.Dynamics/DynamicsServiceCollectionExtensions.cs index 1ef9f69..ac42304 100644 --- a/src/Kentico.Xperience.CRM.Dynamics/DynamicsServiceCollectionExtensions.cs +++ b/src/Kentico.Xperience.CRM.Dynamics/DynamicsServiceCollectionExtensions.cs @@ -1,5 +1,8 @@ -using Kentico.Xperience.CRM.Common; +using CMS.Core; +using CMS.Helpers; +using Kentico.Xperience.CRM.Common; using Kentico.Xperience.CRM.Common.Configuration; +using Kentico.Xperience.CRM.Common.Constants; using Kentico.Xperience.CRM.Dynamics.Configuration; using Kentico.Xperience.CRM.Dynamics.Services; using Microsoft.Extensions.Configuration; @@ -26,8 +29,9 @@ public static IServiceCollection AddDynamicsFormLeadsIntegration(this IServiceCo { serviceCollection.AddKenticoCrmCommonFormLeadsIntegration(formsConfig); - serviceCollection.AddOptions().Bind(configuration); - serviceCollection.TryAddSingleton(GetCrmServiceClient); + serviceCollection.AddOptions().Bind(configuration) + .PostConfigure(ConfigureWithCMSSettings); + serviceCollection.TryAddScoped(GetCrmServiceClient); serviceCollection.AddScoped(); return serviceCollection; } @@ -40,18 +44,45 @@ public static IServiceCollection AddDynamicsFormLeadsIntegration(this IServiceCo /// private static ServiceClient GetCrmServiceClient(IServiceProvider serviceProvider) { - var settings = serviceProvider.GetRequiredService>().CurrentValue; + var settings = serviceProvider.GetRequiredService>().Value; var logger = serviceProvider.GetRequiredService>(); - if (settings.ApiConfig?.IsValid() is not true) + if (!settings.ApiConfig.IsValid()) { throw new InvalidOperationException("Missing API setting"); } - var connectionString = string.IsNullOrWhiteSpace(settings.ApiConfig.ConnectionString) ? - $"AuthType=ClientSecret;Url={settings.ApiConfig.DynamicsUrl};ClientId={settings.ApiConfig.ClientId};ClientSecret={settings.ApiConfig.ClientSecret}" : - settings.ApiConfig.ConnectionString; + var connectionString = string.IsNullOrWhiteSpace(settings.ApiConfig.ConnectionString) + ? $"AuthType=ClientSecret;Url={settings.ApiConfig.DynamicsUrl};ClientId={settings.ApiConfig.ClientId};ClientSecret={settings.ApiConfig.ClientSecret}" + : settings.ApiConfig.ConnectionString; return new ServiceClient(connectionString, logger); } + + private static void ConfigureWithCMSSettings(DynamicsIntegrationSettings settings, ISettingsService settingsService) + { + var formsEnabled = settingsService[SettingKeys.DynamicsFormLeadsEnabled]; + if (!string.IsNullOrWhiteSpace(formsEnabled)) + { + settings.FormLeadsEnabled = ValidationHelper.GetBoolean(formsEnabled, false); + } + + var dynamicsUrl = settingsService[SettingKeys.DynamicsUrl]; + if (!string.IsNullOrWhiteSpace(dynamicsUrl)) + { + settings.ApiConfig.DynamicsUrl = dynamicsUrl; + } + + var clientId = settingsService[SettingKeys.DynamicsClientId]; + if (!string.IsNullOrWhiteSpace(clientId)) + { + settings.ApiConfig.ClientId = clientId; + } + + var clientSecret = settingsService[SettingKeys.DynamicsClientSecret]; + if (!string.IsNullOrWhiteSpace(clientSecret)) + { + settings.ApiConfig.ClientSecret = clientSecret; + } + } } \ No newline at end of file diff --git a/src/Kentico.Xperience.CRM.Dynamics/Services/DynamicsLeadsIntegrationService.cs b/src/Kentico.Xperience.CRM.Dynamics/Services/DynamicsLeadsIntegrationService.cs index 3a35602..46088b4 100644 --- a/src/Kentico.Xperience.CRM.Dynamics/Services/DynamicsLeadsIntegrationService.cs +++ b/src/Kentico.Xperience.CRM.Dynamics/Services/DynamicsLeadsIntegrationService.cs @@ -25,7 +25,7 @@ internal class DynamicsLeadsIntegrationService : LeadsIntegrationServiceCommon, private readonly ILogger logger; private readonly ICRMSyncItemService syncItemService; private readonly IFailedSyncItemService failedSyncItemService; - private readonly IOptionsMonitor settings; + private readonly IOptionsSnapshot settings; public DynamicsLeadsIntegrationService( DynamicsBizFormsMappingConfiguration bizFormMappingConfig, ILeadsIntegrationValidationService validationService, @@ -33,7 +33,7 @@ public DynamicsLeadsIntegrationService( ILogger logger, ICRMSyncItemService syncItemService, IFailedSyncItemService failedSyncItemService, - IOptionsMonitor settings) + IOptionsSnapshot settings) : base(bizFormMappingConfig, validationService, logger) { this.bizFormMappingConfig = bizFormMappingConfig; @@ -62,7 +62,7 @@ protected override async Task SynchronizeLeadAsync(BizFormItem bizFormItem, { await UpdateByEmailOrCreate(bizFormItem, fieldMappings); } - else if (!settings.CurrentValue.IgnoreExistingRecords) + else if (!settings.Value.IgnoreExistingRecords) { await UpdateLeadAsync(existingLead, bizFormItem, fieldMappings); } @@ -101,7 +101,7 @@ private async Task UpdateByEmailOrCreate(BizFormItem bizFormItem, { await CreateLeadAsync(bizFormItem, fieldMappings); } - else if (!settings.CurrentValue.IgnoreExistingRecords) + else if (!settings.Value.IgnoreExistingRecords) { await UpdateLeadAsync(existingLead, bizFormItem, fieldMappings); } diff --git a/src/Kentico.Xperience.CRM.SalesForce/SalesForceIntegrationGlobalEvents.cs b/src/Kentico.Xperience.CRM.SalesForce/SalesForceIntegrationGlobalEvents.cs index 459fc6f..a7bc089 100644 --- a/src/Kentico.Xperience.CRM.SalesForce/SalesForceIntegrationGlobalEvents.cs +++ b/src/Kentico.Xperience.CRM.SalesForce/SalesForceIntegrationGlobalEvents.cs @@ -50,11 +50,11 @@ private void SynchronizeBizFormLead(object? sender, BizFormItemEventArgs e) var failedSyncItemsService = Service.Resolve(); try { - var settings = Service.Resolve>().CurrentValue; - if (!settings.FormLeadsEnabled) return; - using (var serviceScope = Service.Resolve().CreateScope()) { + var settings = serviceScope.ServiceProvider.GetRequiredService>().Value; + if (!settings.FormLeadsEnabled) return; + var leadsIntegrationService = serviceScope.ServiceProvider .GetRequiredService(); diff --git a/src/Kentico.Xperience.CRM.SalesForce/SalesForceServiceCollectionsExtensions.cs b/src/Kentico.Xperience.CRM.SalesForce/SalesForceServiceCollectionsExtensions.cs index 03958fc..0ce72a3 100644 --- a/src/Kentico.Xperience.CRM.SalesForce/SalesForceServiceCollectionsExtensions.cs +++ b/src/Kentico.Xperience.CRM.SalesForce/SalesForceServiceCollectionsExtensions.cs @@ -1,9 +1,14 @@ -using Kentico.Xperience.CRM.Common; +using CMS.Core; +using CMS.Helpers; +using Duende.AccessTokenManagement; +using Kentico.Xperience.CRM.Common; using Kentico.Xperience.CRM.Common.Configuration; +using Kentico.Xperience.CRM.Common.Constants; using Kentico.Xperience.CRM.SalesForce.Configuration; using Kentico.Xperience.CRM.SalesForce.Services; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Options; using System.Globalization; namespace Kentico.Xperience.CRM.SalesForce; @@ -24,13 +29,15 @@ public static IServiceCollection AddSalesForceFormLeadsIntegration(this IService { serviceCollection.AddKenticoCrmCommonFormLeadsIntegration(formsConfig); - serviceCollection.AddOptions().Bind(configuration); + serviceCollection.AddOptions().Bind(configuration) + .PostConfigure(ConfigureWithCMSSettings); + AddSalesForceCommonIntegration(serviceCollection, configuration); serviceCollection.AddScoped(); return serviceCollection; } - + private static void AddSalesForceCommonIntegration(IServiceCollection serviceCollection, IConfiguration configuration) { @@ -38,12 +45,16 @@ private static void AddSalesForceCommonIntegration(IServiceCollection serviceCol serviceCollection.AddDistributedMemoryCache(); //add token management config - serviceCollection.AddClientCredentialsTokenManagement() - .AddClient("salesforce.api.client", client => + serviceCollection.AddClientCredentialsTokenManagement(); + + serviceCollection.AddOptions("salesforce.api.client") + .Configure((client, sp) => { - var apiConfig = configuration.Get()?.ApiConfig; + //cannot use IOptionsSnapshot, so changes in CMS settings needs restarting app to apply immediately + var apiConfig = sp.GetRequiredService>().CurrentValue + .ApiConfig; - if (apiConfig?.IsValid() is not true) + if (!apiConfig.IsValid()) throw new InvalidOperationException("Missing API settings"); client.TokenEndpoint = apiConfig.SalesForceUrl?.TrimEnd('/') + "/services/oauth2/token"; @@ -53,16 +64,46 @@ private static void AddSalesForceCommonIntegration(IServiceCollection serviceCol }); //add http client for salesforce api - serviceCollection.AddHttpClient(client => + serviceCollection.AddHttpClient((provider, client) => { - var apiConfig = configuration.Get()?.ApiConfig; + //cannot use IOptionsSnapshot, so changes in CMS settings needs restarting app to apply immediately + var settings = provider.GetRequiredService>().CurrentValue; - if (apiConfig?.IsValid() is not true) + if (!settings.ApiConfig.IsValid()) throw new InvalidOperationException("Missing API settings"); - string apiVersion = apiConfig.ApiVersion.ToString("F1", CultureInfo.InvariantCulture); - client.BaseAddress = new Uri($"{apiConfig.SalesForceUrl?.TrimEnd('/')}/services/data/v{apiVersion}/"); + string apiVersion = settings.ApiConfig.ApiVersion.ToString("F1", CultureInfo.InvariantCulture); + client.BaseAddress = + new Uri($"{settings.ApiConfig.SalesForceUrl?.TrimEnd('/')}/services/data/v{apiVersion}/"); }) .AddClientCredentialsTokenHandler("salesforce.api.client"); } + + private static void ConfigureWithCMSSettings(SalesForceIntegrationSettings settings, + ISettingsService settingsService) + { + var formsEnabled = settingsService[SettingKeys.SalesForceFormLeadsEnabled]; + if (!string.IsNullOrWhiteSpace(formsEnabled)) + { + settings.FormLeadsEnabled = ValidationHelper.GetBoolean(formsEnabled, false); + } + + var salesForceUrl = settingsService[SettingKeys.SalesForceUrl]; + if (!string.IsNullOrWhiteSpace(salesForceUrl)) + { + settings.ApiConfig.SalesForceUrl = salesForceUrl; + } + + var clientId = settingsService[SettingKeys.SalesForceClientId]; + if (!string.IsNullOrWhiteSpace(clientId)) + { + settings.ApiConfig.ClientId = clientId; + } + + var clientSecret = settingsService[SettingKeys.SalesForceClientSecret]; + if (!string.IsNullOrWhiteSpace(clientSecret)) + { + settings.ApiConfig.ClientSecret = clientSecret; + } + } } \ No newline at end of file diff --git a/src/Kentico.Xperience.CRM.SalesForce/Services/ISalesForceApiService.cs b/src/Kentico.Xperience.CRM.SalesForce/Services/ISalesForceApiService.cs index 444a197..718a0b1 100644 --- a/src/Kentico.Xperience.CRM.SalesForce/Services/ISalesForceApiService.cs +++ b/src/Kentico.Xperience.CRM.SalesForce/Services/ISalesForceApiService.cs @@ -35,7 +35,7 @@ public interface ISalesForceApiService /// /// /// - Task GetLeadById(string id); + Task GetLeadById(string id, string? fields = null); /// /// diff --git a/src/Kentico.Xperience.CRM.SalesForce/Services/SalesForceApiService.cs b/src/Kentico.Xperience.CRM.SalesForce/Services/SalesForceApiService.cs index b92249b..2f5a264 100644 --- a/src/Kentico.Xperience.CRM.SalesForce/Services/SalesForceApiService.cs +++ b/src/Kentico.Xperience.CRM.SalesForce/Services/SalesForceApiService.cs @@ -16,18 +16,18 @@ internal class SalesForceApiService : ISalesForceApiService { private readonly HttpClient httpClient; private readonly ILogger logger; - private readonly SalesForceIntegrationSettings integrationSettings; + private readonly IOptionsSnapshot integrationSettings; private readonly SalesForceApiClient apiClient; public SalesForceApiService( HttpClient httpClient, ILogger logger, - IOptionsMonitor integrationSettings - ) + IOptionsSnapshot integrationSettings + ) { this.httpClient = httpClient; this.logger = logger; - this.integrationSettings = integrationSettings.CurrentValue; + this.integrationSettings = integrationSettings; apiClient = new SalesForceApiClient(httpClient); } @@ -51,10 +51,10 @@ public async Task UpdateLeadAsync(string id, LeadSObject leadSObject) /// public async Task GetLeadIdByExternalId(string fieldName, string externalId) { - var apiVersion = (integrationSettings?.ApiConfig?.ApiVersion ?? SalesForceApiConfig.DefaultVersion) - .ToString("F1", CultureInfo.InvariantCulture); + var apiVersion = integrationSettings.Value.ApiConfig.ApiVersion.ToString("F1", CultureInfo.InvariantCulture); using var request = - new HttpRequestMessage(HttpMethod.Get, $"/services/data/v{apiVersion}/sobjects/lead/{fieldName}/{externalId}?fields=Id"); + new HttpRequestMessage(HttpMethod.Get, + $"/services/data/v{apiVersion}/sobjects/lead/{fieldName}/{externalId}?fields=Id"); request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(MediaTypeNames.Application.Json)); var response = await httpClient.SendAsync(request); @@ -76,15 +76,15 @@ public async Task UpdateLeadAsync(string id, LeadSObject leadSObject) } } - public async Task GetLeadById(string id) - => await apiClient.LeadGET2Async(id, nameof(LeadSObject.Id)); + public async Task GetLeadById(string id, string? fields = null) + => await apiClient.LeadGET2Async(id, fields); public async Task GetLeadByEmail(string email) { - var apiVersion = (integrationSettings?.ApiConfig?.ApiVersion ?? SalesForceApiConfig.DefaultVersion) - .ToString("F1", CultureInfo.InvariantCulture); + var apiVersion = integrationSettings.Value.ApiConfig.ApiVersion.ToString("F1", CultureInfo.InvariantCulture); using var request = - new HttpRequestMessage(HttpMethod.Get, $"/services/data/v{apiVersion}/query?q=SELECT+Id+FROM+Lead+WHERE+Email='{email}'+ORDER+BY+CreatedDate+DESC"); + new HttpRequestMessage(HttpMethod.Get, + $"/services/data/v{apiVersion}/query?q=SELECT+Id+FROM+Lead+WHERE+Email='{email}'+ORDER+BY+CreatedDate+DESC"); request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(MediaTypeNames.Application.Json)); var response = await httpClient.SendAsync(request); diff --git a/src/Kentico.Xperience.CRM.SalesForce/Services/SalesForceLeadsIntegrationService.cs b/src/Kentico.Xperience.CRM.SalesForce/Services/SalesForceLeadsIntegrationService.cs index b3637d9..cef518e 100644 --- a/src/Kentico.Xperience.CRM.SalesForce/Services/SalesForceLeadsIntegrationService.cs +++ b/src/Kentico.Xperience.CRM.SalesForce/Services/SalesForceLeadsIntegrationService.cs @@ -18,7 +18,7 @@ internal class SalesForceLeadsIntegrationService : LeadsIntegrationServiceCommon private readonly ILogger logger; private readonly ICRMSyncItemService syncItemService; private readonly IFailedSyncItemService failedSyncItemService; - private readonly IOptionsMonitor settings; + private readonly IOptionsSnapshot settings; public SalesForceLeadsIntegrationService( SalesForceBizFormsMappingConfiguration bizFormMappingConfig, @@ -27,7 +27,7 @@ public SalesForceLeadsIntegrationService( ILogger logger, ICRMSyncItemService syncItemService, IFailedSyncItemService failedSyncItemService, - IOptionsMonitor settings) + IOptionsSnapshot settings) : base(bizFormMappingConfig, validationService, logger) { this.bizFormMappingConfig = bizFormMappingConfig; @@ -51,12 +51,12 @@ protected override async Task SynchronizeLeadAsync(BizFormItem bizFormItem, } else { - var existingLead = await apiService.GetLeadById(syncItem.CRMSyncItemCRMID); + var existingLead = await apiService.GetLeadById(syncItem.CRMSyncItemCRMID, nameof(LeadSObject.Id)); if (existingLead is null) { await UpdateByEmailOrCreate(bizFormItem, fieldMappings); } - else if (!settings.CurrentValue.IgnoreExistingRecords) + else if (!settings.Value.IgnoreExistingRecords) { await UpdateLeadAsync(existingLead.Id!, bizFormItem, fieldMappings); } @@ -95,7 +95,7 @@ private async Task UpdateByEmailOrCreate(BizFormItem bizFormItem, IEnumerable Date: Wed, 10 Jan 2024 17:50:25 +0100 Subject: [PATCH 07/23] converters, auto mapping, UI listing for synced items --- examples/DancingGoat/Program.cs | 28 ++-- examples/DancingGoat/appsettings.json | 2 +- examples/DancingGoat/packages.lock.json | 1 + .../Admin/CRMSyncItemListing.cs | 47 ++++++ .../BizFormFieldsMappingBuilder.cs | 4 +- .../Configuration/BizFormsMappingBuilder.cs | 22 ++- .../BizFormsMappingConfiguration.cs | 2 +- .../Kentico.Xperience.CRM.Common.csproj | 1 + .../Mapping/ICRMTypeConverter.cs | 6 + .../ServiceCollectionExtensions.cs | 27 +--- .../packages.lock.json | 138 +++++++++++++++-- .../BizFormFieldsMappingBuilderExtensions.cs | 2 + .../BizFormMappingBuilderExtensions.cs | 27 ---- .../DynamicsBizFormsMappingBuilder.cs | 98 ++++++++++++ .../DynamicsBizFormsMappingConfiguration.cs | 6 +- .../FormContactMappingToLeadConverter.cs | 84 +++++++++++ .../DynamicsServiceCollectionExtensions.cs | 7 +- .../DynamicsLeadsIntegrationService.cs | 84 ++++++++--- .../packages.lock.json | 140 ++++++++++++++---- .../SalesForceBizFormsMappingBuilder.cs | 21 +++ .../SalesForceServiceCollectionsExtensions.cs | 12 +- .../packages.lock.json | 116 +++++++++++++-- 22 files changed, 717 insertions(+), 158 deletions(-) create mode 100644 src/Kentico.Xperience.CRM.Common/Admin/CRMSyncItemListing.cs create mode 100644 src/Kentico.Xperience.CRM.Common/Mapping/ICRMTypeConverter.cs delete mode 100644 src/Kentico.Xperience.CRM.Dynamics/Configuration/BizFormMappingBuilderExtensions.cs create mode 100644 src/Kentico.Xperience.CRM.Dynamics/Configuration/DynamicsBizFormsMappingBuilder.cs create mode 100644 src/Kentico.Xperience.CRM.Dynamics/Converters/FormContactMappingToLeadConverter.cs create mode 100644 src/Kentico.Xperience.CRM.SalesForce/Configuration/SalesForceBizFormsMappingBuilder.cs diff --git a/examples/DancingGoat/Program.cs b/examples/DancingGoat/Program.cs index 7955669..d74dcde 100644 --- a/examples/DancingGoat/Program.cs +++ b/examples/DancingGoat/Program.cs @@ -51,17 +51,25 @@ ConfigureMembershipServices(builder.Services); //CRM integration registration start + +// builder.Services.AddDynamicsFormLeadsIntegration(builder => +// builder.AddForm(DancingGoatContactUsItem.CLASS_NAME, //form class name +// c => c +// .MapField("UserFirstName", "firstname") +// .MapField("UserLastName", e => e.LastName) //you can map to Lead object or use own generated Lead class +// .MapField(c => c.UserEmail, e => e.EMailAddress1) //generated form class used +// .MapField(b => b.GetStringValue("UserMessage", ""), e => e.Description) //general BizFormItem used +// ) +// .AddCustomFormLeadsValidationService() //optional +// , +// builder.Configuration.GetSection(DynamicsIntegrationSettings.ConfigKeyName)); //config section with settings + builder.Services.AddDynamicsFormLeadsIntegration(builder => - builder.AddForm(DancingGoatContactUsItem.CLASS_NAME, //form class name - c => c - .MapField("UserFirstName", "firstname") - .MapField("UserLastName", e => e.LastName) //you can map to Lead object or use own generated Lead class - .MapField(c => c.UserEmail, e => e.EMailAddress1) //generated form class used - .MapField(b => b.GetStringValue("UserMessage", ""), e => e.Description) //general BizFormItem used - ) - , - builder.Configuration.GetSection(DynamicsIntegrationSettings.ConfigKeyName)) //config section with settings - .AddCustomFormLeadsValidationService(); //optional + builder.AddFormWithContactMapping(DancingGoatContactUsItem.CLASS_NAME,b => b + .MapField(c => c.UserMessage, e => e.EMailAddress1)) + .AddCustomFormLeadsValidationService() //optional + , + builder.Configuration.GetSection(DynamicsIntegrationSettings.ConfigKeyName)); //config section with settings builder.Services.AddSalesForceFormLeadsIntegration(builder => builder.AddForm(DancingGoatContactUsItem.CLASS_NAME, //form class name diff --git a/examples/DancingGoat/appsettings.json b/examples/DancingGoat/appsettings.json index eaffa9f..2d3a4d8 100644 --- a/examples/DancingGoat/appsettings.json +++ b/examples/DancingGoat/appsettings.json @@ -25,7 +25,7 @@ }, "CMSHashStringSalt": "", "CMSDynamicsCRMIntegration": { - "FormLeadsEnabled": false, + "FormLeadsEnabled": true, "IgnoreExistingRecords": false //"ApiConfig" add to secrets.json }, diff --git a/examples/DancingGoat/packages.lock.json b/examples/DancingGoat/packages.lock.json index 7942cf2..55250dc 100644 --- a/examples/DancingGoat/packages.lock.json +++ b/examples/DancingGoat/packages.lock.json @@ -1379,6 +1379,7 @@ "kentico.xperience.crm.common": { "type": "Project", "dependencies": { + "Kentico.Xperience.Admin": "[27.0.1, )", "Kentico.Xperience.Core": "[27.0.1, )" } }, diff --git a/src/Kentico.Xperience.CRM.Common/Admin/CRMSyncItemListing.cs b/src/Kentico.Xperience.CRM.Common/Admin/CRMSyncItemListing.cs new file mode 100644 index 0000000..661b2ac --- /dev/null +++ b/src/Kentico.Xperience.CRM.Common/Admin/CRMSyncItemListing.cs @@ -0,0 +1,47 @@ +using CMS.DataEngine; +using CMS.FormEngine; +using CMS.OnlineForms; +using Kentico.Xperience.Admin.Base; +using Kentico.Xperience.Admin.DigitalMarketing.UIPages; +using Kentico.Xperience.CRM.Common.Admin; +using Kentico.Xperience.CRM.Common.Classes; +using Kentico.Xperience.CRM.Common.Constants; + +[assembly: + UIPage(typeof(FormEditSection), "crm-sync-listing", typeof(CRMSyncItemListing), + "CRM synchronization", TemplateNames.LISTING, 1000, "xp-graph")] + +namespace Kentico.Xperience.CRM.Common.Admin; + +public class CRMSyncItemListing : ListingPage +{ + private BizFormInfo? editedForm; + private DataClassInfo? dataClassInfo; + protected override string ObjectType => CRMSyncItemInfo.OBJECT_TYPE; + + /// ID of the edited form. + [PageParameter(typeof(IntPageModelBinder), typeof(FormEditSection))] + public int FormId { get; set; } + + private BizFormInfo EditedForm => + this.editedForm ??= AbstractInfo.Provider.Get(this.FormId); + + private DataClassInfo DataClassInfo => this.dataClassInfo ??= + DataClassInfoProviderBase.GetDataClassInfo(this.EditedForm.FormClassID); + + public override Task ConfigurePage() + { + PageConfiguration.ColumnConfigurations + .AddColumn(nameof(CRMSyncItemInfo.CRMSyncItemEntityClass), "Form") + .AddColumn(nameof(CRMSyncItemInfo.CRMSyncItemEntityID), "Form item ID") + .AddColumn(nameof(CRMSyncItemInfo.CRMSyncItemEntityCRM), "CRM") + .AddColumn(nameof(CRMSyncItemInfo.CRMSyncItemCRMID), "CRM ID") + .AddColumn(nameof(CRMSyncItemInfo.CRMSyncItemLastModified), "Last sync time"); + + PageConfiguration.QueryModifiers.AddModifier(q => + q.WhereEquals(nameof(CRMSyncItemInfo.CRMSyncItemEntityClass), DataClassInfo.ClassName) + .OrderByDescending(nameof(CRMSyncItemInfo.CRMSyncItemLastModified))); + + return Task.CompletedTask; + } +} \ No newline at end of file diff --git a/src/Kentico.Xperience.CRM.Common/Configuration/BizFormFieldsMappingBuilder.cs b/src/Kentico.Xperience.CRM.Common/Configuration/BizFormFieldsMappingBuilder.cs index b581b5f..c765942 100644 --- a/src/Kentico.Xperience.CRM.Common/Configuration/BizFormFieldsMappingBuilder.cs +++ b/src/Kentico.Xperience.CRM.Common/Configuration/BizFormFieldsMappingBuilder.cs @@ -28,6 +28,6 @@ public BizFormFieldsMappingBuilder AddMapping(BizFormFieldMapping mapping) fieldMappings.Add(mapping); return this; } - - internal List Build() => fieldMappings; + + public List Build() => fieldMappings; } \ No newline at end of file diff --git a/src/Kentico.Xperience.CRM.Common/Configuration/BizFormsMappingBuilder.cs b/src/Kentico.Xperience.CRM.Common/Configuration/BizFormsMappingBuilder.cs index d088fad..269abca 100644 --- a/src/Kentico.Xperience.CRM.Common/Configuration/BizFormsMappingBuilder.cs +++ b/src/Kentico.Xperience.CRM.Common/Configuration/BizFormsMappingBuilder.cs @@ -1,11 +1,19 @@ -namespace Kentico.Xperience.CRM.Common.Configuration; +using Microsoft.Extensions.DependencyInjection; + +namespace Kentico.Xperience.CRM.Common.Configuration; /// /// Common builder for BizForms to CRM leads configuration mapping /// public class BizFormsMappingBuilder { - private readonly Dictionary forms = new(); + private readonly IServiceCollection serviceCollection; + protected readonly Dictionary forms = new(); + + public BizFormsMappingBuilder(IServiceCollection serviceCollection) + { + this.serviceCollection = serviceCollection; + } public BizFormsMappingBuilder AddForm(string formCodeName, Func configureFields) @@ -24,14 +32,4 @@ public BizFormsMappingBuilder AddForm(string formCodeName, forms.Add(formCodeName.ToLowerInvariant(), configuredBuilder); return this; } - - internal TBizFormsConfiguration Build() - where TBizFormsConfiguration : BizFormsMappingConfiguration, new() - { - return new TBizFormsConfiguration - { - FormsMappings = forms.Select(f => (f.Key, f.Value.Build())) - .ToDictionary(r => r.Key, r => r.Item2), - }; - } } \ No newline at end of file diff --git a/src/Kentico.Xperience.CRM.Common/Configuration/BizFormsMappingConfiguration.cs b/src/Kentico.Xperience.CRM.Common/Configuration/BizFormsMappingConfiguration.cs index df4c37e..b7a4bff 100644 --- a/src/Kentico.Xperience.CRM.Common/Configuration/BizFormsMappingConfiguration.cs +++ b/src/Kentico.Xperience.CRM.Common/Configuration/BizFormsMappingConfiguration.cs @@ -7,5 +7,5 @@ namespace Kentico.Xperience.CRM.Common.Configuration; /// public class BizFormsMappingConfiguration { - public Dictionary> FormsMappings { get; internal init; } = new(); + public Dictionary> FormsMappings { get; init; } = new(); } \ No newline at end of file diff --git a/src/Kentico.Xperience.CRM.Common/Kentico.Xperience.CRM.Common.csproj b/src/Kentico.Xperience.CRM.Common/Kentico.Xperience.CRM.Common.csproj index 09a0046..f3fc3d0 100644 --- a/src/Kentico.Xperience.CRM.Common/Kentico.Xperience.CRM.Common.csproj +++ b/src/Kentico.Xperience.CRM.Common/Kentico.Xperience.CRM.Common.csproj @@ -6,6 +6,7 @@ + all diff --git a/src/Kentico.Xperience.CRM.Common/Mapping/ICRMTypeConverter.cs b/src/Kentico.Xperience.CRM.Common/Mapping/ICRMTypeConverter.cs new file mode 100644 index 0000000..b8e6dee --- /dev/null +++ b/src/Kentico.Xperience.CRM.Common/Mapping/ICRMTypeConverter.cs @@ -0,0 +1,6 @@ +namespace Kentico.Xperience.CRM.Common.Mapping; + +public interface ICRMTypeConverter +{ + Task Convert(TSource source, TDestination destination); +} \ No newline at end of file diff --git a/src/Kentico.Xperience.CRM.Common/ServiceCollectionExtensions.cs b/src/Kentico.Xperience.CRM.Common/ServiceCollectionExtensions.cs index 0797003..91dbfd3 100644 --- a/src/Kentico.Xperience.CRM.Common/ServiceCollectionExtensions.cs +++ b/src/Kentico.Xperience.CRM.Common/ServiceCollectionExtensions.cs @@ -20,38 +20,15 @@ public static class ServiceCollectionExtensions /// /// /// - public static IServiceCollection AddKenticoCrmCommonFormLeadsIntegration( - this IServiceCollection services, Action formsMappingConfig) - where TMappingConfiguration : BizFormsMappingConfiguration, new() + public static IServiceCollection AddKenticoCrmCommonFormLeadsIntegration( + this IServiceCollection services) { services.TryAddSingleton(); - services.TryAddSingleton( - _ => - { - var mappingBuilder = new BizFormsMappingBuilder(); - formsMappingConfig(mappingBuilder); - return mappingBuilder.Build(); - }); - services.TryAddSingleton(); services.TryAddSingleton(); services.TryAddSingleton(); return services; } - - /// - /// Adds custom service for BizForm item validation before sending to CRM - /// - /// - /// - /// - public static IServiceCollection AddCustomFormLeadsValidationService(this IServiceCollection services) - where TService : class, ILeadsIntegrationValidationService - { - services.AddSingleton(); - - return services; - } } \ No newline at end of file diff --git a/src/Kentico.Xperience.CRM.Common/packages.lock.json b/src/Kentico.Xperience.CRM.Common/packages.lock.json index 574d450..4a57b99 100644 --- a/src/Kentico.Xperience.CRM.Common/packages.lock.json +++ b/src/Kentico.Xperience.CRM.Common/packages.lock.json @@ -2,6 +2,18 @@ "version": 2, "dependencies": { "net6.0": { + "Kentico.Xperience.Admin": { + "type": "Direct", + "requested": "[27.0.1, )", + "resolved": "27.0.1", + "contentHash": "Kgk6v0S5OkIFUc/8cj6Nx/pP09Czz21Qwce/iqf4rWC9mnU7fcQ6sXlNmk1VL9jaNKb6XXrOZSWxjWk8AUVTJA==", + "dependencies": { + "Kentico.Aira.Client": "1.0.22", + "Kentico.Xperience.WebApp": "[27.0.1]", + "Microsoft.AspNetCore.SpaServices.Extensions": "6.0.22", + "Microsoft.Extensions.FileProviders.Embedded": "6.0.22" + } + }, "Kentico.Xperience.Core": { "type": "Direct", "requested": "[27.0.1, )", @@ -50,6 +62,14 @@ "System.Text.Encoding.CodePages": "6.0.0" } }, + "AngleSharp.Css": { + "type": "Transitive", + "resolved": "0.17.0", + "contentHash": "bg0AcugmX6BFEi/DHG61QrwRU8iuiX4H8LZehdIzYdqOM/dgb3BsCTzNIcc1XADn4+xfQEdVwJYTSwUxroL4vg==", + "dependencies": { + "AngleSharp": "[0.17.0, 0.18.0)" + } + }, "Azure.Core": { "type": "Transitive", "resolved": "1.25.0", @@ -83,6 +103,33 @@ "resolved": "2.2.1", "contentHash": "A6Zr52zVqJKt18ZBsTnX0qhG0kwIQftVAjLmszmkiR/trSp8H+xj1gUOzk7XHwaKgyREMSV1v9XaKrBUeIOdvQ==" }, + "CommandLineParser": { + "type": "Transitive", + "resolved": "2.9.1", + "contentHash": "OE0sl1/sQ37bjVsPKKtwQlWDgqaxWgtme3xZz7JssWUzg5JpMIyHgCTY9MVMxOg48fJ1AgGT3tgdH5m/kQ5xhA==" + }, + "HtmlSanitizer": { + "type": "Transitive", + "resolved": "8.0.723", + "contentHash": "C4RZX+Mv9OqY+sAM3SD3BdLxvtr9QimIGvLvN5SDjbi7rb6ibeHhGnQA5EyKbkiuQKHO6MBa3h2AZQzjy6z9HA==", + "dependencies": { + "AngleSharp": "[0.17.1]", + "AngleSharp.Css": "[0.17.0]", + "System.Collections.Immutable": "7.0.0" + } + }, + "Kentico.Aira.Client": { + "type": "Transitive", + "resolved": "1.0.22", + "contentHash": "/slLHi7JWaKCBl0EZa01rNqjoaGfOgRj3vNA2wfE88chM43YOPygl22OVEMnyvVLd5nNzzhgv6iX0QyzQlGVxQ==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection": "6.0.1", + "Microsoft.Extensions.Http": "6.0.0", + "Microsoft.Extensions.Options": "6.0.0", + "Microsoft.Extensions.Options.ConfigurationExtensions": "6.0.0", + "System.IdentityModel.Tokens.Jwt": "6.31.0" + } + }, "MailKit": { "type": "Transitive", "resolved": "4.2.0", @@ -91,6 +138,14 @@ "MimeKit": "4.2.0" } }, + "Microsoft.AspNetCore.SpaServices.Extensions": { + "type": "Transitive", + "resolved": "6.0.22", + "contentHash": "RE17e7KHUhyNWwWL93uJzErUZpRjWU71oT4rUbafjA44gEz5YDAfizxAkM0XYCbLBRCbXtoK4q8DJkJvm/LjSA==", + "dependencies": { + "Microsoft.Extensions.FileProviders.Physical": "6.0.0" + } + }, "Microsoft.Bcl.AsyncInterfaces": { "type": "Transitive", "resolved": "1.1.1", @@ -198,6 +253,14 @@ "Microsoft.Extensions.Primitives": "6.0.0" } }, + "Microsoft.Extensions.FileProviders.Embedded": { + "type": "Transitive", + "resolved": "6.0.22", + "contentHash": "kT/7vO2tq68iWQKVeIpH4VI6BDMeGQRaPUoiS+ZIY8QFBNgJNLpKoFC7JExRLMc2j1pFWHPDdToQKpMYSbVXiw==", + "dependencies": { + "Microsoft.Extensions.FileProviders.Abstractions": "6.0.0" + } + }, "Microsoft.Extensions.FileProviders.Physical": { "type": "Transitive", "resolved": "6.0.0", @@ -223,6 +286,17 @@ "Microsoft.Extensions.FileProviders.Abstractions": "6.0.0" } }, + "Microsoft.Extensions.Http": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "15+pa2G0bAMHbHewaQIdr/y6ag2H3yh4rd9hTXavtWDzQBkvpe2RMqFg8BxDpcQWssmjmBApGPcw93QRz6YcMg==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "6.0.0", + "Microsoft.Extensions.Logging": "6.0.0", + "Microsoft.Extensions.Logging.Abstractions": "6.0.0", + "Microsoft.Extensions.Options": "6.0.0" + } + }, "Microsoft.Extensions.Localization": { "type": "Transitive", "resolved": "6.0.22", @@ -239,6 +313,18 @@ "resolved": "6.0.22", "contentHash": "NwkDOL4dXE9JeHMqaSiMjj/19vaNztcO6X6XPCSKQBn4pCCeHXfA+YNokplnjhMv4QrB2cJSjGMoT8XC+USR/g==" }, + "Microsoft.Extensions.Logging": { + "type": "Transitive", + "resolved": "6.0.0", + "contentHash": "eIbyj40QDg1NDz0HBW0S5f3wrLVnKWnDJ/JtZ+yJDFnDj90VoPuoPmFkeaXrtu+0cKm5GRAwoDf+dBWXK0TUdg==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection": "6.0.0", + "Microsoft.Extensions.DependencyInjection.Abstractions": "6.0.0", + "Microsoft.Extensions.Logging.Abstractions": "6.0.0", + "Microsoft.Extensions.Options": "6.0.0", + "System.Diagnostics.DiagnosticSource": "6.0.0" + } + }, "Microsoft.Extensions.Logging.Abstractions": { "type": "Transitive", "resolved": "6.0.4", @@ -292,25 +378,26 @@ }, "Microsoft.IdentityModel.Abstractions": { "type": "Transitive", - "resolved": "6.24.0", - "contentHash": "X6aBK56Ot15qKyG7X37KsPnrwah+Ka55NJWPppWVTDi8xWq7CJgeNw2XyaeHgE1o/mW4THwoabZkBbeG2TPBiw==" + "resolved": "6.31.0", + "contentHash": "SBa2DGEZpMThT3ki6lOK5SwH+fotHddNBKH+pfqrlINnl999BreRS9G0QiLruwfmcTDnFr8xwmNjoGnPQqfZwg==" }, "Microsoft.IdentityModel.JsonWebTokens": { "type": "Transitive", - "resolved": "6.24.0", - "contentHash": "XDWrkThcxfuWp79AvAtg5f+uRS1BxkIbJnsG/e8VPzOWkYYuDg33emLjp5EWcwXYYIDsHnVZD/00kM/PYFQc/g==", + "resolved": "6.31.0", + "contentHash": "r0f4clrrlFApwSf2GRpS5X8hL54h1WUlZdq9ZoOy+cJOOqtNhhdfkfkqwxsTGCH/Ae7glOWNxykZzyRXpwcXVQ==", "dependencies": { - "Microsoft.IdentityModel.Tokens": "6.24.0", + "Microsoft.IdentityModel.Tokens": "6.31.0", "System.Text.Encoding": "4.3.0", + "System.Text.Encodings.Web": "4.7.2", "System.Text.Json": "4.7.2" } }, "Microsoft.IdentityModel.Logging": { "type": "Transitive", - "resolved": "6.24.0", - "contentHash": "qLYWDOowM/zghmYKXw1yfYKlHOdS41i8t4hVXr9bSI90zHqhyhQh9GwVy8pENzs5wHeytU23DymluC9NtgYv7w==", + "resolved": "6.31.0", + "contentHash": "YzW5O27nTXxNgNKm+Pud7hXjUlDa2JshtRG+WftQvQIsBUpFA/WjhxG2uO8YanfXbb/IT9r8Cu/VdYkvZ3+9/g==", "dependencies": { - "Microsoft.IdentityModel.Abstractions": "6.24.0" + "Microsoft.IdentityModel.Abstractions": "6.31.0" } }, "Microsoft.IdentityModel.Protocols": { @@ -333,11 +420,11 @@ }, "Microsoft.IdentityModel.Tokens": { "type": "Transitive", - "resolved": "6.24.0", - "contentHash": "ZPqHi86UYuqJXJ7bLnlEctHKkPKT4lGUFbotoCNiXNCSL02emYlcxzGYsRGWWmbFEcYDMi2dcTLLYNzHqWOTsw==", + "resolved": "6.31.0", + "contentHash": "Q1Ej/OAiqi5b/eB8Ozo5FnQ6vlxjgiomnWWenDi2k7+XqhkA2d5TotGtNXpWcWiGmrotNA/o8p51YesnziA0Sw==", "dependencies": { "Microsoft.CSharp": "4.5.0", - "Microsoft.IdentityModel.Logging": "6.24.0", + "Microsoft.IdentityModel.Logging": "6.31.0", "System.Security.Cryptography.Cng": "4.5.0" } }, @@ -397,6 +484,14 @@ "resolved": "7.0.0", "contentHash": "GLltyqEsE5/3IE+zYRP5sNa1l44qKl9v+bfdMcwg+M9qnQf47wK3H0SUR/T+3N4JEQXF3vV4CSuuo0rsg+nq2A==" }, + "System.Collections.Immutable": { + "type": "Transitive", + "resolved": "7.0.0", + "contentHash": "dQPcs0U1IKnBdRDBkrCTi1FoajSTBzLcVTpjO4MBCMC7f4pDOIPzgBoX8JjG7X6uZRJ8EBxsi8+DR1JuwjnzOQ==", + "dependencies": { + "System.Runtime.CompilerServices.Unsafe": "6.0.0" + } + }, "System.Configuration.ConfigurationManager": { "type": "Transitive", "resolved": "6.0.1", @@ -429,11 +524,11 @@ }, "System.IdentityModel.Tokens.Jwt": { "type": "Transitive", - "resolved": "6.24.0", - "contentHash": "Qibsj9MPWq8S/C0FgvmsLfIlHLE7ay0MJIaAmK94ivN3VyDdglqReed5qMvdQhSL0BzK6v0Z1wB/sD88zVu6Jw==", + "resolved": "6.31.0", + "contentHash": "OTlLhhNHODxZvqst0ku8VbIdYNKi25SyM6/VdbpNUe6aItaecVRPtURGvpcQpzltr9H0wy+ycAqBqLUI4SBtaQ==", "dependencies": { - "Microsoft.IdentityModel.JsonWebTokens": "6.24.0", - "Microsoft.IdentityModel.Tokens": "6.24.0" + "Microsoft.IdentityModel.JsonWebTokens": "6.31.0", + "Microsoft.IdentityModel.Tokens": "6.31.0" } }, "System.Memory": { @@ -560,6 +655,19 @@ "dependencies": { "System.Drawing.Common": "6.0.0" } + }, + "Kentico.Xperience.WebApp": { + "type": "CentralTransitive", + "requested": "[27.0.1, )", + "resolved": "27.0.1", + "contentHash": "2wpyDo6wZmQXq9HFNExtNWsJ8hniR0ba1MX6hHG1UjpxuVkG9tFETK3QAefB3FWXYRa8PdpD2RtvSTVwlen3Lw==", + "dependencies": { + "CommandLineParser": "2.9.1", + "HtmlSanitizer": "8.0.723", + "Kentico.Xperience.Core": "[27.0.1]", + "Microsoft.Extensions.FileProviders.Embedded": "6.0.22", + "Microsoft.Extensions.Localization": "6.0.22" + } } } } diff --git a/src/Kentico.Xperience.CRM.Dynamics/Configuration/BizFormFieldsMappingBuilderExtensions.cs b/src/Kentico.Xperience.CRM.Dynamics/Configuration/BizFormFieldsMappingBuilderExtensions.cs index 8b3ba47..9454f1f 100644 --- a/src/Kentico.Xperience.CRM.Dynamics/Configuration/BizFormFieldsMappingBuilderExtensions.cs +++ b/src/Kentico.Xperience.CRM.Dynamics/Configuration/BizFormFieldsMappingBuilderExtensions.cs @@ -1,5 +1,7 @@ using CMS.OnlineForms; using Kentico.Xperience.CRM.Common.Configuration; +using Kentico.Xperience.CRM.Common.Mapping; +using Kentico.Xperience.CRM.Dynamics.Dataverse.Entities; using Microsoft.Xrm.Sdk; using System.Linq.Expressions; using System.Reflection; diff --git a/src/Kentico.Xperience.CRM.Dynamics/Configuration/BizFormMappingBuilderExtensions.cs b/src/Kentico.Xperience.CRM.Dynamics/Configuration/BizFormMappingBuilderExtensions.cs deleted file mode 100644 index 7021e49..0000000 --- a/src/Kentico.Xperience.CRM.Dynamics/Configuration/BizFormMappingBuilderExtensions.cs +++ /dev/null @@ -1,27 +0,0 @@ -using CMS.Core; -using CMS.OnlineForms.Internal; -using Kentico.Xperience.CRM.Common.Configuration; - -namespace Kentico.Xperience.CRM.Dynamics.Configuration; - -public static class BizFormMappingBuilderExtensions -{ - public static BizFormsMappingBuilder AddFormWithContactMapping(this BizFormsMappingBuilder formsMappingBuilder, - string formCodeName) => AddFormWithContactMapping(formsMappingBuilder, formCodeName, b => b); - - public static BizFormsMappingBuilder AddFormWithContactMapping(this BizFormsMappingBuilder formsMappingBuilder, - string formCodeName, - Func configureFields) - { - if (formCodeName is null) throw new ArgumentNullException(nameof(formCodeName)); - //@TODO use Form contact mapping - var mappingBuilder = new BizFormFieldsMappingBuilder(); - - formsMappingBuilder.AddForm(formCodeName, mappingBuilder); - return formsMappingBuilder; - } - - private static void ConfigureMappingFromContactMapping(string formClassName, BizFormFieldsMappingBuilder mappingBuilder) - { - } -} \ No newline at end of file diff --git a/src/Kentico.Xperience.CRM.Dynamics/Configuration/DynamicsBizFormsMappingBuilder.cs b/src/Kentico.Xperience.CRM.Dynamics/Configuration/DynamicsBizFormsMappingBuilder.cs new file mode 100644 index 0000000..db47826 --- /dev/null +++ b/src/Kentico.Xperience.CRM.Dynamics/Configuration/DynamicsBizFormsMappingBuilder.cs @@ -0,0 +1,98 @@ +using CMS.Core; +using CMS.OnlineForms; +using CMS.OnlineForms.Internal; +using Kentico.Xperience.CRM.Common.Configuration; +using Kentico.Xperience.CRM.Common.Mapping; +using Kentico.Xperience.CRM.Common.Services; +using Kentico.Xperience.CRM.Dynamics.Converters; +using Kentico.Xperience.CRM.Dynamics.Dataverse.Entities; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace Kentico.Xperience.CRM.Dynamics.Configuration; + +public class DynamicsBizFormsMappingBuilder +{ + private readonly IServiceCollection serviceCollection; + protected readonly Dictionary forms = new(); + protected readonly Dictionary> converters = new(); + + public DynamicsBizFormsMappingBuilder(IServiceCollection serviceCollection) + { + this.serviceCollection = serviceCollection; + } + + public DynamicsBizFormsMappingBuilder AddForm(string formCodeName, + Func configureFields) + { + if (formCodeName is null) throw new ArgumentNullException(nameof(formCodeName)); + forms.Add(formCodeName.ToLowerInvariant(), configureFields(new BizFormFieldsMappingBuilder())); + + return this; + } + + public DynamicsBizFormsMappingBuilder AddFormWithContactMapping( + string formCodeName) => AddFormWithContactMapping(formCodeName, b => b); + + public DynamicsBizFormsMappingBuilder AddFormWithContactMapping( + string formCodeName, + Func configureFields) + { + if (formCodeName is null) throw new ArgumentNullException(nameof(formCodeName)); + forms.Add(formCodeName.ToLowerInvariant(), configureFields(new BizFormFieldsMappingBuilder())); + + if (converters.TryGetValue(formCodeName.ToLowerInvariant(), out var values)) + { + values.Add(typeof(FormContactMappingToLeadConverter)); + } + else + { + converters[formCodeName.ToLowerInvariant()] = new List { typeof(FormContactMappingToLeadConverter)}; + } + + serviceCollection.TryAddEnumerable(ServiceDescriptor.Scoped, FormContactMappingToLeadConverter>()); + return this; + } + + public DynamicsBizFormsMappingBuilder AddFormWithConverter(string formCodeName) + where TConverter : class, ICRMTypeConverter + { + if (formCodeName is null) throw new ArgumentNullException(nameof(formCodeName)); + + if (converters.TryGetValue(formCodeName.ToLowerInvariant(), out var values)) + { + values.Add(typeof(FormContactMappingToLeadConverter)); + } + else + { + converters[formCodeName.ToLowerInvariant()] = new List { typeof(TConverter)}; + } + + serviceCollection.TryAddEnumerable(ServiceDescriptor.Scoped, TConverter>()); + return this; + } + + /// + /// Adds custom service for BizForm item validation before sending to CRM + /// + /// + /// + /// + public DynamicsBizFormsMappingBuilder AddCustomFormLeadsValidationService() + where TService : class, ILeadsIntegrationValidationService + { + serviceCollection.AddSingleton(); + + return this; + } + + internal DynamicsBizFormsMappingConfiguration Build() + { + return new DynamicsBizFormsMappingConfiguration + { + FormsMappings = forms.Select(f => (f.Key, f.Value.Build())) + .ToDictionary(r => r.Key, r => r.Item2), + FormsConverters = converters + }; + } +} \ No newline at end of file diff --git a/src/Kentico.Xperience.CRM.Dynamics/Configuration/DynamicsBizFormsMappingConfiguration.cs b/src/Kentico.Xperience.CRM.Dynamics/Configuration/DynamicsBizFormsMappingConfiguration.cs index 1ac315c..77b3195 100644 --- a/src/Kentico.Xperience.CRM.Dynamics/Configuration/DynamicsBizFormsMappingConfiguration.cs +++ b/src/Kentico.Xperience.CRM.Dynamics/Configuration/DynamicsBizFormsMappingConfiguration.cs @@ -1,4 +1,7 @@ -using Kentico.Xperience.CRM.Common.Configuration; +using CMS.OnlineForms; +using Kentico.Xperience.CRM.Common.Configuration; +using Kentico.Xperience.CRM.Common.Mapping; +using Kentico.Xperience.CRM.Dynamics.Dataverse.Entities; namespace Kentico.Xperience.CRM.Dynamics.Configuration; @@ -7,4 +10,5 @@ namespace Kentico.Xperience.CRM.Dynamics.Configuration; /// public class DynamicsBizFormsMappingConfiguration : BizFormsMappingConfiguration { + public Dictionary> FormsConverters { get; init; } = new(); } \ No newline at end of file diff --git a/src/Kentico.Xperience.CRM.Dynamics/Converters/FormContactMappingToLeadConverter.cs b/src/Kentico.Xperience.CRM.Dynamics/Converters/FormContactMappingToLeadConverter.cs new file mode 100644 index 0000000..6162341 --- /dev/null +++ b/src/Kentico.Xperience.CRM.Dynamics/Converters/FormContactMappingToLeadConverter.cs @@ -0,0 +1,84 @@ +using CMS.ContactManagement; +using CMS.OnlineForms; +using CMS.OnlineForms.Internal; +using Kentico.Xperience.CRM.Common.Mapping; +using Kentico.Xperience.CRM.Dynamics.Dataverse.Entities; + +namespace Kentico.Xperience.CRM.Dynamics.Converters; + +public class FormContactMappingToLeadConverter : ICRMTypeConverter +{ + private readonly IContactFieldFromFormRetriever contactFieldFromFormRetriever; + + public FormContactMappingToLeadConverter(IContactFieldFromFormRetriever contactFieldFromFormRetriever) + { + this.contactFieldFromFormRetriever = contactFieldFromFormRetriever; + } + + public Task Convert(BizFormItem source, Lead destination) + { + var firstName = contactFieldFromFormRetriever.Retrieve(source, nameof(ContactInfo.ContactFirstName)); + if (!string.IsNullOrWhiteSpace(firstName)) + { + destination.FirstName = firstName; + } + + var lastName = contactFieldFromFormRetriever.Retrieve(source, nameof(ContactInfo.ContactLastName)); + if (!string.IsNullOrWhiteSpace(lastName)) + { + destination.LastName = lastName; + } + + var email = contactFieldFromFormRetriever.Retrieve(source, nameof(ContactInfo.ContactEmail)); + if (!string.IsNullOrWhiteSpace(email)) + { + destination.EMailAddress1 = email; + } + + var companyName = contactFieldFromFormRetriever.Retrieve(source, nameof(ContactInfo.ContactCompanyName)); + if (!string.IsNullOrWhiteSpace(companyName)) + { + destination.CompanyName = companyName; + } + + var phone = contactFieldFromFormRetriever.Retrieve(source, nameof(ContactInfo.ContactMobilePhone)); + if (!string.IsNullOrWhiteSpace(phone)) + { + destination.MobilePhone = phone; + } + + var bizPhone = contactFieldFromFormRetriever.Retrieve(source, nameof(ContactInfo.ContactBusinessPhone)); + if (!string.IsNullOrWhiteSpace(bizPhone)) + { + destination.Telephone1 = bizPhone; + } + + var middleName = contactFieldFromFormRetriever.Retrieve(source, nameof(ContactInfo.ContactMiddleName)); + if (!string.IsNullOrWhiteSpace(middleName)) + { + destination.MiddleName = middleName; + } + + var jobTitle = contactFieldFromFormRetriever.Retrieve(source, nameof(ContactInfo.ContactJobTitle)); + if (!string.IsNullOrWhiteSpace(jobTitle)) + { + destination.JobTitle = jobTitle; + } + + var address1 = contactFieldFromFormRetriever.Retrieve(source, nameof(ContactInfo.ContactAddress1)); + if (!string.IsNullOrWhiteSpace(address1)) + { + destination.Address1_Line1 = address1; + } + + var city = contactFieldFromFormRetriever.Retrieve(source, nameof(ContactInfo.ContactCity)); + if (!string.IsNullOrWhiteSpace(city)) + { + destination.Address1_City = city; + } + + //@TODO country, state + + return Task.FromResult(destination); + } +} \ No newline at end of file diff --git a/src/Kentico.Xperience.CRM.Dynamics/DynamicsServiceCollectionExtensions.cs b/src/Kentico.Xperience.CRM.Dynamics/DynamicsServiceCollectionExtensions.cs index ac42304..0d192e9 100644 --- a/src/Kentico.Xperience.CRM.Dynamics/DynamicsServiceCollectionExtensions.cs +++ b/src/Kentico.Xperience.CRM.Dynamics/DynamicsServiceCollectionExtensions.cs @@ -24,10 +24,13 @@ public static class DynamicsServiceCollectionExtensions /// /// public static IServiceCollection AddDynamicsFormLeadsIntegration(this IServiceCollection serviceCollection, - Action formsConfig, + Action formsConfig, IConfiguration configuration) { - serviceCollection.AddKenticoCrmCommonFormLeadsIntegration(formsConfig); + serviceCollection.AddKenticoCrmCommonFormLeadsIntegration(); + var mappingBuilder = new DynamicsBizFormsMappingBuilder(serviceCollection); + formsConfig(mappingBuilder); + serviceCollection.TryAddSingleton(_ => mappingBuilder.Build()); serviceCollection.AddOptions().Bind(configuration) .PostConfigure(ConfigureWithCMSSettings); diff --git a/src/Kentico.Xperience.CRM.Dynamics/Services/DynamicsLeadsIntegrationService.cs b/src/Kentico.Xperience.CRM.Dynamics/Services/DynamicsLeadsIntegrationService.cs index 46088b4..6aaee1e 100644 --- a/src/Kentico.Xperience.CRM.Dynamics/Services/DynamicsLeadsIntegrationService.cs +++ b/src/Kentico.Xperience.CRM.Dynamics/Services/DynamicsLeadsIntegrationService.cs @@ -1,6 +1,7 @@ using CMS.Helpers; using CMS.OnlineForms; using Kentico.Xperience.CRM.Common.Constants; +using Kentico.Xperience.CRM.Common.Mapping; using Kentico.Xperience.CRM.Common.Mapping.Implementations; using Kentico.Xperience.CRM.Common.Services; using Kentico.Xperience.CRM.Common.Services.Implementations; @@ -18,34 +19,74 @@ namespace Kentico.Xperience.CRM.Dynamics.Services; /// /// Specific Lead integration service implementation for Dynamics Sales /// -internal class DynamicsLeadsIntegrationService : LeadsIntegrationServiceCommon, IDynamicsLeadsIntegrationService +internal class DynamicsLeadsIntegrationService : IDynamicsLeadsIntegrationService { private readonly DynamicsBizFormsMappingConfiguration bizFormMappingConfig; + private readonly ILeadsIntegrationValidationService validationService; private readonly ServiceClient serviceClient; private readonly ILogger logger; private readonly ICRMSyncItemService syncItemService; private readonly IFailedSyncItemService failedSyncItemService; private readonly IOptionsSnapshot settings; + private readonly IEnumerable> converters; public DynamicsLeadsIntegrationService( - DynamicsBizFormsMappingConfiguration bizFormMappingConfig, ILeadsIntegrationValidationService validationService, + DynamicsBizFormsMappingConfiguration bizFormMappingConfig, + ILeadsIntegrationValidationService validationService, ServiceClient serviceClient, ILogger logger, ICRMSyncItemService syncItemService, IFailedSyncItemService failedSyncItemService, - IOptionsSnapshot settings) - : base(bizFormMappingConfig, validationService, logger) + IOptionsSnapshot settings, + IEnumerable> converters) { this.bizFormMappingConfig = bizFormMappingConfig; + this.validationService = validationService; this.serviceClient = serviceClient; this.logger = logger; this.syncItemService = syncItemService; this.failedSyncItemService = failedSyncItemService; this.settings = settings; + this.converters = converters; } + + /// + /// Validates BizForm item, then get specific mapping and finally specific implementation is called + /// from inherited service + /// + /// + public async Task SynchronizeLeadAsync(BizFormItem bizFormItem) + { + var leadConverters = Enumerable.Empty>(); + var leadMapping = Enumerable.Empty(); + + if (bizFormMappingConfig.FormsConverters.TryGetValue(bizFormItem.BizFormClassName.ToLowerInvariant(), + out var formConverters)) + { + leadConverters = converters.Where(c => formConverters.Contains(c.GetType())); + } - protected override async Task SynchronizeLeadAsync(BizFormItem bizFormItem, - IEnumerable fieldMappings) + if (bizFormMappingConfig.FormsMappings.TryGetValue(bizFormItem.BizFormClassName.ToLowerInvariant(), + out var formMapping)) + { + leadMapping = formMapping; + } + + if (leadConverters.Any() || leadMapping.Any()) + { + if (!await validationService.ValidateFormItem(bizFormItem)) + { + logger.LogInformation("BizForm item {ItemID} for {BizFormDisplayName} refused by validation", + bizFormItem.ItemID, bizFormItem.BizFormInfo.FormDisplayName); + return; + } + + await SynchronizeLeadAsync(bizFormItem, leadMapping, leadConverters); + } + } + + protected async Task SynchronizeLeadAsync(BizFormItem bizFormItem, + IEnumerable fieldMappings, IEnumerable> converters) { try { @@ -53,18 +94,18 @@ protected override async Task SynchronizeLeadAsync(BizFormItem bizFormItem, if (syncItem is null) { - await UpdateByEmailOrCreate(bizFormItem, fieldMappings); + await UpdateByEmailOrCreate(bizFormItem, fieldMappings, converters); } else { var existingLead = await GetLeadById(Guid.Parse(syncItem.CRMSyncItemCRMID)); if (existingLead is null) { - await UpdateByEmailOrCreate(bizFormItem, fieldMappings); + await UpdateByEmailOrCreate(bizFormItem, fieldMappings, converters); } else if (!settings.Value.IgnoreExistingRecords) { - await UpdateLeadAsync(existingLead, bizFormItem, fieldMappings); + await UpdateLeadAsync(existingLead, bizFormItem, fieldMappings, converters); } } } @@ -86,11 +127,11 @@ protected override async Task SynchronizeLeadAsync(BizFormItem bizFormItem, } private async Task UpdateByEmailOrCreate(BizFormItem bizFormItem, - IEnumerable fieldMappings) + IEnumerable fieldMappings, IEnumerable> converters) { Lead? existingLead = null; var tmpLead = new Lead(); - MapLead(bizFormItem, tmpLead, fieldMappings); + MapLead(bizFormItem, tmpLead, fieldMappings, converters); if (!string.IsNullOrWhiteSpace(tmpLead.EMailAddress1)) { @@ -99,19 +140,19 @@ private async Task UpdateByEmailOrCreate(BizFormItem bizFormItem, if (existingLead is null) { - await CreateLeadAsync(bizFormItem, fieldMappings); + await CreateLeadAsync(bizFormItem, fieldMappings, converters); } else if (!settings.Value.IgnoreExistingRecords) { - await UpdateLeadAsync(existingLead, bizFormItem, fieldMappings); + await UpdateLeadAsync(existingLead, bizFormItem, fieldMappings, converters); } } private async Task CreateLeadAsync(BizFormItem bizFormItem, - IEnumerable fieldMappings) + IEnumerable fieldMappings, IEnumerable> converters) { var leadEntity = new Lead(); - MapLead(bizFormItem, leadEntity, fieldMappings); + MapLead(bizFormItem, leadEntity, fieldMappings, converters); if (leadEntity.Subject is null) { @@ -126,9 +167,9 @@ private async Task CreateLeadAsync(BizFormItem bizFormItem, } private async Task UpdateLeadAsync(Lead leadEntity, BizFormItem bizFormItem, - IEnumerable fieldMappings) + IEnumerable fieldMappings, IEnumerable> converters) { - MapLead(bizFormItem, leadEntity, fieldMappings); + MapLead(bizFormItem, leadEntity, fieldMappings, converters); if (leadEntity.Subject is null) { @@ -142,9 +183,14 @@ private async Task UpdateLeadAsync(Lead leadEntity, BizFormItem bizFormItem, bizFormItem.ItemID); } - protected virtual void MapLead(BizFormItem bizFormItem, Lead leadEntity, - IEnumerable fieldMappings) + protected async Task MapLead(BizFormItem bizFormItem, Lead leadEntity, + IEnumerable fieldMappings, IEnumerable> converters) { + foreach (var converter in converters) + { + await converter.Convert(bizFormItem, leadEntity); + } + foreach (var fieldMapping in fieldMappings) { var formFieldValue = fieldMapping.FormFieldMapping.MapFormField(bizFormItem); diff --git a/src/Kentico.Xperience.CRM.Dynamics/packages.lock.json b/src/Kentico.Xperience.CRM.Dynamics/packages.lock.json index ae319e1..b583371 100644 --- a/src/Kentico.Xperience.CRM.Dynamics/packages.lock.json +++ b/src/Kentico.Xperience.CRM.Dynamics/packages.lock.json @@ -77,6 +77,14 @@ "System.Text.Encoding.CodePages": "6.0.0" } }, + "AngleSharp.Css": { + "type": "Transitive", + "resolved": "0.17.0", + "contentHash": "bg0AcugmX6BFEi/DHG61QrwRU8iuiX4H8LZehdIzYdqOM/dgb3BsCTzNIcc1XADn4+xfQEdVwJYTSwUxroL4vg==", + "dependencies": { + "AngleSharp": "[0.17.0, 0.18.0)" + } + }, "Azure.Core": { "type": "Transitive", "resolved": "1.25.0", @@ -110,6 +118,33 @@ "resolved": "2.2.1", "contentHash": "A6Zr52zVqJKt18ZBsTnX0qhG0kwIQftVAjLmszmkiR/trSp8H+xj1gUOzk7XHwaKgyREMSV1v9XaKrBUeIOdvQ==" }, + "CommandLineParser": { + "type": "Transitive", + "resolved": "2.9.1", + "contentHash": "OE0sl1/sQ37bjVsPKKtwQlWDgqaxWgtme3xZz7JssWUzg5JpMIyHgCTY9MVMxOg48fJ1AgGT3tgdH5m/kQ5xhA==" + }, + "HtmlSanitizer": { + "type": "Transitive", + "resolved": "8.0.723", + "contentHash": "C4RZX+Mv9OqY+sAM3SD3BdLxvtr9QimIGvLvN5SDjbi7rb6ibeHhGnQA5EyKbkiuQKHO6MBa3h2AZQzjy6z9HA==", + "dependencies": { + "AngleSharp": "[0.17.1]", + "AngleSharp.Css": "[0.17.0]", + "System.Collections.Immutable": "7.0.0" + } + }, + "Kentico.Aira.Client": { + "type": "Transitive", + "resolved": "1.0.22", + "contentHash": "/slLHi7JWaKCBl0EZa01rNqjoaGfOgRj3vNA2wfE88chM43YOPygl22OVEMnyvVLd5nNzzhgv6iX0QyzQlGVxQ==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection": "6.0.1", + "Microsoft.Extensions.Http": "6.0.0", + "Microsoft.Extensions.Options": "6.0.0", + "Microsoft.Extensions.Options.ConfigurationExtensions": "6.0.0", + "System.IdentityModel.Tokens.Jwt": "6.31.0" + } + }, "MailKit": { "type": "Transitive", "resolved": "4.2.0", @@ -118,6 +153,14 @@ "MimeKit": "4.2.0" } }, + "Microsoft.AspNetCore.SpaServices.Extensions": { + "type": "Transitive", + "resolved": "6.0.22", + "contentHash": "RE17e7KHUhyNWwWL93uJzErUZpRjWU71oT4rUbafjA44gEz5YDAfizxAkM0XYCbLBRCbXtoK4q8DJkJvm/LjSA==", + "dependencies": { + "Microsoft.Extensions.FileProviders.Physical": "6.0.0" + } + }, "Microsoft.Bcl.AsyncInterfaces": { "type": "Transitive", "resolved": "5.0.0", @@ -220,6 +263,14 @@ "Microsoft.Extensions.Primitives": "6.0.0" } }, + "Microsoft.Extensions.FileProviders.Embedded": { + "type": "Transitive", + "resolved": "6.0.22", + "contentHash": "kT/7vO2tq68iWQKVeIpH4VI6BDMeGQRaPUoiS+ZIY8QFBNgJNLpKoFC7JExRLMc2j1pFWHPDdToQKpMYSbVXiw==", + "dependencies": { + "Microsoft.Extensions.FileProviders.Abstractions": "6.0.0" + } + }, "Microsoft.Extensions.FileProviders.Physical": { "type": "Transitive", "resolved": "6.0.0", @@ -247,12 +298,13 @@ }, "Microsoft.Extensions.Http": { "type": "Transitive", - "resolved": "3.1.8", - "contentHash": "GRkzBs2wJG6jTGqRrT8l/Sqk4MiO0yQltiekDNw/X7L2l5/gKSud/6Vcjb9b5SPtgn6lxcn8qCmfDtk2kP/cOw==", + "resolved": "6.0.0", + "contentHash": "15+pa2G0bAMHbHewaQIdr/y6ag2H3yh4rd9hTXavtWDzQBkvpe2RMqFg8BxDpcQWssmjmBApGPcw93QRz6YcMg==", "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "3.1.8", - "Microsoft.Extensions.Logging": "3.1.8", - "Microsoft.Extensions.Options": "3.1.8" + "Microsoft.Extensions.DependencyInjection.Abstractions": "6.0.0", + "Microsoft.Extensions.Logging": "6.0.0", + "Microsoft.Extensions.Logging.Abstractions": "6.0.0", + "Microsoft.Extensions.Options": "6.0.0" } }, "Microsoft.Extensions.Localization": { @@ -273,13 +325,14 @@ }, "Microsoft.Extensions.Logging": { "type": "Transitive", - "resolved": "3.1.8", - "contentHash": "Bch88WGwrgJUabSOiTbPgne/jkCcWTyP97db8GWzQH9RcGi6TThiRm8ggsD+OXBW2UBwAYx1Zb1ns1elsMiomQ==", + "resolved": "6.0.0", + "contentHash": "eIbyj40QDg1NDz0HBW0S5f3wrLVnKWnDJ/JtZ+yJDFnDj90VoPuoPmFkeaXrtu+0cKm5GRAwoDf+dBWXK0TUdg==", "dependencies": { - "Microsoft.Extensions.Configuration.Binder": "3.1.8", - "Microsoft.Extensions.DependencyInjection": "3.1.8", - "Microsoft.Extensions.Logging.Abstractions": "3.1.8", - "Microsoft.Extensions.Options": "3.1.8" + "Microsoft.Extensions.DependencyInjection": "6.0.0", + "Microsoft.Extensions.DependencyInjection.Abstractions": "6.0.0", + "Microsoft.Extensions.Logging.Abstractions": "6.0.0", + "Microsoft.Extensions.Options": "6.0.0", + "System.Diagnostics.DiagnosticSource": "6.0.0" } }, "Microsoft.Extensions.Logging.Abstractions": { @@ -340,25 +393,26 @@ }, "Microsoft.IdentityModel.Abstractions": { "type": "Transitive", - "resolved": "6.24.0", - "contentHash": "X6aBK56Ot15qKyG7X37KsPnrwah+Ka55NJWPppWVTDi8xWq7CJgeNw2XyaeHgE1o/mW4THwoabZkBbeG2TPBiw==" + "resolved": "6.31.0", + "contentHash": "SBa2DGEZpMThT3ki6lOK5SwH+fotHddNBKH+pfqrlINnl999BreRS9G0QiLruwfmcTDnFr8xwmNjoGnPQqfZwg==" }, "Microsoft.IdentityModel.JsonWebTokens": { "type": "Transitive", - "resolved": "6.24.0", - "contentHash": "XDWrkThcxfuWp79AvAtg5f+uRS1BxkIbJnsG/e8VPzOWkYYuDg33emLjp5EWcwXYYIDsHnVZD/00kM/PYFQc/g==", + "resolved": "6.31.0", + "contentHash": "r0f4clrrlFApwSf2GRpS5X8hL54h1WUlZdq9ZoOy+cJOOqtNhhdfkfkqwxsTGCH/Ae7glOWNxykZzyRXpwcXVQ==", "dependencies": { - "Microsoft.IdentityModel.Tokens": "6.24.0", + "Microsoft.IdentityModel.Tokens": "6.31.0", "System.Text.Encoding": "4.3.0", + "System.Text.Encodings.Web": "4.7.2", "System.Text.Json": "4.7.2" } }, "Microsoft.IdentityModel.Logging": { "type": "Transitive", - "resolved": "6.24.0", - "contentHash": "qLYWDOowM/zghmYKXw1yfYKlHOdS41i8t4hVXr9bSI90zHqhyhQh9GwVy8pENzs5wHeytU23DymluC9NtgYv7w==", + "resolved": "6.31.0", + "contentHash": "YzW5O27nTXxNgNKm+Pud7hXjUlDa2JshtRG+WftQvQIsBUpFA/WjhxG2uO8YanfXbb/IT9r8Cu/VdYkvZ3+9/g==", "dependencies": { - "Microsoft.IdentityModel.Abstractions": "6.24.0" + "Microsoft.IdentityModel.Abstractions": "6.31.0" } }, "Microsoft.IdentityModel.Protocols": { @@ -381,11 +435,11 @@ }, "Microsoft.IdentityModel.Tokens": { "type": "Transitive", - "resolved": "6.24.0", - "contentHash": "ZPqHi86UYuqJXJ7bLnlEctHKkPKT4lGUFbotoCNiXNCSL02emYlcxzGYsRGWWmbFEcYDMi2dcTLLYNzHqWOTsw==", + "resolved": "6.31.0", + "contentHash": "Q1Ej/OAiqi5b/eB8Ozo5FnQ6vlxjgiomnWWenDi2k7+XqhkA2d5TotGtNXpWcWiGmrotNA/o8p51YesnziA0Sw==", "dependencies": { "Microsoft.CSharp": "4.5.0", - "Microsoft.IdentityModel.Logging": "6.24.0", + "Microsoft.IdentityModel.Logging": "6.31.0", "System.Security.Cryptography.Cng": "4.5.0" } }, @@ -560,6 +614,14 @@ "System.Threading.Tasks": "4.3.0" } }, + "System.Collections.Immutable": { + "type": "Transitive", + "resolved": "7.0.0", + "contentHash": "dQPcs0U1IKnBdRDBkrCTi1FoajSTBzLcVTpjO4MBCMC7f4pDOIPzgBoX8JjG7X6uZRJ8EBxsi8+DR1JuwjnzOQ==", + "dependencies": { + "System.Runtime.CompilerServices.Unsafe": "6.0.0" + } + }, "System.Configuration.ConfigurationManager": { "type": "Transitive", "resolved": "6.0.1", @@ -632,11 +694,11 @@ }, "System.IdentityModel.Tokens.Jwt": { "type": "Transitive", - "resolved": "6.24.0", - "contentHash": "Qibsj9MPWq8S/C0FgvmsLfIlHLE7ay0MJIaAmK94ivN3VyDdglqReed5qMvdQhSL0BzK6v0Z1wB/sD88zVu6Jw==", + "resolved": "6.31.0", + "contentHash": "OTlLhhNHODxZvqst0ku8VbIdYNKi25SyM6/VdbpNUe6aItaecVRPtURGvpcQpzltr9H0wy+ycAqBqLUI4SBtaQ==", "dependencies": { - "Microsoft.IdentityModel.JsonWebTokens": "6.24.0", - "Microsoft.IdentityModel.Tokens": "6.24.0" + "Microsoft.IdentityModel.JsonWebTokens": "6.31.0", + "Microsoft.IdentityModel.Tokens": "6.31.0" } }, "System.IO": { @@ -1253,8 +1315,34 @@ "kentico.xperience.crm.common": { "type": "Project", "dependencies": { + "Kentico.Xperience.Admin": "[27.0.1, )", "Kentico.Xperience.Core": "[27.0.1, )" } + }, + "Kentico.Xperience.Admin": { + "type": "CentralTransitive", + "requested": "[27.0.1, )", + "resolved": "27.0.1", + "contentHash": "Kgk6v0S5OkIFUc/8cj6Nx/pP09Czz21Qwce/iqf4rWC9mnU7fcQ6sXlNmk1VL9jaNKb6XXrOZSWxjWk8AUVTJA==", + "dependencies": { + "Kentico.Aira.Client": "1.0.22", + "Kentico.Xperience.WebApp": "[27.0.1]", + "Microsoft.AspNetCore.SpaServices.Extensions": "6.0.22", + "Microsoft.Extensions.FileProviders.Embedded": "6.0.22" + } + }, + "Kentico.Xperience.WebApp": { + "type": "CentralTransitive", + "requested": "[27.0.1, )", + "resolved": "27.0.1", + "contentHash": "2wpyDo6wZmQXq9HFNExtNWsJ8hniR0ba1MX6hHG1UjpxuVkG9tFETK3QAefB3FWXYRa8PdpD2RtvSTVwlen3Lw==", + "dependencies": { + "CommandLineParser": "2.9.1", + "HtmlSanitizer": "8.0.723", + "Kentico.Xperience.Core": "[27.0.1]", + "Microsoft.Extensions.FileProviders.Embedded": "6.0.22", + "Microsoft.Extensions.Localization": "6.0.22" + } } } } diff --git a/src/Kentico.Xperience.CRM.SalesForce/Configuration/SalesForceBizFormsMappingBuilder.cs b/src/Kentico.Xperience.CRM.SalesForce/Configuration/SalesForceBizFormsMappingBuilder.cs new file mode 100644 index 0000000..8b4f7ea --- /dev/null +++ b/src/Kentico.Xperience.CRM.SalesForce/Configuration/SalesForceBizFormsMappingBuilder.cs @@ -0,0 +1,21 @@ +using Kentico.Xperience.CRM.Common.Configuration; +using Microsoft.Extensions.DependencyInjection; + +namespace Kentico.Xperience.CRM.SalesForce.Configuration; + +public class SalesForceBizFormsMappingBuilder : BizFormsMappingBuilder +{ + public SalesForceBizFormsMappingBuilder(IServiceCollection serviceCollection) : base(serviceCollection) + { + } + + internal SalesForceBizFormsMappingConfiguration Build() + { + return new SalesForceBizFormsMappingConfiguration + { + FormsMappings = forms.Select(f => (f.Key, f.Value.Build())) + .ToDictionary(r => r.Key, r => r.Item2), + }; + } + +} \ No newline at end of file diff --git a/src/Kentico.Xperience.CRM.SalesForce/SalesForceServiceCollectionsExtensions.cs b/src/Kentico.Xperience.CRM.SalesForce/SalesForceServiceCollectionsExtensions.cs index 0ce72a3..247de54 100644 --- a/src/Kentico.Xperience.CRM.SalesForce/SalesForceServiceCollectionsExtensions.cs +++ b/src/Kentico.Xperience.CRM.SalesForce/SalesForceServiceCollectionsExtensions.cs @@ -8,6 +8,7 @@ using Kentico.Xperience.CRM.SalesForce.Services; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Options; using System.Globalization; @@ -24,10 +25,17 @@ public static class SalesForceServiceCollectionsExtensions /// /// public static IServiceCollection AddSalesForceFormLeadsIntegration(this IServiceCollection serviceCollection, - Action formsConfig, + Action formsConfig, IConfiguration configuration) { - serviceCollection.AddKenticoCrmCommonFormLeadsIntegration(formsConfig); + serviceCollection.AddKenticoCrmCommonFormLeadsIntegration(); + serviceCollection.TryAddSingleton( + _ => + { + var mappingBuilder = new SalesForceBizFormsMappingBuilder(serviceCollection); + formsConfig(mappingBuilder); + return mappingBuilder.Build(); + }); serviceCollection.AddOptions().Bind(configuration) .PostConfigure(ConfigureWithCMSSettings); diff --git a/src/Kentico.Xperience.CRM.SalesForce/packages.lock.json b/src/Kentico.Xperience.CRM.SalesForce/packages.lock.json index 0f65efa..06a4cc7 100644 --- a/src/Kentico.Xperience.CRM.SalesForce/packages.lock.json +++ b/src/Kentico.Xperience.CRM.SalesForce/packages.lock.json @@ -66,6 +66,14 @@ "System.Text.Encoding.CodePages": "6.0.0" } }, + "AngleSharp.Css": { + "type": "Transitive", + "resolved": "0.17.0", + "contentHash": "bg0AcugmX6BFEi/DHG61QrwRU8iuiX4H8LZehdIzYdqOM/dgb3BsCTzNIcc1XADn4+xfQEdVwJYTSwUxroL4vg==", + "dependencies": { + "AngleSharp": "[0.17.0, 0.18.0)" + } + }, "Azure.Core": { "type": "Transitive", "resolved": "1.25.0", @@ -99,6 +107,11 @@ "resolved": "2.2.1", "contentHash": "A6Zr52zVqJKt18ZBsTnX0qhG0kwIQftVAjLmszmkiR/trSp8H+xj1gUOzk7XHwaKgyREMSV1v9XaKrBUeIOdvQ==" }, + "CommandLineParser": { + "type": "Transitive", + "resolved": "2.9.1", + "contentHash": "OE0sl1/sQ37bjVsPKKtwQlWDgqaxWgtme3xZz7JssWUzg5JpMIyHgCTY9MVMxOg48fJ1AgGT3tgdH5m/kQ5xhA==" + }, "Duende.AccessTokenManagement": { "type": "Transitive", "resolved": "2.0.3", @@ -113,6 +126,28 @@ "System.IdentityModel.Tokens.Jwt": "6.15.1" } }, + "HtmlSanitizer": { + "type": "Transitive", + "resolved": "8.0.723", + "contentHash": "C4RZX+Mv9OqY+sAM3SD3BdLxvtr9QimIGvLvN5SDjbi7rb6ibeHhGnQA5EyKbkiuQKHO6MBa3h2AZQzjy6z9HA==", + "dependencies": { + "AngleSharp": "[0.17.1]", + "AngleSharp.Css": "[0.17.0]", + "System.Collections.Immutable": "7.0.0" + } + }, + "Kentico.Aira.Client": { + "type": "Transitive", + "resolved": "1.0.22", + "contentHash": "/slLHi7JWaKCBl0EZa01rNqjoaGfOgRj3vNA2wfE88chM43YOPygl22OVEMnyvVLd5nNzzhgv6iX0QyzQlGVxQ==", + "dependencies": { + "Microsoft.Extensions.DependencyInjection": "6.0.1", + "Microsoft.Extensions.Http": "6.0.0", + "Microsoft.Extensions.Options": "6.0.0", + "Microsoft.Extensions.Options.ConfigurationExtensions": "6.0.0", + "System.IdentityModel.Tokens.Jwt": "6.31.0" + } + }, "MailKit": { "type": "Transitive", "resolved": "4.2.0", @@ -129,6 +164,14 @@ "Microsoft.IdentityModel.Protocols.OpenIdConnect": "6.10.0" } }, + "Microsoft.AspNetCore.SpaServices.Extensions": { + "type": "Transitive", + "resolved": "6.0.22", + "contentHash": "RE17e7KHUhyNWwWL93uJzErUZpRjWU71oT4rUbafjA44gEz5YDAfizxAkM0XYCbLBRCbXtoK4q8DJkJvm/LjSA==", + "dependencies": { + "Microsoft.Extensions.FileProviders.Physical": "6.0.0" + } + }, "Microsoft.Bcl.AsyncInterfaces": { "type": "Transitive", "resolved": "1.1.1", @@ -236,6 +279,14 @@ "Microsoft.Extensions.Primitives": "6.0.0" } }, + "Microsoft.Extensions.FileProviders.Embedded": { + "type": "Transitive", + "resolved": "6.0.22", + "contentHash": "kT/7vO2tq68iWQKVeIpH4VI6BDMeGQRaPUoiS+ZIY8QFBNgJNLpKoFC7JExRLMc2j1pFWHPDdToQKpMYSbVXiw==", + "dependencies": { + "Microsoft.Extensions.FileProviders.Abstractions": "6.0.0" + } + }, "Microsoft.Extensions.FileProviders.Physical": { "type": "Transitive", "resolved": "6.0.0", @@ -353,25 +404,26 @@ }, "Microsoft.IdentityModel.Abstractions": { "type": "Transitive", - "resolved": "6.24.0", - "contentHash": "X6aBK56Ot15qKyG7X37KsPnrwah+Ka55NJWPppWVTDi8xWq7CJgeNw2XyaeHgE1o/mW4THwoabZkBbeG2TPBiw==" + "resolved": "6.31.0", + "contentHash": "SBa2DGEZpMThT3ki6lOK5SwH+fotHddNBKH+pfqrlINnl999BreRS9G0QiLruwfmcTDnFr8xwmNjoGnPQqfZwg==" }, "Microsoft.IdentityModel.JsonWebTokens": { "type": "Transitive", - "resolved": "6.24.0", - "contentHash": "XDWrkThcxfuWp79AvAtg5f+uRS1BxkIbJnsG/e8VPzOWkYYuDg33emLjp5EWcwXYYIDsHnVZD/00kM/PYFQc/g==", + "resolved": "6.31.0", + "contentHash": "r0f4clrrlFApwSf2GRpS5X8hL54h1WUlZdq9ZoOy+cJOOqtNhhdfkfkqwxsTGCH/Ae7glOWNxykZzyRXpwcXVQ==", "dependencies": { - "Microsoft.IdentityModel.Tokens": "6.24.0", + "Microsoft.IdentityModel.Tokens": "6.31.0", "System.Text.Encoding": "4.3.0", + "System.Text.Encodings.Web": "4.7.2", "System.Text.Json": "4.7.2" } }, "Microsoft.IdentityModel.Logging": { "type": "Transitive", - "resolved": "6.24.0", - "contentHash": "qLYWDOowM/zghmYKXw1yfYKlHOdS41i8t4hVXr9bSI90zHqhyhQh9GwVy8pENzs5wHeytU23DymluC9NtgYv7w==", + "resolved": "6.31.0", + "contentHash": "YzW5O27nTXxNgNKm+Pud7hXjUlDa2JshtRG+WftQvQIsBUpFA/WjhxG2uO8YanfXbb/IT9r8Cu/VdYkvZ3+9/g==", "dependencies": { - "Microsoft.IdentityModel.Abstractions": "6.24.0" + "Microsoft.IdentityModel.Abstractions": "6.31.0" } }, "Microsoft.IdentityModel.Protocols": { @@ -394,11 +446,11 @@ }, "Microsoft.IdentityModel.Tokens": { "type": "Transitive", - "resolved": "6.24.0", - "contentHash": "ZPqHi86UYuqJXJ7bLnlEctHKkPKT4lGUFbotoCNiXNCSL02emYlcxzGYsRGWWmbFEcYDMi2dcTLLYNzHqWOTsw==", + "resolved": "6.31.0", + "contentHash": "Q1Ej/OAiqi5b/eB8Ozo5FnQ6vlxjgiomnWWenDi2k7+XqhkA2d5TotGtNXpWcWiGmrotNA/o8p51YesnziA0Sw==", "dependencies": { "Microsoft.CSharp": "4.5.0", - "Microsoft.IdentityModel.Logging": "6.24.0", + "Microsoft.IdentityModel.Logging": "6.31.0", "System.Security.Cryptography.Cng": "4.5.0" } }, @@ -458,6 +510,14 @@ "resolved": "7.0.0", "contentHash": "GLltyqEsE5/3IE+zYRP5sNa1l44qKl9v+bfdMcwg+M9qnQf47wK3H0SUR/T+3N4JEQXF3vV4CSuuo0rsg+nq2A==" }, + "System.Collections.Immutable": { + "type": "Transitive", + "resolved": "7.0.0", + "contentHash": "dQPcs0U1IKnBdRDBkrCTi1FoajSTBzLcVTpjO4MBCMC7f4pDOIPzgBoX8JjG7X6uZRJ8EBxsi8+DR1JuwjnzOQ==", + "dependencies": { + "System.Runtime.CompilerServices.Unsafe": "6.0.0" + } + }, "System.Configuration.ConfigurationManager": { "type": "Transitive", "resolved": "6.0.1", @@ -490,11 +550,11 @@ }, "System.IdentityModel.Tokens.Jwt": { "type": "Transitive", - "resolved": "6.24.0", - "contentHash": "Qibsj9MPWq8S/C0FgvmsLfIlHLE7ay0MJIaAmK94ivN3VyDdglqReed5qMvdQhSL0BzK6v0Z1wB/sD88zVu6Jw==", + "resolved": "6.31.0", + "contentHash": "OTlLhhNHODxZvqst0ku8VbIdYNKi25SyM6/VdbpNUe6aItaecVRPtURGvpcQpzltr9H0wy+ycAqBqLUI4SBtaQ==", "dependencies": { - "Microsoft.IdentityModel.JsonWebTokens": "6.24.0", - "Microsoft.IdentityModel.Tokens": "6.24.0" + "Microsoft.IdentityModel.JsonWebTokens": "6.31.0", + "Microsoft.IdentityModel.Tokens": "6.31.0" } }, "System.Memory": { @@ -625,8 +685,34 @@ "kentico.xperience.crm.common": { "type": "Project", "dependencies": { + "Kentico.Xperience.Admin": "[27.0.1, )", "Kentico.Xperience.Core": "[27.0.1, )" } + }, + "Kentico.Xperience.Admin": { + "type": "CentralTransitive", + "requested": "[27.0.1, )", + "resolved": "27.0.1", + "contentHash": "Kgk6v0S5OkIFUc/8cj6Nx/pP09Czz21Qwce/iqf4rWC9mnU7fcQ6sXlNmk1VL9jaNKb6XXrOZSWxjWk8AUVTJA==", + "dependencies": { + "Kentico.Aira.Client": "1.0.22", + "Kentico.Xperience.WebApp": "[27.0.1]", + "Microsoft.AspNetCore.SpaServices.Extensions": "6.0.22", + "Microsoft.Extensions.FileProviders.Embedded": "6.0.22" + } + }, + "Kentico.Xperience.WebApp": { + "type": "CentralTransitive", + "requested": "[27.0.1, )", + "resolved": "27.0.1", + "contentHash": "2wpyDo6wZmQXq9HFNExtNWsJ8hniR0ba1MX6hHG1UjpxuVkG9tFETK3QAefB3FWXYRa8PdpD2RtvSTVwlen3Lw==", + "dependencies": { + "CommandLineParser": "2.9.1", + "HtmlSanitizer": "8.0.723", + "Kentico.Xperience.Core": "[27.0.1]", + "Microsoft.Extensions.FileProviders.Embedded": "6.0.22", + "Microsoft.Extensions.Localization": "6.0.22" + } } } } From 2dcaa945476bb80b6aef5c182d8da36187fef7c8 Mon Sep 17 00:00:00 2001 From: Martin Foukal Date: Thu, 11 Jan 2024 00:37:03 +0100 Subject: [PATCH 08/23] salesforce converters finished --- examples/DancingGoat/Program.cs | 30 +++--- .../Admin/CRMSyncItemListing.cs | 7 +- .../Configuration/BizFormsMappingBuilder.cs | 35 ------- .../BizFormsMappingConfiguration.cs | 1 + .../LeadsIntegrationServiceCommon.cs | 50 ---------- .../DynamicsBizFormsMappingBuilder.cs | 17 +--- .../DynamicsBizFormsMappingConfiguration.cs | 1 - .../FormContactMappingToLeadConverter.cs | 6 ++ .../DynamicsLeadsIntegrationService.cs | 10 +- .../SalesForceBizFormsMappingBuilder.cs | 82 +++++++++++++++- .../FormContactMappingToLeadConverter.cs | 84 +++++++++++++++++ .../SalesForceServiceCollectionsExtensions.cs | 15 ++- .../SalesForceLeadsIntegrationService.cs | 93 ++++++++++++++----- 13 files changed, 277 insertions(+), 154 deletions(-) delete mode 100644 src/Kentico.Xperience.CRM.Common/Configuration/BizFormsMappingBuilder.cs delete mode 100644 src/Kentico.Xperience.CRM.Common/Services/Implementations/LeadsIntegrationServiceCommon.cs create mode 100644 src/Kentico.Xperience.CRM.SalesForce/Converters/FormContactMappingToLeadConverter.cs diff --git a/examples/DancingGoat/Program.cs b/examples/DancingGoat/Program.cs index d74dcde..29ef315 100644 --- a/examples/DancingGoat/Program.cs +++ b/examples/DancingGoat/Program.cs @@ -67,23 +67,29 @@ builder.Services.AddDynamicsFormLeadsIntegration(builder => builder.AddFormWithContactMapping(DancingGoatContactUsItem.CLASS_NAME,b => b .MapField(c => c.UserMessage, e => e.EMailAddress1)) - .AddCustomFormLeadsValidationService() //optional + .AddCustomValidation() //optional , builder.Configuration.GetSection(DynamicsIntegrationSettings.ConfigKeyName)); //config section with settings +// builder.Services.AddSalesForceFormLeadsIntegration(builder => +// builder.AddForm(DancingGoatContactUsItem.CLASS_NAME, //form class name +// c => c +// .MapField("UserFirstName", "FirstName") //option1: mapping based on source and target field names +// .MapField("UserLastName", e => e.LastName) //option 2: mapping source name string -> member expression to SObject +// .MapField(c => c.UserEmail, e => e.Email) +// //option 3: source mapping function from generated BizForm object -> member expression to SObject +// .MapField(b => b.GetStringValue("UserMessage", ""), e => e.Description) +// //option 4: source mapping function general BizFormItem -> member expression to SObject +// ) +// , +// builder.Configuration.GetSection(SalesForceIntegrationSettings.ConfigKeyName)); //config section with settings + builder.Services.AddSalesForceFormLeadsIntegration(builder => - builder.AddForm(DancingGoatContactUsItem.CLASS_NAME, //form class name - c => c - .MapField("UserFirstName", "FirstName") //option1: mapping based on source and target field names - .MapField("UserLastName", e => e.LastName) //option 2: mapping source name string -> member expression to SObject - .MapField(c => c.UserEmail, e => e.Email) - //option 3: source mapping function from generated BizForm object -> member expression to SObject - .MapField(b => b.GetStringValue("UserMessage", ""), e => e.Description) - //option 4: source mapping function general BizFormItem -> member expression to SObject - ) - //.AddForm("formname") // add another forms definitions + builder.AddFormWithContactMapping(DancingGoatContactUsItem.CLASS_NAME,b => b + .MapField(c => c.UserMessage, e => e.Description)) + .AddCustomValidation() //optional , - builder.Configuration.GetSection(SalesForceIntegrationSettings.ConfigKeyName)); //config section with settings + builder.Configuration.GetSection(DynamicsIntegrationSettings.ConfigKeyName)); //config section with settings //CRM integration registration end diff --git a/src/Kentico.Xperience.CRM.Common/Admin/CRMSyncItemListing.cs b/src/Kentico.Xperience.CRM.Common/Admin/CRMSyncItemListing.cs index 661b2ac..90fa64f 100644 --- a/src/Kentico.Xperience.CRM.Common/Admin/CRMSyncItemListing.cs +++ b/src/Kentico.Xperience.CRM.Common/Admin/CRMSyncItemListing.cs @@ -9,7 +9,7 @@ [assembly: UIPage(typeof(FormEditSection), "crm-sync-listing", typeof(CRMSyncItemListing), - "CRM synchronization", TemplateNames.LISTING, 1000, "xp-graph")] + "CRM synchronization", TemplateNames.LISTING, 1000, Icons.IntegrationScheme)] namespace Kentico.Xperience.CRM.Common.Admin; @@ -24,15 +24,14 @@ public class CRMSyncItemListing : ListingPage public int FormId { get; set; } private BizFormInfo EditedForm => - this.editedForm ??= AbstractInfo.Provider.Get(this.FormId); + this.editedForm ??= AbstractInfo.Provider.Get(FormId); private DataClassInfo DataClassInfo => this.dataClassInfo ??= - DataClassInfoProviderBase.GetDataClassInfo(this.EditedForm.FormClassID); + DataClassInfoProviderBase.GetDataClassInfo(EditedForm.FormClassID); public override Task ConfigurePage() { PageConfiguration.ColumnConfigurations - .AddColumn(nameof(CRMSyncItemInfo.CRMSyncItemEntityClass), "Form") .AddColumn(nameof(CRMSyncItemInfo.CRMSyncItemEntityID), "Form item ID") .AddColumn(nameof(CRMSyncItemInfo.CRMSyncItemEntityCRM), "CRM") .AddColumn(nameof(CRMSyncItemInfo.CRMSyncItemCRMID), "CRM ID") diff --git a/src/Kentico.Xperience.CRM.Common/Configuration/BizFormsMappingBuilder.cs b/src/Kentico.Xperience.CRM.Common/Configuration/BizFormsMappingBuilder.cs deleted file mode 100644 index 269abca..0000000 --- a/src/Kentico.Xperience.CRM.Common/Configuration/BizFormsMappingBuilder.cs +++ /dev/null @@ -1,35 +0,0 @@ -using Microsoft.Extensions.DependencyInjection; - -namespace Kentico.Xperience.CRM.Common.Configuration; - -/// -/// Common builder for BizForms to CRM leads configuration mapping -/// -public class BizFormsMappingBuilder -{ - private readonly IServiceCollection serviceCollection; - protected readonly Dictionary forms = new(); - - public BizFormsMappingBuilder(IServiceCollection serviceCollection) - { - this.serviceCollection = serviceCollection; - } - - public BizFormsMappingBuilder AddForm(string formCodeName, - Func configureFields) - { - if (formCodeName is null) throw new ArgumentNullException(nameof(formCodeName)); - - forms.Add(formCodeName.ToLowerInvariant(), configureFields(new BizFormFieldsMappingBuilder())); - return this; - } - - public BizFormsMappingBuilder AddForm(string formCodeName, - BizFormFieldsMappingBuilder configuredBuilder) - { - if (formCodeName is null) throw new ArgumentNullException(nameof(formCodeName)); - - forms.Add(formCodeName.ToLowerInvariant(), configuredBuilder); - return this; - } -} \ No newline at end of file diff --git a/src/Kentico.Xperience.CRM.Common/Configuration/BizFormsMappingConfiguration.cs b/src/Kentico.Xperience.CRM.Common/Configuration/BizFormsMappingConfiguration.cs index b7a4bff..65dbaf4 100644 --- a/src/Kentico.Xperience.CRM.Common/Configuration/BizFormsMappingConfiguration.cs +++ b/src/Kentico.Xperience.CRM.Common/Configuration/BizFormsMappingConfiguration.cs @@ -8,4 +8,5 @@ namespace Kentico.Xperience.CRM.Common.Configuration; public class BizFormsMappingConfiguration { public Dictionary> FormsMappings { get; init; } = new(); + public Dictionary> FormsConverters { get; init; } = new(); } \ No newline at end of file diff --git a/src/Kentico.Xperience.CRM.Common/Services/Implementations/LeadsIntegrationServiceCommon.cs b/src/Kentico.Xperience.CRM.Common/Services/Implementations/LeadsIntegrationServiceCommon.cs deleted file mode 100644 index e839678..0000000 --- a/src/Kentico.Xperience.CRM.Common/Services/Implementations/LeadsIntegrationServiceCommon.cs +++ /dev/null @@ -1,50 +0,0 @@ -using CMS.OnlineForms; -using Kentico.Xperience.CRM.Common.Configuration; -using Kentico.Xperience.CRM.Common.Mapping.Implementations; -using Microsoft.Extensions.Logging; - -namespace Kentico.Xperience.CRM.Common.Services.Implementations; - -/// -/// This abstract class contains common functionality for specific Lead integration service like calling validation and -/// getting mapping for given BizForm item -/// -public abstract class LeadsIntegrationServiceCommon : ILeadsIntegrationService -{ - private readonly BizFormsMappingConfiguration bizFormMappingConfig; - private readonly ILeadsIntegrationValidationService validationService; - private readonly ILogger logger; - - protected LeadsIntegrationServiceCommon( - BizFormsMappingConfiguration bizFormMappingConfig, - ILeadsIntegrationValidationService validationService, - ILogger logger) - { - this.bizFormMappingConfig = bizFormMappingConfig; - this.validationService = validationService; - this.logger = logger; - } - - /// - /// Validates BizForm item, then get specific mapping and finally specific implementation is called - /// from inherited service - /// - /// - public async Task SynchronizeLeadAsync(BizFormItem bizFormItem) - { - if (!await validationService.ValidateFormItem(bizFormItem)) - { - logger.LogInformation("BizForm item {ItemID} for {BizFormDisplayName} refused by validation", - bizFormItem.ItemID, bizFormItem.BizFormInfo.FormDisplayName); - return; - } - - if (bizFormMappingConfig.FormsMappings.TryGetValue(bizFormItem.BizFormClassName.ToLowerInvariant(), - out var formMapping)) - { - await SynchronizeLeadAsync(bizFormItem, formMapping); - } - } - - protected abstract Task SynchronizeLeadAsync(BizFormItem bizFormItem, IEnumerable fieldMappings); -} \ No newline at end of file diff --git a/src/Kentico.Xperience.CRM.Dynamics/Configuration/DynamicsBizFormsMappingBuilder.cs b/src/Kentico.Xperience.CRM.Dynamics/Configuration/DynamicsBizFormsMappingBuilder.cs index db47826..e2ae5ad 100644 --- a/src/Kentico.Xperience.CRM.Dynamics/Configuration/DynamicsBizFormsMappingBuilder.cs +++ b/src/Kentico.Xperience.CRM.Dynamics/Configuration/DynamicsBizFormsMappingBuilder.cs @@ -31,8 +31,8 @@ public DynamicsBizFormsMappingBuilder AddForm(string formCodeName, return this; } - public DynamicsBizFormsMappingBuilder AddFormWithContactMapping( - string formCodeName) => AddFormWithContactMapping(formCodeName, b => b); + public DynamicsBizFormsMappingBuilder AddFormWithContactMapping(string formCodeName) + => AddFormWithContactMapping(formCodeName, b => b); public DynamicsBizFormsMappingBuilder AddFormWithContactMapping( string formCodeName, @@ -41,16 +41,7 @@ public DynamicsBizFormsMappingBuilder AddFormWithContactMapping( if (formCodeName is null) throw new ArgumentNullException(nameof(formCodeName)); forms.Add(formCodeName.ToLowerInvariant(), configureFields(new BizFormFieldsMappingBuilder())); - if (converters.TryGetValue(formCodeName.ToLowerInvariant(), out var values)) - { - values.Add(typeof(FormContactMappingToLeadConverter)); - } - else - { - converters[formCodeName.ToLowerInvariant()] = new List { typeof(FormContactMappingToLeadConverter)}; - } - - serviceCollection.TryAddEnumerable(ServiceDescriptor.Scoped, FormContactMappingToLeadConverter>()); + AddFormWithConverter(formCodeName); return this; } @@ -78,7 +69,7 @@ public DynamicsBizFormsMappingBuilder AddFormWithConverter(string fo /// /// /// - public DynamicsBizFormsMappingBuilder AddCustomFormLeadsValidationService() + public DynamicsBizFormsMappingBuilder AddCustomValidation() where TService : class, ILeadsIntegrationValidationService { serviceCollection.AddSingleton(); diff --git a/src/Kentico.Xperience.CRM.Dynamics/Configuration/DynamicsBizFormsMappingConfiguration.cs b/src/Kentico.Xperience.CRM.Dynamics/Configuration/DynamicsBizFormsMappingConfiguration.cs index 77b3195..5474942 100644 --- a/src/Kentico.Xperience.CRM.Dynamics/Configuration/DynamicsBizFormsMappingConfiguration.cs +++ b/src/Kentico.Xperience.CRM.Dynamics/Configuration/DynamicsBizFormsMappingConfiguration.cs @@ -10,5 +10,4 @@ namespace Kentico.Xperience.CRM.Dynamics.Configuration; /// public class DynamicsBizFormsMappingConfiguration : BizFormsMappingConfiguration { - public Dictionary> FormsConverters { get; init; } = new(); } \ No newline at end of file diff --git a/src/Kentico.Xperience.CRM.Dynamics/Converters/FormContactMappingToLeadConverter.cs b/src/Kentico.Xperience.CRM.Dynamics/Converters/FormContactMappingToLeadConverter.cs index 6162341..4bbf0b0 100644 --- a/src/Kentico.Xperience.CRM.Dynamics/Converters/FormContactMappingToLeadConverter.cs +++ b/src/Kentico.Xperience.CRM.Dynamics/Converters/FormContactMappingToLeadConverter.cs @@ -77,6 +77,12 @@ public Task Convert(BizFormItem source, Lead destination) destination.Address1_City = city; } + var zipCode = contactFieldFromFormRetriever.Retrieve(source, nameof(ContactInfo.ContactZIP)); + if (!string.IsNullOrWhiteSpace(zipCode)) + { + destination.Address1_PostalCode = zipCode; + } + //@TODO country, state return Task.FromResult(destination); diff --git a/src/Kentico.Xperience.CRM.Dynamics/Services/DynamicsLeadsIntegrationService.cs b/src/Kentico.Xperience.CRM.Dynamics/Services/DynamicsLeadsIntegrationService.cs index 6aaee1e..faca9d4 100644 --- a/src/Kentico.Xperience.CRM.Dynamics/Services/DynamicsLeadsIntegrationService.cs +++ b/src/Kentico.Xperience.CRM.Dynamics/Services/DynamicsLeadsIntegrationService.cs @@ -28,7 +28,7 @@ internal class DynamicsLeadsIntegrationService : IDynamicsLeadsIntegrationServic private readonly ICRMSyncItemService syncItemService; private readonly IFailedSyncItemService failedSyncItemService; private readonly IOptionsSnapshot settings; - private readonly IEnumerable> converters; + private readonly IEnumerable> formsConverters; public DynamicsLeadsIntegrationService( DynamicsBizFormsMappingConfiguration bizFormMappingConfig, @@ -38,7 +38,7 @@ public DynamicsLeadsIntegrationService( ICRMSyncItemService syncItemService, IFailedSyncItemService failedSyncItemService, IOptionsSnapshot settings, - IEnumerable> converters) + IEnumerable> formsConverters) { this.bizFormMappingConfig = bizFormMappingConfig; this.validationService = validationService; @@ -47,7 +47,7 @@ public DynamicsLeadsIntegrationService( this.syncItemService = syncItemService; this.failedSyncItemService = failedSyncItemService; this.settings = settings; - this.converters = converters; + this.formsConverters = formsConverters; } /// @@ -63,7 +63,7 @@ public async Task SynchronizeLeadAsync(BizFormItem bizFormItem) if (bizFormMappingConfig.FormsConverters.TryGetValue(bizFormItem.BizFormClassName.ToLowerInvariant(), out var formConverters)) { - leadConverters = converters.Where(c => formConverters.Contains(c.GetType())); + leadConverters = formsConverters.Where(c => formConverters.Contains(c.GetType())); } if (bizFormMappingConfig.FormsMappings.TryGetValue(bizFormItem.BizFormClassName.ToLowerInvariant(), @@ -85,7 +85,7 @@ public async Task SynchronizeLeadAsync(BizFormItem bizFormItem) } } - protected async Task SynchronizeLeadAsync(BizFormItem bizFormItem, + private async Task SynchronizeLeadAsync(BizFormItem bizFormItem, IEnumerable fieldMappings, IEnumerable> converters) { try diff --git a/src/Kentico.Xperience.CRM.SalesForce/Configuration/SalesForceBizFormsMappingBuilder.cs b/src/Kentico.Xperience.CRM.SalesForce/Configuration/SalesForceBizFormsMappingBuilder.cs index 8b4f7ea..6372979 100644 --- a/src/Kentico.Xperience.CRM.SalesForce/Configuration/SalesForceBizFormsMappingBuilder.cs +++ b/src/Kentico.Xperience.CRM.SalesForce/Configuration/SalesForceBizFormsMappingBuilder.cs @@ -1,12 +1,87 @@ -using Kentico.Xperience.CRM.Common.Configuration; +using CMS.OnlineForms; +using Kentico.Xperience.CRM.Common.Configuration; +using Kentico.Xperience.CRM.Common.Mapping; +using Kentico.Xperience.CRM.Common.Services; +using Kentico.Xperience.CRM.SalesForce.Converters; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using SalesForce.OpenApi; namespace Kentico.Xperience.CRM.SalesForce.Configuration; -public class SalesForceBizFormsMappingBuilder : BizFormsMappingBuilder +public class SalesForceBizFormsMappingBuilder { - public SalesForceBizFormsMappingBuilder(IServiceCollection serviceCollection) : base(serviceCollection) + private readonly IServiceCollection serviceCollection; + protected readonly Dictionary forms = new(); + protected readonly Dictionary> converters = new(); + + public SalesForceBizFormsMappingBuilder(IServiceCollection serviceCollection) + { + this.serviceCollection = serviceCollection; + } + + public SalesForceBizFormsMappingBuilder AddForm(string formCodeName, + Func configureFields) + { + if (formCodeName is null) throw new ArgumentNullException(nameof(formCodeName)); + + forms.Add(formCodeName.ToLowerInvariant(), configureFields(new BizFormFieldsMappingBuilder())); + return this; + } + + public SalesForceBizFormsMappingBuilder AddForm(string formCodeName, + BizFormFieldsMappingBuilder configuredBuilder) { + if (formCodeName is null) throw new ArgumentNullException(nameof(formCodeName)); + + forms.Add(formCodeName.ToLowerInvariant(), configuredBuilder); + return this; + } + + public SalesForceBizFormsMappingBuilder AddFormWithContactMapping(string formCodeName) + => AddFormWithContactMapping(formCodeName, b => b); + + public SalesForceBizFormsMappingBuilder AddFormWithContactMapping( + string formCodeName, + Func configureFields) + { + if (formCodeName is null) throw new ArgumentNullException(nameof(formCodeName)); + forms.Add(formCodeName.ToLowerInvariant(), configureFields(new BizFormFieldsMappingBuilder())); + + AddFormWithConverter(formCodeName); + return this; + } + + public SalesForceBizFormsMappingBuilder AddFormWithConverter(string formCodeName) + where TConverter : class, ICRMTypeConverter + { + if (formCodeName is null) throw new ArgumentNullException(nameof(formCodeName)); + + if (converters.TryGetValue(formCodeName.ToLowerInvariant(), out var values)) + { + values.Add(typeof(FormContactMappingToLeadConverter)); + } + else + { + converters[formCodeName.ToLowerInvariant()] = new List { typeof(TConverter)}; + } + + serviceCollection.TryAddEnumerable(ServiceDescriptor.Scoped, TConverter>()); + return this; + } + + /// + /// Adds custom service for BizForm item validation before sending to CRM + /// + /// + /// + /// + public SalesForceBizFormsMappingBuilder AddCustomValidation() + where TService : class, ILeadsIntegrationValidationService + { + serviceCollection.AddSingleton(); + + return this; } internal SalesForceBizFormsMappingConfiguration Build() @@ -15,6 +90,7 @@ internal SalesForceBizFormsMappingConfiguration Build() { FormsMappings = forms.Select(f => (f.Key, f.Value.Build())) .ToDictionary(r => r.Key, r => r.Item2), + FormsConverters = converters }; } diff --git a/src/Kentico.Xperience.CRM.SalesForce/Converters/FormContactMappingToLeadConverter.cs b/src/Kentico.Xperience.CRM.SalesForce/Converters/FormContactMappingToLeadConverter.cs new file mode 100644 index 0000000..b952d86 --- /dev/null +++ b/src/Kentico.Xperience.CRM.SalesForce/Converters/FormContactMappingToLeadConverter.cs @@ -0,0 +1,84 @@ +using CMS.ContactManagement; +using CMS.OnlineForms; +using CMS.OnlineForms.Internal; +using Kentico.Xperience.CRM.Common.Mapping; +using SalesForce.OpenApi; + +namespace Kentico.Xperience.CRM.SalesForce.Converters; + +public class FormContactMappingToLeadConverter : ICRMTypeConverter +{ + private readonly IContactFieldFromFormRetriever contactFieldFromFormRetriever; + + public FormContactMappingToLeadConverter(IContactFieldFromFormRetriever contactFieldFromFormRetriever) + { + this.contactFieldFromFormRetriever = contactFieldFromFormRetriever; + } + + public Task Convert(BizFormItem source, LeadSObject destination) + { + var firstName = contactFieldFromFormRetriever.Retrieve(source, nameof(ContactInfo.ContactFirstName)); + if (!string.IsNullOrWhiteSpace(firstName)) + { + destination.FirstName = firstName; + } + + var lastName = contactFieldFromFormRetriever.Retrieve(source, nameof(ContactInfo.ContactLastName)); + if (!string.IsNullOrWhiteSpace(lastName)) + { + destination.LastName = lastName; + } + + var email = contactFieldFromFormRetriever.Retrieve(source, nameof(ContactInfo.ContactEmail)); + if (!string.IsNullOrWhiteSpace(email)) + { + destination.Email = email; + } + + var companyName = contactFieldFromFormRetriever.Retrieve(source, nameof(ContactInfo.ContactCompanyName)); + if (!string.IsNullOrWhiteSpace(companyName)) + { + destination.Company = companyName; + } + + var phone = contactFieldFromFormRetriever.Retrieve(source, nameof(ContactInfo.ContactMobilePhone)); + if (!string.IsNullOrWhiteSpace(phone)) + { + destination.MobilePhone = phone; + } + + var bizPhone = contactFieldFromFormRetriever.Retrieve(source, nameof(ContactInfo.ContactBusinessPhone)); + if (!string.IsNullOrWhiteSpace(bizPhone)) + { + destination.Phone = bizPhone; + } + + var jobTitle = contactFieldFromFormRetriever.Retrieve(source, nameof(ContactInfo.ContactJobTitle)); + if (!string.IsNullOrWhiteSpace(jobTitle)) + { + destination.Title = jobTitle; + } + + var address1 = contactFieldFromFormRetriever.Retrieve(source, nameof(ContactInfo.ContactAddress1)); + if (!string.IsNullOrWhiteSpace(address1)) + { + destination.Street = address1; + } + + var city = contactFieldFromFormRetriever.Retrieve(source, nameof(ContactInfo.ContactCity)); + if (!string.IsNullOrWhiteSpace(city)) + { + destination.City = city; + } + + var zipCode = contactFieldFromFormRetriever.Retrieve(source, nameof(ContactInfo.ContactZIP)); + if (!string.IsNullOrWhiteSpace(zipCode)) + { + destination.PostalCode = zipCode; + } + + //@TODO country, state + + return Task.FromResult(destination); + } +} \ No newline at end of file diff --git a/src/Kentico.Xperience.CRM.SalesForce/SalesForceServiceCollectionsExtensions.cs b/src/Kentico.Xperience.CRM.SalesForce/SalesForceServiceCollectionsExtensions.cs index 247de54..6ff8137 100644 --- a/src/Kentico.Xperience.CRM.SalesForce/SalesForceServiceCollectionsExtensions.cs +++ b/src/Kentico.Xperience.CRM.SalesForce/SalesForceServiceCollectionsExtensions.cs @@ -29,25 +29,22 @@ public static IServiceCollection AddSalesForceFormLeadsIntegration(this IService IConfiguration configuration) { serviceCollection.AddKenticoCrmCommonFormLeadsIntegration(); + + var mappingBuilder = new SalesForceBizFormsMappingBuilder(serviceCollection); + formsConfig(mappingBuilder); serviceCollection.TryAddSingleton( - _ => - { - var mappingBuilder = new SalesForceBizFormsMappingBuilder(serviceCollection); - formsConfig(mappingBuilder); - return mappingBuilder.Build(); - }); + _ => mappingBuilder.Build()); serviceCollection.AddOptions().Bind(configuration) .PostConfigure(ConfigureWithCMSSettings); - AddSalesForceCommonIntegration(serviceCollection, configuration); + AddSalesForceCommonIntegration(serviceCollection); serviceCollection.AddScoped(); return serviceCollection; } - private static void AddSalesForceCommonIntegration(IServiceCollection serviceCollection, - IConfiguration configuration) + private static void AddSalesForceCommonIntegration(IServiceCollection serviceCollection) { // default cache for token management serviceCollection.AddDistributedMemoryCache(); diff --git a/src/Kentico.Xperience.CRM.SalesForce/Services/SalesForceLeadsIntegrationService.cs b/src/Kentico.Xperience.CRM.SalesForce/Services/SalesForceLeadsIntegrationService.cs index cef518e..63dfe46 100644 --- a/src/Kentico.Xperience.CRM.SalesForce/Services/SalesForceLeadsIntegrationService.cs +++ b/src/Kentico.Xperience.CRM.SalesForce/Services/SalesForceLeadsIntegrationService.cs @@ -1,5 +1,6 @@ using CMS.OnlineForms; using Kentico.Xperience.CRM.Common.Constants; +using Kentico.Xperience.CRM.Common.Mapping; using Kentico.Xperience.CRM.Common.Mapping.Implementations; using Kentico.Xperience.CRM.Common.Services; using Kentico.Xperience.CRM.Common.Services.Implementations; @@ -11,14 +12,16 @@ namespace Kentico.Xperience.CRM.SalesForce.Services; -internal class SalesForceLeadsIntegrationService : LeadsIntegrationServiceCommon, ISalesForceLeadsIntegrationService +internal class SalesForceLeadsIntegrationService : ISalesForceLeadsIntegrationService { private readonly SalesForceBizFormsMappingConfiguration bizFormMappingConfig; + private readonly ILeadsIntegrationValidationService validationService; private readonly ISalesForceApiService apiService; private readonly ILogger logger; private readonly ICRMSyncItemService syncItemService; private readonly IFailedSyncItemService failedSyncItemService; private readonly IOptionsSnapshot settings; + private readonly IEnumerable> formsConverters; public SalesForceLeadsIntegrationService( SalesForceBizFormsMappingConfiguration bizFormMappingConfig, @@ -27,19 +30,57 @@ public SalesForceLeadsIntegrationService( ILogger logger, ICRMSyncItemService syncItemService, IFailedSyncItemService failedSyncItemService, - IOptionsSnapshot settings) - : base(bizFormMappingConfig, validationService, logger) + IOptionsSnapshot settings, + IEnumerable> formsConverters) { this.bizFormMappingConfig = bizFormMappingConfig; + this.validationService = validationService; this.apiService = apiService; this.logger = logger; this.syncItemService = syncItemService; this.failedSyncItemService = failedSyncItemService; this.settings = settings; + this.formsConverters = formsConverters; } - protected override async Task SynchronizeLeadAsync(BizFormItem bizFormItem, - IEnumerable fieldMappings) + /// + /// Validates BizForm item, then get specific mapping and finally specific implementation is called + /// from inherited service + /// + /// + public async Task SynchronizeLeadAsync(BizFormItem bizFormItem) + { + var leadConverters = Enumerable.Empty>(); + var leadMapping = Enumerable.Empty(); + + if (bizFormMappingConfig.FormsConverters.TryGetValue(bizFormItem.BizFormClassName.ToLowerInvariant(), + out var formConverters)) + { + leadConverters = formsConverters.Where(c => formConverters.Contains(c.GetType())); + } + + if (bizFormMappingConfig.FormsMappings.TryGetValue(bizFormItem.BizFormClassName.ToLowerInvariant(), + out var formMapping)) + { + leadMapping = formMapping; + } + + if (leadConverters.Any() || leadMapping.Any()) + { + if (!await validationService.ValidateFormItem(bizFormItem)) + { + logger.LogInformation("BizForm item {ItemID} for {BizFormDisplayName} refused by validation", + bizFormItem.ItemID, bizFormItem.BizFormInfo.FormDisplayName); + return; + } + + await SynchronizeLeadAsync(bizFormItem, leadMapping, leadConverters); + } + } + + private async Task SynchronizeLeadAsync(BizFormItem bizFormItem, + IEnumerable fieldMappings, + IEnumerable> converters) { try { @@ -47,18 +88,18 @@ protected override async Task SynchronizeLeadAsync(BizFormItem bizFormItem, if (syncItem is null) { - await UpdateByEmailOrCreate(bizFormItem, fieldMappings); + await UpdateByEmailOrCreate(bizFormItem, fieldMappings, converters); } else { var existingLead = await apiService.GetLeadById(syncItem.CRMSyncItemCRMID, nameof(LeadSObject.Id)); if (existingLead is null) { - await UpdateByEmailOrCreate(bizFormItem, fieldMappings); + await UpdateByEmailOrCreate(bizFormItem, fieldMappings, converters); } else if (!settings.Value.IgnoreExistingRecords) { - await UpdateLeadAsync(existingLead.Id!, bizFormItem, fieldMappings); + await UpdateLeadAsync(existingLead.Id!, bizFormItem, fieldMappings, converters); } } } @@ -79,13 +120,14 @@ protected override async Task SynchronizeLeadAsync(BizFormItem bizFormItem, } } - private async Task UpdateByEmailOrCreate(BizFormItem bizFormItem, IEnumerable fieldMappings) + private async Task UpdateByEmailOrCreate(BizFormItem bizFormItem, IEnumerable fieldMappings, + IEnumerable> converters) { string? existingLeadId = null; - + var tmpLead = new LeadSObject(); - MapLead(bizFormItem, tmpLead, fieldMappings); - + MapLead(bizFormItem, tmpLead, fieldMappings, converters); + if (!string.IsNullOrWhiteSpace(tmpLead.Email)) { existingLeadId = await apiService.GetLeadByEmail(tmpLead.Email); @@ -93,18 +135,19 @@ private async Task UpdateByEmailOrCreate(BizFormItem bizFormItem, IEnumerable fieldMappings) + private async Task CreateLeadAsync(BizFormItem bizFormItem, IEnumerable fieldMappings, + IEnumerable> converters) { var lead = new LeadSObject(); - MapLead(bizFormItem, lead, fieldMappings); + MapLead(bizFormItem, lead, fieldMappings, converters); lead.LeadSource ??= $"Form {bizFormItem.BizFormInfo.FormDisplayName} - ID: {bizFormItem.ItemID}"; lead.Company ??= "undefined"; //required field - set to 'undefined' to prevent errors @@ -117,23 +160,29 @@ private async Task CreateLeadAsync(BizFormItem bizFormItem, IEnumerable fieldMappings) + IEnumerable fieldMappings, + IEnumerable> converters) { var lead = new LeadSObject(); - MapLead(bizFormItem, lead, fieldMappings); - + MapLead(bizFormItem, lead, fieldMappings, converters); + lead.LeadSource ??= $"Form {bizFormItem.BizFormInfo.FormDisplayName} - ID: {bizFormItem.ItemID}"; await apiService.UpdateLeadAsync(leadId, lead); - + syncItemService.LogFormLeadUpdateItem(bizFormItem, leadId, CRMType.SalesForce); failedSyncItemService.DeleteFailedSyncItem(CRMType.SalesForce, bizFormItem.BizFormClassName, bizFormItem.ItemID); } - protected virtual void MapLead(BizFormItem bizFormItem, LeadSObject lead, - IEnumerable fieldMappings) + private async Task MapLead(BizFormItem bizFormItem, LeadSObject lead, + IEnumerable fieldMappings, IEnumerable> converters) { + foreach (var converter in converters) + { + await converter.Convert(bizFormItem, lead); + } + foreach (var fieldMapping in fieldMappings) { var formFieldValue = fieldMapping.FormFieldMapping.MapFormField(bizFormItem); From ba9e6e76e2636bbe37f624b08af37a29f987451f Mon Sep 17 00:00:00 2001 From: Martin Foukal Date: Thu, 11 Jan 2024 00:53:31 +0100 Subject: [PATCH 09/23] configuration simplify --- examples/DancingGoat/Program.cs | 17 +++++++---------- .../DynamicsServiceCollectionExtensions.cs | 15 ++++++++++++--- .../SalesForceServiceCollectionsExtensions.cs | 14 +++++++++++--- 3 files changed, 30 insertions(+), 16 deletions(-) diff --git a/examples/DancingGoat/Program.cs b/examples/DancingGoat/Program.cs index 29ef315..7f83668 100644 --- a/examples/DancingGoat/Program.cs +++ b/examples/DancingGoat/Program.cs @@ -65,11 +65,9 @@ // builder.Configuration.GetSection(DynamicsIntegrationSettings.ConfigKeyName)); //config section with settings builder.Services.AddDynamicsFormLeadsIntegration(builder => - builder.AddFormWithContactMapping(DancingGoatContactUsItem.CLASS_NAME,b => b - .MapField(c => c.UserMessage, e => e.EMailAddress1)) - .AddCustomValidation() //optional - , - builder.Configuration.GetSection(DynamicsIntegrationSettings.ConfigKeyName)); //config section with settings + builder.AddFormWithContactMapping(DancingGoatContactUsItem.CLASS_NAME, b => b + .MapField(c => c.UserMessage, e => e.EMailAddress1)) + .AddCustomValidation()); //optional // builder.Services.AddSalesForceFormLeadsIntegration(builder => // builder.AddForm(DancingGoatContactUsItem.CLASS_NAME, //form class name @@ -85,11 +83,10 @@ // builder.Configuration.GetSection(SalesForceIntegrationSettings.ConfigKeyName)); //config section with settings builder.Services.AddSalesForceFormLeadsIntegration(builder => - builder.AddFormWithContactMapping(DancingGoatContactUsItem.CLASS_NAME,b => b - .MapField(c => c.UserMessage, e => e.Description)) - .AddCustomValidation() //optional - , - builder.Configuration.GetSection(DynamicsIntegrationSettings.ConfigKeyName)); //config section with settings + builder.AddFormWithContactMapping(DancingGoatContactUsItem.CLASS_NAME, b => b + .MapField(c => c.UserMessage, e => e.Description)) + .AddCustomValidation()); //optional + //CRM integration registration end diff --git a/src/Kentico.Xperience.CRM.Dynamics/DynamicsServiceCollectionExtensions.cs b/src/Kentico.Xperience.CRM.Dynamics/DynamicsServiceCollectionExtensions.cs index 0d192e9..12a9664 100644 --- a/src/Kentico.Xperience.CRM.Dynamics/DynamicsServiceCollectionExtensions.cs +++ b/src/Kentico.Xperience.CRM.Dynamics/DynamicsServiceCollectionExtensions.cs @@ -25,15 +25,24 @@ public static class DynamicsServiceCollectionExtensions /// public static IServiceCollection AddDynamicsFormLeadsIntegration(this IServiceCollection serviceCollection, Action formsConfig, - IConfiguration configuration) + IConfiguration? configuration = null) { serviceCollection.AddKenticoCrmCommonFormLeadsIntegration(); var mappingBuilder = new DynamicsBizFormsMappingBuilder(serviceCollection); formsConfig(mappingBuilder); serviceCollection.TryAddSingleton(_ => mappingBuilder.Build()); - serviceCollection.AddOptions().Bind(configuration) - .PostConfigure(ConfigureWithCMSSettings); + if (configuration is null) + { + serviceCollection.AddOptions() + .Configure(ConfigureWithCMSSettings); + } + else + { + serviceCollection.AddOptions().Bind(configuration) + .PostConfigure(ConfigureWithCMSSettings); + } + serviceCollection.TryAddScoped(GetCrmServiceClient); serviceCollection.AddScoped(); return serviceCollection; diff --git a/src/Kentico.Xperience.CRM.SalesForce/SalesForceServiceCollectionsExtensions.cs b/src/Kentico.Xperience.CRM.SalesForce/SalesForceServiceCollectionsExtensions.cs index 6ff8137..42b6bfa 100644 --- a/src/Kentico.Xperience.CRM.SalesForce/SalesForceServiceCollectionsExtensions.cs +++ b/src/Kentico.Xperience.CRM.SalesForce/SalesForceServiceCollectionsExtensions.cs @@ -26,7 +26,7 @@ public static class SalesForceServiceCollectionsExtensions /// public static IServiceCollection AddSalesForceFormLeadsIntegration(this IServiceCollection serviceCollection, Action formsConfig, - IConfiguration configuration) + IConfiguration? configuration = null) { serviceCollection.AddKenticoCrmCommonFormLeadsIntegration(); @@ -35,8 +35,16 @@ public static IServiceCollection AddSalesForceFormLeadsIntegration(this IService serviceCollection.TryAddSingleton( _ => mappingBuilder.Build()); - serviceCollection.AddOptions().Bind(configuration) - .PostConfigure(ConfigureWithCMSSettings); + if (configuration is null) + { + serviceCollection.AddOptions() + .Configure(ConfigureWithCMSSettings); + } + else + { + serviceCollection.AddOptions().Bind(configuration) + .PostConfigure(ConfigureWithCMSSettings); + } AddSalesForceCommonIntegration(serviceCollection); From a19fefcb124bba581b286cfa0873b630fbc5209e Mon Sep 17 00:00:00 2001 From: Martin Foukal Date: Fri, 12 Jan 2024 15:09:12 +0100 Subject: [PATCH 10/23] comments --- .../Admin/CRMSyncItemListing.cs | 3 ++ .../CommonIntegrationSettings.cs | 9 ++++ .../Constants/SettingKeys.cs | 9 +++- .../Installers/CRMModuleInstaller.cs | 25 +++++++-- .../ServiceCollectionExtensions.cs | 4 +- .../DynamicsBizFormsMappingBuilder.cs | 50 +++++++++++++---- .../FormContactMappingToLeadConverter.cs | 3 ++ .../DynamicsServiceCollectionExtensions.cs | 45 ++++++---------- .../SalesForceBizFormsMappingBuilder.cs | 53 ++++++++++++------- .../FormContactMappingToLeadConverter.cs | 3 ++ .../Models/QueryResult.cs | 4 ++ .../Models/QueryResultBase.cs | 3 ++ .../SalesForceServiceCollectionsExtensions.cs | 31 +++-------- .../Services/ISalesForceApiService.cs | 4 +- 14 files changed, 155 insertions(+), 91 deletions(-) diff --git a/src/Kentico.Xperience.CRM.Common/Admin/CRMSyncItemListing.cs b/src/Kentico.Xperience.CRM.Common/Admin/CRMSyncItemListing.cs index 90fa64f..6e9ed24 100644 --- a/src/Kentico.Xperience.CRM.Common/Admin/CRMSyncItemListing.cs +++ b/src/Kentico.Xperience.CRM.Common/Admin/CRMSyncItemListing.cs @@ -13,6 +13,9 @@ namespace Kentico.Xperience.CRM.Common.Admin; +/// +/// Admin listing page for displaying synced items in CMS for selected form +/// public class CRMSyncItemListing : ListingPage { private BizFormInfo? editedForm; diff --git a/src/Kentico.Xperience.CRM.Common/Configuration/CommonIntegrationSettings.cs b/src/Kentico.Xperience.CRM.Common/Configuration/CommonIntegrationSettings.cs index 73180da..4848c7f 100644 --- a/src/Kentico.Xperience.CRM.Common/Configuration/CommonIntegrationSettings.cs +++ b/src/Kentico.Xperience.CRM.Common/Configuration/CommonIntegrationSettings.cs @@ -6,12 +6,21 @@ /// public class CommonIntegrationSettings where TApiConfig : new() { + /// + /// If enabled BizForm leads synchronization + /// public bool FormLeadsEnabled { get; set; } // @TODO phase 2 public bool ContactsEnabled { get; set; } + /// + /// If true no existing item with same email or paired record by ID is updated + /// public bool IgnoreExistingRecords { get; set; } + /// + /// Specific CRM API configuration + /// public TApiConfig ApiConfig { get; set; } = new(); } \ No newline at end of file diff --git a/src/Kentico.Xperience.CRM.Common/Constants/SettingKeys.cs b/src/Kentico.Xperience.CRM.Common/Constants/SettingKeys.cs index a8bb3df..4b825af 100644 --- a/src/Kentico.Xperience.CRM.Common/Constants/SettingKeys.cs +++ b/src/Kentico.Xperience.CRM.Common/Constants/SettingKeys.cs @@ -1,14 +1,19 @@ namespace Kentico.Xperience.CRM.Common.Constants; +/// +/// CMS settings keys +/// public class SettingKeys { public const string DynamicsFormLeadsEnabled = "CMSDynamicsCRMIntegrationFormLeadsEnabled"; public const string DynamicsUrl = "CMSDynamicsCRMIntegrationDynamicsUrl"; public const string DynamicsClientId = "CMSDynamicsCRMIntegrationClientId"; - public const string DynamicsClientSecret = "CMSDynamicsCRMIntegrationDynamicsClientSecret"; + public const string DynamicsClientSecret = "CMSDynamicsCRMIntegrationClientSecret"; + public const string DynamicsIgnoreExistingRecords = "CMSDynamicsCRMIgnoreExistingRecords"; public const string SalesForceFormLeadsEnabled = "CMSSalesforceCRMIntegrationFormLeadsEnabled"; public const string SalesForceUrl = "CMSSalesforceCRMIntegrationSalesforceUrl"; public const string SalesForceClientId = "CMSSalesforceCRMIntegrationClientId"; - public const string SalesForceClientSecret = "CMSSalesforceCRMIntegrationSalesforceClientSecret"; + public const string SalesForceClientSecret = "CMSSalesforceCRMIntegrationClientSecret"; + public const string SalesForceIgnoreExistingRecords = "CMSSalesforceCRMIntegrationIgnoreExistingRecords"; } \ No newline at end of file diff --git a/src/Kentico.Xperience.CRM.Common/Installers/CRMModuleInstaller.cs b/src/Kentico.Xperience.CRM.Common/Installers/CRMModuleInstaller.cs index a72f745..d7e2eb0 100644 --- a/src/Kentico.Xperience.CRM.Common/Installers/CRMModuleInstaller.cs +++ b/src/Kentico.Xperience.CRM.Common/Installers/CRMModuleInstaller.cs @@ -10,8 +10,10 @@ namespace Kentico.Xperience.CRM.Common.Installers; /// /// This installer creates custom module for common crm functionality -/// Currently this module contains only custom class for failed synchronizations items -/// which is created when not exists on start. +/// Currently this module contains custom class for failed synchronizations items +/// and synced items class +/// Custom settings are created +/// Objects are created when not exists on start. /// public class CRMModuleInstaller : ICRMModuleInstaller { @@ -314,7 +316,7 @@ private void InstallSettings(ResourceInfo resourceInfo, string crmType) SettingsKeyInfo.Provider.Set(settingClientId); } - var settingClientSecret = SettingsKeyInfo.Provider.Get($"CMS{crmType}CRMIntegration{crmType}ClientSecret"); + var settingClientSecret = SettingsKeyInfo.Provider.Get($"CMS{crmType}CRMIntegrationClientSecret"); if (settingClientSecret is null) { settingClientSecret = new SettingsKeyInfo @@ -330,5 +332,22 @@ private void InstallSettings(ResourceInfo resourceInfo, string crmType) SettingsKeyInfo.Provider.Set(settingClientSecret); } + + var settingsIgnoreExisting = SettingsKeyInfo.Provider.Get($"CMS{crmType}CRMIntegrationIgnoreExistingRecords"); + if (settingsIgnoreExisting is null) + { + settingsIgnoreExisting = new SettingsKeyInfo + { + KeyName = $"CMS{crmType}CRMIntegration{crmType}ClientSecret", + KeyDisplayName = "Ignore existing records", + KeyDescription = "", + KeyType = "string", + KeyCategoryID = crmCategory.CategoryID, + KeyIsCustom = true, + KeyExplanationText = "", + }; + + SettingsKeyInfo.Provider.Set(settingsIgnoreExisting); + } } } \ No newline at end of file diff --git a/src/Kentico.Xperience.CRM.Common/ServiceCollectionExtensions.cs b/src/Kentico.Xperience.CRM.Common/ServiceCollectionExtensions.cs index 91dbfd3..0af82a1 100644 --- a/src/Kentico.Xperience.CRM.Common/ServiceCollectionExtensions.cs +++ b/src/Kentico.Xperience.CRM.Common/ServiceCollectionExtensions.cs @@ -1,6 +1,4 @@ -using Kentico.Xperience.CRM.Common.Classes; -using Kentico.Xperience.CRM.Common.Configuration; -using Kentico.Xperience.CRM.Common.Installers; +using Kentico.Xperience.CRM.Common.Installers; using Kentico.Xperience.CRM.Common.Services; using Kentico.Xperience.CRM.Common.Services.Implementations; using Microsoft.Extensions.DependencyInjection; diff --git a/src/Kentico.Xperience.CRM.Dynamics/Configuration/DynamicsBizFormsMappingBuilder.cs b/src/Kentico.Xperience.CRM.Dynamics/Configuration/DynamicsBizFormsMappingBuilder.cs index e2ae5ad..0618078 100644 --- a/src/Kentico.Xperience.CRM.Dynamics/Configuration/DynamicsBizFormsMappingBuilder.cs +++ b/src/Kentico.Xperience.CRM.Dynamics/Configuration/DynamicsBizFormsMappingBuilder.cs @@ -11,29 +11,52 @@ namespace Kentico.Xperience.CRM.Dynamics.Configuration; +/// +/// Mapping builder for BizForm to Leads mapping +/// public class DynamicsBizFormsMappingBuilder { private readonly IServiceCollection serviceCollection; protected readonly Dictionary forms = new(); protected readonly Dictionary> converters = new(); - + public DynamicsBizFormsMappingBuilder(IServiceCollection serviceCollection) { this.serviceCollection = serviceCollection; } + /// + /// Adds Form with mapping + /// + /// + /// + /// + /// public DynamicsBizFormsMappingBuilder AddForm(string formCodeName, Func configureFields) { if (formCodeName is null) throw new ArgumentNullException(nameof(formCodeName)); forms.Add(formCodeName.ToLowerInvariant(), configureFields(new BizFormFieldsMappingBuilder())); - + return this; } - public DynamicsBizFormsMappingBuilder AddFormWithContactMapping(string formCodeName) + /// + /// Adds form when conversion is added automatically based on Form-Contact mapping + /// + /// + /// + public DynamicsBizFormsMappingBuilder AddFormWithContactMapping(string formCodeName) => AddFormWithContactMapping(formCodeName, b => b); + /// + /// Adds form when conversion is added automatically based on Form-Contact mapping + /// with custom mapping combined + /// + /// + /// + /// + /// public DynamicsBizFormsMappingBuilder AddFormWithContactMapping( string formCodeName, Func configureFields) @@ -45,24 +68,33 @@ public DynamicsBizFormsMappingBuilder AddFormWithContactMapping( return this; } + /// + /// Adds form with custom converter. Use this method when you want to have full control. You can add multiple + /// converters for same form + /// + /// + /// + /// + /// public DynamicsBizFormsMappingBuilder AddFormWithConverter(string formCodeName) where TConverter : class, ICRMTypeConverter { if (formCodeName is null) throw new ArgumentNullException(nameof(formCodeName)); - + if (converters.TryGetValue(formCodeName.ToLowerInvariant(), out var values)) { values.Add(typeof(FormContactMappingToLeadConverter)); } else { - converters[formCodeName.ToLowerInvariant()] = new List { typeof(TConverter)}; + converters[formCodeName.ToLowerInvariant()] = new List { typeof(TConverter) }; } - - serviceCollection.TryAddEnumerable(ServiceDescriptor.Scoped, TConverter>()); + + serviceCollection.TryAddEnumerable(ServiceDescriptor + .Scoped, TConverter>()); return this; } - + /// /// Adds custom service for BizForm item validation before sending to CRM /// @@ -76,7 +108,7 @@ public DynamicsBizFormsMappingBuilder AddCustomValidation() return this; } - + internal DynamicsBizFormsMappingConfiguration Build() { return new DynamicsBizFormsMappingConfiguration diff --git a/src/Kentico.Xperience.CRM.Dynamics/Converters/FormContactMappingToLeadConverter.cs b/src/Kentico.Xperience.CRM.Dynamics/Converters/FormContactMappingToLeadConverter.cs index 4bbf0b0..349441d 100644 --- a/src/Kentico.Xperience.CRM.Dynamics/Converters/FormContactMappingToLeadConverter.cs +++ b/src/Kentico.Xperience.CRM.Dynamics/Converters/FormContactMappingToLeadConverter.cs @@ -6,6 +6,9 @@ namespace Kentico.Xperience.CRM.Dynamics.Converters; +/// +/// Converter for mapping BizForm to Lead based on Form-Contact mapping in CMS +/// public class FormContactMappingToLeadConverter : ICRMTypeConverter { private readonly IContactFieldFromFormRetriever contactFieldFromFormRetriever; diff --git a/src/Kentico.Xperience.CRM.Dynamics/DynamicsServiceCollectionExtensions.cs b/src/Kentico.Xperience.CRM.Dynamics/DynamicsServiceCollectionExtensions.cs index 12a9664..5d96f6b 100644 --- a/src/Kentico.Xperience.CRM.Dynamics/DynamicsServiceCollectionExtensions.cs +++ b/src/Kentico.Xperience.CRM.Dynamics/DynamicsServiceCollectionExtensions.cs @@ -35,14 +35,13 @@ public static IServiceCollection AddDynamicsFormLeadsIntegration(this IServiceCo if (configuration is null) { serviceCollection.AddOptions() - .Configure(ConfigureWithCMSSettings); + .Configure(ConfigureWithCMSSettings); } else { - serviceCollection.AddOptions().Bind(configuration) - .PostConfigure(ConfigureWithCMSSettings); + serviceCollection.AddOptions().Bind(configuration); } - + serviceCollection.TryAddScoped(GetCrmServiceClient); serviceCollection.AddScoped(); return serviceCollection; @@ -64,37 +63,23 @@ private static ServiceClient GetCrmServiceClient(IServiceProvider serviceProvide throw new InvalidOperationException("Missing API setting"); } - var connectionString = string.IsNullOrWhiteSpace(settings.ApiConfig.ConnectionString) - ? $"AuthType=ClientSecret;Url={settings.ApiConfig.DynamicsUrl};ClientId={settings.ApiConfig.ClientId};ClientSecret={settings.ApiConfig.ClientSecret}" - : settings.ApiConfig.ConnectionString; + var connectionString = string.IsNullOrWhiteSpace(settings.ApiConfig.ConnectionString) ? + $"AuthType=ClientSecret;Url={settings.ApiConfig.DynamicsUrl};ClientId={settings.ApiConfig.ClientId};ClientSecret={settings.ApiConfig.ClientSecret}" : + settings.ApiConfig.ConnectionString; return new ServiceClient(connectionString, logger); } private static void ConfigureWithCMSSettings(DynamicsIntegrationSettings settings, ISettingsService settingsService) { - var formsEnabled = settingsService[SettingKeys.DynamicsFormLeadsEnabled]; - if (!string.IsNullOrWhiteSpace(formsEnabled)) - { - settings.FormLeadsEnabled = ValidationHelper.GetBoolean(formsEnabled, false); - } - - var dynamicsUrl = settingsService[SettingKeys.DynamicsUrl]; - if (!string.IsNullOrWhiteSpace(dynamicsUrl)) - { - settings.ApiConfig.DynamicsUrl = dynamicsUrl; - } - - var clientId = settingsService[SettingKeys.DynamicsClientId]; - if (!string.IsNullOrWhiteSpace(clientId)) - { - settings.ApiConfig.ClientId = clientId; - } - - var clientSecret = settingsService[SettingKeys.DynamicsClientSecret]; - if (!string.IsNullOrWhiteSpace(clientSecret)) - { - settings.ApiConfig.ClientSecret = clientSecret; - } + settings.FormLeadsEnabled = + ValidationHelper.GetBoolean(settingsService[SettingKeys.DynamicsFormLeadsEnabled], false); + + settings.IgnoreExistingRecords = + ValidationHelper.GetBoolean(settingsService[SettingKeys.DynamicsIgnoreExistingRecords], false); + + settings.ApiConfig.DynamicsUrl = settingsService[SettingKeys.DynamicsUrl]; + settings.ApiConfig.ClientId = settingsService[SettingKeys.DynamicsClientId]; + settings.ApiConfig.ClientSecret = settingsService[SettingKeys.DynamicsClientSecret]; } } \ No newline at end of file diff --git a/src/Kentico.Xperience.CRM.SalesForce/Configuration/SalesForceBizFormsMappingBuilder.cs b/src/Kentico.Xperience.CRM.SalesForce/Configuration/SalesForceBizFormsMappingBuilder.cs index 6372979..12ac536 100644 --- a/src/Kentico.Xperience.CRM.SalesForce/Configuration/SalesForceBizFormsMappingBuilder.cs +++ b/src/Kentico.Xperience.CRM.SalesForce/Configuration/SalesForceBizFormsMappingBuilder.cs @@ -9,26 +9,28 @@ namespace Kentico.Xperience.CRM.SalesForce.Configuration; +/// +/// Mapping builder for BizForm to SalesForce mapping +/// public class SalesForceBizFormsMappingBuilder { private readonly IServiceCollection serviceCollection; protected readonly Dictionary forms = new(); protected readonly Dictionary> converters = new(); - + public SalesForceBizFormsMappingBuilder(IServiceCollection serviceCollection) { this.serviceCollection = serviceCollection; } - public SalesForceBizFormsMappingBuilder AddForm(string formCodeName, - Func configureFields) - { - if (formCodeName is null) throw new ArgumentNullException(nameof(formCodeName)); - - forms.Add(formCodeName.ToLowerInvariant(), configureFields(new BizFormFieldsMappingBuilder())); - return this; - } - + /// + /// Adds form when conversion is added automatically based on Form-Contact mapping + /// with custom mapping combined + /// + /// + /// + /// + /// public SalesForceBizFormsMappingBuilder AddForm(string formCodeName, BizFormFieldsMappingBuilder configuredBuilder) { @@ -37,8 +39,13 @@ public SalesForceBizFormsMappingBuilder AddForm(string formCodeName, forms.Add(formCodeName.ToLowerInvariant(), configuredBuilder); return this; } - - public SalesForceBizFormsMappingBuilder AddFormWithContactMapping(string formCodeName) + + /// + /// Adds form when conversion is added automatically based on Form-Contact mapping + /// + /// + /// + public SalesForceBizFormsMappingBuilder AddFormWithContactMapping(string formCodeName) => AddFormWithContactMapping(formCodeName, b => b); public SalesForceBizFormsMappingBuilder AddFormWithContactMapping( @@ -52,24 +59,33 @@ public SalesForceBizFormsMappingBuilder AddFormWithContactMapping( return this; } + /// + /// Adds form when conversion is added automatically based on Form-Contact mapping + /// with custom mapping combined + /// + /// + /// + /// + /// public SalesForceBizFormsMappingBuilder AddFormWithConverter(string formCodeName) where TConverter : class, ICRMTypeConverter { if (formCodeName is null) throw new ArgumentNullException(nameof(formCodeName)); - + if (converters.TryGetValue(formCodeName.ToLowerInvariant(), out var values)) { values.Add(typeof(FormContactMappingToLeadConverter)); } else { - converters[formCodeName.ToLowerInvariant()] = new List { typeof(TConverter)}; + converters[formCodeName.ToLowerInvariant()] = new List { typeof(TConverter) }; } - - serviceCollection.TryAddEnumerable(ServiceDescriptor.Scoped, TConverter>()); + + serviceCollection.TryAddEnumerable(ServiceDescriptor + .Scoped, TConverter>()); return this; } - + /// /// Adds custom service for BizForm item validation before sending to CRM /// @@ -83,7 +99,7 @@ public SalesForceBizFormsMappingBuilder AddCustomValidation() return this; } - + internal SalesForceBizFormsMappingConfiguration Build() { return new SalesForceBizFormsMappingConfiguration @@ -93,5 +109,4 @@ internal SalesForceBizFormsMappingConfiguration Build() FormsConverters = converters }; } - } \ No newline at end of file diff --git a/src/Kentico.Xperience.CRM.SalesForce/Converters/FormContactMappingToLeadConverter.cs b/src/Kentico.Xperience.CRM.SalesForce/Converters/FormContactMappingToLeadConverter.cs index b952d86..91a0dbe 100644 --- a/src/Kentico.Xperience.CRM.SalesForce/Converters/FormContactMappingToLeadConverter.cs +++ b/src/Kentico.Xperience.CRM.SalesForce/Converters/FormContactMappingToLeadConverter.cs @@ -6,6 +6,9 @@ namespace Kentico.Xperience.CRM.SalesForce.Converters; +/// +/// Converter for mapping BizForm to Lead based on Form-Contact mapping in CMS +/// public class FormContactMappingToLeadConverter : ICRMTypeConverter { private readonly IContactFieldFromFormRetriever contactFieldFromFormRetriever; diff --git a/src/Kentico.Xperience.CRM.SalesForce/Models/QueryResult.cs b/src/Kentico.Xperience.CRM.SalesForce/Models/QueryResult.cs index 0a1db51..e0747b8 100644 --- a/src/Kentico.Xperience.CRM.SalesForce/Models/QueryResult.cs +++ b/src/Kentico.Xperience.CRM.SalesForce/Models/QueryResult.cs @@ -2,6 +2,10 @@ namespace Kentico.Xperience.CRM.SalesForce.Models; +/// +/// Query result +/// +/// public class QueryResult : QueryResultBase { [JsonPropertyName("records")] public List Records { get; set; } = new(); diff --git a/src/Kentico.Xperience.CRM.SalesForce/Models/QueryResultBase.cs b/src/Kentico.Xperience.CRM.SalesForce/Models/QueryResultBase.cs index 2e6b226..86d0c6b 100644 --- a/src/Kentico.Xperience.CRM.SalesForce/Models/QueryResultBase.cs +++ b/src/Kentico.Xperience.CRM.SalesForce/Models/QueryResultBase.cs @@ -2,6 +2,9 @@ namespace Kentico.Xperience.CRM.SalesForce.Models; +/// +/// Base model for query result +/// public class QueryResultBase { [JsonPropertyName("totalSize")] public int TotalSize { get; set; } diff --git a/src/Kentico.Xperience.CRM.SalesForce/SalesForceServiceCollectionsExtensions.cs b/src/Kentico.Xperience.CRM.SalesForce/SalesForceServiceCollectionsExtensions.cs index 42b6bfa..7cb0f4f 100644 --- a/src/Kentico.Xperience.CRM.SalesForce/SalesForceServiceCollectionsExtensions.cs +++ b/src/Kentico.Xperience.CRM.SalesForce/SalesForceServiceCollectionsExtensions.cs @@ -42,8 +42,7 @@ public static IServiceCollection AddSalesForceFormLeadsIntegration(this IService } else { - serviceCollection.AddOptions().Bind(configuration) - .PostConfigure(ConfigureWithCMSSettings); + serviceCollection.AddOptions().Bind(configuration); } AddSalesForceCommonIntegration(serviceCollection); @@ -95,28 +94,14 @@ private static void AddSalesForceCommonIntegration(IServiceCollection serviceCol private static void ConfigureWithCMSSettings(SalesForceIntegrationSettings settings, ISettingsService settingsService) { - var formsEnabled = settingsService[SettingKeys.SalesForceFormLeadsEnabled]; - if (!string.IsNullOrWhiteSpace(formsEnabled)) - { - settings.FormLeadsEnabled = ValidationHelper.GetBoolean(formsEnabled, false); - } + settings.FormLeadsEnabled = + ValidationHelper.GetBoolean(settingsService[SettingKeys.SalesForceFormLeadsEnabled], false); - var salesForceUrl = settingsService[SettingKeys.SalesForceUrl]; - if (!string.IsNullOrWhiteSpace(salesForceUrl)) - { - settings.ApiConfig.SalesForceUrl = salesForceUrl; - } + settings.IgnoreExistingRecords = + ValidationHelper.GetBoolean(settingsService[SettingKeys.SalesForceIgnoreExistingRecords], false); - var clientId = settingsService[SettingKeys.SalesForceClientId]; - if (!string.IsNullOrWhiteSpace(clientId)) - { - settings.ApiConfig.ClientId = clientId; - } - - var clientSecret = settingsService[SettingKeys.SalesForceClientSecret]; - if (!string.IsNullOrWhiteSpace(clientSecret)) - { - settings.ApiConfig.ClientSecret = clientSecret; - } + settings.ApiConfig.SalesForceUrl = settingsService[SettingKeys.SalesForceUrl]; + settings.ApiConfig.ClientId = settingsService[SettingKeys.SalesForceClientId]; + settings.ApiConfig.ClientSecret = settingsService[SettingKeys.SalesForceClientSecret]; } } \ No newline at end of file diff --git a/src/Kentico.Xperience.CRM.SalesForce/Services/ISalesForceApiService.cs b/src/Kentico.Xperience.CRM.SalesForce/Services/ISalesForceApiService.cs index 718a0b1..eec8d28 100644 --- a/src/Kentico.Xperience.CRM.SalesForce/Services/ISalesForceApiService.cs +++ b/src/Kentico.Xperience.CRM.SalesForce/Services/ISalesForceApiService.cs @@ -31,14 +31,14 @@ public interface ISalesForceApiService Task GetLeadIdByExternalId(string fieldName, string externalId); /// - /// + /// Get Lead by primary Id /// /// /// Task GetLeadById(string id, string? fields = null); /// - /// + /// Get Lead by email /// /// /// From 188ffc004a0c44efded53e67506fc02302dcf4c7 Mon Sep 17 00:00:00 2001 From: Martin Foukal Date: Fri, 12 Jan 2024 15:18:16 +0100 Subject: [PATCH 11/23] cleanup --- examples/DancingGoat/Program.cs | 2 -- src/Kentico.Xperience.CRM.Common/Admin/CRMSyncItemListing.cs | 2 -- .../Installers/CRMModuleInstaller.cs | 1 - .../Installers/ICRMModuleInstaller.cs | 5 +---- .../Services/Implementations/CRMSyncItemService.cs | 1 - .../Services/Implementations/FailedSyncItemService.cs | 1 - .../Configuration/BizFormFieldsMappingBuilderExtensions.cs | 2 -- .../Configuration/DynamicsBizFormsMappingBuilder.cs | 4 +--- .../Configuration/DynamicsBizFormsMappingConfiguration.cs | 5 +---- .../DynamicsIntegrationGlobalEvents.cs | 1 - .../DynamicsServiceCollectionExtensions.cs | 1 - .../Services/DynamicsLeadsIntegrationService.cs | 4 +--- .../SalesForceServiceCollectionsExtensions.cs | 1 - .../Services/SalesForceLeadsIntegrationService.cs | 1 - 14 files changed, 4 insertions(+), 27 deletions(-) diff --git a/examples/DancingGoat/Program.cs b/examples/DancingGoat/Program.cs index 7f83668..3671cc9 100644 --- a/examples/DancingGoat/Program.cs +++ b/examples/DancingGoat/Program.cs @@ -1,4 +1,3 @@ -using CMS.OnlineForms; using CMS.OnlineForms.Types; using DancingGoat; using DancingGoat.Models; @@ -9,7 +8,6 @@ using Kentico.OnlineMarketing.Web.Mvc; using Kentico.PageBuilder.Web.Mvc; using Kentico.Web.Mvc; -using Kentico.Xperience.CRM.Common; using Kentico.Xperience.CRM.Dynamics; using Kentico.Xperience.CRM.Dynamics.Configuration; using Kentico.Xperience.CRM.Dynamics.Dataverse.Entities; diff --git a/src/Kentico.Xperience.CRM.Common/Admin/CRMSyncItemListing.cs b/src/Kentico.Xperience.CRM.Common/Admin/CRMSyncItemListing.cs index 6e9ed24..7fef4e8 100644 --- a/src/Kentico.Xperience.CRM.Common/Admin/CRMSyncItemListing.cs +++ b/src/Kentico.Xperience.CRM.Common/Admin/CRMSyncItemListing.cs @@ -1,11 +1,9 @@ using CMS.DataEngine; -using CMS.FormEngine; using CMS.OnlineForms; using Kentico.Xperience.Admin.Base; using Kentico.Xperience.Admin.DigitalMarketing.UIPages; using Kentico.Xperience.CRM.Common.Admin; using Kentico.Xperience.CRM.Common.Classes; -using Kentico.Xperience.CRM.Common.Constants; [assembly: UIPage(typeof(FormEditSection), "crm-sync-listing", typeof(CRMSyncItemListing), diff --git a/src/Kentico.Xperience.CRM.Common/Installers/CRMModuleInstaller.cs b/src/Kentico.Xperience.CRM.Common/Installers/CRMModuleInstaller.cs index d7e2eb0..2e5f473 100644 --- a/src/Kentico.Xperience.CRM.Common/Installers/CRMModuleInstaller.cs +++ b/src/Kentico.Xperience.CRM.Common/Installers/CRMModuleInstaller.cs @@ -1,5 +1,4 @@ using CMS.Base; -using CMS.Core; using CMS.DataEngine; using CMS.FormEngine; using CMS.Modules; diff --git a/src/Kentico.Xperience.CRM.Common/Installers/ICRMModuleInstaller.cs b/src/Kentico.Xperience.CRM.Common/Installers/ICRMModuleInstaller.cs index 23c26b9..96d1808 100644 --- a/src/Kentico.Xperience.CRM.Common/Installers/ICRMModuleInstaller.cs +++ b/src/Kentico.Xperience.CRM.Common/Installers/ICRMModuleInstaller.cs @@ -1,7 +1,4 @@ -using CMS.Base; -using Kentico.Xperience.CRM.Common.Constants; - -namespace Kentico.Xperience.CRM.Common.Installers; +namespace Kentico.Xperience.CRM.Common.Installers; public interface ICRMModuleInstaller { diff --git a/src/Kentico.Xperience.CRM.Common/Services/Implementations/CRMSyncItemService.cs b/src/Kentico.Xperience.CRM.Common/Services/Implementations/CRMSyncItemService.cs index f9d3deb..4adea0a 100644 --- a/src/Kentico.Xperience.CRM.Common/Services/Implementations/CRMSyncItemService.cs +++ b/src/Kentico.Xperience.CRM.Common/Services/Implementations/CRMSyncItemService.cs @@ -1,6 +1,5 @@ using CMS.OnlineForms; using Kentico.Xperience.CRM.Common.Classes; -using Kentico.Xperience.CRM.Common.Constants; namespace Kentico.Xperience.CRM.Common.Services.Implementations; diff --git a/src/Kentico.Xperience.CRM.Common/Services/Implementations/FailedSyncItemService.cs b/src/Kentico.Xperience.CRM.Common/Services/Implementations/FailedSyncItemService.cs index ce27398..6e57846 100644 --- a/src/Kentico.Xperience.CRM.Common/Services/Implementations/FailedSyncItemService.cs +++ b/src/Kentico.Xperience.CRM.Common/Services/Implementations/FailedSyncItemService.cs @@ -1,5 +1,4 @@ using CMS.DataEngine; -using CMS.FormEngine; using CMS.OnlineForms; using Kentico.Xperience.CRM.Common.Classes; diff --git a/src/Kentico.Xperience.CRM.Dynamics/Configuration/BizFormFieldsMappingBuilderExtensions.cs b/src/Kentico.Xperience.CRM.Dynamics/Configuration/BizFormFieldsMappingBuilderExtensions.cs index 9454f1f..8b3ba47 100644 --- a/src/Kentico.Xperience.CRM.Dynamics/Configuration/BizFormFieldsMappingBuilderExtensions.cs +++ b/src/Kentico.Xperience.CRM.Dynamics/Configuration/BizFormFieldsMappingBuilderExtensions.cs @@ -1,7 +1,5 @@ using CMS.OnlineForms; using Kentico.Xperience.CRM.Common.Configuration; -using Kentico.Xperience.CRM.Common.Mapping; -using Kentico.Xperience.CRM.Dynamics.Dataverse.Entities; using Microsoft.Xrm.Sdk; using System.Linq.Expressions; using System.Reflection; diff --git a/src/Kentico.Xperience.CRM.Dynamics/Configuration/DynamicsBizFormsMappingBuilder.cs b/src/Kentico.Xperience.CRM.Dynamics/Configuration/DynamicsBizFormsMappingBuilder.cs index 0618078..d43deb6 100644 --- a/src/Kentico.Xperience.CRM.Dynamics/Configuration/DynamicsBizFormsMappingBuilder.cs +++ b/src/Kentico.Xperience.CRM.Dynamics/Configuration/DynamicsBizFormsMappingBuilder.cs @@ -1,6 +1,4 @@ -using CMS.Core; -using CMS.OnlineForms; -using CMS.OnlineForms.Internal; +using CMS.OnlineForms; using Kentico.Xperience.CRM.Common.Configuration; using Kentico.Xperience.CRM.Common.Mapping; using Kentico.Xperience.CRM.Common.Services; diff --git a/src/Kentico.Xperience.CRM.Dynamics/Configuration/DynamicsBizFormsMappingConfiguration.cs b/src/Kentico.Xperience.CRM.Dynamics/Configuration/DynamicsBizFormsMappingConfiguration.cs index 5474942..1ac315c 100644 --- a/src/Kentico.Xperience.CRM.Dynamics/Configuration/DynamicsBizFormsMappingConfiguration.cs +++ b/src/Kentico.Xperience.CRM.Dynamics/Configuration/DynamicsBizFormsMappingConfiguration.cs @@ -1,7 +1,4 @@ -using CMS.OnlineForms; -using Kentico.Xperience.CRM.Common.Configuration; -using Kentico.Xperience.CRM.Common.Mapping; -using Kentico.Xperience.CRM.Dynamics.Dataverse.Entities; +using Kentico.Xperience.CRM.Common.Configuration; namespace Kentico.Xperience.CRM.Dynamics.Configuration; diff --git a/src/Kentico.Xperience.CRM.Dynamics/DynamicsIntegrationGlobalEvents.cs b/src/Kentico.Xperience.CRM.Dynamics/DynamicsIntegrationGlobalEvents.cs index 3266786..123a315 100644 --- a/src/Kentico.Xperience.CRM.Dynamics/DynamicsIntegrationGlobalEvents.cs +++ b/src/Kentico.Xperience.CRM.Dynamics/DynamicsIntegrationGlobalEvents.cs @@ -5,7 +5,6 @@ using CMS.OnlineForms; using Kentico.Xperience.CRM.Common.Constants; using Kentico.Xperience.CRM.Common.Installers; -using Kentico.Xperience.CRM.Common.Services; using Kentico.Xperience.CRM.Dynamics; using Kentico.Xperience.CRM.Dynamics.Configuration; using Kentico.Xperience.CRM.Dynamics.Services; diff --git a/src/Kentico.Xperience.CRM.Dynamics/DynamicsServiceCollectionExtensions.cs b/src/Kentico.Xperience.CRM.Dynamics/DynamicsServiceCollectionExtensions.cs index 5d96f6b..b099612 100644 --- a/src/Kentico.Xperience.CRM.Dynamics/DynamicsServiceCollectionExtensions.cs +++ b/src/Kentico.Xperience.CRM.Dynamics/DynamicsServiceCollectionExtensions.cs @@ -1,7 +1,6 @@ using CMS.Core; using CMS.Helpers; using Kentico.Xperience.CRM.Common; -using Kentico.Xperience.CRM.Common.Configuration; using Kentico.Xperience.CRM.Common.Constants; using Kentico.Xperience.CRM.Dynamics.Configuration; using Kentico.Xperience.CRM.Dynamics.Services; diff --git a/src/Kentico.Xperience.CRM.Dynamics/Services/DynamicsLeadsIntegrationService.cs b/src/Kentico.Xperience.CRM.Dynamics/Services/DynamicsLeadsIntegrationService.cs index faca9d4..8d861ae 100644 --- a/src/Kentico.Xperience.CRM.Dynamics/Services/DynamicsLeadsIntegrationService.cs +++ b/src/Kentico.Xperience.CRM.Dynamics/Services/DynamicsLeadsIntegrationService.cs @@ -1,10 +1,8 @@ -using CMS.Helpers; -using CMS.OnlineForms; +using CMS.OnlineForms; using Kentico.Xperience.CRM.Common.Constants; using Kentico.Xperience.CRM.Common.Mapping; using Kentico.Xperience.CRM.Common.Mapping.Implementations; using Kentico.Xperience.CRM.Common.Services; -using Kentico.Xperience.CRM.Common.Services.Implementations; using Kentico.Xperience.CRM.Dynamics.Configuration; using Kentico.Xperience.CRM.Dynamics.Dataverse.Entities; using Microsoft.Extensions.Logging; diff --git a/src/Kentico.Xperience.CRM.SalesForce/SalesForceServiceCollectionsExtensions.cs b/src/Kentico.Xperience.CRM.SalesForce/SalesForceServiceCollectionsExtensions.cs index 7cb0f4f..feb516a 100644 --- a/src/Kentico.Xperience.CRM.SalesForce/SalesForceServiceCollectionsExtensions.cs +++ b/src/Kentico.Xperience.CRM.SalesForce/SalesForceServiceCollectionsExtensions.cs @@ -2,7 +2,6 @@ using CMS.Helpers; using Duende.AccessTokenManagement; using Kentico.Xperience.CRM.Common; -using Kentico.Xperience.CRM.Common.Configuration; using Kentico.Xperience.CRM.Common.Constants; using Kentico.Xperience.CRM.SalesForce.Configuration; using Kentico.Xperience.CRM.SalesForce.Services; diff --git a/src/Kentico.Xperience.CRM.SalesForce/Services/SalesForceLeadsIntegrationService.cs b/src/Kentico.Xperience.CRM.SalesForce/Services/SalesForceLeadsIntegrationService.cs index 63dfe46..0253770 100644 --- a/src/Kentico.Xperience.CRM.SalesForce/Services/SalesForceLeadsIntegrationService.cs +++ b/src/Kentico.Xperience.CRM.SalesForce/Services/SalesForceLeadsIntegrationService.cs @@ -3,7 +3,6 @@ using Kentico.Xperience.CRM.Common.Mapping; using Kentico.Xperience.CRM.Common.Mapping.Implementations; using Kentico.Xperience.CRM.Common.Services; -using Kentico.Xperience.CRM.Common.Services.Implementations; using Kentico.Xperience.CRM.SalesForce.Configuration; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; From b369d465cb3b9a9ec1fb30cf7c523342b998440a Mon Sep 17 00:00:00 2001 From: Martin Foukal Date: Fri, 12 Jan 2024 17:40:05 +0100 Subject: [PATCH 12/23] readme, some fixes --- README.md | 157 ++++++++++++++---- examples/DancingGoat/Program.cs | 46 +++-- examples/DancingGoat/appsettings.json | 2 +- .../Constants/SettingKeys.cs | 2 +- .../Installers/CRMModuleInstaller.cs | 36 ++-- .../Workers/FailedSyncItemsWorkerBase.cs | 6 +- .../DynamicsIntegrationGlobalEvents.cs | 3 + .../DynamicsLeadsIntegrationService.cs | 10 ++ .../SalesForceBizFormsMappingBuilder.cs | 11 +- .../SalesForceLeadsIntegrationService.cs | 16 +- 10 files changed, 197 insertions(+), 92 deletions(-) diff --git a/README.md b/README.md index d2ad6e6..829ae7e 100644 --- a/README.md +++ b/README.md @@ -39,21 +39,29 @@ dotnet add package Kentico.Xperience.CRM.SalesForce ## Quick Start -1. Fill CRM (Dynamics/SalesForce) settings +1. Fill CRM (Dynamics/SalesForce) settings (in CMS or appsettings.json) 2. Register services and setup form-lead mapping 3. Start to use ### CRM settings -Integration uses OAuth client credentials scheme. -Fill and add this settings to appsettings.json (API config is recommended to have in [User Secrets](https://learn.microsoft.com/en-us/aspnet/core/security/app-secrets?view=aspnetcore-6.0&tabs=windows)) +There are 2 options how to fill settings: +- use CMS settings: CRM integration settings category is created after first run. +This is primary option when you don't specify IConfiguration section during services registration. +- use application settings: appsettings.json (API config is recommended to have in [User Secrets](https://learn.microsoft.com/en-us/aspnet/core/security/app-secrets?view=aspnetcore-6.0&tabs=windows)) -#### Dynamics settings +Integration uses OAuth client credentials scheme, so you have to setup your CRM environment to enable for using API with +client id and client secret: +- [Dynamics](https://learn.microsoft.com/en-us/power-apps/developer/data-platform/authenticate-oauth) +- [SalesForce](https://help.salesforce.com/s/articleView?id=sf.remoteaccess_oauth_client_credentials_flow.htm&type=5) +#### Dynamics settings +Fill settings in CMS or use this appsettings: ```json { "CMSDynamicsCRMIntegration": { "FormLeadsEnabled": true, + "IgnoreExistingRecords": false, "ApiConfig": { "DynamicsUrl": "", "ClientId": "", @@ -64,11 +72,12 @@ Fill and add this settings to appsettings.json (API config is recommended to hav ``` #### SalesForce settings - +Fill settings in CMS or use this app settings: ```json { "CMSSalesForceCRMIntegration": { "FormLeadsEnabled": true, + "IgnoreExistingRecords": false, "ApiConfig": { "SalesForceUrl": "", "ClientId": "", @@ -91,64 +100,140 @@ You can also set specific API version for SalesForce REST API (default version i Configure mapping for each form between Kentico Form fields and Dynamics Lead entity fields: #### Dynamics Sales +Added form with auto mapping based on Form field mapping to Contacts atttibutes. Uses CMS settings: +```csharp + // Program.cs + + var builder = WebApplication.CreateBuilder(args); + + // ... + builder.Services.AddDynamicsFormLeadsIntegration(builder => + builder.AddFormWithContactMapping(DancingGoatContactUsItem.CLASS_NAME)); +``` + +Same example but with using app setting in code (**CMS setting are ignored!**): + +```csharp + // Program.cs + + var builder = WebApplication.CreateBuilder(args); + + // ... + builder.Services.AddDynamicsFormLeadsIntegration(builder => + builder.AddFormWithContactMapping(DancingGoatContactUsItem.CLASS_NAME), + builder.Configuration.GetSection(DynamicsIntegrationSettings.ConfigKeyName)); +``` +Example how to add form with auto mapping combined with custom mapping and custom validation: ```csharp // Program.cs var builder = WebApplication.CreateBuilder(args); // ... + builder.Services.AddDynamicsFormLeadsIntegration(builder => + builder.AddFormWithContactMapping(DancingGoatContactUsItem.CLASS_NAME, b => b + .MapField(c => c.UserMessage, e => e.EMailAddress1)) + .AddCustomValidation()); +``` + +Example how to add form with own mapping: +```csharp + // Program.cs -builder.Services.AddDynamicsCrmLeadsIntegration(builder => - builder.AddForm(DancingGoatContactUsItem.CLASS_NAME, //form class name - c => c - .MapField("UserFirstName", "firstname") - .MapField("UserLastName", e => e.LastName) //you can map to Lead object or use own generated Lead class - .MapField(c => c.UserEmail, e => e.EMailAddress1) //generated form class used - .MapField(b => b.GetStringValue("UserMessage", ""), e => e.Description) //general BizFormItem used - ) - .ExternalIdField("new_kenticoid") //optional custom field when you want updates to work - , - builder.Configuration.GetSection(DynamicsIntegrationSettings.ConfigKeyName)); //config section with settings + var builder = WebApplication.CreateBuilder(args); + + // ... + builder.Services.AddDynamicsFormLeadsIntegration(builder => + builder.AddForm(DancingGoatContactUsItem.CLASS_NAME, //form class name + c => c + .MapField("UserFirstName", "firstname") + .MapField("UserLastName", e => e.LastName) //you can map to Lead object or use own generated Lead class + .MapField(c => c.UserEmail, e => e.EMailAddress1) //generated form class used + .MapField(b => b.GetStringValue("UserMessage", ""), e => e.Description) //general BizFormItem used + )); ``` -You can also register custom validation service to handle if given form item should be processed to CRM: +Example how to add form with custom converter. +Use this option when you need complex logic and need to use another service via DI: ```csharp - //call this after AddDynamicsCrmLeadsIntegration registration - builder.Services.AddCustomFormLeadsValidationService(); + // Program.cs + + var builder = WebApplication.CreateBuilder(args); + + // ... + builder.Services.AddDynamicsFormLeadsIntegration(builder => + builder.AddFormWithConverter(DancingGoatContactUsItem.CLASS_NAME)); ``` #### SalesForce +Added form with auto mapping based on Form field mapping to Contacts atttibutes. Uses CMS settings: +```csharp + // Program.cs + + var builder = WebApplication.CreateBuilder(args); + + // ... + builder.Services.AddSalesForceFormLeadsIntegration(builder => + builder.AddFormWithContactMapping(DancingGoatContactUsItem.CLASS_NAME)); +``` + +Same example but with using app setting in code (**CMS setting are ignored!**): + +```csharp + // Program.cs + + var builder = WebApplication.CreateBuilder(args); + + // ... + builder.Services.AddSalesForceFormLeadsIntegration(builder => + builder.AddFormWithContactMapping(DancingGoatContactUsItem.CLASS_NAME), + builder.Configuration.GetSection(SalesForceIntegrationSettings.ConfigKeyName)); +``` + +Example how to add form with auto mapping combined with custom mapping and custom validation: ```csharp // Program.cs var builder = WebApplication.CreateBuilder(args); // ... + builder.Services.AddSalesForceFormLeadsIntegration(builder => + builder.AddFormWithContactMapping(DancingGoatContactUsItem.CLASS_NAME, b => b + .MapField(c => c.UserMessage, e => e.Description)) + .AddCustomValidation()); +``` -builder.Services.AddSalesForceCrmLeadsIntegration(builder => - builder.AddForm(DancingGoatContactUsItem.CLASS_NAME, //form class name - c => c - .MapField("UserFirstName", "FirstName") //option1: mapping based on source and target field names - .MapField("UserLastName", - e => e.LastName) //option 2: mapping source name string -> member expression to SObject - .MapField(c => c.UserEmail, e => e.Email) - //option 3: source mapping function from generated BizForm object -> member expression to SObject - .MapField(b => b.GetStringValue("UserMessage", ""), e => e.Description) - //option 4: source mapping function general BizFormItem -> member expression to SObject - ) - .ExternalIdField("KenticoID") //optional custom field when you want updates to work - , - builder.Configuration.GetSection(SalesForceIntegrationSettings.ConfigKeyName)); //config section with settings +Example how to add form with own mapping: +```csharp + // Program.cs + + var builder = WebApplication.CreateBuilder(args); + + // ... + builder.Services.AddSalesForceFormLeadsIntegration(builder => + builder.AddForm(DancingGoatContactUsItem.CLASS_NAME, //form class name + c => c + .MapField("UserFirstName", "FirstName") //option1: mapping based on source and target field names + .MapField("UserLastName", e => e.LastName) //option 2: mapping source name string -> member expression to SObject + .MapField(c => c.UserEmail, e => e.Email) //option 3: source mapping function from generated BizForm object -> member expression to SObject + .MapField(b => b.GetStringValue("UserMessage", ""), e => e.Description) //option 4: source mapping function general BizFormItem -> member expression to SObject + )); ``` -You can also register custom validation service to handle if given form item should be processed to CRM: +Example how to add form with custom converter. +Use this option when you need complex logic and need to use another service via DI: ```csharp - //call this after AddSalesForceCrmLeadsIntegration registration - builder.Services.AddCustomFormLeadsValidationService(); + // Program.cs + + var builder = WebApplication.CreateBuilder(args); + + // ... + builder.Services.AddSalesForceFormLeadsIntegration(builder => + builder.AddFormWithConverter(DancingGoatContactUsItem.CLASS_NAME)); ``` ## Contributing diff --git a/examples/DancingGoat/Program.cs b/examples/DancingGoat/Program.cs index 3671cc9..7db2c09 100644 --- a/examples/DancingGoat/Program.cs +++ b/examples/DancingGoat/Program.cs @@ -1,3 +1,4 @@ +using CMS.OnlineForms; using CMS.OnlineForms.Types; using DancingGoat; using DancingGoat.Models; @@ -50,41 +51,36 @@ //CRM integration registration start -// builder.Services.AddDynamicsFormLeadsIntegration(builder => -// builder.AddForm(DancingGoatContactUsItem.CLASS_NAME, //form class name -// c => c -// .MapField("UserFirstName", "firstname") -// .MapField("UserLastName", e => e.LastName) //you can map to Lead object or use own generated Lead class -// .MapField(c => c.UserEmail, e => e.EMailAddress1) //generated form class used -// .MapField(b => b.GetStringValue("UserMessage", ""), e => e.Description) //general BizFormItem used -// ) -// .AddCustomFormLeadsValidationService() //optional -// , -// builder.Configuration.GetSection(DynamicsIntegrationSettings.ConfigKeyName)); //config section with settings +//builder.Services.AddDynamicsFormLeadsIntegration(builder => +// builder.AddForm(DancingGoatContactUsItem.CLASS_NAME, //form class name +// c => c +// .MapField("UserFirstName", "firstname") +// .MapField("UserLastName", e => e.LastName) //you can map to Lead object or use own generated Lead class +// .MapField(c => c.UserEmail, e => e.EMailAddress1) //generated form class used +// .MapField(b => b.GetStringValue("UserMessage", ""), e => e.Description) //general BizFormItem used +// ) +// .AddCustomValidation() //optional +// , +// builder.Configuration.GetSection(DynamicsIntegrationSettings.ConfigKeyName)); //config section with settings builder.Services.AddDynamicsFormLeadsIntegration(builder => builder.AddFormWithContactMapping(DancingGoatContactUsItem.CLASS_NAME, b => b .MapField(c => c.UserMessage, e => e.EMailAddress1)) .AddCustomValidation()); //optional -// builder.Services.AddSalesForceFormLeadsIntegration(builder => -// builder.AddForm(DancingGoatContactUsItem.CLASS_NAME, //form class name -// c => c -// .MapField("UserFirstName", "FirstName") //option1: mapping based on source and target field names -// .MapField("UserLastName", e => e.LastName) //option 2: mapping source name string -> member expression to SObject -// .MapField(c => c.UserEmail, e => e.Email) -// //option 3: source mapping function from generated BizForm object -> member expression to SObject -// .MapField(b => b.GetStringValue("UserMessage", ""), e => e.Description) -// //option 4: source mapping function general BizFormItem -> member expression to SObject -// ) -// , -// builder.Configuration.GetSection(SalesForceIntegrationSettings.ConfigKeyName)); //config section with settings +//builder.Services.AddSalesForceFormLeadsIntegration(builder => +// builder.AddForm(DancingGoatContactUsItem.CLASS_NAME, //form class name +// c => c +// .MapField("UserFirstName", "FirstName") //option1: mapping based on source and target field names +// .MapField("UserLastName", e => e.LastName) //option 2: mapping source name string -> member expression to SObject +// .MapField(c => c.UserEmail, e => e.Email) //option 3: source mapping function from generated BizForm object -> member expression to SObject +// .MapField(b => b.GetStringValue("UserMessage", ""), e => e.Description) //option 4: source mapping function general BizFormItem -> member expression to SObject +// )); builder.Services.AddSalesForceFormLeadsIntegration(builder => builder.AddFormWithContactMapping(DancingGoatContactUsItem.CLASS_NAME, b => b .MapField(c => c.UserMessage, e => e.Description)) - .AddCustomValidation()); //optional - + .AddCustomValidation()); //CRM integration registration end diff --git a/examples/DancingGoat/appsettings.json b/examples/DancingGoat/appsettings.json index 2d3a4d8..506e2d7 100644 --- a/examples/DancingGoat/appsettings.json +++ b/examples/DancingGoat/appsettings.json @@ -30,7 +30,7 @@ //"ApiConfig" add to secrets.json }, "CMSSalesForceCRMIntegration": { - "FormLeadsEnabled": false, + "FormLeadsEnabled": true, "IgnoreExistingRecords": false //"ApiConfig" add to secrets.json } diff --git a/src/Kentico.Xperience.CRM.Common/Constants/SettingKeys.cs b/src/Kentico.Xperience.CRM.Common/Constants/SettingKeys.cs index 4b825af..aa7cd5d 100644 --- a/src/Kentico.Xperience.CRM.Common/Constants/SettingKeys.cs +++ b/src/Kentico.Xperience.CRM.Common/Constants/SettingKeys.cs @@ -9,7 +9,7 @@ public class SettingKeys public const string DynamicsUrl = "CMSDynamicsCRMIntegrationDynamicsUrl"; public const string DynamicsClientId = "CMSDynamicsCRMIntegrationClientId"; public const string DynamicsClientSecret = "CMSDynamicsCRMIntegrationClientSecret"; - public const string DynamicsIgnoreExistingRecords = "CMSDynamicsCRMIgnoreExistingRecords"; + public const string DynamicsIgnoreExistingRecords = "CMSDynamicsCRMIntegrationIgnoreExistingRecords"; public const string SalesForceFormLeadsEnabled = "CMSSalesforceCRMIntegrationFormLeadsEnabled"; public const string SalesForceUrl = "CMSSalesforceCRMIntegrationSalesforceUrl"; diff --git a/src/Kentico.Xperience.CRM.Common/Installers/CRMModuleInstaller.cs b/src/Kentico.Xperience.CRM.Common/Installers/CRMModuleInstaller.cs index 2e5f473..2a0728b 100644 --- a/src/Kentico.Xperience.CRM.Common/Installers/CRMModuleInstaller.cs +++ b/src/Kentico.Xperience.CRM.Common/Installers/CRMModuleInstaller.cs @@ -281,6 +281,23 @@ private void InstallSettings(ResourceInfo resourceInfo, string crmType) SettingsKeyInfo.Provider.Set(settingFormsEnabled); } + var settingsIgnoreExisting = SettingsKeyInfo.Provider.Get($"CMS{crmType}CRMIntegrationIgnoreExistingRecords"); + if (settingsIgnoreExisting is null) + { + settingsIgnoreExisting = new SettingsKeyInfo + { + KeyName = $"CMS{crmType}CRMIntegrationIgnoreExistingRecords", + KeyDisplayName = "Ignore existing records", + KeyDescription = "", + KeyType = "boolean", + KeyCategoryID = crmCategory.CategoryID, + KeyIsCustom = true, + KeyExplanationText = "If true no existing item with same email or paired record by ID is updated" + }; + + SettingsKeyInfo.Provider.Set(settingsIgnoreExisting); + } + var settingUrl = SettingsKeyInfo.Provider.Get($"CMS{crmType}CRMIntegration{crmType}Url"); if (settingUrl is null) { @@ -320,7 +337,7 @@ private void InstallSettings(ResourceInfo resourceInfo, string crmType) { settingClientSecret = new SettingsKeyInfo { - KeyName = $"CMS{crmType}CRMIntegration{crmType}ClientSecret", + KeyName = $"CMS{crmType}CRMIntegrationClientSecret", KeyDisplayName = "Client Secret", KeyDescription = "", KeyType = "string", @@ -331,22 +348,5 @@ private void InstallSettings(ResourceInfo resourceInfo, string crmType) SettingsKeyInfo.Provider.Set(settingClientSecret); } - - var settingsIgnoreExisting = SettingsKeyInfo.Provider.Get($"CMS{crmType}CRMIntegrationIgnoreExistingRecords"); - if (settingsIgnoreExisting is null) - { - settingsIgnoreExisting = new SettingsKeyInfo - { - KeyName = $"CMS{crmType}CRMIntegration{crmType}ClientSecret", - KeyDisplayName = "Ignore existing records", - KeyDescription = "", - KeyType = "string", - KeyCategoryID = crmCategory.CategoryID, - KeyIsCustom = true, - KeyExplanationText = "", - }; - - SettingsKeyInfo.Provider.Set(settingsIgnoreExisting); - } } } \ No newline at end of file diff --git a/src/Kentico.Xperience.CRM.Common/Workers/FailedSyncItemsWorkerBase.cs b/src/Kentico.Xperience.CRM.Common/Workers/FailedSyncItemsWorkerBase.cs index 1462a08..b700113 100644 --- a/src/Kentico.Xperience.CRM.Common/Workers/FailedSyncItemsWorkerBase.cs +++ b/src/Kentico.Xperience.CRM.Common/Workers/FailedSyncItemsWorkerBase.cs @@ -46,11 +46,13 @@ protected override void Process() var failedSyncItemsService = Service.Resolve(); - var leadsIntegrationService = serviceScope.ServiceProvider - .GetRequiredService(); + ILeadsIntegrationService? leadsIntegrationService = null; foreach (var syncItem in failedSyncItemsService.GetFailedSyncItemsToReSync(CRMName)) { + leadsIntegrationService ??= serviceScope.ServiceProvider + .GetRequiredService(); + var bizFormItem = failedSyncItemsService.GetBizFormItem(syncItem); if (bizFormItem is null) { diff --git a/src/Kentico.Xperience.CRM.Dynamics/DynamicsIntegrationGlobalEvents.cs b/src/Kentico.Xperience.CRM.Dynamics/DynamicsIntegrationGlobalEvents.cs index 123a315..6b739e5 100644 --- a/src/Kentico.Xperience.CRM.Dynamics/DynamicsIntegrationGlobalEvents.cs +++ b/src/Kentico.Xperience.CRM.Dynamics/DynamicsIntegrationGlobalEvents.cs @@ -5,6 +5,7 @@ using CMS.OnlineForms; using Kentico.Xperience.CRM.Common.Constants; using Kentico.Xperience.CRM.Common.Installers; +using Kentico.Xperience.CRM.Common.Services; using Kentico.Xperience.CRM.Dynamics; using Kentico.Xperience.CRM.Dynamics.Configuration; using Kentico.Xperience.CRM.Dynamics.Services; @@ -44,6 +45,7 @@ protected override void OnInit() private void SynchronizeBizFormLead(object? sender, BizFormItemEventArgs e) { + var failedSyncItemsService = Service.Resolve(); try { using (var serviceScope = Service.Resolve().CreateScope()) @@ -60,6 +62,7 @@ private void SynchronizeBizFormLead(object? sender, BizFormItemEventArgs e) catch (Exception exception) { logger.LogError(exception, "Error occured during updating lead"); + failedSyncItemsService.LogFailedLeadItem(e.Item, CRMType.Dynamics); } } } \ No newline at end of file diff --git a/src/Kentico.Xperience.CRM.Dynamics/Services/DynamicsLeadsIntegrationService.cs b/src/Kentico.Xperience.CRM.Dynamics/Services/DynamicsLeadsIntegrationService.cs index 8d861ae..dcae75c 100644 --- a/src/Kentico.Xperience.CRM.Dynamics/Services/DynamicsLeadsIntegrationService.cs +++ b/src/Kentico.Xperience.CRM.Dynamics/Services/DynamicsLeadsIntegrationService.cs @@ -105,6 +105,11 @@ private async Task SynchronizeLeadAsync(BizFormItem bizFormItem, { await UpdateLeadAsync(existingLead, bizFormItem, fieldMappings, converters); } + else + { + logger.LogInformation("BizForm item {ItemID} for {BizFormDisplayName} ignored", + bizFormItem.ItemID, bizFormItem.BizFormInfo.FormDisplayName); + } } } catch (FaultException e) @@ -144,6 +149,11 @@ private async Task UpdateByEmailOrCreate(BizFormItem bizFormItem, { await UpdateLeadAsync(existingLead, bizFormItem, fieldMappings, converters); } + else + { + logger.LogInformation("BizForm item {ItemID} for {BizFormDisplayName} ignored", + bizFormItem.ItemID, bizFormItem.BizFormInfo.FormDisplayName); + } } private async Task CreateLeadAsync(BizFormItem bizFormItem, diff --git a/src/Kentico.Xperience.CRM.SalesForce/Configuration/SalesForceBizFormsMappingBuilder.cs b/src/Kentico.Xperience.CRM.SalesForce/Configuration/SalesForceBizFormsMappingBuilder.cs index 12ac536..96a2765 100644 --- a/src/Kentico.Xperience.CRM.SalesForce/Configuration/SalesForceBizFormsMappingBuilder.cs +++ b/src/Kentico.Xperience.CRM.SalesForce/Configuration/SalesForceBizFormsMappingBuilder.cs @@ -22,21 +22,20 @@ public SalesForceBizFormsMappingBuilder(IServiceCollection serviceCollection) { this.serviceCollection = serviceCollection; } - + /// - /// Adds form when conversion is added automatically based on Form-Contact mapping - /// with custom mapping combined + /// Adds Form with mapping /// /// - /// + /// /// /// public SalesForceBizFormsMappingBuilder AddForm(string formCodeName, - BizFormFieldsMappingBuilder configuredBuilder) + Func configureFields) { if (formCodeName is null) throw new ArgumentNullException(nameof(formCodeName)); - forms.Add(formCodeName.ToLowerInvariant(), configuredBuilder); + forms.Add(formCodeName.ToLowerInvariant(), configureFields(new BizFormFieldsMappingBuilder())); return this; } diff --git a/src/Kentico.Xperience.CRM.SalesForce/Services/SalesForceLeadsIntegrationService.cs b/src/Kentico.Xperience.CRM.SalesForce/Services/SalesForceLeadsIntegrationService.cs index 0253770..3a07c13 100644 --- a/src/Kentico.Xperience.CRM.SalesForce/Services/SalesForceLeadsIntegrationService.cs +++ b/src/Kentico.Xperience.CRM.SalesForce/Services/SalesForceLeadsIntegrationService.cs @@ -100,21 +100,26 @@ private async Task SynchronizeLeadAsync(BizFormItem bizFormItem, { await UpdateLeadAsync(existingLead.Id!, bizFormItem, fieldMappings, converters); } + else + { + logger.LogInformation("BizForm item {ItemID} for {BizFormDisplayName} ignored", + bizFormItem.ItemID, bizFormItem.BizFormInfo.FormDisplayName); + } } } catch (ApiException> e) { - logger.LogError(e, "Update lead failed - api error: {ApiResult}", JsonSerializer.Serialize(e.Result)); + logger.LogError(e, "Sync lead failed - api error: {ApiResult}", JsonSerializer.Serialize(e.Result)); failedSyncItemService.LogFailedLeadItem(bizFormItem, CRMType.SalesForce); } catch (ApiException> e) { - logger.LogError(e, "Update lead failed - api error: {ApiResult}", JsonSerializer.Serialize(e.Result)); + logger.LogError(e, "Sync lead failed - api error: {ApiResult}", JsonSerializer.Serialize(e.Result)); failedSyncItemService.LogFailedLeadItem(bizFormItem, CRMType.SalesForce); } catch (ApiException e) { - logger.LogError(e, "Update lead failed - unexpected api error"); + logger.LogError(e, "Sync lead failed - unexpected api error"); failedSyncItemService.LogFailedLeadItem(bizFormItem, CRMType.SalesForce); } } @@ -140,6 +145,11 @@ private async Task UpdateByEmailOrCreate(BizFormItem bizFormItem, IEnumerable fieldMappings, From 28c6b6696e93e4e261f4a710be1d15fd8e9be48f Mon Sep 17 00:00:00 2001 From: Martin Foukal Date: Wed, 17 Jan 2024 18:27:33 +0100 Subject: [PATCH 13/23] code review changes 1 --- examples/DancingGoat/Program.cs | 2 +- .../Admin/CRMSyncItemListing.cs | 4 +- .../Classes/CRMSyncItemInfo.generated.cs | 3 +- .../Classes/FailedsyncitemInfo.generated.cs | 2 +- .../Installers/CRMModuleInstaller.cs | 37 +++++++++---------- .../Services/ICRMSyncItemService.cs | 6 +-- .../Implementations/CRMSyncItemService.cs | 17 +++++---- .../DynamicsLeadsIntegrationService.cs | 6 +-- .../Services/SalesForceApiService.cs | 3 +- .../SalesForceLeadsIntegrationService.cs | 6 +-- 10 files changed, 42 insertions(+), 44 deletions(-) diff --git a/examples/DancingGoat/Program.cs b/examples/DancingGoat/Program.cs index 7db2c09..938e5b6 100644 --- a/examples/DancingGoat/Program.cs +++ b/examples/DancingGoat/Program.cs @@ -65,7 +65,7 @@ builder.Services.AddDynamicsFormLeadsIntegration(builder => builder.AddFormWithContactMapping(DancingGoatContactUsItem.CLASS_NAME, b => b - .MapField(c => c.UserMessage, e => e.EMailAddress1)) + .MapField(c => c.UserMessage, e => e.Description)) .AddCustomValidation()); //optional //builder.Services.AddSalesForceFormLeadsIntegration(builder => diff --git a/src/Kentico.Xperience.CRM.Common/Admin/CRMSyncItemListing.cs b/src/Kentico.Xperience.CRM.Common/Admin/CRMSyncItemListing.cs index 7fef4e8..a9460a6 100644 --- a/src/Kentico.Xperience.CRM.Common/Admin/CRMSyncItemListing.cs +++ b/src/Kentico.Xperience.CRM.Common/Admin/CRMSyncItemListing.cs @@ -6,8 +6,8 @@ using Kentico.Xperience.CRM.Common.Classes; [assembly: - UIPage(typeof(FormEditSection), "crm-sync-listing", typeof(CRMSyncItemListing), - "CRM synchronization", TemplateNames.LISTING, 1000, Icons.IntegrationScheme)] + UIPage(parentType: typeof(FormEditSection), slug: "crm-sync-listing", uiPageType: typeof(CRMSyncItemListing), + name: "CRM synchronization", templateName: TemplateNames.LISTING, order: 1000, icon: Icons.IntegrationScheme)] namespace Kentico.Xperience.CRM.Common.Admin; diff --git a/src/Kentico.Xperience.CRM.Common/Classes/CRMSyncItemInfo.generated.cs b/src/Kentico.Xperience.CRM.Common/Classes/CRMSyncItemInfo.generated.cs index 57f54d4..7490c84 100644 --- a/src/Kentico.Xperience.CRM.Common/Classes/CRMSyncItemInfo.generated.cs +++ b/src/Kentico.Xperience.CRM.Common/Classes/CRMSyncItemInfo.generated.cs @@ -26,8 +26,7 @@ public partial class CRMSyncItemInfo : AbstractInfo /// Type information. /// -#warning "You will need to configure the type info." - public static readonly ObjectTypeInfo TYPEINFO = new ObjectTypeInfo(typeof(CRMSyncItemInfoProvider), OBJECT_TYPE, "kenticocrmcommon.crmsyncitem", "CRMSyncItemID", "CRMSyncItemLastModified", null, null, null, null, null, null) + public static readonly ObjectTypeInfo TYPEINFO = new ObjectTypeInfo(typeof(CRMSyncItemInfoProvider), OBJECT_TYPE, "KenticoCRMCommon.CRMSyncItem", "CRMSyncItemID", "CRMSyncItemLastModified", null, null, null, null, null, null) { ModuleName = "Kentic.Xperience.CRM.Common", TouchCacheDependencies = true, diff --git a/src/Kentico.Xperience.CRM.Common/Classes/FailedsyncitemInfo.generated.cs b/src/Kentico.Xperience.CRM.Common/Classes/FailedsyncitemInfo.generated.cs index babe13c..2e60052 100644 --- a/src/Kentico.Xperience.CRM.Common/Classes/FailedsyncitemInfo.generated.cs +++ b/src/Kentico.Xperience.CRM.Common/Classes/FailedsyncitemInfo.generated.cs @@ -26,7 +26,7 @@ public partial class FailedSyncItemInfo : AbstractInfo /// Type information. /// - public static readonly ObjectTypeInfo TYPEINFO = new ObjectTypeInfo(typeof(FailedSyncItemInfoProvider), OBJECT_TYPE, "kenticocrmcommon.failedsyncitem", "FailedSyncItemID", "FailedSyncItemLastModified", null, null, null, null, null, null) + public static readonly ObjectTypeInfo TYPEINFO = new ObjectTypeInfo(typeof(FailedSyncItemInfoProvider), OBJECT_TYPE, "KenticoCRMCommon.FailedSyncItem", "FailedSyncItemID", "FailedSyncItemLastModified", null, null, null, null, null, null) { ModuleName = "Kentic.Xperience.CRM.Common", TouchCacheDependencies = true, diff --git a/src/Kentico.Xperience.CRM.Common/Installers/CRMModuleInstaller.cs b/src/Kentico.Xperience.CRM.Common/Installers/CRMModuleInstaller.cs index 2a0728b..3fd1833 100644 --- a/src/Kentico.Xperience.CRM.Common/Installers/CRMModuleInstaller.cs +++ b/src/Kentico.Xperience.CRM.Common/Installers/CRMModuleInstaller.cs @@ -14,7 +14,7 @@ namespace Kentico.Xperience.CRM.Common.Installers; /// Custom settings are created /// Objects are created when not exists on start. /// -public class CRMModuleInstaller : ICRMModuleInstaller +internal class CRMModuleInstaller : ICRMModuleInstaller { private readonly IResourceInfoProvider resourceInfoProvider; @@ -25,12 +25,9 @@ public CRMModuleInstaller(IResourceInfoProvider resourceInfoProvider) public void Install(string crmtype) { - using (new CMSActionContext { ContinuousIntegrationAllowObjectSerialization = false }) - { - var resourceInfo = InstallModule(); - InstallModuleClasses(resourceInfo); - InstallSettings(resourceInfo, crmtype); - } + var resourceInfo = InstallModule(); + InstallModuleClasses(resourceInfo); + InstallSettings(resourceInfo, crmtype); } private ResourceInfo InstallModule() @@ -57,25 +54,25 @@ private void InstallModuleClasses(ResourceInfo resourceInfo) private void InstallSyncedItemClass(ResourceInfo resourceInfo) { - var failedSyncItemClass = DataClassInfoProvider.GetDataClassInfo("kenticocrmcommon.crmsyncitem"); + var failedSyncItemClass = DataClassInfoProvider.GetDataClassInfo(CRMSyncItemInfo.OBJECT_TYPE); if (failedSyncItemClass is not null) { return; } - failedSyncItemClass = DataClassInfo.New("kenticocrmcommon.crmsyncitem"); + failedSyncItemClass = DataClassInfo.New(CRMSyncItemInfo.OBJECT_TYPE); - failedSyncItemClass.ClassName = "kenticocrmcommon.crmsyncitem"; - failedSyncItemClass.ClassTableName = "kenticocrmcommon.crmsyncitem".Replace(".", "_"); + failedSyncItemClass.ClassName = CRMSyncItemInfo.TYPEINFO.ObjectClassName; + failedSyncItemClass.ClassTableName = CRMSyncItemInfo.TYPEINFO.ObjectClassName.Replace(".", "_"); failedSyncItemClass.ClassDisplayName = "CRM sync item"; failedSyncItemClass.ClassResourceID = resourceInfo.ResourceID; failedSyncItemClass.ClassType = ClassType.OTHER; - var formInfo = FormHelper.GetBasicFormDefinition("CRMSyncItemID"); + var formInfo = FormHelper.GetBasicFormDefinition(nameof(CRMSyncItemInfo.CRMSyncItemID)); var formItem = new FormFieldInfo { - Name = "CRMSyncItemEntityClass", + Name = nameof(CRMSyncItemInfo.CRMSyncItemEntityClass), Visible = false, Precision = 0, Size = 100, @@ -86,7 +83,7 @@ private void InstallSyncedItemClass(ResourceInfo resourceInfo) formItem = new FormFieldInfo { - Name = "CRMSyncItemEntityID", + Name = nameof(CRMSyncItemInfo.CRMSyncItemEntityID), Visible = false, Precision = 0, Size = 50, @@ -97,7 +94,7 @@ private void InstallSyncedItemClass(ResourceInfo resourceInfo) formItem = new FormFieldInfo { - Name = "CRMSyncItemCRMID", + Name = nameof(CRMSyncItemInfo.CRMSyncItemCRMID), Visible = false, Precision = 0, Size = 50, @@ -108,7 +105,7 @@ private void InstallSyncedItemClass(ResourceInfo resourceInfo) formItem = new FormFieldInfo { - Name = "CRMSyncItemEntityCRM", + Name = nameof(CRMSyncItemInfo.CRMSyncItemEntityCRM), Visible = false, Precision = 0, Size = 50, @@ -119,13 +116,13 @@ private void InstallSyncedItemClass(ResourceInfo resourceInfo) formItem = new FormFieldInfo { - Name = "CRMSyncItemCreatedByKentico", Visible = false, DataType = "boolean", Enabled = true + Name = nameof(CRMSyncItemInfo.CRMSyncItemCreatedByKentico), Visible = false, DataType = "boolean", Enabled = true }; formInfo.AddFormItem(formItem); formItem = new FormFieldInfo { - Name = "CRMSyncItemLastModified", + Name = nameof(CRMSyncItemInfo.CRMSyncItemLastModified), Visible = false, Precision = 0, DataType = "datetime", @@ -148,8 +145,8 @@ private void InstallFailedSyncItemClass(ResourceInfo resourceInfo) failedSyncItemClass = DataClassInfo.New(FailedSyncItemInfo.OBJECT_TYPE); - failedSyncItemClass.ClassName = FailedSyncItemInfo.OBJECT_TYPE; - failedSyncItemClass.ClassTableName = FailedSyncItemInfo.OBJECT_TYPE.Replace(".", "_"); + failedSyncItemClass.ClassName = FailedSyncItemInfo.TYPEINFO.ObjectClassName; + failedSyncItemClass.ClassTableName = FailedSyncItemInfo.TYPEINFO.ObjectClassName.Replace(".", "_"); failedSyncItemClass.ClassDisplayName = "Failed sync item"; failedSyncItemClass.ClassResourceID = resourceInfo.ResourceID; failedSyncItemClass.ClassType = ClassType.OTHER; diff --git a/src/Kentico.Xperience.CRM.Common/Services/ICRMSyncItemService.cs b/src/Kentico.Xperience.CRM.Common/Services/ICRMSyncItemService.cs index c553c49..9769d69 100644 --- a/src/Kentico.Xperience.CRM.Common/Services/ICRMSyncItemService.cs +++ b/src/Kentico.Xperience.CRM.Common/Services/ICRMSyncItemService.cs @@ -5,7 +5,7 @@ namespace Kentico.Xperience.CRM.Common.Services; public interface ICRMSyncItemService { - void LogFormLeadCreateItem(BizFormItem bizFormItem, string crmId, string crmName); - void LogFormLeadUpdateItem(BizFormItem bizFormItem, string crmId, string crmName); - CRMSyncItemInfo? GetFormLeadSyncItem(BizFormItem bizFormItem, string crmName); + Task LogFormLeadCreateItem(BizFormItem bizFormItem, string crmId, string crmName); + Task LogFormLeadUpdateItem(BizFormItem bizFormItem, string crmId, string crmName); + Task GetFormLeadSyncItem(BizFormItem bizFormItem, string crmName); } \ No newline at end of file diff --git a/src/Kentico.Xperience.CRM.Common/Services/Implementations/CRMSyncItemService.cs b/src/Kentico.Xperience.CRM.Common/Services/Implementations/CRMSyncItemService.cs index 4adea0a..6f018f7 100644 --- a/src/Kentico.Xperience.CRM.Common/Services/Implementations/CRMSyncItemService.cs +++ b/src/Kentico.Xperience.CRM.Common/Services/Implementations/CRMSyncItemService.cs @@ -12,15 +12,15 @@ public CRMSyncItemService(ICRMSyncItemInfoProvider crmSyncItemInfoProvider) this.crmSyncItemInfoProvider = crmSyncItemInfoProvider; } - public void LogFormLeadCreateItem(BizFormItem bizFormItem, string crmId, string crmName) - => LogFormLeadSyncItem(bizFormItem, crmId, crmName, true); + public async Task LogFormLeadCreateItem(BizFormItem bizFormItem, string crmId, string crmName) + => await LogFormLeadSyncItem(bizFormItem, crmId, crmName, true); - public void LogFormLeadUpdateItem(BizFormItem bizFormItem, string crmId, string crmName) - => LogFormLeadSyncItem(bizFormItem, crmId, crmName, false); + public async Task LogFormLeadUpdateItem(BizFormItem bizFormItem, string crmId, string crmName) + => await LogFormLeadSyncItem(bizFormItem, crmId, crmName, false); - private void LogFormLeadSyncItem(BizFormItem bizFormItem, string crmId, string crmName, bool createdByKentico) + private async Task LogFormLeadSyncItem(BizFormItem bizFormItem, string crmId, string crmName, bool createdByKentico) { - var syncItem = GetFormLeadSyncItem(bizFormItem, crmName); + var syncItem = await GetFormLeadSyncItem(bizFormItem, crmName); if (syncItem is null) { new CRMSyncItemInfo @@ -40,12 +40,13 @@ private void LogFormLeadSyncItem(BizFormItem bizFormItem, string crmId, string c } } - public CRMSyncItemInfo? GetFormLeadSyncItem(BizFormItem bizFormItem, string crmName) - => crmSyncItemInfoProvider.Get() + public async Task GetFormLeadSyncItem(BizFormItem bizFormItem, string crmName) + => (await crmSyncItemInfoProvider.Get() .TopN(1) .WhereEquals(nameof(CRMSyncItemInfo.CRMSyncItemEntityClass), bizFormItem.BizFormClassName) .WhereEquals(nameof(CRMSyncItemInfo.CRMSyncItemEntityID), bizFormItem.ItemID) .WhereEquals(nameof(CRMSyncItemInfo.CRMSyncItemEntityCRM), crmName) + .GetEnumerableTypedResultAsync()) .FirstOrDefault(); } \ No newline at end of file diff --git a/src/Kentico.Xperience.CRM.Dynamics/Services/DynamicsLeadsIntegrationService.cs b/src/Kentico.Xperience.CRM.Dynamics/Services/DynamicsLeadsIntegrationService.cs index dcae75c..a8639a8 100644 --- a/src/Kentico.Xperience.CRM.Dynamics/Services/DynamicsLeadsIntegrationService.cs +++ b/src/Kentico.Xperience.CRM.Dynamics/Services/DynamicsLeadsIntegrationService.cs @@ -88,7 +88,7 @@ private async Task SynchronizeLeadAsync(BizFormItem bizFormItem, { try { - var syncItem = syncItemService.GetFormLeadSyncItem(bizFormItem, CRMType.Dynamics); + var syncItem = await syncItemService.GetFormLeadSyncItem(bizFormItem, CRMType.Dynamics); if (syncItem is null) { @@ -169,7 +169,7 @@ private async Task CreateLeadAsync(BizFormItem bizFormItem, var leadId = await serviceClient.CreateAsync(leadEntity); - syncItemService.LogFormLeadCreateItem(bizFormItem, leadId.ToString(), CRMType.Dynamics); + await syncItemService.LogFormLeadCreateItem(bizFormItem, leadId.ToString(), CRMType.Dynamics); failedSyncItemService.DeleteFailedSyncItem(CRMType.Dynamics, bizFormItem.BizFormClassName, bizFormItem.ItemID); } @@ -186,7 +186,7 @@ private async Task UpdateLeadAsync(Lead leadEntity, BizFormItem bizFormItem, await serviceClient.UpdateAsync(leadEntity); - syncItemService.LogFormLeadUpdateItem(bizFormItem, leadEntity.LeadId.ToString()!, CRMType.Dynamics); + await syncItemService.LogFormLeadUpdateItem(bizFormItem, leadEntity.LeadId.ToString()!, CRMType.Dynamics); failedSyncItemService.DeleteFailedSyncItem(CRMType.Dynamics, bizFormItem.BizFormClassName, bizFormItem.ItemID); } diff --git a/src/Kentico.Xperience.CRM.SalesForce/Services/SalesForceApiService.cs b/src/Kentico.Xperience.CRM.SalesForce/Services/SalesForceApiService.cs index 2f5a264..7368fc7 100644 --- a/src/Kentico.Xperience.CRM.SalesForce/Services/SalesForceApiService.cs +++ b/src/Kentico.Xperience.CRM.SalesForce/Services/SalesForceApiService.cs @@ -8,6 +8,7 @@ using System.Net.Http.Headers; using System.Net.Http.Json; using System.Net.Mime; +using System.Web; using SalesForceApiClient = SalesForce.OpenApi.SalesForceApiClient; namespace Kentico.Xperience.CRM.SalesForce.Services; @@ -84,7 +85,7 @@ public async Task UpdateLeadAsync(string id, LeadSObject leadSObject) var apiVersion = integrationSettings.Value.ApiConfig.ApiVersion.ToString("F1", CultureInfo.InvariantCulture); using var request = new HttpRequestMessage(HttpMethod.Get, - $"/services/data/v{apiVersion}/query?q=SELECT+Id+FROM+Lead+WHERE+Email='{email}'+ORDER+BY+CreatedDate+DESC"); + $"/services/data/v{apiVersion}/query?q=SELECT+Id+FROM+Lead+WHERE+Email='{HttpUtility.UrlEncode(email)}'+ORDER+BY+CreatedDate+DESC"); request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue(MediaTypeNames.Application.Json)); var response = await httpClient.SendAsync(request); diff --git a/src/Kentico.Xperience.CRM.SalesForce/Services/SalesForceLeadsIntegrationService.cs b/src/Kentico.Xperience.CRM.SalesForce/Services/SalesForceLeadsIntegrationService.cs index 3a07c13..8bf994f 100644 --- a/src/Kentico.Xperience.CRM.SalesForce/Services/SalesForceLeadsIntegrationService.cs +++ b/src/Kentico.Xperience.CRM.SalesForce/Services/SalesForceLeadsIntegrationService.cs @@ -83,7 +83,7 @@ private async Task SynchronizeLeadAsync(BizFormItem bizFormItem, { try { - var syncItem = syncItemService.GetFormLeadSyncItem(bizFormItem, CRMType.SalesForce); + var syncItem = await syncItemService.GetFormLeadSyncItem(bizFormItem, CRMType.SalesForce); if (syncItem is null) { @@ -163,7 +163,7 @@ private async Task CreateLeadAsync(BizFormItem bizFormItem, IEnumerable Date: Thu, 18 Jan 2024 15:34:33 +0100 Subject: [PATCH 14/23] version 28.0.0 upgrade --- Directory.Packages.props | 10 +- examples/DancingGoat/packages.lock.json | 118 +++++++++--------- .../packages.lock.json | 74 +++++------ .../packages.lock.json | 78 ++++++------ .../packages.lock.json | 78 ++++++------ 5 files changed, 179 insertions(+), 179 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index eb399ec..1e42625 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -6,11 +6,11 @@ true - - - - - + + + + + diff --git a/examples/DancingGoat/packages.lock.json b/examples/DancingGoat/packages.lock.json index 55250dc..8e9549b 100644 --- a/examples/DancingGoat/packages.lock.json +++ b/examples/DancingGoat/packages.lock.json @@ -4,50 +4,50 @@ "net6.0": { "Kentico.Xperience.Admin": { "type": "Direct", - "requested": "[27.0.1, )", - "resolved": "27.0.1", - "contentHash": "Kgk6v0S5OkIFUc/8cj6Nx/pP09Czz21Qwce/iqf4rWC9mnU7fcQ6sXlNmk1VL9jaNKb6XXrOZSWxjWk8AUVTJA==", + "requested": "[28.0.0, )", + "resolved": "28.0.0", + "contentHash": "G54CsUaiegQF1NS3+7p6uyWObSdBVbqfZCDlyzEQSAypZS8Gy1bc8p+SKTpjyNFyj+DpQcX9LJfbam2hUVNOUg==", "dependencies": { - "Kentico.Aira.Client": "1.0.22", - "Kentico.Xperience.WebApp": "[27.0.1]", - "Microsoft.AspNetCore.SpaServices.Extensions": "6.0.22", - "Microsoft.Extensions.FileProviders.Embedded": "6.0.22" + "Kentico.Aira.Client": "1.0.23", + "Kentico.Xperience.WebApp": "[28.0.0]", + "Microsoft.AspNetCore.SpaServices.Extensions": "6.0.24", + "Microsoft.Extensions.FileProviders.Embedded": "6.0.24" } }, "Kentico.Xperience.AzureStorage": { "type": "Direct", - "requested": "[27.0.1, )", - "resolved": "27.0.1", - "contentHash": "Mcz+7aTbElR/cXh15rOBGB1aqk+jWdOmpQqDXYgAcurjc/r6a2FDFv0GqaOAMXdGw2I6444CMereDNAtSn/fNg==", + "requested": "[28.0.0, )", + "resolved": "28.0.0", + "contentHash": "SZIUpq5LXusL/yKJFgBN8ICOMZXa0Idr/IwKXg2LiZImq5johO+6RomvWK2FFv988osnKlPPjobE9LxJJa1Z4g==", "dependencies": { - "Azure.Storage.Blobs": "12.18.0", - "Azure.Storage.Queues": "12.16.0", - "Kentico.Xperience.Core": "27.0.1", + "Azure.Storage.Blobs": "12.19.0", + "Azure.Storage.Queues": "12.17.0", + "Kentico.Xperience.Core": "28.0.0", "Newtonsoft.Json": "13.0.3" } }, "Kentico.Xperience.ImageProcessing": { "type": "Direct", - "requested": "[27.0.1, )", - "resolved": "27.0.1", - "contentHash": "xhENw/0IaRxtugPbzqvYNu/gCYzO7FvVLvNF+dSPL9ZXxBpCiGRLyrU5jihX6qVXW9PzGQAMABfrFJP1viBIfA==", + "requested": "[28.0.0, )", + "resolved": "28.0.0", + "contentHash": "4XKHKrWVI1kI4Ou+9MD7mnNgbpUe4dcSBSWZDXvajGA9YL6PTAfGErJEQuNUSSvJitSsW5CGzkZ1EsePasScQw==", "dependencies": { - "Kentico.Xperience.Core": "27.0.1", + "Kentico.Xperience.Core": "28.0.0", "SkiaSharp": "2.88.6", "SkiaSharp.NativeAssets.Linux.NoDependencies": "2.88.6" } }, "Kentico.Xperience.WebApp": { "type": "Direct", - "requested": "[27.0.1, )", - "resolved": "27.0.1", - "contentHash": "2wpyDo6wZmQXq9HFNExtNWsJ8hniR0ba1MX6hHG1UjpxuVkG9tFETK3QAefB3FWXYRa8PdpD2RtvSTVwlen3Lw==", + "requested": "[28.0.0, )", + "resolved": "28.0.0", + "contentHash": "R54QHACtE8rfGtHKKFpSvY7LKI1IqqALe70QGYz1u6ZyZ23w0O7R9UZH5bgW/Ww0d13IgGQBc3ULaMel9bxJXg==", "dependencies": { "CommandLineParser": "2.9.1", - "HtmlSanitizer": "8.0.723", - "Kentico.Xperience.Core": "[27.0.1]", - "Microsoft.Extensions.FileProviders.Embedded": "6.0.22", - "Microsoft.Extensions.Localization": "6.0.22" + "HtmlSanitizer": "8.0.746", + "Kentico.Xperience.Core": "[28.0.0]", + "Microsoft.Extensions.FileProviders.Embedded": "6.0.24", + "Microsoft.Extensions.Localization": "6.0.24" } }, "AngleSharp": { @@ -97,17 +97,17 @@ }, "Azure.Storage.Blobs": { "type": "Transitive", - "resolved": "12.18.0", - "contentHash": "IUqHRXnabXCzmmvkzqPK4FuGzLxmKSugDEt5Hm5B/JlJFR+aHDsPW4nCLbG0txThqBSKPqcBBU/oA6c5TaFJgA==", + "resolved": "12.19.0", + "contentHash": "5nf/hxY/7XLIwiqOt8b3y+EkY8PcK8s8IFXBdKMEc2nLTuF50cqBuxP7jqQooulhbA6y5QbgriZAYOxIXB3koQ==", "dependencies": { - "Azure.Storage.Common": "12.17.0", + "Azure.Storage.Common": "12.18.0", "System.Text.Json": "4.7.2" } }, "Azure.Storage.Common": { "type": "Transitive", - "resolved": "12.17.0", - "contentHash": "/h8SpUkxMuQy/MbNFeJQGmhYt3JnYfEiGeDojtNgLNzzhyDnRYgjF3ZKYgjORYQpn0Spr+4+v2MZy+0GNJBLrg==", + "resolved": "12.18.0", + "contentHash": "lAYPjO4TRIyomalFkqIgZ0RN4I5zxjBydF24g+Aez0VR4WFx6JPLslhA082+xmwVTSeIz906dYeD2iVZDv1UaQ==", "dependencies": { "Azure.Core": "1.35.0", "System.IO.Hashing": "6.0.0" @@ -115,10 +115,10 @@ }, "Azure.Storage.Queues": { "type": "Transitive", - "resolved": "12.16.0", - "contentHash": "ociB+g4P8d4o6K6dsCxz4qVNyGzmTKC5t7wlDLAezbfAp4m41SAUvxcDU0N4Mqxf04GPQeyImSeMFQfDzZHEcQ==", + "resolved": "12.17.0", + "contentHash": "Vam5f5xYsdX1hf4oHXMnC9hNpR/Ml9Dbp3gpf6yRVtHVkf33IwdVQ2X8FfkJ567rl74mwHZbWaoGeV6a0YA5LA==", "dependencies": { - "Azure.Storage.Common": "12.17.0", + "Azure.Storage.Common": "12.18.0", "System.Memory.Data": "1.0.2", "System.Text.Json": "4.7.2" } @@ -149,8 +149,8 @@ }, "HtmlSanitizer": { "type": "Transitive", - "resolved": "8.0.723", - "contentHash": "C4RZX+Mv9OqY+sAM3SD3BdLxvtr9QimIGvLvN5SDjbi7rb6ibeHhGnQA5EyKbkiuQKHO6MBa3h2AZQzjy6z9HA==", + "resolved": "8.0.746", + "contentHash": "9InxDYXIEnCnWQN2eCLCNL3QGyhmV0+vn12QlVSPig5m0f6XisAM9pq+TWB8aUtxXm1zm39/rOC8Ed7eZaEYew==", "dependencies": { "AngleSharp": "[0.17.1]", "AngleSharp.Css": "[0.17.0]", @@ -159,8 +159,8 @@ }, "Kentico.Aira.Client": { "type": "Transitive", - "resolved": "1.0.22", - "contentHash": "/slLHi7JWaKCBl0EZa01rNqjoaGfOgRj3vNA2wfE88chM43YOPygl22OVEMnyvVLd5nNzzhgv6iX0QyzQlGVxQ==", + "resolved": "1.0.23", + "contentHash": "16jt+oHW6Fa6fDmJlJBTJuMK+A6nwXDX7IcYygYFzq01Sek43oQB2wjhXPRhntsvFYhAbWcBwXPzn0ALlMggSQ==", "dependencies": { "Microsoft.Extensions.DependencyInjection": "6.0.1", "Microsoft.Extensions.Http": "6.0.0", @@ -187,8 +187,8 @@ }, "Microsoft.AspNetCore.SpaServices.Extensions": { "type": "Transitive", - "resolved": "6.0.22", - "contentHash": "RE17e7KHUhyNWwWL93uJzErUZpRjWU71oT4rUbafjA44gEz5YDAfizxAkM0XYCbLBRCbXtoK4q8DJkJvm/LjSA==", + "resolved": "6.0.24", + "contentHash": "onDzS4i2iLMVHtqnTNEFuik2wqarpi88e8Bz/K6FOHwwj+z9kIvcjdmV8qLpC8effWiQot/JHiGzEtsZA/8yWQ==", "dependencies": { "Microsoft.Extensions.FileProviders.Physical": "6.0.0" } @@ -205,11 +205,11 @@ }, "Microsoft.Data.SqlClient": { "type": "Transitive", - "resolved": "5.1.1", - "contentHash": "MW5E9HFvCaV069o8b6YpuRDPBux8s96qDnOJ+4N9QNUCs7c5W3KxwQ+ftpAjbMUlImL+c9WR+l+f5hzjkqhu2g==", + "resolved": "5.1.2", + "contentHash": "q/F1HTOn9QLwgRp4esJIA1b2X15faeV8WozkNhvU3Zk0DcYDWUsEf5WMkbypY4CaNkZntOduods5wLyv8I699w==", "dependencies": { "Azure.Identity": "1.7.0", - "Microsoft.Data.SqlClient.SNI.runtime": "5.1.0", + "Microsoft.Data.SqlClient.SNI.runtime": "5.1.1", "Microsoft.Identity.Client": "4.47.2", "Microsoft.IdentityModel.JsonWebTokens": "6.24.0", "Microsoft.IdentityModel.Protocols.OpenIdConnect": "6.24.0", @@ -225,8 +225,8 @@ }, "Microsoft.Data.SqlClient.SNI.runtime": { "type": "Transitive", - "resolved": "5.1.0", - "contentHash": "jVsElisM5sfBzaaV9kdq2NXZLwIbytetnsOIlJ0cQGgQP4zFNBmkfHBnpwtmKrtBJBEV9+9PVQPVrcCVhDgcIg==" + "resolved": "5.1.1", + "contentHash": "wNGM5ZTQCa2blc9ikXQouybGiyMd6IHPVJvAlBEPtr6JepZEOYeDxGyprYvFVeOxlCXs7avridZQ0nYkHzQWCQ==" }, "Microsoft.Extensions.Caching.Abstractions": { "type": "Transitive", @@ -297,8 +297,8 @@ }, "Microsoft.Extensions.FileProviders.Embedded": { "type": "Transitive", - "resolved": "6.0.22", - "contentHash": "kT/7vO2tq68iWQKVeIpH4VI6BDMeGQRaPUoiS+ZIY8QFBNgJNLpKoFC7JExRLMc2j1pFWHPDdToQKpMYSbVXiw==", + "resolved": "6.0.24", + "contentHash": "HvTvzEkqYwDykjaekrpntZkxetMQ5XinIRYoRKftCqttmABoSfsOpsxCh0eIw9w4H+K8xHN7nqvCWFPP8feT4w==", "dependencies": { "Microsoft.Extensions.FileProviders.Abstractions": "6.0.0" } @@ -341,19 +341,19 @@ }, "Microsoft.Extensions.Localization": { "type": "Transitive", - "resolved": "6.0.22", - "contentHash": "o2Z7kwFxxTUiz2e9TX4Jp6DPI1+uySq5ObGkHXt9B5yMLKX6hGR0SNuwJY4/CL8X2n1m8ppNNCFKVqAQ1++XiA==", + "resolved": "6.0.24", + "contentHash": "PrWXSaBQWtyfOSh5sZrvz3Gz2mCOFlMne8a3I42ipHej0Nd5/RX9kEDs803y2fXO+2aU76J9vHbW39WuzkZWgg==", "dependencies": { "Microsoft.Extensions.DependencyInjection.Abstractions": "6.0.0", - "Microsoft.Extensions.Localization.Abstractions": "6.0.22", + "Microsoft.Extensions.Localization.Abstractions": "6.0.24", "Microsoft.Extensions.Logging.Abstractions": "6.0.4", "Microsoft.Extensions.Options": "6.0.0" } }, "Microsoft.Extensions.Localization.Abstractions": { "type": "Transitive", - "resolved": "6.0.22", - "contentHash": "NwkDOL4dXE9JeHMqaSiMjj/19vaNztcO6X6XPCSKQBn4pCCeHXfA+YNokplnjhMv4QrB2cJSjGMoT8XC+USR/g==" + "resolved": "6.0.24", + "contentHash": "71jCMmXcswCH5poRNE/gKBbRZDSIZ6W5EbWJHRRvuA/yUy2/Cabor6sXoYDJBNQ58B9m28RM9nou+dO7O+HK0g==" }, "Microsoft.Extensions.Logging": { "type": "Transitive", @@ -1379,8 +1379,8 @@ "kentico.xperience.crm.common": { "type": "Project", "dependencies": { - "Kentico.Xperience.Admin": "[27.0.1, )", - "Kentico.Xperience.Core": "[27.0.1, )" + "Kentico.Xperience.Admin": "[28.0.0, )", + "Kentico.Xperience.Core": "[28.0.0, )" } }, "kentico.xperience.crm.salesforce": { @@ -1389,14 +1389,14 @@ "Duende.AccessTokenManagement.OpenIdConnect": "[2.0.3, )", "IdentityModel": "[6.2.0, )", "Kentico.Xperience.CRM.Common": "[1.0.0-prerelease-1, )", - "Kentico.Xperience.Core": "[27.0.1, )" + "Kentico.Xperience.Core": "[28.0.0, )" } }, "Kentico.Xperience.Dynamics.CRM.Integration": { "type": "Project", "dependencies": { "Kentico.Xperience.CRM.Common": "[1.0.0-prerelease-1, )", - "Kentico.Xperience.Core": "[27.0.1, )", + "Kentico.Xperience.Core": "[28.0.0, )", "Microsoft.PowerPlatform.Dataverse.Client": "[1.1.14, )" } }, @@ -1418,20 +1418,20 @@ }, "Kentico.Xperience.Core": { "type": "CentralTransitive", - "requested": "[27.0.1, )", - "resolved": "27.0.1", - "contentHash": "CNNHGX6JWC6UtaW6VNhytXUqQecI4qhkW4DjpQejcHTGHVLC6tEMiMQ0jifSKnWDrMBFY1DqIvjiiJiG93MBkw==", + "requested": "[28.0.0, )", + "resolved": "28.0.0", + "contentHash": "lXfjf8hSN5nrtPHW5IIGZtqdE+Gt03chQprK9yjhAbH2+JyxBbAHRmbfaCkgEWCwc813z22b+goauTDT1ltpoA==", "dependencies": { "AngleSharp": "0.17.1", "MailKit": "4.2.0", - "Microsoft.Data.SqlClient": "5.1.1", + "Microsoft.Data.SqlClient": "5.1.2", "Microsoft.Extensions.Caching.Memory": "6.0.1", "Microsoft.Extensions.Configuration": "6.0.1", "Microsoft.Extensions.Configuration.Binder": "6.0.0", "Microsoft.Extensions.DependencyInjection": "6.0.1", "Microsoft.Extensions.FileProviders.Physical": "6.0.0", "Microsoft.Extensions.Hosting.Abstractions": "6.0.0", - "Microsoft.Extensions.Localization": "6.0.22", + "Microsoft.Extensions.Localization": "6.0.24", "Microsoft.Extensions.Options.ConfigurationExtensions": "6.0.0", "Mono.Cecil": "0.11.5", "Newtonsoft.Json": "13.0.3", diff --git a/src/Kentico.Xperience.CRM.Common/packages.lock.json b/src/Kentico.Xperience.CRM.Common/packages.lock.json index 4a57b99..207960e 100644 --- a/src/Kentico.Xperience.CRM.Common/packages.lock.json +++ b/src/Kentico.Xperience.CRM.Common/packages.lock.json @@ -4,32 +4,32 @@ "net6.0": { "Kentico.Xperience.Admin": { "type": "Direct", - "requested": "[27.0.1, )", - "resolved": "27.0.1", - "contentHash": "Kgk6v0S5OkIFUc/8cj6Nx/pP09Czz21Qwce/iqf4rWC9mnU7fcQ6sXlNmk1VL9jaNKb6XXrOZSWxjWk8AUVTJA==", + "requested": "[28.0.0, )", + "resolved": "28.0.0", + "contentHash": "G54CsUaiegQF1NS3+7p6uyWObSdBVbqfZCDlyzEQSAypZS8Gy1bc8p+SKTpjyNFyj+DpQcX9LJfbam2hUVNOUg==", "dependencies": { - "Kentico.Aira.Client": "1.0.22", - "Kentico.Xperience.WebApp": "[27.0.1]", - "Microsoft.AspNetCore.SpaServices.Extensions": "6.0.22", - "Microsoft.Extensions.FileProviders.Embedded": "6.0.22" + "Kentico.Aira.Client": "1.0.23", + "Kentico.Xperience.WebApp": "[28.0.0]", + "Microsoft.AspNetCore.SpaServices.Extensions": "6.0.24", + "Microsoft.Extensions.FileProviders.Embedded": "6.0.24" } }, "Kentico.Xperience.Core": { "type": "Direct", - "requested": "[27.0.1, )", - "resolved": "27.0.1", - "contentHash": "CNNHGX6JWC6UtaW6VNhytXUqQecI4qhkW4DjpQejcHTGHVLC6tEMiMQ0jifSKnWDrMBFY1DqIvjiiJiG93MBkw==", + "requested": "[28.0.0, )", + "resolved": "28.0.0", + "contentHash": "lXfjf8hSN5nrtPHW5IIGZtqdE+Gt03chQprK9yjhAbH2+JyxBbAHRmbfaCkgEWCwc813z22b+goauTDT1ltpoA==", "dependencies": { "AngleSharp": "0.17.1", "MailKit": "4.2.0", - "Microsoft.Data.SqlClient": "5.1.1", + "Microsoft.Data.SqlClient": "5.1.2", "Microsoft.Extensions.Caching.Memory": "6.0.1", "Microsoft.Extensions.Configuration": "6.0.1", "Microsoft.Extensions.Configuration.Binder": "6.0.0", "Microsoft.Extensions.DependencyInjection": "6.0.1", "Microsoft.Extensions.FileProviders.Physical": "6.0.0", "Microsoft.Extensions.Hosting.Abstractions": "6.0.0", - "Microsoft.Extensions.Localization": "6.0.22", + "Microsoft.Extensions.Localization": "6.0.24", "Microsoft.Extensions.Options.ConfigurationExtensions": "6.0.0", "Mono.Cecil": "0.11.5", "Newtonsoft.Json": "13.0.3", @@ -110,8 +110,8 @@ }, "HtmlSanitizer": { "type": "Transitive", - "resolved": "8.0.723", - "contentHash": "C4RZX+Mv9OqY+sAM3SD3BdLxvtr9QimIGvLvN5SDjbi7rb6ibeHhGnQA5EyKbkiuQKHO6MBa3h2AZQzjy6z9HA==", + "resolved": "8.0.746", + "contentHash": "9InxDYXIEnCnWQN2eCLCNL3QGyhmV0+vn12QlVSPig5m0f6XisAM9pq+TWB8aUtxXm1zm39/rOC8Ed7eZaEYew==", "dependencies": { "AngleSharp": "[0.17.1]", "AngleSharp.Css": "[0.17.0]", @@ -120,8 +120,8 @@ }, "Kentico.Aira.Client": { "type": "Transitive", - "resolved": "1.0.22", - "contentHash": "/slLHi7JWaKCBl0EZa01rNqjoaGfOgRj3vNA2wfE88chM43YOPygl22OVEMnyvVLd5nNzzhgv6iX0QyzQlGVxQ==", + "resolved": "1.0.23", + "contentHash": "16jt+oHW6Fa6fDmJlJBTJuMK+A6nwXDX7IcYygYFzq01Sek43oQB2wjhXPRhntsvFYhAbWcBwXPzn0ALlMggSQ==", "dependencies": { "Microsoft.Extensions.DependencyInjection": "6.0.1", "Microsoft.Extensions.Http": "6.0.0", @@ -140,8 +140,8 @@ }, "Microsoft.AspNetCore.SpaServices.Extensions": { "type": "Transitive", - "resolved": "6.0.22", - "contentHash": "RE17e7KHUhyNWwWL93uJzErUZpRjWU71oT4rUbafjA44gEz5YDAfizxAkM0XYCbLBRCbXtoK4q8DJkJvm/LjSA==", + "resolved": "6.0.24", + "contentHash": "onDzS4i2iLMVHtqnTNEFuik2wqarpi88e8Bz/K6FOHwwj+z9kIvcjdmV8qLpC8effWiQot/JHiGzEtsZA/8yWQ==", "dependencies": { "Microsoft.Extensions.FileProviders.Physical": "6.0.0" } @@ -163,11 +163,11 @@ }, "Microsoft.Data.SqlClient": { "type": "Transitive", - "resolved": "5.1.1", - "contentHash": "MW5E9HFvCaV069o8b6YpuRDPBux8s96qDnOJ+4N9QNUCs7c5W3KxwQ+ftpAjbMUlImL+c9WR+l+f5hzjkqhu2g==", + "resolved": "5.1.2", + "contentHash": "q/F1HTOn9QLwgRp4esJIA1b2X15faeV8WozkNhvU3Zk0DcYDWUsEf5WMkbypY4CaNkZntOduods5wLyv8I699w==", "dependencies": { "Azure.Identity": "1.7.0", - "Microsoft.Data.SqlClient.SNI.runtime": "5.1.0", + "Microsoft.Data.SqlClient.SNI.runtime": "5.1.1", "Microsoft.Identity.Client": "4.47.2", "Microsoft.IdentityModel.JsonWebTokens": "6.24.0", "Microsoft.IdentityModel.Protocols.OpenIdConnect": "6.24.0", @@ -183,8 +183,8 @@ }, "Microsoft.Data.SqlClient.SNI.runtime": { "type": "Transitive", - "resolved": "5.1.0", - "contentHash": "jVsElisM5sfBzaaV9kdq2NXZLwIbytetnsOIlJ0cQGgQP4zFNBmkfHBnpwtmKrtBJBEV9+9PVQPVrcCVhDgcIg==" + "resolved": "5.1.1", + "contentHash": "wNGM5ZTQCa2blc9ikXQouybGiyMd6IHPVJvAlBEPtr6JepZEOYeDxGyprYvFVeOxlCXs7avridZQ0nYkHzQWCQ==" }, "Microsoft.Extensions.Caching.Abstractions": { "type": "Transitive", @@ -255,8 +255,8 @@ }, "Microsoft.Extensions.FileProviders.Embedded": { "type": "Transitive", - "resolved": "6.0.22", - "contentHash": "kT/7vO2tq68iWQKVeIpH4VI6BDMeGQRaPUoiS+ZIY8QFBNgJNLpKoFC7JExRLMc2j1pFWHPDdToQKpMYSbVXiw==", + "resolved": "6.0.24", + "contentHash": "HvTvzEkqYwDykjaekrpntZkxetMQ5XinIRYoRKftCqttmABoSfsOpsxCh0eIw9w4H+K8xHN7nqvCWFPP8feT4w==", "dependencies": { "Microsoft.Extensions.FileProviders.Abstractions": "6.0.0" } @@ -299,19 +299,19 @@ }, "Microsoft.Extensions.Localization": { "type": "Transitive", - "resolved": "6.0.22", - "contentHash": "o2Z7kwFxxTUiz2e9TX4Jp6DPI1+uySq5ObGkHXt9B5yMLKX6hGR0SNuwJY4/CL8X2n1m8ppNNCFKVqAQ1++XiA==", + "resolved": "6.0.24", + "contentHash": "PrWXSaBQWtyfOSh5sZrvz3Gz2mCOFlMne8a3I42ipHej0Nd5/RX9kEDs803y2fXO+2aU76J9vHbW39WuzkZWgg==", "dependencies": { "Microsoft.Extensions.DependencyInjection.Abstractions": "6.0.0", - "Microsoft.Extensions.Localization.Abstractions": "6.0.22", + "Microsoft.Extensions.Localization.Abstractions": "6.0.24", "Microsoft.Extensions.Logging.Abstractions": "6.0.4", "Microsoft.Extensions.Options": "6.0.0" } }, "Microsoft.Extensions.Localization.Abstractions": { "type": "Transitive", - "resolved": "6.0.22", - "contentHash": "NwkDOL4dXE9JeHMqaSiMjj/19vaNztcO6X6XPCSKQBn4pCCeHXfA+YNokplnjhMv4QrB2cJSjGMoT8XC+USR/g==" + "resolved": "6.0.24", + "contentHash": "71jCMmXcswCH5poRNE/gKBbRZDSIZ6W5EbWJHRRvuA/yUy2/Cabor6sXoYDJBNQ58B9m28RM9nou+dO7O+HK0g==" }, "Microsoft.Extensions.Logging": { "type": "Transitive", @@ -658,15 +658,15 @@ }, "Kentico.Xperience.WebApp": { "type": "CentralTransitive", - "requested": "[27.0.1, )", - "resolved": "27.0.1", - "contentHash": "2wpyDo6wZmQXq9HFNExtNWsJ8hniR0ba1MX6hHG1UjpxuVkG9tFETK3QAefB3FWXYRa8PdpD2RtvSTVwlen3Lw==", + "requested": "[28.0.0, )", + "resolved": "28.0.0", + "contentHash": "R54QHACtE8rfGtHKKFpSvY7LKI1IqqALe70QGYz1u6ZyZ23w0O7R9UZH5bgW/Ww0d13IgGQBc3ULaMel9bxJXg==", "dependencies": { "CommandLineParser": "2.9.1", - "HtmlSanitizer": "8.0.723", - "Kentico.Xperience.Core": "[27.0.1]", - "Microsoft.Extensions.FileProviders.Embedded": "6.0.22", - "Microsoft.Extensions.Localization": "6.0.22" + "HtmlSanitizer": "8.0.746", + "Kentico.Xperience.Core": "[28.0.0]", + "Microsoft.Extensions.FileProviders.Embedded": "6.0.24", + "Microsoft.Extensions.Localization": "6.0.24" } } } diff --git a/src/Kentico.Xperience.CRM.Dynamics/packages.lock.json b/src/Kentico.Xperience.CRM.Dynamics/packages.lock.json index b583371..7aea801 100644 --- a/src/Kentico.Xperience.CRM.Dynamics/packages.lock.json +++ b/src/Kentico.Xperience.CRM.Dynamics/packages.lock.json @@ -4,20 +4,20 @@ "net6.0": { "Kentico.Xperience.Core": { "type": "Direct", - "requested": "[27.0.1, )", - "resolved": "27.0.1", - "contentHash": "CNNHGX6JWC6UtaW6VNhytXUqQecI4qhkW4DjpQejcHTGHVLC6tEMiMQ0jifSKnWDrMBFY1DqIvjiiJiG93MBkw==", + "requested": "[28.0.0, )", + "resolved": "28.0.0", + "contentHash": "lXfjf8hSN5nrtPHW5IIGZtqdE+Gt03chQprK9yjhAbH2+JyxBbAHRmbfaCkgEWCwc813z22b+goauTDT1ltpoA==", "dependencies": { "AngleSharp": "0.17.1", "MailKit": "4.2.0", - "Microsoft.Data.SqlClient": "5.1.1", + "Microsoft.Data.SqlClient": "5.1.2", "Microsoft.Extensions.Caching.Memory": "6.0.1", "Microsoft.Extensions.Configuration": "6.0.1", "Microsoft.Extensions.Configuration.Binder": "6.0.0", "Microsoft.Extensions.DependencyInjection": "6.0.1", "Microsoft.Extensions.FileProviders.Physical": "6.0.0", "Microsoft.Extensions.Hosting.Abstractions": "6.0.0", - "Microsoft.Extensions.Localization": "6.0.22", + "Microsoft.Extensions.Localization": "6.0.24", "Microsoft.Extensions.Options.ConfigurationExtensions": "6.0.0", "Mono.Cecil": "0.11.5", "Newtonsoft.Json": "13.0.3", @@ -125,8 +125,8 @@ }, "HtmlSanitizer": { "type": "Transitive", - "resolved": "8.0.723", - "contentHash": "C4RZX+Mv9OqY+sAM3SD3BdLxvtr9QimIGvLvN5SDjbi7rb6ibeHhGnQA5EyKbkiuQKHO6MBa3h2AZQzjy6z9HA==", + "resolved": "8.0.746", + "contentHash": "9InxDYXIEnCnWQN2eCLCNL3QGyhmV0+vn12QlVSPig5m0f6XisAM9pq+TWB8aUtxXm1zm39/rOC8Ed7eZaEYew==", "dependencies": { "AngleSharp": "[0.17.1]", "AngleSharp.Css": "[0.17.0]", @@ -135,8 +135,8 @@ }, "Kentico.Aira.Client": { "type": "Transitive", - "resolved": "1.0.22", - "contentHash": "/slLHi7JWaKCBl0EZa01rNqjoaGfOgRj3vNA2wfE88chM43YOPygl22OVEMnyvVLd5nNzzhgv6iX0QyzQlGVxQ==", + "resolved": "1.0.23", + "contentHash": "16jt+oHW6Fa6fDmJlJBTJuMK+A6nwXDX7IcYygYFzq01Sek43oQB2wjhXPRhntsvFYhAbWcBwXPzn0ALlMggSQ==", "dependencies": { "Microsoft.Extensions.DependencyInjection": "6.0.1", "Microsoft.Extensions.Http": "6.0.0", @@ -155,8 +155,8 @@ }, "Microsoft.AspNetCore.SpaServices.Extensions": { "type": "Transitive", - "resolved": "6.0.22", - "contentHash": "RE17e7KHUhyNWwWL93uJzErUZpRjWU71oT4rUbafjA44gEz5YDAfizxAkM0XYCbLBRCbXtoK4q8DJkJvm/LjSA==", + "resolved": "6.0.24", + "contentHash": "onDzS4i2iLMVHtqnTNEFuik2wqarpi88e8Bz/K6FOHwwj+z9kIvcjdmV8qLpC8effWiQot/JHiGzEtsZA/8yWQ==", "dependencies": { "Microsoft.Extensions.FileProviders.Physical": "6.0.0" } @@ -173,11 +173,11 @@ }, "Microsoft.Data.SqlClient": { "type": "Transitive", - "resolved": "5.1.1", - "contentHash": "MW5E9HFvCaV069o8b6YpuRDPBux8s96qDnOJ+4N9QNUCs7c5W3KxwQ+ftpAjbMUlImL+c9WR+l+f5hzjkqhu2g==", + "resolved": "5.1.2", + "contentHash": "q/F1HTOn9QLwgRp4esJIA1b2X15faeV8WozkNhvU3Zk0DcYDWUsEf5WMkbypY4CaNkZntOduods5wLyv8I699w==", "dependencies": { "Azure.Identity": "1.7.0", - "Microsoft.Data.SqlClient.SNI.runtime": "5.1.0", + "Microsoft.Data.SqlClient.SNI.runtime": "5.1.1", "Microsoft.Identity.Client": "4.47.2", "Microsoft.IdentityModel.JsonWebTokens": "6.24.0", "Microsoft.IdentityModel.Protocols.OpenIdConnect": "6.24.0", @@ -193,8 +193,8 @@ }, "Microsoft.Data.SqlClient.SNI.runtime": { "type": "Transitive", - "resolved": "5.1.0", - "contentHash": "jVsElisM5sfBzaaV9kdq2NXZLwIbytetnsOIlJ0cQGgQP4zFNBmkfHBnpwtmKrtBJBEV9+9PVQPVrcCVhDgcIg==" + "resolved": "5.1.1", + "contentHash": "wNGM5ZTQCa2blc9ikXQouybGiyMd6IHPVJvAlBEPtr6JepZEOYeDxGyprYvFVeOxlCXs7avridZQ0nYkHzQWCQ==" }, "Microsoft.Extensions.Caching.Abstractions": { "type": "Transitive", @@ -265,8 +265,8 @@ }, "Microsoft.Extensions.FileProviders.Embedded": { "type": "Transitive", - "resolved": "6.0.22", - "contentHash": "kT/7vO2tq68iWQKVeIpH4VI6BDMeGQRaPUoiS+ZIY8QFBNgJNLpKoFC7JExRLMc2j1pFWHPDdToQKpMYSbVXiw==", + "resolved": "6.0.24", + "contentHash": "HvTvzEkqYwDykjaekrpntZkxetMQ5XinIRYoRKftCqttmABoSfsOpsxCh0eIw9w4H+K8xHN7nqvCWFPP8feT4w==", "dependencies": { "Microsoft.Extensions.FileProviders.Abstractions": "6.0.0" } @@ -309,19 +309,19 @@ }, "Microsoft.Extensions.Localization": { "type": "Transitive", - "resolved": "6.0.22", - "contentHash": "o2Z7kwFxxTUiz2e9TX4Jp6DPI1+uySq5ObGkHXt9B5yMLKX6hGR0SNuwJY4/CL8X2n1m8ppNNCFKVqAQ1++XiA==", + "resolved": "6.0.24", + "contentHash": "PrWXSaBQWtyfOSh5sZrvz3Gz2mCOFlMne8a3I42ipHej0Nd5/RX9kEDs803y2fXO+2aU76J9vHbW39WuzkZWgg==", "dependencies": { "Microsoft.Extensions.DependencyInjection.Abstractions": "6.0.0", - "Microsoft.Extensions.Localization.Abstractions": "6.0.22", + "Microsoft.Extensions.Localization.Abstractions": "6.0.24", "Microsoft.Extensions.Logging.Abstractions": "6.0.4", "Microsoft.Extensions.Options": "6.0.0" } }, "Microsoft.Extensions.Localization.Abstractions": { "type": "Transitive", - "resolved": "6.0.22", - "contentHash": "NwkDOL4dXE9JeHMqaSiMjj/19vaNztcO6X6XPCSKQBn4pCCeHXfA+YNokplnjhMv4QrB2cJSjGMoT8XC+USR/g==" + "resolved": "6.0.24", + "contentHash": "71jCMmXcswCH5poRNE/gKBbRZDSIZ6W5EbWJHRRvuA/yUy2/Cabor6sXoYDJBNQ58B9m28RM9nou+dO7O+HK0g==" }, "Microsoft.Extensions.Logging": { "type": "Transitive", @@ -1315,33 +1315,33 @@ "kentico.xperience.crm.common": { "type": "Project", "dependencies": { - "Kentico.Xperience.Admin": "[27.0.1, )", - "Kentico.Xperience.Core": "[27.0.1, )" + "Kentico.Xperience.Admin": "[28.0.0, )", + "Kentico.Xperience.Core": "[28.0.0, )" } }, "Kentico.Xperience.Admin": { "type": "CentralTransitive", - "requested": "[27.0.1, )", - "resolved": "27.0.1", - "contentHash": "Kgk6v0S5OkIFUc/8cj6Nx/pP09Czz21Qwce/iqf4rWC9mnU7fcQ6sXlNmk1VL9jaNKb6XXrOZSWxjWk8AUVTJA==", + "requested": "[28.0.0, )", + "resolved": "28.0.0", + "contentHash": "G54CsUaiegQF1NS3+7p6uyWObSdBVbqfZCDlyzEQSAypZS8Gy1bc8p+SKTpjyNFyj+DpQcX9LJfbam2hUVNOUg==", "dependencies": { - "Kentico.Aira.Client": "1.0.22", - "Kentico.Xperience.WebApp": "[27.0.1]", - "Microsoft.AspNetCore.SpaServices.Extensions": "6.0.22", - "Microsoft.Extensions.FileProviders.Embedded": "6.0.22" + "Kentico.Aira.Client": "1.0.23", + "Kentico.Xperience.WebApp": "[28.0.0]", + "Microsoft.AspNetCore.SpaServices.Extensions": "6.0.24", + "Microsoft.Extensions.FileProviders.Embedded": "6.0.24" } }, "Kentico.Xperience.WebApp": { "type": "CentralTransitive", - "requested": "[27.0.1, )", - "resolved": "27.0.1", - "contentHash": "2wpyDo6wZmQXq9HFNExtNWsJ8hniR0ba1MX6hHG1UjpxuVkG9tFETK3QAefB3FWXYRa8PdpD2RtvSTVwlen3Lw==", + "requested": "[28.0.0, )", + "resolved": "28.0.0", + "contentHash": "R54QHACtE8rfGtHKKFpSvY7LKI1IqqALe70QGYz1u6ZyZ23w0O7R9UZH5bgW/Ww0d13IgGQBc3ULaMel9bxJXg==", "dependencies": { "CommandLineParser": "2.9.1", - "HtmlSanitizer": "8.0.723", - "Kentico.Xperience.Core": "[27.0.1]", - "Microsoft.Extensions.FileProviders.Embedded": "6.0.22", - "Microsoft.Extensions.Localization": "6.0.22" + "HtmlSanitizer": "8.0.746", + "Kentico.Xperience.Core": "[28.0.0]", + "Microsoft.Extensions.FileProviders.Embedded": "6.0.24", + "Microsoft.Extensions.Localization": "6.0.24" } } } diff --git a/src/Kentico.Xperience.CRM.SalesForce/packages.lock.json b/src/Kentico.Xperience.CRM.SalesForce/packages.lock.json index 06a4cc7..05a99a2 100644 --- a/src/Kentico.Xperience.CRM.SalesForce/packages.lock.json +++ b/src/Kentico.Xperience.CRM.SalesForce/packages.lock.json @@ -20,20 +20,20 @@ }, "Kentico.Xperience.Core": { "type": "Direct", - "requested": "[27.0.1, )", - "resolved": "27.0.1", - "contentHash": "CNNHGX6JWC6UtaW6VNhytXUqQecI4qhkW4DjpQejcHTGHVLC6tEMiMQ0jifSKnWDrMBFY1DqIvjiiJiG93MBkw==", + "requested": "[28.0.0, )", + "resolved": "28.0.0", + "contentHash": "lXfjf8hSN5nrtPHW5IIGZtqdE+Gt03chQprK9yjhAbH2+JyxBbAHRmbfaCkgEWCwc813z22b+goauTDT1ltpoA==", "dependencies": { "AngleSharp": "0.17.1", "MailKit": "4.2.0", - "Microsoft.Data.SqlClient": "5.1.1", + "Microsoft.Data.SqlClient": "5.1.2", "Microsoft.Extensions.Caching.Memory": "6.0.1", "Microsoft.Extensions.Configuration": "6.0.1", "Microsoft.Extensions.Configuration.Binder": "6.0.0", "Microsoft.Extensions.DependencyInjection": "6.0.1", "Microsoft.Extensions.FileProviders.Physical": "6.0.0", "Microsoft.Extensions.Hosting.Abstractions": "6.0.0", - "Microsoft.Extensions.Localization": "6.0.22", + "Microsoft.Extensions.Localization": "6.0.24", "Microsoft.Extensions.Options.ConfigurationExtensions": "6.0.0", "Mono.Cecil": "0.11.5", "Newtonsoft.Json": "13.0.3", @@ -128,8 +128,8 @@ }, "HtmlSanitizer": { "type": "Transitive", - "resolved": "8.0.723", - "contentHash": "C4RZX+Mv9OqY+sAM3SD3BdLxvtr9QimIGvLvN5SDjbi7rb6ibeHhGnQA5EyKbkiuQKHO6MBa3h2AZQzjy6z9HA==", + "resolved": "8.0.746", + "contentHash": "9InxDYXIEnCnWQN2eCLCNL3QGyhmV0+vn12QlVSPig5m0f6XisAM9pq+TWB8aUtxXm1zm39/rOC8Ed7eZaEYew==", "dependencies": { "AngleSharp": "[0.17.1]", "AngleSharp.Css": "[0.17.0]", @@ -138,8 +138,8 @@ }, "Kentico.Aira.Client": { "type": "Transitive", - "resolved": "1.0.22", - "contentHash": "/slLHi7JWaKCBl0EZa01rNqjoaGfOgRj3vNA2wfE88chM43YOPygl22OVEMnyvVLd5nNzzhgv6iX0QyzQlGVxQ==", + "resolved": "1.0.23", + "contentHash": "16jt+oHW6Fa6fDmJlJBTJuMK+A6nwXDX7IcYygYFzq01Sek43oQB2wjhXPRhntsvFYhAbWcBwXPzn0ALlMggSQ==", "dependencies": { "Microsoft.Extensions.DependencyInjection": "6.0.1", "Microsoft.Extensions.Http": "6.0.0", @@ -166,8 +166,8 @@ }, "Microsoft.AspNetCore.SpaServices.Extensions": { "type": "Transitive", - "resolved": "6.0.22", - "contentHash": "RE17e7KHUhyNWwWL93uJzErUZpRjWU71oT4rUbafjA44gEz5YDAfizxAkM0XYCbLBRCbXtoK4q8DJkJvm/LjSA==", + "resolved": "6.0.24", + "contentHash": "onDzS4i2iLMVHtqnTNEFuik2wqarpi88e8Bz/K6FOHwwj+z9kIvcjdmV8qLpC8effWiQot/JHiGzEtsZA/8yWQ==", "dependencies": { "Microsoft.Extensions.FileProviders.Physical": "6.0.0" } @@ -184,11 +184,11 @@ }, "Microsoft.Data.SqlClient": { "type": "Transitive", - "resolved": "5.1.1", - "contentHash": "MW5E9HFvCaV069o8b6YpuRDPBux8s96qDnOJ+4N9QNUCs7c5W3KxwQ+ftpAjbMUlImL+c9WR+l+f5hzjkqhu2g==", + "resolved": "5.1.2", + "contentHash": "q/F1HTOn9QLwgRp4esJIA1b2X15faeV8WozkNhvU3Zk0DcYDWUsEf5WMkbypY4CaNkZntOduods5wLyv8I699w==", "dependencies": { "Azure.Identity": "1.7.0", - "Microsoft.Data.SqlClient.SNI.runtime": "5.1.0", + "Microsoft.Data.SqlClient.SNI.runtime": "5.1.1", "Microsoft.Identity.Client": "4.47.2", "Microsoft.IdentityModel.JsonWebTokens": "6.24.0", "Microsoft.IdentityModel.Protocols.OpenIdConnect": "6.24.0", @@ -204,8 +204,8 @@ }, "Microsoft.Data.SqlClient.SNI.runtime": { "type": "Transitive", - "resolved": "5.1.0", - "contentHash": "jVsElisM5sfBzaaV9kdq2NXZLwIbytetnsOIlJ0cQGgQP4zFNBmkfHBnpwtmKrtBJBEV9+9PVQPVrcCVhDgcIg==" + "resolved": "5.1.1", + "contentHash": "wNGM5ZTQCa2blc9ikXQouybGiyMd6IHPVJvAlBEPtr6JepZEOYeDxGyprYvFVeOxlCXs7avridZQ0nYkHzQWCQ==" }, "Microsoft.Extensions.ApiDescription.Client": { "type": "Transitive", @@ -281,8 +281,8 @@ }, "Microsoft.Extensions.FileProviders.Embedded": { "type": "Transitive", - "resolved": "6.0.22", - "contentHash": "kT/7vO2tq68iWQKVeIpH4VI6BDMeGQRaPUoiS+ZIY8QFBNgJNLpKoFC7JExRLMc2j1pFWHPDdToQKpMYSbVXiw==", + "resolved": "6.0.24", + "contentHash": "HvTvzEkqYwDykjaekrpntZkxetMQ5XinIRYoRKftCqttmABoSfsOpsxCh0eIw9w4H+K8xHN7nqvCWFPP8feT4w==", "dependencies": { "Microsoft.Extensions.FileProviders.Abstractions": "6.0.0" } @@ -325,19 +325,19 @@ }, "Microsoft.Extensions.Localization": { "type": "Transitive", - "resolved": "6.0.22", - "contentHash": "o2Z7kwFxxTUiz2e9TX4Jp6DPI1+uySq5ObGkHXt9B5yMLKX6hGR0SNuwJY4/CL8X2n1m8ppNNCFKVqAQ1++XiA==", + "resolved": "6.0.24", + "contentHash": "PrWXSaBQWtyfOSh5sZrvz3Gz2mCOFlMne8a3I42ipHej0Nd5/RX9kEDs803y2fXO+2aU76J9vHbW39WuzkZWgg==", "dependencies": { "Microsoft.Extensions.DependencyInjection.Abstractions": "6.0.0", - "Microsoft.Extensions.Localization.Abstractions": "6.0.22", + "Microsoft.Extensions.Localization.Abstractions": "6.0.24", "Microsoft.Extensions.Logging.Abstractions": "6.0.4", "Microsoft.Extensions.Options": "6.0.0" } }, "Microsoft.Extensions.Localization.Abstractions": { "type": "Transitive", - "resolved": "6.0.22", - "contentHash": "NwkDOL4dXE9JeHMqaSiMjj/19vaNztcO6X6XPCSKQBn4pCCeHXfA+YNokplnjhMv4QrB2cJSjGMoT8XC+USR/g==" + "resolved": "6.0.24", + "contentHash": "71jCMmXcswCH5poRNE/gKBbRZDSIZ6W5EbWJHRRvuA/yUy2/Cabor6sXoYDJBNQ58B9m28RM9nou+dO7O+HK0g==" }, "Microsoft.Extensions.Logging": { "type": "Transitive", @@ -685,33 +685,33 @@ "kentico.xperience.crm.common": { "type": "Project", "dependencies": { - "Kentico.Xperience.Admin": "[27.0.1, )", - "Kentico.Xperience.Core": "[27.0.1, )" + "Kentico.Xperience.Admin": "[28.0.0, )", + "Kentico.Xperience.Core": "[28.0.0, )" } }, "Kentico.Xperience.Admin": { "type": "CentralTransitive", - "requested": "[27.0.1, )", - "resolved": "27.0.1", - "contentHash": "Kgk6v0S5OkIFUc/8cj6Nx/pP09Czz21Qwce/iqf4rWC9mnU7fcQ6sXlNmk1VL9jaNKb6XXrOZSWxjWk8AUVTJA==", + "requested": "[28.0.0, )", + "resolved": "28.0.0", + "contentHash": "G54CsUaiegQF1NS3+7p6uyWObSdBVbqfZCDlyzEQSAypZS8Gy1bc8p+SKTpjyNFyj+DpQcX9LJfbam2hUVNOUg==", "dependencies": { - "Kentico.Aira.Client": "1.0.22", - "Kentico.Xperience.WebApp": "[27.0.1]", - "Microsoft.AspNetCore.SpaServices.Extensions": "6.0.22", - "Microsoft.Extensions.FileProviders.Embedded": "6.0.22" + "Kentico.Aira.Client": "1.0.23", + "Kentico.Xperience.WebApp": "[28.0.0]", + "Microsoft.AspNetCore.SpaServices.Extensions": "6.0.24", + "Microsoft.Extensions.FileProviders.Embedded": "6.0.24" } }, "Kentico.Xperience.WebApp": { "type": "CentralTransitive", - "requested": "[27.0.1, )", - "resolved": "27.0.1", - "contentHash": "2wpyDo6wZmQXq9HFNExtNWsJ8hniR0ba1MX6hHG1UjpxuVkG9tFETK3QAefB3FWXYRa8PdpD2RtvSTVwlen3Lw==", + "requested": "[28.0.0, )", + "resolved": "28.0.0", + "contentHash": "R54QHACtE8rfGtHKKFpSvY7LKI1IqqALe70QGYz1u6ZyZ23w0O7R9UZH5bgW/Ww0d13IgGQBc3ULaMel9bxJXg==", "dependencies": { "CommandLineParser": "2.9.1", - "HtmlSanitizer": "8.0.723", - "Kentico.Xperience.Core": "[27.0.1]", - "Microsoft.Extensions.FileProviders.Embedded": "6.0.22", - "Microsoft.Extensions.Localization": "6.0.22" + "HtmlSanitizer": "8.0.746", + "Kentico.Xperience.Core": "[28.0.0]", + "Microsoft.Extensions.FileProviders.Embedded": "6.0.24", + "Microsoft.Extensions.Localization": "6.0.24" } } } From 741147f24c4295f300882913bf1074ecc4816c43 Mon Sep 17 00:00:00 2001 From: Martin Foukal Date: Thu, 18 Jan 2024 15:52:05 +0100 Subject: [PATCH 15/23] DancingGoat 28.0.0 src --- .../DancingGoat/.config/dotnet-tools.json | 2 +- .../SampleDataGeneratorApplication.cs | 19 ++- .../Components/ComponentIdentifiers.cs | 4 +- .../FormBuilderComponentRegister.cs | 2 +- .../ColorPickerEditorViewModel.cs | 2 +- .../InlineEditors/InlineEditorViewModel.cs | 2 +- .../TextEditor/TextEditorViewModel.cs | 2 +- .../PageBuilderComponentRegister.cs | 2 + .../IsInContactGroupConditionType.cs | 7 +- .../Sections/ThemeSectionProperties.cs | 2 +- .../ThreeColumnSectionProperties.cs | 2 +- .../Components/Sections/ZoneRestrictions.cs | 13 +- .../Articles/ArticlesViewComponent.cs | 12 +- .../Banner/BannerViewComponent.cs | 7 +- .../ViewComponents/Banner/Default.cshtml | 2 +- .../ViewComponents/Cafe/CafeViewComponent.cs | 7 +- .../CafeCardSectionViewComponent.cs | 12 +- .../CompanyAddressViewComponent.cs | 9 +- .../NavigationMenuViewComponent.cs | 8 +- .../NavigationMenu/NavigationService.cs | 8 +- .../SocialLinks/SocialLinksViewComponent.cs | 7 +- .../TrackingConsentViewComponent.cs | 6 +- .../CTAButton/CTAButtonWidgetViewModel.cs | 2 +- .../CardWidget/CardWidgetProperties.cs | 6 +- .../CardWidget/CardWidgetViewComponent.cs | 10 +- .../Widgets/CardWidget/CardWidgetViewModel.cs | 2 +- .../Widgets/CardWidget/_CardWidget.cshtml | 2 +- .../HeroImageWidgetProperties.cs | 6 +- .../HeroImageWidgetViewComponent.cs | 10 +- .../HeroImageWidgetViewModel.cs | 2 +- .../HeroImageWidget/_HeroImageWidget.cshtml | 2 +- .../ProductCardListViewModel.cs | 9 +- .../ProductCardProperties.cs | 6 +- .../ProductCardWidget/ProductCardViewModel.cs | 16 +- .../ProductCardWidgetViewComponent.cs | 13 +- .../TestimonialWidgetProperties.cs | 4 +- .../Controllers/AccountController.cs | 88 ++++++++--- .../Controllers/ConsentController.cs | 2 + .../DancingGoatArticleController.cs | 44 +++--- .../DancingGoatConfirmationController.cs | 10 +- .../DancingGoatContactsController.cs | 14 +- .../Controllers/DancingGoatHomeController.cs | 6 +- .../DancingGoatPrivacyController.cs | 22 ++- .../Controllers/HttpErrorsController.cs | 2 +- examples/DancingGoat/Data/Template.zip | Bin 4963752 -> 4949746 bytes .../DancingGoatSamplesModule.cs | 36 +++-- .../SampleContactInfoIdentityCollector.cs | 12 +- .../SampleMemberInfoIdentityCollector.cs | 12 +- .../SampleContactDataCollector.cs | 33 ++-- .../SampleContactDataCollectorCore.cs | 86 ++++++----- .../SampleMemberDataCollector.cs | 31 ++-- .../SampleMemberDataCollectorCore.cs | 13 +- .../HumanReadablePersonalDataWriter.cs | 21 ++- .../Writers/IPersonalDataWriter.cs | 5 +- .../Writers/XmlPersonalDataWriter.cs | 28 +++- .../SampleContactPersonalDataEraser.cs | 10 +- .../SampleMemberPersonalDataEraser.cs | 10 +- .../Helpers/AreaRestrictionHelper.cs | 15 +- .../FormConsentContactGroupGenerator.cs | 12 +- .../DataProtection/FormConsentGenerator.cs | 5 +- .../TrackingConsentGenerator.cs | 10 +- .../TagHelpers/LanguageLinkTagHelper.cs | 14 +- .../Models/Account/LoginViewModel.cs | 2 +- .../Models/Account/RegisterViewModel.cs | 2 +- .../Models/ContentRepositoryBase.cs | 20 ++- .../Models/Reusable/Banner/BannerViewModel.cs | 4 +- .../Models/Reusable/Cafe/CafeRepository.cs | 28 +++- .../Models/Reusable/Cafe/CafeViewModel.cs | 6 +- .../Reusable/Coffee/CoffeeRepository.cs | 20 ++- .../Reusable/Contact/ContactRepository.cs | 14 +- .../Reusable/Contact/ContactViewModel.cs | 14 +- .../Models/Reusable/Event/EventViewModel.cs | 12 +- .../Models/Reusable/Image/ImageRepository.cs | 13 +- .../Reusable/Reference/ReferenceViewModel.cs | 12 +- .../SocialLink/SocialLinkRepository.cs | 22 ++- .../SocialLink/SocialLinkViewModel.cs | 9 +- .../ArticlePage/ArticleDetailViewModel.cs | 7 +- .../ArticlePage/ArticlePageRepository.cs | 39 +++-- .../WebPage/ArticlePage/ArticleViewModel.cs | 6 +- .../ArticlePage/RelatedArticleViewModel.cs | 6 +- .../ArticlesSectionRepository.cs | 18 ++- .../ArticlesSectionViewModel.cs | 9 +- .../ConfirmationPageRepository.cs | 12 +- .../ConfirmationPageViewModel.cs | 4 +- .../ContactsPage/ContactsIndexViewModel.cs | 6 +- .../WebPage/HomePage/HomePageRepository.cs | 18 ++- .../WebPage/HomePage/HomePageViewModel.cs | 6 +- .../NavigationItemRepository.cs | 8 +- .../NavigationItem/NavigationItemViewModel.cs | 2 +- .../Privacy/PrivacyConsentViewModel.cs | 2 +- .../WebPage/Privacy/PrivacyViewModel.cs | 5 +- .../PageTemplates/Article/_Article.cshtml | 73 +++++++++ .../Article/_ArticleWithSidebar.cshtml | 45 ++++++ examples/DancingGoat/Program.cs | 56 ++++--- ...tWebsiteChannelPrimaryLanguageRetriever.cs | 20 ++- ...tWebsiteChannelPrimaryLanguageRetriever.cs | 5 +- .../Services/IServiceCollectionExtensions.cs | 9 +- .../Views/Shared/_DancingGoatLayout.cshtml | 142 ++++++++++++++++++ .../Views/Shared/_LandingPageLayout.cshtml | 1 - .../DancingGoat/Views/Shared/_Layout.cshtml | 141 +---------------- .../Views/Shared/_Reference.cshtml | 6 +- examples/DancingGoat/Views/_ViewStart.cshtml | 18 ++- .../DancingGoat/wwwroot/Scripts/mobileMenu.js | 23 +++ 103 files changed, 1142 insertions(+), 491 deletions(-) create mode 100644 examples/DancingGoat/PageTemplates/Article/_Article.cshtml create mode 100644 examples/DancingGoat/PageTemplates/Article/_ArticleWithSidebar.cshtml create mode 100644 examples/DancingGoat/Views/Shared/_DancingGoatLayout.cshtml create mode 100644 examples/DancingGoat/wwwroot/Scripts/mobileMenu.js diff --git a/examples/DancingGoat/.config/dotnet-tools.json b/examples/DancingGoat/.config/dotnet-tools.json index ddafc16..a102233 100644 --- a/examples/DancingGoat/.config/dotnet-tools.json +++ b/examples/DancingGoat/.config/dotnet-tools.json @@ -3,7 +3,7 @@ "isRoot": true, "tools": { "kentico.xperience.dbmanager": { - "version": "27.0.1", + "version": "28.0.0", "commands": [ "kentico-xperience-dbmanager" ] diff --git a/examples/DancingGoat/AdminComponents/Apps/SampleDataGenerator/SampleDataGeneratorApplication.cs b/examples/DancingGoat/AdminComponents/Apps/SampleDataGenerator/SampleDataGeneratorApplication.cs index fb75fef..8b58525 100644 --- a/examples/DancingGoat/AdminComponents/Apps/SampleDataGenerator/SampleDataGeneratorApplication.cs +++ b/examples/DancingGoat/AdminComponents/Apps/SampleDataGenerator/SampleDataGeneratorApplication.cs @@ -1,4 +1,8 @@ -using CMS.Base; +using System; +using System.Collections.Generic; +using System.Threading.Tasks; + +using CMS.Base; using CMS.ContactManagement; using CMS.Core; using CMS.DataEngine; @@ -127,10 +131,12 @@ private void EnableDataProtectionSamples() } - private OverviewCard GetGdprCard() => new OverviewCard + private OverviewCard GetGdprCard() { - Headline = "Set up data protection (GDPR) demo", - Actions = new[] + return new OverviewCard + { + Headline = "Set up data protection (GDPR) demo", + Actions = new[] { new Kentico.Xperience.Admin.Base.Action(ActionType.Command) { @@ -138,7 +144,7 @@ private void EnableDataProtectionSamples() Parameter = nameof(GenerateGdprSampleData) } }, - Components = new List() + Components = new List() { new StringContentCardComponent { @@ -146,7 +152,8 @@ private void EnableDataProtectionSamples() Once enabled, the demo functionality cannot be disabled. Use on demo instances only." } } - }; + }; + } private async Task SetChannelDefaultCookieLevelToEssential(int websiteChannelId) diff --git a/examples/DancingGoat/Components/ComponentIdentifiers.cs b/examples/DancingGoat/Components/ComponentIdentifiers.cs index 5b508f5..a14c0d8 100644 --- a/examples/DancingGoat/Components/ComponentIdentifiers.cs +++ b/examples/DancingGoat/Components/ComponentIdentifiers.cs @@ -18,5 +18,7 @@ public static class ComponentIdentifiers // Page templates public const string LANDING_PAGE_SINGLE_COLUMN_TEMPLATE = "DancingGoat.LandingPageSingleColumn"; + public const string ARTICLE_TEMPLATE = "DancingGoat.Article"; + public const string ARTICLE_WITH_SIDEBAR_TEMPLATE = "DancingGoat.ArticleWithSidebar"; } -} +} \ No newline at end of file diff --git a/examples/DancingGoat/Components/FormBuilderComponentRegister.cs b/examples/DancingGoat/Components/FormBuilderComponentRegister.cs index a8afe79..24f4781 100644 --- a/examples/DancingGoat/Components/FormBuilderComponentRegister.cs +++ b/examples/DancingGoat/Components/FormBuilderComponentRegister.cs @@ -2,4 +2,4 @@ using Kentico.Forms.Web.Mvc; -[assembly: RegisterFormSection("DancingGoat.TitledSection", "Section with title", "~/Components/FormSections/TitledSection/_TitledSection.cshtml", Description = "Single-column section with one zone and an editable title", IconClass = "icon-rectangle-a", PropertiesType = typeof(TitledSectionProperties))] +[assembly: RegisterFormSection("DancingGoat.TitledSection", "Section with title", "~/Components/FormSections/TitledSection/_TitledSection.cshtml", Description = "Single-column section with one zone and an editable title", IconClass = "icon-rectangle-a", PropertiesType = typeof(TitledSectionProperties))] \ No newline at end of file diff --git a/examples/DancingGoat/Components/InlineEditors/ColorPickerEditor/ColorPickerEditorViewModel.cs b/examples/DancingGoat/Components/InlineEditors/ColorPickerEditor/ColorPickerEditorViewModel.cs index 9c68348..8293a82 100644 --- a/examples/DancingGoat/Components/InlineEditors/ColorPickerEditor/ColorPickerEditorViewModel.cs +++ b/examples/DancingGoat/Components/InlineEditors/ColorPickerEditor/ColorPickerEditorViewModel.cs @@ -10,4 +10,4 @@ public sealed class ColorPickerEditorViewModel : InlineEditorViewModel /// public string ColorCssClass { get; set; } } -} +} \ No newline at end of file diff --git a/examples/DancingGoat/Components/InlineEditors/InlineEditorViewModel.cs b/examples/DancingGoat/Components/InlineEditors/InlineEditorViewModel.cs index 916b8b7..e12fe37 100644 --- a/examples/DancingGoat/Components/InlineEditors/InlineEditorViewModel.cs +++ b/examples/DancingGoat/Components/InlineEditors/InlineEditorViewModel.cs @@ -10,4 +10,4 @@ public abstract class InlineEditorViewModel /// public string PropertyName { get; set; } } -} +} \ No newline at end of file diff --git a/examples/DancingGoat/Components/InlineEditors/TextEditor/TextEditorViewModel.cs b/examples/DancingGoat/Components/InlineEditors/TextEditor/TextEditorViewModel.cs index b122df9..bafe5d9 100644 --- a/examples/DancingGoat/Components/InlineEditors/TextEditor/TextEditorViewModel.cs +++ b/examples/DancingGoat/Components/InlineEditors/TextEditor/TextEditorViewModel.cs @@ -16,4 +16,4 @@ public sealed class TextEditorViewModel : InlineEditorViewModel /// public string PlaceholderText { get; set; } = "Type your text"; } -} +} \ No newline at end of file diff --git a/examples/DancingGoat/Components/PageBuilderComponentRegister.cs b/examples/DancingGoat/Components/PageBuilderComponentRegister.cs index 85d1010..1db2b0f 100644 --- a/examples/DancingGoat/Components/PageBuilderComponentRegister.cs +++ b/examples/DancingGoat/Components/PageBuilderComponentRegister.cs @@ -20,3 +20,5 @@ // Page templates [assembly: RegisterPageTemplate(ComponentIdentifiers.LANDING_PAGE_SINGLE_COLUMN_TEMPLATE, "Single column landing page", propertiesType: typeof(LandingPageSingleColumnProperties), customViewName: "~/PageTemplates/LandingPage/_DancingGoat_LandingPageSingleColumn.cshtml", ContentTypeNames = new string[] { LandingPage.CONTENT_TYPE_NAME }, Description = "A default single column page template with two sections differentiated by a background color.", IconClass = "xp-l-header-text")] +[assembly: RegisterPageTemplate(ComponentIdentifiers.ARTICLE_TEMPLATE, "Article detail", customViewName: "~/PageTemplates/Article/_Article.cshtml", ContentTypeNames = new string[] { ArticlePage.CONTENT_TYPE_NAME }, Description = "Displays an article detail with related articles underneath.", IconClass = "xp-l-text")] +[assembly: RegisterPageTemplate(ComponentIdentifiers.ARTICLE_WITH_SIDEBAR_TEMPLATE, "Article detail with sidebar", customViewName: "~/PageTemplates/Article/_ArticleWithSidebar.cshtml", ContentTypeNames = new string[] { ArticlePage.CONTENT_TYPE_NAME }, Description = "Displays an article detail with sidebar.", IconClass = "xp-l-text-col")] diff --git a/examples/DancingGoat/Components/Personalization/ConditionTypes/IsInContactGroupConditionType.cs b/examples/DancingGoat/Components/Personalization/ConditionTypes/IsInContactGroupConditionType.cs index ef90d41..23a616e 100644 --- a/examples/DancingGoat/Components/Personalization/ConditionTypes/IsInContactGroupConditionType.cs +++ b/examples/DancingGoat/Components/Personalization/ConditionTypes/IsInContactGroupConditionType.cs @@ -1,4 +1,7 @@ -using CMS.ContactManagement; +using System.Collections.Generic; +using System.Linq; + +using CMS.ContactManagement; using CMS.DataEngine; using DancingGoat.Personalization; @@ -43,4 +46,4 @@ public override bool Evaluate() return contact.IsInAnyContactGroup(SelectedContactGroups.Select(c => c.ObjectCodeName).ToArray()); } } -} +} \ No newline at end of file diff --git a/examples/DancingGoat/Components/Sections/ThemeSectionProperties.cs b/examples/DancingGoat/Components/Sections/ThemeSectionProperties.cs index 1d57989..2fa7bb6 100644 --- a/examples/DancingGoat/Components/Sections/ThemeSectionProperties.cs +++ b/examples/DancingGoat/Components/Sections/ThemeSectionProperties.cs @@ -14,4 +14,4 @@ public class ThemeSectionProperties : ISectionProperties [DropDownComponent(Label = "Color scheme", Order = 1, Options = ";None\nsection-white;Flat white\nsection-cappuccino;Cappuccino")] public string Theme { get; set; } } -} +} \ No newline at end of file diff --git a/examples/DancingGoat/Components/Sections/ThreeColumnSection/ThreeColumnSectionProperties.cs b/examples/DancingGoat/Components/Sections/ThreeColumnSection/ThreeColumnSectionProperties.cs index aa491a1..99dc9b6 100644 --- a/examples/DancingGoat/Components/Sections/ThreeColumnSection/ThreeColumnSectionProperties.cs +++ b/examples/DancingGoat/Components/Sections/ThreeColumnSection/ThreeColumnSectionProperties.cs @@ -13,4 +13,4 @@ public class ThreeColumnSectionProperties : ThemeSectionProperties [TextInputComponent(Label = "Title", Order = 1)] public string Title { get; set; } } -} +} \ No newline at end of file diff --git a/examples/DancingGoat/Components/Sections/ZoneRestrictions.cs b/examples/DancingGoat/Components/Sections/ZoneRestrictions.cs index c221d91..ab62de6 100644 --- a/examples/DancingGoat/Components/Sections/ZoneRestrictions.cs +++ b/examples/DancingGoat/Components/Sections/ZoneRestrictions.cs @@ -1,6 +1,10 @@ -using DancingGoat.Widgets; +using System.Collections.Generic; +using System.Linq; + using Kentico.PageBuilder.Web.Mvc; +using DancingGoat.Widgets; + namespace DancingGoat.Sections { /// @@ -47,8 +51,11 @@ public static IEnumerable GetWideZoneRestrictions() } - private static IEnumerable GetWidgetsIdentifiers() => new ComponentDefinitionProvider() + private static IEnumerable GetWidgetsIdentifiers() + { + return new ComponentDefinitionProvider() .GetAll() .Select(definition => definition.Identifier); + } } -} +} \ No newline at end of file diff --git a/examples/DancingGoat/Components/ViewComponents/Articles/ArticlesViewComponent.cs b/examples/DancingGoat/Components/ViewComponents/Articles/ArticlesViewComponent.cs index e29723b..1ac19fa 100644 --- a/examples/DancingGoat/Components/ViewComponents/Articles/ArticlesViewComponent.cs +++ b/examples/DancingGoat/Components/ViewComponents/Articles/ArticlesViewComponent.cs @@ -1,4 +1,8 @@ -using CMS.Websites; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +using CMS.Websites; using DancingGoat.Models; @@ -37,7 +41,7 @@ public ArticlesViewComponent( public async Task InvokeAsync(WebPageRelatedItem articlesSectionItem) { - string languageName = currentLanguageRetriever.Get(); + var languageName = currentLanguageRetriever.Get(); var articlesSection = await articlesSectionRepository.GetArticlesSection(articlesSectionItem.WebPageGuid, languageName); if (articlesSection == null) @@ -55,11 +59,11 @@ public async Task InvokeAsync(WebPageRelatedItem articl models.Add(model); } - string url = (await urlRetriever.Retrieve(articlesSection, languageName)).RelativePath; + var url = (await urlRetriever.Retrieve(articlesSection, languageName)).RelativePath; var viewModel = ArticlesSectionViewModel.GetViewModel(models, url); return View("~/Components/ViewComponents/Articles/Default.cshtml", viewModel); } } -} +} \ No newline at end of file diff --git a/examples/DancingGoat/Components/ViewComponents/Banner/BannerViewComponent.cs b/examples/DancingGoat/Components/ViewComponents/Banner/BannerViewComponent.cs index b9347d3..85a197e 100644 --- a/examples/DancingGoat/Components/ViewComponents/Banner/BannerViewComponent.cs +++ b/examples/DancingGoat/Components/ViewComponents/Banner/BannerViewComponent.cs @@ -10,6 +10,9 @@ namespace DancingGoat.ViewComponents /// public class BannerViewComponent : ViewComponent { - public ViewViewComponentResult Invoke(BannerViewModel banner) => View("~/Components/ViewComponents/Banner/Default.cshtml", banner); + public ViewViewComponentResult Invoke(BannerViewModel banner) + { + return View("~/Components/ViewComponents/Banner/Default.cshtml", banner); + } } -} +} \ No newline at end of file diff --git a/examples/DancingGoat/Components/ViewComponents/Banner/Default.cshtml b/examples/DancingGoat/Components/ViewComponents/Banner/Default.cshtml index af6e4e0..70ae72f 100644 --- a/examples/DancingGoat/Components/ViewComponents/Banner/Default.cshtml +++ b/examples/DancingGoat/Components/ViewComponents/Banner/Default.cshtml @@ -4,7 +4,7 @@ string styleAttribute = null; if (!string.IsNullOrEmpty(Model.BackgroundImageUrl)) { - styleAttribute = $"style=\"background-image: url('{Url.Content(Model.BackgroundImageUrl)}');\""; + styleAttribute = $"style=\"background-image: url('{Url.Content(HTMLHelper.HTMLEncode(Model.BackgroundImageUrl))}');\""; } } diff --git a/examples/DancingGoat/Components/ViewComponents/Cafe/CafeViewComponent.cs b/examples/DancingGoat/Components/ViewComponents/Cafe/CafeViewComponent.cs index 3f802c2..5cc5e21 100644 --- a/examples/DancingGoat/Components/ViewComponents/Cafe/CafeViewComponent.cs +++ b/examples/DancingGoat/Components/ViewComponents/Cafe/CafeViewComponent.cs @@ -10,6 +10,9 @@ namespace DancingGoat.ViewComponents /// public class CafeViewComponent : ViewComponent { - public ViewViewComponentResult Invoke(CafeViewModel cafe) => View("~/Components/ViewComponents/Cafe/Default.cshtml", cafe); + public ViewViewComponentResult Invoke(CafeViewModel cafe) + { + return View("~/Components/ViewComponents/Cafe/Default.cshtml", cafe); + } } -} +} \ No newline at end of file diff --git a/examples/DancingGoat/Components/ViewComponents/CafeCardSection/CafeCardSectionViewComponent.cs b/examples/DancingGoat/Components/ViewComponents/CafeCardSection/CafeCardSectionViewComponent.cs index 060942d..32a3b46 100644 --- a/examples/DancingGoat/Components/ViewComponents/CafeCardSection/CafeCardSectionViewComponent.cs +++ b/examples/DancingGoat/Components/ViewComponents/CafeCardSection/CafeCardSectionViewComponent.cs @@ -1,4 +1,7 @@ -using DancingGoat.Models; +using System.Collections.Generic; +using System.Linq; + +using DancingGoat.Models; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.ViewComponents; @@ -8,8 +11,11 @@ namespace DancingGoat.ViewComponents /// /// Cafe card section view component. /// - public class CafeCardSectionViewComponent : ViewComponent + public class CafeCardSectionViewComponent: ViewComponent { - public ViewViewComponentResult Invoke(IEnumerable cafes) => View("~/Components/ViewComponents/CafeCardSection/Default.cshtml", cafes.Take(3)); + public ViewViewComponentResult Invoke(IEnumerable cafes) + { + return View("~/Components/ViewComponents/CafeCardSection/Default.cshtml", cafes.Take(3)); + } } } diff --git a/examples/DancingGoat/Components/ViewComponents/CompanyAddress/CompanyAddressViewComponent.cs b/examples/DancingGoat/Components/ViewComponents/CompanyAddress/CompanyAddressViewComponent.cs index 0701c93..b2ae0f0 100644 --- a/examples/DancingGoat/Components/ViewComponents/CompanyAddress/CompanyAddressViewComponent.cs +++ b/examples/DancingGoat/Components/ViewComponents/CompanyAddress/CompanyAddressViewComponent.cs @@ -1,4 +1,6 @@ -using DancingGoat.Models; +using System.Threading.Tasks; + +using DancingGoat.Models; using Microsoft.AspNetCore.Mvc; @@ -9,7 +11,10 @@ public class CompanyAddressViewComponent : ViewComponent private readonly ContactRepository contactRepository; - public CompanyAddressViewComponent(ContactRepository contactRepository) => this.contactRepository = contactRepository; + public CompanyAddressViewComponent(ContactRepository contactRepository) + { + this.contactRepository = contactRepository; + } public async Task InvokeAsync() diff --git a/examples/DancingGoat/Components/ViewComponents/NavigationMenu/NavigationMenuViewComponent.cs b/examples/DancingGoat/Components/ViewComponents/NavigationMenu/NavigationMenuViewComponent.cs index 7cac3dc..b7e0fbe 100644 --- a/examples/DancingGoat/Components/ViewComponents/NavigationMenu/NavigationMenuViewComponent.cs +++ b/examples/DancingGoat/Components/ViewComponents/NavigationMenu/NavigationMenuViewComponent.cs @@ -1,4 +1,6 @@ -using Kentico.Content.Web.Mvc.Routing; +using System.Threading.Tasks; + +using Kentico.Content.Web.Mvc.Routing; using Microsoft.AspNetCore.Mvc; @@ -18,11 +20,11 @@ public NavigationMenuViewComponent(NavigationService navigationService, IPreferr public async Task InvokeAsync() { - string languageName = currentLanguageRetriever.Get(); + var languageName = currentLanguageRetriever.Get(); var navigationViewModels = await navigationService.GetNavigationItemViewModels(languageName, HttpContext.RequestAborted); return View($"~/Components/ViewComponents/NavigationMenu/Default.cshtml", navigationViewModels); } } -} +} \ No newline at end of file diff --git a/examples/DancingGoat/Components/ViewComponents/NavigationMenu/NavigationService.cs b/examples/DancingGoat/Components/ViewComponents/NavigationMenu/NavigationService.cs index 6679c7a..94f7c0b 100644 --- a/examples/DancingGoat/Components/ViewComponents/NavigationMenu/NavigationService.cs +++ b/examples/DancingGoat/Components/ViewComponents/NavigationMenu/NavigationService.cs @@ -1,4 +1,10 @@ -using CMS.Helpers; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +using CMS.Helpers; using CMS.Websites; using CMS.Websites.Routing; diff --git a/examples/DancingGoat/Components/ViewComponents/SocialLinks/SocialLinksViewComponent.cs b/examples/DancingGoat/Components/ViewComponents/SocialLinks/SocialLinksViewComponent.cs index 595504f..b480f59 100644 --- a/examples/DancingGoat/Components/ViewComponents/SocialLinks/SocialLinksViewComponent.cs +++ b/examples/DancingGoat/Components/ViewComponents/SocialLinks/SocialLinksViewComponent.cs @@ -1,4 +1,7 @@ -using DancingGoat.Models; +using System.Linq; +using System.Threading.Tasks; + +using DancingGoat.Models; using Kentico.Content.Web.Mvc.Routing; @@ -20,7 +23,7 @@ public SocialLinksViewComponent(SocialLinkRepository socialLinkRepository, IPref public async Task InvokeAsync() { - string languageName = currentLanguageRetriever.Get(); + var languageName = currentLanguageRetriever.Get(); var socialLinks = await socialLinkRepository.GetSocialLinks(languageName, HttpContext.RequestAborted); diff --git a/examples/DancingGoat/Components/ViewComponents/TrackingConsent/TrackingConsentViewComponent.cs b/examples/DancingGoat/Components/ViewComponents/TrackingConsent/TrackingConsentViewComponent.cs index 8db6721..c3ade28 100644 --- a/examples/DancingGoat/Components/ViewComponents/TrackingConsent/TrackingConsentViewComponent.cs +++ b/examples/DancingGoat/Components/ViewComponents/TrackingConsent/TrackingConsentViewComponent.cs @@ -1,4 +1,6 @@ -using CMS.ContactManagement; +using System.Threading.Tasks; + +using CMS.ContactManagement; using CMS.DataProtection; using CMS.Websites; using CMS.Websites.Routing; @@ -46,7 +48,7 @@ public async Task InvokeAsync() if (consent != null) { - string currentLanguage = currentLanguageRetriever.Get(); + var currentLanguage = currentLanguageRetriever.Get(); var consentModel = new ConsentViewModel { ConsentShortText = (await consent.GetConsentTextAsync(currentLanguage)).ShortText, diff --git a/examples/DancingGoat/Components/Widgets/CTAButton/CTAButtonWidgetViewModel.cs b/examples/DancingGoat/Components/Widgets/CTAButton/CTAButtonWidgetViewModel.cs index 7d2ea2e..e4666ff 100644 --- a/examples/DancingGoat/Components/Widgets/CTAButton/CTAButtonWidgetViewModel.cs +++ b/examples/DancingGoat/Components/Widgets/CTAButton/CTAButtonWidgetViewModel.cs @@ -22,4 +22,4 @@ public class CTAButtonWidgetViewModel /// public bool OpenInNewTab { get; set; } } -} +} \ No newline at end of file diff --git a/examples/DancingGoat/Components/Widgets/CardWidget/CardWidgetProperties.cs b/examples/DancingGoat/Components/Widgets/CardWidget/CardWidgetProperties.cs index a679fe0..cd47e46 100644 --- a/examples/DancingGoat/Components/Widgets/CardWidget/CardWidgetProperties.cs +++ b/examples/DancingGoat/Components/Widgets/CardWidget/CardWidgetProperties.cs @@ -1,4 +1,6 @@ -using CMS.ContentEngine; +using System.Collections.Generic; + +using CMS.ContentEngine; using Kentico.PageBuilder.Web.Mvc; using Kentico.Xperience.Admin.Base.FormAnnotations; @@ -21,4 +23,4 @@ public class CardWidgetProperties : IWidgetProperties /// public string Text { get; set; } } -} +} \ No newline at end of file diff --git a/examples/DancingGoat/Components/Widgets/CardWidget/CardWidgetViewComponent.cs b/examples/DancingGoat/Components/Widgets/CardWidget/CardWidgetViewComponent.cs index 7bc6135..974e5f6 100644 --- a/examples/DancingGoat/Components/Widgets/CardWidget/CardWidgetViewComponent.cs +++ b/examples/DancingGoat/Components/Widgets/CardWidget/CardWidgetViewComponent.cs @@ -1,3 +1,6 @@ +using System.Linq; +using System.Threading.Tasks; + using DancingGoat.Models; using DancingGoat.Widgets; @@ -27,7 +30,10 @@ public class CardWidgetViewComponent : ViewComponent /// Creates an instance of class. /// /// Repository for images. - public CardWidgetViewComponent(ImageRepository imageRepository) => this.imageRepository = imageRepository; + public CardWidgetViewComponent(ImageRepository imageRepository) + { + this.imageRepository = imageRepository; + } public async Task InvokeAsync(CardWidgetProperties properties) @@ -54,4 +60,4 @@ private async Task GetImage(CardWidgetProperties properties) return await imageRepository.GetImage(image.Identifier); } } -} +} \ No newline at end of file diff --git a/examples/DancingGoat/Components/Widgets/CardWidget/CardWidgetViewModel.cs b/examples/DancingGoat/Components/Widgets/CardWidget/CardWidgetViewModel.cs index 7e27780..4c36108 100644 --- a/examples/DancingGoat/Components/Widgets/CardWidget/CardWidgetViewModel.cs +++ b/examples/DancingGoat/Components/Widgets/CardWidget/CardWidgetViewModel.cs @@ -16,4 +16,4 @@ public class CardWidgetViewModel /// public string Text { get; set; } } -} +} \ No newline at end of file diff --git a/examples/DancingGoat/Components/Widgets/CardWidget/_CardWidget.cshtml b/examples/DancingGoat/Components/Widgets/CardWidget/_CardWidget.cshtml index 7c79c5f..60af8fb 100644 --- a/examples/DancingGoat/Components/Widgets/CardWidget/_CardWidget.cshtml +++ b/examples/DancingGoat/Components/Widgets/CardWidget/_CardWidget.cshtml @@ -7,7 +7,7 @@ string styleAttribute = null; if (!string.IsNullOrEmpty(Model.ImagePath)) { - styleAttribute = $"style=\"background-image: url('{Url.Content(Model.ImagePath)}');\""; + styleAttribute = $"style=\"background-image: url('{Url.Content(HTMLHelper.HTMLEncode(Model.ImagePath))}');\""; } } diff --git a/examples/DancingGoat/Components/Widgets/HeroImageWidget/HeroImageWidgetProperties.cs b/examples/DancingGoat/Components/Widgets/HeroImageWidget/HeroImageWidgetProperties.cs index d714986..cf92a29 100644 --- a/examples/DancingGoat/Components/Widgets/HeroImageWidget/HeroImageWidgetProperties.cs +++ b/examples/DancingGoat/Components/Widgets/HeroImageWidget/HeroImageWidgetProperties.cs @@ -1,4 +1,6 @@ -using CMS.ContentEngine; +using System.Collections.Generic; + +using CMS.ContentEngine; using Kentico.Forms.Web.Mvc; using Kentico.PageBuilder.Web.Mvc; @@ -44,4 +46,4 @@ public class HeroImageWidgetProperties : IWidgetProperties [DropDownComponent(Label = "Color scheme", Order = 3, Options = "light;Light\ndark;Dark")] public string Theme { get; set; } = "dark"; } -} +} \ No newline at end of file diff --git a/examples/DancingGoat/Components/Widgets/HeroImageWidget/HeroImageWidgetViewComponent.cs b/examples/DancingGoat/Components/Widgets/HeroImageWidget/HeroImageWidgetViewComponent.cs index 345a48a..80941fd 100644 --- a/examples/DancingGoat/Components/Widgets/HeroImageWidget/HeroImageWidgetViewComponent.cs +++ b/examples/DancingGoat/Components/Widgets/HeroImageWidget/HeroImageWidgetViewComponent.cs @@ -1,3 +1,6 @@ +using System.Linq; +using System.Threading.Tasks; + using DancingGoat.Models; using DancingGoat.Widgets; @@ -27,7 +30,10 @@ public class HeroImageWidgetViewComponent : ViewComponent /// Creates an instance of class. /// /// Repository for images. - public HeroImageWidgetViewComponent(ImageRepository imageRepository) => this.imageRepository = imageRepository; + public HeroImageWidgetViewComponent(ImageRepository imageRepository) + { + this.imageRepository = imageRepository; + } public async Task InvokeAsync(HeroImageWidgetProperties properties) @@ -57,4 +63,4 @@ private async Task GetImage(HeroImageWidgetProperties properties) return await imageRepository.GetImage(image.Identifier); } } -} +} \ No newline at end of file diff --git a/examples/DancingGoat/Components/Widgets/HeroImageWidget/HeroImageWidgetViewModel.cs b/examples/DancingGoat/Components/Widgets/HeroImageWidget/HeroImageWidgetViewModel.cs index 6c50c57..84f095a 100644 --- a/examples/DancingGoat/Components/Widgets/HeroImageWidget/HeroImageWidgetViewModel.cs +++ b/examples/DancingGoat/Components/Widgets/HeroImageWidget/HeroImageWidgetViewModel.cs @@ -34,4 +34,4 @@ public class HeroImageWidgetViewModel /// public string Theme { get; set; } } -} +} \ No newline at end of file diff --git a/examples/DancingGoat/Components/Widgets/HeroImageWidget/_HeroImageWidget.cshtml b/examples/DancingGoat/Components/Widgets/HeroImageWidget/_HeroImageWidget.cshtml index 3d39078..8717d8f 100644 --- a/examples/DancingGoat/Components/Widgets/HeroImageWidget/_HeroImageWidget.cshtml +++ b/examples/DancingGoat/Components/Widgets/HeroImageWidget/_HeroImageWidget.cshtml @@ -8,7 +8,7 @@ string styleAttribute = null; if (!string.IsNullOrEmpty(Model.ImagePath)) { - styleAttribute = $"style=\"background-image: url('{Url.Content(Model.ImagePath)}');\""; + styleAttribute = $"style=\"background-image: url('{Url.Content(HTMLHelper.HTMLEncode(Model.ImagePath))}');\""; } } diff --git a/examples/DancingGoat/Components/Widgets/ProductCardWidget/ProductCardListViewModel.cs b/examples/DancingGoat/Components/Widgets/ProductCardWidget/ProductCardListViewModel.cs index 5de6028..26c15fc 100644 --- a/examples/DancingGoat/Components/Widgets/ProductCardWidget/ProductCardListViewModel.cs +++ b/examples/DancingGoat/Components/Widgets/ProductCardWidget/ProductCardListViewModel.cs @@ -1,4 +1,7 @@ -using DancingGoat.Models; +using System.Collections.Generic; +using System.Linq; + +using DancingGoat.Models; namespace DancingGoat.Widgets { @@ -21,7 +24,7 @@ public class ProductCardListViewModel public static ProductCardListViewModel GetViewModel(IEnumerable products) { var productModels = new List(); - + foreach (var product in products.Where(product => product != null)) { var productModel = ProductCardViewModel.GetViewModel(product); @@ -34,4 +37,4 @@ public static ProductCardListViewModel GetViewModel(IEnumerable products }; } } -} +} \ No newline at end of file diff --git a/examples/DancingGoat/Components/Widgets/ProductCardWidget/ProductCardProperties.cs b/examples/DancingGoat/Components/Widgets/ProductCardWidget/ProductCardProperties.cs index 2ef7298..478674a 100644 --- a/examples/DancingGoat/Components/Widgets/ProductCardWidget/ProductCardProperties.cs +++ b/examples/DancingGoat/Components/Widgets/ProductCardWidget/ProductCardProperties.cs @@ -1,4 +1,6 @@ -using CMS.ContentEngine; +using System.Collections.Generic; + +using CMS.ContentEngine; using DancingGoat.Models; @@ -18,4 +20,4 @@ public class ProductCardProperties : IWidgetProperties [ContentItemSelectorComponent(Coffee.CONTENT_TYPE_NAME, Label = "Selected products", Order = 1)] public IEnumerable SelectedProducts { get; set; } = new List(); } -} +} \ No newline at end of file diff --git a/examples/DancingGoat/Components/Widgets/ProductCardWidget/ProductCardViewModel.cs b/examples/DancingGoat/Components/Widgets/ProductCardWidget/ProductCardViewModel.cs index 289167a..6b5b86d 100644 --- a/examples/DancingGoat/Components/Widgets/ProductCardWidget/ProductCardViewModel.cs +++ b/examples/DancingGoat/Components/Widgets/ProductCardWidget/ProductCardViewModel.cs @@ -1,4 +1,6 @@ -using DancingGoat.Models; +using System.Linq; + +using DancingGoat.Models; namespace DancingGoat.Widgets { @@ -38,11 +40,11 @@ public static ProductCardViewModel GetViewModel(Coffee product) } return new ProductCardViewModel - { - Heading = product.CoffeeName, - ImagePath = product.CoffeeImage.FirstOrDefault()?.ImageFile.Url, - Text = product.CoffeeShortDescription - }; + { + Heading = product.CoffeeName, + ImagePath = (product.CoffeeImage.FirstOrDefault())?.ImageFile.Url, + Text = product.CoffeeShortDescription + }; } } -} +} \ No newline at end of file diff --git a/examples/DancingGoat/Components/Widgets/ProductCardWidget/ProductCardWidgetViewComponent.cs b/examples/DancingGoat/Components/Widgets/ProductCardWidget/ProductCardWidgetViewComponent.cs index 6408521..f6bbdbf 100644 --- a/examples/DancingGoat/Components/Widgets/ProductCardWidget/ProductCardWidgetViewComponent.cs +++ b/examples/DancingGoat/Components/Widgets/ProductCardWidget/ProductCardWidgetViewComponent.cs @@ -1,4 +1,8 @@ -using DancingGoat.Models; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +using DancingGoat.Models; using DancingGoat.Widgets; using Kentico.PageBuilder.Web.Mvc; @@ -28,7 +32,10 @@ public class ProductCardWidgetViewComponent : ViewComponent /// Creates an instance of class. /// /// Repository for retrieving products. - public ProductCardWidgetViewComponent(CoffeeRepository repository) => this.repository = repository; + public ProductCardWidgetViewComponent(CoffeeRepository repository) + { + this.repository = repository; + } public async Task InvokeAsync(ProductCardProperties properties) @@ -41,4 +48,4 @@ public async Task InvokeAsync(ProductCardProperties pro return View("~/Components/Widgets/ProductCardWidget/_ProductCardWidget.cshtml", model); } } -} +} \ No newline at end of file diff --git a/examples/DancingGoat/Components/Widgets/TestimonialWidget/TestimonialWidgetProperties.cs b/examples/DancingGoat/Components/Widgets/TestimonialWidget/TestimonialWidgetProperties.cs index 57c31e9..3decca3 100644 --- a/examples/DancingGoat/Components/Widgets/TestimonialWidget/TestimonialWidgetProperties.cs +++ b/examples/DancingGoat/Components/Widgets/TestimonialWidget/TestimonialWidgetProperties.cs @@ -12,7 +12,7 @@ public class TestimonialWidgetProperties : IWidgetProperties /// public string QuotationText { get; set; } - + /// /// Author text. /// @@ -24,4 +24,4 @@ public class TestimonialWidgetProperties : IWidgetProperties /// public string ColorCssClass { get; set; } = "first-color"; } -} +} \ No newline at end of file diff --git a/examples/DancingGoat/Controllers/AccountController.cs b/examples/DancingGoat/Controllers/AccountController.cs index 4603dfa..704a8f4 100644 --- a/examples/DancingGoat/Controllers/AccountController.cs +++ b/examples/DancingGoat/Controllers/AccountController.cs @@ -1,9 +1,16 @@ -using System.Net; +using System; +using System.Net; +using System.Threading; +using System.Threading.Tasks; using CMS.Core; +using CMS.DataEngine; +using CMS.Websites; +using CMS.Websites.Routing; using DancingGoat.Models; +using Kentico.Content.Web.Mvc.Routing; using Kentico.Membership; using Microsoft.AspNetCore.Authorization; @@ -19,33 +26,49 @@ public class AccountController : Controller { private readonly IStringLocalizer localizer; private readonly IEventLogService eventLogService; + private readonly IInfoProvider websiteChannelProvider; + private readonly IWebPageUrlRetriever webPageUrlRetriever; + private readonly IWebsiteChannelContext websiteChannelContext; + private readonly IPreferredLanguageRetriever currentLanguageRetriever; private readonly UserManager userManager; private readonly SignInManager signInManager; - public AccountController(UserManager userManager, - SignInManager signInManager, - IStringLocalizer localizer, - IEventLogService eventLogService) + public AccountController( + UserManager userManager, + SignInManager signInManager, + IStringLocalizer localizer, + IEventLogService eventLogService, + IInfoProvider websiteChannelProvider, + IWebPageUrlRetriever webPageUrlRetriever, + IWebsiteChannelContext websiteChannelContext, + IPreferredLanguageRetriever preferredLanguageRetriever) { this.userManager = userManager; this.signInManager = signInManager; this.localizer = localizer; this.eventLogService = eventLogService; + this.websiteChannelProvider = websiteChannelProvider; + this.webPageUrlRetriever = webPageUrlRetriever; + this.websiteChannelContext = websiteChannelContext; + this.currentLanguageRetriever = preferredLanguageRetriever; } // GET: Account/Login [HttpGet] [AllowAnonymous] - public ActionResult Login() => View(); + public ActionResult Login() + { + return View(); + } // POST: Account/Login [HttpPost] [AllowAnonymous] [ValidateAntiForgeryToken] - public async Task Login(LoginViewModel model, string returnUrl) + public async Task Login(LoginViewModel model, string returnUrl, CancellationToken cancellationToken = default) { if (!ModelState.IsValid) { @@ -65,14 +88,13 @@ public async Task Login(LoginViewModel model, string returnUrl) if (signInResult.Succeeded) { - string decodedReturnUrl = WebUtility.UrlDecode(returnUrl); + var decodedReturnUrl = WebUtility.UrlDecode(returnUrl); if (!string.IsNullOrEmpty(decodedReturnUrl) && Url.IsLocalUrl(decodedReturnUrl)) { return Redirect(decodedReturnUrl); } - // There should be redirect to home page URL which cannot be obtained right now. - return Redirect("/"); + return Redirect(await GetHomeWebPageUrl(cancellationToken)); } ModelState.AddModelError(string.Empty, localizer["Your sign-in attempt was not successful. Please try again."].ToString()); @@ -85,22 +107,24 @@ public async Task Login(LoginViewModel model, string returnUrl) [Authorize] [HttpPost] [ValidateAntiForgeryToken] - public ActionResult Logout() + public async Task Logout(CancellationToken cancellationToken = default) { - signInManager.SignOutAsync(); - // There should be redirect to home page URL which cannot be obtained right now. - return Redirect("/"); + await signInManager.SignOutAsync(); + return Redirect(await GetHomeWebPageUrl(cancellationToken)); } // GET: Account/Register - public ActionResult Register() => View(); + public ActionResult Register() + { + return View(); + } // POST: Account/Register [HttpPost] [ValidateAntiForgeryToken] - public async Task Register(RegisterViewModel model) + public async Task Register(RegisterViewModel model, CancellationToken cancellationToken = default) { if (!ModelState.IsValid) { @@ -132,7 +156,7 @@ public async Task Register(RegisterViewModel model) if (signInResult.Succeeded) { - return Redirect("/"); + return Redirect(await GetHomeWebPageUrl(cancellationToken)); } } @@ -143,5 +167,33 @@ public async Task Register(RegisterViewModel model) return View(model); } + + + private async Task GetHomeWebPageUrl(CancellationToken cancellationToken) + { + var websiteChannelId = websiteChannelContext.WebsiteChannelID; + var websiteChannel = await websiteChannelProvider.GetAsync(websiteChannelId, cancellationToken); + + if (websiteChannel == null) + { + return string.Empty; + } + + var homePageUrl = await webPageUrlRetriever.Retrieve( + websiteChannel.WebsiteChannelHomePage, + websiteChannelContext.WebsiteChannelName, + currentLanguageRetriever.Get(), + websiteChannelContext.IsPreview, + cancellationToken + ); + + if (string.IsNullOrEmpty(homePageUrl?.RelativePath)) + { + return "/"; + } + + return homePageUrl.RelativePath; + + } } -} +} \ No newline at end of file diff --git a/examples/DancingGoat/Controllers/ConsentController.cs b/examples/DancingGoat/Controllers/ConsentController.cs index 06b045b..2d148e7 100644 --- a/examples/DancingGoat/Controllers/ConsentController.cs +++ b/examples/DancingGoat/Controllers/ConsentController.cs @@ -3,6 +3,8 @@ using CMS.Helpers; using DancingGoat.Helpers.Generator; + +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; namespace DancingGoat.Controllers diff --git a/examples/DancingGoat/Controllers/DancingGoatArticleController.cs b/examples/DancingGoat/Controllers/DancingGoatArticleController.cs index 095e6ad..cf03528 100644 --- a/examples/DancingGoat/Controllers/DancingGoatArticleController.cs +++ b/examples/DancingGoat/Controllers/DancingGoatArticleController.cs @@ -1,4 +1,7 @@ -using CMS.Websites; +using System.Collections.Generic; +using System.Threading.Tasks; + +using CMS.Websites; using DancingGoat; using DancingGoat.Controllers; @@ -6,6 +9,7 @@ using Kentico.Content.Web.Mvc; using Kentico.Content.Web.Mvc.Routing; +using Kentico.PageBuilder.Web.Mvc.PageTemplates; using Microsoft.AspNetCore.Mvc; @@ -23,24 +27,24 @@ public class DancingGoatArticleController : Controller private readonly IPreferredLanguageRetriever currentLanguageRetriever; - public DancingGoatArticleController( - ArticlePageRepository articlePageRepository, - ArticlesSectionRepository articlesSectionRepository, - IWebPageUrlRetriever urlRetriever, - IWebPageDataContextRetriever webPageDataContextRetriever, - IPreferredLanguageRetriever currentLanguageRetriever) - { - this.articlePageRepository = articlePageRepository; - this.articlesSectionRepository = articlesSectionRepository; - this.urlRetriever = urlRetriever; - this.webPageDataContextRetriever = webPageDataContextRetriever; - this.currentLanguageRetriever = currentLanguageRetriever; - } + public DancingGoatArticleController( + ArticlePageRepository articlePageRepository, + ArticlesSectionRepository articlesSectionRepository, + IWebPageUrlRetriever urlRetriever, + IWebPageDataContextRetriever webPageDataContextRetriever, + IPreferredLanguageRetriever currentLanguageRetriever) + { + this.articlePageRepository = articlePageRepository; + this.articlesSectionRepository = articlesSectionRepository; + this.urlRetriever = urlRetriever; + this.webPageDataContextRetriever = webPageDataContextRetriever; + this.currentLanguageRetriever = currentLanguageRetriever; + } - public async Task Index() + public async Task Index() { - string languageName = currentLanguageRetriever.Get(); + var languageName = currentLanguageRetriever.Get(); var webPage = webPageDataContextRetriever.Retrieve().WebPage; @@ -61,8 +65,8 @@ public async Task Index() public async Task Article() { - string languageName = currentLanguageRetriever.Get(); - int webPageItemId = webPageDataContextRetriever.Retrieve().WebPage.WebPageItemID; + var languageName = currentLanguageRetriever.Get(); + var webPageItemId = webPageDataContextRetriever.Retrieve().WebPage.WebPageItemID; var article = await articlePageRepository.GetArticle(webPageItemId, languageName, HttpContext.RequestAborted); @@ -71,7 +75,9 @@ public async Task Article() return NotFound(); } - return View(await ArticleDetailViewModel.GetViewModel(article, languageName, articlePageRepository, urlRetriever)); + var model = await ArticleDetailViewModel.GetViewModel(article, languageName, articlePageRepository, urlRetriever); + + return new TemplateResult(model); } } } diff --git a/examples/DancingGoat/Controllers/DancingGoatConfirmationController.cs b/examples/DancingGoat/Controllers/DancingGoatConfirmationController.cs index 9b392ea..f213f4a 100644 --- a/examples/DancingGoat/Controllers/DancingGoatConfirmationController.cs +++ b/examples/DancingGoat/Controllers/DancingGoatConfirmationController.cs @@ -1,4 +1,6 @@ -using DancingGoat; +using System.Threading.Tasks; + +using DancingGoat; using DancingGoat.Controllers; using DancingGoat.Models; @@ -27,12 +29,12 @@ public DancingGoatConfirmationController(ConfirmationPageRepository confirmation public async Task Index() { - int webPageItemId = webPageDataContextRetriever.Retrieve().WebPage.WebPageItemID; - string languageName = currentLanguageRetriever.Get(); + var webPageItemId = webPageDataContextRetriever.Retrieve().WebPage.WebPageItemID; + var languageName = currentLanguageRetriever.Get(); var confirmationPage = await confirmationPageRepository.GetConfirmationPage(webPageItemId, languageName, cancellationToken: HttpContext.RequestAborted); return View(ConfirmationPageViewModel.GetViewModel(confirmationPage)); } } -} +} \ No newline at end of file diff --git a/examples/DancingGoat/Controllers/DancingGoatContactsController.cs b/examples/DancingGoat/Controllers/DancingGoatContactsController.cs index 5886bbd..9b378aa 100644 --- a/examples/DancingGoat/Controllers/DancingGoatContactsController.cs +++ b/examples/DancingGoat/Controllers/DancingGoatContactsController.cs @@ -1,4 +1,9 @@ -using DancingGoat; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +using DancingGoat; using DancingGoat.Controllers; using DancingGoat.Models; using Kentico.Content.Web.Mvc.Routing; @@ -44,6 +49,9 @@ private async Task GetIndexViewModel(CancellationToken c } - private List GetCompanyCafesModel(IEnumerable cafes) => cafes.Select(cafe => CafeViewModel.GetViewModel(cafe)).ToList(); + private List GetCompanyCafesModel(IEnumerable cafes) + { + return cafes.Select(cafe => CafeViewModel.GetViewModel(cafe)).ToList(); + } } -} +} \ No newline at end of file diff --git a/examples/DancingGoat/Controllers/DancingGoatHomeController.cs b/examples/DancingGoat/Controllers/DancingGoatHomeController.cs index c675ce4..38d5823 100644 --- a/examples/DancingGoat/Controllers/DancingGoatHomeController.cs +++ b/examples/DancingGoat/Controllers/DancingGoatHomeController.cs @@ -1,4 +1,6 @@ -using DancingGoat; +using System.Threading.Tasks; + +using DancingGoat; using DancingGoat.Controllers; using DancingGoat.Models; @@ -32,4 +34,4 @@ public async Task Index() return View(HomePageViewModel.GetViewModel(homePage)); } } -} +} \ No newline at end of file diff --git a/examples/DancingGoat/Controllers/DancingGoatPrivacyController.cs b/examples/DancingGoat/Controllers/DancingGoatPrivacyController.cs index 9573cd6..8971c48 100644 --- a/examples/DancingGoat/Controllers/DancingGoatPrivacyController.cs +++ b/examples/DancingGoat/Controllers/DancingGoatPrivacyController.cs @@ -1,10 +1,15 @@ -using CMS.ContactManagement; +using System.Collections.Generic; +using System.Linq; + +using CMS.ContactManagement; using CMS.DataProtection; using DancingGoat; using DancingGoat.Controllers; using DancingGoat.Helpers.Generator; using DancingGoat.Models; + +using Kentico.Content.Web.Mvc; using Kentico.Content.Web.Mvc.Routing; using Microsoft.AspNetCore.Mvc; @@ -28,7 +33,10 @@ private ContactInfo CurrentContact { get { - currentContact ??= ContactManagementContext.CurrentContact; + if (currentContact == null) + { + currentContact = ContactManagementContext.CurrentContact; + } return currentContact; } @@ -86,15 +94,21 @@ public ActionResult Revoke(string returnUrl, string consentName) } - private IEnumerable GetAgreedConsentsForCurrentContact() => consentAgreementService.GetAgreedConsents(CurrentContact) + private IEnumerable GetAgreedConsentsForCurrentContact() + { + return consentAgreementService.GetAgreedConsents(CurrentContact) .Select(consent => new PrivacyConsentViewModel { Name = consent.Name, Title = consent.DisplayName, Text = consent.GetConsentText(currentLanguageRetriever.Get()).ShortText }); + } - private bool IsDemoEnabled() => consentInfoProvider.Get(TrackingConsentGenerator.CONSENT_NAME) != null; + private bool IsDemoEnabled() + { + return consentInfoProvider.Get(TrackingConsentGenerator.CONSENT_NAME) != null; + } } } diff --git a/examples/DancingGoat/Controllers/HttpErrorsController.cs b/examples/DancingGoat/Controllers/HttpErrorsController.cs index 7057f60..06b2b2c 100644 --- a/examples/DancingGoat/Controllers/HttpErrorsController.cs +++ b/examples/DancingGoat/Controllers/HttpErrorsController.cs @@ -14,4 +14,4 @@ public IActionResult Error(int code) return StatusCode(code); } } -} +} \ No newline at end of file diff --git a/examples/DancingGoat/Data/Template.zip b/examples/DancingGoat/Data/Template.zip index 0d060c7fa361100345f2028bad8c86bc8e0fbfe3..e4076cacbbfd79df7e7038b7f49c36b59a4b24f9 100644 GIT binary patch delta 57339 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≷Wg(nnsVzy01Vet-8&i@-5fa9OES@pv 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 WaVkeh!j{*T*EEF1 DataProtectionEvents.RevokeConsentAgreement.Execute += (sender, args) => - { - if (args.Consent.ConsentName.Equals(TrackingConsentGenerator.CONSENT_NAME, StringComparison.Ordinal)) - { - DeleteContactActivities(args.Contact); + private void RegisterConsentRevokeHandler() + { + DataProtectionEvents.RevokeConsentAgreement.Execute += (sender, args) => + { + if (args.Consent.ConsentName.Equals(TrackingConsentGenerator.CONSENT_NAME, StringComparison.Ordinal)) + { + DeleteContactActivities(args.Contact); - // Remove cookies used for contact tracking - var cookieAccessor = Service.Resolve(); + // Remove cookies used for contact tracking + var cookieAccessor = Service.Resolve(); #pragma warning disable CS0618 // CookieName is obsolete - cookieAccessor.Remove(CookieName.CurrentContact); - cookieAccessor.Remove(CookieName.CrossSiteContact); + cookieAccessor.Remove(CookieName.CurrentContact); + cookieAccessor.Remove(CookieName.CrossSiteContact); #pragma warning restore CS0618 // CookieName is obsolete - // Set the cookie level to default - var cookieLevelProvider = Service.Resolve(); - cookieLevelProvider.SetCurrentCookieLevel(cookieLevelProvider.GetDefaultCookieLevel()); - } - }; + // Set the cookie level to default + var cookieLevelProvider = Service.Resolve(); + cookieLevelProvider.SetCurrentCookieLevel(cookieLevelProvider.GetDefaultCookieLevel()); + } + }; + } } } diff --git a/examples/DancingGoat/DataProtectionSamples/IdentityCollectors/SampleContactInfoIdentityCollector.cs b/examples/DancingGoat/DataProtectionSamples/IdentityCollectors/SampleContactInfoIdentityCollector.cs index d90f8f3..b546b27 100644 --- a/examples/DancingGoat/DataProtectionSamples/IdentityCollectors/SampleContactInfoIdentityCollector.cs +++ b/examples/DancingGoat/DataProtectionSamples/IdentityCollectors/SampleContactInfoIdentityCollector.cs @@ -1,4 +1,7 @@ -using CMS.ContactManagement; +using System.Collections.Generic; +using System.Linq; + +using CMS.ContactManagement; using CMS.DataEngine; using CMS.DataProtection; @@ -16,7 +19,10 @@ internal class SampleContactInfoIdentityCollector : IIdentityCollector /// Initializes a new instance of the class. /// /// Contact info provider. - public SampleContactInfoIdentityCollector(IContactInfoProvider contactInfoProvider) => this.contactInfoProvider = contactInfoProvider; + public SampleContactInfoIdentityCollector(IContactInfoProvider contactInfoProvider) + { + this.contactInfoProvider = contactInfoProvider; + } /// @@ -35,7 +41,7 @@ public void Collect(IDictionary dataSubjectIdentifiersFilter, Li return; } - string email = dataSubjectIdentifiersFilter["email"] as string; + var email = dataSubjectIdentifiersFilter["email"] as string; if (string.IsNullOrWhiteSpace(email)) { return; diff --git a/examples/DancingGoat/DataProtectionSamples/IdentityCollectors/SampleMemberInfoIdentityCollector.cs b/examples/DancingGoat/DataProtectionSamples/IdentityCollectors/SampleMemberInfoIdentityCollector.cs index 25ec544..dc004ce 100644 --- a/examples/DancingGoat/DataProtectionSamples/IdentityCollectors/SampleMemberInfoIdentityCollector.cs +++ b/examples/DancingGoat/DataProtectionSamples/IdentityCollectors/SampleMemberInfoIdentityCollector.cs @@ -1,4 +1,7 @@ -using CMS.DataEngine; +using System.Collections.Generic; +using System.Linq; + +using CMS.DataEngine; using CMS.DataProtection; using CMS.Membership; @@ -16,7 +19,10 @@ internal class SampleMemberInfoIdentityCollector : IIdentityCollector /// Initializes a new instance of the class. /// /// Member info provider. - public SampleMemberInfoIdentityCollector(IMemberInfoProvider memberInfoProvider) => this.memberInfoProvider = memberInfoProvider; + public SampleMemberInfoIdentityCollector(IMemberInfoProvider memberInfoProvider) + { + this.memberInfoProvider = memberInfoProvider; + } /// @@ -34,7 +40,7 @@ public void Collect(IDictionary dataSubjectIdentifiersFilter, Li return; } - string email = dataSubjectIdentifiersFilter["email"] as string; + var email = dataSubjectIdentifiersFilter["email"] as string; if (string.IsNullOrWhiteSpace(email)) { return; diff --git a/examples/DancingGoat/DataProtectionSamples/PersonalDataCollectors/SampleContactDataCollector.cs b/examples/DancingGoat/DataProtectionSamples/PersonalDataCollectors/SampleContactDataCollector.cs index 8b87622..954f3bd 100644 --- a/examples/DancingGoat/DataProtectionSamples/PersonalDataCollectors/SampleContactDataCollector.cs +++ b/examples/DancingGoat/DataProtectionSamples/PersonalDataCollectors/SampleContactDataCollector.cs @@ -1,4 +1,6 @@ -using CMS.Activities; +using System.Collections.Generic; + +using CMS.Activities; using CMS.ContactManagement; using CMS.DataEngine; using CMS.DataProtection; @@ -58,20 +60,29 @@ public SampleContactDataCollector( /// containing personal data. public PersonalDataCollectorResult Collect(IEnumerable identities, string outputFormat) { - using var writer = CreateWriter(outputFormat); - var dataCollector = new SampleContactDataCollectorCore(writer, activityInfoProvider, countryInfoProvider, stateInfoProvider, consentAgreementInfoProvider, - accountContactInfoProvider, accountInfoProvider, bizFormInfoProvider); - return new PersonalDataCollectorResult + using (var writer = CreateWriter(outputFormat)) { - Text = dataCollector.CollectData(identities) - }; + var dataCollector = new SampleContactDataCollectorCore(writer, activityInfoProvider, countryInfoProvider, stateInfoProvider, consentAgreementInfoProvider, + accountContactInfoProvider, accountInfoProvider, bizFormInfoProvider); + return new PersonalDataCollectorResult + { + Text = dataCollector.CollectData(identities) + }; + } } - private IPersonalDataWriter CreateWriter(string outputFormat) => outputFormat.ToLowerInvariant() switch + private IPersonalDataWriter CreateWriter(string outputFormat) { - PersonalDataFormat.MACHINE_READABLE => new XmlPersonalDataWriter(), - _ => new HumanReadablePersonalDataWriter(), - }; + switch (outputFormat.ToLowerInvariant()) + { + case PersonalDataFormat.MACHINE_READABLE: + return new XmlPersonalDataWriter(); + + case PersonalDataFormat.HUMAN_READABLE: + default: + return new HumanReadablePersonalDataWriter(); + } + } } } diff --git a/examples/DancingGoat/DataProtectionSamples/PersonalDataCollectors/SampleContactDataCollectorCore.cs b/examples/DancingGoat/DataProtectionSamples/PersonalDataCollectors/SampleContactDataCollectorCore.cs index c969ce0..19b3175 100644 --- a/examples/DancingGoat/DataProtectionSamples/PersonalDataCollectors/SampleContactDataCollectorCore.cs +++ b/examples/DancingGoat/DataProtectionSamples/PersonalDataCollectors/SampleContactDataCollectorCore.cs @@ -1,4 +1,7 @@ -using System.Data; +using System; +using System.Collections.Generic; +using System.Data; +using System.Linq; using System.Xml; using CMS.Activities; @@ -26,8 +29,7 @@ internal class SampleContactDataCollectorCore private readonly IBizFormInfoProvider bizFormInfoProvider; // Lists store Tuples of database column names and their corresponding display names. - private readonly List contactInfoColumns = new() - { + private readonly List contactInfoColumns = new List { new CollectedColumn("ContactFirstName", "First name"), new CollectedColumn("ContactMiddleName", "Middle name"), new CollectedColumn("ContactLastName", "Last name"), @@ -51,31 +53,27 @@ internal class SampleContactDataCollectorCore }; - private readonly List consentAgreementInfoColumns = new() - { + private readonly List consentAgreementInfoColumns = new List { new CollectedColumn("ConsentAgreementGuid", "GUID"), new CollectedColumn("ConsentAgreementRevoked", "Consent action"), new CollectedColumn("ConsentAgreementTime", "Performed on") }; - private readonly List consentInfoColumns = new() - { + private readonly List consentInfoColumns = new List { new CollectedColumn("ConsentGUID", "GUID"), new CollectedColumn("ConsentDisplayName", "Consent name"), new CollectedColumn("ConsentContent", "Full text") }; - private readonly List consentArchiveInfoColumns = new() - { + private readonly List consentArchiveInfoColumns = new List { new CollectedColumn("ConsentArchiveGUID", "GUID"), new CollectedColumn("ConsentArchiveContent", "Full text") }; - private readonly List activityInfoColumns = new() - { + private readonly List activityInfoColumns = new List { new CollectedColumn("ActivityId", "ID"), new CollectedColumn("ActivityCreated", "Created"), new CollectedColumn("ActivityType", "Type"), @@ -85,8 +83,7 @@ internal class SampleContactDataCollectorCore }; - private readonly List accountInfoColumns = new() - { + private readonly List accountInfoColumns = new List { new CollectedColumn("AccountName", "Name"), new CollectedColumn("AccountAddress1", "Address"), new CollectedColumn("AccountAddress2", "Address 2"), @@ -101,20 +98,17 @@ internal class SampleContactDataCollectorCore }; - private readonly List countryInfoColumns = new() - { + private readonly List countryInfoColumns = new List { new CollectedColumn("CountryDisplayName", "Country") }; - private readonly List stateInfoColumns = new() - { + private readonly List stateInfoColumns = new List { new CollectedColumn("StateDisplayName", "State") }; - private readonly List contactGroupInfoColumns = new() - { + private readonly List contactGroupInfoColumns = new List { new CollectedColumn("ContactGroupGUID", "GUID"), new CollectedColumn("ContactGroupName", "Contact group name"), new CollectedColumn("ContactGroupDescription", "Contact group description") @@ -157,7 +151,7 @@ public FormDefinition(string emailColumn, List formColumns) // Dancing Goat specific forms definitions // GUIDs are used to select only specific forms on the Dancing Goat sample site - private readonly Dictionary dancingGoatForms = new() + private readonly Dictionary dancingGoatForms = new Dictionary() { { // BusinessCustomerRegistration @@ -267,13 +261,17 @@ private object TransformGenderValue(string columnName, object columnValue) { if (columnName.Equals("ContactGender", StringComparison.InvariantCultureIgnoreCase)) { - int? gender = columnValue as int?; - return gender switch + var gender = columnValue as int?; + switch (gender) { - 1 => "male", - 2 => "female", - _ => "undefined", - }; + case 1: + return "male"; + case 2: + return "female"; + case 0: + default: + return "undefined"; + } } return columnValue; @@ -286,13 +284,13 @@ private object TransfromConsentText(string columnName, object columnValue) columnName.Equals("ConsentArchiveContent", StringComparison.InvariantCultureIgnoreCase)) { var consentXml = new XmlDocument(); - consentXml.LoadXml((columnValue as string) ?? string.Empty); + consentXml.LoadXml((columnValue as string) ?? String.Empty); // Select the first node var xmlNode = consentXml.SelectSingleNode("/ConsentContent/ConsentLanguageVersions/ConsentLanguageVersion/FullText"); // Strip HTML tags - string result = HTMLHelper.StripTags(xmlNode?.InnerText); + var result = HTMLHelper.StripTags(xmlNode?.InnerText); return result; } @@ -305,7 +303,7 @@ private object TransformConsentAction(string columnName, object columnValue) { if (columnName.Equals("ConsentAgreementRevoked", StringComparison.InvariantCultureIgnoreCase)) { - bool revoked = (bool)columnValue; + var revoked = (bool)columnValue; return revoked ? "Revoked" : "Agreed"; } @@ -399,8 +397,8 @@ private void WriteContacts(IEnumerable contacts) writer.WriteStartSection(ContactInfo.OBJECT_TYPE, "Contact"); writer.WriteBaseInfo(contactInfo, contactInfoColumns, TransformGenderValue); - int countryID = contactInfo.ContactCountryID; - int stateID = contactInfo.ContactStateID; + var countryID = contactInfo.ContactCountryID; + var stateID = contactInfo.ContactStateID; if (countryID != 0) { writer.WriteBaseInfo(countryInfoProvider.Get(countryID), countryInfoColumns); @@ -446,7 +444,8 @@ private void WriteConsents(ICollection contactIDs) { var consentAgreementInfo = new ConsentAgreementInfo(row); - if (!consents.TryGetValue(consentAgreementInfo.ConsentAgreementConsentID, out var consentInfo)) + ConsentInfo consentInfo; + if (!consents.TryGetValue(consentAgreementInfo.ConsentAgreementConsentID, out consentInfo)) { consentInfo = new ConsentInfo(row); consents.Add(consentAgreementInfo.ConsentAgreementConsentID, consentInfo); @@ -481,7 +480,8 @@ private void WriteConsents(ICollection contactIDs) /// Consent ID. private static List GetRevocationsOfSameConsent(Dictionary> consentRevocations, int consentId) { - if (!consentRevocations.TryGetValue(consentId, out var revocationsOfSameConsent)) + List revocationsOfSameConsent; + if (!consentRevocations.TryGetValue(consentId, out revocationsOfSameConsent)) { revocationsOfSameConsent = new List(); consentRevocations.Add(consentId, revocationsOfSameConsent); @@ -499,7 +499,8 @@ private static List GetRevocationsOfSameConsent(Dictionary /// Consent hash. private static List GetAgreementsOfSameConsentContent(Dictionary> consentContentAgreements, string consentHash) { - if (!consentContentAgreements.TryGetValue(consentHash, out var agreementsOfSameConsent)) + List agreementsOfSameConsent; + if (!consentContentAgreements.TryGetValue(consentHash, out agreementsOfSameConsent)) { agreementsOfSameConsent = new List(); consentContentAgreements.Add(consentHash, agreementsOfSameConsent); @@ -514,7 +515,10 @@ private static List GetAgreementsOfSameConsentContent(Dict /// /// Consent agreement. /// Consent. - private static bool IsAgreementOfDifferentConsentContent(ConsentAgreementInfo consentAgreementInfo, ConsentInfo consentInfo) => consentAgreementInfo.ConsentAgreementConsentHash != consentInfo.ConsentHash; + private static bool IsAgreementOfDifferentConsentContent(ConsentAgreementInfo consentAgreementInfo, ConsentInfo consentInfo) + { + return consentAgreementInfo.ConsentAgreementConsentHash != consentInfo.ConsentHash; + } /// @@ -533,9 +537,11 @@ private void WriteConsents(Dictionary consents, Dictionary revocationsOfSameConsent; + consentRevocations.TryGetValue(consentAgreement.ConsentAgreementConsentID, out revocationsOfSameConsent); WriteConsent(consentInfo, consentArchiveInfo, agreementsOfSameConsentContent, revocationsOfSameConsent); } @@ -655,8 +661,10 @@ private void WriteContactAccounts(ICollection contactIDs) foreach (var accountInfo in accountInfos) { - countryInfos.TryGetValue(accountInfo.AccountCountryID, out var countryInfo); - stateInfos.TryGetValue(accountInfo.AccountStateID, out var stateInfo); + CountryInfo countryInfo; + StateInfo stateInfo; + countryInfos.TryGetValue(accountInfo.AccountCountryID, out countryInfo); + stateInfos.TryGetValue(accountInfo.AccountStateID, out stateInfo); writer.WriteStartSection(AccountInfo.OBJECT_TYPE, "Account"); diff --git a/examples/DancingGoat/DataProtectionSamples/PersonalDataCollectors/SampleMemberDataCollector.cs b/examples/DancingGoat/DataProtectionSamples/PersonalDataCollectors/SampleMemberDataCollector.cs index de8477a..ee7ed55 100644 --- a/examples/DancingGoat/DataProtectionSamples/PersonalDataCollectors/SampleMemberDataCollector.cs +++ b/examples/DancingGoat/DataProtectionSamples/PersonalDataCollectors/SampleMemberDataCollector.cs @@ -1,4 +1,6 @@ -using CMS.DataEngine; +using System.Collections.Generic; + +using CMS.DataEngine; using CMS.DataProtection; namespace Samples.DancingGoat @@ -16,19 +18,28 @@ internal class SampleMemberDataCollector : IPersonalDataCollector /// containing personal data. public PersonalDataCollectorResult Collect(IEnumerable identities, string outputFormat) { - using var writer = CreateWriter(outputFormat); - var dataCollector = new SampleMemberDataCollectorCore(writer); - return new PersonalDataCollectorResult + using (var writer = CreateWriter(outputFormat)) { - Text = dataCollector.CollectData(identities) - }; + var dataCollector = new SampleMemberDataCollectorCore(writer); + return new PersonalDataCollectorResult + { + Text = dataCollector.CollectData(identities) + }; + } } - private IPersonalDataWriter CreateWriter(string outputFormat) => outputFormat.ToLowerInvariant() switch + private IPersonalDataWriter CreateWriter(string outputFormat) { - PersonalDataFormat.MACHINE_READABLE => new XmlPersonalDataWriter(), - _ => new HumanReadablePersonalDataWriter(), - }; + switch (outputFormat.ToLowerInvariant()) + { + case PersonalDataFormat.MACHINE_READABLE: + return new XmlPersonalDataWriter(); + + case PersonalDataFormat.HUMAN_READABLE: + default: + return new HumanReadablePersonalDataWriter(); + } + } } } diff --git a/examples/DancingGoat/DataProtectionSamples/PersonalDataCollectors/SampleMemberDataCollectorCore.cs b/examples/DancingGoat/DataProtectionSamples/PersonalDataCollectors/SampleMemberDataCollectorCore.cs index e5f32ac..9a05f05 100644 --- a/examples/DancingGoat/DataProtectionSamples/PersonalDataCollectors/SampleMemberDataCollectorCore.cs +++ b/examples/DancingGoat/DataProtectionSamples/PersonalDataCollectors/SampleMemberDataCollectorCore.cs @@ -1,4 +1,7 @@ -using CMS.DataEngine; +using System.Collections.Generic; +using System.Linq; + +using CMS.DataEngine; using CMS.Membership; namespace Samples.DancingGoat @@ -9,8 +12,7 @@ namespace Samples.DancingGoat internal class SampleMemberDataCollectorCore { // Lists store Tuples of database column names and their corresponding display names. - private readonly List memberInfoColumns = new() - { + private readonly List memberInfoColumns = new List { new CollectedColumn("MemberName", "Name"), new CollectedColumn("MemberIsExternal", "Is external"), new CollectedColumn("MemberEmail", "Email"), @@ -27,7 +29,10 @@ internal class SampleMemberDataCollectorCore /// Constructs a new instance of the . /// /// Writer to format output data. - public SampleMemberDataCollectorCore(IPersonalDataWriter writer) => this.writer = writer; + public SampleMemberDataCollectorCore(IPersonalDataWriter writer) + { + this.writer = writer; + } /// diff --git a/examples/DancingGoat/DataProtectionSamples/PersonalDataCollectors/Writers/HumanReadablePersonalDataWriter.cs b/examples/DancingGoat/DataProtectionSamples/PersonalDataCollectors/Writers/HumanReadablePersonalDataWriter.cs index 54a4c81..a38372a 100644 --- a/examples/DancingGoat/DataProtectionSamples/PersonalDataCollectors/Writers/HumanReadablePersonalDataWriter.cs +++ b/examples/DancingGoat/DataProtectionSamples/PersonalDataCollectors/Writers/HumanReadablePersonalDataWriter.cs @@ -1,4 +1,6 @@ -using System.Globalization; +using System; +using System.Collections.Generic; +using System.Globalization; using System.Text; using CMS.Base; @@ -11,7 +13,7 @@ namespace Samples.DancingGoat /// internal sealed class HumanReadablePersonalDataWriter : IPersonalDataWriter { - private static readonly string DECIMAL_PRECISION = new('#', 26); + private static readonly string DECIMAL_PRECISION = new string('#', 26); private static readonly string DECIMAL_FORMAT = "{0:0.00" + DECIMAL_PRECISION + "}"; private readonly StringBuilder stringBuilder; @@ -54,7 +56,10 @@ public void WriteStartSection(string sectionName, string sectionDisplayName) /// /// Writes appropriate indentation. /// - private void Indent() => stringBuilder.Append('\t', indentationLevel); + private void Indent() + { + stringBuilder.Append('\t', indentationLevel); + } /// @@ -76,8 +81,8 @@ public void WriteBaseInfo(BaseInfo baseInfo, List columns, Func foreach (var column in columns) { - string columnName = column.Name; - string columnDisplayName = column.DisplayName; + var columnName = column.Name; + var columnDisplayName = column.DisplayName; if (string.IsNullOrWhiteSpace(columnDisplayName) || columnName.Equals(baseInfo.TypeInfo.IDColumn, StringComparison.Ordinal) || columnName.Equals(baseInfo.TypeInfo.GUIDColumn, StringComparison.Ordinal)) { continue; @@ -159,7 +164,10 @@ public void WriteEndSection() /// Gets result of previous write calls. /// /// String containing formatted data. - public string GetResult() => stringBuilder.ToString(); + public string GetResult() + { + return stringBuilder.ToString(); + } /// @@ -170,7 +178,6 @@ public void WriteEndSection() /// public void Dispose() { - // Method intentionally left empty. } } } diff --git a/examples/DancingGoat/DataProtectionSamples/PersonalDataCollectors/Writers/IPersonalDataWriter.cs b/examples/DancingGoat/DataProtectionSamples/PersonalDataCollectors/Writers/IPersonalDataWriter.cs index f73ff9f..6b6f52b 100644 --- a/examples/DancingGoat/DataProtectionSamples/PersonalDataCollectors/Writers/IPersonalDataWriter.cs +++ b/examples/DancingGoat/DataProtectionSamples/PersonalDataCollectors/Writers/IPersonalDataWriter.cs @@ -1,4 +1,7 @@ -using CMS.DataEngine; +using System; +using System.Collections.Generic; + +using CMS.DataEngine; namespace Samples.DancingGoat { diff --git a/examples/DancingGoat/DataProtectionSamples/PersonalDataCollectors/Writers/XmlPersonalDataWriter.cs b/examples/DancingGoat/DataProtectionSamples/PersonalDataCollectors/Writers/XmlPersonalDataWriter.cs index 613557a..6ba6b3d 100644 --- a/examples/DancingGoat/DataProtectionSamples/PersonalDataCollectors/Writers/XmlPersonalDataWriter.cs +++ b/examples/DancingGoat/DataProtectionSamples/PersonalDataCollectors/Writers/XmlPersonalDataWriter.cs @@ -1,4 +1,6 @@ -using System.Text; +using System; +using System.Collections.Generic; +using System.Text; using System.Xml; using CMS.DataEngine; @@ -30,7 +32,10 @@ public XmlPersonalDataWriter() /// /// Name of the section in machine readable format. Represents name of the tag. /// Name of the section in human readable format. This parameter is ignored. - public void WriteStartSection(string sectionName, string sectionDisplayName) => xmlWriter.WriteStartElement(TransformElementName(sectionName)); + public void WriteStartSection(string sectionName, string sectionDisplayName) + { + xmlWriter.WriteStartElement(TransformElementName(sectionName)); + } /// @@ -38,7 +43,10 @@ public XmlPersonalDataWriter() /// /// Name to transform. /// Transformed name. - private string TransformElementName(string originalName) => originalName.Replace('.', '_'); + private string TransformElementName(string originalName) + { + return originalName.Replace('.', '_'); + } /// @@ -60,13 +68,13 @@ public void WriteBaseInfo(BaseInfo baseInfo, List columns, Func foreach (var columnTuple in columns) { - string columnName = columnTuple.Name; + var columnName = columnTuple.Name; if (string.IsNullOrWhiteSpace(columnTuple.DisplayName)) { continue; } - object value = baseInfo.GetValue(columnName); + var value = baseInfo.GetValue(columnName); if (value == null) { continue; @@ -101,7 +109,10 @@ public void WriteSectionValue(string sectionName, string sectionDisplayName, str /// /// Writes XML end tag. /// - public void WriteEndSection() => xmlWriter.WriteEndElement(); + public void WriteEndSection() + { + xmlWriter.WriteEndElement(); + } /// @@ -119,6 +130,9 @@ public string GetResult() /// /// Releases all resources used by the current instance of the class. /// - public void Dispose() => xmlWriter.Dispose(); + public void Dispose() + { + xmlWriter.Dispose(); + } } } diff --git a/examples/DancingGoat/DataProtectionSamples/PersonalDataErasers/SampleContactPersonalDataEraser.cs b/examples/DancingGoat/DataProtectionSamples/PersonalDataErasers/SampleContactPersonalDataEraser.cs index cc97af7..7a0971d 100644 --- a/examples/DancingGoat/DataProtectionSamples/PersonalDataErasers/SampleContactPersonalDataEraser.cs +++ b/examples/DancingGoat/DataProtectionSamples/PersonalDataErasers/SampleContactPersonalDataEraser.cs @@ -1,4 +1,8 @@ -using CMS.Activities; +using System; +using System.Collections.Generic; +using System.Linq; + +using CMS.Activities; using CMS.ContactManagement; using CMS.DataEngine; using CMS.DataProtection; @@ -23,7 +27,7 @@ internal class SampleContactPersonalDataEraser : IPersonalDataEraser /// /// GUIDs are used to select only specific forms on the Dancing Goat sample sites. /// - private readonly Dictionary dancingGoatForms = new() + private readonly Dictionary dancingGoatForms = new Dictionary { // DancingGoatCoreContactUsNew { new Guid("0081DC2E-47F4-4ACD-80AE-FE39612F379C"), "UserEmail" }, @@ -206,4 +210,4 @@ private void DeleteContacts(IEnumerable contacts, IDictionary class. /// /// Member info provider. - public SampleMemberPersonalDataEraser(IMemberInfoProvider memberInfoProvider) => this.memberInfoProvider = memberInfoProvider; + public SampleMemberPersonalDataEraser(IMemberInfoProvider memberInfoProvider) + { + this.memberInfoProvider = memberInfoProvider; + } /// diff --git a/examples/DancingGoat/Helpers/AreaRestrictionHelper.cs b/examples/DancingGoat/Helpers/AreaRestrictionHelper.cs index 4dcc6fe..10404ff 100644 --- a/examples/DancingGoat/Helpers/AreaRestrictionHelper.cs +++ b/examples/DancingGoat/Helpers/AreaRestrictionHelper.cs @@ -1,4 +1,8 @@ -using Kentico.PageBuilder.Web.Mvc; +using System; +using System.Collections.Generic; +using System.Linq; + +using Kentico.PageBuilder.Web.Mvc; namespace DancingGoat.Helpers { @@ -12,7 +16,7 @@ public static class AreaRestrictionHelper /// public static string[] GetLandingPageRestrictions() { - string[] allowedScopes = new[] { "Kentico.", "DancingGoat.General.", "DancingGoat.LandingPage." }; + var allowedScopes = new[] { "Kentico.", "DancingGoat.General.", "DancingGoat.LandingPage." }; return GetWidgetsIdentifiers() .Where(id => allowedScopes.Any(scope => id.StartsWith(scope, StringComparison.OrdinalIgnoreCase))) @@ -20,8 +24,11 @@ public static string[] GetLandingPageRestrictions() } - private static IEnumerable GetWidgetsIdentifiers() => new ComponentDefinitionProvider() + private static IEnumerable GetWidgetsIdentifiers() + { + return new ComponentDefinitionProvider() .GetAll() .Select(definition => definition.Identifier); + } } -} +} \ No newline at end of file diff --git a/examples/DancingGoat/Helpers/Generators/DataProtection/FormConsentContactGroupGenerator.cs b/examples/DancingGoat/Helpers/Generators/DataProtection/FormConsentContactGroupGenerator.cs index c90e8eb..f2ec24a 100644 --- a/examples/DancingGoat/Helpers/Generators/DataProtection/FormConsentContactGroupGenerator.cs +++ b/examples/DancingGoat/Helpers/Generators/DataProtection/FormConsentContactGroupGenerator.cs @@ -16,10 +16,16 @@ public class FormContactGroupGenerator /// Initializes a new instance of the class. /// /// Contact group info provider. - public FormContactGroupGenerator(IContactGroupInfoProvider contactGroupInfoProvider) => this.contactGroupInfoProvider = contactGroupInfoProvider; + public FormContactGroupGenerator(IContactGroupInfoProvider contactGroupInfoProvider) + { + this.contactGroupInfoProvider = contactGroupInfoProvider; + } - public void Generate() => CreateContactGroupWithFormConsentAgreementRule(); + public void Generate() + { + CreateContactGroupWithFormConsentAgreementRule(); + } private void CreateContactGroupWithFormConsentAgreementRule() @@ -43,7 +49,7 @@ private void CreateContactGroupWithFormConsentAgreementRule() private string GetFormConsentMacroRule() { - string rule = $@"{{%Rule(""(Contact.AgreedWithConsent(""{FormConsentGenerator.CONSENT_NAME}""))"", "" + var rule = $@"{{%Rule(""(Contact.AgreedWithConsent(""{FormConsentGenerator.CONSENT_NAME}""))"", ""

{FormConsentGenerator.CONSENT_DISPLAY_NAME}{FormConsentGenerator.CONSENT_NAME}0select consenttext0

has0select operationtext0

"")%}}"; diff --git a/examples/DancingGoat/Helpers/Generators/DataProtection/FormConsentGenerator.cs b/examples/DancingGoat/Helpers/Generators/DataProtection/FormConsentGenerator.cs index c1f7cdd..64f7ee6 100644 --- a/examples/DancingGoat/Helpers/Generators/DataProtection/FormConsentGenerator.cs +++ b/examples/DancingGoat/Helpers/Generators/DataProtection/FormConsentGenerator.cs @@ -1,4 +1,7 @@ -using CMS.DataEngine; +using System; +using System.Linq; + +using CMS.DataEngine; using CMS.DataProtection; using CMS.FormEngine; using CMS.OnlineForms; diff --git a/examples/DancingGoat/Helpers/Generators/DataProtection/TrackingConsentGenerator.cs b/examples/DancingGoat/Helpers/Generators/DataProtection/TrackingConsentGenerator.cs index 2b2b585..5acfae3 100644 --- a/examples/DancingGoat/Helpers/Generators/DataProtection/TrackingConsentGenerator.cs +++ b/examples/DancingGoat/Helpers/Generators/DataProtection/TrackingConsentGenerator.cs @@ -31,10 +31,16 @@ public class TrackingConsentGenerator /// Initializes a new instance of the class. /// /// Consent info provider. - public TrackingConsentGenerator(IConsentInfoProvider consentInfoProvider) => this.consentInfoProvider = consentInfoProvider; + public TrackingConsentGenerator(IConsentInfoProvider consentInfoProvider) + { + this.consentInfoProvider = consentInfoProvider; + } - public void Generate() => CreateConsent(); + public void Generate() + { + CreateConsent(); + } private void CreateConsent() diff --git a/examples/DancingGoat/Helpers/TagHelpers/LanguageLinkTagHelper.cs b/examples/DancingGoat/Helpers/TagHelpers/LanguageLinkTagHelper.cs index c899e97..fc9bfd5 100644 --- a/examples/DancingGoat/Helpers/TagHelpers/LanguageLinkTagHelper.cs +++ b/examples/DancingGoat/Helpers/TagHelpers/LanguageLinkTagHelper.cs @@ -1,11 +1,17 @@ -using CMS.Websites; +using System; +using System.Threading.Tasks; + +using CMS.Websites; using Kentico.Content.Web.Mvc; using Kentico.Content.Web.Mvc.Routing; + +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Extensions; using Microsoft.AspNetCore.Mvc.Rendering; using Microsoft.AspNetCore.Mvc.ViewFeatures; using Microsoft.AspNetCore.Razor.TagHelpers; +using Microsoft.AspNetCore.Routing; namespace DancingGoat.Helpers { @@ -63,7 +69,7 @@ public override async Task ProcessAsync(TagHelperContext context, TagHelperOutpu // Create a link for the current language (the URL stays as it is) if (currentLanguageRetriever.Get() == LanguageName) { - string url = UriHelper.GetEncodedUrl(httpContext.Request); + var url = UriHelper.GetEncodedUrl(httpContext.Request); CreateActionLinkWithHref(output, url); return; } @@ -78,7 +84,7 @@ public override async Task ProcessAsync(TagHelperContext context, TagHelperOutpu // Add query parameters (e.g. when performing a search) foreach (var queryParam in queryParams) { - string key = queryParam.Key; + var key = queryParam.Key; if (!string.IsNullOrEmpty(key)) { @@ -104,7 +110,7 @@ private void CreateActionLinkWithHref(TagHelperOutput output, string url) private async Task GenerateActionLink(RouteValueDictionary routeValues) { // Link for the primary language needs to be generated by route name in order to not put language prefix into query string - string websiteChannelPrimaryLanguage = await websiteChannelPrimaryLanguageRetriever.Get(); + var websiteChannelPrimaryLanguage = await websiteChannelPrimaryLanguageRetriever.Get(); if (string.Equals(LanguageName, websiteChannelPrimaryLanguage, StringComparison.InvariantCultureIgnoreCase)) { diff --git a/examples/DancingGoat/Models/Account/LoginViewModel.cs b/examples/DancingGoat/Models/Account/LoginViewModel.cs index c865cc2..2127246 100644 --- a/examples/DancingGoat/Models/Account/LoginViewModel.cs +++ b/examples/DancingGoat/Models/Account/LoginViewModel.cs @@ -20,4 +20,4 @@ public class LoginViewModel [DisplayName("Stay signed in")] public bool StaySignedIn { get; set; } } -} +} \ No newline at end of file diff --git a/examples/DancingGoat/Models/Account/RegisterViewModel.cs b/examples/DancingGoat/Models/Account/RegisterViewModel.cs index 386b39f..ada5add 100644 --- a/examples/DancingGoat/Models/Account/RegisterViewModel.cs +++ b/examples/DancingGoat/Models/Account/RegisterViewModel.cs @@ -35,4 +35,4 @@ public class RegisterViewModel [Compare("Password", ErrorMessage = "Password does not match the confirmation password")] public string PasswordConfirmation { get; set; } } -} +} \ No newline at end of file diff --git a/examples/DancingGoat/Models/ContentRepositoryBase.cs b/examples/DancingGoat/Models/ContentRepositoryBase.cs index 6f7c6c9..cc244df 100644 --- a/examples/DancingGoat/Models/ContentRepositoryBase.cs +++ b/examples/DancingGoat/Models/ContentRepositoryBase.cs @@ -1,4 +1,10 @@ -using CMS.ContentEngine; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +using CMS.ContentEngine; using CMS.Helpers; using CMS.Websites; using CMS.Websites.Routing; @@ -69,7 +75,10 @@ public Task> GetCachedQueryResult( throw new ArgumentNullException(nameof(cacheDependenciesFunc)); } - queryOptions ??= new ContentQueryExecutionOptions(); + if (queryOptions == null) + { + queryOptions = new ContentQueryExecutionOptions(); + } return GetCachedQueryResultInternal(queryBuilder, queryOptions, container => mapper.Map(container), cacheSettings, cacheDependenciesFunc, cancellationToken); } @@ -111,7 +120,10 @@ public Task> GetCachedQueryResult( throw new ArgumentNullException(nameof(cacheDependenciesFunc)); } - queryOptions ??= new ContentQueryExecutionOptions(); + if (queryOptions == null) + { + queryOptions = new ContentQueryExecutionOptions(); + } return GetCachedQueryResultInternal(queryBuilder, queryOptions, resultSelector, cacheSettings, cacheDependenciesFunc, cancellationToken); } @@ -137,7 +149,7 @@ private async Task> GetCachedQueryResultInternal(ContentItemQu { var result = await executor.GetWebPageResult(queryBuilder, resultSelector, options: queryOptions, cancellationToken: cancellationToken); - if (cacheSettings.Cached = result != null && result.Any()) + if (cacheSettings.Cached = (result != null && result.Any())) { cacheSettings.CacheDependency = CacheHelper.GetCacheDependency(await cacheDependenciesFunc(result, cancellationToken)); } diff --git a/examples/DancingGoat/Models/Reusable/Banner/BannerViewModel.cs b/examples/DancingGoat/Models/Reusable/Banner/BannerViewModel.cs index 52f0e64..dc931d7 100644 --- a/examples/DancingGoat/Models/Reusable/Banner/BannerViewModel.cs +++ b/examples/DancingGoat/Models/Reusable/Banner/BannerViewModel.cs @@ -1,4 +1,6 @@ -namespace DancingGoat.Models +using System.Linq; + +namespace DancingGoat.Models { public record BannerViewModel(string BackgroundImageUrl, string HeaderText, string Text) { diff --git a/examples/DancingGoat/Models/Reusable/Cafe/CafeRepository.cs b/examples/DancingGoat/Models/Reusable/Cafe/CafeRepository.cs index 05d0333..4f8daeb 100644 --- a/examples/DancingGoat/Models/Reusable/Cafe/CafeRepository.cs +++ b/examples/DancingGoat/Models/Reusable/Cafe/CafeRepository.cs @@ -1,4 +1,10 @@ -using CMS.ContentEngine; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +using CMS.ContentEngine; using CMS.Helpers; using CMS.Websites; using CMS.Websites.Routing; @@ -14,12 +20,15 @@ public partial class CafeRepository : ContentRepositoryBase public CafeRepository( - IWebsiteChannelContext websiteChannelContext, - IContentQueryExecutor executor, - IWebPageQueryResultMapper mapper, - IProgressiveCache cache, + IWebsiteChannelContext websiteChannelContext, + IContentQueryExecutor executor, + IWebPageQueryResultMapper mapper, + IProgressiveCache cache, ILinkedItemsDependencyAsyncRetriever linkedItemsDependencyRetriever) - : base(websiteChannelContext, executor, mapper, cache) => this.linkedItemsDependencyRetriever = linkedItemsDependencyRetriever; + : base(websiteChannelContext, executor, mapper, cache) + { + this.linkedItemsDependencyRetriever = linkedItemsDependencyRetriever; + } /// /// Returns an enumerable collection of company cafes ordered by a position in the content tree. @@ -36,12 +45,15 @@ public async Task> GetCompanyCafes(int count = 0, Cancellation } - private static ContentItemQueryBuilder GetQueryBuilder(int count) => new ContentItemQueryBuilder() + private static ContentItemQueryBuilder GetQueryBuilder(int count) + { + return new ContentItemQueryBuilder() .ForContentType(Cafe.CONTENT_TYPE_NAME, config => config .WithLinkedItems(1) .TopN(count) .Where(where => where.WhereTrue(nameof(Cafe.CafeIsCompanyCafe)))); + } private async Task> GetDependencyCacheKeys(IEnumerable cafes, CancellationToken cancellationToken) @@ -54,4 +66,4 @@ private async Task> GetDependencyCacheKeys(IEnumerable cafes, return dependencyCacheKeys; } } -} +} \ No newline at end of file diff --git a/examples/DancingGoat/Models/Reusable/Cafe/CafeViewModel.cs b/examples/DancingGoat/Models/Reusable/Cafe/CafeViewModel.cs index c0ff811..81e1f95 100644 --- a/examples/DancingGoat/Models/Reusable/Cafe/CafeViewModel.cs +++ b/examples/DancingGoat/Models/Reusable/Cafe/CafeViewModel.cs @@ -1,9 +1,11 @@ -namespace DancingGoat.Models +using System.Linq; + +namespace DancingGoat.Models { public record CafeViewModel(string Name, string PhotoPath, string PhotoShortDescription, string Street, string City, string Country, string ZipCode, string Phone) { /// - /// Maps to a . + /// Maps to a . /// public static CafeViewModel GetViewModel(Cafe cafe) { diff --git a/examples/DancingGoat/Models/Reusable/Coffee/CoffeeRepository.cs b/examples/DancingGoat/Models/Reusable/Coffee/CoffeeRepository.cs index cc59497..db2fadb 100644 --- a/examples/DancingGoat/Models/Reusable/Coffee/CoffeeRepository.cs +++ b/examples/DancingGoat/Models/Reusable/Coffee/CoffeeRepository.cs @@ -1,4 +1,10 @@ -using CMS.ContentEngine; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +using CMS.ContentEngine; using CMS.Helpers; using CMS.Websites; using CMS.Websites.Routing; @@ -16,7 +22,10 @@ public partial class CoffeeRepository : ContentRepositoryBase /// Initializes a new instance of the class that returns coffees. /// public CoffeeRepository(IWebsiteChannelContext websiteChannelContext, IContentQueryExecutor executor, IWebPageQueryResultMapper mapper, IProgressiveCache cache, ILinkedItemsDependencyRetriever linkedItemsDependencyRetriever) - : base(websiteChannelContext, executor, mapper, cache) => this.linkedItemsDependencyRetriever = linkedItemsDependencyRetriever; + : base(websiteChannelContext, executor, mapper, cache) + { + this.linkedItemsDependencyRetriever = linkedItemsDependencyRetriever; + } /// @@ -32,11 +41,14 @@ public async Task> GetCoffees(ICollection coffeeGuids, } - private static ContentItemQueryBuilder GetQueryBuilder(ICollection coffeeGuids) => new ContentItemQueryBuilder() + private static ContentItemQueryBuilder GetQueryBuilder(ICollection coffeeGuids) + { + return new ContentItemQueryBuilder() .ForContentType(Coffee.CONTENT_TYPE_NAME, config => config .WithLinkedItems(1) .Where(where => where.WhereIn(nameof(IContentQueryDataContainer.ContentItemGUID), coffeeGuids))); + } private Task> GetDependencyCacheKeys(IEnumerable coffees, IEnumerable coffeeGuids) @@ -51,4 +63,4 @@ private Task> GetDependencyCacheKeys(IEnumerable coffees, I return Task.FromResult>(dependencyCacheKeys); } } -} +} \ No newline at end of file diff --git a/examples/DancingGoat/Models/Reusable/Contact/ContactRepository.cs b/examples/DancingGoat/Models/Reusable/Contact/ContactRepository.cs index 5d75d14..deb5e14 100644 --- a/examples/DancingGoat/Models/Reusable/Contact/ContactRepository.cs +++ b/examples/DancingGoat/Models/Reusable/Contact/ContactRepository.cs @@ -1,4 +1,9 @@ -using CMS.ContentEngine; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +using CMS.ContentEngine; using CMS.Helpers; using CMS.Websites; using CMS.Websites.Routing; @@ -31,8 +36,11 @@ public async Task GetContact(CancellationToken cancellationToken = defa } - private static ContentItemQueryBuilder GetQueryBuilder() => new ContentItemQueryBuilder() + private static ContentItemQueryBuilder GetQueryBuilder() + { + return new ContentItemQueryBuilder() .ForContentType(Contact.CONTENT_TYPE_NAME, config => config.TopN(1)); + } private static Task> GetDependencyCacheKeys(IEnumerable contacts, CancellationToken cancellationToken) @@ -49,4 +57,4 @@ private static Task> GetDependencyCacheKeys(IEnumerable co return Task.FromResult>(dependencyCacheKeys); } } -} +} \ No newline at end of file diff --git a/examples/DancingGoat/Models/Reusable/Contact/ContactViewModel.cs b/examples/DancingGoat/Models/Reusable/Contact/ContactViewModel.cs index 3e01f8f..a007d16 100644 --- a/examples/DancingGoat/Models/Reusable/Contact/ContactViewModel.cs +++ b/examples/DancingGoat/Models/Reusable/Contact/ContactViewModel.cs @@ -13,14 +13,14 @@ public static ContactViewModel GetViewModel(Contact contact) } return new ContactViewModel( - contact.ContactName, - contact.ContactStreet, - contact.ContactCity, - contact.ContactCountry, - contact.ContactZipCode, - contact.ContactPhone, + contact.ContactName, + contact.ContactStreet, + contact.ContactCity, + contact.ContactCountry, + contact.ContactZipCode, + contact.ContactPhone, contact.ContactEmail ); } } -} +} \ No newline at end of file diff --git a/examples/DancingGoat/Models/Reusable/Event/EventViewModel.cs b/examples/DancingGoat/Models/Reusable/Event/EventViewModel.cs index 2f195c3..3cc4233 100644 --- a/examples/DancingGoat/Models/Reusable/Event/EventViewModel.cs +++ b/examples/DancingGoat/Models/Reusable/Event/EventViewModel.cs @@ -1,4 +1,8 @@ -namespace DancingGoat.Models +using System; +using System.Collections.Generic; +using System.Linq; + +namespace DancingGoat.Models { public record EventViewModel(string Title, string HeroBannerImageUrl, string HeroBannerShortDescription, string PromoText, DateTime Date, string Location, IEnumerable Coffees) { @@ -16,9 +20,9 @@ public static EventViewModel GetViewModel(Event eventContentItem) var cafe = eventContentItem.EventCafe?.FirstOrDefault(); return new EventViewModel( - eventContentItem.EventTitle, - bannerImage?.ImageFile.Url, - bannerImage?.ImageShortDescription, + eventContentItem.EventTitle, + bannerImage?.ImageFile.Url, + bannerImage?.ImageShortDescription, eventContentItem.EventPromoText, eventContentItem.EventDate, cafe?.CafeName, diff --git a/examples/DancingGoat/Models/Reusable/Image/ImageRepository.cs b/examples/DancingGoat/Models/Reusable/Image/ImageRepository.cs index 4567cb7..50f9de0 100644 --- a/examples/DancingGoat/Models/Reusable/Image/ImageRepository.cs +++ b/examples/DancingGoat/Models/Reusable/Image/ImageRepository.cs @@ -1,4 +1,10 @@ -using CMS.ContentEngine; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +using CMS.ContentEngine; using CMS.Helpers; using CMS.Websites; using CMS.Websites.Routing; @@ -28,11 +34,14 @@ public async Task GetImage(Guid imageGuid, CancellationToken cancellation } - private static ContentItemQueryBuilder GetQueryBuilder(Guid imageGuid) => new ContentItemQueryBuilder() + private static ContentItemQueryBuilder GetQueryBuilder(Guid imageGuid) + { + return new ContentItemQueryBuilder() .ForContentType(Image.CONTENT_TYPE_NAME, config => config .TopN(1) .Where(where => where.WhereEquals(nameof(IContentQueryDataContainer.ContentItemGUID), imageGuid))); + } private static Task> GetDependencyCacheKeys(IEnumerable images, CancellationToken cancellationToken) diff --git a/examples/DancingGoat/Models/Reusable/Reference/ReferenceViewModel.cs b/examples/DancingGoat/Models/Reusable/Reference/ReferenceViewModel.cs index ae69a25..b5a060d 100644 --- a/examples/DancingGoat/Models/Reusable/Reference/ReferenceViewModel.cs +++ b/examples/DancingGoat/Models/Reusable/Reference/ReferenceViewModel.cs @@ -1,4 +1,6 @@ -namespace DancingGoat.Models +using System.Linq; + +namespace DancingGoat.Models { public record ReferenceViewModel(string Name, string Description, string Text, string ImageUrl, string ImageShortDescription) { @@ -13,10 +15,10 @@ public static ReferenceViewModel GetViewModel(Reference reference) } return new ReferenceViewModel( - reference.ReferenceName, - reference.ReferenceDescription, - reference.ReferenceText, - reference.ReferenceImage.FirstOrDefault()?.ImageFile.Url, + reference.ReferenceName, + reference.ReferenceDescription, + reference.ReferenceText, + reference.ReferenceImage.FirstOrDefault()?.ImageFile.Url, reference.ReferenceImage.FirstOrDefault()?.ImageShortDescription ); } diff --git a/examples/DancingGoat/Models/Reusable/SocialLink/SocialLinkRepository.cs b/examples/DancingGoat/Models/Reusable/SocialLink/SocialLinkRepository.cs index e666fe8..dc8d632 100644 --- a/examples/DancingGoat/Models/Reusable/SocialLink/SocialLinkRepository.cs +++ b/examples/DancingGoat/Models/Reusable/SocialLink/SocialLinkRepository.cs @@ -1,4 +1,10 @@ -using CMS.ContentEngine; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +using CMS.ContentEngine; using CMS.Helpers; using CMS.Websites; using CMS.Websites.Routing; @@ -14,7 +20,10 @@ public class SocialLinkRepository : ContentRepositoryBase public SocialLinkRepository(IWebsiteChannelContext websiteChannelContext, IContentQueryExecutor executor, IWebPageQueryResultMapper mapper, IProgressiveCache cache, ILinkedItemsDependencyRetriever linkedItemsDependencyRetriever) - : base(websiteChannelContext, executor, mapper, cache) => this.linkedItemsDependencyRetriever = linkedItemsDependencyRetriever; + : base(websiteChannelContext, executor, mapper, cache) + { + this.linkedItemsDependencyRetriever = linkedItemsDependencyRetriever; + } /// @@ -30,9 +39,12 @@ public async Task> GetSocialLinks(string languageName, C } - private static ContentItemQueryBuilder GetQueryBuilder(string languageName) => new ContentItemQueryBuilder() + private static ContentItemQueryBuilder GetQueryBuilder(string languageName) + { + return new ContentItemQueryBuilder() .ForContentType(SocialLink.CONTENT_TYPE_NAME, config => config.WithLinkedItems(1)) .InLanguage(languageName); + } private Task> GetDependencyCacheKeys(IEnumerable socialLinks, CancellationToken cancellationToken) @@ -48,10 +60,10 @@ private Task> GetDependencyCacheKeys(IEnumerable social private static IEnumerable GetCacheByIdKeys(IEnumerable itemIds) { - foreach (int id in itemIds) + foreach (var id in itemIds) { yield return CacheHelper.BuildCacheItemName(new[] { "contentitem", "byid", id.ToString() }, false); } } } -} +} \ No newline at end of file diff --git a/examples/DancingGoat/Models/Reusable/SocialLink/SocialLinkViewModel.cs b/examples/DancingGoat/Models/Reusable/SocialLink/SocialLinkViewModel.cs index 12815cd..bd6e518 100644 --- a/examples/DancingGoat/Models/Reusable/SocialLink/SocialLinkViewModel.cs +++ b/examples/DancingGoat/Models/Reusable/SocialLink/SocialLinkViewModel.cs @@ -1,4 +1,7 @@ -namespace DancingGoat.Models +using System; +using System.Linq; + +namespace DancingGoat.Models { public record SocialLinkViewModel(string Title, string Url, string IconPath) { @@ -7,8 +10,8 @@ public record SocialLinkViewModel(string Title, string Url, string IconPath) /// public static SocialLinkViewModel GetViewModel(SocialLink socialLink) { - string socialLinkUrl = Uri.TryCreate(socialLink.SocialLinkUrl, UriKind.Absolute, out var _) ? socialLink.SocialLinkUrl : string.Empty; + var socialLinkUrl = Uri.TryCreate(socialLink.SocialLinkUrl, UriKind.Absolute, out var _) ? socialLink.SocialLinkUrl : String.Empty; return new SocialLinkViewModel(socialLink.SocialLinkTitle, socialLinkUrl, socialLink.SocialLinkIcon.FirstOrDefault()?.ImageFile?.Url); } } -} +} \ No newline at end of file diff --git a/examples/DancingGoat/Models/WebPage/ArticlePage/ArticleDetailViewModel.cs b/examples/DancingGoat/Models/WebPage/ArticlePage/ArticleDetailViewModel.cs index 179244d..3016d6b 100644 --- a/examples/DancingGoat/Models/WebPage/ArticlePage/ArticleDetailViewModel.cs +++ b/examples/DancingGoat/Models/WebPage/ArticlePage/ArticleDetailViewModel.cs @@ -1,4 +1,9 @@ -using CMS.Websites; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +using CMS.Websites; namespace DancingGoat.Models { diff --git a/examples/DancingGoat/Models/WebPage/ArticlePage/ArticlePageRepository.cs b/examples/DancingGoat/Models/WebPage/ArticlePage/ArticlePageRepository.cs index ae264af..0999459 100644 --- a/examples/DancingGoat/Models/WebPage/ArticlePage/ArticlePageRepository.cs +++ b/examples/DancingGoat/Models/WebPage/ArticlePage/ArticlePageRepository.cs @@ -1,4 +1,10 @@ -using CMS.ContentEngine; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +using CMS.ContentEngine; using CMS.DataEngine; using CMS.Helpers; using CMS.Websites; @@ -18,12 +24,15 @@ public class ArticlePageRepository : ContentRepositoryBase /// Initializes new instance of . /// public ArticlePageRepository( - IWebsiteChannelContext websiteChannelContext, - IContentQueryExecutor executor, - IWebPageQueryResultMapper mapper, + IWebsiteChannelContext websiteChannelContext, + IContentQueryExecutor executor, + IWebPageQueryResultMapper mapper, IProgressiveCache cache, IWebPageLinkedItemsDependencyAsyncRetriever webPageLinkedItemsDependencyRetriever) - : base(websiteChannelContext, executor, mapper, cache) => this.webPageLinkedItemsDependencyRetriever = webPageLinkedItemsDependencyRetriever; + : base(websiteChannelContext, executor, mapper, cache) + { + this.webPageLinkedItemsDependencyRetriever = webPageLinkedItemsDependencyRetriever; + } /// @@ -83,35 +92,47 @@ public async Task GetArticle(int id, string languageName, Cancellat - private ContentItemQueryBuilder GetQueryBuilder(int topN, string treePath, string languageName) => GetQueryBuilder( + private ContentItemQueryBuilder GetQueryBuilder(int topN, string treePath, string languageName) + { + return GetQueryBuilder( languageName, config => config .WithLinkedItems(1) .TopN(topN) .OrderBy(OrderByColumn.Desc(nameof(ArticlePage.ArticlePagePublishDate))) .ForWebsite(WebsiteChannelContext.WebsiteChannelName, PathMatch.Children(treePath))); + } - private ContentItemQueryBuilder GetQueryBuilder(ICollection guids, string languageName) => GetQueryBuilder( + private ContentItemQueryBuilder GetQueryBuilder(ICollection guids, string languageName) + { + return GetQueryBuilder( languageName, config => config .WithLinkedItems(1) .OrderBy(OrderByColumn.Desc(nameof(ArticlePage.ArticlePagePublishDate))) .ForWebsite(WebsiteChannelContext.WebsiteChannelName) .Where(where => where.WhereIn(nameof(IWebPageContentQueryDataContainer.WebPageItemGUID), guids))); + } - private ContentItemQueryBuilder GetQueryBuilder(int id, string languageName) => GetQueryBuilder( + private ContentItemQueryBuilder GetQueryBuilder(int id, string languageName) + { + return GetQueryBuilder( languageName, config => config .WithLinkedItems(1) .ForWebsite(WebsiteChannelContext.WebsiteChannelName) .Where(where => where.WhereEquals(nameof(IWebPageContentQueryDataContainer.WebPageItemID), id))); + } - private static ContentItemQueryBuilder GetQueryBuilder(string languageName, Action configureQuery = null) => new ContentItemQueryBuilder() + private static ContentItemQueryBuilder GetQueryBuilder(string languageName, Action configureQuery = null) + { + return new ContentItemQueryBuilder() .ForContentType(ArticlePage.CONTENT_TYPE_NAME, configureQuery) .InLanguage(languageName); + } private async Task> GetDependencyCacheKeys(IEnumerable articles, CancellationToken cancellationToken) diff --git a/examples/DancingGoat/Models/WebPage/ArticlePage/ArticleViewModel.cs b/examples/DancingGoat/Models/WebPage/ArticlePage/ArticleViewModel.cs index 86a0288..e3feaff 100644 --- a/examples/DancingGoat/Models/WebPage/ArticlePage/ArticleViewModel.cs +++ b/examples/DancingGoat/Models/WebPage/ArticlePage/ArticleViewModel.cs @@ -1,4 +1,8 @@ -using CMS.Websites; +using System; +using System.Linq; +using System.Threading.Tasks; + +using CMS.Websites; namespace DancingGoat.Models { diff --git a/examples/DancingGoat/Models/WebPage/ArticlePage/RelatedArticleViewModel.cs b/examples/DancingGoat/Models/WebPage/ArticlePage/RelatedArticleViewModel.cs index 176b5dd..e65daf2 100644 --- a/examples/DancingGoat/Models/WebPage/ArticlePage/RelatedArticleViewModel.cs +++ b/examples/DancingGoat/Models/WebPage/ArticlePage/RelatedArticleViewModel.cs @@ -1,4 +1,8 @@ -using CMS.Websites; +using System; +using System.Linq; +using System.Threading.Tasks; + +using CMS.Websites; namespace DancingGoat.Models { diff --git a/examples/DancingGoat/Models/WebPage/ArticlesSection/ArticlesSectionRepository.cs b/examples/DancingGoat/Models/WebPage/ArticlesSection/ArticlesSectionRepository.cs index 38b9ce1..ae36480 100644 --- a/examples/DancingGoat/Models/WebPage/ArticlesSection/ArticlesSectionRepository.cs +++ b/examples/DancingGoat/Models/WebPage/ArticlesSection/ArticlesSectionRepository.cs @@ -1,4 +1,10 @@ -using CMS.ContentEngine; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +using CMS.ContentEngine; using CMS.Helpers; using CMS.Websites; using CMS.Websites.Routing; @@ -88,7 +94,9 @@ private static IEnumerable GetDependencyCacheKeys(ArticlesSection articl } - private ContentItemQueryBuilder GetQueryBuilder(int id, string languageName) => new ContentItemQueryBuilder() + private ContentItemQueryBuilder GetQueryBuilder(int id, string languageName) + { + return new ContentItemQueryBuilder() .ForContentType(ArticlesSection.CONTENT_TYPE_NAME, config => config @@ -96,9 +104,12 @@ private static IEnumerable GetDependencyCacheKeys(ArticlesSection articl .Where(where => where.WhereEquals(nameof(WebPageFields.WebPageItemID), id)) .TopN(1)) .InLanguage(languageName); + } - private ContentItemQueryBuilder GetQueryBuilder(Guid guid, string languageName) => new ContentItemQueryBuilder() + private ContentItemQueryBuilder GetQueryBuilder(Guid guid, string languageName) + { + return new ContentItemQueryBuilder() .ForContentType(ArticlesSection.CONTENT_TYPE_NAME, config => config @@ -106,5 +117,6 @@ private static IEnumerable GetDependencyCacheKeys(ArticlesSection articl .Where(where => where.WhereEquals(nameof(IWebPageContentQueryDataContainer.WebPageItemGUID), guid)) .TopN(1)) .InLanguage(languageName); + } } } diff --git a/examples/DancingGoat/Models/WebPage/ArticlesSection/ArticlesSectionViewModel.cs b/examples/DancingGoat/Models/WebPage/ArticlesSection/ArticlesSectionViewModel.cs index 5ba8d5f..de31415 100644 --- a/examples/DancingGoat/Models/WebPage/ArticlesSection/ArticlesSectionViewModel.cs +++ b/examples/DancingGoat/Models/WebPage/ArticlesSection/ArticlesSectionViewModel.cs @@ -1,10 +1,15 @@ -namespace DancingGoat.Models +using System.Collections.Generic; + +namespace DancingGoat.Models { public record ArticlesSectionViewModel(IEnumerable Articles, string ArticlesPath) { /// /// Maps to a . /// - public static ArticlesSectionViewModel GetViewModel(IEnumerable Articles, string ArticlesPath) => new ArticlesSectionViewModel(Articles, ArticlesPath); + public static ArticlesSectionViewModel GetViewModel(IEnumerable Articles, string ArticlesPath) + { + return new ArticlesSectionViewModel(Articles, ArticlesPath); + } } } diff --git a/examples/DancingGoat/Models/WebPage/ConfirmationPage/ConfirmationPageRepository.cs b/examples/DancingGoat/Models/WebPage/ConfirmationPage/ConfirmationPageRepository.cs index 20835a4..d81fb45 100644 --- a/examples/DancingGoat/Models/WebPage/ConfirmationPage/ConfirmationPageRepository.cs +++ b/examples/DancingGoat/Models/WebPage/ConfirmationPage/ConfirmationPageRepository.cs @@ -1,4 +1,9 @@ -using CMS.ContentEngine; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +using CMS.ContentEngine; using CMS.Helpers; using CMS.Websites; using CMS.Websites.Routing; @@ -34,13 +39,16 @@ public async Task GetConfirmationPage(int webPageItemId, strin } - private ContentItemQueryBuilder GetQueryBuilder(int webPageItemId, string languageName) => new ContentItemQueryBuilder() + private ContentItemQueryBuilder GetQueryBuilder(int webPageItemId, string languageName) + { + return new ContentItemQueryBuilder() .ForContentType(ConfirmationPage.CONTENT_TYPE_NAME, config => config .ForWebsite(WebsiteChannelContext.WebsiteChannelName, includeUrlPath: false) .Where(where => where .WhereEquals(nameof(IWebPageContentQueryDataContainer.WebPageItemID), webPageItemId)) .TopN(1)) .InLanguage(languageName); + } private static Task> GetDependencyCacheKeys(IEnumerable confirmationPages, CancellationToken cancellationToken) diff --git a/examples/DancingGoat/Models/WebPage/ConfirmationPage/ConfirmationPageViewModel.cs b/examples/DancingGoat/Models/WebPage/ConfirmationPage/ConfirmationPageViewModel.cs index eb07419..766dec2 100644 --- a/examples/DancingGoat/Models/WebPage/ConfirmationPage/ConfirmationPageViewModel.cs +++ b/examples/DancingGoat/Models/WebPage/ConfirmationPage/ConfirmationPageViewModel.cs @@ -1,4 +1,6 @@ -using CMS.Websites; +using System.Linq; + +using CMS.Websites; namespace DancingGoat.Models { diff --git a/examples/DancingGoat/Models/WebPage/ContactsPage/ContactsIndexViewModel.cs b/examples/DancingGoat/Models/WebPage/ContactsPage/ContactsIndexViewModel.cs index a700847..131d1ec 100644 --- a/examples/DancingGoat/Models/WebPage/ContactsPage/ContactsIndexViewModel.cs +++ b/examples/DancingGoat/Models/WebPage/ContactsPage/ContactsIndexViewModel.cs @@ -1,4 +1,6 @@ -namespace DancingGoat.Models +using System.Collections.Generic; + +namespace DancingGoat.Models { public class ContactsIndexViewModel { @@ -13,4 +15,4 @@ public class ContactsIndexViewModel /// public List CompanyCafes { get; set; } } -} +} \ No newline at end of file diff --git a/examples/DancingGoat/Models/WebPage/HomePage/HomePageRepository.cs b/examples/DancingGoat/Models/WebPage/HomePage/HomePageRepository.cs index f78224d..6ed0ce8 100644 --- a/examples/DancingGoat/Models/WebPage/HomePage/HomePageRepository.cs +++ b/examples/DancingGoat/Models/WebPage/HomePage/HomePageRepository.cs @@ -1,4 +1,10 @@ -using CMS.ContentEngine; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +using CMS.ContentEngine; using CMS.Helpers; using CMS.Websites; using CMS.Websites.Routing; @@ -17,7 +23,10 @@ public class HomePageRepository : ContentRepositoryBase /// Initializes new instance of . /// public HomePageRepository(IWebsiteChannelContext websiteChannelContext, IContentQueryExecutor executor, IWebPageQueryResultMapper mapper, IProgressiveCache cache, IWebPageLinkedItemsDependencyAsyncRetriever webPageLinkedItemsDependencyRetriever) - : base(websiteChannelContext, executor, mapper, cache) => this.webPageLinkedItemsDependencyRetriever = webPageLinkedItemsDependencyRetriever; + : base(websiteChannelContext, executor, mapper, cache) + { + this.webPageLinkedItemsDependencyRetriever = webPageLinkedItemsDependencyRetriever; + } /// @@ -35,7 +44,9 @@ public async Task GetHomePage(int webPageItemId, string languageName, } - private ContentItemQueryBuilder GetQueryBuilder(int webPageItemId, string languageName) => new ContentItemQueryBuilder() + private ContentItemQueryBuilder GetQueryBuilder(int webPageItemId, string languageName) + { + return new ContentItemQueryBuilder() .ForContentType(HomePage.CONTENT_TYPE_NAME, config => config @@ -44,6 +55,7 @@ public async Task GetHomePage(int webPageItemId, string languageName, .Where(where => where.WhereEquals(nameof(IWebPageContentQueryDataContainer.WebPageItemID), webPageItemId)) .TopN(1)) .InLanguage(languageName); + } private async Task> GetDependencyCacheKeys(IEnumerable homePages, CancellationToken cancellationToken) diff --git a/examples/DancingGoat/Models/WebPage/HomePage/HomePageViewModel.cs b/examples/DancingGoat/Models/WebPage/HomePage/HomePageViewModel.cs index 05f2cc5..ceb728e 100644 --- a/examples/DancingGoat/Models/WebPage/HomePage/HomePageViewModel.cs +++ b/examples/DancingGoat/Models/WebPage/HomePage/HomePageViewModel.cs @@ -1,4 +1,8 @@ -using CMS.Websites; +using System; +using System.Collections.Generic; +using System.Linq; + +using CMS.Websites; namespace DancingGoat.Models { diff --git a/examples/DancingGoat/Models/WebPage/NavigationItem/NavigationItemRepository.cs b/examples/DancingGoat/Models/WebPage/NavigationItem/NavigationItemRepository.cs index 106c55c..c9615c9 100644 --- a/examples/DancingGoat/Models/WebPage/NavigationItem/NavigationItemRepository.cs +++ b/examples/DancingGoat/Models/WebPage/NavigationItem/NavigationItemRepository.cs @@ -1,4 +1,10 @@ -using CMS.ContentEngine; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +using CMS.ContentEngine; using CMS.Helpers; using CMS.Websites; using CMS.Websites.Routing; diff --git a/examples/DancingGoat/Models/WebPage/NavigationItem/NavigationItemViewModel.cs b/examples/DancingGoat/Models/WebPage/NavigationItem/NavigationItemViewModel.cs index 4e8345a..57973e7 100644 --- a/examples/DancingGoat/Models/WebPage/NavigationItem/NavigationItemViewModel.cs +++ b/examples/DancingGoat/Models/WebPage/NavigationItem/NavigationItemViewModel.cs @@ -3,4 +3,4 @@ public record NavigationItemViewModel(string Caption, string RelativeUrl) { } -} +} \ No newline at end of file diff --git a/examples/DancingGoat/Models/WebPage/Privacy/PrivacyConsentViewModel.cs b/examples/DancingGoat/Models/WebPage/Privacy/PrivacyConsentViewModel.cs index 523d9e5..2dc4a98 100644 --- a/examples/DancingGoat/Models/WebPage/Privacy/PrivacyConsentViewModel.cs +++ b/examples/DancingGoat/Models/WebPage/Privacy/PrivacyConsentViewModel.cs @@ -10,4 +10,4 @@ public class PrivacyConsentViewModel public string Text { get; set; } } -} +} \ No newline at end of file diff --git a/examples/DancingGoat/Models/WebPage/Privacy/PrivacyViewModel.cs b/examples/DancingGoat/Models/WebPage/Privacy/PrivacyViewModel.cs index 76b6c18..5d1d48e 100644 --- a/examples/DancingGoat/Models/WebPage/Privacy/PrivacyViewModel.cs +++ b/examples/DancingGoat/Models/WebPage/Privacy/PrivacyViewModel.cs @@ -1,4 +1,7 @@ -namespace DancingGoat.Models +using System.Collections.Generic; +using System.Linq; + +namespace DancingGoat.Models { public class PrivacyViewModel { diff --git a/examples/DancingGoat/PageTemplates/Article/_Article.cshtml b/examples/DancingGoat/PageTemplates/Article/_Article.cshtml new file mode 100644 index 0000000..aa237cb --- /dev/null +++ b/examples/DancingGoat/PageTemplates/Article/_Article.cshtml @@ -0,0 +1,73 @@ +@using Kentico.Content.Web.Mvc.PageBuilder + +@using DancingGoat.PageTemplates +@using DancingGoat.Models + +@model TemplateViewModel + +@{ + Layout = "~/Views/Shared/_DancingGoatLayout.cshtml"; + var viewModel = Model.GetTemplateModel(); + + ViewBag.Title = viewModel.Title; + + var hasRelatedArticles = viewModel.RelatedArticles.Any(); +} + + +
+ @if (hasRelatedArticles) + { +
+

@viewModel.Title

+
+ @viewModel.PublicationDate.ToString("D") +
+ @if (!string.IsNullOrEmpty(viewModel.TeaserUrl)) + { +
+
+ @viewModel.Title +
+
+ } +
+
+ @Html.Raw(viewModel.Text) +
+
+
+ +
+ +
+ } + else + { +
+
+ @if (!string.IsNullOrEmpty(viewModel.TeaserUrl)) + { +
+ @viewModel.Title +
+
+
+ } +
+

@viewModel.Title

+
+ @viewModel.PublicationDate.ToString("D") +
+
+
+
+
+ @Html.Raw(viewModel.Text) +
+
+
+ } + +
+
\ No newline at end of file diff --git a/examples/DancingGoat/PageTemplates/Article/_ArticleWithSidebar.cshtml b/examples/DancingGoat/PageTemplates/Article/_ArticleWithSidebar.cshtml new file mode 100644 index 0000000..90d24c1 --- /dev/null +++ b/examples/DancingGoat/PageTemplates/Article/_ArticleWithSidebar.cshtml @@ -0,0 +1,45 @@ +@using Kentico.Content.Web.Mvc.PageBuilder + +@using DancingGoat.PageTemplates +@using DancingGoat.Widgets +@using DancingGoat.Models + +@model TemplateViewModel + +@{ + Layout = "~/Views/Shared/_DancingGoatLayout.cshtml"; + var templateModel = Model.GetTemplateModel(); + + // The page's content takes 12 points of width which are divided between the sidebar and the article + var articleWidth = 9; + var sidebardBootstrapWidth = 12 - articleWidth; + + ViewBag.Title = templateModel.Title; +} + +
+
+

@templateModel.Title

+
+ @templateModel.PublicationDate.ToString("D") +
+ @if (!string.IsNullOrEmpty(templateModel.TeaserUrl)) + { +
+
+ @templateModel.Title +
+
+ } +
+
+ @Html.Kentico().ResolveUrls(templateModel.Text) +
+
+
+
+ +
+
+
\ No newline at end of file diff --git a/examples/DancingGoat/Program.cs b/examples/DancingGoat/Program.cs index 938e5b6..2b9a86d 100644 --- a/examples/DancingGoat/Program.cs +++ b/examples/DancingGoat/Program.cs @@ -1,4 +1,3 @@ -using CMS.OnlineForms; using CMS.OnlineForms.Types; using DancingGoat; using DancingGoat.Models; @@ -15,9 +14,10 @@ using Kentico.Xperience.CRM.SalesForce; using Kentico.Xperience.CRM.SalesForce.Configuration; using Microsoft.AspNetCore.Identity; -using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Abstractions; using Microsoft.AspNetCore.Mvc.Routing; +using Microsoft.AspNetCore.Mvc; + var builder = WebApplication.CreateBuilder(args); @@ -28,7 +28,12 @@ { DefaultSectionIdentifier = ComponentIdentifiers.SINGLE_COLUMN_SECTION, RegisterDefaultSection = false, - ContentTypeNames = new[] { LandingPage.CONTENT_TYPE_NAME, ContactsPage.CONTENT_TYPE_NAME } + ContentTypeNames = new[] + { + LandingPage.CONTENT_TYPE_NAME, + ContactsPage.CONTENT_TYPE_NAME, + ArticlePage.CONTENT_TYPE_NAME + } }); features.UseWebPageRouting(); @@ -43,7 +48,9 @@ .AddControllersWithViews() .AddViewLocalization() .AddDataAnnotationsLocalization(options => - options.DataAnnotationLocalizerProvider = (type, factory) => factory.Create(typeof(SharedResources))); + { + options.DataAnnotationLocalizerProvider = (type, factory) => factory.Create(typeof(SharedResources)); + }); builder.Services.AddDancingGoatServices(); @@ -104,21 +111,27 @@ app.Kentico().MapRoutes(); app.MapControllerRoute( - name: "error", - pattern: "error/{code}", - defaults: new { controller = "HttpErrors", action = "Error" } + name: "error", + pattern: "error/{code}", + defaults: new { controller = "HttpErrors", action = "Error" } ); app.MapControllerRoute( name: DancingGoatConstants.DEFAULT_ROUTE_NAME, pattern: $"{{{WebPageRoutingOptions.LANGUAGE_ROUTE_VALUE_KEY}}}/{{controller}}/{{action}}", - constraints: new { controller = DancingGoatConstants.CONSTRAINT_FOR_NON_ROUTER_PAGE_CONTROLLERS } + constraints: new + { + controller = DancingGoatConstants.CONSTRAINT_FOR_NON_ROUTER_PAGE_CONTROLLERS + } ); app.MapControllerRoute( name: DancingGoatConstants.DEFAULT_ROUTE_WITHOUT_LANGUAGE_PREFIX_NAME, pattern: "{controller}/{action}", - constraints: new { controller = DancingGoatConstants.CONSTRAINT_FOR_NON_ROUTER_PAGE_CONTROLLERS } + constraints: new + { + controller = DancingGoatConstants.CONSTRAINT_FOR_NON_ROUTER_PAGE_CONTROLLERS + } ); app.Run(); @@ -127,16 +140,16 @@ static void ConfigureMembershipServices(IServiceCollection services) { services.AddIdentity(options => - { - options.Password.RequireDigit = false; - options.Password.RequireNonAlphanumeric = false; - options.Password.RequiredLength = 0; - options.Password.RequireUppercase = false; - options.Password.RequireLowercase = false; - options.Password.RequiredUniqueChars = 0; - // Ensures, that disabled member cannot sign in. - options.SignIn.RequireConfirmedAccount = true; - }) + { + options.Password.RequireDigit = false; + options.Password.RequireNonAlphanumeric = false; + options.Password.RequiredLength = 0; + options.Password.RequireUppercase = false; + options.Password.RequireLowercase = false; + options.Password.RequiredUniqueChars = 0; + // Ensures, that disabled member cannot sign in. + options.SignIn.RequireConfirmedAccount = true; + }) .AddUserStore>() .AddRoleStore() .AddUserManager>() @@ -150,9 +163,8 @@ static void ConfigureMembershipServices(IServiceCollection services) options.Events.OnRedirectToAccessDenied = ctx => { var factory = ctx.HttpContext.RequestServices.GetRequiredService(); - var urlHelper = factory.GetUrlHelper(new ActionContext(ctx.HttpContext, - new RouteData(ctx.HttpContext.Request.RouteValues), new ActionDescriptor())); - string url = urlHelper.Action("Login", "Account") + new Uri(ctx.RedirectUri).Query; + var urlHelper = factory.GetUrlHelper(new ActionContext(ctx.HttpContext, new RouteData(ctx.HttpContext.Request.RouteValues), new ActionDescriptor())); + var url = urlHelper.Action("Login", "Account") + new Uri(ctx.RedirectUri).Query; ctx.Response.Redirect(url); diff --git a/examples/DancingGoat/Services/CurrentWebsiteChannelPrimaryLanguageRetriever.cs b/examples/DancingGoat/Services/CurrentWebsiteChannelPrimaryLanguageRetriever.cs index d1d5b29..949089d 100644 --- a/examples/DancingGoat/Services/CurrentWebsiteChannelPrimaryLanguageRetriever.cs +++ b/examples/DancingGoat/Services/CurrentWebsiteChannelPrimaryLanguageRetriever.cs @@ -1,4 +1,8 @@ -using CMS.ContentEngine; +using System; +using System.Threading; +using System.Threading.Tasks; + +using CMS.ContentEngine; using CMS.DataEngine; using CMS.Websites; using CMS.Websites.Routing; @@ -31,9 +35,19 @@ public CurrentWebsiteChannelPrimaryLanguageRetriever( /// public async Task Get(CancellationToken cancellationToken = default) { - var websiteChannel = await websiteChannelInfoProvider.GetAsync(websiteChannelContext.WebsiteChannelID, cancellationToken) ?? throw new InvalidOperationException($"Website channel with ID {websiteChannelContext.WebsiteChannelID} does not exist."); + var websiteChannel = await websiteChannelInfoProvider.GetAsync(websiteChannelContext.WebsiteChannelID, cancellationToken); + + if (websiteChannel == null) + { + throw new InvalidOperationException($"Website channel with ID {websiteChannelContext.WebsiteChannelID} does not exist."); + } + + var languageInfo = await contentLanguageInfoProvider.GetAsync(websiteChannel.WebsiteChannelPrimaryContentLanguageID, cancellationToken); - var languageInfo = await contentLanguageInfoProvider.GetAsync(websiteChannel.WebsiteChannelPrimaryContentLanguageID, cancellationToken) ?? throw new InvalidOperationException($"Content language with ID {websiteChannel.WebsiteChannelPrimaryContentLanguageID} does not exist."); + if (languageInfo == null) + { + throw new InvalidOperationException($"Content language with ID {websiteChannel.WebsiteChannelPrimaryContentLanguageID} does not exist."); + } return languageInfo.ContentLanguageName; } diff --git a/examples/DancingGoat/Services/ICurrentWebsiteChannelPrimaryLanguageRetriever.cs b/examples/DancingGoat/Services/ICurrentWebsiteChannelPrimaryLanguageRetriever.cs index 86ebebd..ff1cc86 100644 --- a/examples/DancingGoat/Services/ICurrentWebsiteChannelPrimaryLanguageRetriever.cs +++ b/examples/DancingGoat/Services/ICurrentWebsiteChannelPrimaryLanguageRetriever.cs @@ -1,4 +1,7 @@ -namespace DancingGoat +using System.Threading; +using System.Threading.Tasks; + +namespace DancingGoat { /// /// Retrieves current website channel primary language. diff --git a/examples/DancingGoat/Services/IServiceCollectionExtensions.cs b/examples/DancingGoat/Services/IServiceCollectionExtensions.cs index 896042b..0d8c542 100644 --- a/examples/DancingGoat/Services/IServiceCollectionExtensions.cs +++ b/examples/DancingGoat/Services/IServiceCollectionExtensions.cs @@ -1,6 +1,8 @@ using DancingGoat.Models; using DancingGoat.ViewComponents; +using Microsoft.Extensions.DependencyInjection; + namespace DancingGoat { public static class IServiceCollectionExtensions @@ -12,7 +14,7 @@ public static void AddDancingGoatServices(this IServiceCollection services) { AddViewComponentServices(services); AddRepositories(services); - + services.AddSingleton(); } @@ -32,6 +34,9 @@ private static void AddRepositories(IServiceCollection services) } - private static void AddViewComponentServices(IServiceCollection services) => services.AddSingleton(); + private static void AddViewComponentServices(IServiceCollection services) + { + services.AddSingleton(); + } } } diff --git a/examples/DancingGoat/Views/Shared/_DancingGoatLayout.cshtml b/examples/DancingGoat/Views/Shared/_DancingGoatLayout.cshtml new file mode 100644 index 0000000..609db2e --- /dev/null +++ b/examples/DancingGoat/Views/Shared/_DancingGoatLayout.cshtml @@ -0,0 +1,142 @@ +@using CMS.Websites.Routing; +@using CMS.Websites; +@using Kentico.Content.Web.Mvc.Routing; +@using Kentico.Forms.Web.Mvc.Widgets +@using Kentico.Xperience.Admin.Base.Forms +@using DancingGoat.Controllers +@using DancingGoat.Models; +@using DancingGoat.ViewComponents +@using Kentico.Activities.Web.Mvc + +@inject HomePageRepository homepageRepository; +@inject IWebPageUrlRetriever webPageUrlRetriever; +@inject IPreferredLanguageRetriever currentLanguageRetriever; +@inject IWebsiteChannelContext websiteChannelContext; + +@{ + const string ENGLISH = "English"; + const string ESPANOL = "Español"; + + var language = currentLanguageRetriever.Get(); + + var homePageUrl = (await webPageUrlRetriever.Retrieve(DancingGoatConstants.HOME_PAGE_PATH, websiteChannelContext.WebsiteChannelName, language)).RelativePath; + + var routeDataLanguage = Convert.ToString(@ViewContext.RouteData.Values[WebPageRoutingOptions.LANGUAGE_ROUTE_VALUE_KEY]); + var currentLanguage = routeDataLanguage.Equals("es", StringComparison.OrdinalIgnoreCase) ? "ES" : "EN"; + + var subscriptionFormWidgetProperties = new FormWidgetProperties + { + SelectedForm = new List { new() { ObjectCodeName = "DancingGoatSubscription" } }, + AfterSubmitDisplayText = HtmlLocalizer["Thank you for subscribing! Now we just need to confirm your email address - please click the link in the email we sent you. Thanks!"].Value + }; +} + + + + + + + + + + @ViewBag.Title - Dancing Goat + @RenderSection("styles", required: false) + @Html.Kentico().ActivityLoggingScriptV2() + + + +
+ +
+ +
+
+
+ @RenderBody() + +
+
+
+
+ + + + @RenderSection("scripts", required: false) + + diff --git a/examples/DancingGoat/Views/Shared/_LandingPageLayout.cshtml b/examples/DancingGoat/Views/Shared/_LandingPageLayout.cshtml index ddb9639..0af8542 100644 --- a/examples/DancingGoat/Views/Shared/_LandingPageLayout.cshtml +++ b/examples/DancingGoat/Views/Shared/_LandingPageLayout.cshtml @@ -29,7 +29,6 @@ - diff --git a/examples/DancingGoat/Views/Shared/_Layout.cshtml b/examples/DancingGoat/Views/Shared/_Layout.cshtml index 9a53b2b..c610685 100644 --- a/examples/DancingGoat/Views/Shared/_Layout.cshtml +++ b/examples/DancingGoat/Views/Shared/_Layout.cshtml @@ -1,142 +1,11 @@ -@using CMS.Websites.Routing; -@using CMS.Websites; -@using Kentico.Content.Web.Mvc.Routing; -@using Kentico.Forms.Web.Mvc.Widgets -@using Kentico.Xperience.Admin.Base.Forms -@using DancingGoat.Controllers -@using DancingGoat.Models; -@using DancingGoat.ViewComponents -@using Kentico.Activities.Web.Mvc - -@inject HomePageRepository homepageRepository; -@inject IWebPageUrlRetriever webPageUrlRetriever; -@inject IPreferredLanguageRetriever currentLanguageRetriever; -@inject IWebsiteChannelContext websiteChannelContext; - -@{ - const string ENGLISH = "English"; - const string ESPANOL = "Español"; - - var language = currentLanguageRetriever.Get(); - - var homePageUrl = (await webPageUrlRetriever.Retrieve(DancingGoatConstants.HOME_PAGE_PATH, websiteChannelContext.WebsiteChannelName, language)).RelativePath; - - var routeDataLanguage = Convert.ToString(@ViewContext.RouteData.Values[WebPageRoutingOptions.LANGUAGE_ROUTE_VALUE_KEY]); - var currentLanguage = routeDataLanguage.Equals("es", StringComparison.OrdinalIgnoreCase) ? "ES" : "EN"; - - var subscriptionFormWidgetProperties = new FormWidgetProperties - { - SelectedForm = new List { new() { ObjectCodeName = "DancingGoatSubscription" } }, - AfterSubmitDisplayText = HtmlLocalizer["Thank you for subscribing! Now we just need to confirm your email address - please click the link in the email we sent you. Thanks!"].Value - }; -} - - - + - - - @ViewBag.Title - Dancing Goat - @RenderSection("styles", required: false) - @Html.Kentico().ActivityLoggingAPI() - @Html.Kentico().ActivityLoggingScript() - + @ViewBag.Title - -
- -
- -
-
-
- @RenderBody() - -
-
-
-
- - - @RenderSection("scripts", required: false) + + @RenderBody() - + \ No newline at end of file diff --git a/examples/DancingGoat/Views/Shared/_Reference.cshtml b/examples/DancingGoat/Views/Shared/_Reference.cshtml index 1d1956b..63e2f85 100644 --- a/examples/DancingGoat/Views/Shared/_Reference.cshtml +++ b/examples/DancingGoat/Views/Shared/_Reference.cshtml @@ -9,10 +9,10 @@ @Model.ImageShortDescription
-
@Html.Raw(Model.Text)
+
@Html.Raw(HTMLHelper.HTMLEncode(Model.Text))
-
@Html.Raw(Model.Name)
-
@Html.Raw(Model.Description)
+
@Html.Raw(HTMLHelper.HTMLEncode(Model.Name))
+
@Html.Raw(HTMLHelper.HTMLEncode(Model.Description))
diff --git a/examples/DancingGoat/Views/_ViewStart.cshtml b/examples/DancingGoat/Views/_ViewStart.cshtml index a5f1004..276f870 100644 --- a/examples/DancingGoat/Views/_ViewStart.cshtml +++ b/examples/DancingGoat/Views/_ViewStart.cshtml @@ -1,3 +1,17 @@ -@{ - Layout = "_Layout"; +@using CMS.ContentEngine; +@using CMS.DataEngine; +@using CMS.Websites; +@using CMS.Websites.Routing; + +@inject IWebsiteChannelContext websiteChannelContext; + +@{ + if (string.Equals(websiteChannelContext.WebsiteChannelName, DancingGoatConstants.WEBSITE_CHANNEL_NAME, StringComparison.Ordinal)) + { + Layout = "_DancingGoatLayout"; + } + else + { + Layout = "_Layout"; + } } diff --git a/examples/DancingGoat/wwwroot/Scripts/mobileMenu.js b/examples/DancingGoat/wwwroot/Scripts/mobileMenu.js new file mode 100644 index 0000000..6e593e0 --- /dev/null +++ b/examples/DancingGoat/wwwroot/Scripts/mobileMenu.js @@ -0,0 +1,23 @@ +(function () { + const navToggle = document.querySelector('.nav-toggle'); + const menu = document.querySelector('.nav-menu'); + + // Toggles mobile menu on toggle button click + navToggle.addEventListener('click', function (e) { + e.preventDefault(); + navToggle.classList.toggle('active'); + menu.classList.toggle('active'); + }); + + // Close mobile menu on click away + document.addEventListener('mouseup', function (e) { + // If menu is active and if target is not menu and its children + // nor menu toggle button and its children + if (menu.classList.contains('active') && !menu.contains(e.target) && + e.target !== navToggle && !navToggle.contains(e.target)) { + e.stopPropagation(); + navToggle.classList.remove('active'); + menu.classList.remove('active'); + } + }); +})(); \ No newline at end of file From 6b2bc65931155fe152574fe790b3aeed37010a5a Mon Sep 17 00:00:00 2001 From: Martin Foukal Date: Thu, 18 Jan 2024 18:03:45 +0100 Subject: [PATCH 16/23] CRM setting info custom class - wip --- examples/DancingGoat/DancingGoat.csproj | 3 + .../CRMIntegrationSettingsInfo.generated.cs | 170 +++++++++++++++ ...tegrationSettingsInfoProvider.generated.cs | 19 ++ ...tegrationSettingsInfoProvider.generated.cs | 11 + .../Installers/CRMModuleInstaller.cs | 202 ++++++++---------- .../DynamicsIntegrationSettingsApplication.cs | 19 ++ .../Admin/DynamicsIntegrationSettingsEdit.cs | 24 +++ .../DynamicsIntegrationGlobalEvents.cs | 12 +- .../SalesForceIntegrationGlobalEvents.cs | 12 +- 9 files changed, 350 insertions(+), 122 deletions(-) create mode 100644 src/Kentico.Xperience.CRM.Common/Classes/CRMIntegrationSettingsInfo.generated.cs create mode 100644 src/Kentico.Xperience.CRM.Common/Classes/CRMIntegrationSettingsInfoProvider.generated.cs create mode 100644 src/Kentico.Xperience.CRM.Common/Classes/ICRMIntegrationSettingsInfoProvider.generated.cs create mode 100644 src/Kentico.Xperience.CRM.Dynamics/Admin/DynamicsIntegrationSettingsApplication.cs create mode 100644 src/Kentico.Xperience.CRM.Dynamics/Admin/DynamicsIntegrationSettingsEdit.cs diff --git a/examples/DancingGoat/DancingGoat.csproj b/examples/DancingGoat/DancingGoat.csproj index ff6da52..38a70ff 100644 --- a/examples/DancingGoat/DancingGoat.csproj +++ b/examples/DancingGoat/DancingGoat.csproj @@ -32,4 +32,7 @@ + + + \ No newline at end of file diff --git a/src/Kentico.Xperience.CRM.Common/Classes/CRMIntegrationSettingsInfo.generated.cs b/src/Kentico.Xperience.CRM.Common/Classes/CRMIntegrationSettingsInfo.generated.cs new file mode 100644 index 0000000..d4450a3 --- /dev/null +++ b/src/Kentico.Xperience.CRM.Common/Classes/CRMIntegrationSettingsInfo.generated.cs @@ -0,0 +1,170 @@ +using System; +using System.Data; +using System.Runtime.Serialization; + +using CMS; +using CMS.DataEngine; +using CMS.Helpers; +using Kentico.Xperience.CRM.Common.Classes; + +[assembly: RegisterObjectType(typeof(CRMIntegrationSettingsInfo), CRMIntegrationSettingsInfo.OBJECT_TYPE)] + +namespace Kentico.Xperience.CRM.Common.Classes +{ + /// + /// Data container class for . + /// + [Serializable] + public partial class CRMIntegrationSettingsInfo : AbstractInfo + { + /// + /// Object type. + /// + public const string OBJECT_TYPE = "kenticocrmcommon.crmintegrationsettings"; + + + /// + /// Type information. + /// + public static readonly ObjectTypeInfo TYPEINFO = new ObjectTypeInfo(typeof(CRMIntegrationSettingsInfoProvider), OBJECT_TYPE, "KenticoCRMCommon.CRMIntegrationSettings", "CRMIntegrationSettingsItemID", null, null, null, null, null, null, null) + { + TouchCacheDependencies = true, + }; + + + /// + /// CRM integration settings item ID. + /// + [DatabaseField] + public virtual int CRMIntegrationSettingsItemID + { + get => ValidationHelper.GetInteger(GetValue(nameof(CRMIntegrationSettingsItemID)), 0); + set => SetValue(nameof(CRMIntegrationSettingsItemID), value); + } + + + /// + /// CRM integration settings forms enabled. + /// + [DatabaseField] + public virtual bool CRMIntegrationSettingsFormsEnabled + { + get => ValidationHelper.GetBoolean(GetValue(nameof(CRMIntegrationSettingsFormsEnabled)), false); + set => SetValue(nameof(CRMIntegrationSettingsFormsEnabled), value); + } + + + /// + /// CRM integration settings contacts enabled. + /// + [DatabaseField] + public virtual bool CRMIntegrationSettingsContactsEnabled + { + get => ValidationHelper.GetBoolean(GetValue(nameof(CRMIntegrationSettingsContactsEnabled)), false); + set => SetValue(nameof(CRMIntegrationSettingsContactsEnabled), value); + } + + + /// + /// CRM integration settings ignore existing records. + /// + [DatabaseField] + public virtual bool CRMIntegrationSettingsIgnoreExistingRecords + { + get => ValidationHelper.GetBoolean(GetValue(nameof(CRMIntegrationSettingsIgnoreExistingRecords)), false); + set => SetValue(nameof(CRMIntegrationSettingsIgnoreExistingRecords), value); + } + + + /// + /// CRM integration settings url. + /// + [DatabaseField] + public virtual string CRMIntegrationSettingsUrl + { + get => ValidationHelper.GetString(GetValue(nameof(CRMIntegrationSettingsUrl)), String.Empty); + set => SetValue(nameof(CRMIntegrationSettingsUrl), value); + } + + + /// + /// CRM integration settings client id. + /// + [DatabaseField] + public virtual string CRMIntegrationSettingsClientId + { + get => ValidationHelper.GetString(GetValue(nameof(CRMIntegrationSettingsClientId)), String.Empty); + set => SetValue(nameof(CRMIntegrationSettingsClientId), value); + } + + + /// + /// CRM integration settings client secret. + /// + [DatabaseField] + public virtual string CRMIntegrationSettingsClientSecret + { + get => ValidationHelper.GetString(GetValue(nameof(CRMIntegrationSettingsClientSecret)), String.Empty); + set => SetValue(nameof(CRMIntegrationSettingsClientSecret), value); + } + + + /// + /// CRM integration settings CRM type. + /// + [DatabaseField] + public virtual string CRMIntegrationSettingsCRMType + { + get => ValidationHelper.GetString(GetValue(nameof(CRMIntegrationSettingsCRMType)), String.Empty); + set => SetValue(nameof(CRMIntegrationSettingsCRMType), value); + } + + + /// + /// Deletes the object using appropriate provider. + /// + protected override void DeleteObject() + { + Provider.Delete(this); + } + + + /// + /// Updates the object using appropriate provider. + /// + protected override void SetObject() + { + Provider.Set(this); + } + + + /// + /// Constructor for de-serialization. + /// + /// Serialization info. + /// Streaming context. + protected CRMIntegrationSettingsInfo(SerializationInfo info, StreamingContext context) + : base(info, context) + { + } + + + /// + /// Creates an empty instance of the class. + /// + public CRMIntegrationSettingsInfo() + : base(TYPEINFO) + { + } + + + /// + /// Creates a new instances of the class from the given . + /// + /// DataRow with the object data. + public CRMIntegrationSettingsInfo(DataRow dr) + : base(TYPEINFO, dr) + { + } + } +} \ No newline at end of file diff --git a/src/Kentico.Xperience.CRM.Common/Classes/CRMIntegrationSettingsInfoProvider.generated.cs b/src/Kentico.Xperience.CRM.Common/Classes/CRMIntegrationSettingsInfoProvider.generated.cs new file mode 100644 index 0000000..98480a7 --- /dev/null +++ b/src/Kentico.Xperience.CRM.Common/Classes/CRMIntegrationSettingsInfoProvider.generated.cs @@ -0,0 +1,19 @@ +using CMS.DataEngine; + +namespace Kentico.Xperience.CRM.Common.Classes +{ + /// + /// Class providing management. + /// + [ProviderInterface(typeof(ICRMIntegrationSettingsInfoProvider))] + public partial class CRMIntegrationSettingsInfoProvider : AbstractInfoProvider, ICRMIntegrationSettingsInfoProvider + { + /// + /// Initializes a new instance of the class. + /// + public CRMIntegrationSettingsInfoProvider() + : base(CRMIntegrationSettingsInfo.TYPEINFO) + { + } + } +} \ No newline at end of file diff --git a/src/Kentico.Xperience.CRM.Common/Classes/ICRMIntegrationSettingsInfoProvider.generated.cs b/src/Kentico.Xperience.CRM.Common/Classes/ICRMIntegrationSettingsInfoProvider.generated.cs new file mode 100644 index 0000000..f03ea46 --- /dev/null +++ b/src/Kentico.Xperience.CRM.Common/Classes/ICRMIntegrationSettingsInfoProvider.generated.cs @@ -0,0 +1,11 @@ +using CMS.DataEngine; + +namespace Kentico.Xperience.CRM.Common.Classes +{ + /// + /// Declares members for management. + /// + public partial interface ICRMIntegrationSettingsInfoProvider : IInfoProvider, IInfoByIdProvider + { + } +} \ No newline at end of file diff --git a/src/Kentico.Xperience.CRM.Common/Installers/CRMModuleInstaller.cs b/src/Kentico.Xperience.CRM.Common/Installers/CRMModuleInstaller.cs index 3fd1833..04f16d2 100644 --- a/src/Kentico.Xperience.CRM.Common/Installers/CRMModuleInstaller.cs +++ b/src/Kentico.Xperience.CRM.Common/Installers/CRMModuleInstaller.cs @@ -27,7 +27,7 @@ public void Install(string crmtype) { var resourceInfo = InstallModule(); InstallModuleClasses(resourceInfo); - InstallSettings(resourceInfo, crmtype); + InstallCRMIntegrationSettingsClass(resourceInfo); } private ResourceInfo InstallModule() @@ -217,133 +217,105 @@ private void InstallFailedSyncItemClass(ResourceInfo resourceInfo) DataClassInfoProvider.SetDataClassInfo(failedSyncItemClass); } - - private void InstallSettings(ResourceInfo resourceInfo, string crmType) + + private void InstallCRMIntegrationSettingsClass(ResourceInfo resourceInfo) { - var crmIntegrations = SettingsCategoryInfo.Provider.Get("kenticocrmcommon.crmintegrations"); - if (crmIntegrations is null) + var settingsCRM = DataClassInfoProvider.GetDataClassInfo("kenticocrmcommon.crmintegrationsettings"); + if (settingsCRM is not null) { - var rootSettings = SettingsCategoryInfo.Provider.Get("CMS.Settings"); - if (rootSettings is null) - throw new InvalidOperationException("Category 'CMS.Settings' root not found"); - - crmIntegrations = new SettingsCategoryInfo - { - CategoryName = "kenticocrmcommon.crmintegrations", - CategoryDisplayName = "CRM integrations", - CategoryParentID = rootSettings.CategoryID, - CategoryLevel = 1, - CategoryResourceID = resourceInfo.ResourceID, - CategoryIsCustom = true, - CategoryIsGroup = false, - CategoryOrder = SettingsCategoryInfo.Provider.Get() - .Where(c => c.CategoryLevel == 1) - .Max(c => c.CategoryOrder) + 1 - }; - - SettingsCategoryInfo.Provider.Set(crmIntegrations); + return; } + + settingsCRM = DataClassInfo.New("kenticocrmcommon.crmintegrationsettings"); + + settingsCRM.ClassName = "KenticoCRMCommon.CRMIntegrationSettings"; + settingsCRM.ClassTableName = "KenticoCRMCommon_CRMIntegrationSettings"; + settingsCRM.ClassDisplayName = "CRM integration settings"; + settingsCRM.ClassResourceID = resourceInfo.ResourceID; + settingsCRM.ClassType = ClassType.OTHER; + + var formInfo = FormHelper.GetBasicFormDefinition("CRMIntegrationSettingsItemID"); - var crmCategory = SettingsCategoryInfo.Provider.Get($"kenticocrmcommon.{crmType}"); - if (crmCategory is null) + var formItem = new FormFieldInfo { - crmCategory = new SettingsCategoryInfo - { - CategoryName = $"kenticocrmcommon.{crmType}", - CategoryDisplayName = $"{crmType} settings", - CategoryParentID = crmIntegrations.CategoryID, - CategoryLevel = 2, - CategoryResourceID = resourceInfo.ResourceID, - CategoryIsCustom = true, - CategoryIsGroup = true - }; - - SettingsCategoryInfo.Provider.Set(crmCategory); - } - - var settingFormsEnabled = SettingsKeyInfo.Provider.Get($"CMS{crmType}CRMIntegrationFormLeadsEnabled"); - if (settingFormsEnabled is null) + Name = "CRMIntegrationSettingsFormsEnabled", + Caption = "Forms enabled", + Visible = true, + DataType = "boolean", + Enabled = true + }; + formInfo.AddFormItem(formItem); + + formItem = new FormFieldInfo { - settingFormsEnabled = new SettingsKeyInfo - { - KeyName = $"CMS{crmType}CRMIntegrationFormLeadsEnabled", - KeyDisplayName = "Form leads enabled", - KeyDescription = "", - KeyType = "boolean", - KeyCategoryID = crmCategory.CategoryID, - KeyIsCustom = true, - KeyExplanationText = "", - }; - - SettingsKeyInfo.Provider.Set(settingFormsEnabled); - } + Name = "CRMIntegrationSettingsContactsEnabled", + Caption = "Contacts enabled", + Visible = false, + DataType = "boolean", + Enabled = true + }; + formInfo.AddFormItem(formItem); - var settingsIgnoreExisting = SettingsKeyInfo.Provider.Get($"CMS{crmType}CRMIntegrationIgnoreExistingRecords"); - if (settingsIgnoreExisting is null) + formItem = new FormFieldInfo { - settingsIgnoreExisting = new SettingsKeyInfo - { - KeyName = $"CMS{crmType}CRMIntegrationIgnoreExistingRecords", - KeyDisplayName = "Ignore existing records", - KeyDescription = "", - KeyType = "boolean", - KeyCategoryID = crmCategory.CategoryID, - KeyIsCustom = true, - KeyExplanationText = "If true no existing item with same email or paired record by ID is updated" - }; - - SettingsKeyInfo.Provider.Set(settingsIgnoreExisting); - } + Name = "CRMIntegrationSettingsIgnoreExistingRecords", + Caption = "Ignore existing records", + Visible = true, + DataType = "boolean", + Enabled = true + }; + formInfo.AddFormItem(formItem); + + formItem = new FormFieldInfo + { + Name = "CRMIntegrationSettingsUrl", + Caption = "CRM URL", + Visible = true, + Precision = 0, + Size = 100, + DataType = "text", + Enabled = true + }; + formInfo.AddFormItem(formItem); - var settingUrl = SettingsKeyInfo.Provider.Get($"CMS{crmType}CRMIntegration{crmType}Url"); - if (settingUrl is null) + formItem = new FormFieldInfo { - settingUrl = new SettingsKeyInfo - { - KeyName = $"CMS{crmType}CRMIntegration{crmType}Url", - KeyDisplayName = $"{crmType} URL", - KeyDescription = "", - KeyType = "string", - KeyCategoryID = crmCategory.CategoryID, - KeyIsCustom = true, - KeyExplanationText = "", - }; - - SettingsKeyInfo.Provider.Set(settingUrl); - } + Name = "CRMIntegrationSettingsClientId", + Caption = "Client ID", + Visible = true, + Precision = 0, + Size = 100, + DataType = "text", + Enabled = true + }; + formInfo.AddFormItem(formItem); - var settingClientId = SettingsKeyInfo.Provider.Get($"CMS{crmType}CRMIntegrationClientId"); - if (settingClientId is null) + formItem = new FormFieldInfo { - settingClientId = new SettingsKeyInfo - { - KeyName = $"CMS{crmType}CRMIntegrationClientId", - KeyDisplayName = "Client ID", - KeyDescription = "", - KeyType = "string", - KeyCategoryID = crmCategory.CategoryID, - KeyIsCustom = true, - KeyExplanationText = "", - }; - - SettingsKeyInfo.Provider.Set(settingClientId); - } + Name = "CRMIntegrationSettingsClientSecret", + Caption = "Client Secret", + Visible = true, + Precision = 0, + Size = 100, + DataType = "text", + Enabled = true + }; + formInfo.AddFormItem(formItem); - var settingClientSecret = SettingsKeyInfo.Provider.Get($"CMS{crmType}CRMIntegrationClientSecret"); - if (settingClientSecret is null) + formItem = new FormFieldInfo { - settingClientSecret = new SettingsKeyInfo - { - KeyName = $"CMS{crmType}CRMIntegrationClientSecret", - KeyDisplayName = "Client Secret", - KeyDescription = "", - KeyType = "string", - KeyCategoryID = crmCategory.CategoryID, - KeyIsCustom = true, - KeyExplanationText = "", - }; - - SettingsKeyInfo.Provider.Set(settingClientSecret); - } + Name = "CRMIntegrationSettingsCRMType", + Visible = false, + Precision = 0, + Size = 50, + DataType = "text", + Enabled = true + }; + formInfo.AddFormItem(formItem); + + + settingsCRM.ClassFormDefinition = formInfo.GetXmlDefinition(); + + DataClassInfoProvider.SetDataClassInfo(settingsCRM); } } \ No newline at end of file diff --git a/src/Kentico.Xperience.CRM.Dynamics/Admin/DynamicsIntegrationSettingsApplication.cs b/src/Kentico.Xperience.CRM.Dynamics/Admin/DynamicsIntegrationSettingsApplication.cs new file mode 100644 index 0000000..5e02a71 --- /dev/null +++ b/src/Kentico.Xperience.CRM.Dynamics/Admin/DynamicsIntegrationSettingsApplication.cs @@ -0,0 +1,19 @@ +using Kentico.Xperience.Admin.Base; +using Kentico.Xperience.Admin.Base.UIPages; +using Kentico.Xperience.CRM.Dynamics.Admin; + +[assembly: UIApplication( + identifier: DynamicsIntegrationSettingsApplication.IDENTIFIER, + type: typeof(DynamicsIntegrationSettingsApplication), + slug: "dynamics-settings", + name: "Dynamics CRM Integration Settings", + category: BaseApplicationCategories.CONFIGURATION, + icon: Icons.IntegrationScheme, + templateName: TemplateNames.SECTION_LAYOUT)] + +namespace Kentico.Xperience.CRM.Dynamics.Admin; + +public class DynamicsIntegrationSettingsApplication : ApplicationPage +{ + public const string IDENTIFIER = "Kentico.Xperience.CRM.Dynamics.IntegrationSettings"; +} \ No newline at end of file diff --git a/src/Kentico.Xperience.CRM.Dynamics/Admin/DynamicsIntegrationSettingsEdit.cs b/src/Kentico.Xperience.CRM.Dynamics/Admin/DynamicsIntegrationSettingsEdit.cs new file mode 100644 index 0000000..8bb5d38 --- /dev/null +++ b/src/Kentico.Xperience.CRM.Dynamics/Admin/DynamicsIntegrationSettingsEdit.cs @@ -0,0 +1,24 @@ +using Kentico.Xperience.Admin.Base; +using Kentico.Xperience.Admin.Base.Forms; +using Kentico.Xperience.CRM.Common.Classes; +using Kentico.Xperience.CRM.Dynamics.Admin; + +[assembly: UIPage( + parentType: typeof(DynamicsIntegrationSettingsApplication), + slug: "edit", + uiPageType: typeof(DynamicsIntegrationSettingsEdit), + name: "Edit settings", + templateName: TemplateNames.EDIT, + order:UIPageOrder.First)] + +namespace Kentico.Xperience.CRM.Dynamics.Admin; + +public class DynamicsIntegrationSettingsEdit : InfoEditPage +{ + public DynamicsIntegrationSettingsEdit(IFormComponentMapper formComponentMapper, IFormDataBinder formDataBinder) : + base(formComponentMapper, formDataBinder) + { + } + + public override int ObjectId { get; set; } = 1; //just 1 settings in DB (with ID 1) +} \ No newline at end of file diff --git a/src/Kentico.Xperience.CRM.Dynamics/DynamicsIntegrationGlobalEvents.cs b/src/Kentico.Xperience.CRM.Dynamics/DynamicsIntegrationGlobalEvents.cs index 6b739e5..4a4deb7 100644 --- a/src/Kentico.Xperience.CRM.Dynamics/DynamicsIntegrationGlobalEvents.cs +++ b/src/Kentico.Xperience.CRM.Dynamics/DynamicsIntegrationGlobalEvents.cs @@ -29,12 +29,18 @@ public DynamicsIntegrationGlobalEvents() : base(nameof(DynamicsIntegrationGlobal private ILogger logger = null!; - protected override void OnInit() + protected override void OnInit(ModuleInitParameters parameters) { - base.OnInit(); - + base.OnInit(parameters); + + var services = parameters.Services; + + logger = services.GetRequiredService>(); + services.GetRequiredService().Install(CRMType.Dynamics); + BizFormItemEvents.Insert.After += SynchronizeBizFormLead; BizFormItemEvents.Update.After += SynchronizeBizFormLead; + logger = Service.Resolve>(); Service.Resolve().Install(CRMType.Dynamics); RequestEvents.RunEndRequestTasks.Execute += (_, _) => diff --git a/src/Kentico.Xperience.CRM.SalesForce/SalesForceIntegrationGlobalEvents.cs b/src/Kentico.Xperience.CRM.SalesForce/SalesForceIntegrationGlobalEvents.cs index a7bc089..531f45f 100644 --- a/src/Kentico.Xperience.CRM.SalesForce/SalesForceIntegrationGlobalEvents.cs +++ b/src/Kentico.Xperience.CRM.SalesForce/SalesForceIntegrationGlobalEvents.cs @@ -29,14 +29,18 @@ public SalesForceIntegrationGlobalEvents() : base(nameof(SalesForceIntegrationGl { } - protected override void OnInit() + protected override void OnInit(ModuleInitParameters parameters) { base.OnInit(); - + + var services = parameters.Services; + + logger = services.GetRequiredService>(); + Service.Resolve().Install(CRMType.SalesForce); + BizFormItemEvents.Insert.After += SynchronizeBizFormLead; BizFormItemEvents.Update.After += SynchronizeBizFormLead; - logger = Service.Resolve>(); - Service.Resolve().Install(CRMType.SalesForce); + ThreadWorker.Current.EnsureRunningThread(); RequestEvents.RunEndRequestTasks.Execute += (_, _) => From 6f4a57029b3287714167c1df3de3fb48911df467 Mon Sep 17 00:00:00 2001 From: Martin Foukal Date: Fri, 19 Jan 2024 18:14:29 +0100 Subject: [PATCH 17/23] admin settings re-work to custom class and ui edit pages --- examples/DancingGoat/DancingGoat.csproj | 3 - .../Admin/CRMIntegrationSettingsEdit.cs | 62 +++++++++++++++++++ .../Admin/CRMSyncItemListing.cs | 6 +- .../CRMIntegrationSettingsInfo.generated.cs | 6 +- .../Installers/CRMModuleInstaller.cs | 25 ++++---- .../Models/CRMIntegrationSettingsModel.cs | 31 ++++++++++ .../ServiceCollectionExtensions.cs | 1 + .../Services/ICRMSettingsService.cs | 11 ++++ .../Implementations/CRMSettingsService.cs | 28 +++++++++ .../DynamicsIntegrationSettingsApplication.cs | 2 +- .../Admin/DynamicsIntegrationSettingsEdit.cs | 20 +++--- .../DynamicsServiceCollectionExtensions.cs | 18 +++--- .../Workers/FailedItemsWorker.cs | 2 +- ...SalesForceIntegrationSettingApplication.cs | 19 ++++++ .../SalesForceIntegrationSettingsEdit.cs | 28 +++++++++ .../SalesForceServiceCollectionsExtensions.cs | 20 +++--- .../Workers/FailedItemsWorker.cs | 2 +- 17 files changed, 232 insertions(+), 52 deletions(-) create mode 100644 src/Kentico.Xperience.CRM.Common/Admin/CRMIntegrationSettingsEdit.cs create mode 100644 src/Kentico.Xperience.CRM.Common/Models/CRMIntegrationSettingsModel.cs create mode 100644 src/Kentico.Xperience.CRM.Common/Services/ICRMSettingsService.cs create mode 100644 src/Kentico.Xperience.CRM.Common/Services/Implementations/CRMSettingsService.cs create mode 100644 src/Kentico.Xperience.CRM.SalesForce/Admin/SalesForceIntegrationSettingApplication.cs create mode 100644 src/Kentico.Xperience.CRM.SalesForce/Admin/SalesForceIntegrationSettingsEdit.cs diff --git a/examples/DancingGoat/DancingGoat.csproj b/examples/DancingGoat/DancingGoat.csproj index 38a70ff..ff6da52 100644 --- a/examples/DancingGoat/DancingGoat.csproj +++ b/examples/DancingGoat/DancingGoat.csproj @@ -32,7 +32,4 @@ - - - \ No newline at end of file diff --git a/src/Kentico.Xperience.CRM.Common/Admin/CRMIntegrationSettingsEdit.cs b/src/Kentico.Xperience.CRM.Common/Admin/CRMIntegrationSettingsEdit.cs new file mode 100644 index 0000000..862b675 --- /dev/null +++ b/src/Kentico.Xperience.CRM.Common/Admin/CRMIntegrationSettingsEdit.cs @@ -0,0 +1,62 @@ +using Kentico.Xperience.Admin.Base; +using Kentico.Xperience.Admin.Base.Forms; +using Kentico.Xperience.CRM.Common.Classes; +using Kentico.Xperience.CRM.Common.Models; +using IFormItemCollectionProvider = Kentico.Xperience.Admin.Base.Forms.Internal.IFormItemCollectionProvider; + +namespace Kentico.Xperience.CRM.Common.Admin; + +/// +/// Base edit page for Admin CRM integration setting +/// +public abstract class CRMIntegrationSettingsEdit : ModelEditPage +{ + private readonly ICRMIntegrationSettingsInfoProvider crmIntegrationSettingsInfoProvider; + + protected CRMIntegrationSettingsEdit(IFormItemCollectionProvider formItemCollectionProvider, + IFormDataBinder formDataBinder, + ICRMIntegrationSettingsInfoProvider crmIntegrationSettingsInfoProvider) : base(formItemCollectionProvider, + formDataBinder) + { + this.crmIntegrationSettingsInfoProvider = crmIntegrationSettingsInfoProvider; + } + + private CRMIntegrationSettingsInfo? settingsInfo; + + private CRMIntegrationSettingsInfo? SettingsInfo => settingsInfo ??= crmIntegrationSettingsInfoProvider.Get() + .WhereEquals(nameof(CRMIntegrationSettingsInfo.CRMIntegrationSettingsCRMName), CRMName) + .TopN(1) + .FirstOrDefault(); + + protected override Task ProcessFormData(CRMIntegrationSettingsModel model, + ICollection formItems) + { + var info = SettingsInfo ?? new CRMIntegrationSettingsInfo(); + info.CRMIntegrationSettingsFormsEnabled = model.FormsEnabled; + info.CRMIntegrationSettingsContactsEnabled = model.ContactsEnabled; + info.CRMIntegrationSettingsIgnoreExistingRecords = model.IgnoreExistingRecords; + info.CRMIntegrationSettingsClientId = model.ClientId; + info.CRMIntegrationSettingsClientSecret = model.ClientSecret; + info.CRMIntegrationSettingsUrl = model.Url; + info.CRMIntegrationSettingsCRMName = CRMName; + + crmIntegrationSettingsInfoProvider.Set(info); + + return base.ProcessFormData(model, formItems); + } + + private CRMIntegrationSettingsModel? model; + protected override CRMIntegrationSettingsModel Model => model ??= SettingsInfo is null ? + new CRMIntegrationSettingsModel() : + new CRMIntegrationSettingsModel + { + FormsEnabled = SettingsInfo.CRMIntegrationSettingsFormsEnabled, + ContactsEnabled = SettingsInfo.CRMIntegrationSettingsContactsEnabled, + IgnoreExistingRecords = SettingsInfo.CRMIntegrationSettingsIgnoreExistingRecords, + Url = SettingsInfo.CRMIntegrationSettingsUrl, + ClientId = SettingsInfo.CRMIntegrationSettingsClientId, + ClientSecret = SettingsInfo.CRMIntegrationSettingsClientSecret + }; + + protected abstract string CRMName { get; } +} \ No newline at end of file diff --git a/src/Kentico.Xperience.CRM.Common/Admin/CRMSyncItemListing.cs b/src/Kentico.Xperience.CRM.Common/Admin/CRMSyncItemListing.cs index a9460a6..0356ffa 100644 --- a/src/Kentico.Xperience.CRM.Common/Admin/CRMSyncItemListing.cs +++ b/src/Kentico.Xperience.CRM.Common/Admin/CRMSyncItemListing.cs @@ -14,7 +14,7 @@ namespace Kentico.Xperience.CRM.Common.Admin; /// /// Admin listing page for displaying synced items in CMS for selected form /// -public class CRMSyncItemListing : ListingPage +internal class CRMSyncItemListing : ListingPage { private BizFormInfo? editedForm; private DataClassInfo? dataClassInfo; @@ -23,7 +23,7 @@ public class CRMSyncItemListing : ListingPage /// ID of the edited form. [PageParameter(typeof(IntPageModelBinder), typeof(FormEditSection))] public int FormId { get; set; } - + private BizFormInfo EditedForm => this.editedForm ??= AbstractInfo.Provider.Get(FormId); @@ -42,6 +42,6 @@ public override Task ConfigurePage() q.WhereEquals(nameof(CRMSyncItemInfo.CRMSyncItemEntityClass), DataClassInfo.ClassName) .OrderByDescending(nameof(CRMSyncItemInfo.CRMSyncItemLastModified))); - return Task.CompletedTask; + return base.ConfigurePage(); } } \ No newline at end of file diff --git a/src/Kentico.Xperience.CRM.Common/Classes/CRMIntegrationSettingsInfo.generated.cs b/src/Kentico.Xperience.CRM.Common/Classes/CRMIntegrationSettingsInfo.generated.cs index d4450a3..52510ec 100644 --- a/src/Kentico.Xperience.CRM.Common/Classes/CRMIntegrationSettingsInfo.generated.cs +++ b/src/Kentico.Xperience.CRM.Common/Classes/CRMIntegrationSettingsInfo.generated.cs @@ -113,10 +113,10 @@ public virtual string CRMIntegrationSettingsClientSecret /// CRM integration settings CRM type. ///
[DatabaseField] - public virtual string CRMIntegrationSettingsCRMType + public virtual string CRMIntegrationSettingsCRMName { - get => ValidationHelper.GetString(GetValue(nameof(CRMIntegrationSettingsCRMType)), String.Empty); - set => SetValue(nameof(CRMIntegrationSettingsCRMType), value); + get => ValidationHelper.GetString(GetValue(nameof(CRMIntegrationSettingsCRMName)), String.Empty); + set => SetValue(nameof(CRMIntegrationSettingsCRMName), value); } diff --git a/src/Kentico.Xperience.CRM.Common/Installers/CRMModuleInstaller.cs b/src/Kentico.Xperience.CRM.Common/Installers/CRMModuleInstaller.cs index 04f16d2..10d89bf 100644 --- a/src/Kentico.Xperience.CRM.Common/Installers/CRMModuleInstaller.cs +++ b/src/Kentico.Xperience.CRM.Common/Installers/CRMModuleInstaller.cs @@ -220,25 +220,26 @@ private void InstallFailedSyncItemClass(ResourceInfo resourceInfo) private void InstallCRMIntegrationSettingsClass(ResourceInfo resourceInfo) { - var settingsCRM = DataClassInfoProvider.GetDataClassInfo("kenticocrmcommon.crmintegrationsettings"); + var settingsCRM = DataClassInfoProvider.GetDataClassInfo(CRMIntegrationSettingsInfo.OBJECT_TYPE); if (settingsCRM is not null) { return; } - settingsCRM = DataClassInfo.New("kenticocrmcommon.crmintegrationsettings"); + settingsCRM = DataClassInfo.New(CRMIntegrationSettingsInfo.OBJECT_TYPE); - settingsCRM.ClassName = "KenticoCRMCommon.CRMIntegrationSettings"; - settingsCRM.ClassTableName = "KenticoCRMCommon_CRMIntegrationSettings"; + settingsCRM.ClassName = CRMIntegrationSettingsInfo.TYPEINFO.ObjectClassName; + settingsCRM.ClassTableName = CRMIntegrationSettingsInfo.TYPEINFO.ObjectClassName.Replace(".", "_"); settingsCRM.ClassDisplayName = "CRM integration settings"; settingsCRM.ClassResourceID = resourceInfo.ResourceID; settingsCRM.ClassType = ClassType.OTHER; - var formInfo = FormHelper.GetBasicFormDefinition("CRMIntegrationSettingsItemID"); + var formInfo = + FormHelper.GetBasicFormDefinition(nameof(CRMIntegrationSettingsInfo.CRMIntegrationSettingsItemID)); var formItem = new FormFieldInfo { - Name = "CRMIntegrationSettingsFormsEnabled", + Name = nameof(CRMIntegrationSettingsInfo.CRMIntegrationSettingsFormsEnabled), Caption = "Forms enabled", Visible = true, DataType = "boolean", @@ -248,7 +249,7 @@ private void InstallCRMIntegrationSettingsClass(ResourceInfo resourceInfo) formItem = new FormFieldInfo { - Name = "CRMIntegrationSettingsContactsEnabled", + Name = nameof(CRMIntegrationSettingsInfo.CRMIntegrationSettingsContactsEnabled), Caption = "Contacts enabled", Visible = false, DataType = "boolean", @@ -258,7 +259,7 @@ private void InstallCRMIntegrationSettingsClass(ResourceInfo resourceInfo) formItem = new FormFieldInfo { - Name = "CRMIntegrationSettingsIgnoreExistingRecords", + Name = nameof(CRMIntegrationSettingsInfo.CRMIntegrationSettingsIgnoreExistingRecords), Caption = "Ignore existing records", Visible = true, DataType = "boolean", @@ -268,7 +269,7 @@ private void InstallCRMIntegrationSettingsClass(ResourceInfo resourceInfo) formItem = new FormFieldInfo { - Name = "CRMIntegrationSettingsUrl", + Name = nameof(CRMIntegrationSettingsInfo.CRMIntegrationSettingsUrl), Caption = "CRM URL", Visible = true, Precision = 0, @@ -280,7 +281,7 @@ private void InstallCRMIntegrationSettingsClass(ResourceInfo resourceInfo) formItem = new FormFieldInfo { - Name = "CRMIntegrationSettingsClientId", + Name = nameof(CRMIntegrationSettingsInfo.CRMIntegrationSettingsClientId), Caption = "Client ID", Visible = true, Precision = 0, @@ -292,7 +293,7 @@ private void InstallCRMIntegrationSettingsClass(ResourceInfo resourceInfo) formItem = new FormFieldInfo { - Name = "CRMIntegrationSettingsClientSecret", + Name = nameof(CRMIntegrationSettingsInfo.CRMIntegrationSettingsClientSecret), Caption = "Client Secret", Visible = true, Precision = 0, @@ -304,7 +305,7 @@ private void InstallCRMIntegrationSettingsClass(ResourceInfo resourceInfo) formItem = new FormFieldInfo { - Name = "CRMIntegrationSettingsCRMType", + Name = nameof(CRMIntegrationSettingsInfo.CRMIntegrationSettingsCRMName), Visible = false, Precision = 0, Size = 50, diff --git a/src/Kentico.Xperience.CRM.Common/Models/CRMIntegrationSettingsModel.cs b/src/Kentico.Xperience.CRM.Common/Models/CRMIntegrationSettingsModel.cs new file mode 100644 index 0000000..5b74209 --- /dev/null +++ b/src/Kentico.Xperience.CRM.Common/Models/CRMIntegrationSettingsModel.cs @@ -0,0 +1,31 @@ +using Kentico.Xperience.Admin.Base.FormAnnotations; + +namespace Kentico.Xperience.CRM.Common.Models; + +/// +/// Model for CRM integration settings in Admin +/// +public class CRMIntegrationSettingsModel +{ + [CheckBoxComponent(Label = "Forms enabled", Order = 1)] + public bool FormsEnabled { get; set; } + + [CheckBoxComponent(Label = "Contacts enabled", Order = 2)] + public bool ContactsEnabled { get; set; } + + [CheckBoxComponent(Label = "Ignore existing records", Order = 3)] + public bool IgnoreExistingRecords { get; set; } + + [UrlValidationRule] + [TextInputComponent(Label = "CRM URL", Order = 4)] + [RequiredValidationRule] + public string? Url { get; set; } + + [RequiredValidationRule] + [TextInputComponent(Label = "Client ID", Order = 5)] + public string? ClientId { get; set; } + + [RequiredValidationRule] + [TextInputComponent(Label = "Client secret", Order = 6)] + public string? ClientSecret { get; set; } +} \ No newline at end of file diff --git a/src/Kentico.Xperience.CRM.Common/ServiceCollectionExtensions.cs b/src/Kentico.Xperience.CRM.Common/ServiceCollectionExtensions.cs index 0af82a1..05c6ed5 100644 --- a/src/Kentico.Xperience.CRM.Common/ServiceCollectionExtensions.cs +++ b/src/Kentico.Xperience.CRM.Common/ServiceCollectionExtensions.cs @@ -26,6 +26,7 @@ public static IServiceCollection AddKenticoCrmCommonFormLeadsIntegration( services.TryAddSingleton(); services.TryAddSingleton(); services.TryAddSingleton(); + services.TryAddSingleton(); return services; } diff --git a/src/Kentico.Xperience.CRM.Common/Services/ICRMSettingsService.cs b/src/Kentico.Xperience.CRM.Common/Services/ICRMSettingsService.cs new file mode 100644 index 0000000..797179e --- /dev/null +++ b/src/Kentico.Xperience.CRM.Common/Services/ICRMSettingsService.cs @@ -0,0 +1,11 @@ +using Kentico.Xperience.CRM.Common.Classes; + +namespace Kentico.Xperience.CRM.Common.Services; + +/// +/// Service for getting admin CRM settings +/// +public interface ICRMSettingsService +{ + CRMIntegrationSettingsInfo? GetSettings(string crmName); +} \ No newline at end of file diff --git a/src/Kentico.Xperience.CRM.Common/Services/Implementations/CRMSettingsService.cs b/src/Kentico.Xperience.CRM.Common/Services/Implementations/CRMSettingsService.cs new file mode 100644 index 0000000..eacdb02 --- /dev/null +++ b/src/Kentico.Xperience.CRM.Common/Services/Implementations/CRMSettingsService.cs @@ -0,0 +1,28 @@ +using CMS.Helpers; +using Kentico.Xperience.CRM.Common.Classes; + +namespace Kentico.Xperience.CRM.Common.Services.Implementations; + +internal class CRMSettingsService : ICRMSettingsService +{ + private readonly ICRMIntegrationSettingsInfoProvider integrationSettingsInfoProvider; + private readonly IProgressiveCache cache; + + public CRMSettingsService(ICRMIntegrationSettingsInfoProvider integrationSettingsInfoProvider, IProgressiveCache cache) + { + this.integrationSettingsInfoProvider = integrationSettingsInfoProvider; + this.cache = cache; + } + + + public CRMIntegrationSettingsInfo? GetSettings(string crmName) => + cache.Load(cs => integrationSettingsInfoProvider + .Get() + .WhereEquals(nameof(CRMIntegrationSettingsInfo.CRMIntegrationSettingsCRMName), crmName) + .TopN(1) + .FirstOrDefault() + , new CacheSettings(20, $"{nameof(CRMSettingsService)}|{crmName}") + { + CacheDependency = CacheHelper.GetCacheDependency($"{CRMIntegrationSettingsInfo.OBJECT_TYPE}|all") + }); +} \ No newline at end of file diff --git a/src/Kentico.Xperience.CRM.Dynamics/Admin/DynamicsIntegrationSettingsApplication.cs b/src/Kentico.Xperience.CRM.Dynamics/Admin/DynamicsIntegrationSettingsApplication.cs index 5e02a71..afd5cbe 100644 --- a/src/Kentico.Xperience.CRM.Dynamics/Admin/DynamicsIntegrationSettingsApplication.cs +++ b/src/Kentico.Xperience.CRM.Dynamics/Admin/DynamicsIntegrationSettingsApplication.cs @@ -13,7 +13,7 @@ namespace Kentico.Xperience.CRM.Dynamics.Admin; -public class DynamicsIntegrationSettingsApplication : ApplicationPage +internal class DynamicsIntegrationSettingsApplication : ApplicationPage { public const string IDENTIFIER = "Kentico.Xperience.CRM.Dynamics.IntegrationSettings"; } \ No newline at end of file diff --git a/src/Kentico.Xperience.CRM.Dynamics/Admin/DynamicsIntegrationSettingsEdit.cs b/src/Kentico.Xperience.CRM.Dynamics/Admin/DynamicsIntegrationSettingsEdit.cs index 8bb5d38..e4df3e3 100644 --- a/src/Kentico.Xperience.CRM.Dynamics/Admin/DynamicsIntegrationSettingsEdit.cs +++ b/src/Kentico.Xperience.CRM.Dynamics/Admin/DynamicsIntegrationSettingsEdit.cs @@ -1,24 +1,28 @@ using Kentico.Xperience.Admin.Base; using Kentico.Xperience.Admin.Base.Forms; +using Kentico.Xperience.CRM.Common.Admin; using Kentico.Xperience.CRM.Common.Classes; +using Kentico.Xperience.CRM.Common.Constants; using Kentico.Xperience.CRM.Dynamics.Admin; +using IFormItemCollectionProvider = Kentico.Xperience.Admin.Base.Forms.Internal.IFormItemCollectionProvider; [assembly: UIPage( parentType: typeof(DynamicsIntegrationSettingsApplication), - slug: "edit", - uiPageType: typeof(DynamicsIntegrationSettingsEdit), - name: "Edit settings", + slug: "edit", + uiPageType: typeof(DynamicsIntegrationSettingsEdit), + name: "Edit settings", templateName: TemplateNames.EDIT, - order:UIPageOrder.First)] + order: UIPageOrder.First)] namespace Kentico.Xperience.CRM.Dynamics.Admin; -public class DynamicsIntegrationSettingsEdit : InfoEditPage +internal class DynamicsIntegrationSettingsEdit : CRMIntegrationSettingsEdit { - public DynamicsIntegrationSettingsEdit(IFormComponentMapper formComponentMapper, IFormDataBinder formDataBinder) : - base(formComponentMapper, formDataBinder) + public DynamicsIntegrationSettingsEdit(IFormItemCollectionProvider formItemCollectionProvider, + IFormDataBinder formDataBinder, ICRMIntegrationSettingsInfoProvider crmIntegrationSettingsInfoProvider) : base( + formItemCollectionProvider, formDataBinder, crmIntegrationSettingsInfoProvider) { } - public override int ObjectId { get; set; } = 1; //just 1 settings in DB (with ID 1) + protected override string CRMName => CRMType.Dynamics; } \ No newline at end of file diff --git a/src/Kentico.Xperience.CRM.Dynamics/DynamicsServiceCollectionExtensions.cs b/src/Kentico.Xperience.CRM.Dynamics/DynamicsServiceCollectionExtensions.cs index b099612..5cbceed 100644 --- a/src/Kentico.Xperience.CRM.Dynamics/DynamicsServiceCollectionExtensions.cs +++ b/src/Kentico.Xperience.CRM.Dynamics/DynamicsServiceCollectionExtensions.cs @@ -2,6 +2,7 @@ using CMS.Helpers; using Kentico.Xperience.CRM.Common; using Kentico.Xperience.CRM.Common.Constants; +using Kentico.Xperience.CRM.Common.Services; using Kentico.Xperience.CRM.Dynamics.Configuration; using Kentico.Xperience.CRM.Dynamics.Services; using Microsoft.Extensions.Configuration; @@ -34,7 +35,7 @@ public static IServiceCollection AddDynamicsFormLeadsIntegration(this IServiceCo if (configuration is null) { serviceCollection.AddOptions() - .Configure(ConfigureWithCMSSettings); + .Configure(ConfigureWithCMSSettings); } else { @@ -69,16 +70,15 @@ private static ServiceClient GetCrmServiceClient(IServiceProvider serviceProvide return new ServiceClient(connectionString, logger); } - private static void ConfigureWithCMSSettings(DynamicsIntegrationSettings settings, ISettingsService settingsService) + private static void ConfigureWithCMSSettings(DynamicsIntegrationSettings settings, ICRMSettingsService settingsService) { - settings.FormLeadsEnabled = - ValidationHelper.GetBoolean(settingsService[SettingKeys.DynamicsFormLeadsEnabled], false); + var settingsInfo = settingsService.GetSettings(CRMType.Dynamics); + settings.FormLeadsEnabled = settingsInfo?.CRMIntegrationSettingsFormsEnabled ?? false; - settings.IgnoreExistingRecords = - ValidationHelper.GetBoolean(settingsService[SettingKeys.DynamicsIgnoreExistingRecords], false); + settings.IgnoreExistingRecords = settingsInfo?.CRMIntegrationSettingsIgnoreExistingRecords ?? false; - settings.ApiConfig.DynamicsUrl = settingsService[SettingKeys.DynamicsUrl]; - settings.ApiConfig.ClientId = settingsService[SettingKeys.DynamicsClientId]; - settings.ApiConfig.ClientSecret = settingsService[SettingKeys.DynamicsClientSecret]; + settings.ApiConfig.DynamicsUrl = settingsInfo?.CRMIntegrationSettingsUrl; + settings.ApiConfig.ClientId = settingsInfo?.CRMIntegrationSettingsClientId; + settings.ApiConfig.ClientSecret = settingsInfo?.CRMIntegrationSettingsClientSecret; } } \ No newline at end of file diff --git a/src/Kentico.Xperience.CRM.Dynamics/Workers/FailedItemsWorker.cs b/src/Kentico.Xperience.CRM.Dynamics/Workers/FailedItemsWorker.cs index b7de399..c07fd22 100644 --- a/src/Kentico.Xperience.CRM.Dynamics/Workers/FailedItemsWorker.cs +++ b/src/Kentico.Xperience.CRM.Dynamics/Workers/FailedItemsWorker.cs @@ -8,7 +8,7 @@ namespace Kentico.Xperience.CRM.Dynamics.Workers; /// /// Specific thread worker for Dynamics which try to synchronize failed items. It run each 1 minute. /// -public class FailedItemsWorker : FailedSyncItemsWorkerBase { protected override string CRMName => CRMType.Dynamics; diff --git a/src/Kentico.Xperience.CRM.SalesForce/Admin/SalesForceIntegrationSettingApplication.cs b/src/Kentico.Xperience.CRM.SalesForce/Admin/SalesForceIntegrationSettingApplication.cs new file mode 100644 index 0000000..6333ccb --- /dev/null +++ b/src/Kentico.Xperience.CRM.SalesForce/Admin/SalesForceIntegrationSettingApplication.cs @@ -0,0 +1,19 @@ +using Kentico.Xperience.Admin.Base; +using Kentico.Xperience.Admin.Base.UIPages; +using Kentico.Xperience.CRM.SalesForce.Admin; + +[assembly: UIApplication( + identifier: SalesForceIntegrationSettingApplication.IDENTIFIER, + type: typeof(SalesForceIntegrationSettingApplication), + slug: "salesforce-settings", + name: "SalesForce CRM Integration Settings", + category: BaseApplicationCategories.CONFIGURATION, + icon: Icons.IntegrationScheme, + templateName: TemplateNames.SECTION_LAYOUT)] + +namespace Kentico.Xperience.CRM.SalesForce.Admin; + +internal class SalesForceIntegrationSettingApplication : ApplicationPage +{ + public const string IDENTIFIER = "Kentico.Xperience.CRM.SalesForce.IntegrationSettings"; +} \ No newline at end of file diff --git a/src/Kentico.Xperience.CRM.SalesForce/Admin/SalesForceIntegrationSettingsEdit.cs b/src/Kentico.Xperience.CRM.SalesForce/Admin/SalesForceIntegrationSettingsEdit.cs new file mode 100644 index 0000000..d97388b --- /dev/null +++ b/src/Kentico.Xperience.CRM.SalesForce/Admin/SalesForceIntegrationSettingsEdit.cs @@ -0,0 +1,28 @@ +using Kentico.Xperience.Admin.Base; +using Kentico.Xperience.Admin.Base.Forms; +using Kentico.Xperience.CRM.Common.Admin; +using Kentico.Xperience.CRM.Common.Classes; +using Kentico.Xperience.CRM.Common.Constants; +using Kentico.Xperience.CRM.SalesForce.Admin; +using IFormItemCollectionProvider = Kentico.Xperience.Admin.Base.Forms.Internal.IFormItemCollectionProvider; + +[assembly: UIPage( + parentType: typeof(SalesForceIntegrationSettingApplication), + slug: "edit", + uiPageType: typeof(SalesForceIntegrationSettingsEdit), + name: "Edit settings", + templateName: TemplateNames.EDIT, + order: UIPageOrder.First)] + +namespace Kentico.Xperience.CRM.SalesForce.Admin; + +internal class SalesForceIntegrationSettingsEdit : CRMIntegrationSettingsEdit +{ + public SalesForceIntegrationSettingsEdit(IFormItemCollectionProvider formItemCollectionProvider, + IFormDataBinder formDataBinder, ICRMIntegrationSettingsInfoProvider crmIntegrationSettingsInfoProvider) : base( + formItemCollectionProvider, formDataBinder, crmIntegrationSettingsInfoProvider) + { + } + + protected override string CRMName => CRMType.SalesForce; +} \ No newline at end of file diff --git a/src/Kentico.Xperience.CRM.SalesForce/SalesForceServiceCollectionsExtensions.cs b/src/Kentico.Xperience.CRM.SalesForce/SalesForceServiceCollectionsExtensions.cs index feb516a..e429c3b 100644 --- a/src/Kentico.Xperience.CRM.SalesForce/SalesForceServiceCollectionsExtensions.cs +++ b/src/Kentico.Xperience.CRM.SalesForce/SalesForceServiceCollectionsExtensions.cs @@ -1,8 +1,8 @@ using CMS.Core; -using CMS.Helpers; using Duende.AccessTokenManagement; using Kentico.Xperience.CRM.Common; using Kentico.Xperience.CRM.Common.Constants; +using Kentico.Xperience.CRM.Common.Services; using Kentico.Xperience.CRM.SalesForce.Configuration; using Kentico.Xperience.CRM.SalesForce.Services; using Microsoft.Extensions.Configuration; @@ -37,7 +37,7 @@ public static IServiceCollection AddSalesForceFormLeadsIntegration(this IService if (configuration is null) { serviceCollection.AddOptions() - .Configure(ConfigureWithCMSSettings); + .Configure(ConfigureWithCMSSettings); } else { @@ -90,17 +90,15 @@ private static void AddSalesForceCommonIntegration(IServiceCollection serviceCol .AddClientCredentialsTokenHandler("salesforce.api.client"); } - private static void ConfigureWithCMSSettings(SalesForceIntegrationSettings settings, - ISettingsService settingsService) + private static void ConfigureWithCMSSettings(SalesForceIntegrationSettings settings, ICRMSettingsService settingsService) { - settings.FormLeadsEnabled = - ValidationHelper.GetBoolean(settingsService[SettingKeys.SalesForceFormLeadsEnabled], false); + var settingsInfo = settingsService.GetSettings(CRMType.SalesForce); + settings.FormLeadsEnabled = settingsInfo?.CRMIntegrationSettingsFormsEnabled ?? false; - settings.IgnoreExistingRecords = - ValidationHelper.GetBoolean(settingsService[SettingKeys.SalesForceIgnoreExistingRecords], false); + settings.IgnoreExistingRecords = settingsInfo?.CRMIntegrationSettingsIgnoreExistingRecords ?? false; - settings.ApiConfig.SalesForceUrl = settingsService[SettingKeys.SalesForceUrl]; - settings.ApiConfig.ClientId = settingsService[SettingKeys.SalesForceClientId]; - settings.ApiConfig.ClientSecret = settingsService[SettingKeys.SalesForceClientSecret]; + settings.ApiConfig.SalesForceUrl = settingsInfo?.CRMIntegrationSettingsUrl; + settings.ApiConfig.ClientId = settingsInfo?.CRMIntegrationSettingsClientId; + settings.ApiConfig.ClientSecret = settingsInfo?.CRMIntegrationSettingsClientSecret; } } \ No newline at end of file diff --git a/src/Kentico.Xperience.CRM.SalesForce/Workers/FailedItemsWorker.cs b/src/Kentico.Xperience.CRM.SalesForce/Workers/FailedItemsWorker.cs index a48c482..e0ba2ca 100644 --- a/src/Kentico.Xperience.CRM.SalesForce/Workers/FailedItemsWorker.cs +++ b/src/Kentico.Xperience.CRM.SalesForce/Workers/FailedItemsWorker.cs @@ -8,7 +8,7 @@ namespace Kentico.Xperience.CRM.SalesForce.Workers; /// /// Specific thread worker for SalesForce which try to synchronize failed items. It run each 1 minute. /// -public class FailedItemsWorker : FailedSyncItemsWorkerBase { protected override string CRMName => CRMType.SalesForce; From ac54f2eb4cb7825b69e4b528181e1a93088f1356 Mon Sep 17 00:00:00 2001 From: Martin Foukal Date: Tue, 23 Jan 2024 00:59:40 +0100 Subject: [PATCH 18/23] readme more simple and usage guide doc filled --- Kentico.Xperience.CRM.sln | 5 + README.md | 120 ++--------- docs/Usage-Guide.md | 197 ++++++++++++++++++- images/screenshots/Dynamics_CRM_settings.png | Bin 0 -> 111524 bytes 4 files changed, 221 insertions(+), 101 deletions(-) create mode 100644 images/screenshots/Dynamics_CRM_settings.png diff --git a/Kentico.Xperience.CRM.sln b/Kentico.Xperience.CRM.sln index 71b9e99..11cb6d9 100644 --- a/Kentico.Xperience.CRM.sln +++ b/Kentico.Xperience.CRM.sln @@ -37,6 +37,11 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "examples", "examples", "{62 EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "DancingGoat", "examples\DancingGoat\DancingGoat.csproj", "{A512E2C5-03C4-4707-800F-4BC6AA4C875C}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "docs", "docs", "{DDCE1F24-20E6-4AF4-8D37-E90B2F6C6A2C}" + ProjectSection(SolutionItems) = preProject + docs\Usage-Guide.md = docs\Usage-Guide.md + EndProjectSection +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU diff --git a/README.md b/README.md index 829ae7e..a811c11 100644 --- a/README.md +++ b/README.md @@ -11,8 +11,8 @@ Xperience by Kentico integrations with Microsoft Dynamics and Salesforce Sales C The versions of this library are supported by the following versions of Xperience by Kentico | Xperience Version | Library Version | -| ----------------- | --------------- | -| >= 27.0.1 | 1.x | +|-------------------|-----------------| +| >= 28.0.0 | 0.9 | ### Dependencies @@ -37,6 +37,10 @@ Add the package to your application using the .NET CLI dotnet add package Kentico.Xperience.CRM.SalesForce ``` +## Screenshots + +![Dynamics settings](/images/screenshots/Dynamics_CRM_settings.png "Dynamics CRM settings") + ## Quick Start 1. Fill CRM (Dynamics/SalesForce) settings (in CMS or appsettings.json) @@ -48,52 +52,7 @@ dotnet add package Kentico.Xperience.CRM.SalesForce There are 2 options how to fill settings: - use CMS settings: CRM integration settings category is created after first run. This is primary option when you don't specify IConfiguration section during services registration. -- use application settings: appsettings.json (API config is recommended to have in [User Secrets](https://learn.microsoft.com/en-us/aspnet/core/security/app-secrets?view=aspnetcore-6.0&tabs=windows)) - -Integration uses OAuth client credentials scheme, so you have to setup your CRM environment to enable for using API with -client id and client secret: -- [Dynamics](https://learn.microsoft.com/en-us/power-apps/developer/data-platform/authenticate-oauth) -- [SalesForce](https://help.salesforce.com/s/articleView?id=sf.remoteaccess_oauth_client_credentials_flow.htm&type=5) - -#### Dynamics settings -Fill settings in CMS or use this appsettings: -```json -{ - "CMSDynamicsCRMIntegration": { - "FormLeadsEnabled": true, - "IgnoreExistingRecords": false, - "ApiConfig": { - "DynamicsUrl": "", - "ClientId": "", - "ClientSecret": "" - } - } -} -``` - -#### SalesForce settings -Fill settings in CMS or use this app settings: -```json -{ - "CMSSalesForceCRMIntegration": { - "FormLeadsEnabled": true, - "IgnoreExistingRecords": false, - "ApiConfig": { - "SalesForceUrl": "", - "ClientId": "", - "ClientSecret": "" - } - } -} -``` - -You can also set specific API version for SalesForce REST API (default version is 59). - -```json -{ - "CMSSalesForceCRMIntegration:ApiConfig:ApiVersion": 59 -} -``` +- use application settings: [appsettings.json](./docs/Usage-Guide.md#crm-settings) (API config is recommended to have in [User Secrets](https://learn.microsoft.com/en-us/aspnet/core/security/app-secrets?view=aspnetcore-6.0&tabs=windows)) ### Forms data - Leads integration @@ -111,32 +70,6 @@ Added form with auto mapping based on Form field mapping to Contacts atttibutes. builder.AddFormWithContactMapping(DancingGoatContactUsItem.CLASS_NAME)); ``` -Same example but with using app setting in code (**CMS setting are ignored!**): - -```csharp - // Program.cs - - var builder = WebApplication.CreateBuilder(args); - - // ... - builder.Services.AddDynamicsFormLeadsIntegration(builder => - builder.AddFormWithContactMapping(DancingGoatContactUsItem.CLASS_NAME), - builder.Configuration.GetSection(DynamicsIntegrationSettings.ConfigKeyName)); -``` - -Example how to add form with auto mapping combined with custom mapping and custom validation: -```csharp - // Program.cs - - var builder = WebApplication.CreateBuilder(args); - - // ... - builder.Services.AddDynamicsFormLeadsIntegration(builder => - builder.AddFormWithContactMapping(DancingGoatContactUsItem.CLASS_NAME, b => b - .MapField(c => c.UserMessage, e => e.EMailAddress1)) - .AddCustomValidation()); -``` - Example how to add form with own mapping: ```csharp // Program.cs @@ -180,32 +113,6 @@ Added form with auto mapping based on Form field mapping to Contacts atttibutes. builder.AddFormWithContactMapping(DancingGoatContactUsItem.CLASS_NAME)); ``` -Same example but with using app setting in code (**CMS setting are ignored!**): - -```csharp - // Program.cs - - var builder = WebApplication.CreateBuilder(args); - - // ... - builder.Services.AddSalesForceFormLeadsIntegration(builder => - builder.AddFormWithContactMapping(DancingGoatContactUsItem.CLASS_NAME), - builder.Configuration.GetSection(SalesForceIntegrationSettings.ConfigKeyName)); -``` - -Example how to add form with auto mapping combined with custom mapping and custom validation: -```csharp - // Program.cs - - var builder = WebApplication.CreateBuilder(args); - - // ... - builder.Services.AddSalesForceFormLeadsIntegration(builder => - builder.AddFormWithContactMapping(DancingGoatContactUsItem.CLASS_NAME, b => b - .MapField(c => c.UserMessage, e => e.Description)) - .AddCustomValidation()); -``` - Example how to add form with own mapping: ```csharp // Program.cs @@ -236,6 +143,19 @@ Use this option when you need complex logic and need to use another service via builder.AddFormWithConverter(DancingGoatContactUsItem.CLASS_NAME)); ``` +## Full Instructions + +View the [Usage Guide](./docs/Usage-Guide.md) for more detailed instructions. + +## Projects + +| Project | Description | +|--------------------------------------|------------------------------------------------------------------------------------------| +| src/Kentico.Xperience.CRM.Dynamics | Xperience by Kentico Dynamics Sales CRM integration library | +| src/Kentico.Xperience.CRM.SalesForce | Xperience by Kentico SalesForce CRM integration library | +| src/Kentico.Xperience.CRM.Common | Xperience by Kentico common integration functionality (used by Dynamics/SalesForce libs) | +| examples/DancingGoat | Example project to showcase CRM integration | + ## Contributing To see the guidelines for Contributing to Kentico open source software, please see [Kentico's `CONTRIBUTING.md`](https://github.com/Kentico/.github/blob/main/CONTRIBUTING.md) for more information and follow the [Kentico's `CODE_OF_CONDUCT`](https://github.com/Kentico/.github/blob/main/CODE_OF_CONDUCT.md). diff --git a/docs/Usage-Guide.md b/docs/Usage-Guide.md index b487aae..54d5b68 100644 --- a/docs/Usage-Guide.md +++ b/docs/Usage-Guide.md @@ -1,3 +1,198 @@ # Usage Guide ----Organize the Usage Guide into sections using Markdown Headings. This guide should mirror the steps in the Quick Start but with far more details and include any optional steps.--- +## Screenshots + +![Dynamics settings](../images/screenshots/Dynamics_CRM_settings.png "Dynamics CRM settings") + +## CRM settings + +There are 2 options how to fill settings: +- use CMS settings: CRM integration settings category is created after first run. + This is primary option when you don't specify IConfiguration section during services registration. +- use application settings: appsettings.json (API config is recommended to have in [User Secrets](https://learn.microsoft.com/en-us/aspnet/core/security/app-secrets?view=aspnetcore-6.0&tabs=windows)) + +Integration uses OAuth client credentials scheme, so you have to setup your CRM environment to enable for using API with +client id and client secret: +- [Dynamics](https://learn.microsoft.com/en-us/power-apps/developer/data-platform/authenticate-oauth) +- [SalesForce](https://help.salesforce.com/s/articleView?id=sf.remoteaccess_oauth_client_credentials_flow.htm&type=5) + +### Dynamics settings +Fill settings in CMS or use this appsettings: +```json +{ + "CMSDynamicsCRMIntegration": { + "FormLeadsEnabled": true, + "IgnoreExistingRecords": false, + "ApiConfig": { + "DynamicsUrl": "", + "ClientId": "", + "ClientSecret": "" + } + } +} +``` + +### SalesForce settings +Fill settings in CMS or use this app settings: +```json +{ + "CMSSalesForceCRMIntegration": { + "FormLeadsEnabled": true, + "IgnoreExistingRecords": false, + "ApiConfig": { + "SalesForceUrl": "", + "ClientId": "", + "ClientSecret": "" + } + } +} +``` + +You can also set specific API version for SalesForce REST API (default version is 59). + +```json +{ + "CMSSalesForceCRMIntegration:ApiConfig:ApiVersion": 59 +} +``` + +## Forms data - Leads integration + +Configure mapping for each form between Kentico Form fields and Dynamics Lead entity fields: + +### Dynamics Sales +Added form with auto mapping based on Form field mapping to Contacts atttibutes. Uses CMS settings: +```csharp + // Program.cs + + var builder = WebApplication.CreateBuilder(args); + + // ... + builder.Services.AddDynamicsFormLeadsIntegration(builder => + builder.AddFormWithContactMapping(DancingGoatContactUsItem.CLASS_NAME)); +``` + +Same example but with using app setting in code (**CMS setting are ignored!**): + +```csharp + // Program.cs + + var builder = WebApplication.CreateBuilder(args); + + // ... + builder.Services.AddDynamicsFormLeadsIntegration(builder => + builder.AddFormWithContactMapping(DancingGoatContactUsItem.CLASS_NAME), + builder.Configuration.GetSection(DynamicsIntegrationSettings.ConfigKeyName)); +``` + +Example how to add form with auto mapping combined with custom mapping and custom validation: +```csharp + // Program.cs + + var builder = WebApplication.CreateBuilder(args); + + // ... + builder.Services.AddDynamicsFormLeadsIntegration(builder => + builder.AddFormWithContactMapping(DancingGoatContactUsItem.CLASS_NAME, b => b + .MapField(c => c.UserMessage, e => e.EMailAddress1)) + .AddCustomValidation()); +``` + +Example how to add form with own mapping: +```csharp + // Program.cs + + var builder = WebApplication.CreateBuilder(args); + + // ... + builder.Services.AddDynamicsFormLeadsIntegration(builder => + builder.AddForm(DancingGoatContactUsItem.CLASS_NAME, //form class name + c => c + .MapField("UserFirstName", "firstname") + .MapField("UserLastName", e => e.LastName) //you can map to Lead object or use own generated Lead class + .MapField(c => c.UserEmail, e => e.EMailAddress1) //generated form class used + .MapField(b => b.GetStringValue("UserMessage", ""), e => e.Description) //general BizFormItem used + )); +``` + +Example how to add form with custom converter. +Use this option when you need complex logic and need to use another service via DI: + +```csharp + // Program.cs + + var builder = WebApplication.CreateBuilder(args); + + // ... + builder.Services.AddDynamicsFormLeadsIntegration(builder => + builder.AddFormWithConverter(DancingGoatContactUsItem.CLASS_NAME)); +``` + +### SalesForce + +Added form with auto mapping based on Form field mapping to Contacts atttibutes. Uses CMS settings: +```csharp + // Program.cs + + var builder = WebApplication.CreateBuilder(args); + + // ... + builder.Services.AddSalesForceFormLeadsIntegration(builder => + builder.AddFormWithContactMapping(DancingGoatContactUsItem.CLASS_NAME)); +``` + +Same example but with using app setting in code (**CMS setting are ignored!**): + +```csharp + // Program.cs + + var builder = WebApplication.CreateBuilder(args); + + // ... + builder.Services.AddSalesForceFormLeadsIntegration(builder => + builder.AddFormWithContactMapping(DancingGoatContactUsItem.CLASS_NAME), + builder.Configuration.GetSection(SalesForceIntegrationSettings.ConfigKeyName)); +``` + +Example how to add form with auto mapping combined with custom mapping and custom validation: +```csharp + // Program.cs + + var builder = WebApplication.CreateBuilder(args); + + // ... + builder.Services.AddSalesForceFormLeadsIntegration(builder => + builder.AddFormWithContactMapping(DancingGoatContactUsItem.CLASS_NAME, b => b + .MapField(c => c.UserMessage, e => e.Description)) + .AddCustomValidation()); +``` + +Example how to add form with own mapping: +```csharp + // Program.cs + + var builder = WebApplication.CreateBuilder(args); + + // ... + builder.Services.AddSalesForceFormLeadsIntegration(builder => + builder.AddForm(DancingGoatContactUsItem.CLASS_NAME, //form class name + c => c + .MapField("UserFirstName", "FirstName") //option1: mapping based on source and target field names + .MapField("UserLastName", e => e.LastName) //option 2: mapping source name string -> member expression to SObject + .MapField(c => c.UserEmail, e => e.Email) //option 3: source mapping function from generated BizForm object -> member expression to SObject + .MapField(b => b.GetStringValue("UserMessage", ""), e => e.Description) //option 4: source mapping function general BizFormItem -> member expression to SObject + )); +``` + +Example how to add form with custom converter. +Use this option when you need complex logic and need to use another service via DI: + +```csharp + // Program.cs + + var builder = WebApplication.CreateBuilder(args); + + // ... + builder.Services.AddSalesForceFormLeadsIntegration(builder => + builder.AddFormWithConverter(DancingGoatContactUsItem.CLASS_NAME)); +``` diff --git a/images/screenshots/Dynamics_CRM_settings.png b/images/screenshots/Dynamics_CRM_settings.png new file mode 100644 index 0000000000000000000000000000000000000000..5cd6a79acd490d3c6fe62377a3ba5f8030ee65d7 GIT binary patch literal 111524 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.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¥4c!1bk&6+yIFJ3*Drng6x{D6}aya5BCOx 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�=%b;wVB-p(h8xc&+8z3wxk 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 literal 0 HcmV?d00001 From e73f82568945fdda9edeba1767278c57cf8a4037 Mon Sep 17 00:00:00 2001 From: Martin Foukal Date: Tue, 23 Jan 2024 13:40:42 +0100 Subject: [PATCH 19/23] Sync item listing typo fixes, email column, client secret as password component --- .../Admin/CRMSyncItemListing.cs | 39 ++++++++++++++++--- .../Models/CRMIntegrationSettingsModel.cs | 13 ++++--- 2 files changed, 41 insertions(+), 11 deletions(-) diff --git a/src/Kentico.Xperience.CRM.Common/Admin/CRMSyncItemListing.cs b/src/Kentico.Xperience.CRM.Common/Admin/CRMSyncItemListing.cs index 0356ffa..d37dd44 100644 --- a/src/Kentico.Xperience.CRM.Common/Admin/CRMSyncItemListing.cs +++ b/src/Kentico.Xperience.CRM.Common/Admin/CRMSyncItemListing.cs @@ -1,5 +1,8 @@ using CMS.DataEngine; +using CMS.FormEngine; +using CMS.Helpers; using CMS.OnlineForms; +using CMS.OnlineForms.Internal; using Kentico.Xperience.Admin.Base; using Kentico.Xperience.Admin.DigitalMarketing.UIPages; using Kentico.Xperience.CRM.Common.Admin; @@ -7,7 +10,7 @@ [assembly: UIPage(parentType: typeof(FormEditSection), slug: "crm-sync-listing", uiPageType: typeof(CRMSyncItemListing), - name: "CRM synchronization", templateName: TemplateNames.LISTING, order: 1000, icon: Icons.IntegrationScheme)] + name: "CRM sync", templateName: TemplateNames.LISTING, order: 1000, icon: Icons.IntegrationScheme)] namespace Kentico.Xperience.CRM.Common.Admin; @@ -16,6 +19,7 @@ namespace Kentico.Xperience.CRM.Common.Admin; ///
internal class CRMSyncItemListing : ListingPage { + private readonly IContactFieldFromFormRetriever contactFieldFromFormRetriever; private BizFormInfo? editedForm; private DataClassInfo? dataClassInfo; protected override string ObjectType => CRMSyncItemInfo.OBJECT_TYPE; @@ -23,20 +27,45 @@ internal class CRMSyncItemListing : ListingPage /// ID of the edited form. [PageParameter(typeof(IntPageModelBinder), typeof(FormEditSection))] public int FormId { get; set; } - + private BizFormInfo EditedForm => this.editedForm ??= AbstractInfo.Provider.Get(FormId); private DataClassInfo DataClassInfo => this.dataClassInfo ??= DataClassInfoProviderBase.GetDataClassInfo(EditedForm.FormClassID); + public CRMSyncItemListing(IContactFieldFromFormRetriever contactFieldFromFormRetriever) + { + this.contactFieldFromFormRetriever = contactFieldFromFormRetriever; + } + public override Task ConfigurePage() { + var primaryColumnName = new BizFormItem(DataClassInfo.ClassName).TypeInfo.IDColumn; + PageConfiguration.ColumnConfigurations - .AddColumn(nameof(CRMSyncItemInfo.CRMSyncItemEntityID), "Form item ID") - .AddColumn(nameof(CRMSyncItemInfo.CRMSyncItemEntityCRM), "CRM") + .AddColumn(nameof(CRMSyncItemInfo.CRMSyncItemEntityID), formatter: (value, _) => + { + var bizFormItem = + BizFormItemProvider.GetItem(ValidationHelper.GetInteger(value, 0), DataClassInfo.ClassName); + // first try to get email by mapping to Contact email + var email = contactFieldFromFormRetriever.RetrieveContactEmail(bizFormItem); + if (email is not null) + { + return email; + } + + //secondary try to find email field from field with 'Email' in his name + var emailField = new FormInfo(DataClassInfo.ClassFormDefinition).ItemsList.OfType() + .FirstOrDefault(fi => + fi.Name.Contains("email", StringComparison.InvariantCultureIgnoreCase) && fi.DataType == "text") + ?.Name; + return bizFormItem.GetStringValue(emailField, string.Empty); + }, caption: "Email") + .AddColumn(nameof(CRMSyncItemInfo.CRMSyncItemEntityID), "Form item ID", maxWidth: 20) + .AddColumn(nameof(CRMSyncItemInfo.CRMSyncItemEntityCRM), "CRM", maxWidth: 20) .AddColumn(nameof(CRMSyncItemInfo.CRMSyncItemCRMID), "CRM ID") - .AddColumn(nameof(CRMSyncItemInfo.CRMSyncItemLastModified), "Last sync time"); + .AddColumn(nameof(CRMSyncItemInfo.CRMSyncItemLastModified), "Last sync"); PageConfiguration.QueryModifiers.AddModifier(q => q.WhereEquals(nameof(CRMSyncItemInfo.CRMSyncItemEntityClass), DataClassInfo.ClassName) diff --git a/src/Kentico.Xperience.CRM.Common/Models/CRMIntegrationSettingsModel.cs b/src/Kentico.Xperience.CRM.Common/Models/CRMIntegrationSettingsModel.cs index 5b74209..5333cef 100644 --- a/src/Kentico.Xperience.CRM.Common/Models/CRMIntegrationSettingsModel.cs +++ b/src/Kentico.Xperience.CRM.Common/Models/CRMIntegrationSettingsModel.cs @@ -9,23 +9,24 @@ public class CRMIntegrationSettingsModel { [CheckBoxComponent(Label = "Forms enabled", Order = 1)] public bool FormsEnabled { get; set; } - + [CheckBoxComponent(Label = "Contacts enabled", Order = 2)] public bool ContactsEnabled { get; set; } - + [CheckBoxComponent(Label = "Ignore existing records", Order = 3)] public bool IgnoreExistingRecords { get; set; } - + [UrlValidationRule] [TextInputComponent(Label = "CRM URL", Order = 4)] [RequiredValidationRule] public string? Url { get; set; } - + [RequiredValidationRule] [TextInputComponent(Label = "Client ID", Order = 5)] public string? ClientId { get; set; } - + [RequiredValidationRule] - [TextInputComponent(Label = "Client secret", Order = 6)] + [PasswordComponent(Label = "Client secret", Order = 6, RequiredLength = 0, RequireDigit = false, + RequireLowercase = false, RequireUppercase = false, RequiredUniqueChars = 0, RequireNonAlphanumeric = false)] public string? ClientSecret { get; set; } } \ No newline at end of file From d14e158beaca040258c8d84070989c5541015eb3 Mon Sep 17 00:00:00 2001 From: Martin Foukal Date: Tue, 23 Jan 2024 14:24:12 +0100 Subject: [PATCH 20/23] cln and settings desc --- docs/Usage-Guide.md | 11 +++++++++++ .../Installers/CRMModuleInstaller.cs | 5 ++--- .../Admin/DynamicsIntegrationSettingsApplication.cs | 6 +++--- .../DynamicsServiceCollectionExtensions.cs | 4 +--- .../Admin/SalesForceIntegrationSettingApplication.cs | 6 +++--- .../SalesForceServiceCollectionsExtensions.cs | 3 +-- 6 files changed, 21 insertions(+), 14 deletions(-) diff --git a/docs/Usage-Guide.md b/docs/Usage-Guide.md index 54d5b68..0efcfb9 100644 --- a/docs/Usage-Guide.md +++ b/docs/Usage-Guide.md @@ -16,6 +16,17 @@ client id and client secret: - [Dynamics](https://learn.microsoft.com/en-us/power-apps/developer/data-platform/authenticate-oauth) - [SalesForce](https://help.salesforce.com/s/articleView?id=sf.remoteaccess_oauth_client_credentials_flow.htm&type=5) +### CRM settings description + +| Setting | Description | +|-------------------------|--------------------------------------------------------------------------------------| +| Forms enabled | If enabled form submissions for registered forms are sent to CRM Leads | +| Contacts enabled (TBD) | If enabled online marketing contacts are synced to CRM Leads or Contacts | +| Ignore existing records | If enabled then no updates in CRM will be performed on records with same ID or email | +| CRM URL | Base Dynamics / SalesForce instance URL | +| Client ID | Client ID for OAuth 2.0 client credentials scheme | +| Client secret | Client secret for OAuth 2.0 client credentials scheme | + ### Dynamics settings Fill settings in CMS or use this appsettings: ```json diff --git a/src/Kentico.Xperience.CRM.Common/Installers/CRMModuleInstaller.cs b/src/Kentico.Xperience.CRM.Common/Installers/CRMModuleInstaller.cs index 10d89bf..304f1cb 100644 --- a/src/Kentico.Xperience.CRM.Common/Installers/CRMModuleInstaller.cs +++ b/src/Kentico.Xperience.CRM.Common/Installers/CRMModuleInstaller.cs @@ -1,5 +1,4 @@ -using CMS.Base; -using CMS.DataEngine; +using CMS.DataEngine; using CMS.FormEngine; using CMS.Modules; using Kentico.Xperience.CRM.Common.Classes; @@ -131,7 +130,7 @@ private void InstallSyncedItemClass(ResourceInfo resourceInfo) formInfo.AddFormItem(formItem); failedSyncItemClass.ClassFormDefinition = formInfo.GetXmlDefinition(); - + DataClassInfoProvider.SetDataClassInfo(failedSyncItemClass); } diff --git a/src/Kentico.Xperience.CRM.Dynamics/Admin/DynamicsIntegrationSettingsApplication.cs b/src/Kentico.Xperience.CRM.Dynamics/Admin/DynamicsIntegrationSettingsApplication.cs index afd5cbe..a6ae429 100644 --- a/src/Kentico.Xperience.CRM.Dynamics/Admin/DynamicsIntegrationSettingsApplication.cs +++ b/src/Kentico.Xperience.CRM.Dynamics/Admin/DynamicsIntegrationSettingsApplication.cs @@ -3,12 +3,12 @@ using Kentico.Xperience.CRM.Dynamics.Admin; [assembly: UIApplication( - identifier: DynamicsIntegrationSettingsApplication.IDENTIFIER, + identifier: DynamicsIntegrationSettingsApplication.IDENTIFIER, type: typeof(DynamicsIntegrationSettingsApplication), slug: "dynamics-settings", - name: "Dynamics CRM Integration Settings", + name: "Dynamics CRM Integration Settings", category: BaseApplicationCategories.CONFIGURATION, - icon: Icons.IntegrationScheme, + icon: Icons.IntegrationScheme, templateName: TemplateNames.SECTION_LAYOUT)] namespace Kentico.Xperience.CRM.Dynamics.Admin; diff --git a/src/Kentico.Xperience.CRM.Dynamics/DynamicsServiceCollectionExtensions.cs b/src/Kentico.Xperience.CRM.Dynamics/DynamicsServiceCollectionExtensions.cs index 5cbceed..e4f2546 100644 --- a/src/Kentico.Xperience.CRM.Dynamics/DynamicsServiceCollectionExtensions.cs +++ b/src/Kentico.Xperience.CRM.Dynamics/DynamicsServiceCollectionExtensions.cs @@ -1,6 +1,4 @@ -using CMS.Core; -using CMS.Helpers; -using Kentico.Xperience.CRM.Common; +using Kentico.Xperience.CRM.Common; using Kentico.Xperience.CRM.Common.Constants; using Kentico.Xperience.CRM.Common.Services; using Kentico.Xperience.CRM.Dynamics.Configuration; diff --git a/src/Kentico.Xperience.CRM.SalesForce/Admin/SalesForceIntegrationSettingApplication.cs b/src/Kentico.Xperience.CRM.SalesForce/Admin/SalesForceIntegrationSettingApplication.cs index 6333ccb..db8cd31 100644 --- a/src/Kentico.Xperience.CRM.SalesForce/Admin/SalesForceIntegrationSettingApplication.cs +++ b/src/Kentico.Xperience.CRM.SalesForce/Admin/SalesForceIntegrationSettingApplication.cs @@ -3,12 +3,12 @@ using Kentico.Xperience.CRM.SalesForce.Admin; [assembly: UIApplication( - identifier: SalesForceIntegrationSettingApplication.IDENTIFIER, + identifier: SalesForceIntegrationSettingApplication.IDENTIFIER, type: typeof(SalesForceIntegrationSettingApplication), slug: "salesforce-settings", - name: "SalesForce CRM Integration Settings", + name: "SalesForce CRM Integration Settings", category: BaseApplicationCategories.CONFIGURATION, - icon: Icons.IntegrationScheme, + icon: Icons.IntegrationScheme, templateName: TemplateNames.SECTION_LAYOUT)] namespace Kentico.Xperience.CRM.SalesForce.Admin; diff --git a/src/Kentico.Xperience.CRM.SalesForce/SalesForceServiceCollectionsExtensions.cs b/src/Kentico.Xperience.CRM.SalesForce/SalesForceServiceCollectionsExtensions.cs index e429c3b..4a8afe5 100644 --- a/src/Kentico.Xperience.CRM.SalesForce/SalesForceServiceCollectionsExtensions.cs +++ b/src/Kentico.Xperience.CRM.SalesForce/SalesForceServiceCollectionsExtensions.cs @@ -1,5 +1,4 @@ -using CMS.Core; -using Duende.AccessTokenManagement; +using Duende.AccessTokenManagement; using Kentico.Xperience.CRM.Common; using Kentico.Xperience.CRM.Common.Constants; using Kentico.Xperience.CRM.Common.Services; From 06f698928d52f0c46071f44ea06947efc00dce9e Mon Sep 17 00:00:00 2001 From: Martin Foukal Date: Tue, 23 Jan 2024 16:09:14 +0100 Subject: [PATCH 21/23] CRM setting main application, added permissions --- .../CRMIntegrationSettingsApplication.cs | 26 +++++++++++++++++++ .../DynamicsIntegrationSettingsApplication.cs | 19 -------------- .../Admin/DynamicsIntegrationSettingsEdit.cs | 6 ++--- ...SalesForceIntegrationSettingApplication.cs | 19 -------------- .../SalesForceIntegrationSettingsEdit.cs | 8 +++--- 5 files changed, 33 insertions(+), 45 deletions(-) create mode 100644 src/Kentico.Xperience.CRM.Common/Admin/CRMIntegrationSettingsApplication.cs delete mode 100644 src/Kentico.Xperience.CRM.Dynamics/Admin/DynamicsIntegrationSettingsApplication.cs delete mode 100644 src/Kentico.Xperience.CRM.SalesForce/Admin/SalesForceIntegrationSettingApplication.cs diff --git a/src/Kentico.Xperience.CRM.Common/Admin/CRMIntegrationSettingsApplication.cs b/src/Kentico.Xperience.CRM.Common/Admin/CRMIntegrationSettingsApplication.cs new file mode 100644 index 0000000..f81b91a --- /dev/null +++ b/src/Kentico.Xperience.CRM.Common/Admin/CRMIntegrationSettingsApplication.cs @@ -0,0 +1,26 @@ +using CMS.Membership; +using Kentico.Xperience.Admin.Base; +using Kentico.Xperience.Admin.Base.UIPages; +using Kentico.Xperience.CRM.Common.Admin; + +[assembly: UIApplication( + identifier: CRMIntegrationSettingsApplication.IDENTIFIER, + type: typeof(CRMIntegrationSettingsApplication), + slug: "crm-integration", + name: "CRM integration", + category: BaseApplicationCategories.CONFIGURATION, + icon: Icons.IntegrationScheme, + templateName: TemplateNames.SECTION_LAYOUT)] + +namespace Kentico.Xperience.CRM.Common.Admin; + + +/// +/// Application entry point to CRM integration settings +/// +[UIPermission(SystemPermissions.VIEW)] +[UIPermission(SystemPermissions.UPDATE)] +public class CRMIntegrationSettingsApplication : ApplicationPage +{ + public const string IDENTIFIER = "Kentico.Xperience.CRM.Common.IntegrationSettings"; +} \ No newline at end of file diff --git a/src/Kentico.Xperience.CRM.Dynamics/Admin/DynamicsIntegrationSettingsApplication.cs b/src/Kentico.Xperience.CRM.Dynamics/Admin/DynamicsIntegrationSettingsApplication.cs deleted file mode 100644 index a6ae429..0000000 --- a/src/Kentico.Xperience.CRM.Dynamics/Admin/DynamicsIntegrationSettingsApplication.cs +++ /dev/null @@ -1,19 +0,0 @@ -using Kentico.Xperience.Admin.Base; -using Kentico.Xperience.Admin.Base.UIPages; -using Kentico.Xperience.CRM.Dynamics.Admin; - -[assembly: UIApplication( - identifier: DynamicsIntegrationSettingsApplication.IDENTIFIER, - type: typeof(DynamicsIntegrationSettingsApplication), - slug: "dynamics-settings", - name: "Dynamics CRM Integration Settings", - category: BaseApplicationCategories.CONFIGURATION, - icon: Icons.IntegrationScheme, - templateName: TemplateNames.SECTION_LAYOUT)] - -namespace Kentico.Xperience.CRM.Dynamics.Admin; - -internal class DynamicsIntegrationSettingsApplication : ApplicationPage -{ - public const string IDENTIFIER = "Kentico.Xperience.CRM.Dynamics.IntegrationSettings"; -} \ No newline at end of file diff --git a/src/Kentico.Xperience.CRM.Dynamics/Admin/DynamicsIntegrationSettingsEdit.cs b/src/Kentico.Xperience.CRM.Dynamics/Admin/DynamicsIntegrationSettingsEdit.cs index e4df3e3..07ef487 100644 --- a/src/Kentico.Xperience.CRM.Dynamics/Admin/DynamicsIntegrationSettingsEdit.cs +++ b/src/Kentico.Xperience.CRM.Dynamics/Admin/DynamicsIntegrationSettingsEdit.cs @@ -7,10 +7,10 @@ using IFormItemCollectionProvider = Kentico.Xperience.Admin.Base.Forms.Internal.IFormItemCollectionProvider; [assembly: UIPage( - parentType: typeof(DynamicsIntegrationSettingsApplication), - slug: "edit", + parentType: typeof(CRMIntegrationSettingsApplication), + slug: "dynamics-settings-edit", uiPageType: typeof(DynamicsIntegrationSettingsEdit), - name: "Edit settings", + name: "Dynamics CRM", templateName: TemplateNames.EDIT, order: UIPageOrder.First)] diff --git a/src/Kentico.Xperience.CRM.SalesForce/Admin/SalesForceIntegrationSettingApplication.cs b/src/Kentico.Xperience.CRM.SalesForce/Admin/SalesForceIntegrationSettingApplication.cs deleted file mode 100644 index db8cd31..0000000 --- a/src/Kentico.Xperience.CRM.SalesForce/Admin/SalesForceIntegrationSettingApplication.cs +++ /dev/null @@ -1,19 +0,0 @@ -using Kentico.Xperience.Admin.Base; -using Kentico.Xperience.Admin.Base.UIPages; -using Kentico.Xperience.CRM.SalesForce.Admin; - -[assembly: UIApplication( - identifier: SalesForceIntegrationSettingApplication.IDENTIFIER, - type: typeof(SalesForceIntegrationSettingApplication), - slug: "salesforce-settings", - name: "SalesForce CRM Integration Settings", - category: BaseApplicationCategories.CONFIGURATION, - icon: Icons.IntegrationScheme, - templateName: TemplateNames.SECTION_LAYOUT)] - -namespace Kentico.Xperience.CRM.SalesForce.Admin; - -internal class SalesForceIntegrationSettingApplication : ApplicationPage -{ - public const string IDENTIFIER = "Kentico.Xperience.CRM.SalesForce.IntegrationSettings"; -} \ No newline at end of file diff --git a/src/Kentico.Xperience.CRM.SalesForce/Admin/SalesForceIntegrationSettingsEdit.cs b/src/Kentico.Xperience.CRM.SalesForce/Admin/SalesForceIntegrationSettingsEdit.cs index d97388b..484fc67 100644 --- a/src/Kentico.Xperience.CRM.SalesForce/Admin/SalesForceIntegrationSettingsEdit.cs +++ b/src/Kentico.Xperience.CRM.SalesForce/Admin/SalesForceIntegrationSettingsEdit.cs @@ -7,12 +7,12 @@ using IFormItemCollectionProvider = Kentico.Xperience.Admin.Base.Forms.Internal.IFormItemCollectionProvider; [assembly: UIPage( - parentType: typeof(SalesForceIntegrationSettingApplication), - slug: "edit", + parentType: typeof(CRMIntegrationSettingsApplication), + slug: "salesforce-settings-edit", uiPageType: typeof(SalesForceIntegrationSettingsEdit), - name: "Edit settings", + name: "Salesforce CRM", templateName: TemplateNames.EDIT, - order: UIPageOrder.First)] + order: 200)] namespace Kentico.Xperience.CRM.SalesForce.Admin; From 6efc8f71706bd2a0858bed1902ab6c27ad1fb707 Mon Sep 17 00:00:00 2001 From: "Sean G. Wright" Date: Wed, 24 Jan 2024 01:00:41 -0500 Subject: [PATCH 22/23] refactor(sln): VSCode, NuGet, formatting, extension names, linting --- .editorconfig | 1 + .vscode/extensions.json | 1 - .vscode/launch.json | 12 +-- .vscode/settings.json | 5 +- .vscode/tasks.json | 97 +++++++++---------- Directory.Build.props | 9 +- Directory.Packages.props | 19 ++-- README.md | 30 +++--- docs/Usage-Guide.md | 37 ++++--- examples/DancingGoat/Program.cs | 8 +- examples/DancingGoat/packages.lock.json | 64 ++++++------ global.json | 2 +- .../Admin/CRMIntegrationSettingsEdit.cs | 4 +- .../Admin/CRMSyncItemListing.cs | 2 - .../Classes/CRMSyncItemInfo.generated.cs | 1 - .../Classes/FailedsyncitemInfo.generated.cs | 1 - .../BizFormFieldsMappingBuilder.cs | 2 +- .../CommonIntegrationSettings.cs | 1 - .../Constants/SettingKeys.cs | 19 ---- .../Installers/CRMModuleInstaller.cs | 25 ++--- .../Kentico.Xperience.CRM.Common.csproj | 4 - .../ServiceCollectionExtensions.cs | 2 - .../Workers/FailedSyncItemsWorkerBase.cs | 4 +- .../packages.lock.json | 26 +---- .../DynamicsBizFormsMappingBuilder.cs | 1 - .../DynamicsBizFormsMappingConfiguration.cs | 2 + .../FormContactMappingToLeadConverter.cs | 51 +++++++--- .../DynamicsIntegrationGlobalEvents.cs | 24 +++-- .../DynamicsServiceCollectionExtensions.cs | 2 +- ....Xperience.Dynamics.CRM.Integration.csproj | 20 ---- .../DynamicsLeadsIntegrationService.cs | 26 ++--- .../packages.lock.json | 50 ++++++---- src/Kentico.Xperience.CRM.Libs.sln | 34 +++++++ .../SalesForceBizFormsMappingBuilder.cs | 1 - .../SalesForceBizFormsMappingConfiguration.cs | 2 + .../FormContactMappingToLeadConverter.cs | 49 +++++++--- .../SalesForceIntegrationGlobalEvents.cs | 24 +++-- .../SalesForceServiceCollectionsExtensions.cs | 4 +- .../Services/ISalesForceApiService.cs | 1 + .../SalesForceLeadsIntegrationService.cs | 10 +- .../packages.lock.json | 34 +++---- .../packages.lock.json | 58 ++++------- 42 files changed, 405 insertions(+), 364 deletions(-) delete mode 100644 src/Kentico.Xperience.CRM.Common/Constants/SettingKeys.cs delete mode 100644 src/Kentico.Xperience.CRM.Dynamics/Kentico.Xperience.Dynamics.CRM.Integration.csproj create mode 100644 src/Kentico.Xperience.CRM.Libs.sln diff --git a/.editorconfig b/.editorconfig index 2a2cf3b..6ebd5c5 100644 --- a/.editorconfig +++ b/.editorconfig @@ -230,4 +230,5 @@ dotnet_naming_style.begins_with_i.capitalization = pascal_case # Exclude generated files from style rules [**/Dataverse/**/*.cs] +generated_code = true dotnet_diagnostic.CS8981.severity = none \ No newline at end of file diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 02f36ce..2fd528c 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -1,7 +1,6 @@ { "recommendations": [ "ms-dotnettools.csdevkit", - "k--kato.docomment", "editorconfig.editorconfig", "davidanson.vscode-markdownlint", "tintoy.msbuild-project-tools", diff --git a/.vscode/launch.json b/.vscode/launch.json index 5465b7f..c794d92 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -1,24 +1,22 @@ { - // Use IntelliSense to learn about possible attributes. - // Hover to view descriptions of existing attributes. - // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 "version": "0.2.0", "configurations": [ { "name": ".NET Core Launch (web)", "type": "coreclr", "request": "launch", - "preLaunchTask": "build", - "program": "${workspaceFolder}/src/Kentico.Xperience.SalesforceSalesCloud.Sample/bin/Debug/net6.0/DancingGoat.dll", + "preLaunchTask": ".NET: build (Solution)", + "program": "${workspaceFolder}/examples/DancingGoat/bin/Debug/net6.0/DancingGoat.dll", "args": [], - "cwd": "${workspaceFolder}/src/Kentico.Xperience.SalesforceSalesCloud.Sample", + "cwd": "${workspaceFolder}/examples/DancingGoat", "stopAtEntry": false, "serverReadyAction": { "action": "openExternally", "pattern": "\\bNow listening on:\\s+(https?://\\S+)" }, "env": { - "ASPNETCORE_ENVIRONMENT": "Development" + "ASPNETCORE_ENVIRONMENT": "Development", + "DOTNET_WATCH_RESTART_ON_RUDE_EDIT": "true" }, "sourceFileMap": { "/Views": "${workspaceFolder}/Views" diff --git a/.vscode/settings.json b/.vscode/settings.json index 566df7c..84cebfb 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -30,11 +30,8 @@ "editor.defaultFormatter": "ms-dotnettools.csharp" }, - "dotnet.defaultSolution": "Kentico.Xperience.SalesforceSalesCloud.sln", + "dotnet.defaultSolution": "Kentico.Xperience.CRM.sln", - "eslint.workingDirectories": [ - "./src/Kentico.Xperience.SalesforceSalesCloud/Admin/Client" - ], "[aspnetcorerazor]": { "editor.defaultFormatter": "ms-dotnettools.csharp" }, diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 65a3451..9625147 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -2,74 +2,69 @@ "version": "2.0.0", "tasks": [ { - "type": "npm", - "script": "install", - "path": "src/Kentico.Xperience.SalesforceSalesCloud/Admin/Client", - "group": "clean", - "problemMatcher": [], - "label": "npm: install - src/Kentico.Xperience.SalesforceSalesCloud/Admin/Client", - "detail": "install dependencies from package" - }, - { - "type": "dotnet", - "task": "build", - "problemMatcher": ["$msCompile"], - "group": "build", - "label": "dotnet: build" - }, - { - "type": "shell", + "label": ".NET: build (Solution)", "command": "dotnet", - "args": ["format"], - "problemMatcher": ["$msCompile"], - "group": "none", - "options": { - "cwd": "${workspaceFolder}/src/Kentico.Xperience.SalesforceSalesCloud/" - }, - "label": "dotnet: format" + "type": "process", + "args": [ + "build", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary" + ], + "problemMatcher": "$msCompile" }, { - "type": "dotnet", - "task": "clean", - "problemMatcher": ["$msCompile"], - "group": "clean", - "label": "dotnet: clean" + "label": ".NET: rebuild (Solution)", + "command": "dotnet", + "type": "process", + "args": [ + "build", + "--no-incremental", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary" + ], + "problemMatcher": "$msCompile" }, { - "type": "npm", - "script": "build", - "path": "src/Kentico.Xperience.SalesforceSalesCloud/Admin/Client", - "group": "build", - "problemMatcher": [], - "label": "npm: build - src/Kentico.Xperience.SalesforceSalesCloud/Admin/Client", - "detail": "webpack --mode=production" + "label": ".NET: clean (Solution)", + "command": "dotnet", + "type": "process", + "args": [ + "clean", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary" + ], + "problemMatcher": "$msCompile" }, { - "type": "npm", - "script": "build:dev", - "path": "src/Kentico.Xperience.SalesforceSalesCloud/Admin/Client", - "group": "build", - "problemMatcher": [], - "label": "npm: build:dev - src/Kentico.Xperience.SalesforceSalesCloud/Admin/Client", - "detail": "webpack --mode=development" + "label": ".NET: test (Solution)", + "command": "dotnet", + "type": "process", + "args": [ + "test", + "/property:GenerateFullPaths=true", + "/consoleloggerparameters:NoSummary" + ], + "problemMatcher": "$msCompile" }, { - "type": "npm", - "script": "start", - "path": "src/Kentico.Xperience.SalesforceSalesCloud/Admin/Client", - "problemMatcher": [], - "label": "npm: start - src/Kentico.Xperience.SalesforceSalesCloud/Admin/Client", - "detail": "webpack serve --mode development" + "label": ".NET: format (src)", + "command": "dotnet", + "type": "process", + "args": ["format", "Kentico.Xperience.CRM.Libs.sln"], + "problemMatcher": "$msCompile", + "options": { + "cwd": "${workspaceFolder}/src/" + } }, { - "label": "dotnet: watch DancingGoat", + "label": ".NET: watch (DancingGoat)", "command": "dotnet", "type": "process", "args": [ "watch", "run", "--project", - "${workspaceFolder}/src/Kentico.Xperience.SalesforceSalesCloud.Sample/DancingGoat.csproj" + "${workspaceFolder}/examples/DancingGoat/DancingGoat.csproj" ], "options": { "env": { diff --git a/Directory.Build.props b/Directory.Build.props index 03dedfb..4ef8598 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -5,7 +5,8 @@ $(Company) Copyright © $(Company) $([System.DateTime]::Now.Year) $(Company)™ - 1.0.0-prerelease-1 + 1.0.0 + prerelease-1 MIT https://github.com/Kentico/xperience-by-kentico-crm @@ -17,7 +18,7 @@ - + @@ -28,15 +29,13 @@ true true true - $(NoWarn);1591 + $(NoWarn);1591;S101;S1121 false true - true - true true snupkg diff --git a/Directory.Packages.props b/Directory.Packages.props index 1e42625..c38cd02 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -10,17 +10,16 @@ - - - - - + + + + - + - - - - + + + +
\ No newline at end of file diff --git a/README.md b/README.md index a811c11..b446820 100644 --- a/README.md +++ b/README.md @@ -11,8 +11,8 @@ Xperience by Kentico integrations with Microsoft Dynamics and Salesforce Sales C The versions of this library are supported by the following versions of Xperience by Kentico | Xperience Version | Library Version | -|-------------------|-----------------| -| >= 28.0.0 | 0.9 | +| ----------------- | --------------- | +| >= 28.0.0 | 1.0.0 | ### Dependencies @@ -50,34 +50,38 @@ dotnet add package Kentico.Xperience.CRM.SalesForce ### CRM settings There are 2 options how to fill settings: -- use CMS settings: CRM integration settings category is created after first run. -This is primary option when you don't specify IConfiguration section during services registration. -- use application settings: [appsettings.json](./docs/Usage-Guide.md#crm-settings) (API config is recommended to have in [User Secrets](https://learn.microsoft.com/en-us/aspnet/core/security/app-secrets?view=aspnetcore-6.0&tabs=windows)) + +- Use CMS settings: CRM integration settings category is created after first run. + This is primary option when you don't specify IConfiguration section during services registration. +- Use application settings: [appsettings.json](./docs/Usage-Guide.md#crm-settings) (API config is recommended to have in [User Secrets](https://learn.microsoft.com/en-us/aspnet/core/security/app-secrets?view=aspnetcore-6.0&tabs=windows)) ### Forms data - Leads integration Configure mapping for each form between Kentico Form fields and Dynamics Lead entity fields: #### Dynamics Sales + Added form with auto mapping based on Form field mapping to Contacts atttibutes. Uses CMS settings: + ```csharp // Program.cs var builder = WebApplication.CreateBuilder(args); // ... - builder.Services.AddDynamicsFormLeadsIntegration(builder => + builder.Services.AddKenticoCRMDynamics(builder => builder.AddFormWithContactMapping(DancingGoatContactUsItem.CLASS_NAME)); ``` Example how to add form with own mapping: + ```csharp // Program.cs var builder = WebApplication.CreateBuilder(args); // ... - builder.Services.AddDynamicsFormLeadsIntegration(builder => + builder.Services.AddKenticoCRMDynamics(builder => builder.AddForm(DancingGoatContactUsItem.CLASS_NAME, //form class name c => c .MapField("UserFirstName", "firstname") @@ -96,31 +100,33 @@ Use this option when you need complex logic and need to use another service via var builder = WebApplication.CreateBuilder(args); // ... - builder.Services.AddDynamicsFormLeadsIntegration(builder => + builder.Services.AddKenticoCRMDynamics(builder => builder.AddFormWithConverter(DancingGoatContactUsItem.CLASS_NAME)); ``` #### SalesForce Added form with auto mapping based on Form field mapping to Contacts atttibutes. Uses CMS settings: + ```csharp // Program.cs var builder = WebApplication.CreateBuilder(args); // ... - builder.Services.AddSalesForceFormLeadsIntegration(builder => + builder.Services.AddKenticoCRMSalesForce(builder => builder.AddFormWithContactMapping(DancingGoatContactUsItem.CLASS_NAME)); ``` Example how to add form with own mapping: + ```csharp // Program.cs var builder = WebApplication.CreateBuilder(args); // ... - builder.Services.AddSalesForceFormLeadsIntegration(builder => + builder.Services.AddKenticoCRMSalesForce(builder => builder.AddForm(DancingGoatContactUsItem.CLASS_NAME, //form class name c => c .MapField("UserFirstName", "FirstName") //option1: mapping based on source and target field names @@ -139,7 +145,7 @@ Use this option when you need complex logic and need to use another service via var builder = WebApplication.CreateBuilder(args); // ... - builder.Services.AddSalesForceFormLeadsIntegration(builder => + builder.Services.AddKenticoCRMSalesForce(builder => builder.AddFormWithConverter(DancingGoatContactUsItem.CLASS_NAME)); ``` @@ -150,7 +156,7 @@ View the [Usage Guide](./docs/Usage-Guide.md) for more detailed instructions. ## Projects | Project | Description | -|--------------------------------------|------------------------------------------------------------------------------------------| +| ------------------------------------ | ---------------------------------------------------------------------------------------- | | src/Kentico.Xperience.CRM.Dynamics | Xperience by Kentico Dynamics Sales CRM integration library | | src/Kentico.Xperience.CRM.SalesForce | Xperience by Kentico SalesForce CRM integration library | | src/Kentico.Xperience.CRM.Common | Xperience by Kentico common integration functionality (used by Dynamics/SalesForce libs) | diff --git a/docs/Usage-Guide.md b/docs/Usage-Guide.md index 0efcfb9..58db3b0 100644 --- a/docs/Usage-Guide.md +++ b/docs/Usage-Guide.md @@ -7,19 +7,21 @@ ## CRM settings There are 2 options how to fill settings: + - use CMS settings: CRM integration settings category is created after first run. This is primary option when you don't specify IConfiguration section during services registration. - use application settings: appsettings.json (API config is recommended to have in [User Secrets](https://learn.microsoft.com/en-us/aspnet/core/security/app-secrets?view=aspnetcore-6.0&tabs=windows)) Integration uses OAuth client credentials scheme, so you have to setup your CRM environment to enable for using API with client id and client secret: + - [Dynamics](https://learn.microsoft.com/en-us/power-apps/developer/data-platform/authenticate-oauth) - [SalesForce](https://help.salesforce.com/s/articleView?id=sf.remoteaccess_oauth_client_credentials_flow.htm&type=5) ### CRM settings description | Setting | Description | -|-------------------------|--------------------------------------------------------------------------------------| +| ----------------------- | ------------------------------------------------------------------------------------ | | Forms enabled | If enabled form submissions for registered forms are sent to CRM Leads | | Contacts enabled (TBD) | If enabled online marketing contacts are synced to CRM Leads or Contacts | | Ignore existing records | If enabled then no updates in CRM will be performed on records with same ID or email | @@ -28,7 +30,9 @@ client id and client secret: | Client secret | Client secret for OAuth 2.0 client credentials scheme | ### Dynamics settings + Fill settings in CMS or use this appsettings: + ```json { "CMSDynamicsCRMIntegration": { @@ -44,7 +48,9 @@ Fill settings in CMS or use this appsettings: ``` ### SalesForce settings + Fill settings in CMS or use this app settings: + ```json { "CMSSalesForceCRMIntegration": { @@ -72,14 +78,16 @@ You can also set specific API version for SalesForce REST API (default version i Configure mapping for each form between Kentico Form fields and Dynamics Lead entity fields: ### Dynamics Sales + Added form with auto mapping based on Form field mapping to Contacts atttibutes. Uses CMS settings: + ```csharp // Program.cs var builder = WebApplication.CreateBuilder(args); // ... - builder.Services.AddDynamicsFormLeadsIntegration(builder => + builder.Services.AddKenticoCRMDynamics(builder => builder.AddFormWithContactMapping(DancingGoatContactUsItem.CLASS_NAME)); ``` @@ -91,32 +99,34 @@ Same example but with using app setting in code (**CMS setting are ignored!**): var builder = WebApplication.CreateBuilder(args); // ... - builder.Services.AddDynamicsFormLeadsIntegration(builder => - builder.AddFormWithContactMapping(DancingGoatContactUsItem.CLASS_NAME), + builder.Services.AddKenticoCRMDynamics(builder => + builder.AddFormWithContactMapping(DancingGoatContactUsItem.CLASS_NAME), builder.Configuration.GetSection(DynamicsIntegrationSettings.ConfigKeyName)); ``` Example how to add form with auto mapping combined with custom mapping and custom validation: + ```csharp // Program.cs var builder = WebApplication.CreateBuilder(args); // ... - builder.Services.AddDynamicsFormLeadsIntegration(builder => + builder.Services.AddKenticoCRMDynamics(builder => builder.AddFormWithContactMapping(DancingGoatContactUsItem.CLASS_NAME, b => b .MapField(c => c.UserMessage, e => e.EMailAddress1)) .AddCustomValidation()); ``` Example how to add form with own mapping: + ```csharp // Program.cs var builder = WebApplication.CreateBuilder(args); // ... - builder.Services.AddDynamicsFormLeadsIntegration(builder => + builder.Services.AddKenticoCRMDynamics(builder => builder.AddForm(DancingGoatContactUsItem.CLASS_NAME, //form class name c => c .MapField("UserFirstName", "firstname") @@ -135,20 +145,21 @@ Use this option when you need complex logic and need to use another service via var builder = WebApplication.CreateBuilder(args); // ... - builder.Services.AddDynamicsFormLeadsIntegration(builder => + builder.Services.AddKenticoCRMDynamics(builder => builder.AddFormWithConverter(DancingGoatContactUsItem.CLASS_NAME)); ``` ### SalesForce Added form with auto mapping based on Form field mapping to Contacts atttibutes. Uses CMS settings: + ```csharp // Program.cs var builder = WebApplication.CreateBuilder(args); // ... - builder.Services.AddSalesForceFormLeadsIntegration(builder => + builder.Services.AddKenticoCRMSalesForce(builder => builder.AddFormWithContactMapping(DancingGoatContactUsItem.CLASS_NAME)); ``` @@ -160,32 +171,34 @@ Same example but with using app setting in code (**CMS setting are ignored!**): var builder = WebApplication.CreateBuilder(args); // ... - builder.Services.AddSalesForceFormLeadsIntegration(builder => + builder.Services.AddKenticoCRMSalesForce(builder => builder.AddFormWithContactMapping(DancingGoatContactUsItem.CLASS_NAME), builder.Configuration.GetSection(SalesForceIntegrationSettings.ConfigKeyName)); ``` Example how to add form with auto mapping combined with custom mapping and custom validation: + ```csharp // Program.cs var builder = WebApplication.CreateBuilder(args); // ... - builder.Services.AddSalesForceFormLeadsIntegration(builder => + builder.Services.AddKenticoCRMSalesForce(builder => builder.AddFormWithContactMapping(DancingGoatContactUsItem.CLASS_NAME, b => b .MapField(c => c.UserMessage, e => e.Description)) .AddCustomValidation()); ``` Example how to add form with own mapping: + ```csharp // Program.cs var builder = WebApplication.CreateBuilder(args); // ... - builder.Services.AddSalesForceFormLeadsIntegration(builder => + builder.Services.AddKenticoCRMSalesForce(builder => builder.AddForm(DancingGoatContactUsItem.CLASS_NAME, //form class name c => c .MapField("UserFirstName", "FirstName") //option1: mapping based on source and target field names @@ -204,6 +217,6 @@ Use this option when you need complex logic and need to use another service via var builder = WebApplication.CreateBuilder(args); // ... - builder.Services.AddSalesForceFormLeadsIntegration(builder => + builder.Services.AddKenticoCRMSalesForce(builder => builder.AddFormWithConverter(DancingGoatContactUsItem.CLASS_NAME)); ``` diff --git a/examples/DancingGoat/Program.cs b/examples/DancingGoat/Program.cs index 2b9a86d..ba82cd1 100644 --- a/examples/DancingGoat/Program.cs +++ b/examples/DancingGoat/Program.cs @@ -58,7 +58,7 @@ //CRM integration registration start -//builder.Services.AddDynamicsFormLeadsIntegration(builder => +//builder.Services.AddKenticoCRMDynamics(builder => // builder.AddForm(DancingGoatContactUsItem.CLASS_NAME, //form class name // c => c // .MapField("UserFirstName", "firstname") @@ -70,12 +70,12 @@ // , // builder.Configuration.GetSection(DynamicsIntegrationSettings.ConfigKeyName)); //config section with settings -builder.Services.AddDynamicsFormLeadsIntegration(builder => +builder.Services.AddKenticoCRMDynamics(builder => builder.AddFormWithContactMapping(DancingGoatContactUsItem.CLASS_NAME, b => b .MapField(c => c.UserMessage, e => e.Description)) .AddCustomValidation()); //optional -//builder.Services.AddSalesForceFormLeadsIntegration(builder => +//builder.Services.AddKenticoCRMSalesForce(builder => // builder.AddForm(DancingGoatContactUsItem.CLASS_NAME, //form class name // c => c // .MapField("UserFirstName", "FirstName") //option1: mapping based on source and target field names @@ -84,7 +84,7 @@ // .MapField(b => b.GetStringValue("UserMessage", ""), e => e.Description) //option 4: source mapping function general BizFormItem -> member expression to SObject // )); -builder.Services.AddSalesForceFormLeadsIntegration(builder => +builder.Services.AddKenticoCRMSalesForce(builder => builder.AddFormWithContactMapping(DancingGoatContactUsItem.CLASS_NAME, b => b .MapField(c => c.UserMessage, e => e.Description)) .AddCustomValidation()); diff --git a/examples/DancingGoat/packages.lock.json b/examples/DancingGoat/packages.lock.json index 8e9549b..ab8de3d 100644 --- a/examples/DancingGoat/packages.lock.json +++ b/examples/DancingGoat/packages.lock.json @@ -135,16 +135,16 @@ }, "Duende.AccessTokenManagement": { "type": "Transitive", - "resolved": "2.0.3", - "contentHash": "+Gf464caPFzPsprf/oyKHwVLJmiGk404vtjpGsmJVM9VpM4B8I9140ANDJwN2S5ihOcL3zH7mv59re0KT31TfA==", + "resolved": "2.1.0", + "contentHash": "SZXYF/pS/Tkc3fY7az/S3Y/uODVtKU7KbDyaZBOqNl9y9MSmga+FEAmgDfdjCq+f9wRy9QTkicGKHSSKcwT4HQ==", "dependencies": { - "IdentityModel": "6.1.0", + "IdentityModel": "6.2.0", "Microsoft.Extensions.Caching.Abstractions": "6.0.0", "Microsoft.Extensions.DependencyInjection.Abstractions": "6.0.0", "Microsoft.Extensions.Http": "6.0.0", "Microsoft.Extensions.Logging.Abstractions": "6.0.0", "Microsoft.Extensions.Options": "6.0.0", - "System.IdentityModel.Tokens.Jwt": "6.15.1" + "System.IdentityModel.Tokens.Jwt": "6.15.0" } }, "HtmlSanitizer": { @@ -408,18 +408,19 @@ }, "Microsoft.Identity.Client": { "type": "Transitive", - "resolved": "4.47.2", - "contentHash": "SPgesZRbXoDxg8Vv7k5Ou0ee7uupVw0E8ZCc4GKw25HANRLz1d5OSr0fvTVQRnEswo5Obk8qD4LOapYB+n5kzQ==", + "resolved": "4.56.0", + "contentHash": "rr4zbidvHy9r4NvOAs5hdd964Ao2A0pAeFBJKR95u1CJAVzbd1p6tPTXUZ+5ld0cfThiVSGvz6UHwY6JjraTpA==", "dependencies": { "Microsoft.IdentityModel.Abstractions": "6.22.0" } }, "Microsoft.Identity.Client.Extensions.Msal": { "type": "Transitive", - "resolved": "2.19.3", - "contentHash": "zVVZjn8aW7W79rC1crioDgdOwaFTQorsSO6RgVlDDjc7MvbEGz071wSNrjVhzR0CdQn6Sefx7Abf1o7vasmrLg==", + "resolved": "4.56.0", + "contentHash": "H12YAzEGK55vZ+QpxUzozhW8ZZtgPDuWvgA0JbdIR9UhMUplj29JhIgE2imuH8W2Nw9D8JKygR1uxRFtpSNcrg==", "dependencies": { - "Microsoft.Identity.Client": "4.38.0", + "Microsoft.Identity.Client": "4.56.0", + "System.IO.FileSystem.AccessControl": "5.0.0", "System.Security.Cryptography.ProtectedData": "4.5.0" } }, @@ -787,6 +788,15 @@ "System.Threading.Tasks": "4.3.0" } }, + "System.IO.FileSystem.AccessControl": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "SxHB3nuNrpptVk+vZ/F+7OHEpoHUIKKMl02bUmYHQr1r+glbZQxs7pRtsf4ENO29TVm2TH3AEeep2fJcy92oYw==", + "dependencies": { + "System.Security.AccessControl": "5.0.0", + "System.Security.Principal.Windows": "5.0.0" + } + }, "System.IO.FileSystem.Primitives": { "type": "Transitive", "resolved": "4.3.0", @@ -1239,19 +1249,19 @@ }, "System.Text.Encodings.Web": { "type": "Transitive", - "resolved": "6.0.0", - "contentHash": "Vg8eB5Tawm1IFqj4TVK1czJX89rhFxJo9ELqc/Eiq0eXy13RK00eubyU6TJE6y+GQXjyV5gSfiewDUZjQgSE0w==", + "resolved": "7.0.0", + "contentHash": "OP6umVGxc0Z0MvZQBVigj4/U31Pw72ITihDWP9WiWDm+q5aoe0GaJivsfYGq53o6dxH7DcXWiCTl7+0o2CGdmg==", "dependencies": { "System.Runtime.CompilerServices.Unsafe": "6.0.0" } }, "System.Text.Json": { "type": "Transitive", - "resolved": "6.0.7", - "contentHash": "/Tf/9XjprpHolbcDOrxsKVYy/mUG/FS7aGd9YUgBVEiHeQH4kAE0T1sMbde7q6B5xcrNUsJ5iW7D1RvHudQNqA==", + "resolved": "7.0.3", + "contentHash": "AyjhwXN1zTFeIibHimfJn6eAsZ7rTBib79JQpzg8WAuR/HKDu9JGNHTuu3nbbXQ/bgI+U4z6HtZmCHNXB1QXrQ==", "dependencies": { "System.Runtime.CompilerServices.Unsafe": "6.0.0", - "System.Text.Encodings.Web": "6.0.0" + "System.Text.Encodings.Web": "7.0.0" } }, "System.Text.RegularExpressions": { @@ -1386,7 +1396,7 @@ "kentico.xperience.crm.salesforce": { "type": "Project", "dependencies": { - "Duende.AccessTokenManagement.OpenIdConnect": "[2.0.3, )", + "Duende.AccessTokenManagement.OpenIdConnect": "[2.1.0, )", "IdentityModel": "[6.2.0, )", "Kentico.Xperience.CRM.Common": "[1.0.0-prerelease-1, )", "Kentico.Xperience.Core": "[28.0.0, )" @@ -1397,16 +1407,16 @@ "dependencies": { "Kentico.Xperience.CRM.Common": "[1.0.0-prerelease-1, )", "Kentico.Xperience.Core": "[28.0.0, )", - "Microsoft.PowerPlatform.Dataverse.Client": "[1.1.14, )" + "Microsoft.PowerPlatform.Dataverse.Client": "[1.1.17, )" } }, "Duende.AccessTokenManagement.OpenIdConnect": { "type": "CentralTransitive", - "requested": "[2.0.3, )", - "resolved": "2.0.3", - "contentHash": "lFbbzI95P3tIhuvJfXded66R369hUcsLXX00pSpsBP3yoTTqS28dulhkV2r7/EvZ+pagHLTFbzjzaN7Am/B9XQ==", + "requested": "[2.1.0, )", + "resolved": "2.1.0", + "contentHash": "Lgvnsg4WFbw2FUZEp0YMiTcamDoT/jyJMYARUAH3a8Xul9WiIRMZXpqhmJdJ3DzYdqgEs8oa3cyHP0YjLhqE3Q==", "dependencies": { - "Duende.AccessTokenManagement": "2.0.3", + "Duende.AccessTokenManagement": "2.1.0", "Microsoft.AspNetCore.Authentication.OpenIdConnect": "6.0.0" } }, @@ -1441,16 +1451,16 @@ }, "Microsoft.PowerPlatform.Dataverse.Client": { "type": "CentralTransitive", - "requested": "[1.1.14, )", - "resolved": "1.1.14", - "contentHash": "AoWyada9Y3lI88pmbCRWZt0rr5ET7uz3ntEQFfd2UxiBM9rvaijjHn/gOQpm0ERH5Ip53VxnlshtrtTFMs7RoA==", + "requested": "[1.1.17, )", + "resolved": "1.1.17", + "contentHash": "gSgD7N52EATY0PkNIbBtqZeG0FDGK11X1x8nBnPclfykw73bvqyfortjNIwAGcj0esMqk8DSieuRCVi47hg9qQ==", "dependencies": { "Microsoft.Extensions.Caching.Memory": "3.1.8", "Microsoft.Extensions.DependencyInjection": "3.1.8", "Microsoft.Extensions.Http": "3.1.8", "Microsoft.Extensions.Logging": "3.1.8", - "Microsoft.Identity.Client": "4.35.1", - "Microsoft.Identity.Client.Extensions.Msal": "2.18.9", + "Microsoft.Identity.Client": "4.56.0", + "Microsoft.Identity.Client.Extensions.Msal": "4.56.0", "Microsoft.Rest.ClientRuntime": "2.3.24", "Microsoft.VisualBasic": "10.3.0", "Newtonsoft.Json": "13.0.1", @@ -1468,12 +1478,10 @@ "System.Runtime.Serialization.Primitives": "4.3.0", "System.Runtime.Serialization.Xml": "4.3.0", "System.Security.Cryptography.Algorithms": "4.3.1", - "System.Security.Cryptography.Pkcs": "6.0.3", "System.Security.Cryptography.ProtectedData": "4.7.0", - "System.Security.Cryptography.Xml": "6.0.1", "System.Security.Permissions": "5.0.0", "System.ServiceModel.Http": "4.10.2", - "System.Text.Json": "6.0.7" + "System.Text.Json": "7.0.3" } } } diff --git a/global.json b/global.json index 112ac59..bc022a1 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@ { "sdk": { - "version": "7.0.306", + "version": "8.0.101", "rollForward": "latestMajor", "allowPrerelease": false } diff --git a/src/Kentico.Xperience.CRM.Common/Admin/CRMIntegrationSettingsEdit.cs b/src/Kentico.Xperience.CRM.Common/Admin/CRMIntegrationSettingsEdit.cs index 862b675..d1d08af 100644 --- a/src/Kentico.Xperience.CRM.Common/Admin/CRMIntegrationSettingsEdit.cs +++ b/src/Kentico.Xperience.CRM.Common/Admin/CRMIntegrationSettingsEdit.cs @@ -20,9 +20,9 @@ protected CRMIntegrationSettingsEdit(IFormItemCollectionProvider formItemCollect { this.crmIntegrationSettingsInfoProvider = crmIntegrationSettingsInfoProvider; } - + private CRMIntegrationSettingsInfo? settingsInfo; - + private CRMIntegrationSettingsInfo? SettingsInfo => settingsInfo ??= crmIntegrationSettingsInfoProvider.Get() .WhereEquals(nameof(CRMIntegrationSettingsInfo.CRMIntegrationSettingsCRMName), CRMName) .TopN(1) diff --git a/src/Kentico.Xperience.CRM.Common/Admin/CRMSyncItemListing.cs b/src/Kentico.Xperience.CRM.Common/Admin/CRMSyncItemListing.cs index d37dd44..6e8c8df 100644 --- a/src/Kentico.Xperience.CRM.Common/Admin/CRMSyncItemListing.cs +++ b/src/Kentico.Xperience.CRM.Common/Admin/CRMSyncItemListing.cs @@ -41,8 +41,6 @@ public CRMSyncItemListing(IContactFieldFromFormRetriever contactFieldFromFormRet public override Task ConfigurePage() { - var primaryColumnName = new BizFormItem(DataClassInfo.ClassName).TypeInfo.IDColumn; - PageConfiguration.ColumnConfigurations .AddColumn(nameof(CRMSyncItemInfo.CRMSyncItemEntityID), formatter: (value, _) => { diff --git a/src/Kentico.Xperience.CRM.Common/Classes/CRMSyncItemInfo.generated.cs b/src/Kentico.Xperience.CRM.Common/Classes/CRMSyncItemInfo.generated.cs index 7490c84..2ffe2c6 100644 --- a/src/Kentico.Xperience.CRM.Common/Classes/CRMSyncItemInfo.generated.cs +++ b/src/Kentico.Xperience.CRM.Common/Classes/CRMSyncItemInfo.generated.cs @@ -28,7 +28,6 @@ public partial class CRMSyncItemInfo : AbstractInfo public static readonly ObjectTypeInfo TYPEINFO = new ObjectTypeInfo(typeof(CRMSyncItemInfoProvider), OBJECT_TYPE, "KenticoCRMCommon.CRMSyncItem", "CRMSyncItemID", "CRMSyncItemLastModified", null, null, null, null, null, null) { - ModuleName = "Kentic.Xperience.CRM.Common", TouchCacheDependencies = true, }; diff --git a/src/Kentico.Xperience.CRM.Common/Classes/FailedsyncitemInfo.generated.cs b/src/Kentico.Xperience.CRM.Common/Classes/FailedsyncitemInfo.generated.cs index 2e60052..3d6155e 100644 --- a/src/Kentico.Xperience.CRM.Common/Classes/FailedsyncitemInfo.generated.cs +++ b/src/Kentico.Xperience.CRM.Common/Classes/FailedsyncitemInfo.generated.cs @@ -28,7 +28,6 @@ public partial class FailedSyncItemInfo : AbstractInfo public static readonly ObjectTypeInfo TYPEINFO = new ObjectTypeInfo(typeof(FailedSyncItemInfoProvider), OBJECT_TYPE, "KenticoCRMCommon.FailedSyncItem", "FailedSyncItemID", "FailedSyncItemLastModified", null, null, null, null, null, null) { - ModuleName = "Kentic.Xperience.CRM.Common", TouchCacheDependencies = true, }; diff --git a/src/Kentico.Xperience.CRM.Common/Configuration/BizFormFieldsMappingBuilder.cs b/src/Kentico.Xperience.CRM.Common/Configuration/BizFormFieldsMappingBuilder.cs index c765942..2cf0c44 100644 --- a/src/Kentico.Xperience.CRM.Common/Configuration/BizFormFieldsMappingBuilder.cs +++ b/src/Kentico.Xperience.CRM.Common/Configuration/BizFormFieldsMappingBuilder.cs @@ -28,6 +28,6 @@ public BizFormFieldsMappingBuilder AddMapping(BizFormFieldMapping mapping) fieldMappings.Add(mapping); return this; } - + public List Build() => fieldMappings; } \ No newline at end of file diff --git a/src/Kentico.Xperience.CRM.Common/Configuration/CommonIntegrationSettings.cs b/src/Kentico.Xperience.CRM.Common/Configuration/CommonIntegrationSettings.cs index 4848c7f..14811eb 100644 --- a/src/Kentico.Xperience.CRM.Common/Configuration/CommonIntegrationSettings.cs +++ b/src/Kentico.Xperience.CRM.Common/Configuration/CommonIntegrationSettings.cs @@ -11,7 +11,6 @@ /// public bool FormLeadsEnabled { get; set; } - // @TODO phase 2 public bool ContactsEnabled { get; set; } /// diff --git a/src/Kentico.Xperience.CRM.Common/Constants/SettingKeys.cs b/src/Kentico.Xperience.CRM.Common/Constants/SettingKeys.cs deleted file mode 100644 index aa7cd5d..0000000 --- a/src/Kentico.Xperience.CRM.Common/Constants/SettingKeys.cs +++ /dev/null @@ -1,19 +0,0 @@ -namespace Kentico.Xperience.CRM.Common.Constants; - -/// -/// CMS settings keys -/// -public class SettingKeys -{ - public const string DynamicsFormLeadsEnabled = "CMSDynamicsCRMIntegrationFormLeadsEnabled"; - public const string DynamicsUrl = "CMSDynamicsCRMIntegrationDynamicsUrl"; - public const string DynamicsClientId = "CMSDynamicsCRMIntegrationClientId"; - public const string DynamicsClientSecret = "CMSDynamicsCRMIntegrationClientSecret"; - public const string DynamicsIgnoreExistingRecords = "CMSDynamicsCRMIntegrationIgnoreExistingRecords"; - - public const string SalesForceFormLeadsEnabled = "CMSSalesforceCRMIntegrationFormLeadsEnabled"; - public const string SalesForceUrl = "CMSSalesforceCRMIntegrationSalesforceUrl"; - public const string SalesForceClientId = "CMSSalesforceCRMIntegrationClientId"; - public const string SalesForceClientSecret = "CMSSalesforceCRMIntegrationClientSecret"; - public const string SalesForceIgnoreExistingRecords = "CMSSalesforceCRMIntegrationIgnoreExistingRecords"; -} \ No newline at end of file diff --git a/src/Kentico.Xperience.CRM.Common/Installers/CRMModuleInstaller.cs b/src/Kentico.Xperience.CRM.Common/Installers/CRMModuleInstaller.cs index 304f1cb..8b11390 100644 --- a/src/Kentico.Xperience.CRM.Common/Installers/CRMModuleInstaller.cs +++ b/src/Kentico.Xperience.CRM.Common/Installers/CRMModuleInstaller.cs @@ -22,7 +22,7 @@ public CRMModuleInstaller(IResourceInfoProvider resourceInfoProvider) this.resourceInfoProvider = resourceInfoProvider; } - public void Install(string crmtype) + public void Install(string crmType) { var resourceInfo = InstallModule(); InstallModuleClasses(resourceInfo); @@ -115,7 +115,10 @@ private void InstallSyncedItemClass(ResourceInfo resourceInfo) formItem = new FormFieldInfo { - Name = nameof(CRMSyncItemInfo.CRMSyncItemCreatedByKentico), Visible = false, DataType = "boolean", Enabled = true + Name = nameof(CRMSyncItemInfo.CRMSyncItemCreatedByKentico), + Visible = false, + DataType = "boolean", + Enabled = true }; formInfo.AddFormItem(formItem); @@ -130,7 +133,7 @@ private void InstallSyncedItemClass(ResourceInfo resourceInfo) formInfo.AddFormItem(formItem); failedSyncItemClass.ClassFormDefinition = formInfo.GetXmlDefinition(); - + DataClassInfoProvider.SetDataClassInfo(failedSyncItemClass); } @@ -216,7 +219,7 @@ private void InstallFailedSyncItemClass(ResourceInfo resourceInfo) DataClassInfoProvider.SetDataClassInfo(failedSyncItemClass); } - + private void InstallCRMIntegrationSettingsClass(ResourceInfo resourceInfo) { var settingsCRM = DataClassInfoProvider.GetDataClassInfo(CRMIntegrationSettingsInfo.OBJECT_TYPE); @@ -235,7 +238,7 @@ private void InstallCRMIntegrationSettingsClass(ResourceInfo resourceInfo) var formInfo = FormHelper.GetBasicFormDefinition(nameof(CRMIntegrationSettingsInfo.CRMIntegrationSettingsItemID)); - + var formItem = new FormFieldInfo { Name = nameof(CRMIntegrationSettingsInfo.CRMIntegrationSettingsFormsEnabled), @@ -245,7 +248,7 @@ private void InstallCRMIntegrationSettingsClass(ResourceInfo resourceInfo) Enabled = true }; formInfo.AddFormItem(formItem); - + formItem = new FormFieldInfo { Name = nameof(CRMIntegrationSettingsInfo.CRMIntegrationSettingsContactsEnabled), @@ -255,7 +258,7 @@ private void InstallCRMIntegrationSettingsClass(ResourceInfo resourceInfo) Enabled = true }; formInfo.AddFormItem(formItem); - + formItem = new FormFieldInfo { Name = nameof(CRMIntegrationSettingsInfo.CRMIntegrationSettingsIgnoreExistingRecords), @@ -277,7 +280,7 @@ private void InstallCRMIntegrationSettingsClass(ResourceInfo resourceInfo) Enabled = true }; formInfo.AddFormItem(formItem); - + formItem = new FormFieldInfo { Name = nameof(CRMIntegrationSettingsInfo.CRMIntegrationSettingsClientId), @@ -289,7 +292,7 @@ private void InstallCRMIntegrationSettingsClass(ResourceInfo resourceInfo) Enabled = true }; formInfo.AddFormItem(formItem); - + formItem = new FormFieldInfo { Name = nameof(CRMIntegrationSettingsInfo.CRMIntegrationSettingsClientSecret), @@ -301,7 +304,7 @@ private void InstallCRMIntegrationSettingsClass(ResourceInfo resourceInfo) Enabled = true }; formInfo.AddFormItem(formItem); - + formItem = new FormFieldInfo { Name = nameof(CRMIntegrationSettingsInfo.CRMIntegrationSettingsCRMName), @@ -312,7 +315,7 @@ private void InstallCRMIntegrationSettingsClass(ResourceInfo resourceInfo) Enabled = true }; formInfo.AddFormItem(formItem); - + settingsCRM.ClassFormDefinition = formInfo.GetXmlDefinition(); diff --git a/src/Kentico.Xperience.CRM.Common/Kentico.Xperience.CRM.Common.csproj b/src/Kentico.Xperience.CRM.Common/Kentico.Xperience.CRM.Common.csproj index f3fc3d0..5700310 100644 --- a/src/Kentico.Xperience.CRM.Common/Kentico.Xperience.CRM.Common.csproj +++ b/src/Kentico.Xperience.CRM.Common/Kentico.Xperience.CRM.Common.csproj @@ -8,10 +8,6 @@ - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - diff --git a/src/Kentico.Xperience.CRM.Common/ServiceCollectionExtensions.cs b/src/Kentico.Xperience.CRM.Common/ServiceCollectionExtensions.cs index 05c6ed5..10e9995 100644 --- a/src/Kentico.Xperience.CRM.Common/ServiceCollectionExtensions.cs +++ b/src/Kentico.Xperience.CRM.Common/ServiceCollectionExtensions.cs @@ -15,8 +15,6 @@ public static class ServiceCollectionExtensions /// Adds common services for CRM integration. This method is usually used from specific CRM integration library /// /// - /// - /// /// public static IServiceCollection AddKenticoCrmCommonFormLeadsIntegration( this IServiceCollection services) diff --git a/src/Kentico.Xperience.CRM.Common/Workers/FailedSyncItemsWorkerBase.cs b/src/Kentico.Xperience.CRM.Common/Workers/FailedSyncItemsWorkerBase.cs index b700113..8a27718 100644 --- a/src/Kentico.Xperience.CRM.Common/Workers/FailedSyncItemsWorkerBase.cs +++ b/src/Kentico.Xperience.CRM.Common/Workers/FailedSyncItemsWorkerBase.cs @@ -40,7 +40,7 @@ protected override void Process() try { using var serviceScope = Service.Resolve().CreateScope(); - + var settings = serviceScope.ServiceProvider.GetRequiredService>().Value; if (!settings.FormLeadsEnabled) return; @@ -52,7 +52,7 @@ protected override void Process() { leadsIntegrationService ??= serviceScope.ServiceProvider .GetRequiredService(); - + var bizFormItem = failedSyncItemsService.GetBizFormItem(syncItem); if (bizFormItem is null) { diff --git a/src/Kentico.Xperience.CRM.Common/packages.lock.json b/src/Kentico.Xperience.CRM.Common/packages.lock.json index 207960e..81bbde0 100644 --- a/src/Kentico.Xperience.CRM.Common/packages.lock.json +++ b/src/Kentico.Xperience.CRM.Common/packages.lock.json @@ -37,21 +37,11 @@ "System.Configuration.ConfigurationManager": "6.0.1" } }, - "Microsoft.SourceLink.GitHub": { - "type": "Direct", - "requested": "[1.1.1, )", - "resolved": "1.1.1", - "contentHash": "IaJGnOv/M7UQjRJks7B6p7pbPnOwisYGOIzqCz5ilGFTApZ3ktOR+6zJ12ZRPInulBmdAf1SrGdDG2MU8g6XTw==", - "dependencies": { - "Microsoft.Build.Tasks.Git": "1.1.1", - "Microsoft.SourceLink.Common": "1.1.1" - } - }, "SonarAnalyzer.CSharp": { "type": "Direct", - "requested": "[9.6.0.74858, )", - "resolved": "9.6.0.74858", - "contentHash": "tBwiOBYcoSyHVUejjz6iH3VOpSFx7gJSuQ3695BQaoqQBcqy0IeRsKAk5FAR4fbm3QLyEM9bimKk1l038TFN2w==" + "requested": "[9.17.0.82934, )", + "resolved": "9.17.0.82934", + "contentHash": "vHMvduSho3HACmC4jIPamuJiWenJKOIxcnvHNZ1vOztvQ0VeIYAAOvvw79BKpzZOEDEpM9uUOJgCSPgBetSzSw==" }, "AngleSharp": { "type": "Transitive", @@ -151,11 +141,6 @@ "resolved": "1.1.1", "contentHash": "yuvf07qFWFqtK3P/MRkEKLhn5r2UbSpVueRziSqj0yJQIKFwG1pq9mOayK3zE5qZCTs0CbrwL9M6R8VwqyGy2w==" }, - "Microsoft.Build.Tasks.Git": { - "type": "Transitive", - "resolved": "1.1.1", - "contentHash": "AT3HlgTjsqHnWpBHSNeR0KxbLZD7bztlZVj7I8vgeYG9SYqbeFGh0TM/KVtC6fg53nrWHl3VfZFvb5BiQFcY6Q==" - }, "Microsoft.CSharp": { "type": "Transitive", "resolved": "4.5.0", @@ -438,11 +423,6 @@ "resolved": "1.1.0", "contentHash": "aOZA3BWfz9RXjpzt0sRJJMjAscAUm3Hoa4UWAfceV9UTYxgwZ1lZt5nO2myFf+/jetYQo4uTP7zS8sJY67BBxg==" }, - "Microsoft.SourceLink.Common": { - "type": "Transitive", - "resolved": "1.1.1", - "contentHash": "WMcGpWKrmJmzrNeuaEb23bEMnbtR/vLmvZtkAP5qWu7vQsY59GqfRJd65sFpBszbd2k/bQ8cs8eWawQKAabkVg==" - }, "Microsoft.SqlServer.Server": { "type": "Transitive", "resolved": "1.0.0", diff --git a/src/Kentico.Xperience.CRM.Dynamics/Configuration/DynamicsBizFormsMappingBuilder.cs b/src/Kentico.Xperience.CRM.Dynamics/Configuration/DynamicsBizFormsMappingBuilder.cs index d43deb6..7322bf0 100644 --- a/src/Kentico.Xperience.CRM.Dynamics/Configuration/DynamicsBizFormsMappingBuilder.cs +++ b/src/Kentico.Xperience.CRM.Dynamics/Configuration/DynamicsBizFormsMappingBuilder.cs @@ -96,7 +96,6 @@ public DynamicsBizFormsMappingBuilder AddFormWithConverter(string fo /// /// Adds custom service for BizForm item validation before sending to CRM /// - /// /// /// public DynamicsBizFormsMappingBuilder AddCustomValidation() diff --git a/src/Kentico.Xperience.CRM.Dynamics/Configuration/DynamicsBizFormsMappingConfiguration.cs b/src/Kentico.Xperience.CRM.Dynamics/Configuration/DynamicsBizFormsMappingConfiguration.cs index 1ac315c..b4a8861 100644 --- a/src/Kentico.Xperience.CRM.Dynamics/Configuration/DynamicsBizFormsMappingConfiguration.cs +++ b/src/Kentico.Xperience.CRM.Dynamics/Configuration/DynamicsBizFormsMappingConfiguration.cs @@ -5,6 +5,8 @@ namespace Kentico.Xperience.CRM.Dynamics.Configuration; /// /// Specific configuration for BizForm mapping to Lead in Dynamics Sales /// +#pragma warning disable S2094 // Classes should not be empty public class DynamicsBizFormsMappingConfiguration : BizFormsMappingConfiguration +#pragma warning restore S2094 // Classes should not be empty { } \ No newline at end of file diff --git a/src/Kentico.Xperience.CRM.Dynamics/Converters/FormContactMappingToLeadConverter.cs b/src/Kentico.Xperience.CRM.Dynamics/Converters/FormContactMappingToLeadConverter.cs index 349441d..024f559 100644 --- a/src/Kentico.Xperience.CRM.Dynamics/Converters/FormContactMappingToLeadConverter.cs +++ b/src/Kentico.Xperience.CRM.Dynamics/Converters/FormContactMappingToLeadConverter.cs @@ -1,4 +1,7 @@ using CMS.ContactManagement; +using CMS.Core; +using CMS.DataEngine; +using CMS.Globalization; using CMS.OnlineForms; using CMS.OnlineForms.Internal; using Kentico.Xperience.CRM.Common.Mapping; @@ -12,10 +15,20 @@ namespace Kentico.Xperience.CRM.Dynamics.Converters; public class FormContactMappingToLeadConverter : ICRMTypeConverter { private readonly IContactFieldFromFormRetriever contactFieldFromFormRetriever; + private readonly IInfoByIdProvider countries; + private readonly IInfoByIdProvider states; + private readonly IConversionService conversion; - public FormContactMappingToLeadConverter(IContactFieldFromFormRetriever contactFieldFromFormRetriever) + public FormContactMappingToLeadConverter( + IContactFieldFromFormRetriever contactFieldFromFormRetriever, + IInfoByIdProvider countries, + IInfoByIdProvider states, + IConversionService conversion) { this.contactFieldFromFormRetriever = contactFieldFromFormRetriever; + this.countries = countries; + this.states = states; + this.conversion = conversion; } public Task Convert(BizFormItem source, Lead destination) @@ -25,68 +38,80 @@ public Task Convert(BizFormItem source, Lead destination) { destination.FirstName = firstName; } - + var lastName = contactFieldFromFormRetriever.Retrieve(source, nameof(ContactInfo.ContactLastName)); if (!string.IsNullOrWhiteSpace(lastName)) { destination.LastName = lastName; } - + var email = contactFieldFromFormRetriever.Retrieve(source, nameof(ContactInfo.ContactEmail)); if (!string.IsNullOrWhiteSpace(email)) { destination.EMailAddress1 = email; } - + var companyName = contactFieldFromFormRetriever.Retrieve(source, nameof(ContactInfo.ContactCompanyName)); if (!string.IsNullOrWhiteSpace(companyName)) { destination.CompanyName = companyName; } - + var phone = contactFieldFromFormRetriever.Retrieve(source, nameof(ContactInfo.ContactMobilePhone)); if (!string.IsNullOrWhiteSpace(phone)) { destination.MobilePhone = phone; } - + var bizPhone = contactFieldFromFormRetriever.Retrieve(source, nameof(ContactInfo.ContactBusinessPhone)); if (!string.IsNullOrWhiteSpace(bizPhone)) { destination.Telephone1 = bizPhone; } - + var middleName = contactFieldFromFormRetriever.Retrieve(source, nameof(ContactInfo.ContactMiddleName)); if (!string.IsNullOrWhiteSpace(middleName)) { destination.MiddleName = middleName; } - + var jobTitle = contactFieldFromFormRetriever.Retrieve(source, nameof(ContactInfo.ContactJobTitle)); if (!string.IsNullOrWhiteSpace(jobTitle)) { destination.JobTitle = jobTitle; } - + var address1 = contactFieldFromFormRetriever.Retrieve(source, nameof(ContactInfo.ContactAddress1)); if (!string.IsNullOrWhiteSpace(address1)) { destination.Address1_Line1 = address1; } - + var city = contactFieldFromFormRetriever.Retrieve(source, nameof(ContactInfo.ContactCity)); if (!string.IsNullOrWhiteSpace(city)) { destination.Address1_City = city; } - + var zipCode = contactFieldFromFormRetriever.Retrieve(source, nameof(ContactInfo.ContactZIP)); if (!string.IsNullOrWhiteSpace(zipCode)) { destination.Address1_PostalCode = zipCode; } - - //@TODO country, state + + string countryIDVal = contactFieldFromFormRetriever.Retrieve(source, nameof(ContactInfo.ContactCountryID)); + if (!string.IsNullOrWhiteSpace(countryIDVal)) + { + var country = countries.Get(conversion.GetInteger(countryIDVal, 0)); + destination.Address1_Country = country?.CountryDisplayName; + } + + string stateIDVal = contactFieldFromFormRetriever.Retrieve(source, nameof(ContactInfo.ContactStateID)); + if (!string.IsNullOrWhiteSpace(stateIDVal)) + { + var state = states.Get(conversion.GetInteger(stateIDVal, 0)); + destination.Address1_StateOrProvince = state?.StateDisplayName; + } return Task.FromResult(destination); } diff --git a/src/Kentico.Xperience.CRM.Dynamics/DynamicsIntegrationGlobalEvents.cs b/src/Kentico.Xperience.CRM.Dynamics/DynamicsIntegrationGlobalEvents.cs index 4a4deb7..1aa3aa5 100644 --- a/src/Kentico.Xperience.CRM.Dynamics/DynamicsIntegrationGlobalEvents.cs +++ b/src/Kentico.Xperience.CRM.Dynamics/DynamicsIntegrationGlobalEvents.cs @@ -28,27 +28,33 @@ public DynamicsIntegrationGlobalEvents() : base(nameof(DynamicsIntegrationGlobal } private ILogger logger = null!; + private ICRMModuleInstaller? installer; protected override void OnInit(ModuleInitParameters parameters) { base.OnInit(parameters); - + var services = parameters.Services; - + logger = services.GetRequiredService>(); - services.GetRequiredService().Install(CRMType.Dynamics); - + installer = services.GetRequiredService(); + + ApplicationEvents.Initialized.Execute += InitializeModule; + BizFormItemEvents.Insert.After += SynchronizeBizFormLead; BizFormItemEvents.Update.After += SynchronizeBizFormLead; - - logger = Service.Resolve>(); - Service.Resolve().Install(CRMType.Dynamics); + RequestEvents.RunEndRequestTasks.Execute += (_, _) => { FailedItemsWorker.Current.EnsureRunningThread(); }; } - + + private void InitializeModule(object? sender, EventArgs e) + { + installer?.Install(CRMType.Dynamics); + } + private void SynchronizeBizFormLead(object? sender, BizFormItemEventArgs e) { var failedSyncItemsService = Service.Resolve(); @@ -58,7 +64,7 @@ private void SynchronizeBizFormLead(object? sender, BizFormItemEventArgs e) { var settings = serviceScope.ServiceProvider.GetRequiredService>().Value; if (!settings.FormLeadsEnabled) return; - + var leadsIntegrationService = serviceScope.ServiceProvider .GetRequiredService(); diff --git a/src/Kentico.Xperience.CRM.Dynamics/DynamicsServiceCollectionExtensions.cs b/src/Kentico.Xperience.CRM.Dynamics/DynamicsServiceCollectionExtensions.cs index e4f2546..db17453 100644 --- a/src/Kentico.Xperience.CRM.Dynamics/DynamicsServiceCollectionExtensions.cs +++ b/src/Kentico.Xperience.CRM.Dynamics/DynamicsServiceCollectionExtensions.cs @@ -21,7 +21,7 @@ public static class DynamicsServiceCollectionExtensions /// /// /// - public static IServiceCollection AddDynamicsFormLeadsIntegration(this IServiceCollection serviceCollection, + public static IServiceCollection AddKenticoCRMDynamics(this IServiceCollection serviceCollection, Action formsConfig, IConfiguration? configuration = null) { diff --git a/src/Kentico.Xperience.CRM.Dynamics/Kentico.Xperience.Dynamics.CRM.Integration.csproj b/src/Kentico.Xperience.CRM.Dynamics/Kentico.Xperience.Dynamics.CRM.Integration.csproj deleted file mode 100644 index 412cd5c..0000000 --- a/src/Kentico.Xperience.CRM.Dynamics/Kentico.Xperience.Dynamics.CRM.Integration.csproj +++ /dev/null @@ -1,20 +0,0 @@ - - - - net6.0 - enable - enable - Kentico.Xperience.Dynamics.CRM.Integration - Kentico.Xperience.CRM.Dynamics - - - - - - - - - - - - diff --git a/src/Kentico.Xperience.CRM.Dynamics/Services/DynamicsLeadsIntegrationService.cs b/src/Kentico.Xperience.CRM.Dynamics/Services/DynamicsLeadsIntegrationService.cs index a8639a8..5228fe7 100644 --- a/src/Kentico.Xperience.CRM.Dynamics/Services/DynamicsLeadsIntegrationService.cs +++ b/src/Kentico.Xperience.CRM.Dynamics/Services/DynamicsLeadsIntegrationService.cs @@ -29,7 +29,7 @@ internal class DynamicsLeadsIntegrationService : IDynamicsLeadsIntegrationServic private readonly IEnumerable> formsConverters; public DynamicsLeadsIntegrationService( - DynamicsBizFormsMappingConfiguration bizFormMappingConfig, + DynamicsBizFormsMappingConfiguration bizFormMappingConfig, ILeadsIntegrationValidationService validationService, ServiceClient serviceClient, ILogger logger, @@ -47,7 +47,7 @@ public DynamicsLeadsIntegrationService( this.settings = settings; this.formsConverters = formsConverters; } - + /// /// Validates BizForm item, then get specific mapping and finally specific implementation is called /// from inherited service @@ -57,7 +57,7 @@ public async Task SynchronizeLeadAsync(BizFormItem bizFormItem) { var leadConverters = Enumerable.Empty>(); var leadMapping = Enumerable.Empty(); - + if (bizFormMappingConfig.FormsConverters.TryGetValue(bizFormItem.BizFormClassName.ToLowerInvariant(), out var formConverters)) { @@ -89,7 +89,7 @@ private async Task SynchronizeLeadAsync(BizFormItem bizFormItem, try { var syncItem = await syncItemService.GetFormLeadSyncItem(bizFormItem, CRMType.Dynamics); - + if (syncItem is null) { await UpdateByEmailOrCreate(bizFormItem, fieldMappings, converters); @@ -134,8 +134,8 @@ private async Task UpdateByEmailOrCreate(BizFormItem bizFormItem, { Lead? existingLead = null; var tmpLead = new Lead(); - MapLead(bizFormItem, tmpLead, fieldMappings, converters); - + await MapLead(bizFormItem, tmpLead, fieldMappings, converters); + if (!string.IsNullOrWhiteSpace(tmpLead.EMailAddress1)) { existingLead = await GetLeadByEmail(tmpLead.EMailAddress1); @@ -160,7 +160,7 @@ private async Task CreateLeadAsync(BizFormItem bizFormItem, IEnumerable fieldMappings, IEnumerable> converters) { var leadEntity = new Lead(); - MapLead(bizFormItem, leadEntity, fieldMappings, converters); + await MapLead(bizFormItem, leadEntity, fieldMappings, converters); if (leadEntity.Subject is null) { @@ -168,16 +168,16 @@ private async Task CreateLeadAsync(BizFormItem bizFormItem, } var leadId = await serviceClient.CreateAsync(leadEntity); - + await syncItemService.LogFormLeadCreateItem(bizFormItem, leadId.ToString(), CRMType.Dynamics); failedSyncItemService.DeleteFailedSyncItem(CRMType.Dynamics, bizFormItem.BizFormClassName, bizFormItem.ItemID); } private async Task UpdateLeadAsync(Lead leadEntity, BizFormItem bizFormItem, - IEnumerable fieldMappings, IEnumerable> converters) + IEnumerable fieldMappings, IEnumerable> converters) { - MapLead(bizFormItem, leadEntity, fieldMappings, converters); + await MapLead(bizFormItem, leadEntity, fieldMappings, converters); if (leadEntity.Subject is null) { @@ -185,7 +185,7 @@ private async Task UpdateLeadAsync(Lead leadEntity, BizFormItem bizFormItem, } await serviceClient.UpdateAsync(leadEntity); - + await syncItemService.LogFormLeadUpdateItem(bizFormItem, leadEntity.LeadId.ToString()!, CRMType.Dynamics); failedSyncItemService.DeleteFailedSyncItem(CRMType.Dynamics, bizFormItem.BizFormClassName, bizFormItem.ItemID); @@ -198,7 +198,7 @@ protected async Task MapLead(BizFormItem bizFormItem, Lead leadEntity, { await converter.Convert(bizFormItem, leadEntity); } - + foreach (var fieldMapping in fieldMappings) { var formFieldValue = fieldMapping.FormFieldMapping.MapFormField(bizFormItem); @@ -206,7 +206,7 @@ protected async Task MapLead(BizFormItem bizFormItem, Lead leadEntity, _ = fieldMapping.CRMFieldMapping switch { CRMFieldNameMapping m => leadEntity[m.CrmFieldName] = formFieldValue, - _ => throw new ArgumentOutOfRangeException(nameof(fieldMapping.CRMFieldMapping), + _ => throw new ArgumentOutOfRangeException(nameof(fieldMappings), fieldMapping.CRMFieldMapping.GetType(), "Unsupported mapping") }; } diff --git a/src/Kentico.Xperience.CRM.Dynamics/packages.lock.json b/src/Kentico.Xperience.CRM.Dynamics/packages.lock.json index 7aea801..ecace36 100644 --- a/src/Kentico.Xperience.CRM.Dynamics/packages.lock.json +++ b/src/Kentico.Xperience.CRM.Dynamics/packages.lock.json @@ -27,16 +27,16 @@ }, "Microsoft.PowerPlatform.Dataverse.Client": { "type": "Direct", - "requested": "[1.1.14, )", - "resolved": "1.1.14", - "contentHash": "AoWyada9Y3lI88pmbCRWZt0rr5ET7uz3ntEQFfd2UxiBM9rvaijjHn/gOQpm0ERH5Ip53VxnlshtrtTFMs7RoA==", + "requested": "[1.1.17, )", + "resolved": "1.1.17", + "contentHash": "gSgD7N52EATY0PkNIbBtqZeG0FDGK11X1x8nBnPclfykw73bvqyfortjNIwAGcj0esMqk8DSieuRCVi47hg9qQ==", "dependencies": { "Microsoft.Extensions.Caching.Memory": "3.1.8", "Microsoft.Extensions.DependencyInjection": "3.1.8", "Microsoft.Extensions.Http": "3.1.8", "Microsoft.Extensions.Logging": "3.1.8", - "Microsoft.Identity.Client": "4.35.1", - "Microsoft.Identity.Client.Extensions.Msal": "2.18.9", + "Microsoft.Identity.Client": "4.56.0", + "Microsoft.Identity.Client.Extensions.Msal": "4.56.0", "Microsoft.Rest.ClientRuntime": "2.3.24", "Microsoft.VisualBasic": "10.3.0", "Newtonsoft.Json": "13.0.1", @@ -54,19 +54,17 @@ "System.Runtime.Serialization.Primitives": "4.3.0", "System.Runtime.Serialization.Xml": "4.3.0", "System.Security.Cryptography.Algorithms": "4.3.1", - "System.Security.Cryptography.Pkcs": "6.0.3", "System.Security.Cryptography.ProtectedData": "4.7.0", - "System.Security.Cryptography.Xml": "6.0.1", "System.Security.Permissions": "5.0.0", "System.ServiceModel.Http": "4.10.2", - "System.Text.Json": "6.0.7" + "System.Text.Json": "7.0.3" } }, "SonarAnalyzer.CSharp": { "type": "Direct", - "requested": "[9.6.0.74858, )", - "resolved": "9.6.0.74858", - "contentHash": "tBwiOBYcoSyHVUejjz6iH3VOpSFx7gJSuQ3695BQaoqQBcqy0IeRsKAk5FAR4fbm3QLyEM9bimKk1l038TFN2w==" + "requested": "[9.17.0.82934, )", + "resolved": "9.17.0.82934", + "contentHash": "vHMvduSho3HACmC4jIPamuJiWenJKOIxcnvHNZ1vOztvQ0VeIYAAOvvw79BKpzZOEDEpM9uUOJgCSPgBetSzSw==" }, "AngleSharp": { "type": "Transitive", @@ -376,18 +374,19 @@ }, "Microsoft.Identity.Client": { "type": "Transitive", - "resolved": "4.47.2", - "contentHash": "SPgesZRbXoDxg8Vv7k5Ou0ee7uupVw0E8ZCc4GKw25HANRLz1d5OSr0fvTVQRnEswo5Obk8qD4LOapYB+n5kzQ==", + "resolved": "4.56.0", + "contentHash": "rr4zbidvHy9r4NvOAs5hdd964Ao2A0pAeFBJKR95u1CJAVzbd1p6tPTXUZ+5ld0cfThiVSGvz6UHwY6JjraTpA==", "dependencies": { "Microsoft.IdentityModel.Abstractions": "6.22.0" } }, "Microsoft.Identity.Client.Extensions.Msal": { "type": "Transitive", - "resolved": "2.19.3", - "contentHash": "zVVZjn8aW7W79rC1crioDgdOwaFTQorsSO6RgVlDDjc7MvbEGz071wSNrjVhzR0CdQn6Sefx7Abf1o7vasmrLg==", + "resolved": "4.56.0", + "contentHash": "H12YAzEGK55vZ+QpxUzozhW8ZZtgPDuWvgA0JbdIR9UhMUplj29JhIgE2imuH8W2Nw9D8JKygR1uxRFtpSNcrg==", "dependencies": { - "Microsoft.Identity.Client": "4.38.0", + "Microsoft.Identity.Client": "4.56.0", + "System.IO.FileSystem.AccessControl": "5.0.0", "System.Security.Cryptography.ProtectedData": "4.5.0" } }, @@ -728,6 +727,15 @@ "System.Threading.Tasks": "4.3.0" } }, + "System.IO.FileSystem.AccessControl": { + "type": "Transitive", + "resolved": "5.0.0", + "contentHash": "SxHB3nuNrpptVk+vZ/F+7OHEpoHUIKKMl02bUmYHQr1r+glbZQxs7pRtsf4ENO29TVm2TH3AEeep2fJcy92oYw==", + "dependencies": { + "System.Security.AccessControl": "5.0.0", + "System.Security.Principal.Windows": "5.0.0" + } + }, "System.IO.FileSystem.Primitives": { "type": "Transitive", "resolved": "4.3.0", @@ -1175,19 +1183,19 @@ }, "System.Text.Encodings.Web": { "type": "Transitive", - "resolved": "6.0.0", - "contentHash": "Vg8eB5Tawm1IFqj4TVK1czJX89rhFxJo9ELqc/Eiq0eXy13RK00eubyU6TJE6y+GQXjyV5gSfiewDUZjQgSE0w==", + "resolved": "7.0.0", + "contentHash": "OP6umVGxc0Z0MvZQBVigj4/U31Pw72ITihDWP9WiWDm+q5aoe0GaJivsfYGq53o6dxH7DcXWiCTl7+0o2CGdmg==", "dependencies": { "System.Runtime.CompilerServices.Unsafe": "6.0.0" } }, "System.Text.Json": { "type": "Transitive", - "resolved": "6.0.7", - "contentHash": "/Tf/9XjprpHolbcDOrxsKVYy/mUG/FS7aGd9YUgBVEiHeQH4kAE0T1sMbde7q6B5xcrNUsJ5iW7D1RvHudQNqA==", + "resolved": "7.0.3", + "contentHash": "AyjhwXN1zTFeIibHimfJn6eAsZ7rTBib79JQpzg8WAuR/HKDu9JGNHTuu3nbbXQ/bgI+U4z6HtZmCHNXB1QXrQ==", "dependencies": { "System.Runtime.CompilerServices.Unsafe": "6.0.0", - "System.Text.Encodings.Web": "6.0.0" + "System.Text.Encodings.Web": "7.0.0" } }, "System.Text.RegularExpressions": { diff --git a/src/Kentico.Xperience.CRM.Libs.sln b/src/Kentico.Xperience.CRM.Libs.sln new file mode 100644 index 0000000..ff78c79 --- /dev/null +++ b/src/Kentico.Xperience.CRM.Libs.sln @@ -0,0 +1,34 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 17 +VisualStudioVersion = 17.0.31903.59 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Kentico.Xperience.CRM.Common", "Kentico.Xperience.CRM.Common\Kentico.Xperience.CRM.Common.csproj", "{C3608BA1-C86C-4D13-9835-7E1A062BA353}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Kentico.Xperience.CRM.Dynamics", "Kentico.Xperience.CRM.Dynamics\Kentico.Xperience.CRM.Dynamics.csproj", "{91405C0F-E95E-4350-8F02-86FDBC9382EB}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Kentico.Xperience.CRM.SalesForce", "Kentico.Xperience.CRM.SalesForce\Kentico.Xperience.CRM.SalesForce.csproj", "{F3367A36-DC9B-42B3-B579-3F598F5603F9}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {C3608BA1-C86C-4D13-9835-7E1A062BA353}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C3608BA1-C86C-4D13-9835-7E1A062BA353}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C3608BA1-C86C-4D13-9835-7E1A062BA353}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C3608BA1-C86C-4D13-9835-7E1A062BA353}.Release|Any CPU.Build.0 = Release|Any CPU + {91405C0F-E95E-4350-8F02-86FDBC9382EB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {91405C0F-E95E-4350-8F02-86FDBC9382EB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {91405C0F-E95E-4350-8F02-86FDBC9382EB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {91405C0F-E95E-4350-8F02-86FDBC9382EB}.Release|Any CPU.Build.0 = Release|Any CPU + {F3367A36-DC9B-42B3-B579-3F598F5603F9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F3367A36-DC9B-42B3-B579-3F598F5603F9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F3367A36-DC9B-42B3-B579-3F598F5603F9}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F3367A36-DC9B-42B3-B579-3F598F5603F9}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal diff --git a/src/Kentico.Xperience.CRM.SalesForce/Configuration/SalesForceBizFormsMappingBuilder.cs b/src/Kentico.Xperience.CRM.SalesForce/Configuration/SalesForceBizFormsMappingBuilder.cs index 96a2765..ec0dc9f 100644 --- a/src/Kentico.Xperience.CRM.SalesForce/Configuration/SalesForceBizFormsMappingBuilder.cs +++ b/src/Kentico.Xperience.CRM.SalesForce/Configuration/SalesForceBizFormsMappingBuilder.cs @@ -88,7 +88,6 @@ public SalesForceBizFormsMappingBuilder AddFormWithConverter(string /// /// Adds custom service for BizForm item validation before sending to CRM /// - /// /// /// public SalesForceBizFormsMappingBuilder AddCustomValidation() diff --git a/src/Kentico.Xperience.CRM.SalesForce/Configuration/SalesForceBizFormsMappingConfiguration.cs b/src/Kentico.Xperience.CRM.SalesForce/Configuration/SalesForceBizFormsMappingConfiguration.cs index 76f4b17..31550c6 100644 --- a/src/Kentico.Xperience.CRM.SalesForce/Configuration/SalesForceBizFormsMappingConfiguration.cs +++ b/src/Kentico.Xperience.CRM.SalesForce/Configuration/SalesForceBizFormsMappingConfiguration.cs @@ -5,6 +5,8 @@ namespace Kentico.Xperience.CRM.SalesForce.Configuration; /// /// Specific configuration for BizForm mapping to Lead in SalesForce Sales /// +#pragma warning disable S2094 // Classes should not be empty public class SalesForceBizFormsMappingConfiguration : BizFormsMappingConfiguration +#pragma warning restore S2094 // Classes should not be empty { } \ No newline at end of file diff --git a/src/Kentico.Xperience.CRM.SalesForce/Converters/FormContactMappingToLeadConverter.cs b/src/Kentico.Xperience.CRM.SalesForce/Converters/FormContactMappingToLeadConverter.cs index 91a0dbe..1fc2fc5 100644 --- a/src/Kentico.Xperience.CRM.SalesForce/Converters/FormContactMappingToLeadConverter.cs +++ b/src/Kentico.Xperience.CRM.SalesForce/Converters/FormContactMappingToLeadConverter.cs @@ -1,4 +1,7 @@ using CMS.ContactManagement; +using CMS.Core; +using CMS.DataEngine; +using CMS.Globalization; using CMS.OnlineForms; using CMS.OnlineForms.Internal; using Kentico.Xperience.CRM.Common.Mapping; @@ -12,10 +15,20 @@ namespace Kentico.Xperience.CRM.SalesForce.Converters; public class FormContactMappingToLeadConverter : ICRMTypeConverter { private readonly IContactFieldFromFormRetriever contactFieldFromFormRetriever; + private readonly IInfoByIdProvider countries; + private readonly IInfoByIdProvider states; + private readonly IConversionService conversion; - public FormContactMappingToLeadConverter(IContactFieldFromFormRetriever contactFieldFromFormRetriever) + public FormContactMappingToLeadConverter( + IContactFieldFromFormRetriever contactFieldFromFormRetriever, + IInfoByIdProvider countries, + IInfoByIdProvider states, + IConversionService conversion) { this.contactFieldFromFormRetriever = contactFieldFromFormRetriever; + this.countries = countries; + this.states = states; + this.conversion = conversion; } public Task Convert(BizFormItem source, LeadSObject destination) @@ -25,62 +38,74 @@ public Task Convert(BizFormItem source, LeadSObject destination) { destination.FirstName = firstName; } - + var lastName = contactFieldFromFormRetriever.Retrieve(source, nameof(ContactInfo.ContactLastName)); if (!string.IsNullOrWhiteSpace(lastName)) { destination.LastName = lastName; } - + var email = contactFieldFromFormRetriever.Retrieve(source, nameof(ContactInfo.ContactEmail)); if (!string.IsNullOrWhiteSpace(email)) { destination.Email = email; } - + var companyName = contactFieldFromFormRetriever.Retrieve(source, nameof(ContactInfo.ContactCompanyName)); if (!string.IsNullOrWhiteSpace(companyName)) { destination.Company = companyName; } - + var phone = contactFieldFromFormRetriever.Retrieve(source, nameof(ContactInfo.ContactMobilePhone)); if (!string.IsNullOrWhiteSpace(phone)) { destination.MobilePhone = phone; } - + var bizPhone = contactFieldFromFormRetriever.Retrieve(source, nameof(ContactInfo.ContactBusinessPhone)); if (!string.IsNullOrWhiteSpace(bizPhone)) { destination.Phone = bizPhone; } - + var jobTitle = contactFieldFromFormRetriever.Retrieve(source, nameof(ContactInfo.ContactJobTitle)); if (!string.IsNullOrWhiteSpace(jobTitle)) { destination.Title = jobTitle; } - + var address1 = contactFieldFromFormRetriever.Retrieve(source, nameof(ContactInfo.ContactAddress1)); if (!string.IsNullOrWhiteSpace(address1)) { destination.Street = address1; } - + var city = contactFieldFromFormRetriever.Retrieve(source, nameof(ContactInfo.ContactCity)); if (!string.IsNullOrWhiteSpace(city)) { destination.City = city; } - + var zipCode = contactFieldFromFormRetriever.Retrieve(source, nameof(ContactInfo.ContactZIP)); if (!string.IsNullOrWhiteSpace(zipCode)) { destination.PostalCode = zipCode; } - - //@TODO country, state + + string countryIDVal = contactFieldFromFormRetriever.Retrieve(source, nameof(ContactInfo.ContactCountryID)); + if (!string.IsNullOrWhiteSpace(countryIDVal)) + { + var country = countries.Get(conversion.GetInteger(countryIDVal, 0)); + destination.Country = country?.CountryDisplayName; + } + + string stateIDVal = contactFieldFromFormRetriever.Retrieve(source, nameof(ContactInfo.ContactStateID)); + if (!string.IsNullOrWhiteSpace(stateIDVal)) + { + var state = states.Get(conversion.GetInteger(stateIDVal, 0)); + destination.State = state?.StateDisplayName; + } return Task.FromResult(destination); } diff --git a/src/Kentico.Xperience.CRM.SalesForce/SalesForceIntegrationGlobalEvents.cs b/src/Kentico.Xperience.CRM.SalesForce/SalesForceIntegrationGlobalEvents.cs index 531f45f..b7e7755 100644 --- a/src/Kentico.Xperience.CRM.SalesForce/SalesForceIntegrationGlobalEvents.cs +++ b/src/Kentico.Xperience.CRM.SalesForce/SalesForceIntegrationGlobalEvents.cs @@ -24,6 +24,7 @@ namespace Kentico.Xperience.CRM.SalesForce; internal class SalesForceIntegrationGlobalEvents : Module { private ILogger logger = null!; + private ICRMModuleInstaller? installer; public SalesForceIntegrationGlobalEvents() : base(nameof(SalesForceIntegrationGlobalEvents)) { @@ -32,23 +33,30 @@ public SalesForceIntegrationGlobalEvents() : base(nameof(SalesForceIntegrationGl protected override void OnInit(ModuleInitParameters parameters) { base.OnInit(); - + var services = parameters.Services; - + logger = services.GetRequiredService>(); - Service.Resolve().Install(CRMType.SalesForce); - + installer = services.GetRequiredService(); + + ApplicationEvents.Initialized.Execute += InitializeModule; + BizFormItemEvents.Insert.After += SynchronizeBizFormLead; BizFormItemEvents.Update.After += SynchronizeBizFormLead; - + ThreadWorker.Current.EnsureRunningThread(); - + RequestEvents.RunEndRequestTasks.Execute += (_, _) => { FailedItemsWorker.Current.EnsureRunningThread(); }; } - + + private void InitializeModule(object? sender, EventArgs e) + { + installer?.Install(CRMType.SalesForce); + } + private void SynchronizeBizFormLead(object? sender, BizFormItemEventArgs e) { var failedSyncItemsService = Service.Resolve(); @@ -58,7 +66,7 @@ private void SynchronizeBizFormLead(object? sender, BizFormItemEventArgs e) { var settings = serviceScope.ServiceProvider.GetRequiredService>().Value; if (!settings.FormLeadsEnabled) return; - + var leadsIntegrationService = serviceScope.ServiceProvider .GetRequiredService(); diff --git a/src/Kentico.Xperience.CRM.SalesForce/SalesForceServiceCollectionsExtensions.cs b/src/Kentico.Xperience.CRM.SalesForce/SalesForceServiceCollectionsExtensions.cs index 4a8afe5..3641868 100644 --- a/src/Kentico.Xperience.CRM.SalesForce/SalesForceServiceCollectionsExtensions.cs +++ b/src/Kentico.Xperience.CRM.SalesForce/SalesForceServiceCollectionsExtensions.cs @@ -22,12 +22,12 @@ public static class SalesForceServiceCollectionsExtensions /// /// /// - public static IServiceCollection AddSalesForceFormLeadsIntegration(this IServiceCollection serviceCollection, + public static IServiceCollection AddKenticoCRMSalesForce(this IServiceCollection serviceCollection, Action formsConfig, IConfiguration? configuration = null) { serviceCollection.AddKenticoCrmCommonFormLeadsIntegration(); - + var mappingBuilder = new SalesForceBizFormsMappingBuilder(serviceCollection); formsConfig(mappingBuilder); serviceCollection.TryAddSingleton( diff --git a/src/Kentico.Xperience.CRM.SalesForce/Services/ISalesForceApiService.cs b/src/Kentico.Xperience.CRM.SalesForce/Services/ISalesForceApiService.cs index eec8d28..3dff3ba 100644 --- a/src/Kentico.Xperience.CRM.SalesForce/Services/ISalesForceApiService.cs +++ b/src/Kentico.Xperience.CRM.SalesForce/Services/ISalesForceApiService.cs @@ -34,6 +34,7 @@ public interface ISalesForceApiService /// Get Lead by primary Id /// /// + /// /// Task GetLeadById(string id, string? fields = null); diff --git a/src/Kentico.Xperience.CRM.SalesForce/Services/SalesForceLeadsIntegrationService.cs b/src/Kentico.Xperience.CRM.SalesForce/Services/SalesForceLeadsIntegrationService.cs index 8bf994f..31396a2 100644 --- a/src/Kentico.Xperience.CRM.SalesForce/Services/SalesForceLeadsIntegrationService.cs +++ b/src/Kentico.Xperience.CRM.SalesForce/Services/SalesForceLeadsIntegrationService.cs @@ -130,7 +130,7 @@ private async Task UpdateByEmailOrCreate(BizFormItem bizFormItem, IEnumerable> converters) { var lead = new LeadSObject(); - MapLead(bizFormItem, lead, fieldMappings, converters); + await MapLead(bizFormItem, lead, fieldMappings, converters); lead.LeadSource ??= $"Form {bizFormItem.BizFormInfo.FormDisplayName} - ID: {bizFormItem.ItemID}"; lead.Company ??= "undefined"; //required field - set to 'undefined' to prevent errors @@ -173,7 +173,7 @@ private async Task UpdateLeadAsync(string leadId, BizFormItem bizFormItem, IEnumerable> converters) { var lead = new LeadSObject(); - MapLead(bizFormItem, lead, fieldMappings, converters); + await MapLead(bizFormItem, lead, fieldMappings, converters); lead.LeadSource ??= $"Form {bizFormItem.BizFormInfo.FormDisplayName} - ID: {bizFormItem.ItemID}"; @@ -191,7 +191,7 @@ private async Task MapLead(BizFormItem bizFormItem, LeadSObject lead, { await converter.Convert(bizFormItem, lead); } - + foreach (var fieldMapping in fieldMappings) { var formFieldValue = fieldMapping.FormFieldMapping.MapFormField(bizFormItem); @@ -199,7 +199,7 @@ private async Task MapLead(BizFormItem bizFormItem, LeadSObject lead, { CRMFieldNameMapping m => lead.AdditionalProperties[m.CrmFieldName] = formFieldValue, CRMFieldMappingFunction m => m.MapCrmField(lead, formFieldValue), - _ => throw new ArgumentOutOfRangeException(nameof(fieldMapping.CRMFieldMapping), + _ => throw new ArgumentOutOfRangeException(nameof(fieldMappings), fieldMapping.CRMFieldMapping.GetType(), "Unsupported mapping") }; } diff --git a/src/Kentico.Xperience.CRM.SalesForce/packages.lock.json b/src/Kentico.Xperience.CRM.SalesForce/packages.lock.json index 05a99a2..5114f51 100644 --- a/src/Kentico.Xperience.CRM.SalesForce/packages.lock.json +++ b/src/Kentico.Xperience.CRM.SalesForce/packages.lock.json @@ -4,11 +4,11 @@ "net6.0": { "Duende.AccessTokenManagement.OpenIdConnect": { "type": "Direct", - "requested": "[2.0.3, )", - "resolved": "2.0.3", - "contentHash": "lFbbzI95P3tIhuvJfXded66R369hUcsLXX00pSpsBP3yoTTqS28dulhkV2r7/EvZ+pagHLTFbzjzaN7Am/B9XQ==", + "requested": "[2.1.0, )", + "resolved": "2.1.0", + "contentHash": "Lgvnsg4WFbw2FUZEp0YMiTcamDoT/jyJMYARUAH3a8Xul9WiIRMZXpqhmJdJ3DzYdqgEs8oa3cyHP0YjLhqE3Q==", "dependencies": { - "Duende.AccessTokenManagement": "2.0.3", + "Duende.AccessTokenManagement": "2.1.0", "Microsoft.AspNetCore.Authentication.OpenIdConnect": "6.0.0" } }, @@ -43,19 +43,19 @@ }, "NSwag.ApiDescription.Client": { "type": "Direct", - "requested": "[13.18.2, )", - "resolved": "13.18.2", - "contentHash": "uViMdjUscfeqrlDY4q9O2a0t2cMqsOx1kdX9WLyQjTXea1xjAHgQFCYlgE6ibwkYcUDJDlwEYUzvsJelL6SY3g==", + "requested": "[14.0.2, )", + "resolved": "14.0.2", + "contentHash": "H/TicFz7WmwvlveyL5YcAhbFqvY1Zh7e++7LwaBfNfq2oAlvXg4e2ZQv9L0+eDt2qpoTbuN7mD0DKmkELq2cpw==", "dependencies": { "Microsoft.Extensions.ApiDescription.Client": "6.0.3", - "NSwag.MSBuild": "13.18.2" + "NSwag.MSBuild": "14.0.2" } }, "SonarAnalyzer.CSharp": { "type": "Direct", - "requested": "[9.6.0.74858, )", - "resolved": "9.6.0.74858", - "contentHash": "tBwiOBYcoSyHVUejjz6iH3VOpSFx7gJSuQ3695BQaoqQBcqy0IeRsKAk5FAR4fbm3QLyEM9bimKk1l038TFN2w==" + "requested": "[9.17.0.82934, )", + "resolved": "9.17.0.82934", + "contentHash": "vHMvduSho3HACmC4jIPamuJiWenJKOIxcnvHNZ1vOztvQ0VeIYAAOvvw79BKpzZOEDEpM9uUOJgCSPgBetSzSw==" }, "AngleSharp": { "type": "Transitive", @@ -114,16 +114,16 @@ }, "Duende.AccessTokenManagement": { "type": "Transitive", - "resolved": "2.0.3", - "contentHash": "+Gf464caPFzPsprf/oyKHwVLJmiGk404vtjpGsmJVM9VpM4B8I9140ANDJwN2S5ihOcL3zH7mv59re0KT31TfA==", + "resolved": "2.1.0", + "contentHash": "SZXYF/pS/Tkc3fY7az/S3Y/uODVtKU7KbDyaZBOqNl9y9MSmga+FEAmgDfdjCq+f9wRy9QTkicGKHSSKcwT4HQ==", "dependencies": { - "IdentityModel": "6.1.0", + "IdentityModel": "6.2.0", "Microsoft.Extensions.Caching.Abstractions": "6.0.0", "Microsoft.Extensions.DependencyInjection.Abstractions": "6.0.0", "Microsoft.Extensions.Http": "6.0.0", "Microsoft.Extensions.Logging.Abstractions": "6.0.0", "Microsoft.Extensions.Options": "6.0.0", - "System.IdentityModel.Tokens.Jwt": "6.15.1" + "System.IdentityModel.Tokens.Jwt": "6.15.0" } }, "HtmlSanitizer": { @@ -497,8 +497,8 @@ }, "NSwag.MSBuild": { "type": "Transitive", - "resolved": "13.18.2", - "contentHash": "SRQ3mONkfbJWhZxA1yOFMJMXavUvnXwcoYU23qoVqSEY2G+y8jZuq9ErWm76JT0Kn5/Ml5UhG1FWmLhkqd4/+A==" + "resolved": "14.0.2", + "contentHash": "CnW1JgxdO5yKaYV0PxVCMpLv5XC8kN18iH8T1QrKjzTgu8ypNSVUOb+D+kwARiWsjiscebAIjKVvtTYcLS/FSQ==" }, "System.Buffers": { "type": "Transitive", diff --git a/test/Kentico.Xperience.CRM.Common.Tests/packages.lock.json b/test/Kentico.Xperience.CRM.Common.Tests/packages.lock.json index 7c52f36..1f21ce5 100644 --- a/test/Kentico.Xperience.CRM.Common.Tests/packages.lock.json +++ b/test/Kentico.Xperience.CRM.Common.Tests/packages.lock.json @@ -10,28 +10,25 @@ }, "Microsoft.NET.Test.Sdk": { "type": "Direct", - "requested": "[17.7.2, )", - "resolved": "17.7.2", - "contentHash": "WOSF/GUYcnrLGkdvCbXDRig2rShtBwfQc5l7IxQE6PZI3CeXAqF1SpyzwlGA5vw+MdEAXs3niy+ZkGBBWna6tw==", + "requested": "[17.8.0, )", + "resolved": "17.8.0", + "contentHash": "BmTYGbD/YuDHmApIENdoyN1jCk0Rj1fJB0+B/fVekyTdVidr91IlzhqzytiUgaEAzL1ZJcYCme0MeBMYvJVzvw==", "dependencies": { - "Microsoft.CodeCoverage": "17.7.2", - "Microsoft.TestPlatform.TestHost": "17.7.2" + "Microsoft.CodeCoverage": "17.8.0", + "Microsoft.TestPlatform.TestHost": "17.8.0" } }, "NUnit": { "type": "Direct", - "requested": "[3.13.3, )", - "resolved": "3.13.3", - "contentHash": "KNPDpls6EfHwC3+nnA67fh5wpxeLb3VLFAfLxrug6JMYDLHH6InaQIWR7Sc3y75d/9IKzMksH/gi08W7XWbmnQ==", - "dependencies": { - "NETStandard.Library": "2.0.0" - } + "requested": "[4.0.1, )", + "resolved": "4.0.1", + "contentHash": "jNTHZ01hJsDNPDSBycoHpavFZUBf9vRVQLCyuo78LRrrFj6Ol/CeqK+NIeTk5d8Eycjk59KseWb7X5Ge6z7CgQ==" }, "NUnit.Analyzers": { "type": "Direct", - "requested": "[3.9.0, )", - "resolved": "3.9.0", - "contentHash": "8bGAEljlBnzR+uU8oGQhTVKnbgBw1Mo71qjVkgzHdvtUkiB5XOIDyjAcS4KUo/j+F2Zv/xBUZRkCWXmejx4bfA==" + "requested": "[3.10.0, )", + "resolved": "3.10.0", + "contentHash": "+F2deTIQms+hJNzuoO68zgP1ftH61+meBx8iMn/90owv+eZpFTyQberapJ87jBm8f22/mwQnWufemjEVIvW11g==" }, "NUnit3TestAdapter": { "type": "Direct", @@ -41,24 +38,19 @@ }, "SonarAnalyzer.CSharp": { "type": "Direct", - "requested": "[9.6.0.74858, )", - "resolved": "9.6.0.74858", - "contentHash": "tBwiOBYcoSyHVUejjz6iH3VOpSFx7gJSuQ3695BQaoqQBcqy0IeRsKAk5FAR4fbm3QLyEM9bimKk1l038TFN2w==" + "requested": "[9.17.0.82934, )", + "resolved": "9.17.0.82934", + "contentHash": "vHMvduSho3HACmC4jIPamuJiWenJKOIxcnvHNZ1vOztvQ0VeIYAAOvvw79BKpzZOEDEpM9uUOJgCSPgBetSzSw==" }, "Microsoft.CodeCoverage": { "type": "Transitive", - "resolved": "17.7.2", - "contentHash": "ntbkwIqwszkfCRjxVZOyEQiHauiYsY9NtYjw9ASsoxDSiG8YtV6AGcOAwrAk3TZv2UOq4MrpX+3MYEeMHSb03w==" - }, - "Microsoft.NETCore.Platforms": { - "type": "Transitive", - "resolved": "1.1.0", - "contentHash": "kz0PEW2lhqygehI/d6XsPCQzD7ff7gUJaVGPVETX611eadGsA3A877GdSlU0LRVMCTH/+P3o2iDTak+S08V2+A==" + "resolved": "17.8.0", + "contentHash": "KC8SXWbGIdoFVdlxKk9WHccm0llm9HypcHMLUUFabRiTS3SO2fQXNZfdiF3qkEdTJhbRrxhdRxjL4jbtwPq4Ew==" }, "Microsoft.TestPlatform.ObjectModel": { "type": "Transitive", - "resolved": "17.7.2", - "contentHash": "aHzQWgDMVBnk39HhQVmn06w+YxzF1h2V5/M4WgrNQAn7q97GR4Si3vLRTDlmJo9nK/Nknce+H4tXx4gqOKyLeg==", + "resolved": "17.8.0", + "contentHash": "AYy6vlpGMfz5kOFq99L93RGbqftW/8eQTqjT9iGXW6s9MRP3UdtY8idJ8rJcjeSja8A18IhIro5YnH3uv1nz4g==", "dependencies": { "NuGet.Frameworks": "6.5.0", "System.Reflection.Metadata": "1.6.0" @@ -66,21 +58,13 @@ }, "Microsoft.TestPlatform.TestHost": { "type": "Transitive", - "resolved": "17.7.2", - "contentHash": "pv9yVD7IKPLJV28zYjLsWFiM3j506I2ye+6NquG8vsbm/gR7lgyig8IgY6Vo57VMvGaAKwtUECzcj+C5tH271Q==", + "resolved": "17.8.0", + "contentHash": "9ivcl/7SGRmOT0YYrHQGohWiT5YCpkmy/UEzldfVisLm6QxbLaK3FAJqZXI34rnRLmqqDCeMQxKINwmKwAPiDw==", "dependencies": { - "Microsoft.TestPlatform.ObjectModel": "17.7.2", + "Microsoft.TestPlatform.ObjectModel": "17.8.0", "Newtonsoft.Json": "13.0.1" } }, - "NETStandard.Library": { - "type": "Transitive", - "resolved": "2.0.0", - "contentHash": "7jnbRU+L08FXKMxqUflxEXtVymWvNOrS8yHgu9s6EM8Anr6T/wIX4nZ08j/u3Asz+tCufp3YVwFSEvFTPYmBPA==", - "dependencies": { - "Microsoft.NETCore.Platforms": "1.1.0" - } - }, "Newtonsoft.Json": { "type": "Transitive", "resolved": "13.0.1", From 9a7f643d2813e89a79ea0d14afaba9a95dce5b2e Mon Sep 17 00:00:00 2001 From: "Sean G. Wright" Date: Wed, 24 Jan 2024 01:06:14 -0500 Subject: [PATCH 23/23] build(CRM.SalesForce): revert NSwag package to highest previous major --- Directory.Packages.props | 2 +- .../packages.lock.json | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index c38cd02..1da0159 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -20,6 +20,6 @@ - +
\ No newline at end of file diff --git a/src/Kentico.Xperience.CRM.SalesForce/packages.lock.json b/src/Kentico.Xperience.CRM.SalesForce/packages.lock.json index 5114f51..3fa0891 100644 --- a/src/Kentico.Xperience.CRM.SalesForce/packages.lock.json +++ b/src/Kentico.Xperience.CRM.SalesForce/packages.lock.json @@ -43,12 +43,12 @@ }, "NSwag.ApiDescription.Client": { "type": "Direct", - "requested": "[14.0.2, )", - "resolved": "14.0.2", - "contentHash": "H/TicFz7WmwvlveyL5YcAhbFqvY1Zh7e++7LwaBfNfq2oAlvXg4e2ZQv9L0+eDt2qpoTbuN7mD0DKmkELq2cpw==", + "requested": "[13.20.0, )", + "resolved": "13.20.0", + "contentHash": "ylDDEqtlSrTUH9oSPttT+ZoRCTusx8U8kbmqjfNuujvfLQz6KGPctkEhXLZYn4Bdy3v5z+a2koPN+DbeyyCxXA==", "dependencies": { "Microsoft.Extensions.ApiDescription.Client": "6.0.3", - "NSwag.MSBuild": "14.0.2" + "NSwag.MSBuild": "13.20.0" } }, "SonarAnalyzer.CSharp": { @@ -497,8 +497,8 @@ }, "NSwag.MSBuild": { "type": "Transitive", - "resolved": "14.0.2", - "contentHash": "CnW1JgxdO5yKaYV0PxVCMpLv5XC8kN18iH8T1QrKjzTgu8ypNSVUOb+D+kwARiWsjiscebAIjKVvtTYcLS/FSQ==" + "resolved": "13.20.0", + "contentHash": "3Z+7gCh+hb9DbK7O0iRgt2iDLOeZAVMpGyJ4UQUs/yUClQSNdA+QWmStlnSyx+jCTQX+tAcLSN6H5kw5Q6hCLA==" }, "System.Buffers": { "type": "Transitive",