Deep dive: Generate repeatable groups from associated records
| Warning | Work in progress! We are in the process of updating the information on this page. Subject to change. |
In this guide, we will demonstrate how a custom JavaScript function can be used to generate as many repeatable groups as there are associated records of the parent. This use case showcases a scenario where we open a questionnaire directly from a work order, with as many repeatable groups as there are associated work order incidents. These repeatable groups are automatically generated based on the number of related incidents. Each group contains a lookup question filled with an incident.
Prerequisites
For this script to work, your questionnaire must contain the following:
- Workorder lookup question.
- Repeatable group with workorder lookup incident question.
Download sample files
For your convenience, we are providing you sample file: [script]
Script
This script is explicitly built for workorder and workorder incidents. If you wish to use the script in another scenario, redefine these variables and modify the fetches.
const CHECKLIST_STEP_GROUP_NAME = "group";
const CHECKLIST_STEP_GROUP_NAME = "group";
const WO_INCIDENT_ENTITY_NAME = "msdyn_workorderincident";
const WO_INCIDENT_LOOKUP_QUESTION_NAME = "work-order-incident";
const WORKORDER_LOOKUP_QUESTION_NAME = "resco_workorder";
let templateGroupId = null;
let loadedWOIncidents = [];
let workOrderId = null;
const LOCALIZED_LOADING_MESSAGE = "Loading..."; // can be load from MobileCRM.Localization
let contentDefinitions = [];
let waitDialog = null;
const QUESTION_NAMES = {
INSTRUCTIONS: "-label-and-description", // question1, "-label-and-description" is the name of the question defined in the Questionnaire Designer
// add more question which content should be loaded from the step definition
}
// wait dialog needs to be always closed, otherwise the UI will be blocked
function closeWaitDialog() {
if (waitDialog !== null) {
waitDialog.close();
waitDialog = null;
}
}
function onError(text) {
closeWaitDialog();
MobileCRM.bridge.alert(text);
}
window.onload = function () {
initialize();
}
function initialize() {
waitDialog = MobileCRM.UI.Form.showPleaseWait(LOCALIZED_LOADING_MESSAGE);
generateQuestionnaireContent();
}
function generateQuestionnaireContent() {
try {
MobileCRM.UI.QuestionnaireForm.requestObject(function (questionnaireForm) {
let questionnaire = questionnaireForm.questionnaire;
let workOrderValue = questionnaireForm.questions.find(q => q.name == WORKORDER_LOOKUP_QUESTION_NAME);
workOrderId = workOrderValue.value.id;
loadTemplateGroupId(questionnaire.properties.resco_templateid.id)
.then((loadedTemplateGroupId) => {
templateGroupId = loadedTemplateGroupId;
generateGroups();
})
.catch((error) => {
onError(error);
});
}, function (err) {
onError("Error QuestionnaireForm requestObject: " + err);
}, null);
} catch (error) {
onError(error);
}
}
/**
* Load the group id of the template group. Note that the group id will change with newer verstion of the template.
* @param {string} templateId the id of the current questionnaire template
*/
function loadTemplateGroupId(templateId) {
return new Promise((resolve, reject) => {
var entity = new MobileCRM.FetchXml.Entity("resco_questiongroup");
entity.addAttribute("resco_questiongroupid");
var filter = new MobileCRM.FetchXml.Filter();
filter.where("resco_name", "eq", CHECKLIST_STEP_GROUP_NAME);
filter.where("resco_questionnaireid", "eq", templateId);
entity.filter = filter;
var fetch = new MobileCRM.FetchXml.Fetch(entity);
fetch.execute(
"Array",
function (result) {
if (result.length > 0 && result[0][0] != undefined) {
var groupId = result[0][0];
resolve(groupId);
} else {
onError("Template group not found.");
}
},
function (error) {
onError("Error fetching question group: " + error
);
},
null
);
});
}
function generateGroups() {
LoadWorkOrderIncidents().then((checkListSteps) => {
checkListSteps.forEach((step) => {
loadedWOIncidents.push(step);
});
if (checkListSteps.length > 0) {
MobileCRM.UI.QuestionnaireForm.requestObject(function (questionnaireForm) {
let repeatableGroup = questionnaireForm.groups.find(g => g.templateGroup == templateGroupId);
let repeatableGroupAll = questionnaireForm.groups.filter(g => g.templateGroup == templateGroupId);
let repeatableGroupLastIndex = questionnaireForm.groups.find(g => g.repeatIndex == checkListSteps.length);
if (repeatableGroupLastIndex == undefined) {
repeatableGroup.repeatGroup(false, function () {
//two groups already exist, therefore lenght -2
repeatNext(checkListSteps.length - 2);
}, onError, null);
}
}, function (err) {
onError("Error QuestionnaireForm requestObject: " + err);
closeWaitDialog();
}, null);
}
}).catch((error) => {
onError(error);
});
}
/**
* Repeats the recursively
* @param {number} count the number of times the group should be repeated
*/
function repeatNext(count) {
if (count > 0) {
MobileCRM.UI.QuestionnaireForm.requestObject(function (questionnaireForm) {
let repeatableGroup = questionnaireForm.groups.find(g => g.templateGroup == templateGroupId);
repeatableGroup.repeatGroup(false, function () {
repeatNext(count - 1)
}, onError, null);
}, function (err) {
onError("Error QuestionnaireForm requestObject: " + err);
closeWaitDialog();
}, null);
}
else
{
// affter all groups are repeated, set the values
setGroupValues();
}
}
function LoadWorkOrderIncidents() {
return new Promise((resolve, reject) => {
var entity = new MobileCRM.FetchXml.Entity(WO_INCIDENT_ENTITY_NAME);
entity.addAttributes();
var filter = new MobileCRM.FetchXml.Filter();
filter.where("msdyn_workorder", "eq", workOrderId);
entity.filter = filter;
var fetch = new MobileCRM.FetchXml.Fetch(entity);
fetch.execute(
"DynamicEntities",
function (results) {
if (results.length > 0) {
resolve(results);
} else {
onError("No work order incidents found for the given work order.");
closeWaitDialog();
}
},
function (error) {
onError("Error fetching work order incidents: " + error);
closeWaitDialog();
},
null
);
});
}
/**
* Load the checklist steps from the definition entity
*/
function setGroupValues() {
MobileCRM.UI.QuestionnaireForm.requestObject(function (questionnaireForm) {
for (let i = 0; i < loadedWOIncidents.length; i++) {
try {
let currentWorkOrderIncident = loadedWOIncidents[i];
let currentWorkOrderIncidentReference = new MobileCRM.Reference(currentWorkOrderIncident.entityName, currentWorkOrderIncident.id, currentWorkOrderIncident.primaryName);
//let group = questionnaireForm.groups.find(g => g.id == _repeatable_roup_id);
// find the group with the lowest index
let newGroup = questionnaireForm.groups.find(g => g.repeatIndex == i + 1);
// construct the group index suffix. e.g. #001, #002, #003, ...
// "-label-and-description#001"
// "-label-and-description#002", ...
// this index suffix is added to the question name to identify the question in the group with that index as e.g., "<resco_questionname>#003"
let currentGroupIndex = `#${String(newGroup.repeatIndex).padStart(3, '0')}`;
//find specific question within the group by name and group id
let workOrderIncidentQuestion = questionnaireForm.questions.find(q => q.groupId == newGroup.id && q.name == WO_INCIDENT_LOOKUP_QUESTION_NAME + currentGroupIndex);
if (workOrderIncidentQuestion != null) {
workOrderIncidentQuestion.trySetAnswer(currentWorkOrderIncidentReference, function (error) {
onError("Error setting asset question answer: " + error);
}, null);
}
/*
prefill more questions here
let <ABC_QUESTION> = questionnaireForm.questions.find(q => q.groupId == newGroup.id && q.name == QUESTION_NAMES.ABC_QUESTION + currentGroupIndex);
if (ABC_QUESTION != null)
ABC_QUESTION.label = groupDefinition.ABC_QUESTIONVALUE;
*/
} catch (error) {
onError(error);
closeWaitDialog();
}
}
MobileCRM.UI.QuestionnaireForm.focusQuestion("instructions", onError, null);
closeWaitDialog();
}, function (err) {
onError("Error QuestionnaireForm requestObject: " + err);
closeWaitDialog();
}, null);
}
Execution order
These are the main functions used in the script:
- generateQuestionnaireContent
- loads the questionnaire context
- Initializes loadTemplateGroupId and generates groups
- loadTemplateGroupId
- finds the ID of the group we want to repeat
- generateGroups
- Initializes LoadWorkorderIncidents and repeatNext, by invoking repeatGroup, we already have two instances of group, therefore we subtract 2.
- LoadWorkorderIncident
- Fetches and returns dynamic entities with incidents.
- repeatNext
- generates as many repeatable groups as there are incidents
- initializes setGroupValues.
- setGroupValues
- fills in the questions from the incident into already created repeatable groups.