Jump to content

Deep dive: Generate repeatable groups from associated records

From Resco's Wiki
Revision as of 13:18, 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]

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.