Join the #BuildWithBuildAgent Challenge! Get recognized, earn exclusive swag, and inspire the ServiceNow Community with what you can build using Build Agent.  Join the Challenge.

Need support for OAM image scale survey alignment changes from vertical to horizontal

Asanthoshini
Tera Contributor

Need support for OAM image scale survey alignment changes from vertical to horizontal
Description: We used Image Scale survey type for customer service ratings and integrated OAM for Outlook responses. I'm aiming to display ratings horizontally instead of vertically.

I have changed the code the script include by changing the columnset and column to rowset and row.

Still I didn't get the required result. Here is the script:

 

var SurveyAdaptiveCardGenerator_Buckeye = Class.create();
SurveyAdaptiveCardGenerator_Buckeye.prototype = {
initialize: function(instanceId) {
this.instanceId = instanceId;
var instanceGr = new GlideRecord('asmt_assessment_instance');
if ((!instanceId || !instanceGr.get(instanceId) || instanceGr.getTableName() !== 'asmt_assessment_instance')) {
this.abort = true;
gs.info("OAM - aborting due to invalid survey instance record: " + instanceId);
return;
}
this.instanceGr = instanceGr;
this.surveyGr = instanceGr.metric_type.getRefRecord();
if (!this.surveyGr || !this.surveyGr.isValidRecord() || !this.surveyGr.ms_adaptive_cards) {
this.abort = true;
gs.info("OAM - aborting due to invalid survey definition record or oam flag not checked");
return;
}

this.surveyId = this.surveyGr.getUniqueValue();
this.actionBody = {};
this.card = {};
this.baseURI = new sn_ms_oam.AdaptiveCardCustomURLHelper().getServiceNowURL();
},

abort: false,
metrixAnswerPrefix: "ASMTQUESTION:",
metricAddInfoPrefix: "ADDINFO:ASMTQUESTION:",
metrixMultiAnswerPrefix: "ASMTDEFINITION:",
metricMultiAddInfoPrefix: "ADDINFO:ASMTDEFINITION:",
datePrefix: "OAM_Date", // Only for "Date/Time" question, or "Date" question
timePrefix: "OAM_Time", // Only for "Date/Time" question

generate: function() {
return this.generateSurveyCard();
},

getRecipients: function() {
return this._getExpectedActors();
},
generateSurveyCard: function() {
if (this.abort)
return "";
if (!new sn_ms_oam.OAMSurveyUtil().allInstanceQuestionsSupported(this.instanceId)){
gs.info("OAM - survey instance " + this.instanceId + " contains unsupported survey questions. Aborting.");
return "";
}

this.card = {
"type": "AdaptiveCard",
"version": "1.0",
"originator": gs.getProperty('sn_ms_oam.outlookactionable.originator'),
"padding": "none",
"expectedActors": this._getExpectedActors(),
"hideOriginalBody": true,
"body": []
};

this._addIntroduction();
this._processSurveyCategories();
this._addActions(this.surveyGr);
return JSON.stringify(this.card);
},

_getExpectedActors: function(){
var actors = [], email;
email = this.instanceGr.user.email.toString();
if(email) {
actors.push(email);
}
return actors;
},

getSuccessResponseBody: function() {
if (this.abort)
return "";

this.card = {
"type": "AdaptiveCard",
"version": "1.0",
"originator": gs.getProperty('sn_ms_oam.outlookactionable.originator'),
"style": "emphasis",
"hideOriginalBody": true,
"body": [],
};
this._addCompleteMessage();
this._addEndNote();
return JSON.stringify(this.card);
},

getErrorResponseBody: function() {
if (this.abort)
return "";

this.card = {
"type": "AdaptiveCard",
"version": "1.0",
"originator": gs.getProperty('sn_ms_oam.outlookactionable.originator'),
"style": "emphasis",
"hideOriginalBody": true,
"body": [],
};
return JSON.stringify(this.card);
},

_addCompleteMessage: function() {
var messageObj = {
"type": "TextBlock",
"text": "You have completed this survey",
"weight": "bolder",
"size": "default",
"color": "default",
"wrap": true,
"separator": true
};
this.card.body.push(messageObj);
},

_addEndNote: function() {
var endNote = this.surveyGr.end_note.toString();
if (!endNote)
return;

var endNoteObj = {
"type": "TextBlock",
"text": this._html2Markdown(endNote),
"size": "large",
"color": "default",
"wrap": true,
"separator": true
};
this.card.body.push(endNoteObj);
},

_html2Markdown: function(str) {
//strip html for now
return str.replace(/<(?:.|\n)*?>/gm, '');
},

_addIntroduction: function() {
var introduction = this.surveyGr.introduction.toString(),
introductionObj;
if (!introduction)
return;

introductionObj = {
"type": "Container",
"style": "emphasis",
"padding": "default",
"items": [
{
"type": "TextBlock",
"text": this._html2Markdown(introduction),
"size": "large",
"wrap": true,
"color": "default"
}
]
};
this.card.body.push(introductionObj);
},

_processSurveyCategories: function() {
var gr = new GlideRecord('asmt_metric_category'),
categoryNode;
gr.addQuery('metric_type', this.surveyId);
gr.orderBy('order');
gr.query();
while (gr.next()) {
categoryNode = {
"type": "Container",
"style": "default",
"separator": true,
"spacing": "none",
"padding": "default",
"items": [{
"type": "TextBlock",
"text": gr.name.toString(),
"weight": "bolder",
"size": "medium"
}]
};
this._addCategoryQuestions(gr.sys_id.toString(), categoryNode);
this.card.body.push(categoryNode);
}
},

_addCategoryQuestions: function(categoryId, categoryNode) {
var question = new GlideRecord('asmt_assessment_instance_question');
var questionNode;
var questionCallbackNode;
question.addQuery('instance', this.instanceId);
question.addQuery('category', categoryId);
question.orderBy('metric.order');
question.query();
while (question.next())
this._addQuestion(question, categoryNode);
},

_addQuestion: function(questionGr, categoryNode) {
var questionNode = this._getQuestionNode(questionGr);
if (questionNode) {
this._addQuestionToAction(questionGr);
categoryNode.items.push(questionNode);
}
},

_addQuestionToAction: function(questionGr) {
var metricGr = questionGr.metric.getRefRecord();
var metricId = metricGr.getUniqueValue();
var datatype = metricGr.datatype.toString();
var addInfo = metricGr.allow_add_info;
var questionId = questionGr.getUniqueValue();
var questionNode;
var questionInfoNode;

switch (datatype) {
case 'string':
case 'boolean':
case 'choice':
case 'long':
case 'percentage':
case 'checkbox':
case 'scale':
case 'numericscale':
this.actionBody[this.metrixAnswerPrefix + questionId + ''] = "{{" + questionId + ".value}}";
if (addInfo)
this.actionBody[this.metricAddInfoPrefix + questionId + ''] = "{{" + questionId + "_ADDINFO.value}}";
break;
case 'multiplecheckbox':
this.actionBody[this.metrixMultiAnswerPrefix + metricId + ''] = "{{" + metricId + ".value}}";
if (addInfo)
this.actionBody[this.metricMultiAddInfoPrefix + metricId] = "{{" + questionId + "_ADDINFO.value}}";
break;
case 'date':
this.actionBody[this.metrixAnswerPrefix + this.datePrefix + questionId] = "{{" + this.datePrefix + questionId + ".value}}";
if (addInfo)
this.actionBody[this.metricAddInfoPrefix + questionId + ''] = "{{" + questionId + "_ADDINFO.value}}";
break;
case 'datetime':
this.actionBody[this.metrixAnswerPrefix + this.timePrefix + questionId] = "{{" + this.timePrefix + questionId + ".value}}";
this.actionBody[this.metrixAnswerPrefix + this.datePrefix + questionId] = "{{" + this.datePrefix + questionId + ".value}}";
if (addInfo)
this.actionBody[this.metricAddInfoPrefix + questionId + ''] = "{{" + questionId + "_ADDINFO.value}}";
break;
case 'template':
// The template question with image option, or image scale question is only supported in one click survey.
// Also, template question doesn't have additional information field.
// The template question without image option should build body here.
this.actionBody[this.metrixAnswerPrefix + questionId + ''] = "{{" + questionId + ".value}}";
break;
}
},

_getQuestionCallbackNode: function(questionGr) {
var node = {
"id": questionGr.sys_id.toString(),
"value": "{{" + questionGr.sys_id + ".value}}"
};
return node;
},

_getQuestionNode: function(questionGr) {
var node;
var addInfoNode;
var metricGr = questionGr.metric.getRefRecord();
var addInfo = metricGr.allow_add_info;
var addInfoLabel = metricGr.getDisplayValue('add_info_label');
if (addInfo)
addInfoNode = {
"type": "Input.Text",
"id": questionGr.sys_id + "_ADDINFO",
"placeholder": addInfoLabel ? addInfoLabel : gs.getMessage("Additional Information")
};
var datatype = metricGr.datatype.toString();
var questionLabel = new global.AssessmentUtils().getQuestionLabel(questionGr);

var questionNode = {
"type": "Container",
"items": []
};

if (datatype !== 'checkbox') {
questionNode.items.push({
"type": "TextBlock",
"wrap": true,
"text": (metricGr.mandatory.toString() === 'true' ? '\\* ' : '') + questionLabel
});
}

switch (datatype) {
case 'string':
node = {
"type": "Input.Text",
"id": questionGr.getUniqueValue(),
"isMultiline": metricGr.string_option.toString() === 'multiline'
};
break;
case 'boolean':
node = {
"type": "Input.ChoiceSet",
"id": questionGr.getUniqueValue(),
"style": "expanded",
"choices": [{
"title": "Yes",
"value": "1"
},
{
"title": "No",
"value": "0"
}
]
};
break;
case 'multiplecheckbox':
node = {
"type": "Input.ChoiceSet",
"id": questionGr.metric.toString(),
"style": "expanded",
"choices": this._getChoicesForQuestion(metricGr.sys_id.toString()),
"isMultiSelect": true
};
break;
case 'choice':
node = {
"type": "Input.ChoiceSet",
"id": questionGr.getUniqueValue(),
"style": "compact",
"choices": this._getChoicesForQuestion(metricGr.sys_id.toString())
};
break;
case 'long':
node = {
"type": "Input.Number",
"id": questionGr.getUniqueValue(),
"min": metricGr.min.toString(),
"max": metricGr.max.toString(),
"placeholder": gs.getMessage("Range: {0} - {1}", [metricGr.min.toString(), metricGr.max.toString()])
};
break;
case 'percentage':
node = {
"type": "Input.Number",
"id": questionGr.getUniqueValue(),
"placeholder": gs.getMessage("% Range: {0} - {1}", [metricGr.min.toString(), metricGr.max.toString()]),
"min": metricGr.min.toString(),
"max": metricGr.max.toString()
};
break;
case 'date':
node = {
"type": "Input.Date",
"id": this.datePrefix + questionGr.getUniqueValue()
};
break;
case 'datetime':
node = {
"type": "ColumnSet",
"columns": [
{
"type": "Column",
"items": [
{
"type": "Input.Date",
"id": this.datePrefix + questionGr.getUniqueValue()
}
]
},
{
"type": "Column",
"items": [
{
"type": "Input.Time",
"id": this.timePrefix + questionGr.getUniqueValue(),
"placeholder": "Enter a time"
}
]
}
]
};
break;
case 'checkbox':
node = {
"type": "Input.Toggle",
"id": questionGr.getUniqueValue(),
"title": questionLabel
};
break;
case 'scale':
case 'numericscale':
node = {
"type": "Input.ChoiceSet",
"id": questionGr.getUniqueValue(),
"style": "expanded",
"isMultiSelect": false,
"choices": this._getChoicesForQuestion(metricGr.getUniqueValue())
};
break;
case 'imagescale':
node = this._buildNodeForImageOptionQuestion(questionGr, metricGr, "asmt_metric_definition", addInfo, addInfoNode);
break;
case 'template':
var templateGr = metricGr.template.getRefRecord();
if (templateGr.allow_image)
node = this._buildNodeForImageOptionQuestion(questionGr, metricGr, "asmt_template_definition", addInfo, addInfoNode);
else
node = {
"type": "Input.ChoiceSet",
"id": questionGr.getUniqueValue(),
"style": "expanded",
"choices": this._getChoicesForTemplateQuestion(templateGr)
};
break;
}

if (node) {
questionNode.items.push(node);

// For image scale question, we added additional information field separately
if (addInfo && datatype !== "imagescale")
questionNode.items.push(addInfoNode);
}
return questionNode;
},

/**
* Build the node for image scale question or template question with image option.
*
* @Param {GlideRecord} questionGr - The glide record of question instance.
* @Param {GlideRecord} metricGr - The glide record of assessment metric.
* @Param {String} optionDefTableName - the table that defines question options, such as "asmt_template_definition", or "asmt_metric_definition".
* @Param {Boolean} addInfo - True if we have additional information field for this question.
* @Param {Object} addInfoNode - the node for additional information field. Undefined if "addInfo" is false.
* @Param {Object} node
*/
_buildNodeForImageOptionQuestion: function(questionGr, metricGr, optionDefTableName, addInfo, addInfoNode) {
var node = {
"type": "Container",
"items": []
};
var imageIdArr = [];
var imageOptions = {};// Used for each submit button
var metricSysId = metricGr.getUniqueValue();
var grOptionDef = new GlideRecord(optionDefTableName);
if (optionDefTableName === "asmt_metric_definition")
grOptionDef.addQuery("metric", metricSysId);
else if (optionDefTableName === "asmt_template_definition") {
var templateGr = metricGr.template.getRefRecord();
grOptionDef.addQuery("template", templateGr.getUniqueValue());
}
grOptionDef.orderBy("value");
grOptionDef.query();
while (grOptionDef.next()) {
var selectedImageValue = grOptionDef.getDisplayValue('selected_image');// something like "a9ecff2087421010adcc66d107cb0b39.iix"
var unselectedImageValue = grOptionDef.getDisplayValue('unselected_image');// something like "a9ecff2087421010adcc66d107cb0b39.iix"
var imageValue = grOptionDef.getValue("value");
var imageDisplayValue = grOptionDef.getValue("display");
var optionDefSysId = grOptionDef.getUniqueValue();
var columns = [
{
"type": "Column",
"width": "auto",
"isVisible": true,// By default, we show unselected image
"id": metricSysId + "_" + optionDefSysId + "_unselected",// Template system id may be used by multiple metrics, so adding metric system id here to differentiate.
"items": [{
"type": "Image",
"url": gs.getProperty('glide.servlet.uri') + unselectedImageValue,
"size": "small",
"horizontalAlignment": "Center"
}],
"verticalContentAlignment": "Center"
}, {
"type": "Column",
"width": "auto",
"isVisible": false,// By default, we don't show selected image
"id": metricSysId + "_" + optionDefSysId + "_selected",// Template system id may be used by multiple metrics, so adding metric system id here to differentiate.
"items": [{
"type": "Image",
"url": gs.getProperty('glide.servlet.uri') + selectedImageValue,
"size": "small",
"horizontalAlignment": "Center"
}],
"verticalContentAlignment": "Center"
}, {
"type": "Column",
"width": 4,
"items": [
{
"type": "TextBlock",
"text": imageDisplayValue,
"id": metricSysId + "_" + optionDefSysId + "_text",
"horizontalAlignment": "Center"
}
],
"verticalContentAlignment": "Center"
}
];
imageIdArr.push(metricSysId + "_" + optionDefSysId + "_unselected");
imageIdArr.push(metricSysId + "_" + optionDefSysId + "_selected");
imageOptions[metricSysId + "_" + optionDefSysId + "_submit"] = imageValue;
var currentTemplateBody = {
"type": "ColumnSet",
"columns": columns,
"id": metricSysId + "_" + optionDefSysId,
"selectAction": {
"type":"Action.ToggleVisibility",
}
};
node.items.push(currentTemplateBody);
}// end of while loop

// Add targetElements for each template body that we implemented above
// The targetElements is the pre-set condition to control when to show each image
for (var i = 0; i < node["items"].length; i ++) {
var body = node["items"][i];
var bodyId = body["id"];
var sectionAction = body["selectAction"];
var targetElements = [];
var targetElement;
for (var j = 0; j < imageIdArr.length; j ++) {
var imageId = imageIdArr[j];
// Highlight the correct image, and unhighlight others
if (imageId.indexOf("unselected") >= 0 && imageId.indexOf(bodyId) >= 0) {
targetElement = {
"elementId": imageId,
"isVisible": false
};
}
else if (imageId.indexOf("selected") >= 0 && imageId.indexOf(bodyId) >= 0) {
targetElement = {
"elementId": imageId,
"isVisible": true
};
}
else if (imageId.indexOf("unselected") >= 0 && imageId.indexOf(bodyId) === -1) {
targetElement = {
"elementId": imageId,
"isVisible": true
};
}
else if (imageId.indexOf("selected") >= 0 && imageId.indexOf(bodyId) === -1) {
targetElement = {
"elementId": imageId,
"isVisible": false
};
}
targetElements.push(targetElement);
}

// Add targetElement for additional information field if any
if (addInfo) {
targetElements.push({
"elementId": questionGr.sys_id + "_ADDINFO",
"isVisible": true
});
}

// Add targetElements for submit buttons
for (var submitBtnId in imageOptions) {
// E.g., bodyId is metricSysId + "_" + optionDefSysId, and submitBtnId is metricSysId + "_" + optionDefSysId + "_submit". So submitBtnId should cover bodyId.
if (submitBtnId.indexOf(bodyId) !== -1)
targetElements.push({
"elementId": submitBtnId,
"isVisible": true
});
else
targetElements.push({
"elementId": submitBtnId,
"isVisible": false
});
}
sectionAction["targetElements"] = targetElements;
}

// Add additional information node here if it's configured.
// By default, it shouldn't be displayed until the user makes selection.
if (addInfo) {
addInfoNode["isVisible"] = true;
node.items.push(addInfoNode);
}

// Add individual submit button for each image option
for (var submitBtnId in imageOptions) {
var imageValue = imageOptions[submitBtnId];
var httpBody = {};
httpBody.type = "survey";
httpBody.sysparm_instance_id = this.instanceId;
httpBody.sysparm_action = "submit";
httpBody[this.metrixAnswerPrefix + questionGr.getUniqueValue() + ""] = imageValue;
if (addInfo)
httpBody[this.metricAddInfoPrefix + questionGr.getUniqueValue() + ''] = "{{" + questionGr.getUniqueValue() + "_ADDINFO.value}}";
node.items.push(
{
"type": "ActionSet",
"horizontalAlignment": "Left",
"id": submitBtnId,
"isVisible": false,// By default, we hide submit button for each image option until the user selects an option
"actions": [
{
"type": "Action.Http",
"title": "Submit",
"url": this.baseURI + "api/sn_ms_oam/oam/survey",
"method": "POST",
"headers": [
{
"name": "Content-Type",
"value": "application/json"
}
],
"body": JSON.stringify(httpBody),
"isPrimary": false
}
]
}
);
}
return node;
},

_getChoicesForQuestion: function(questionId) {
var choices = [],
choice;
var gr = new GlideRecord('asmt_metric_definition');
gr.addQuery('metric', questionId);
gr.orderBy('order');
gr.query();
while (gr.next()) {
choice = {};
choice.title = gr.display.toString();
choice.value = gr.value.toString();
choices.push(choice);
}
return choices;
},

/**
* @Param {GlideRecord} templateGr - asmt metric's template reference
* @return {Object[]} choices - An array of choice nodes
*/
_getChoicesForTemplateQuestion: function(templateGr) {
var choices = [],
choice;
var grTempDef = new GlideRecord('asmt_template_definition');
grTempDef.addQuery("template", templateGr.getUniqueValue());
grTempDef.orderBy("value");
grTempDef.query();
while (grTempDef.next()) {
var imageValue = grTempDef.getValue("value");
var imageDisplayValue = grTempDef.getValue("display");// Should be translated
choice = {};
choice.title = imageDisplayValue;
choice.value = imageValue;
choices.push(choice);
}
return choices;
},

_addActions: function(grMetricType) {
// If it's the one question survey and the question is template or image scale, then we don't show the final submit button.
// Because the template or image scale question already has a submit button for each option.
var oamSurveyUtil = new OAMSurveyUtil();
if (oamSurveyUtil.isSingleQuestionSurvey(grMetricType)) {
var grMetric = new GlideRecord("asmt_metric");
grMetric.addQuery("metric_type", grMetricType.getUniqueValue());
grMetric.query();
if (grMetric.next() && grMetric.getValue("datatype") === "imagescale") {
return;
}
// If it's a template question and it has image option, then ignore as well.
if (grMetric.getValue("datatype") === "template") {
var templateGr = grMetric.template.getRefRecord();
if (templateGr.allow_image)
return;
}
}

var actionsContainer = {
"type": "Container",
"style": "default",
"separator": true,
"spacing": "none",
"padding": "default",
"items": [
{
"type": "ActionSet",
"actions": []
}
]
};
var action = {
"method": "POST",
"title": "Submit",
"type": "Action.Http",
"headers": [
{
"name": "Content-Type",
"value": "application/json"
}
],
"url": this.baseURI + "api/sn_ms_oam/oam/survey"
};
actionsContainer.items[0].actions.push(action);
this.actionBody.type = "survey";
this.actionBody.sysparm_instance_id = this.instanceId;
this.actionBody.sysparm_action = "submit";
action.body = JSON.stringify(this.actionBody);
this.card.body.push(actionsContainer);
},

type: 'SurveyAdaptiveCardGenerator_Buckeye'
};

Attached the screenshots of the current and expected surveys.

Current Survey:

Asanthoshini_0-1706090321171.png

 

Expected Survey:

Asanthoshini_1-1706090357578.png

 

 

 


Kindly help me in alignment changes.

 

Thanks,

Santhoshini.

1 REPLY 1

Abbas_5
Tera Sage
Tera Sage

Hello @Asanthoshini,
Please refer to the below link:

https://www.servicenow.com/community/itsm-forum/image-scale-in-surveys/m-p/545024


Mark my correct and helpful, if it is helpful and please hit the thumbs-up button to mark it as the correct solution.
Thanks & Regards,
Abbas Shaik