Skip to content

Commit ccd4967

Browse files
authored
Merge pull request #42 from umbraco/v10/feature/activecampaign-integration
ActiveCampaign - Contacts Workflow
2 parents 0211c5e + 3b2e656 commit ccd4967

39 files changed

+1117
-0
lines changed
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
trigger:
2+
- main-v10
3+
4+
pool:
5+
vmImage: 'windows-latest'
6+
7+
variables:
8+
projectName: 'Umbraco.Forms.Integrations.Crm.ActiveCampaign'
9+
project: 'src/$(projectName)/$(projectName).csproj'
10+
buildPlatform: 'Any CPU'
11+
buildConfiguration: 'Release'
12+
13+
steps:
14+
- task: NuGetToolInstaller@1
15+
displayName: 'Install NuGet'
16+
17+
- task: DotNetCoreCLI@2
18+
displayName: 'NuGet Restore'
19+
inputs:
20+
command: 'restore'
21+
feedsToUse: 'select'
22+
projects: '$(project)'
23+
includeNuGetOrg: true
24+
25+
- task: VSBuild@1
26+
displayName: 'Build Project'
27+
inputs:
28+
solution: '$(project)'
29+
msbuildArgs: '/p:DeployOnBuild=true /p:WebPublishMethod=Package /p:PackageAsSingleFile=true /p:SkipInvalidConfigurations=true /p:PackageLocation="$(build.artifactStagingDirectory)"'
30+
platform: '$(buildPlatform)'
31+
configuration: '$(buildConfiguration)'
32+
33+
- task: DotNetCoreCLI@2
34+
displayName: 'Create NuGet Package'
35+
inputs:
36+
command: 'pack'
37+
arguments: '--configuration $(buildConfiguration)'
38+
packagesToPack: '$(project)'
39+
versioningScheme: 'off'
40+
41+
- task: PublishBuildArtifacts@1
42+
displayName: 'Publish Build Artifacts'
43+
inputs:
44+
PathtoPublish: '$(Build.ArtifactStagingDirectory)'
45+
ArtifactName: 'drop'
46+
publishLocation: 'Container'
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
using Microsoft.Extensions.DependencyInjection;
2+
3+
using Umbraco.Cms.Core.Composing;
4+
using Umbraco.Cms.Core.DependencyInjection;
5+
using Umbraco.Forms.Core.Providers;
6+
using Umbraco.Forms.Integrations.Crm.ActiveCampaign.Configuration;
7+
using Umbraco.Forms.Integrations.Crm.ActiveCampaign.Services;
8+
9+
namespace Umbraco.Forms.Integrations.Crm.ActiveCampaign
10+
{
11+
public class ActiveCampaignComposer : IComposer
12+
{
13+
public void Compose(IUmbracoBuilder builder)
14+
{
15+
var options = builder.Services
16+
.AddOptions<ActiveCampaignSettings>()
17+
.Bind(builder.Config.GetSection(Constants.SettingsPath));
18+
19+
builder.WithCollectionBuilder<WorkflowCollectionBuilder>()
20+
.Add<ActiveCampaignContactsWorkflow>();
21+
22+
builder.Services
23+
.AddHttpClient(Constants.HttpClient, client =>
24+
{
25+
client.BaseAddress = new Uri(
26+
$"{builder.Config.GetSection(Constants.SettingsPath)[nameof(ActiveCampaignSettings.BaseUrl)]}/api/3/");
27+
client.DefaultRequestHeaders
28+
.Add("Api-Token", builder.Config.GetSection(Constants.SettingsPath)[nameof(ActiveCampaignSettings.ApiKey)]);
29+
});
30+
31+
builder.Services.AddSingleton<IAccountService, AccountService>();
32+
builder.Services.AddSingleton<IContactService, ContactService>();
33+
}
34+
}
35+
}
Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
1+
using Microsoft.Extensions.Logging;
2+
using Microsoft.Extensions.Options;
3+
4+
using System.Text.Json;
5+
6+
using Umbraco.Forms.Core;
7+
using Umbraco.Forms.Core.Enums;
8+
using Umbraco.Forms.Core.Persistence.Dtos;
9+
using Umbraco.Forms.Integrations.Crm.ActiveCampaign.Configuration;
10+
using Umbraco.Forms.Integrations.Crm.ActiveCampaign.Models.Dtos;
11+
using Umbraco.Forms.Integrations.Crm.ActiveCampaign.Services;
12+
13+
namespace Umbraco.Forms.Integrations.Crm.ActiveCampaign
14+
{
15+
public class ActiveCampaignContactsWorkflow : WorkflowType
16+
{
17+
private readonly ActiveCampaignSettings _settings;
18+
19+
private readonly IAccountService _accountService;
20+
21+
private readonly IContactService _contactService;
22+
23+
private readonly ILogger<ActiveCampaignContactsWorkflow> _logger;
24+
25+
[Core.Attributes.Setting("Account",
26+
Description = "Please select an account",
27+
View = "~/App_Plugins/UmbracoForms.Integrations/Crm/ActiveCampaign/accountpicker.html")]
28+
public string Account { get; set; }
29+
30+
[Core.Attributes.Setting("Contact Mappings",
31+
Description = "Map contact details with form fields",
32+
View = "~/App_Plugins/UmbracoForms.Integrations/Crm/ActiveCampaign/contact-mapper.html")]
33+
public string ContactMappings { get; set; }
34+
35+
[Core.Attributes.Setting("Custom Field Mappings",
36+
Description = "Map contact custom fields with form fields",
37+
View = "~/App_Plugins/UmbracoForms.Integrations/Crm/ActiveCampaign/customfield-mapper.html")]
38+
public string CustomFieldMappings { get; set; }
39+
40+
public ActiveCampaignContactsWorkflow(IOptions<ActiveCampaignSettings> options,
41+
IAccountService accountService, IContactService contactService,
42+
ILogger<ActiveCampaignContactsWorkflow> logger)
43+
{
44+
Id = new Guid(Constants.WorkflowId);
45+
Name = "ActiveCampaign Contacts Workflow";
46+
Description = "Submit form data to ActiveCampaign Contacts";
47+
Icon = "icon-users";
48+
49+
_settings = options.Value;
50+
51+
_accountService = accountService;
52+
53+
_contactService = contactService;
54+
55+
_logger = logger;
56+
}
57+
58+
public override WorkflowExecutionStatus Execute(WorkflowExecutionContext context)
59+
{
60+
try
61+
{
62+
var mappings = JsonSerializer.Deserialize<List<ContactMappingDto>>(ContactMappings);
63+
64+
var email = context.Record.RecordFields[Guid.Parse(mappings.First(p => p.ContactField == "email").FormField.Id)]
65+
.ValuesAsString();
66+
67+
// Check if contact exists.
68+
var contacts = _contactService.Get(email).ConfigureAwait(false).GetAwaiter().GetResult();
69+
70+
var requestDto = new ContactDetailDto { Contact = Build(context.Record) };
71+
72+
if (contacts.Contacts.Count > 0) requestDto.Contact.Id = contacts.Contacts.First().Id;
73+
74+
// Set contact custom fields.
75+
if (!string.IsNullOrEmpty(CustomFieldMappings))
76+
{
77+
var customFieldMappings = JsonSerializer.Deserialize<List<CustomFieldMappingDto>>(CustomFieldMappings);
78+
79+
requestDto.Contact.FieldValues = customFieldMappings.Select(p => new CustomFieldValueDto
80+
{
81+
Field = p.CustomField.Id,
82+
Value = context.Record.RecordFields[Guid.Parse(p.FormField.Id)].ValuesAsString()
83+
}).ToList();
84+
}
85+
86+
var contactId = _contactService.CreateOrUpdate(requestDto, contacts.Contacts.Count > 0)
87+
.ConfigureAwait(false).GetAwaiter().GetResult();
88+
89+
if (string.IsNullOrEmpty(contactId))
90+
{
91+
_logger.LogError($"Failed to create/update contact: {email}");
92+
93+
return WorkflowExecutionStatus.Failed;
94+
}
95+
96+
// Associate contact with account if last one is specified.
97+
if (!string.IsNullOrEmpty(Account))
98+
{
99+
var associationResponse = _accountService.CreateAssociation(int.Parse(Account), int.Parse(contactId))
100+
.ConfigureAwait(false).GetAwaiter().GetResult();
101+
}
102+
103+
return WorkflowExecutionStatus.Completed;
104+
}
105+
catch(Exception ex)
106+
{
107+
_logger.LogError(ex, ex.Message);
108+
109+
return WorkflowExecutionStatus.Failed;
110+
}
111+
}
112+
113+
public override List<Exception> ValidateSettings()
114+
{
115+
var list = new List<Exception>();
116+
117+
if (string.IsNullOrEmpty(ContactMappings))
118+
list.Add(new Exception("Contact mappings are required."));
119+
120+
var mappings = JsonSerializer.Deserialize<List<ContactMappingDto>>(ContactMappings);
121+
foreach(var contactField in _settings.ContactFields.Where(p => p.Required))
122+
{
123+
if(!mappings.Any(p => p.ContactField == contactField.Name))
124+
{
125+
list.Add(new Exception("Invalid contact mappings. Please make sure the mandatory fields are mapped."));
126+
break;
127+
}
128+
}
129+
130+
return list;
131+
}
132+
133+
/// <summary>
134+
/// Create Contact instance using the mapped details and record fields
135+
/// </summary>
136+
/// <param name="record"></param>
137+
/// <returns></returns>
138+
private ContactDto Build(Record record)
139+
{
140+
var mappings = JsonSerializer.Deserialize<List<ContactMappingDto>>(ContactMappings);
141+
142+
return new ContactDto
143+
{
144+
Email = ReadMappingValue(record, mappings, "email"),
145+
FirstName = ReadMappingValue(record, mappings, "firstName"),
146+
LastName = ReadMappingValue(record, mappings, "lastName"),
147+
Phone = ReadMappingValue(record, mappings, "phone")
148+
};
149+
}
150+
151+
private string ReadMappingValue(Record record, List<ContactMappingDto> mappings, string name)
152+
{
153+
var mappingItem = mappings.FirstOrDefault(p => p.ContactField == name);
154+
155+
return mappingItem != null
156+
? record.RecordFields[Guid.Parse(mappingItem.FormField.Id)].ValuesAsString()
157+
: string.Empty;
158+
}
159+
}
160+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
function accountPickerController($scope, umbracoFormsIntegrationsCrmActiveCampaignResource) {
2+
3+
var vm = this;
4+
5+
umbracoFormsIntegrationsCrmActiveCampaignResource.getAccounts().then(function (response) {
6+
vm.accounts = response.accounts;
7+
8+
vm.selectedAccount = $scope.setting.value;
9+
});
10+
11+
vm.save = function () {
12+
$scope.setting.value = vm.selectedAccount.length > 0
13+
? vm.selectedAccount : "";
14+
15+
16+
}
17+
}
18+
19+
angular.module("umbraco")
20+
.controller("UmbracoForms.Integrations.Crm.ActiveCampaign.AccountPickerController", accountPickerController)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
<div ng-controller="UmbracoForms.Integrations.Crm.ActiveCampaign.AccountPickerController as vm">
2+
<div>
3+
<select ng-model="vm.selectedAccount" ng-change="vm.save()">
4+
<option value="">Select an account</option>
5+
<option ng-repeat="account in vm.accounts" value="{{ account.id }}">{{ account.name }}</option>
6+
</select>
7+
</div>
8+
9+
<!--
10+
<div ng-if="vm.contactFields.length > 0" class="alert alert-info mt2 mr3" role="alert">
11+
<span>Mandatory fields: {{ vm.requiredContactFields }}</span>
12+
</div>
13+
14+
<div class="mt2">
15+
<umb-button type="button" class="mt2"
16+
action="vm.addMapping()"
17+
label="Add mapping"
18+
disabled="vm.selectedMapping.contactField.length === 0 || vm.selectedMapping.formField.length === 0">
19+
</umb-button>
20+
</div>
21+
22+
<div class="umb-forms-mappings mt2" ng-if="vm.contactMappings.length > 0">
23+
24+
<div class="umb-forms-mapping-header">
25+
<div class="umb-forms-mapping-field -no-margin-left">Contact Field</div>
26+
<div class="umb-forms-mapping-field">Form Field</div>
27+
<div class="umb-forms-mapping-remove -no-margin-right"></div>
28+
</div>
29+
30+
<div class="umb-forms-mapping" ng-repeat="mapping in vm.contactMappings">
31+
<div class="umb-forms-mapping-field -no-margin-left">{{ mapping.contactField }}</div>
32+
<div class="umb-forms-mapping-field">{{ mapping.formField.value }}</div>
33+
<div class="umb-forms-mapping-remove -no-margin-right">
34+
<a href="" ng-click="vm.deleteMapping($index)">
35+
<i class="icon-trash"></i>
36+
</a>
37+
</div>
38+
</div>
39+
</div>-->
40+
41+
</div>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
function activeCampaignResource($http, umbRequestHelper) {
2+
3+
const accountsApiEndpoint = "backoffice/UmbracoFormsIntegrationsCrmActiveCampaign/Accounts";
4+
const contactsApiEndpoint = "backoffice/UmbracoFormsIntegrationsCrmActiveCampaign/Contacts";
5+
6+
return {
7+
checkApiAccess: function () {
8+
return umbRequestHelper.resourcePromise(
9+
$http.get(`${contactsApiEndpoint}/CheckApiAccess`),
10+
"Failed to get resource");
11+
},
12+
getAccounts: function () {
13+
return umbRequestHelper.resourcePromise(
14+
$http.get(`${accountsApiEndpoint}/GetAccounts`),
15+
"Failed to get resource");
16+
},
17+
getContactFields: function () {
18+
return umbRequestHelper.resourcePromise(
19+
$http.get(`${contactsApiEndpoint}/GetContactFields`),
20+
"Failed to get resource");
21+
},
22+
getCustomFields: function () {
23+
return umbRequestHelper.resourcePromise(
24+
$http.get(`${contactsApiEndpoint}/GetCustomFields`),
25+
"Failed to get resource");
26+
}
27+
};
28+
}
29+
30+
angular.module('umbraco.resources').factory('umbracoFormsIntegrationsCrmActiveCampaignResource', activeCampaignResource);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
function activeCampaignService($routeParams, pickerResource) {
2+
return {
3+
getFormFields: function (callback) {
4+
var formId = $routeParams.id;
5+
6+
if (formId !== -1) {
7+
pickerResource.getAllFields(formId).then(function (response) {
8+
callback(response.data);
9+
});
10+
} else callback([]);
11+
}
12+
};
13+
}
14+
15+
angular.module("umbraco.services")
16+
.factory("activeCampaignService", activeCampaignService)

0 commit comments

Comments
 (0)