Adding a custom transition (flow) to a new campaign element
Glossary Item Box
Introduction
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.
General 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.
Case description
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.
ATTENTION
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).
ATTENTION
Perform steps 1-6 from the “Adding a custom campaign element” article before you implement the case.
Source code
You can download the package with case implementation using the following link.
Case implementation algorithm
1. Creating the [TestSmsTarget] and [TestSmsResponseType] objects
Create schemas for the [TestSmsTarget] (fig. 1) and [TestSmsResponseType] (fig. 2) objects in the development package.
Learn more about creating object schemas in the “Creating the entity schema” 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:
Table 1. 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 “Creating the entity schema” article.
Fig. 1. Columns and properties of the [TestSmsTarget] object schema
Fig. 2. Columns and properties of the [TestSmsResponseType] object schema
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 (Fig. 3). The response identifiers (Ids) will be used in the edit page code of the condition flow properties from the bulk SMS.
Fig. 3. The [Test sms response] lookup
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 “Creating a custom client module schema” article. Set the following properties for the created schema (fig. 4):
- [Title] – “ProcessTestSmsConditionalTransitionSchema”
- [Name] – “ProcessTestSmsConditionalTransitionSchema”
ATTENTION
The schema name for the arrow must contain the “Process” prefix
Fig. 4. Properties of the ProcessTestSmsConditionalTransitionSchema module schema
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 “Creating a custom client module schema” article.
Set the following properties for the created schema (fig. 5):
- [Title] – “TestSmsConditionalTransitionPropertiesPage”
- [Name] – “TestSmsConditionalTransitionPropertiesPage”
- [Parent object] – “CampaignConditionalSequenceFlowPropertiesPage”
Fig. 5. Properties of the edit page schema
Add localizable strings, whose properties are listed in table 2 to the created schema.
Table 2. Primary properties of the localizable strings
[Name] | [Value] |
---|---|
ReactionModeCaption | What is the result of the {0} step? |
ReactionModeDefault | Transfer participants regardless of their response |
ReactionModeWithCondition | Set up responses for transferring participants |
IsTestSmsDelivered | Test SMS delivered |
IsErrorWhileReceiving | 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 “Creating the [Source code] schema” 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 “Creating a custom client module schema” 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 “Creating a custom client module schema“ article.
Add the following source code to the [Source code] section of the schema:
// Set the previously created TestSmsCampaignConnectorManager module as a dependancy define("BootstrapModulesV2", ["TestSmsCampaignConnectorManager"], function() {});
Save the created schema.
NOTE
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.