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 eb399ec..1da0159 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -6,21 +6,20 @@ true - - - - - - - - - + + + + + + + + - + - - - - + + + + \ No newline at end of file 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 d2ad6e6..b446820 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ The versions of this library are supported by the following versions of Xperienc | Xperience Version | Library Version | | ----------------- | --------------- | -| >= 27.0.1 | 1.x | +| >= 28.0.0 | 1.0.0 | ### Dependencies @@ -37,54 +37,23 @@ 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 +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)) - -#### Dynamics settings - -```json -{ - "CMSDynamicsCRMIntegration": { - "FormLeadsEnabled": true, - "ApiConfig": { - "DynamicsUrl": "", - "ClientId": "", - "ClientSecret": "" - } - } -} -``` - -#### SalesForce settings - -```json -{ - "CMSSalesForceCRMIntegration": { - "FormLeadsEnabled": true, - "ApiConfig": { - "SalesForceUrl": "", - "ClientId": "", - "ClientSecret": "" - } - } -} -``` - -You can also set specific API version for SalesForce REST API (default version is 59). +There are 2 options how to fill settings: -```json -{ - "CMSSalesForceCRMIntegration:ApiConfig:ApiVersion": 59 -} -``` +- 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 @@ -92,65 +61,107 @@ Configure mapping for each form between Kentico Form fields and Dynamics Lead en #### 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.AddKenticoCRMDynamics(builder => + builder.AddFormWithContactMapping(DancingGoatContactUsItem.CLASS_NAME)); +``` + +Example how to add form with own mapping: -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 +```csharp + // Program.cs + + var builder = WebApplication.CreateBuilder(args); + + // ... + builder.Services.AddKenticoCRMDynamics(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.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.AddKenticoCRMSalesForce(builder => + builder.AddFormWithContactMapping(DancingGoatContactUsItem.CLASS_NAME)); +``` + +Example how to add form with own mapping: + +```csharp + // Program.cs -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 + var builder = WebApplication.CreateBuilder(args); + + // ... + 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 + .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.AddKenticoCRMSalesForce(builder => + 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..58db3b0 100644 --- a/docs/Usage-Guide.md +++ b/docs/Usage-Guide.md @@ -1,3 +1,222 @@ # 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) + +### 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 +{ + "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.AddKenticoCRMDynamics(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.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.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.AddKenticoCRMDynamics(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.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.AddKenticoCRMSalesForce(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.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.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.AddKenticoCRMSalesForce(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.AddKenticoCRMSalesForce(builder => + builder.AddFormWithConverter(DancingGoatContactUsItem.CLASS_NAME)); +``` 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/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/Data/Template.zip b/examples/DancingGoat/Data/Template.zip index 0d060c7..e4076ca 100644 Binary files a/examples/DancingGoat/Data/Template.zip and b/examples/DancingGoat/Data/Template.zip differ diff --git a/examples/DancingGoat/DataProtectionSamples/DancingGoatSamplesModule.cs b/examples/DancingGoat/DataProtectionSamples/DancingGoatSamplesModule.cs index c01f850..a566a7a 100644 --- a/examples/DancingGoat/DataProtectionSamples/DancingGoatSamplesModule.cs +++ b/examples/DancingGoat/DataProtectionSamples/DancingGoatSamplesModule.cs @@ -1,4 +1,7 @@ -using CMS; +using System; +using System.Collections.Generic; + +using CMS; using CMS.Activities; using CMS.Base; using CMS.ContactManagement; @@ -123,25 +126,28 @@ internal void DeleteContactActivities(ContactInfo contact) } - private void RegisterConsentRevokeHandler() => 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 636da6a..ba82cd1 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,16 +8,16 @@ 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; 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); @@ -29,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(); @@ -44,40 +48,46 @@ .AddControllersWithViews() .AddViewLocalization() .AddDataAnnotationsLocalization(options => - options.DataAnnotationLocalizerProvider = (type, factory) => factory.Create(typeof(SharedResources))); + { + options.DataAnnotationLocalizerProvider = (type, factory) => factory.Create(typeof(SharedResources)); + }); builder.Services.AddDancingGoatServices(); ConfigureMembershipServices(builder.Services); //CRM integration registration start -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("crf1c_kenticoid") //optional custom field when you want updates to work - , - builder.Configuration.GetSection(DynamicsIntegrationSettings.ConfigKeyName)) //config section with settings - .AddCustomFormLeadsValidationService(); //optional - -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__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 + +//builder.Services.AddKenticoCRMDynamics(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.AddKenticoCRMDynamics(builder => + builder.AddFormWithContactMapping(DancingGoatContactUsItem.CLASS_NAME, b => b + .MapField(c => c.UserMessage, e => e.Description)) + .AddCustomValidation()); //optional + +//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 +// .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.AddKenticoCRMSalesForce(builder => + builder.AddFormWithContactMapping(DancingGoatContactUsItem.CLASS_NAME, b => b + .MapField(c => c.UserMessage, e => e.Description)) + .AddCustomValidation()); //CRM integration registration end @@ -101,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(); @@ -124,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>() @@ -147,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/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/examples/DancingGoat/packages.lock.json b/examples/DancingGoat/packages.lock.json index 7942cf2..ab8de3d 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" } @@ -135,22 +135,22 @@ }, "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": { "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", @@ -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": { @@ -1379,33 +1389,34 @@ "kentico.xperience.crm.common": { "type": "Project", "dependencies": { - "Kentico.Xperience.Core": "[27.0.1, )" + "Kentico.Xperience.Admin": "[28.0.0, )", + "Kentico.Xperience.Core": "[28.0.0, )" } }, "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": "[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, )", - "Microsoft.PowerPlatform.Dataverse.Client": "[1.1.14, )" + "Kentico.Xperience.Core": "[28.0.0, )", + "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" } }, @@ -1417,20 +1428,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", @@ -1440,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", @@ -1467,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/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 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/images/screenshots/Dynamics_CRM_settings.png b/images/screenshots/Dynamics_CRM_settings.png new file mode 100644 index 0000000..5cd6a79 Binary files /dev/null and b/images/screenshots/Dynamics_CRM_settings.png differ 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.Common/Admin/CRMIntegrationSettingsEdit.cs b/src/Kentico.Xperience.CRM.Common/Admin/CRMIntegrationSettingsEdit.cs new file mode 100644 index 0000000..d1d08af --- /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 new file mode 100644 index 0000000..6e8c8df --- /dev/null +++ b/src/Kentico.Xperience.CRM.Common/Admin/CRMSyncItemListing.cs @@ -0,0 +1,74 @@ +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; +using Kentico.Xperience.CRM.Common.Classes; + +[assembly: + UIPage(parentType: typeof(FormEditSection), slug: "crm-sync-listing", uiPageType: typeof(CRMSyncItemListing), + name: "CRM sync", templateName: TemplateNames.LISTING, order: 1000, icon: Icons.IntegrationScheme)] + +namespace Kentico.Xperience.CRM.Common.Admin; + +/// +/// Admin listing page for displaying synced items in CMS for selected form +/// +internal class CRMSyncItemListing : ListingPage +{ + private readonly IContactFieldFromFormRetriever contactFieldFromFormRetriever; + 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(FormId); + + private DataClassInfo DataClassInfo => this.dataClassInfo ??= + DataClassInfoProviderBase.GetDataClassInfo(EditedForm.FormClassID); + + public CRMSyncItemListing(IContactFieldFromFormRetriever contactFieldFromFormRetriever) + { + this.contactFieldFromFormRetriever = contactFieldFromFormRetriever; + } + + public override Task ConfigurePage() + { + PageConfiguration.ColumnConfigurations + .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"); + + PageConfiguration.QueryModifiers.AddModifier(q => + q.WhereEquals(nameof(CRMSyncItemInfo.CRMSyncItemEntityClass), DataClassInfo.ClassName) + .OrderByDescending(nameof(CRMSyncItemInfo.CRMSyncItemLastModified))); + + 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 new file mode 100644 index 0000000..52510ec --- /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 CRMIntegrationSettingsCRMName + { + get => ValidationHelper.GetString(GetValue(nameof(CRMIntegrationSettingsCRMName)), String.Empty); + set => SetValue(nameof(CRMIntegrationSettingsCRMName), 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/CRMSyncItemInfo.generated.cs b/src/Kentico.Xperience.CRM.Common/Classes/CRMSyncItemInfo.generated.cs new file mode 100644 index 0000000..2ffe2c6 --- /dev/null +++ b/src/Kentico.Xperience.CRM.Common/Classes/CRMSyncItemInfo.generated.cs @@ -0,0 +1,159 @@ +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. + /// + public static readonly ObjectTypeInfo TYPEINFO = new ObjectTypeInfo(typeof(CRMSyncItemInfoProvider), OBJECT_TYPE, "KenticoCRMCommon.CRMSyncItem", "CRMSyncItemID", "CRMSyncItemLastModified", null, null, null, null, null, null) + { + 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/FailedsyncitemInfo.generated.cs b/src/Kentico.Xperience.CRM.Common/Classes/FailedsyncitemInfo.generated.cs index babe13c..3d6155e 100644 --- a/src/Kentico.Xperience.CRM.Common/Classes/FailedsyncitemInfo.generated.cs +++ b/src/Kentico.Xperience.CRM.Common/Classes/FailedsyncitemInfo.generated.cs @@ -26,9 +26,8 @@ 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/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/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/BizFormFieldsMappingBuilder.cs b/src/Kentico.Xperience.CRM.Common/Configuration/BizFormFieldsMappingBuilder.cs index b581b5f..2cf0c44 100644 --- a/src/Kentico.Xperience.CRM.Common/Configuration/BizFormFieldsMappingBuilder.cs +++ b/src/Kentico.Xperience.CRM.Common/Configuration/BizFormFieldsMappingBuilder.cs @@ -29,5 +29,5 @@ public BizFormFieldsMappingBuilder AddMapping(BizFormFieldMapping 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 deleted file mode 100644 index f455ba2..0000000 --- a/src/Kentico.Xperience.CRM.Common/Configuration/BizFormsMappingBuilder.cs +++ /dev/null @@ -1,36 +0,0 @@ -namespace Kentico.Xperience.CRM.Common.Configuration; - -/// -/// Common builder for BizForms to CRM leads configuration mapping -/// -public class BizFormsMappingBuilder -{ - private readonly Dictionary forms = new(); - private string? externalIdFieldName; - - 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 ExternalIdField(string fieldName) - { - externalIdFieldName = fieldName; - 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), - 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..65dbaf4 100644 --- a/src/Kentico.Xperience.CRM.Common/Configuration/BizFormsMappingConfiguration.cs +++ b/src/Kentico.Xperience.CRM.Common/Configuration/BizFormsMappingConfiguration.cs @@ -7,6 +7,6 @@ namespace Kentico.Xperience.CRM.Common.Configuration; ///
public class BizFormsMappingConfiguration { - public Dictionary> FormsMappings { get; internal init; } = new(); - public string? ExternalIdFieldName { get; internal init; } + 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/Configuration/CommonIntegrationSettings.cs b/src/Kentico.Xperience.CRM.Common/Configuration/CommonIntegrationSettings.cs index a06303e..14811eb 100644 --- a/src/Kentico.Xperience.CRM.Common/Configuration/CommonIntegrationSettings.cs +++ b/src/Kentico.Xperience.CRM.Common/Configuration/CommonIntegrationSettings.cs @@ -4,11 +4,22 @@ /// Common setting for Kentico-CRM integration /// /// -public class CommonIntegrationSettings +public class CommonIntegrationSettings where TApiConfig : new() { + /// + /// If enabled BizForm leads synchronization + /// public bool FormLeadsEnabled { get; set; } - // @TODO phase 2 + public bool ContactsEnabled { get; set; } - public TApiConfig? ApiConfig { 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/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..8b11390 --- /dev/null +++ b/src/Kentico.Xperience.CRM.Common/Installers/CRMModuleInstaller.cs @@ -0,0 +1,324 @@ +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 custom class for failed synchronizations items +/// and synced items class +/// Custom settings are created +/// Objects are created when not exists on start. +/// +internal class CRMModuleInstaller : ICRMModuleInstaller +{ + private readonly IResourceInfoProvider resourceInfoProvider; + + public CRMModuleInstaller(IResourceInfoProvider resourceInfoProvider) + { + this.resourceInfoProvider = resourceInfoProvider; + } + + public void Install(string crmType) + { + var resourceInfo = InstallModule(); + InstallModuleClasses(resourceInfo); + InstallCRMIntegrationSettingsClass(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) + { + InstallSyncedItemClass(resourceInfo); + InstallFailedSyncItemClass(resourceInfo); + } + + private void InstallSyncedItemClass(ResourceInfo resourceInfo) + { + var failedSyncItemClass = DataClassInfoProvider.GetDataClassInfo(CRMSyncItemInfo.OBJECT_TYPE); + if (failedSyncItemClass is not null) + { + return; + } + + failedSyncItemClass = DataClassInfo.New(CRMSyncItemInfo.OBJECT_TYPE); + + 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(nameof(CRMSyncItemInfo.CRMSyncItemID)); + + var formItem = new FormFieldInfo + { + Name = nameof(CRMSyncItemInfo.CRMSyncItemEntityClass), + Visible = false, + Precision = 0, + Size = 100, + DataType = "text", + Enabled = true + }; + formInfo.AddFormItem(formItem); + + formItem = new FormFieldInfo + { + Name = nameof(CRMSyncItemInfo.CRMSyncItemEntityID), + Visible = false, + Precision = 0, + Size = 50, + DataType = "text", + Enabled = true + }; + formInfo.AddFormItem(formItem); + + formItem = new FormFieldInfo + { + Name = nameof(CRMSyncItemInfo.CRMSyncItemCRMID), + Visible = false, + Precision = 0, + Size = 50, + DataType = "text", + Enabled = true + }; + formInfo.AddFormItem(formItem); + + formItem = new FormFieldInfo + { + Name = nameof(CRMSyncItemInfo.CRMSyncItemEntityCRM), + Visible = false, + Precision = 0, + Size = 50, + DataType = "text", + Enabled = true + }; + formInfo.AddFormItem(formItem); + + formItem = new FormFieldInfo + { + Name = nameof(CRMSyncItemInfo.CRMSyncItemCreatedByKentico), + Visible = false, + DataType = "boolean", + Enabled = true + }; + formInfo.AddFormItem(formItem); + + formItem = new FormFieldInfo + { + Name = nameof(CRMSyncItemInfo.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.TYPEINFO.ObjectClassName; + failedSyncItemClass.ClassTableName = FailedSyncItemInfo.TYPEINFO.ObjectClassName.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 InstallCRMIntegrationSettingsClass(ResourceInfo resourceInfo) + { + var settingsCRM = DataClassInfoProvider.GetDataClassInfo(CRMIntegrationSettingsInfo.OBJECT_TYPE); + if (settingsCRM is not null) + { + return; + } + + settingsCRM = DataClassInfo.New(CRMIntegrationSettingsInfo.OBJECT_TYPE); + + 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(nameof(CRMIntegrationSettingsInfo.CRMIntegrationSettingsItemID)); + + var formItem = new FormFieldInfo + { + Name = nameof(CRMIntegrationSettingsInfo.CRMIntegrationSettingsFormsEnabled), + Caption = "Forms enabled", + Visible = true, + DataType = "boolean", + Enabled = true + }; + formInfo.AddFormItem(formItem); + + formItem = new FormFieldInfo + { + Name = nameof(CRMIntegrationSettingsInfo.CRMIntegrationSettingsContactsEnabled), + Caption = "Contacts enabled", + Visible = false, + DataType = "boolean", + Enabled = true + }; + formInfo.AddFormItem(formItem); + + formItem = new FormFieldInfo + { + Name = nameof(CRMIntegrationSettingsInfo.CRMIntegrationSettingsIgnoreExistingRecords), + Caption = "Ignore existing records", + Visible = true, + DataType = "boolean", + Enabled = true + }; + formInfo.AddFormItem(formItem); + + formItem = new FormFieldInfo + { + Name = nameof(CRMIntegrationSettingsInfo.CRMIntegrationSettingsUrl), + Caption = "CRM URL", + Visible = true, + Precision = 0, + Size = 100, + DataType = "text", + Enabled = true + }; + formInfo.AddFormItem(formItem); + + formItem = new FormFieldInfo + { + Name = nameof(CRMIntegrationSettingsInfo.CRMIntegrationSettingsClientId), + Caption = "Client ID", + Visible = true, + Precision = 0, + Size = 100, + DataType = "text", + Enabled = true + }; + formInfo.AddFormItem(formItem); + + formItem = new FormFieldInfo + { + Name = nameof(CRMIntegrationSettingsInfo.CRMIntegrationSettingsClientSecret), + Caption = "Client Secret", + Visible = true, + Precision = 0, + Size = 100, + DataType = "text", + Enabled = true + }; + formInfo.AddFormItem(formItem); + + formItem = new FormFieldInfo + { + Name = nameof(CRMIntegrationSettingsInfo.CRMIntegrationSettingsCRMName), + 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.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..96d1808 --- /dev/null +++ b/src/Kentico.Xperience.CRM.Common/Installers/ICRMModuleInstaller.cs @@ -0,0 +1,6 @@ +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/Kentico.Xperience.CRM.Common.csproj b/src/Kentico.Xperience.CRM.Common/Kentico.Xperience.CRM.Common.csproj index 09a0046..5700310 100644 --- a/src/Kentico.Xperience.CRM.Common/Kentico.Xperience.CRM.Common.csproj +++ b/src/Kentico.Xperience.CRM.Common/Kentico.Xperience.CRM.Common.csproj @@ -6,11 +6,8 @@ + - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - 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/Models/CRMIntegrationSettingsModel.cs b/src/Kentico.Xperience.CRM.Common/Models/CRMIntegrationSettingsModel.cs new file mode 100644 index 0000000..5333cef --- /dev/null +++ b/src/Kentico.Xperience.CRM.Common/Models/CRMIntegrationSettingsModel.cs @@ -0,0 +1,32 @@ +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] + [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 diff --git a/src/Kentico.Xperience.CRM.Common/ServiceCollectionExtensions.cs b/src/Kentico.Xperience.CRM.Common/ServiceCollectionExtensions.cs index 4bb6900..10e9995 100644 --- a/src/Kentico.Xperience.CRM.Common/ServiceCollectionExtensions.cs +++ b/src/Kentico.Xperience.CRM.Common/ServiceCollectionExtensions.cs @@ -1,5 +1,4 @@ -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; @@ -16,39 +15,16 @@ public static class ServiceCollectionExtensions /// Adds common services for CRM integration. This method is usually used from specific CRM integration library /// /// - /// - /// /// - public static IServiceCollection AddKenticoCrmCommonIntegration( - 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(); + 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/ICRMSyncItemService.cs b/src/Kentico.Xperience.CRM.Common/Services/ICRMSyncItemService.cs new file mode 100644 index 0000000..9769d69 --- /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 +{ + 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/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/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.Common/Services/Implementations/CRMSyncItemService.cs b/src/Kentico.Xperience.CRM.Common/Services/Implementations/CRMSyncItemService.cs new file mode 100644 index 0000000..6f018f7 --- /dev/null +++ b/src/Kentico.Xperience.CRM.Common/Services/Implementations/CRMSyncItemService.cs @@ -0,0 +1,52 @@ +using CMS.OnlineForms; +using Kentico.Xperience.CRM.Common.Classes; + +namespace Kentico.Xperience.CRM.Common.Services.Implementations; + +internal class CRMSyncItemService : ICRMSyncItemService +{ + private readonly ICRMSyncItemInfoProvider crmSyncItemInfoProvider; + + public CRMSyncItemService(ICRMSyncItemInfoProvider crmSyncItemInfoProvider) + { + this.crmSyncItemInfoProvider = crmSyncItemInfoProvider; + } + + public async Task LogFormLeadCreateItem(BizFormItem bizFormItem, string crmId, string crmName) + => await LogFormLeadSyncItem(bizFormItem, crmId, crmName, true); + + public async Task LogFormLeadUpdateItem(BizFormItem bizFormItem, string crmId, string crmName) + => await LogFormLeadSyncItem(bizFormItem, crmId, crmName, false); + + private async Task LogFormLeadSyncItem(BizFormItem bizFormItem, string crmId, string crmName, bool createdByKentico) + { + var syncItem = await 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 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.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.Common/Services/Implementations/LeadsIntegrationServiceCommon.cs b/src/Kentico.Xperience.CRM.Common/Services/Implementations/LeadsIntegrationServiceCommon.cs deleted file mode 100644 index 2f21ccc..0000000 --- a/src/Kentico.Xperience.CRM.Common/Services/Implementations/LeadsIntegrationServiceCommon.cs +++ /dev/null @@ -1,75 +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 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) - { - 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 UpdateLeadAsync(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}"; -} \ 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 9b74089..8a27718 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,25 +39,27 @@ protected override void Process() try { - var settings = Service.Resolve>().Value; + 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(); + ILeadsIntegrationService? leadsIntegrationService = null; foreach (var syncItem in failedSyncItemsService.GetFailedSyncItemsToReSync(CRMName)) { + leadsIntegrationService ??= serviceScope.ServiceProvider + .GetRequiredService(); + var bizFormItem = failedSyncItemsService.GetBizFormItem(syncItem); if (bizFormItem is null) { 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.Common/packages.lock.json b/src/Kentico.Xperience.CRM.Common/packages.lock.json index 574d450..81bbde0 100644 --- a/src/Kentico.Xperience.CRM.Common/packages.lock.json +++ b/src/Kentico.Xperience.CRM.Common/packages.lock.json @@ -2,22 +2,34 @@ "version": 2, "dependencies": { "net6.0": { + "Kentico.Xperience.Admin": { + "type": "Direct", + "requested": "[28.0.0, )", + "resolved": "28.0.0", + "contentHash": "G54CsUaiegQF1NS3+7p6uyWObSdBVbqfZCDlyzEQSAypZS8Gy1bc8p+SKTpjyNFyj+DpQcX9LJfbam2hUVNOUg==", + "dependencies": { + "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", @@ -25,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", @@ -50,6 +52,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 +93,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.746", + "contentHash": "9InxDYXIEnCnWQN2eCLCNL3QGyhmV0+vn12QlVSPig5m0f6XisAM9pq+TWB8aUtxXm1zm39/rOC8Ed7eZaEYew==", + "dependencies": { + "AngleSharp": "[0.17.1]", + "AngleSharp.Css": "[0.17.0]", + "System.Collections.Immutable": "7.0.0" + } + }, + "Kentico.Aira.Client": { + "type": "Transitive", + "resolved": "1.0.23", + "contentHash": "16jt+oHW6Fa6fDmJlJBTJuMK+A6nwXDX7IcYygYFzq01Sek43oQB2wjhXPRhntsvFYhAbWcBwXPzn0ALlMggSQ==", + "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,15 +128,18 @@ "MimeKit": "4.2.0" } }, - "Microsoft.Bcl.AsyncInterfaces": { + "Microsoft.AspNetCore.SpaServices.Extensions": { "type": "Transitive", - "resolved": "1.1.1", - "contentHash": "yuvf07qFWFqtK3P/MRkEKLhn5r2UbSpVueRziSqj0yJQIKFwG1pq9mOayK3zE5qZCTs0CbrwL9M6R8VwqyGy2w==" + "resolved": "6.0.24", + "contentHash": "onDzS4i2iLMVHtqnTNEFuik2wqarpi88e8Bz/K6FOHwwj+z9kIvcjdmV8qLpC8effWiQot/JHiGzEtsZA/8yWQ==", + "dependencies": { + "Microsoft.Extensions.FileProviders.Physical": "6.0.0" + } }, - "Microsoft.Build.Tasks.Git": { + "Microsoft.Bcl.AsyncInterfaces": { "type": "Transitive", "resolved": "1.1.1", - "contentHash": "AT3HlgTjsqHnWpBHSNeR0KxbLZD7bztlZVj7I8vgeYG9SYqbeFGh0TM/KVtC6fg53nrWHl3VfZFvb5BiQFcY6Q==" + "contentHash": "yuvf07qFWFqtK3P/MRkEKLhn5r2UbSpVueRziSqj0yJQIKFwG1pq9mOayK3zE5qZCTs0CbrwL9M6R8VwqyGy2w==" }, "Microsoft.CSharp": { "type": "Transitive", @@ -108,11 +148,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", @@ -128,8 +168,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", @@ -198,6 +238,14 @@ "Microsoft.Extensions.Primitives": "6.0.0" } }, + "Microsoft.Extensions.FileProviders.Embedded": { + "type": "Transitive", + "resolved": "6.0.24", + "contentHash": "HvTvzEkqYwDykjaekrpntZkxetMQ5XinIRYoRKftCqttmABoSfsOpsxCh0eIw9w4H+K8xHN7nqvCWFPP8feT4w==", + "dependencies": { + "Microsoft.Extensions.FileProviders.Abstractions": "6.0.0" + } + }, "Microsoft.Extensions.FileProviders.Physical": { "type": "Transitive", "resolved": "6.0.0", @@ -223,21 +271,44 @@ "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", - "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", + "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", @@ -292,25 +363,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 +405,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" } }, @@ -351,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", @@ -397,6 +464,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 +504,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 +635,19 @@ "dependencies": { "System.Drawing.Common": "6.0.0" } + }, + "Kentico.Xperience.WebApp": { + "type": "CentralTransitive", + "requested": "[28.0.0, )", + "resolved": "28.0.0", + "contentHash": "R54QHACtE8rfGtHKKFpSvY7LKI1IqqALe70QGYz1u6ZyZ23w0O7R9UZH5bgW/Ww0d13IgGQBc3ULaMel9bxJXg==", + "dependencies": { + "CommandLineParser": "2.9.1", + "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/Admin/DynamicsIntegrationSettingsEdit.cs b/src/Kentico.Xperience.CRM.Dynamics/Admin/DynamicsIntegrationSettingsEdit.cs new file mode 100644 index 0000000..07ef487 --- /dev/null +++ b/src/Kentico.Xperience.CRM.Dynamics/Admin/DynamicsIntegrationSettingsEdit.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.Dynamics.Admin; +using IFormItemCollectionProvider = Kentico.Xperience.Admin.Base.Forms.Internal.IFormItemCollectionProvider; + +[assembly: UIPage( + parentType: typeof(CRMIntegrationSettingsApplication), + slug: "dynamics-settings-edit", + uiPageType: typeof(DynamicsIntegrationSettingsEdit), + name: "Dynamics CRM", + templateName: TemplateNames.EDIT, + order: UIPageOrder.First)] + +namespace Kentico.Xperience.CRM.Dynamics.Admin; + +internal class DynamicsIntegrationSettingsEdit : CRMIntegrationSettingsEdit +{ + public DynamicsIntegrationSettingsEdit(IFormItemCollectionProvider formItemCollectionProvider, + IFormDataBinder formDataBinder, ICRMIntegrationSettingsInfoProvider crmIntegrationSettingsInfoProvider) : base( + formItemCollectionProvider, formDataBinder, crmIntegrationSettingsInfoProvider) + { + } + + protected override string CRMName => CRMType.Dynamics; +} \ 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..7322bf0 --- /dev/null +++ b/src/Kentico.Xperience.CRM.Dynamics/Configuration/DynamicsBizFormsMappingBuilder.cs @@ -0,0 +1,118 @@ +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.Dynamics.Converters; +using Kentico.Xperience.CRM.Dynamics.Dataverse.Entities; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; + +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; + } + + /// + /// 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) + { + if (formCodeName is null) throw new ArgumentNullException(nameof(formCodeName)); + forms.Add(formCodeName.ToLowerInvariant(), configureFields(new BizFormFieldsMappingBuilder())); + + AddFormWithConverter(formCodeName); + 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) }; + } + + serviceCollection.TryAddEnumerable(ServiceDescriptor + .Scoped, TConverter>()); + return this; + } + + /// + /// Adds custom service for BizForm item validation before sending to CRM + /// + /// + /// + public DynamicsBizFormsMappingBuilder AddCustomValidation() + 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..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 new file mode 100644 index 0000000..024f559 --- /dev/null +++ b/src/Kentico.Xperience.CRM.Dynamics/Converters/FormContactMappingToLeadConverter.cs @@ -0,0 +1,118 @@ +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; +using Kentico.Xperience.CRM.Dynamics.Dataverse.Entities; + +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; + private readonly IInfoByIdProvider countries; + private readonly IInfoByIdProvider states; + private readonly IConversionService conversion; + + 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) + { + 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; + } + + var zipCode = contactFieldFromFormRetriever.Retrieve(source, nameof(ContactInfo.ContactZIP)); + if (!string.IsNullOrWhiteSpace(zipCode)) + { + destination.Address1_PostalCode = zipCode; + } + + 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); + } +} \ No newline at end of file diff --git a/src/Kentico.Xperience.CRM.Dynamics/DynamicsBizFormGlobalEvents.cs b/src/Kentico.Xperience.CRM.Dynamics/DynamicsBizFormGlobalEvents.cs deleted file mode 100644 index 61ced1b..0000000 --- a/src/Kentico.Xperience.CRM.Dynamics/DynamicsBizFormGlobalEvents.cs +++ /dev/null @@ -1,92 +0,0 @@ -using CMS; -using CMS.Base; -using CMS.Core; -using CMS.DataEngine; -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; -using Kentico.Xperience.CRM.Dynamics.Workers; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; - -[assembly: RegisterModule(typeof(DynamicsBizFormGlobalEvents))] - -namespace Kentico.Xperience.CRM.Dynamics; - -/// -/// Module with bizformitem event handlers for Dynamics integration -/// -internal class DynamicsBizFormGlobalEvents : Module -{ - public DynamicsBizFormGlobalEvents() : base(nameof(DynamicsBizFormGlobalEvents)) - { - } - - private ILogger logger = null!; - - protected override void OnInit() - { - base.OnInit(); - - BizFormItemEvents.Insert.After += BizFormInserted; - BizFormItemEvents.Update.After += BizFormUpdated; - logger = Service.Resolve>(); - Service.Resolve().Install(); - ThreadWorker.Current.EnsureRunningThread(); - } - - private void BizFormInserted(object? sender, BizFormItemEventArgs e) - { - var failedSyncItemsService = Service.Resolve(); - try - { - var settings = Service.Resolve>().Value; - 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>().Value; - 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(); - } - } - catch (Exception exception) - { - logger.LogError(exception, "Error occured during updating lead"); - } - } -} \ No newline at end of file diff --git a/src/Kentico.Xperience.CRM.Dynamics/DynamicsIntegrationGlobalEvents.cs b/src/Kentico.Xperience.CRM.Dynamics/DynamicsIntegrationGlobalEvents.cs new file mode 100644 index 0000000..1aa3aa5 --- /dev/null +++ b/src/Kentico.Xperience.CRM.Dynamics/DynamicsIntegrationGlobalEvents.cs @@ -0,0 +1,80 @@ +using CMS; +using CMS.Base; +using CMS.Core; +using CMS.DataEngine; +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; +using Kentico.Xperience.CRM.Dynamics.Workers; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +[assembly: RegisterModule(typeof(DynamicsIntegrationGlobalEvents))] + +namespace Kentico.Xperience.CRM.Dynamics; + +/// +/// Module with BizFormItem and ContactInfo event handlers for Dynamics integration +/// +internal class DynamicsIntegrationGlobalEvents : Module +{ + public DynamicsIntegrationGlobalEvents() : base(nameof(DynamicsIntegrationGlobalEvents)) + { + } + + private ILogger logger = null!; + private ICRMModuleInstaller? installer; + + protected override void OnInit(ModuleInitParameters parameters) + { + base.OnInit(parameters); + + var services = parameters.Services; + + logger = services.GetRequiredService>(); + installer = services.GetRequiredService(); + + ApplicationEvents.Initialized.Execute += InitializeModule; + + BizFormItemEvents.Insert.After += SynchronizeBizFormLead; + BizFormItemEvents.Update.After += SynchronizeBizFormLead; + + 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(); + try + { + using (var serviceScope = Service.Resolve().CreateScope()) + { + var settings = serviceScope.ServiceProvider.GetRequiredService>().Value; + if (!settings.FormLeadsEnabled) return; + + var leadsIntegrationService = serviceScope.ServiceProvider + .GetRequiredService(); + + leadsIntegrationService.SynchronizeLeadAsync(e.Item).ConfigureAwait(false).GetAwaiter().GetResult(); + } + } + 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/DynamicsServiceCollectionExtensions.cs b/src/Kentico.Xperience.CRM.Dynamics/DynamicsServiceCollectionExtensions.cs index ff76f96..db17453 100644 --- a/src/Kentico.Xperience.CRM.Dynamics/DynamicsServiceCollectionExtensions.cs +++ b/src/Kentico.Xperience.CRM.Dynamics/DynamicsServiceCollectionExtensions.cs @@ -1,9 +1,11 @@ using Kentico.Xperience.CRM.Common; -using Kentico.Xperience.CRM.Common.Configuration; +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; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Microsoft.PowerPlatform.Dataverse.Client; @@ -19,14 +21,26 @@ public static class DynamicsServiceCollectionExtensions /// /// /// - public static IServiceCollection AddDynamicsCrmLeadsIntegration(this IServiceCollection serviceCollection, - Action formsConfig, - IConfiguration configuration) + public static IServiceCollection AddKenticoCRMDynamics(this IServiceCollection serviceCollection, + Action formsConfig, + IConfiguration? configuration = null) { - serviceCollection.AddKenticoCrmCommonIntegration(formsConfig); + serviceCollection.AddKenticoCrmCommonFormLeadsIntegration(); + var mappingBuilder = new DynamicsBizFormsMappingBuilder(serviceCollection); + formsConfig(mappingBuilder); + serviceCollection.TryAddSingleton(_ => mappingBuilder.Build()); - serviceCollection.AddOptions().Bind(configuration); - serviceCollection.AddSingleton(GetCrmServiceClient); + if (configuration is null) + { + serviceCollection.AddOptions() + .Configure(ConfigureWithCMSSettings); + } + else + { + serviceCollection.AddOptions().Bind(configuration); + } + + serviceCollection.TryAddScoped(GetCrmServiceClient); serviceCollection.AddScoped(); return serviceCollection; } @@ -39,10 +53,10 @@ 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>().Value; + var logger = serviceProvider.GetRequiredService>(); - if (settings.ApiConfig?.IsValid() is not true) + if (!settings.ApiConfig.IsValid()) { throw new InvalidOperationException("Missing API setting"); } @@ -53,4 +67,16 @@ private static ServiceClient GetCrmServiceClient(IServiceProvider serviceProvide return new ServiceClient(connectionString, logger); } + + private static void ConfigureWithCMSSettings(DynamicsIntegrationSettings settings, ICRMSettingsService settingsService) + { + var settingsInfo = settingsService.GetSettings(CRMType.Dynamics); + settings.FormLeadsEnabled = settingsInfo?.CRMIntegrationSettingsFormsEnabled ?? false; + + settings.IgnoreExistingRecords = settingsInfo?.CRMIntegrationSettingsIgnoreExistingRecords ?? false; + + 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/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 0cfa6e9..5228fe7 100644 --- a/src/Kentico.Xperience.CRM.Dynamics/Services/DynamicsLeadsIntegrationService.cs +++ b/src/Kentico.Xperience.CRM.Dynamics/Services/DynamicsLeadsIntegrationService.cs @@ -1,11 +1,12 @@ 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; +using Microsoft.Extensions.Options; using Microsoft.PowerPlatform.Dataverse.Client; using Microsoft.Xrm.Sdk; using Microsoft.Xrm.Sdk.Query; @@ -16,112 +17,188 @@ 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> formsConverters; public DynamicsLeadsIntegrationService( - DynamicsBizFormsMappingConfiguration bizFormMappingConfig, ILeadsIntegrationValidationService validationService, + DynamicsBizFormsMappingConfiguration bizFormMappingConfig, + ILeadsIntegrationValidationService validationService, ServiceClient serviceClient, ILogger logger, - IFailedSyncItemService failedSyncItemService) - : base(bizFormMappingConfig, validationService, logger) + ICRMSyncItemService syncItemService, + IFailedSyncItemService failedSyncItemService, + IOptionsSnapshot settings, + IEnumerable> formsConverters) { this.bizFormMappingConfig = bizFormMappingConfig; + this.validationService = validationService; this.serviceClient = serviceClient; this.logger = logger; + this.syncItemService = syncItemService; this.failedSyncItemService = failedSyncItemService; + this.settings = settings; + this.formsConverters = formsConverters; } - protected override async Task CreateLeadAsync(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) { - try - { - var leadEntity = new Lead(); - MapLead(bizFormItem, leadEntity, fieldMappings); - - if (leadEntity.Subject is null) - { - leadEntity.Subject = $"Form {bizFormItem.BizFormInfo.FormDisplayName} - ID: {bizFormItem.ItemID}"; - } - - if (bizFormMappingConfig.ExternalIdFieldName is { Length: > 0 } externalIdFieldName) - { - leadEntity[externalIdFieldName] = FormatExternalId(bizFormItem); - } + var leadConverters = Enumerable.Empty>(); + var leadMapping = Enumerable.Empty(); - await serviceClient.CreateAsync(leadEntity); - return true; - } - catch (FaultException e) + if (bizFormMappingConfig.FormsConverters.TryGetValue(bizFormItem.BizFormClassName.ToLowerInvariant(), + out var formConverters)) { - logger.LogError(e, "Create lead failed - api error: {ApiResult}", e.Detail); - failedSyncItemService.LogFailedLeadItem(bizFormItem, CRMType.Dynamics); + leadConverters = formsConverters.Where(c => formConverters.Contains(c.GetType())); } - catch (Exception e) when (e.InnerException is FaultException ie) + + if (bizFormMappingConfig.FormsMappings.TryGetValue(bizFormItem.BizFormClassName.ToLowerInvariant(), + out var formMapping)) { - logger.LogError(e, "Create lead failed - api error: {ApiResult}", ie.Detail); - failedSyncItemService.LogFailedLeadItem(bizFormItem, CRMType.Dynamics); + leadMapping = formMapping; } - catch (Exception e) + + if (leadConverters.Any() || leadMapping.Any()) { - logger.LogError(e, "Create lead failed - unknown api error"); - failedSyncItemService.LogFailedLeadItem(bizFormItem, CRMType.Dynamics); - } + if (!await validationService.ValidateFormItem(bizFormItem)) + { + logger.LogInformation("BizForm item {ItemID} for {BizFormDisplayName} refused by validation", + bizFormItem.ItemID, bizFormItem.BizFormInfo.FormDisplayName); + return; + } - return false; + await SynchronizeLeadAsync(bizFormItem, leadMapping, leadConverters); + } } - protected override async Task UpdateLeadAsync(BizFormItem bizFormItem, - IEnumerable fieldMappings) + private async Task SynchronizeLeadAsync(BizFormItem bizFormItem, + IEnumerable fieldMappings, IEnumerable> converters) { try { - var leadEntity = await GetLeadByExternalId(FormatExternalId(bizFormItem)); - if (leadEntity is not null) + var syncItem = await syncItemService.GetFormLeadSyncItem(bizFormItem, CRMType.Dynamics); + + if (syncItem is null) { - MapLead(bizFormItem, leadEntity, fieldMappings); - await serviceClient.UpdateAsync(leadEntity); - failedSyncItemService.DeleteFailedSyncItem(CRMType.Dynamics, bizFormItem.BizFormClassName, bizFormItem.ItemID); - return true; + await UpdateByEmailOrCreate(bizFormItem, fieldMappings, converters); } else { - if (await CreateLeadAsync(bizFormItem, fieldMappings)) + var existingLead = await GetLeadById(Guid.Parse(syncItem.CRMSyncItemCRMID)); + if (existingLead is null) { - failedSyncItemService.DeleteFailedSyncItem(CRMType.Dynamics, bizFormItem.BizFormClassName, bizFormItem.ItemID); - return true; + await UpdateByEmailOrCreate(bizFormItem, fieldMappings, converters); + } + else if (!settings.Value.IgnoreExistingRecords) + { + await UpdateLeadAsync(existingLead, bizFormItem, fieldMappings, converters); + } + else + { + logger.LogInformation("BizForm item {ItemID} for {BizFormDisplayName} ignored", + bizFormItem.ItemID, bizFormItem.BizFormInfo.FormDisplayName); } - - return false; } } catch (FaultException e) { - logger.LogError(e, "Update 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, "Update 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, "Update lead failed - unknown api error"); + logger.LogError(e, "Sync lead failed - unknown api error"); failedSyncItemService.LogFailedLeadItem(bizFormItem, CRMType.Dynamics); } + } + + private async Task UpdateByEmailOrCreate(BizFormItem bizFormItem, + IEnumerable fieldMappings, IEnumerable> converters) + { + Lead? existingLead = null; + var tmpLead = new Lead(); + await MapLead(bizFormItem, tmpLead, fieldMappings, converters); + + if (!string.IsNullOrWhiteSpace(tmpLead.EMailAddress1)) + { + existingLead = await GetLeadByEmail(tmpLead.EMailAddress1); + } + + if (existingLead is null) + { + await CreateLeadAsync(bizFormItem, fieldMappings, converters); + } + else if (!settings.Value.IgnoreExistingRecords) + { + 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, + IEnumerable fieldMappings, IEnumerable> converters) + { + var leadEntity = new Lead(); + await MapLead(bizFormItem, leadEntity, fieldMappings, converters); + + if (leadEntity.Subject is null) + { + leadEntity.Subject = $"Form {bizFormItem.BizFormInfo.FormDisplayName} - ID: {bizFormItem.ItemID}"; + } + + var leadId = await serviceClient.CreateAsync(leadEntity); - return false; + await syncItemService.LogFormLeadCreateItem(bizFormItem, leadId.ToString(), CRMType.Dynamics); + failedSyncItemService.DeleteFailedSyncItem(CRMType.Dynamics, bizFormItem.BizFormClassName, + bizFormItem.ItemID); } - protected virtual void MapLead(BizFormItem bizFormItem, Lead leadEntity, - IEnumerable fieldMappings) + private async Task UpdateLeadAsync(Lead leadEntity, BizFormItem bizFormItem, + IEnumerable fieldMappings, IEnumerable> converters) { + await MapLead(bizFormItem, leadEntity, fieldMappings, converters); + + if (leadEntity.Subject is null) + { + leadEntity.Subject = $"Form {bizFormItem.BizFormInfo.FormDisplayName} - ID: {bizFormItem.ItemID}"; + } + + await serviceClient.UpdateAsync(leadEntity); + + await syncItemService.LogFormLeadUpdateItem(bizFormItem, leadEntity.LeadId.ToString()!, CRMType.Dynamics); + failedSyncItemService.DeleteFailedSyncItem(CRMType.Dynamics, bizFormItem.BizFormClassName, + bizFormItem.ItemID); + } + + 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); @@ -129,18 +206,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(fieldMappings), + 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.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.Dynamics/packages.lock.json b/src/Kentico.Xperience.CRM.Dynamics/packages.lock.json index ae319e1..ecace36 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", @@ -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", @@ -77,6 +75,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 +116,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.746", + "contentHash": "9InxDYXIEnCnWQN2eCLCNL3QGyhmV0+vn12QlVSPig5m0f6XisAM9pq+TWB8aUtxXm1zm39/rOC8Ed7eZaEYew==", + "dependencies": { + "AngleSharp": "[0.17.1]", + "AngleSharp.Css": "[0.17.0]", + "System.Collections.Immutable": "7.0.0" + } + }, + "Kentico.Aira.Client": { + "type": "Transitive", + "resolved": "1.0.23", + "contentHash": "16jt+oHW6Fa6fDmJlJBTJuMK+A6nwXDX7IcYygYFzq01Sek43oQB2wjhXPRhntsvFYhAbWcBwXPzn0ALlMggSQ==", + "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 +151,14 @@ "MimeKit": "4.2.0" } }, + "Microsoft.AspNetCore.SpaServices.Extensions": { + "type": "Transitive", + "resolved": "6.0.24", + "contentHash": "onDzS4i2iLMVHtqnTNEFuik2wqarpi88e8Bz/K6FOHwwj+z9kIvcjdmV8qLpC8effWiQot/JHiGzEtsZA/8yWQ==", + "dependencies": { + "Microsoft.Extensions.FileProviders.Physical": "6.0.0" + } + }, "Microsoft.Bcl.AsyncInterfaces": { "type": "Transitive", "resolved": "5.0.0", @@ -130,11 +171,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", @@ -150,8 +191,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", @@ -220,6 +261,14 @@ "Microsoft.Extensions.Primitives": "6.0.0" } }, + "Microsoft.Extensions.FileProviders.Embedded": { + "type": "Transitive", + "resolved": "6.0.24", + "contentHash": "HvTvzEkqYwDykjaekrpntZkxetMQ5XinIRYoRKftCqttmABoSfsOpsxCh0eIw9w4H+K8xHN7nqvCWFPP8feT4w==", + "dependencies": { + "Microsoft.Extensions.FileProviders.Abstractions": "6.0.0" + } + }, "Microsoft.Extensions.FileProviders.Physical": { "type": "Transitive", "resolved": "6.0.0", @@ -247,39 +296,41 @@ }, "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": { "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", - "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": { @@ -323,42 +374,44 @@ }, "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" } }, "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 +434,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 +613,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 +693,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": { @@ -666,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", @@ -1113,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": { @@ -1253,7 +1323,33 @@ "kentico.xperience.crm.common": { "type": "Project", "dependencies": { - "Kentico.Xperience.Core": "[27.0.1, )" + "Kentico.Xperience.Admin": "[28.0.0, )", + "Kentico.Xperience.Core": "[28.0.0, )" + } + }, + "Kentico.Xperience.Admin": { + "type": "CentralTransitive", + "requested": "[28.0.0, )", + "resolved": "28.0.0", + "contentHash": "G54CsUaiegQF1NS3+7p6uyWObSdBVbqfZCDlyzEQSAypZS8Gy1bc8p+SKTpjyNFyj+DpQcX9LJfbam2hUVNOUg==", + "dependencies": { + "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": "[28.0.0, )", + "resolved": "28.0.0", + "contentHash": "R54QHACtE8rfGtHKKFpSvY7LKI1IqqALe70QGYz1u6ZyZ23w0O7R9UZH5bgW/Ww0d13IgGQBc3ULaMel9bxJXg==", + "dependencies": { + "CommandLineParser": "2.9.1", + "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.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/Admin/SalesForceIntegrationSettingsEdit.cs b/src/Kentico.Xperience.CRM.SalesForce/Admin/SalesForceIntegrationSettingsEdit.cs new file mode 100644 index 0000000..484fc67 --- /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(CRMIntegrationSettingsApplication), + slug: "salesforce-settings-edit", + uiPageType: typeof(SalesForceIntegrationSettingsEdit), + name: "Salesforce CRM", + templateName: TemplateNames.EDIT, + order: 200)] + +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/Configuration/SalesForceBizFormsMappingBuilder.cs b/src/Kentico.Xperience.CRM.SalesForce/Configuration/SalesForceBizFormsMappingBuilder.cs new file mode 100644 index 0000000..ec0dc9f --- /dev/null +++ b/src/Kentico.Xperience.CRM.SalesForce/Configuration/SalesForceBizFormsMappingBuilder.cs @@ -0,0 +1,110 @@ +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; + +/// +/// 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; + } + + /// + /// Adds Form with mapping + /// + /// + /// + /// + /// + 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 + /// + /// + /// + 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; + } + + /// + /// 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) }; + } + + 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() + { + return new SalesForceBizFormsMappingConfiguration + { + 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.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 new file mode 100644 index 0000000..1fc2fc5 --- /dev/null +++ b/src/Kentico.Xperience.CRM.SalesForce/Converters/FormContactMappingToLeadConverter.cs @@ -0,0 +1,112 @@ +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; +using SalesForce.OpenApi; + +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; + private readonly IInfoByIdProvider countries; + private readonly IInfoByIdProvider states; + private readonly IConversionService conversion; + + 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) + { + 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; + } + + 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); + } +} \ No newline at end of file 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..e0747b8 --- /dev/null +++ b/src/Kentico.Xperience.CRM.SalesForce/Models/QueryResult.cs @@ -0,0 +1,12 @@ +using System.Text.Json.Serialization; + +namespace Kentico.Xperience.CRM.SalesForce.Models; + +/// +/// Query result +/// +/// +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..86d0c6b --- /dev/null +++ b/src/Kentico.Xperience.CRM.SalesForce/Models/QueryResultBase.cs @@ -0,0 +1,13 @@ +using System.Text.Json.Serialization; + +namespace Kentico.Xperience.CRM.SalesForce.Models; + +/// +/// Base model for query result +/// +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/SalesForceBizFormGlobalEvents.cs b/src/Kentico.Xperience.CRM.SalesForce/SalesForceBizFormGlobalEvents.cs deleted file mode 100644 index 86b9ec6..0000000 --- a/src/Kentico.Xperience.CRM.SalesForce/SalesForceBizFormGlobalEvents.cs +++ /dev/null @@ -1,92 +0,0 @@ -using CMS; -using CMS.Base; -using CMS.Core; -using CMS.DataEngine; -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.SalesForce; -using Kentico.Xperience.CRM.SalesForce.Configuration; -using Kentico.Xperience.CRM.SalesForce.Services; -using Kentico.Xperience.CRM.SalesForce.Workers; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; - -[assembly: RegisterModule(typeof(SalesForceBizFormGlobalEvents))] - -namespace Kentico.Xperience.CRM.SalesForce; - -/// -/// Module with bizformitem event handlers for SalesForce Sales integration -/// -internal class SalesForceBizFormGlobalEvents : Module -{ - private ILogger logger = null!; - - public SalesForceBizFormGlobalEvents() : base(nameof(SalesForceBizFormGlobalEvents)) - { - } - - protected override void OnInit() - { - base.OnInit(); - - BizFormItemEvents.Insert.After += BizFormInserted; - BizFormItemEvents.Update.After += BizFormUpdated; - logger = Service.Resolve>(); - Service.Resolve().Install(); - ThreadWorker.Current.EnsureRunningThread(); - } - - private void BizFormInserted(object? sender, BizFormItemEventArgs e) - { - var failedSyncItemsService = Service.Resolve(); - try - { - var settings = Service.Resolve>().Value; - 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>().Value; - 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(); - } - } - catch (Exception exception) - { - logger.LogError(exception, "Error occured during updating lead"); - } - } -} \ No newline at end of file diff --git a/src/Kentico.Xperience.CRM.SalesForce/SalesForceIntegrationGlobalEvents.cs b/src/Kentico.Xperience.CRM.SalesForce/SalesForceIntegrationGlobalEvents.cs new file mode 100644 index 0000000..b7e7755 --- /dev/null +++ b/src/Kentico.Xperience.CRM.SalesForce/SalesForceIntegrationGlobalEvents.cs @@ -0,0 +1,82 @@ +using CMS; +using CMS.Base; +using CMS.Core; +using CMS.DataEngine; +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.SalesForce; +using Kentico.Xperience.CRM.SalesForce.Configuration; +using Kentico.Xperience.CRM.SalesForce.Services; +using Kentico.Xperience.CRM.SalesForce.Workers; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; + +[assembly: RegisterModule(typeof(SalesForceIntegrationGlobalEvents))] + +namespace Kentico.Xperience.CRM.SalesForce; + +/// +/// Module with BizFormItem and ContactInfo event handlers for SalesForce integration +/// +internal class SalesForceIntegrationGlobalEvents : Module +{ + private ILogger logger = null!; + private ICRMModuleInstaller? installer; + + public SalesForceIntegrationGlobalEvents() : base(nameof(SalesForceIntegrationGlobalEvents)) + { + } + + protected override void OnInit(ModuleInitParameters parameters) + { + base.OnInit(); + + var services = parameters.Services; + + logger = services.GetRequiredService>(); + 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(); + try + { + using (var serviceScope = Service.Resolve().CreateScope()) + { + var settings = serviceScope.ServiceProvider.GetRequiredService>().Value; + if (!settings.FormLeadsEnabled) return; + + var leadsIntegrationService = serviceScope.ServiceProvider + .GetRequiredService(); + + 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/SalesForceServiceCollectionsExtensions.cs b/src/Kentico.Xperience.CRM.SalesForce/SalesForceServiceCollectionsExtensions.cs index 0e7452b..3641868 100644 --- a/src/Kentico.Xperience.CRM.SalesForce/SalesForceServiceCollectionsExtensions.cs +++ b/src/Kentico.Xperience.CRM.SalesForce/SalesForceServiceCollectionsExtensions.cs @@ -1,12 +1,17 @@ -using Kentico.Xperience.CRM.Common; -using Kentico.Xperience.CRM.Common.Configuration; +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; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Options; using System.Globalization; namespace Kentico.Xperience.CRM.SalesForce; + public static class SalesForceServiceCollectionsExtensions { /// @@ -17,23 +22,49 @@ public static class SalesForceServiceCollectionsExtensions /// /// /// - public static IServiceCollection AddSalesForceCrmLeadsIntegration(this IServiceCollection serviceCollection, - Action formsConfig, - IConfiguration configuration) + public static IServiceCollection AddKenticoCRMSalesForce(this IServiceCollection serviceCollection, + Action formsConfig, + IConfiguration? configuration = null) { - serviceCollection.AddKenticoCrmCommonIntegration(formsConfig); - serviceCollection.AddOptions().Bind(configuration); + serviceCollection.AddKenticoCrmCommonFormLeadsIntegration(); + + var mappingBuilder = new SalesForceBizFormsMappingBuilder(serviceCollection); + formsConfig(mappingBuilder); + serviceCollection.TryAddSingleton( + _ => mappingBuilder.Build()); + + if (configuration is null) + { + serviceCollection.AddOptions() + .Configure(ConfigureWithCMSSettings); + } + else + { + serviceCollection.AddOptions().Bind(configuration); + } + + AddSalesForceCommonIntegration(serviceCollection); + + serviceCollection.AddScoped(); + return serviceCollection; + } + private static void AddSalesForceCommonIntegration(IServiceCollection serviceCollection) + { // default cache for token management 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"; @@ -43,20 +74,30 @@ public static IServiceCollection AddSalesForceCrmLeadsIntegration(this IServiceC }); //add http client for salesforce api - serviceCollection.AddHttpClient(client => - { - var apiConfig = configuration.Get()?.ApiConfig; + serviceCollection.AddHttpClient((provider, client) => + { + //cannot use IOptionsSnapshot, so changes in CMS settings needs restarting app to apply immediately + var settings = provider.GetRequiredService>().CurrentValue; + + if (!settings.ApiConfig.IsValid()) + throw new InvalidOperationException("Missing API settings"); - if (apiConfig?.IsValid() is not true) - throw new InvalidOperationException("Missing API settings"); + 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"); + } - string apiVersion = apiConfig.ApiVersion.ToString("F1", CultureInfo.InvariantCulture); - client.BaseAddress = new Uri($"{apiConfig.SalesForceUrl?.TrimEnd('/')}/services/data/v{apiVersion}/"); - }) - .AddClientCredentialsTokenHandler("salesforce.api.client"); + private static void ConfigureWithCMSSettings(SalesForceIntegrationSettings settings, ICRMSettingsService settingsService) + { + var settingsInfo = settingsService.GetSettings(CRMType.SalesForce); + settings.FormLeadsEnabled = settingsInfo?.CRMIntegrationSettingsFormsEnabled ?? false; + settings.IgnoreExistingRecords = settingsInfo?.CRMIntegrationSettingsIgnoreExistingRecords ?? false; - serviceCollection.AddScoped(); - return serviceCollection; + 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/Services/ISalesForceApiService.cs b/src/Kentico.Xperience.CRM.SalesForce/Services/ISalesForceApiService.cs index fb46de4..3dff3ba 100644 --- a/src/Kentico.Xperience.CRM.SalesForce/Services/ISalesForceApiService.cs +++ b/src/Kentico.Xperience.CRM.SalesForce/Services/ISalesForceApiService.cs @@ -29,4 +29,19 @@ public interface ISalesForceApiService /// External ID value /// Task GetLeadIdByExternalId(string fieldName, string externalId); + + /// + /// Get Lead by primary Id + /// + /// + /// + /// + Task GetLeadById(string id, string? fields = null); + + /// + /// Get Lead by 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 1469124..7368fc7 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; @@ -7,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; @@ -15,18 +17,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); } @@ -50,10 +52,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); @@ -74,4 +76,28 @@ 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, string? fields = null) + => await apiClient.LeadGET2Async(id, fields); + + public async Task GetLeadByEmail(string email) + { + 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='{HttpUtility.UrlEncode(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 3afdd18..31396a2 100644 --- a/src/Kentico.Xperience.CRM.SalesForce/Services/SalesForceLeadsIntegrationService.cs +++ b/src/Kentico.Xperience.CRM.SalesForce/Services/SalesForceLeadsIntegrationService.cs @@ -1,8 +1,8 @@ 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.SalesForce.Configuration; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; @@ -11,114 +11,187 @@ 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, ILeadsIntegrationValidationService validationService, ISalesForceApiService apiService, ILogger logger, - IFailedSyncItemService failedSyncItemService) - : base(bizFormMappingConfig, validationService, logger) + ICRMSyncItemService syncItemService, + IFailedSyncItemService failedSyncItemService, + 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 CreateLeadAsync(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) { - try - { - var lead = new LeadSObject(); - MapLead(bizFormItem, lead, fieldMappings); + var leadConverters = Enumerable.Empty>(); + var leadMapping = Enumerable.Empty(); - 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; - } - catch (ApiException> e) + if (bizFormMappingConfig.FormsConverters.TryGetValue(bizFormItem.BizFormClassName.ToLowerInvariant(), + out var formConverters)) { - logger.LogError(e, "Create lead failed - api error: {ApiResult}", JsonSerializer.Serialize(e.Result)); - failedSyncItemService.LogFailedLeadItem(bizFormItem, CRMType.SalesForce); + leadConverters = formsConverters.Where(c => formConverters.Contains(c.GetType())); } - catch (ApiException> e) + + if (bizFormMappingConfig.FormsMappings.TryGetValue(bizFormItem.BizFormClassName.ToLowerInvariant(), + out var formMapping)) { - logger.LogError(e, "Create lead failed - api error: {ApiResult}", JsonSerializer.Serialize(e.Result)); - failedSyncItemService.LogFailedLeadItem(bizFormItem, CRMType.SalesForce); + leadMapping = formMapping; } - catch (ApiException e) + + if (leadConverters.Any() || leadMapping.Any()) { - logger.LogError(e, "Create lead failed - unexpected api error"); - failedSyncItemService.LogFailedLeadItem(bizFormItem, CRMType.SalesForce); - } + if (!await validationService.ValidateFormItem(bizFormItem)) + { + logger.LogInformation("BizForm item {ItemID} for {BizFormDisplayName} refused by validation", + bizFormItem.ItemID, bizFormItem.BizFormInfo.FormDisplayName); + return; + } - return false; + await SynchronizeLeadAsync(bizFormItem, leadMapping, leadConverters); + } } - protected override async Task UpdateLeadAsync(BizFormItem bizFormItem, - IEnumerable fieldMappings) + private async Task SynchronizeLeadAsync(BizFormItem bizFormItem, + IEnumerable fieldMappings, + IEnumerable> converters) { try { - string? leadId = string.IsNullOrWhiteSpace(bizFormMappingConfig.ExternalIdFieldName) ? null : - await apiService.GetLeadIdByExternalId(bizFormMappingConfig.ExternalIdFieldName!, - FormatExternalId(bizFormItem)); + var syncItem = await syncItemService.GetFormLeadSyncItem(bizFormItem, CRMType.SalesForce); - if (leadId is not null) + 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, converters); } else { - if (await CreateLeadAsync(bizFormItem, fieldMappings)) + var existingLead = await apiService.GetLeadById(syncItem.CRMSyncItemCRMID, nameof(LeadSObject.Id)); + if (existingLead is null) { - failedSyncItemService.DeleteFailedSyncItem(CRMType.SalesForce, bizFormItem.BizFormClassName, bizFormItem.ItemID); - return true; + await UpdateByEmailOrCreate(bizFormItem, fieldMappings, converters); + } + else if (!settings.Value.IgnoreExistingRecords) + { + await UpdateLeadAsync(existingLead.Id!, bizFormItem, fieldMappings, converters); + } + else + { + logger.LogInformation("BizForm item {ItemID} for {BizFormDisplayName} ignored", + bizFormItem.ItemID, bizFormItem.BizFormInfo.FormDisplayName); } - - return false; } } 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); } + } + + private async Task UpdateByEmailOrCreate(BizFormItem bizFormItem, IEnumerable fieldMappings, + IEnumerable> converters) + { + string? existingLeadId = null; - return false; + var tmpLead = new LeadSObject(); + await MapLead(bizFormItem, tmpLead, fieldMappings, converters); + + if (!string.IsNullOrWhiteSpace(tmpLead.Email)) + { + existingLeadId = await apiService.GetLeadByEmail(tmpLead.Email); + } + + if (existingLeadId is null) + { + await CreateLeadAsync(bizFormItem, fieldMappings, converters); + } + else if (!settings.Value.IgnoreExistingRecords) + { + await UpdateLeadAsync(existingLeadId, bizFormItem, fieldMappings, converters); + } + else + { + logger.LogInformation("BizForm item {ItemID} for {BizFormDisplayName} ignored", + bizFormItem.ItemID, bizFormItem.BizFormInfo.FormDisplayName); + } } - protected virtual void MapLead(BizFormItem bizFormItem, LeadSObject lead, - IEnumerable fieldMappings) + private async Task CreateLeadAsync(BizFormItem bizFormItem, IEnumerable fieldMappings, + IEnumerable> converters) { + var lead = new LeadSObject(); + 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 + + var result = await apiService.CreateLeadAsync(lead); + + await syncItemService.LogFormLeadCreateItem(bizFormItem, result.Id!, CRMType.SalesForce); + failedSyncItemService.DeleteFailedSyncItem(CRMType.SalesForce, bizFormItem.BizFormClassName, + bizFormItem.ItemID); + } + + private async Task UpdateLeadAsync(string leadId, BizFormItem bizFormItem, + IEnumerable fieldMappings, + IEnumerable> converters) + { + var lead = new LeadSObject(); + await MapLead(bizFormItem, lead, fieldMappings, converters); + + lead.LeadSource ??= $"Form {bizFormItem.BizFormInfo.FormDisplayName} - ID: {bizFormItem.ItemID}"; + + await apiService.UpdateLeadAsync(leadId, lead); + + await syncItemService.LogFormLeadUpdateItem(bizFormItem, leadId, CRMType.SalesForce); + failedSyncItemService.DeleteFailedSyncItem(CRMType.SalesForce, bizFormItem.BizFormClassName, + bizFormItem.ItemID); + } + + 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); @@ -126,7 +199,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(fieldMappings), + fieldMapping.CRMFieldMapping.GetType(), "Unsupported mapping") }; } } 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; diff --git a/src/Kentico.Xperience.CRM.SalesForce/packages.lock.json b/src/Kentico.Xperience.CRM.SalesForce/packages.lock.json index 0f65efa..3fa0891 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" } }, @@ -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", @@ -43,19 +43,19 @@ }, "NSwag.ApiDescription.Client": { "type": "Direct", - "requested": "[13.18.2, )", - "resolved": "13.18.2", - "contentHash": "uViMdjUscfeqrlDY4q9O2a0t2cMqsOx1kdX9WLyQjTXea1xjAHgQFCYlgE6ibwkYcUDJDlwEYUzvsJelL6SY3g==", + "requested": "[13.20.0, )", + "resolved": "13.20.0", + "contentHash": "ylDDEqtlSrTUH9oSPttT+ZoRCTusx8U8kbmqjfNuujvfLQz6KGPctkEhXLZYn4Bdy3v5z+a2koPN+DbeyyCxXA==", "dependencies": { "Microsoft.Extensions.ApiDescription.Client": "6.0.3", - "NSwag.MSBuild": "13.18.2" + "NSwag.MSBuild": "13.20.0" } }, "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", @@ -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,18 +107,45 @@ "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", - "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": { + "type": "Transitive", + "resolved": "8.0.746", + "contentHash": "9InxDYXIEnCnWQN2eCLCNL3QGyhmV0+vn12QlVSPig5m0f6XisAM9pq+TWB8aUtxXm1zm39/rOC8Ed7eZaEYew==", + "dependencies": { + "AngleSharp": "[0.17.1]", + "AngleSharp.Css": "[0.17.0]", + "System.Collections.Immutable": "7.0.0" + } + }, + "Kentico.Aira.Client": { + "type": "Transitive", + "resolved": "1.0.23", + "contentHash": "16jt+oHW6Fa6fDmJlJBTJuMK+A6nwXDX7IcYygYFzq01Sek43oQB2wjhXPRhntsvFYhAbWcBwXPzn0ALlMggSQ==", + "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": { @@ -129,6 +164,14 @@ "Microsoft.IdentityModel.Protocols.OpenIdConnect": "6.10.0" } }, + "Microsoft.AspNetCore.SpaServices.Extensions": { + "type": "Transitive", + "resolved": "6.0.24", + "contentHash": "onDzS4i2iLMVHtqnTNEFuik2wqarpi88e8Bz/K6FOHwwj+z9kIvcjdmV8qLpC8effWiQot/JHiGzEtsZA/8yWQ==", + "dependencies": { + "Microsoft.Extensions.FileProviders.Physical": "6.0.0" + } + }, "Microsoft.Bcl.AsyncInterfaces": { "type": "Transitive", "resolved": "1.1.1", @@ -141,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", @@ -161,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", @@ -236,6 +279,14 @@ "Microsoft.Extensions.Primitives": "6.0.0" } }, + "Microsoft.Extensions.FileProviders.Embedded": { + "type": "Transitive", + "resolved": "6.0.24", + "contentHash": "HvTvzEkqYwDykjaekrpntZkxetMQ5XinIRYoRKftCqttmABoSfsOpsxCh0eIw9w4H+K8xHN7nqvCWFPP8feT4w==", + "dependencies": { + "Microsoft.Extensions.FileProviders.Abstractions": "6.0.0" + } + }, "Microsoft.Extensions.FileProviders.Physical": { "type": "Transitive", "resolved": "6.0.0", @@ -274,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", @@ -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" } }, @@ -445,8 +497,8 @@ }, "NSwag.MSBuild": { "type": "Transitive", - "resolved": "13.18.2", - "contentHash": "SRQ3mONkfbJWhZxA1yOFMJMXavUvnXwcoYU23qoVqSEY2G+y8jZuq9ErWm76JT0Kn5/Ml5UhG1FWmLhkqd4/+A==" + "resolved": "13.20.0", + "contentHash": "3Z+7gCh+hb9DbK7O0iRgt2iDLOeZAVMpGyJ4UQUs/yUClQSNdA+QWmStlnSyx+jCTQX+tAcLSN6H5kw5Q6hCLA==" }, "System.Buffers": { "type": "Transitive", @@ -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,7 +685,33 @@ "kentico.xperience.crm.common": { "type": "Project", "dependencies": { - "Kentico.Xperience.Core": "[27.0.1, )" + "Kentico.Xperience.Admin": "[28.0.0, )", + "Kentico.Xperience.Core": "[28.0.0, )" + } + }, + "Kentico.Xperience.Admin": { + "type": "CentralTransitive", + "requested": "[28.0.0, )", + "resolved": "28.0.0", + "contentHash": "G54CsUaiegQF1NS3+7p6uyWObSdBVbqfZCDlyzEQSAypZS8Gy1bc8p+SKTpjyNFyj+DpQcX9LJfbam2hUVNOUg==", + "dependencies": { + "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": "[28.0.0, )", + "resolved": "28.0.0", + "contentHash": "R54QHACtE8rfGtHKKFpSvY7LKI1IqqALe70QGYz1u6ZyZ23w0O7R9UZH5bgW/Ww0d13IgGQBc3ULaMel9bxJXg==", + "dependencies": { + "CommandLineParser": "2.9.1", + "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/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",