Pipeline CRM and Umbraco Forms

Heads Up!

This article is several years old now, and much has happened since then, so please keep that in mind while reading it.

Umbraco Forms has come a long way. With a shiny new UI, a wide range of customisation and integration options, it is now one of the stronger aspects of the Umbraco ecosystem. But to be effective, your website owners will also need tools to handle submissions in an organised and collaborative manner.

This is the job of a Customer Relationship Management system (CRM). If your website owners are in the market for a new piece of kit, consider Pipeline CRM by GrowCreate: it's free, open-source and a bit awesome (I might be a bit biased here). Pipeline is fully embedded in the Umbraco 7 back-office and pre-integrated with the Content, Media and Members areas.

In this article I will show you how to integrate Pipeline CRM with Umbraco Forms, creating Opportunities and Contacts from form submissions. To do this, we will create a configurable Workflow Type, allow the editor to map fields, and use the helpers to send data to Pipeline.

Before we start

Before we start, make sure you got the basics in place: you will need Umbraco Forms and Pipeline CRM installed on a v7 Umbraco website. All this can be achieved by the power of Nuget:

PM > Install-Package UmbracoForms
PM > Install-Package PipelineCRM

We now need to plan our integration. When a new submission comes in to Umbraco Forms, it gets saved in the database and one or more workflows get triggered. Workflows have access to the data and can execute custom .NET code, so this is the natural 'hook' for our integration. Make sure you have a look at the documentation, as it will help you with the specifics.

A new WorkflowType will get data into Pipeline, however not all Forms have the same fields. We need a way for form designers to map which fields in the current form correspond to the ones that Pipeline needs, and for that we need a mapper SettingType.

The steps we will follow are:

  1. Create a Workflow Type
  2. Create a mapper that connects form fields with Pipeline fields
  3. Hook the mapper to the Workflow Type, to send data to Pipeline
  4. Create a form, attach and configure the Workflow Type
  5. Add the form to a page, and fire out a test

Let’s do this

Creating the Workflow Type

Start a new class in a new Project or in the App_Code, make it inherit from Umbraco.Forms.Core.WorkflowType and add the settings so that users can customise the Opportunity name, value and field mappings, as well as and the identifiers for the Workflow Type.

Also, Every WorkflowType needs an Execute class that describes what happens when the workflow is triggered. For now, leave that blank and we will come back to it in the next section. Finally, add a ValidateSettings class - for now we will just check that there are values for the mandatory settings. You starter WorkflowType should look like this:

using System;
using System.Collections.Generic;
using Umbraco.Core.Logging;
using Umbraco.Forms.Core;
using Umbraco.Forms.Core.Attributes;
using Umbraco.Forms.Core.Enums;
using GrowCreate.PipelineCRM.Helpers;
using Newtonsoft.Json.Linq;
using System.Linq;

namespace GrowCreate.PipelineCRM.FormWorkflows
{
    public class CreateOpportunity : WorkflowType
    {
        [Setting("Title", prevalues = "New website enquiry", description = "Opportunity title - {name} will be subbed", view = "textstring")]
        public string OpportunityTitle { get; set; }

        [Setting("Value", prevalues = "1000", description = "Value of the new oppotunity", view = "textstring")]
        public string OpportunityValue { get; set; }

        [Setting("Mappings", prevalues = "", description = "Map form fields to Pipeline", view = "pipelinemapper")]
        public string MappingsField { get; set; }    
        
        public CreateOpportunity()
        {
            this.Id = new Guid("d991fe7c-03cf-45fb-ad56-0d2fe1fa48f2");
            this.Name = "Create an opportunity in Pipeline CRM";
            this.Description = "Creates a new CRM opportunity based on the form input.";
            this.Icon = "icon-inbox";
        }

        public override WorkflowExecutionStatus Execute(Record record, RecordEventArgs e)
        {
            try
            {
                // magic will go here

                return WorkflowExecutionStatus.Completed;
            }
            catch (Exception ex)
            {
                LogHelper.WarnWithException(this.GetType(), string.Format("Unable to create Opportunity: {0}", ex.Message), ex);
                return WorkflowExecutionStatus.Failed;
            }
        }

        public override List<Exception> ValidateSettings()
        {
            List<Exception> exceptions = new List<Exception>();

            // some workflow fields are mandatory
            if (string.IsNullOrEmpty(OpportunityTitle) || string.IsNullOrEmpty(MappingsField))
            {
                exceptions.Add(new Exception("Opportunity title and mappings must be set."));
            }

            return exceptions;
        }
    }
}

Srtarting out: \App_Code\CreatePipelineOpportunity.cs

Create a mapper

