Add a custom flow to a new campaign element
Use Campaign designer to set up your marketing campaigns. You can create a visual campaign diagram that would consist of interconnected pre-configured elements. You can also add custom campaign elements.
Algorithm of adding a custom flow (an arrow)
- Create a new schema for the Flow element.
- Create the edit page for the Flow element properties.
- Create the server part for the Flow element.
- Create the executed element for the flow transition.
- Create the
CampaignConnectorManager
replacing module for adding the flow operation logic. - Connect the
CampaignConnectorManager
replacing module.
Create a custom flow (an arrow) from the new campaign element for sending SMS-messages to a user and add a possibility to select the bulk sms response condition. On the setup page, a user can select an option to either ignore the responses or take them into consideration based on the list of possible bulk sms response types. If no response type is selected, the flow is enabled for any response.
In this case, the Usr
prefix is not used in schema names. You can change the prefix used by default in the Prefix for object name system setting (the SchemaNamePrefix
code).
Perform steps 1-6 from the "Add a custom campaign element" article before you implement the case.
Example implementation algorithm
1. Creating the TestSmsTarget and TestSmsResponseType objects
Create schemas for the TestSmsTarget and TestSmsResponseType objects in the development package.
Learn more about creating object schemas in the Object article.
Add a column with the following properties to the TestSmsResponseType schema:
- Title – "Name"
- Name – "Name"
- Data type – "Text (50 characters)"
Add columns with the following properties to the TestSmsTarget schema:
Primary column properties of the TestSmsTarget object schema
Title | Name | Data type |
---|---|---|
Phone number | PhoneNumber | "Text (50 characters)" |
SMS text | SmsText | "Text (50 characters)" |
Contact | Contact | "Lookup" – "Contact" |
Test SMS response | TestSmsResponse | "Lookup" – "TestSmsResponseType" |
Learn more about adding object schema columns in the Object article.
Save and publish the objects.
Create a lookup for the TestSmsResponseType object and populate it with the "Sms delivered", "Canceled", "Error while receiving", etc. values. The response identifiers (Ids) will be used in the edit page code of the condition flow properties from the bulk SMS.
2. Creating a new schema for the flow element
To display the element in the Campaign designer user interface, create a new module schema in the development package. The procedure for creating a module schema is covered in the Client module article. Set the following properties for the created schema:
- Title – "ProcessTestSmsConditionalTransitionSchema."
- Name – "ProcessTestSmsConditionalTransitionSchema."
The schema name for the arrow must contain the "Process" prefix
Add the following source code to the Source code section of the schema:
define("ProcessTestSmsConditionalTransitionSchema", ["CampaignEnums", "ProcessTestSmsConditionalTransitionSchemaResources", "ProcessCampaignConditionalSequenceFlowSchema"], function(CampaignEnums) {
Ext.define("Terrasoft.manager.ProcessTestSmsConditionalTransitionSchema", {
extend: "Terrasoft.ProcessCampaignConditionalSequenceFlowSchema",
alternateClassName: "Terrasoft.ProcessTestSmsConditionalTransitionSchema",
managerItemUId: "4b5e70b0-a631-458e-ab22-856ddc913444",
mixins: {
parametrizedProcessSchemaElement: "Terrasoft.ParametrizedProcessSchemaElement"
},
// The full type name of the connected arrow element.
typeName: "Terrasoft.Configuration.TestSmsConditionalTransitionElement, Terrasoft.Configuration",
// The name of the arrow element for connecting to campaign elements.
connectionUserHandleName: "TestSmsConditionalTransition",
// The name of the arrow property setup page.
editPageSchemaName: "TestSmsConditionalTransitionPropertiesPage",
elementType: CampaignEnums.CampaignSchemaElementTypes.CONDITIONAL_TRANSITION,
// Bulk sms response collection.
testSmsResponseId: null,
// The checkbox that takes into consideration the response condition for contact transition.
isResponseBasedStart: false,
getSerializableProperties: function() {
var baseSerializableProperties = this.callParent(arguments);
// Properties for serialization and transfer to the server part when saving.
Ext.Array.push(baseSerializableProperties, ["testSmsResponseId", "isResponseBasedStart"]);
return baseSerializableProperties;
}
});
return Terrasoft.ProcessTestSmsConditionalTransitionSchema;
});
Save the created schema.
3. Creating the edit page of the flow element properties
To display and modify campaign element properties, create its edit page in the development package. Create the schema replacing CampaignConditionalSequenceFlowPropertiesPage
(the CampaignDesigner
package). The procedure for creating a replacing schema is covered in the Client module article.
Set the following properties for the created schema:
- Title – "TestSmsConditionalTransitionPropertiesPage."
- Name – "TestSmsConditionalTransitionPropertiesPage."
- Parent object – "CampaignConditionalSequenceFlowPropertiesPage."
Add localizable strings, whose properties are listed in table to the created schema.
Primary properties of the localizable strings
Name | Value |
---|---|
| What is the result of the 0 step? |
| Transfer participants regardless of their response |
| Set up responses for transferring participants |
| Test SMS delivered |
| Error while receiving |
Add the following source code to the Source code section of the schema:
define("TestSmsConditionalTransitionPropertiesPage", ["BusinessRuleModule"], function(BusinessRuleModule) {
return {
messages: {},
attributes: {
"ReactionModeEnum": {
dataValueType: this.Terrasoft.DataValueType.CUSTOM_OBJECT,
type: this.Terrasoft.ViewModelColumnType.VIRTUAL_COLUMN,
value: {
Default: {
value: "0",
captionName: "Resources.Strings.ReactionModeDefault"
},
WithCondition: {
value: "1",
captionName: "Resources.Strings.ReactionModeWithCondition"
}
}
},
"ReactionMode": {
"dataValueType": this.Terrasoft.DataValueType.LOOKUP,
"type": this.Terrasoft.ViewModelColumnType.VIRTUAL_COLUMN,
"isRequired": true
},
"IsTestSmsDelivered": {
"dataValueType": this.Terrasoft.DataValueType.BOOLEAN,
"type": this.Terrasoft.ViewModelColumnType.VIRTUAL_COLUMN
},
"IsErrorWhileReceiving": {
"dataValueType": this.Terrasoft.DataValueType.BOOLEAN,
"type": this.Terrasoft.ViewModelColumnType.VIRTUAL_COLUMN
}
},
rules:
{
"ReactionConditionDecision": {
"BindReactionConditionDecisionRequiredToReactionMode": {
"ruleType": BusinessRuleModule.enums.RuleType.BINDPARAMETER,
"property": BusinessRuleModule.enums.Property.REQUIRED,
"conditions": [{
"leftExpression": {
"type": BusinessRuleModule.enums.ValueType.ATTRIBUTE,
"attribute": "ReactionMode"
},
"comparisonType": this.Terrasoft.ComparisonType.EQUAL,
"rightExpression": {
"type": BusinessRuleModule.enums.ValueType.CONSTANT,
"value": "1"
}
}]
}
}
},
methods: {
// Ensures sms-response correspondence (based on the TestSmsResponseType lookup).
// We assume that Creatio already contains the TestSmsResponseType lookup
// with the TestSmsDelivered and ErrorWhileReceiving records.
getResponseConfig: function() {
return {
"IsTestSmsDelivered": "F2FC75B3-58C3-49A6-B2F2-353262068145",
"IsErrorWhileReceiving": "37B9F9D5-E897-4B7B-A65E-3B3799A18D72"
};
},
subscribeEvents: function() {
this.callParent(arguments);
// Connecting the handler to the event of changing the ReactionMode attribute value
this.on("change:ReactionMode", this.onReactionModeLookupChanged, this);
},
// Event handler-method of changing the ReactionMode attribute.
onReactionModeLookupChanged: function() {
var reactionModeEnum = this.get("ReactionModeEnum");
var reactionMode = this.get("ReactionMode");
var decisionModeEnabled = (reactionMode && reactionMode.value === reactionModeEnum.WithCondition.value);
if (!decisionModeEnabled) {
this.set("ReactionConditionDecision", null);
}
},
// Initiates the viewModel properties to display the page when opening.
initParameters: function(element) {
this.callParent(arguments);
var isResponseBasedStart = element.isResponseBasedStart;
this.initReactionMode(isResponseBasedStart);
this.initTestSmsResponses(element.testSmsResponseId);
},
// Auxiliary method cutting the line to the specified length and adding allipsis at the end.
cutString: function(strValue, strLength) {
var ellipsis = Ext.String.ellipsis(strValue.substring(strLength), 0);
return strValue.substring(0, strLength) + ellipsis;
},
// Sets the status value to "Sms delivered".
initIsTestSmsDelivered: function(value) {
if (value === undefined) {
value = this.get("IsTestSmsDelivered");
}
this.set("IsTestSmsDelivered", value);
},
// Sets the status value to "Error while receiving".
initIsErrorWhileReceiving: function(value) {
if (value === undefined) {
var isErrorWhileReceiving = this.get("IsErrorWhileReceiving");
value = isErrorWhileReceiving;
}
this.set("IsErrorWhileReceiving", value);
},
// Initiates the selected responses when opening the page.
initTestSmsResponses: function(responseIdsJson) {
if (!responseIdsJson) {
return;
}
var responseIds = JSON.parse(responseIdsJson);
var config = this.getResponseConfig();
Terrasoft.each(config, function(propValue, propName) {
if (responseIds.indexOf(propValue) > -1) {
this.set(propName, true);
}
}, this);
},
initReactionMode: function(value) {
var isDefault = !value;
this.setLookupValue(isDefault, "ReactionMode", "WithCondition", this);
},
// Auxiliary method extracting the identifier array from the incoming JSON parameter.
getIds: function(idsJson) {
if (idsJson) {
try {
var ids = JSON.parse(idsJson);
if (this.Ext.isArray(ids)) {
return ids;
}
} catch (error) {
return [];
}
}
return [];
},
onPrepareReactionModeList: function(filter, list) {
this.prepareList("ReactionModeEnum", list, this);
},
// Saves the response values and the setting of adding the response condition.
saveValues: function() {
this.callParent(arguments);
var element = this.get("ProcessElement");
var isResponseBasedStart = this.getIsReactionModeWithConditions();
element.isResponseBasedStart = isResponseBasedStart;
element.testSmsResponseId = this.getTestSmsResponseId(isResponseBasedStart);
},
// Receives the serialized Ids of the selected responses.
getTestSmsResponseId: function(isResponseActive) {
var responseIds = [];
if (isResponseActive) {
var config = this.getResponseConfig();
Terrasoft.each(config, function(propValue, propName) {
var attrValue = this.get(propName);
if (attrValue && propValue) {
responseIds.push(propValue);
}
}, this);
}
return JSON.stringify(responseIds);
},
getLookupValue: function(parameterName) {
var value = this.get(parameterName);
return value ? value.value : null;
},
getContextHelpCode: function() {
return "CampaignConditionalSequenceFlow";
},
getIsReactionModeWithConditions: function() {
return this.isLookupValueEqual("ReactionMode", "1", this);
},
getSourceElement: function() {
var flowElement = this.get("ProcessElement");
if (flowElement) {
return flowElement.findSourceElement();
}
return null;
},
// Adds the name of the element that the arrow is generated from to the text.
getQuestionCaption: function() {
var caption = this.get("Resources.Strings.ReactionModeCaption");
caption = this.Ext.String.format(caption, this.getSourceElement().getCaption());
return caption;
}
},
diff: /**SCHEMA_DIFF*/[
// Container.
{
"operation": "insert",
"name": "ReactionContainer",
"propertyName": "items",
"parentName": "ContentContainer",
"className": "Terrasoft.GridLayoutEdit",
"values":
{
"layout":
{
"column": 0,
"row": 2,
"colSpan": 24
},
"itemType": this.Terrasoft.ViewItemType.GRID_LAYOUT,
"items": []
}
},
// Title.
{
"operation": "insert",
"name": "ReactionModeLabel",
"parentName": "ReactionContainer",
"propertyName": "items",
"values":
{
"layout":
{
"column": 0,
"row": 0,
"colSpan": 24
},
"itemType": this.Terrasoft.ViewItemType.LABEL,
"caption":
{
"bindTo": "getQuestionCaption"
},
"classes":
{
"labelClass": ["t-title-label-proc"]
}
}
},
// List.
{
"operation": "insert",
"name": "ReactionMode",
"parentName": "ReactionContainer",
"propertyName": "items",
"values":
{
"contentType": this.Terrasoft.ContentType.ENUM,
"controlConfig":
{
"prepareList":
{
"bindTo": "onPrepareReactionModeList"
}
},
"isRequired": true,
"layout":
{
"column": 0,
"row": 1,
"colSpan": 24
},
"labelConfig":
{
"visible": false
},
"wrapClass": ["no-caption-control"]
}
},
// List element.
{
"operation": "insert",
"parentName": "ReactionContainer",
"propertyName": "items",
"name": "IsTestSmsDelivered",
"values":
{
"wrapClass": ["t-checkbox-control"],
"visible":
{
"bindTo": "ReactionMode",
"bindConfig":
{
converter: "getIsReactionModeWithConditions"
}
},
"caption":
{
"bindTo": "Resources.Strings.IsTestSmsDelivered"
},
"layout":
{
"column": 0,
"row": 2,
"colSpan": 22
}
}
},
// List element.
{
"operation": "insert",
"parentName": "ReactionContainer",
"propertyName": "items",
"name": "IsErrorWhileReceiving",
"values":
{
"wrapClass": ["t-checkbox-control"],
"visible":
{
"bindTo": "ReactionMode",
"bindConfig":
{
converter: "getIsReactionModeWithConditions"
}
},
"caption":
{
"bindTo": "Resources.Strings.IsErrorWhileReceiving"
},
"layout":
{
"column": 0,
"row": 3,
"colSpan": 22
}
}
}
]/**SCHEMA_DIFF*/
};
});
Save the created schema.
4. Creating the server part of the flow element from the Bulk SMS element
To implement saving the base and user campaign element properties, create a class interacting with the server part of the application. The class should inherit CampaignSchemaElement
and override the ApplyMetaDataValue()
and WriteMetaData()
methods.
Create the source code schema with the following properties:
- Title – "TestSmsConditionalTransitionElement."
- Name – "TestSmsConditionalTransitionElement."
Creating the source code schema is covered in the Source code (C#) article.
Add the following source code to the Source code section of the schema:
namespace Terrasoft.Configuration
{
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Globalization;
using System.Linq;
using Newtonsoft.Json;
using Terrasoft.Common;
using Terrasoft.Core;
using Terrasoft.Core.Campaign;
using Terrasoft.Core.DB;
using Terrasoft.Core.Process;
[DesignModeProperty(Name = "TestSmsResponseId",
UsageType = DesignModeUsageType.NotVisible, MetaPropertyName = TestSmsResponseIdPropertyName)]
[DesignModeProperty(Name = "IsResponseBasedStart",
UsageType = DesignModeUsageType.Advanced, MetaPropertyName = IsResponseBasedStartPropertyName)]
public class TestSmsConditionalTransitionElement : ConditionalSequenceFlowElement
{
private const string TestSmsResponseIdPropertyName = "TestSmsResponseId";
private const string IsResponseBasedStartPropertyName = "IsResponseBasedStart";
public TestSmsConditionalTransitionElement() {}
public TestSmsConditionalTransitionElement(TestSmsConditionalTransitionElement source)
: this(source, null, null) {}
public TestSmsConditionalTransitionElement(TestSmsConditionalTransitionElement source,
Dictionary<Guid, Guid> dictToRebind, Core.Campaign.CampaignSchema parentSchema)
: base(source, dictToRebind, parentSchema) {
IsResponseBasedStart = source.IsResponseBasedStart;
_testSmsResponseIdJson = JsonConvert.SerializeObject(source.TestSmsResponseId);
}
private string _testSmsResponseIdJson;
private IEnumerable<Guid> Responses {
get {
return TestSmsResponseId;
}
}
[MetaTypeProperty("{DC597899-B831-458A-A58E-FB43B1E266AC}")]
public IEnumerable<Guid> TestSmsResponseId {
get {
return !string.IsNullOrWhiteSpace(_testSmsResponseIdJson)
? JsonConvert.DeserializeObject<IEnumerable<Guid>>(_testSmsResponseIdJson)
: Enumerable.Empty<Guid>();
}
}
[MetaTypeProperty("{3FFA4EA0-62CC-49A8-91FF-4096AEC561F6}",
IsExtraProperty = true, IsUserProperty = true)]
public virtual bool IsResponseBasedStart {
get;
set;
}
protected override void ApplyMetaDataValue(DataReader reader) {
base.ApplyMetaDataValue(reader);
switch (reader.CurrentName) {
case TestSmsResponseIdPropertyName:
_testSmsResponseIdJson = reader.GetValue<string>();
break;
case IsResponseBasedStartPropertyName:
IsResponseBasedStart = reader.GetBoolValue();
break;
default:
break;
}
}
public override void WriteMetaData(DataWriter writer) {
base.WriteMetaData(writer);
writer.WriteValue(IsResponseBasedStartPropertyName, IsResponseBasedStart, false);
writer.WriteValue(TestSmsResponseIdPropertyName, _testSmsResponseIdJson, null);
}
public override object Clone() {
return new TestSmsConditionalTransitionElement(this);
}
public override object Copy(Dictionary<Guid, Guid> dictToRebind, Core.Campaign.CampaignSchema parentSchema) {
return new TestSmsConditionalTransitionElement(this, dictToRebind, parentSchema);
}
// Overrides the factory method for creating the executed element
// Returns the element with the TestSmsConditionalTransitionFlowElement type
public override ProcessFlowElement CreateProcessFlowElement(UserConnection userConnection) {
var sourceElement = SourceRef as TestSmsElement;
var executableElement = new TestSmsConditionalTransitionFlowElement {
UserConnection = userConnection,
TestSmsResponses = TestSmsResponseId,
PhoneNumber = sourceElement.PhoneNumber,
SmsText = sourceElement.SmsText
};
InitializeCampaignProcessFlowElement(executableElement);
InitializeCampaignTransitionFlowElement(executableElement);
InitializeConditionalTransitionFlowElement(executableElement);
return executableElement;
}
}
}
Save and publish the created schema.
5. Creating the executed element for the flow from the Bulk SMS element
To add a functionality that would take into consideration the response type as per the sent bulk sms, create the executed element. It is a class, the inheritor of the ConditionalTransitionFlowElement
class.
To create the executed element, add the source code schema with the following properties in the development package:
- Title – "TestSmsConditionalTransitionElement."
- Name – "TestSmsConditionalTransitionElement."
Add the following source code to the Source code section of the schema:
namespace Terrasoft.Configuration
{
using System;
using System.Collections.Generic;
using System.Linq;
using Terrasoft.Common;
using Terrasoft.Core.DB;
public class TestSmsConditionalTransitionFlowElement : ConditionalTransitionFlowElement
{
public string SmsText { get; set; }
public string PhoneNumber { get; set; }
public IEnumerable<Guid> TestSmsResponses { get; set; }
private void ExtendWithResponses() {
TransitionQuery.CheckArgumentNull("TransitionQuery");
if (TestSmsResponses.Any()) {
Query responseSelect = GetSelectByParticipantResponses();
TransitionQuery.And("ContactId").In(responseSelect);
}
}
private Query GetSelectByParticipantResponses() {
var responseSelect = new Select(UserConnection)
.Column("ContactId")
.From("TestSmsTarget")
.Where("SmsText").IsEqual(Column.Parameter(SmsText))
.And("PhoneNumber").IsEqual(Column.Parameter(PhoneNumber))
.And("TestSmsResponseId")
.In(Column.Parameters(TestSmsResponses)) as Select;
responseSelect.SpecifyNoLockHints(true);
return responseSelect;
}
protected override void CreateQuery() {
base.CreateQuery();
ExtendWithResponses();
}
}
}
Save and publish the created schema.
6. Creating the CampaignConnectorManager replacing module for adding the flow operation logic
To add specific logic of the flow operation upon changing the source (i.e. the element that the outgoing arrow is generated from), create a new module schema replacing the CampaignConnectorManager
module in the development package. The procedure for creating a module schema is covered in the Client module article. Set the following properties for the created schema:
- Title – "TestSmsCampaignConnectorManager."
- Name – "TestSmsCampaignConnectorManager."
Add the following source code to the Source code section of the schema:
define("TestSmsCampaignConnectorManager", [], function() {
Ext.define("Terrasoft.TestSmsCampaignConnectorManager", {
// Specify replacing of the CampaignConnectorManager module
override: "Terrasoft.CampaignConnectorManager",
// Add mapping of the name of arrow source campaign element – arrow type (full name)
initMappingCollection: function() {
this.callParent(arguments);
this.connectorTypesMappingCollection.addIfNotExists("TestSmsElementSchema",
"Terrasoft.ProcessTestSmsConditionalTransitionSchema");
},
// Virtual method for reload
// Arrow processing logic before its sudstitute by an arrow with a new type.
additionalBeforeChange: function(prevTransition, sourceItem, targetItem) {
// additional logic here
},
// Virtual method for reload
// Populating specific fields of the created arrow based on the previous arrow.
fillAdditionalProperties: function(prevElement, newElement) {
if (newElement.getTypeInfo().typeName === "ProcessTestSmsConditionalTransitionSchema") {
// Copy the configured responses if the previous arrow is of the same type
newElement.testSmsResponseId = prevElement.testSmsResponseId ? prevElement.testSmsResponseId : null;
// Copy the configuration of response setup
newElement.isResponseBasedStart = prevElement.isResponseBasedStart
? prevElement.isResponseBasedStart
: false;
}
}
});
});
Save the created schema.
7. Connecting the CampaignConnectorManager replacing module
To connect the module created on the previous step, create a replacing client module and specify BootstrapModulesV2
from the NUI
package as the parent object. The procedure of creating a replacing client module is covered in the Client module article.
Add the following source code to the Source code section of the schema:
// Set the previously created TestSmsCampaignConnectorManager module as a dependency
define("BootstrapModulesV2", ["TestSmsCampaignConnectorManager"], function() {});
Save the created schema.
During an actual implementation of this case, we recommend creating a separate Bulk SMS object schema. The TestSmsElement and TestSmsConditionalTransitionElement objects will contain the Id of this object and not the SmsText
, PhoneNumber
... fields. The TestSmsCampaignProcessElement
executed element in the Execute()
method must contain the logic of adding contacts to the bulk sms audience. A separate mechanism (or several mechanisms) must perform sending of the bulk sms and afterwards record the participants’ responses. Based on these responses, the arrow will transfer the campaign audience to the following campaign step.