Implement a custom flow for a campaign element
To implement the example:
- Implement a custom campaign element. Read more >>>
- Create a schema for the campaign element flow. Read more >>>
- Create a property panel for the campaign element flow. Read more >>>
- Create an executable element for the campaign element flow. Read more >>>
- Implement the business logic of the campaign element flow. Read more >>>
- Extend campaign connector manager. Read more >>>
- Connect the business logic of the campaign element flow. Read more >>>
1. Implement a custom campaign element
Instructions: Implement a custom campaign element.
2. Create a schema for the campaign element flow
1. Create the SMS sending status lookup
-
Open the Configuration section. Instructions: Open the Configuration section.
-
Create a
sdkCustomCampaignElementFlow
package. Instructions: Create a user-made package using Configuration section. -
Change the current package to
sdkCustomCampaignElementFlow
. Instructions: Change the current package. -
Click Add → Object.
-
Fill out the schema properties.
For this example, use the following schema properties.
Property
Property value
General property block
Code
UsrSmsSendingStatus
Title
SMS sending status
Inheritance property block
Parent object
BaseLookup
-
Click Save and publish.
-
Register the lookup.
-
Click
in the top right → System setup → Lookups.
-
Create a lookup.
-
Click New lookup.
-
Fill out the lookup properties.
Property
Property value
Name
SMS sending status
Object
SMS sending status
-
Click Save.
-
-
Fill out the lookup.
For this example, add the following lookup values:
- Delivered
- Canceled
- Error while receiving
-
2. Create SMS recipient object
-
Open the Configuration section. Instructions: Open the Configuration section.
-
Select the
sdkCustomCampaignElementFlow
package. -
Click Add → Object.
-
Fill out the schema properties.
For this example, use the following schema properties.
Property
Property value
General property block
Code
UsrSmsRecipient
Title
Sms recipient
Inheritance property block
Parent object
BaseEntity
-
Add columns.
For this example, add the following columns:
- column that contains the phone number
- column that contains the SMS text
- column that contains the SMS receiver
- column that contains the SMS sending status
To do this:
-
Go to the Columns node's context menu.
-
Click
→ select column type → fill out the column properties.
Column
Column type
Property
Property value
Column that contains the phone number
Text (50 characters)
Code
UsrPhoneNumber
Title
Phone
Column that contains the SMS text
Text (50 characters)
Code
UsrSmsText
Title
SMS text
Column that contains the SMS receiver
Lookup
Code
UsrContactLookup
Title
Contact
Lookup
Contact
Column that contains the SMS sending status
Lookup
Code
UsrSmsSendingStatusLookup
Title
SMS sending status
Lookup
UsrSmsSendingStatus
-
Click Save and publish.
3. Create SMS responses object
-
Click Add → Object.
-
Fill out the schema properties.
For this example, use the following schema properties.
Property
Property value
General property block
Code
UsrSmsResponses
Title
SMS responses
Inheritance property block
Parent object
BaseEntity
-
Add columns.
For this example, add the column that contains the SMS response. To do this:
-
Go to the Columns node's context menu.
-
Click
→ select column type → fill out the column properties.
Column
Column type
Property
Property value
Column that contains the SMS response
Text (50 characters)
Code
UsrResponse
Title
Response
-
-
Click Save and publish.
4. Create a flow for a campaign element
-
Click Add → Module.
-
Fill out the schema properties.
For this example, use the following schema properties.
Property
Property value
Code
UsrProcessSmsConditionalTransitionSchema
Title
Element conditional transition schema
-
Add source code that implements the flow conditions.
UsrProcessSmsConditionalTransitionSchemadefine("UsrProcessSmsConditionalTransitionSchema", ["CampaignEnums",
"UsrProcessSmsConditionalTransitionSchemaResources",
"ProcessCampaignConditionalSequenceFlowSchema"], function(CampaignEnums) {
Ext.define("Terrasoft.manager.UsrProcessSmsConditionalTransitionSchema", {
/* Parent schema. */
extend: "Terrasoft.ProcessCampaignConditionalSequenceFlowSchema",
/* Alternative class name. */
alternateClassName: "Terrasoft.UsrProcessSmsConditionalTransitionSchema",
/* ID of the new campaign element. */
managerItemUId: "4b5e70b0-a631-458e-ab22-856ddc913444",
/* Mixins that extend functionality. */
mixins: {
parametrizedProcessSchemaElement: "Terrasoft.ParametrizedProcessSchemaElement"
},
/* Full name of the class that implements the campaign element logic.
This class saves and reads the element properties from the schema
properties. Since the class is placed in the regular package, use the
"Terrasoft.Configuration.UsrSmsConditionalTransitionElement,
Terrasoft.Configuration" typeName. If the class is placed in the
assembly package, use
typeName: "Terrasoft.Configuration.UsrSmsConditionalTransitionElement,
SomeAssemblyPackageName" */
typeName: "Terrasoft.Configuration.UsrSmsConditionalTransitionElement," +
"Terrasoft.Configuration",
/* Arrow element name used for linking campaign elements. */
connectionUserHandleName: "SmsConditionalTransition",
/* Schema name of the edit page. */
editPageSchemaName: "UsrSmsConditionalTransitionPropertiesPanel",
/* Element type. */
elementType: CampaignEnums.CampaignSchemaElementTypes.CONDITIONAL_TRANSITION,
/* ID of the SMS response option linked to the transition. */
smsResponseId: null,
/* Define whether the transition is based on a specific SMS
response condition. */
isResponseBasedStart: false,
/* Expand the properties for serialization. */
getSerializableProperties: function() {
var baseSerializableProperties = this.callParent(arguments);
Ext.Array.push(baseSerializableProperties, ["smsResponseId",
"isResponseBasedStart"]);
return baseSerializableProperties;
}
});
return Terrasoft.UsrProcessSmsConditionalTransitionSchema;
}
); -
Click Save.
5. Add the campaign element frow to the Campaign Designer
-
Click Add → Replacing view model.
-
Fill out the schema properties.
For this example, use the following schema properties.
Property
Property value
Parent object
CampaignElementSchemaManagerEx
-
Add the source code.
CampaignElementSchemaManagerExrequire(["CampaignElementSchemaManager", "UsrProcessSmsConditionalTransitionSchema"],
function() {
/* Register the "UsrProcessSmsConditionalTransitionSchema" in the Campaign Designer. */
var coreElementClassNames = Terrasoft.CampaignElementSchemaManager.coreElementClassNames;
/* Add the custom schema to the list of available schemas. */
coreElementClassNames.push({
itemType: "Terrasoft.UsrProcessSmsConditionalTransitionSchema"
});
}
); -
Click Save.
3. Create a property panel for the campaign element flow
-
Click Add → Page view model.
-
Fill out the schema properties.
For this example, use the following schema properties.
Property
Property value
Code
UsrSmsConditionalTransitionPropertiesPanel
Title
Properties panel for the custom flow
Parent object
CampaignConditionalSequenceFlowPropertiesPage
-
Add the localizable strings that include the field names.
-
Create a localizable string. Instructions: Add a localizable string.
-
Fill out the localizable string parameters.
Code
Value
ReactionModeCaption
What is the result of the {0} step?
ReactionModeDefault
Transfer participants regardless of their response
ReactionModeWithCondition
Set up responses for transferring participants
IsSmsDelivered
SMS is delivered
IsErrorWhileReceiving
Error while receiving
-
-
Implement the business logic of the campaign element flow.
UsrSmsConditionalTransitionPropertiesPaneldefine("UsrSmsConditionalTransitionPropertiesPanel", ["BusinessRuleModule",
"LookupUtilities"], function(BusinessRuleModule, LookupUtilities) {
return {
messages: {},
/* Schema attributes. */
attributes: {
/* Attribute that stores data about list of participants. */
"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"
}
}
},
/* Attribute that stores data about participants' responses. */
"ReactionMode": {
"dataValueType": this.Terrasoft.DataValueType.LOOKUP,
"type": this.Terrasoft.ViewModelColumnType.VIRTUAL_COLUMN,
"isRequired": true
},
/* Attribute that stores data about SMS delivery status. */
"IsSmsDelivered": {
"dataValueType": this.Terrasoft.DataValueType.BOOLEAN,
"type": this.Terrasoft.ViewModelColumnType.VIRTUAL_COLUMN
},
/* Attribute that stores data about error occurred while
receiving SMS. */
"IsErrorWhileReceiving": {
"dataValueType": this.Terrasoft.DataValueType.BOOLEAN,
"type": this.Terrasoft.ViewModelColumnType.VIRTUAL_COLUMN
}
},
/* Schema business rules. */
rules: {
/* Make the "ReactionConditionDecision" business rule
required when the "ReactionMode" attribute is equal "1." */
"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"
}
}]
}
}
},
/* Schema methods. */
methods: {
/* Return mapping of attributes to the "SMS response
type" ("SmsResponseType" code) lookup records. */
getResponseConfig: function() {
return {
"IsSmsDelivered": "5a9f08a4-c7f2-4dc8-9940-13ba1b2762cf",
"IsErrorWhileReceiving": "4a790626-bfbb-4ca7-a9f2-595000262561"
};
},
/* Bind the handler to the event that changes the "ReactionMode"
attribute value. */
subscribeEvents: function() {
this.callParent(arguments);
/* Subscribe to changes of the "ReactionMode" attribute
value. */
this.on(
"change:ReactionMode",
this.onReactionModeLookupChanged,
this
);
},
/* Handle the ReactionMode attribute change event. */
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);
}
},
/* Initialize attributes for displaying the page when opened. */
initParameters: function(element) {
this.callParent(arguments);
var isResponseBasedStart = element.isResponseBasedStart;
this.initReactionMode(isResponseBasedStart);
this.initSmsResponses(element.smsResponseId);
},
/* Cut the string to the specified length and append an
ellipsis at the end. Auxiliary method. */
cutString: function(strValue, strLength) {
var ellipsis = Ext.String.ellipsis(
strValue.substring(strLength),
0
);
return strValue.substring(0, strLength) + ellipsis;
},
/* Set the status value to "Delivered." */
initIsSmsDelivered: function(value) {
if (value === undefined) {
value = this.get("IsSmsDelivered");
}
this.set("IsSmsDelivered", value);
},
/* Set the status value to "Error while receiving." */
initIsErrorWhileReceiving: function(value) {
if (value === undefined) {
var isErrorWhileReceiving = this.get("IsErrorWhileReceiving");
value = isErrorWhileReceiving;
}
this.set("IsErrorWhileReceiving", value);
},
/* Initialize the selected responses when opening the page. */
initSmsResponses: 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);
},
/* Initialize the "ReactionMode" attribute based on
"isResponseBasedStart." */
initReactionMode: function(value) {
var isDefault = !value;
this.setLookupValue(
isDefault,
"ReactionMode",
"WithCondition",
this
);
},
/* Parse JSON array of IDs. Return "[]" on error. */
getIds: function(idsJson) {
if (idsJson) {
try {
var ids = JSON.parse(idsJson);
if (this.Ext.isArray(ids)) {
return ids;
}
} catch (error) {
return [];
}
}
return [];
},
/* Populate "ReactionMode" attribute list from "ReactionModeEnum". */
onPrepareReactionModeList: function(filter, list) {
this.prepareList("ReactionModeEnum", list, this);
},
/* Save UI values back to the process element before save. */
saveValues: function() {
this.callParent(arguments);
var element = this.get("ProcessElement");
var isResponseBasedStart = this.getIsReactionModeWithConditions();
element.isResponseBasedStart = isResponseBasedStart;
element.smsResponseId = this.getSmsResponseId(isResponseBasedStart);
},
/* Serialize selected response IDs to JSON.*/
getSmsResponseId: 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);
},
/* Return lookup underlying ID or null. */
getLookupValue: function(parameterName) {
var value = this.get(parameterName);
return value ? value.value : null;
},
/* Help context code for the property page. */
getContextHelpCode: function() {
return "CampaignConditionalSequenceFlow";
},
/* Return "true" if "ReactionMode" is set to "1." */
getIsReactionModeWithConditions: function() {
return this.isLookupValueEqual("ReactionMode", "1", this);
},
/* Return source element of the transition if available. */
getSourceElement: function() {
var flowElement = this.get("ProcessElement");
if (flowElement) {
return flowElement.findSourceElement();
}
return null;
},
/* Format question caption including the source element name. */
getQuestionCaption: function() {
var caption = this.get("Resources.Strings.ReactionModeCaption");
caption = this.Ext.String.format(
caption,
this.getSourceElement().getCaption()
);
return caption;
}
},
/* The schema view description. */
diff: /**SCHEMA_DIFF*/[
/* Page 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 label. */
{
"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"]
}
}
},
/* "ReactionMode" 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"]
}
},
/* "IsSmsDelivered" checkbox.*/
{
"operation": "insert",
"parentName": "ReactionContainer",
"propertyName": "items",
"name": "IsSmsDelivered",
"values":
{
"wrapClass": ["t-checkbox-control"],
"visible":
{
"bindTo": "ReactionMode",
"bindConfig":
{
converter: "getIsReactionModeWithConditions"
}
},
"caption":
{
"bindTo": "Resources.Strings.IsSmsDelivered"
},
"layout":
{
"column": 0,
"row": 2,
"colSpan": 22
}
}
},
/* "IsErrorWhileReceiving" checkbox.*/
{
"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*/
};
}
); -
Click Save.
4. Create an executable element for the campaign element flow
-
Click Add → Source code.
-
Fill out the schema properties.
For this example, use the following schema properties.
Property
Property value
Code
UsrSmsConditionalTransitionFlowElement
Title
Conditional transition of the SMS process element
-
Implement an executable element that collects responses of the bulk SMS.
- Specify the
ConditionalTransitionFlowElement
class as a parent class. - Override the
CreateQuery()
method.
UsrSmsConditionalTransitionFlowElementnamespace Terrasoft.Configuration
{
using System;
using System.Collections.Generic;
using System.Linq;
using Terrasoft.Common;
using Terrasoft.Core.DB;
/* Define a custom element flow. */
public class UsrSmsConditionalTransitionFlowElement
: ConditionalTransitionFlowElement
{
/* Assign the "SmsText" attribute value in the class. */
public string SmsText {
get;
set;
}
/* Assign the "PhoneNumber" attribute value in the class. */
public string PhoneNumber {
get;
set;
}
/* Assign the collection of SMS response IDs in the class. */
public IEnumerable<Guid> SmsResponses {
get;
set;
}
/* Extend the base "TransitionQuery" with participant responses
filter. */
private void ExtendWithResponses() {
TransitionQuery.CheckArgumentNull("TransitionQuery");
if (SmsResponses.Any()) {
Query responseSelect = GetSelectByParticipantResponses();
TransitionQuery.And("ContactId").In(responseSelect);
}
}
/* Build a subquery selecting "ContactId" from "UsrSmsRecipient"
filtered by "SmsText," "PhoneNumber," and "SmsResponseId." */
private Query GetSelectByParticipantResponses() {
var responseSelect = new Select(UserConnection)
.Column("ContactId")
.From("UsrSmsRecipient")
.Where("SmsText").IsEqual(Column.Parameter(SmsText))
.And("PhoneNumber").IsEqual(Column.Parameter(PhoneNumber))
.And("SmsResponseId")
.In(Column.Parameters(SmsResponses)) as Select;
responseSelect.SpecifyNoLockHints(true);
return responseSelect;
}
/* Extend base query with response filters. */
protected override void CreateQuery() {
base.CreateQuery();
ExtendWithResponses();
}
}
} - Specify the
-
Click Save and publish.
5. Implement the business logic of the campaign element flow
-
Click Add → Source code.
-
Fill out the schema properties.
For this example, use the following schema properties.
Property
Property value
Code
UsrSmsConditionalTransitionElement
Title
Transition the SMS element
-
Specify the
ConditionalSequenceFlowElement
class as a parent class. -
Override the methods.
ApplyMetaDataValue(DataReader reader)
WriteMetaData(DataWriter writer)
Clone()
Copy(Dictionary<Guid, Guid> dictToRebind, Core.Campaign.CampaignSchema parentSchema)
CreateProcessFlowElement(UserConnection userConnection)
UsrSmsConditionalTransitionElementnamespace 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 = "SmsResponseId",
UsageType = DesignModeUsageType.NotVisible,
MetaPropertyName = SmsResponseIdPropertyName)]
[DesignModeProperty(
Name = "IsResponseBasedStart",
UsageType = DesignModeUsageType.Advanced,
MetaPropertyName = IsResponseBasedStartPropertyName)]
public class UsrSmsConditionalTransitionElement
: ConditionalSequenceFlowElement
{
/* Constants for property names used in metadata serialization. */
private const string SmsResponseIdPropertyName = "SmsResponseId";
private const string IsResponseBasedStartPropertyName = "IsResponseBasedStart";
/* Default constructor. */
public UsrSmsConditionalTransitionElement() {}
/* Copy constructor without rebind and schema. */
public UsrSmsConditionalTransitionElement(
UsrSmsConditionalTransitionElement source)
: this(source, null, null)
{}
/* Copy constructors for cloning the element in schema operations. */
public UsrSmsConditionalTransitionElement(
UsrSmsConditionalTransitionElement source,
Dictionary<Guid, Guid> dictToRebind,
Core.Campaign.CampaignSchema parentSchema)
: base(source, dictToRebind, parentSchema) {
IsResponseBasedStart = source.IsResponseBasedStart;
_smsResponseIdJson = JsonConvert.SerializeObject(
source.SmsResponseId
);
}
/* Store "SmsResponseId" in serialized JSON format. */
private string _smsResponseIdJson;
/* Internal helper property, return the collection of response IDs. */
private IEnumerable<Guid> Responses {
get {
return SmsResponseId;
}
}
/* Meta property that stores a collection of SMS response IDs. */
[MetaTypeProperty("{DC597899-B831-458A-A58E-FB43B1E266AC}")]
public IEnumerable<Guid> SmsResponseId {
get {
return !string.IsNullOrWhiteSpace(_smsResponseIdJson)
? JsonConvert.DeserializeObject<IEnumerable<Guid>>(_smsResponseIdJson)
: Enumerable.Empty<Guid>();
}
}
/* Meta property that defines if the transition starts based on a
response. */
[MetaTypeProperty("{3FFA4EA0-62CC-49A8-91FF-4096AEC561F6}",
IsExtraProperty = true, IsUserProperty = true)]
public virtual bool IsResponseBasedStart {
get;
set;
}
/* Read element metadata from the schema definition during
deserialization. */
protected override void ApplyMetaDataValue(DataReader reader) {
base.ApplyMetaDataValue(reader);
switch (reader.CurrentName) {
case SmsResponseIdPropertyName:
_smsResponseIdJson = reader.GetValue<string>();
break;
case IsResponseBasedStartPropertyName:
IsResponseBasedStart = reader.GetBoolValue();
break;
default:
break;
}
}
/* Write element metadata to the schema definition during serialization. */
public override void WriteMetaData(DataWriter writer) {
base.WriteMetaData(writer);
writer.WriteValue(
IsResponseBasedStartPropertyName,
IsResponseBasedStart,
false
);
writer.WriteValue(
SmsResponseIdPropertyName,
_smsResponseIdJson,
null
);
}
/* Create a clone of the schema element. */
public override object Clone() {
return new UsrSmsConditionalTransitionElement(this);
}
/* Create a copy of the element using rebind support for schema references. */
public override object Copy(
Dictionary<Guid, Guid> dictToRebind,
Core.Campaign.CampaignSchema parentSchema
) {
return new UsrSmsConditionalTransitionElement(
this,
dictToRebind,
parentSchema
);
}
/* Create the runtime process flow element that will be executed in
the campaign. Pass metadata values to the executable process element. */
public override ProcessFlowElement CreateProcessFlowElement(
UserConnection userConnection
) {
var sourceElement = SourceRef as UsrSmsElement;
var executableElement = new UsrSmsConditionalTransitionFlowElement {
UserConnection = userConnection,
SmsResponses = SmsResponseId,
PhoneNumber = sourceElement.PhoneNumber,
SmsText = sourceElement.SmsText
};
InitializeCampaignProcessFlowElement(executableElement);
InitializeCampaignTransitionFlowElement(executableElement);
InitializeConditionalTransitionFlowElement(executableElement);
return executableElement;
}
}
} -
Click Save and publish.
6. Extend campaign connector manager
-
Click Add → Module.
-
Fill out the schema properties.
For this example, use the following schema properties.
Property
Property value
Code
UsrSmsCampaignConnectorManager
Title
SMS campaign connector manager
-
Add the source code.
UsrSmsCampaignConnectorManagerdefine("UsrSmsCampaignConnectorManager", [], function() {
Ext.define("Terrasoft.UsrSmsCampaignConnectorManager", {
/* Override the standard "CampaignConnectorManager" module to extend
its logic with custom SMS connector handling. */
override: "Terrasoft.CampaignConnectorManager",
/* Initialize mapping between the campaign element (arrow source) and
the corresponding transition schema (arrow type). Add a mapping for
the custom SMS element. */
initMappingCollection: function() {
this.callParent(arguments);
this.connectorTypesMappingCollection.addIfNotExists(
"UsrSmsElement",
"Terrasoft.UsrProcessSmsConditionalTransitionSchema"
);
},
/* Virtual method override. Called before the arrow is replaced with
a new type. Can be used to execute additional preprocessing logic
when the connector type changes. */
additionalBeforeChange: function(prevTransition, sourceItem, targetItem) {
/* Implement custom preprocessing logic. */
},
/* Virtual method override. Populate additional properties of the
new arrow based on the properties of the previous arrow. */
fillAdditionalProperties: function(prevElement, newElement) {
if (newElement.getTypeInfo().typeName === "UsrProcessSmsConditionalTransitionSchema") {
/* Copy the configured SMS responses from the previous arrow,
if they exist. */
newElement.smsResponseId = prevElement.smsResponseId
? prevElement.smsResponseId
: null;
/* Copy the flag that defines whether the transition is response-based. */
newElement.isResponseBasedStart = prevElement.isResponseBasedStart
? prevElement.isResponseBasedStart
: false;
}
}
});
}); -
Click Save.
7. Connect the business logic of the campaign element flow
-
Click Add → Replacing view model.
-
Fill out the schema properties.
For this example, use the following schema properties.
Property
Property value
Parent object
BootstrapModulesV2
-
Add the source code.
BootstrapModulesV2/* Define a replacing view model schema named "BootstrapModulesV2." This schema
is used to include additional client modules that must be loaded during the
app startup process. Custom SMS campaign connector manager and the custom
conditional transition schema are added as dependencies.
"UsrSmsCampaignConnectorManager" is the previously created custom connector
manager that overrides "CampaignConnectorManager" and implements SMS-specific
flow logic.
"UsrProcessSmsConditionalTransitionSchema" is the custom schema that defines
the conditional transition for the SMS campaign element.*/
define("BootstrapModulesV2", ["UsrSmsCampaignConnectorManager",
"UsrProcessSmsConditionalTransitionSchema"], function() {});
/* Explicitly require the same dependencies to ensure they are loaded at
runtime when the app starts. */
require(["UsrSmsCampaignConnectorManager","UsrProcessSmsConditionalTransitionSchema"]); -
Click Save.
View the result
- Open the Campaigns section.
- Create a campaign that has arbitrary parameters. Instructions Add a campaign (user documentation).
- Click Edit campaign flow.
- Add the SMS element to the canvas.
- Add arbitrary elements to the canvas. For example, Exit from campaign.
- Add a flow between the SMS and Exit from campaign elements.
As a result, the flow executes the following actions:
- Omit the responses to the bulk SMS. View the result >>>
- Handle delivered SMS responses and SMS responses whose receiving ends with error. View the result >>>
- Receive SMS responses, regardless of their value. View the result >>>
If you need to use the current example in your instance, we recommend creating the Bulk SMS object schema instead of storing SMS details directly in campaign elements. The UsrTestSmsElement
and UsrSmsConditionalTransitionElement
object schemas should reference only the ID of the bulk SMS record, rather than fields such as SmsText
or PhoneNumber
. The UsrTestSmsCampaignProcessElement
executed element should implement the audience registration logic in its Execute()
method. For example, by adding contacts to the bulk SMS audience. A separate mechanism (or multiple mechanisms) must handle sending SMS messages and recording participant responses. Based on these responses, the campaign flow will transfer the audience to the next campaign step.
Source code
- UsrProcessSmsConditionalTransitionSchema
- CampaignElementSchemaManagerEx
- UsrSmsConditionalTransitionPropertiesPanel
- UsrSmsConditionalTransitionFlowElement
- UsrSmsConditionalTransitionElement
- UsrSmsCampaignConnectorManager
- BootstrapModulesV2
define("UsrProcessSmsConditionalTransitionSchema", ["CampaignEnums",
"UsrProcessSmsConditionalTransitionSchemaResources",
"ProcessCampaignConditionalSequenceFlowSchema"], function(CampaignEnums) {
Ext.define("Terrasoft.manager.UsrProcessSmsConditionalTransitionSchema", {
/* Parent schema. */
extend: "Terrasoft.ProcessCampaignConditionalSequenceFlowSchema",
/* Alternative class name. */
alternateClassName: "Terrasoft.UsrProcessSmsConditionalTransitionSchema",
/* ID of the new campaign element. */
managerItemUId: "4b5e70b0-a631-458e-ab22-856ddc913444",
/* Mixins that extend functionality. */
mixins: {
parametrizedProcessSchemaElement: "Terrasoft.ParametrizedProcessSchemaElement"
},
/* Full name of the class that implements the campaign element logic.
This class saves and reads the element properties from the schema
properties. Since the class is placed in the regular package, use the
"Terrasoft.Configuration.UsrSmsConditionalTransitionElement,
Terrasoft.Configuration" typeName. If the class is placed in the
assembly package, use
typeName: "Terrasoft.Configuration.UsrSmsConditionalTransitionElement,
SomeAssemblyPackageName" */
typeName: "Terrasoft.Configuration.UsrSmsConditionalTransitionElement," +
"Terrasoft.Configuration",
/* Arrow element name used for linking campaign elements. */
connectionUserHandleName: "SmsConditionalTransition",
/* Schema name of the edit page. */
editPageSchemaName: "UsrSmsConditionalTransitionPropertiesPanel",
/* Element type. */
elementType: CampaignEnums.CampaignSchemaElementTypes.CONDITIONAL_TRANSITION,
/* ID of the SMS response option linked to the transition. */
smsResponseId: null,
/* Define whether the transition is based on a specific SMS
response condition. */
isResponseBasedStart: false,
/* Expand the properties for serialization. */
getSerializableProperties: function() {
var baseSerializableProperties = this.callParent(arguments);
Ext.Array.push(baseSerializableProperties, ["smsResponseId",
"isResponseBasedStart"]);
return baseSerializableProperties;
}
});
return Terrasoft.UsrProcessSmsConditionalTransitionSchema;
}
);
require(["CampaignElementSchemaManager", "UsrProcessSmsConditionalTransitionSchema"],
function() {
/* Register the "UsrProcessSmsConditionalTransitionSchema" in the Campaign Designer. */
var coreElementClassNames = Terrasoft.CampaignElementSchemaManager.coreElementClassNames;
/* Add the custom schema to the list of available schemas. */
coreElementClassNames.push({
itemType: "Terrasoft.UsrProcessSmsConditionalTransitionSchema"
});
}
);
define("UsrSmsConditionalTransitionPropertiesPanel", ["BusinessRuleModule",
"LookupUtilities"], function(BusinessRuleModule, LookupUtilities) {
return {
messages: {},
/* Schema attributes. */
attributes: {
/* Attribute that stores data about list of participants. */
"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"
}
}
},
/* Attribute that stores data about participants' responses. */
"ReactionMode": {
"dataValueType": this.Terrasoft.DataValueType.LOOKUP,
"type": this.Terrasoft.ViewModelColumnType.VIRTUAL_COLUMN,
"isRequired": true
},
/* Attribute that stores data about SMS delivery status. */
"IsSmsDelivered": {
"dataValueType": this.Terrasoft.DataValueType.BOOLEAN,
"type": this.Terrasoft.ViewModelColumnType.VIRTUAL_COLUMN
},
/* Attribute that stores data about error occurred while
receiving SMS. */
"IsErrorWhileReceiving": {
"dataValueType": this.Terrasoft.DataValueType.BOOLEAN,
"type": this.Terrasoft.ViewModelColumnType.VIRTUAL_COLUMN
}
},
/* Schema business rules. */
rules: {
/* Make the "ReactionConditionDecision" business rule
required when the "ReactionMode" attribute is equal "1." */
"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"
}
}]
}
}
},
/* Schema methods. */
methods: {
/* Return mapping of attributes to the "SMS response
type" ("SmsResponseType" code) lookup records. */
getResponseConfig: function() {
return {
"IsSmsDelivered": "5a9f08a4-c7f2-4dc8-9940-13ba1b2762cf",
"IsErrorWhileReceiving": "4a790626-bfbb-4ca7-a9f2-595000262561"
};
},
/* Bind the handler to the event that changes the "ReactionMode"
attribute value. */
subscribeEvents: function() {
this.callParent(arguments);
/* Subscribe to changes of the "ReactionMode" attribute
value. */
this.on(
"change:ReactionMode",
this.onReactionModeLookupChanged,
this
);
},
/* Handle the ReactionMode attribute change event. */
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);
}
},
/* Initialize attributes for displaying the page when opened. */
initParameters: function(element) {
this.callParent(arguments);
var isResponseBasedStart = element.isResponseBasedStart;
this.initReactionMode(isResponseBasedStart);
this.initSmsResponses(element.smsResponseId);
},
/* Cut the string to the specified length and append an
ellipsis at the end. Auxiliary method. */
cutString: function(strValue, strLength) {
var ellipsis = Ext.String.ellipsis(
strValue.substring(strLength),
0
);
return strValue.substring(0, strLength) + ellipsis;
},
/* Set the status value to "Delivered." */
initIsSmsDelivered: function(value) {
if (value === undefined) {
value = this.get("IsSmsDelivered");
}
this.set("IsSmsDelivered", value);
},
/* Set the status value to "Error while receiving." */
initIsErrorWhileReceiving: function(value) {
if (value === undefined) {
var isErrorWhileReceiving = this.get("IsErrorWhileReceiving");
value = isErrorWhileReceiving;
}
this.set("IsErrorWhileReceiving", value);
},
/* Initialize the selected responses when opening the page. */
initSmsResponses: 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);
},
/* Initialize the "ReactionMode" attribute based on
"isResponseBasedStart." */
initReactionMode: function(value) {
var isDefault = !value;
this.setLookupValue(
isDefault,
"ReactionMode",
"WithCondition",
this
);
},
/* Parse JSON array of IDs. Return "[]" on error. */
getIds: function(idsJson) {
if (idsJson) {
try {
var ids = JSON.parse(idsJson);
if (this.Ext.isArray(ids)) {
return ids;
}
} catch (error) {
return [];
}
}
return [];
},
/* Populate "ReactionMode" attribute list from "ReactionModeEnum". */
onPrepareReactionModeList: function(filter, list) {
this.prepareList("ReactionModeEnum", list, this);
},
/* Save UI values back to the process element before save. */
saveValues: function() {
this.callParent(arguments);
var element = this.get("ProcessElement");
var isResponseBasedStart = this.getIsReactionModeWithConditions();
element.isResponseBasedStart = isResponseBasedStart;
element.smsResponseId = this.getSmsResponseId(isResponseBasedStart);
},
/* Serialize selected response IDs to JSON.*/
getSmsResponseId: 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);
},
/* Return lookup underlying ID or null. */
getLookupValue: function(parameterName) {
var value = this.get(parameterName);
return value ? value.value : null;
},
/* Help context code for the property page. */
getContextHelpCode: function() {
return "CampaignConditionalSequenceFlow";
},
/* Return "true" if "ReactionMode" is set to "1." */
getIsReactionModeWithConditions: function() {
return this.isLookupValueEqual("ReactionMode", "1", this);
},
/* Return source element of the transition if available. */
getSourceElement: function() {
var flowElement = this.get("ProcessElement");
if (flowElement) {
return flowElement.findSourceElement();
}
return null;
},
/* Format question caption including the source element name. */
getQuestionCaption: function() {
var caption = this.get("Resources.Strings.ReactionModeCaption");
caption = this.Ext.String.format(
caption,
this.getSourceElement().getCaption()
);
return caption;
}
},
/* The schema view description. */
diff: /**SCHEMA_DIFF*/[
/* Page 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 label. */
{
"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"]
}
}
},
/* "ReactionMode" 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"]
}
},
/* "IsSmsDelivered" checkbox.*/
{
"operation": "insert",
"parentName": "ReactionContainer",
"propertyName": "items",
"name": "IsSmsDelivered",
"values":
{
"wrapClass": ["t-checkbox-control"],
"visible":
{
"bindTo": "ReactionMode",
"bindConfig":
{
converter: "getIsReactionModeWithConditions"
}
},
"caption":
{
"bindTo": "Resources.Strings.IsSmsDelivered"
},
"layout":
{
"column": 0,
"row": 2,
"colSpan": 22
}
}
},
/* "IsErrorWhileReceiving" checkbox.*/
{
"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*/
};
}
);
namespace Terrasoft.Configuration
{
using System;
using System.Collections.Generic;
using System.Linq;
using Terrasoft.Common;
using Terrasoft.Core.DB;
/* Define a custom element flow. */
public class UsrSmsConditionalTransitionFlowElement
: ConditionalTransitionFlowElement
{
/* Assign the "SmsText" attribute value in the class. */
public string SmsText {
get;
set;
}
/* Assign the "PhoneNumber" attribute value in the class. */
public string PhoneNumber {
get;
set;
}
/* Assign the collection of SMS response IDs in the class. */
public IEnumerable<Guid> SmsResponses {
get;
set;
}
/* Extend the base "TransitionQuery" with participant responses
filter. */
private void ExtendWithResponses() {
TransitionQuery.CheckArgumentNull("TransitionQuery");
if (SmsResponses.Any()) {
Query responseSelect = GetSelectByParticipantResponses();
TransitionQuery.And("ContactId").In(responseSelect);
}
}
/* Build a subquery selecting "ContactId" from "UsrSmsRecipient"
filtered by "SmsText," "PhoneNumber," and "SmsResponseId." */
private Query GetSelectByParticipantResponses() {
var responseSelect = new Select(UserConnection)
.Column("ContactId")
.From("UsrSmsRecipient")
.Where("SmsText").IsEqual(Column.Parameter(SmsText))
.And("PhoneNumber").IsEqual(Column.Parameter(PhoneNumber))
.And("SmsResponseId")
.In(Column.Parameters(SmsResponses)) as Select;
responseSelect.SpecifyNoLockHints(true);
return responseSelect;
}
/* Extend base query with response filters. */
protected override void CreateQuery() {
base.CreateQuery();
ExtendWithResponses();
}
}
}
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 = "SmsResponseId",
UsageType = DesignModeUsageType.NotVisible,
MetaPropertyName = SmsResponseIdPropertyName)]
[DesignModeProperty(
Name = "IsResponseBasedStart",
UsageType = DesignModeUsageType.Advanced,
MetaPropertyName = IsResponseBasedStartPropertyName)]
public class UsrSmsConditionalTransitionElement
: ConditionalSequenceFlowElement
{
/* Constants for property names used in metadata serialization. */
private const string SmsResponseIdPropertyName = "SmsResponseId";
private const string IsResponseBasedStartPropertyName = "IsResponseBasedStart";
/* Default constructor. */
public UsrSmsConditionalTransitionElement() {}
/* Copy constructor without rebind and schema. */
public UsrSmsConditionalTransitionElement(
UsrSmsConditionalTransitionElement source)
: this(source, null, null)
{}
/* Copy constructors for cloning the element in schema operations. */
public UsrSmsConditionalTransitionElement(
UsrSmsConditionalTransitionElement source,
Dictionary<Guid, Guid> dictToRebind,
Core.Campaign.CampaignSchema parentSchema)
: base(source, dictToRebind, parentSchema) {
IsResponseBasedStart = source.IsResponseBasedStart;
_smsResponseIdJson = JsonConvert.SerializeObject(
source.SmsResponseId
);
}
/* Store "SmsResponseId" in serialized JSON format. */
private string _smsResponseIdJson;
/* Internal helper property, return the collection of response IDs. */
private IEnumerable<Guid> Responses {
get {
return SmsResponseId;
}
}
/* Meta property that stores a collection of SMS response IDs. */
[MetaTypeProperty("{DC597899-B831-458A-A58E-FB43B1E266AC}")]
public IEnumerable<Guid> SmsResponseId {
get {
return !string.IsNullOrWhiteSpace(_smsResponseIdJson)
? JsonConvert.DeserializeObject<IEnumerable<Guid>>(_smsResponseIdJson)
: Enumerable.Empty<Guid>();
}
}
/* Meta property that defines if the transition starts based on a
response. */
[MetaTypeProperty("{3FFA4EA0-62CC-49A8-91FF-4096AEC561F6}",
IsExtraProperty = true, IsUserProperty = true)]
public virtual bool IsResponseBasedStart {
get;
set;
}
/* Read element metadata from the schema definition during
deserialization. */
protected override void ApplyMetaDataValue(DataReader reader) {
base.ApplyMetaDataValue(reader);
switch (reader.CurrentName) {
case SmsResponseIdPropertyName:
_smsResponseIdJson = reader.GetValue<string>();
break;
case IsResponseBasedStartPropertyName:
IsResponseBasedStart = reader.GetBoolValue();
break;
default:
break;
}
}
/* Write element metadata to the schema definition during serialization. */
public override void WriteMetaData(DataWriter writer) {
base.WriteMetaData(writer);
writer.WriteValue(
IsResponseBasedStartPropertyName,
IsResponseBasedStart,
false
);
writer.WriteValue(
SmsResponseIdPropertyName,
_smsResponseIdJson,
null
);
}
/* Create a clone of the schema element. */
public override object Clone() {
return new UsrSmsConditionalTransitionElement(this);
}
/* Create a copy of the element using rebind support for schema references. */
public override object Copy(
Dictionary<Guid, Guid> dictToRebind,
Core.Campaign.CampaignSchema parentSchema
) {
return new UsrSmsConditionalTransitionElement(
this,
dictToRebind,
parentSchema
);
}
/* Create the runtime process flow element that will be executed in
the campaign. Pass metadata values to the executable process element. */
public override ProcessFlowElement CreateProcessFlowElement(
UserConnection userConnection
) {
var sourceElement = SourceRef as UsrSmsElement;
var executableElement = new UsrSmsConditionalTransitionFlowElement {
UserConnection = userConnection,
SmsResponses = SmsResponseId,
PhoneNumber = sourceElement.PhoneNumber,
SmsText = sourceElement.SmsText
};
InitializeCampaignProcessFlowElement(executableElement);
InitializeCampaignTransitionFlowElement(executableElement);
InitializeConditionalTransitionFlowElement(executableElement);
return executableElement;
}
}
}
define("UsrSmsCampaignConnectorManager", [], function() {
Ext.define("Terrasoft.UsrSmsCampaignConnectorManager", {
/* Override the standard "CampaignConnectorManager" module to extend
its logic with custom SMS connector handling. */
override: "Terrasoft.CampaignConnectorManager",
/* Initialize mapping between the campaign element (arrow source) and
the corresponding transition schema (arrow type). Add a mapping for
the custom SMS element. */
initMappingCollection: function() {
this.callParent(arguments);
this.connectorTypesMappingCollection.addIfNotExists(
"UsrSmsElement",
"Terrasoft.UsrProcessSmsConditionalTransitionSchema"
);
},
/* Virtual method override. Called before the arrow is replaced with
a new type. Can be used to execute additional preprocessing logic
when the connector type changes. */
additionalBeforeChange: function(prevTransition, sourceItem, targetItem) {
/* Implement custom preprocessing logic. */
},
/* Virtual method override. Populate additional properties of the
new arrow based on the properties of the previous arrow. */
fillAdditionalProperties: function(prevElement, newElement) {
if (newElement.getTypeInfo().typeName === "UsrProcessSmsConditionalTransitionSchema") {
/* Copy the configured SMS responses from the previous arrow,
if they exist. */
newElement.smsResponseId = prevElement.smsResponseId
? prevElement.smsResponseId
: null;
/* Copy the flag that defines whether the transition is response-based. */
newElement.isResponseBasedStart = prevElement.isResponseBasedStart
? prevElement.isResponseBasedStart
: false;
}
}
});
});
/* Define a replacing view model schema named "BootstrapModulesV2." This schema
is used to include additional client modules that must be loaded during the
app startup process. Custom SMS campaign connector manager and the custom
conditional transition schema are added as dependencies.
"UsrSmsCampaignConnectorManager" is the previously created custom connector
manager that overrides "CampaignConnectorManager" and implements SMS-specific
flow logic.
"UsrProcessSmsConditionalTransitionSchema" is the custom schema that defines
the conditional transition for the SMS campaign element.*/
define("BootstrapModulesV2", ["UsrSmsCampaignConnectorManager",
"UsrProcessSmsConditionalTransitionSchema"], function() {});
/* Explicitly require the same dependencies to ensure they are loaded at
runtime when the app starts. */
require(["UsrSmsCampaignConnectorManager","UsrProcessSmsConditionalTransitionSchema"]);
Resources
Package with example implementation (campaign element)
Package with example implementation (flow for campaign element)