The most complex part of the integration is to map the Form fields to Pipeline fields. Umbraco Forms already includes a mapper Setting Type (\App_Plugins\UmbracoForms\Backoffice\Common\SettingTypes\fieldmapper.html), which we will happily copy and customise.

Copy the the View in the same folder and customise as shown below. The only difference with the built-in mapper is that we have Pipeline fields are predetermined, so we can remove the Add/Remove buttons and change the mapping.alias from an <input> to a <span>.

<div ng-controller="UmbracoForms.SettingTypes.PipelineMapperController">

    <div class="umb-forms-mappings" ng-if="mappings.length > 0">

        <div class="umb-forms-mapping-header">
            <div class="umb-forms-mapping-field -no-margin-left">Pipeline field</div>
            <div class="umb-forms-mapping-field">Form value</div>
            <div class="umb-forms-mapping-field">Static value</div>
        </div>

        <div class="umb-forms-mapping" ng-repeat="mapping in mappings">

            <div class="umb-forms-mapping-field -no-margin-left">
                <span>{{ mapping.alias }}</span>
            </div>

            <div class="umb-forms-mapping-field">
                <select class="-full-width"
                    ng-options="field.id as field.value for field in fields"
                    ng-model="mapping.value"
                    ng-change="stringifyValue()">
                    <option value="">Map a field</option>
                </select>
            </div>

            <div class="umb-forms-mapping-field">
                <input
                    class="-full-width-input"
                    type="text"
                    placeholder="Static value"
                    ng-model="mapping.staticValue"
                    on-blur="stringifyValue()" />
            </div>

        </div>

    </div>
</div>

Complete listing: \App_Plugins\UmbracoForms\Backoffice\Common\SettingTypes\pipelinemapper.html

Next, we need to wire up the Controller - you can either append the code in umbraco.forms.js, or create a new .js file and add it to the package.manifest. Either way, the code is a copy of the UmbracoForms.SettingTypes.FieldMapperController controller, but with the Add/Remove functions removed, and a pre-set mappings array:

angular.module("umbraco").controller("UmbracoForms.SettingTypes.PipelineMapperController",
	function ($scope, $routeParams, pickerResource) {

	    function init() {
	       
	        if (!$scope.setting.value) {
	            $scope.mappings = [
                    { alias: "Name" }, 
                    { alias: "Email" }, 
                    { alias: "Telephone" },
                    { alias: "Organisation" },
                    { alias: "Message" }];
	        } else {
	            $scope.mappings = JSON.parse($scope.setting.value);
	        }

	        var formId = $routeParams.id;

	        if (formId === -1 && $scope.model && $scope.model.fields) {

	        } else {

	            pickerResource.getAllFields($routeParams.id).then(function (response) {
	                $scope.fields = response.data;
	            });
	        }
	    }

	    $scope.stringifyValue = function () {
	        $scope.setting.value = JSON.stringify($scope.mappings);
	    };

	    init();

	});

Complete listing: \PipelineTest\App_Plugins\UmbracoForms\js\pipeline.mapper.js

To test the mapper, go to the back-office and Create a simple form with fields like Name, Email, Company, Telephone and Your question. Attach the new Workflow and you should be able to set the settings and map fields:

Design an Umbraco Form and configure the Pipeline mapping

Hook up the mapper

Our mapper ready, so let’s hook it up to our Workflow Type. Before doing so, it is worth looking at the JSON file that is generated to plan our approach - your file will have a unique name, but you can find it in \App_Plugins\UmbracoForms\Data\workflows.

Notice how the MappingsField is saved as a JSON string. The plan is to parse it with JSON.NET, extract the field Guids for each required field alias, use the Guid to grab the data from the Form Record and send it our to Pipeline. It makes more sense in code!

The heart of the mapper is in a new method called GetFormValue, which returns the value from a Record based on a field alias. It's only 3 lines of code, which the comments hopefully explain. Use this in your Execute method, to grab the values we need from the Form and send them to Pipeline.

In this case, you’ve used the CreatePipeline Helper, which also creates Contact and Organisation in a single go. If you want more granular control, you can go straight to the Services - for more documentation on how to integrate with Pipeline head on to the GitHub wiki.

A little house-keeping to finish off: adjust the Execute and ValidateSettings class, to ensure the Mappings and Record have for the fields that Pipeline requires. The complete listing for your WorkflowType should look like this:

using System;
using System.Collections.Generic;
using Umbraco.Core.Logging;
using Umbraco.Forms.Core;
using Umbraco.Forms.Core.Attributes;
using Umbraco.Forms.Core.Enums;
using GrowCreate.PipelineCRM.Helpers;
using Newtonsoft.Json.Linq;
using System.Linq;

