Jump to content

Deep dive: Generate repeatable groups from associated records

From Resco's Wiki
Revision as of 13:52, 19 November 2025 by Marek Rodak (talk | contribs) (Script)
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]

About the 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.
The script consists of two main async functions, repeatGroup and trySetAnswer. To successfully generate repeatable groups and then fill them with data, we have do each step separately. The reason is that if we did this in sequence, trySetAnswer might try to set an answer for a group that doesn't yet exist. Therefore, we use successCallback of the

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.