namespace GrowCreate.PipelineCRM.FormWorkflows
{
    public class CreateOpportunity : WorkflowType
    {
        [Setting("Title", prevalues = "New website enquiry", description = "Opportunity title - {name} will be subbed", view = "textstring")]
        public string OpportunityTitle { get; set; }

        [Setting("Value", prevalues = "1000", description = "Value of the new oppotunity", view = "textstring")]
        public string OpportunityValue { get; set; }

        [Setting("Mappings", prevalues = "", description = "Map form fields to Pipeline", view = "pipelinemapper")]
        public string MappingsField { get; set; }    
        
        public CreateOpportunity()
        {
            this.Id = new Guid("d991fe7c-03cf-45fb-ad56-0d2fe1fa48f2");
            this.Name = "Create an opportunity in Pipeline CRM";
            this.Description = "Creates a new CRM opportunity based on the form input.";
            this.Icon = "icon-inbox";
        }

        public override WorkflowExecutionStatus Execute(Record record, RecordEventArgs e)
        {
            try
            {
                // make sure opp value is an integer
                int oppValue = int.TryParse(OpportunityValue, out oppValue) ? oppValue : 0;

                // get the correct values from the form
                var name = GetFormValue(record, "name");
                var email = GetFormValue(record, "email");
                var telephone = GetFormValue(record, "telephone");
                var organisation = GetFormValue(record, "organisation");
                var message = GetFormValue(record, "message");

                // check we have data for all required fields
                if (string.IsNullOrEmpty(email) || string.IsNullOrEmpty(name) || string.IsNullOrEmpty(OpportunityTitle))
                {
                    LogHelper.Warn(this.GetType(), string.Format("Unable to create Opportunity: Missing name, email or messagre"));
                    return WorkflowExecutionStatus.Failed;
                }

                // use the helper to create a new opportunity
                // this also creates a contact and organisation if content is found
                var pipelineHelper = PipelineHelper.CreatePipeline(
                    Title: OpportunityTitle.Replace("{name}", name),
                    contactName: name, 
                    contactEmail: email,
                    contactTelephone: telephone,
                    organisationName: organisation,
                    comments: message,
                    opportunityValue:oppValue
                    );
                    
                return WorkflowExecutionStatus.Completed;
            }
            catch (Exception ex)
            {
                LogHelper.WarnWithException(this.GetType(), string.Format("Unable to create Opportunity: {0}", ex.Message), ex);
                return WorkflowExecutionStatus.Failed;
            }
        }

        public override List<Exception> ValidateSettings()
        {
            List<Exception> exceptions = new List<Exception>();

            // some workflow fields are mandatory
            if (string.IsNullOrEmpty(OpportunityTitle) || string.IsNullOrEmpty(MappingsField))
            {
                exceptions.Add(new Exception("Opportunity title and mappings must be set."));
            }

            // we also need certain mappings to be set
            var settings = JArray.Parse(MappingsField) as IEnumerable<dynamic>;
            if (!settings.Any(x => x.alias.ToString().ToLower() == "name") || !settings.Any(x => x.alias.ToString().ToLower() == "email") || !settings.Any(x => x.alias.ToString().ToLower() == "message"))
            {
                exceptions.Add(new Exception("Mappings for name, email and message must be set."));
            }

            return exceptions;
        }

        public string GetFormValue(Record record, string alias)
        {
            // parse the mappings JSON string
            var settings = JArray.Parse(MappingsField) as IEnumerable<dynamic>;

            // get the field guid for the given alias
            var fieldGuid = (Guid)settings.FirstOrDefault(x => x.alias.ToString().ToLower() == alias.ToLower()).value;

            // get and return the corresponding value from the record
            return fieldGuid == null ? "" : record.GetRecordField(fieldGuid).ValuesAsString();
        }
    }
}

Complete listing: \App_Code\CreateOpportunity.cs

Et voila!

To test our integration, hook up the Form to a web page, and fire a test:

Hook up the form and fire a test

Back into the back office, go to Pipeline CRM and browse at the new Opportunity which your website owners can start working on.

PipelineCRM screenshot with Form input

Conclusion

Umbraco Forms and Pipeline CRM are natural partners. Combining a powerful form designer with comprehensive CRM functionality can make for fantastic user experiences. As both systems are fitted with APIs and integration hooks, you can get started quickly and inexpensively.

Taking it further

What else can you build with this functionality? Here are some ideas:

Shameless plug

Pipeline open-source and funded by a commercial version which includes modules not available in the open-source version, including multi-step workflows, automation and reporting. If you have more complex requirements, get in touch with GrowCreate.

Theo Paraskevopoulos

Theo is on Twitter as