Deep dive: Generate repeatable groups from associated records

From Resco's Wiki
Jump to navigation Jump to search

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. Each group contains a lookup question filled with an incident.

Execution

This is how the script execution looks like in the app:

Prerequisites

For this script to work, your questionnaire must contain the following:

  • Workorder lookup question.
  • Repeatable group with workorder lookup incident question.

If your scenario requires different fields or questions, you must redefine the script variables and fetches. Additionally, if you want to work with multiple questions in the repeatable group, these have to be predefined beforehand.

Download sample files

For your convenience, we are providing you sample file:
File:GenerateRepeatableGroupsScript.zip - JavaScript code

Set up your app project

In this section, we add the JavaScript code to your app project.

  1. Start Woodford and edit the app project used for opening the questionnaire.
  2. Go to Components > Offline HTML.
  3. Upload the attached file [script].html or use New File template and paste the script above inside the <script></script> tags. If you already have an HTML file for your questionnaire, just add the function to that file.
    adding js code to your project
  4. Verify that the Offline HTML folder also includes an updated version of JSBridge.js (used for facilitating the communication between our custom script and the app). Also, the uploaded script must link that file. See Including the JSBridge.js file for detailed instructions.
  5. Save all changes and publish the app project.

Set up the questionnaire

Finally, we need to add the script reference to the questionnaire.

  1. Start Questionnaire Designer.
  2. Click Import and select the downloaded questionnaire.
  3. Edit the questionnaire and verify the following settings. If you are using your own, custom questionnaire, use these as inspiration for recreating the needed functionality.
  4. Go to questionnaire Options and on the Rules tab, set Script Path to generateRepeatableGroups.html.
    enter script path in questionnaire

About the script

It consists of two main async functions, repeatGroup and trySetAnswer. To successfully generate repeatable groups and then fill them with data, we have to 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 repeatGroup to call trySetAnswer.

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 repeatGroupAsync, we already have two instances of group, therefore we subtract 2.
LoadIncidents
  • 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.

Script

// CONFIGURABLE PARAMETERS - Customize these for different entity types
	const PARAMS = {
		// name of the question group defined in Questionnaire Designer
		checklist_step_group_name: "group",
		// entity name containing the incident/task items (e.g., "task", "incident", "resco_incident")
		incident_entity_name: "task",
		// lookup question name for the incident entity within the group
		incident_lookup_question_name: "lookup",
		// parent lookup question name that links to the parent entity (e.g., "resco_regardingid" for workorder)
		parent_lookup_question_name: "resco_regardingid",
		// relationship field name used in FetchXml filter (e.g., "msdyn_workorder" for workorder)
		parent_relationship_field_name: "regardingobjectid",
	};

		let templateGroupId = null;
        let loadedIncidents = [];
        let parentEntityId = null;

		const LOCALIZED_LOADING_MESSAGE = "Loading..."; // can be load from MobileCRM.Localization

		let waitDialog = null;

		// 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 parentEntityValue = questionnaireForm.questions.find(q => q.name == PARAMS.parent_lookup_question_name);
				parentEntityId = parentEntityValue.value.id;
				if (parentEntityId == null) {
					onError("Parent entity reference is null.");
					return;
				}
					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);
			}
		}


		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", PARAMS.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() {
			LoadIncidents().then((checkListSteps) => {
                checkListSteps.forEach((step) => {
					loadedIncidents.push(step);
                });

				if (checkListSteps.length > 0) {
					MobileCRM.UI.QuestionnaireForm.requestObject(function (questionnaireForm) {

						let repeatableGroup = questionnaireForm.groups.find(g => g.templateGroup == templateGroupId);
						let repeatableGroupLastIndex = questionnaireForm.groups.find(g => g.repeatIndex == checkListSteps.length);

						if (repeatableGroupLastIndex == undefined) {
							
							repeatableGroup.repeatGroupAsync()
								.then(() => {
									//two groups already exist, therefore lenght -2
									repeatNext(checkListSteps.length - 2);
								})
								.catch((err) => {
									onError("Error repeating group: " + err);
									closeWaitDialog();
								});
						}

					}, function (err) {
						onError("Error QuestionnaireForm requestObject: " + err);
						closeWaitDialog();
					}, null);
				} else {
					// No incidents to load, close wait dialog
					closeWaitDialog();
				}

			}).catch((error) => {
				onError(error);
				closeWaitDialog();
			});
		}
			

        function repeatNext(count) {
			if (count > 0) {
				MobileCRM.UI.QuestionnaireForm.requestObject(function (questionnaireForm) {
				let repeatableGroup = questionnaireForm.groups.find(g => g.templateGroup == templateGroupId);
				repeatableGroup.repeatGroupAsync()
				.then(() => {
				repeatNext(count - 1)
				})
				.catch(err => {
				onError("Error QuestionnaireForm requestObject: " + err);
				closeWaitDialog();
				})
				}, function (err) { 
				onError("Error QuestionnaireForm requestObject: " + err);
				closeWaitDialog();
				}, null);
			}
			else
			{
				// after all groups are repeated, set the values
				setGroupValues();
			}
		}
			

        function LoadIncidents() {
            return new Promise((resolve, reject) => {
                var entity = new MobileCRM.FetchXml.Entity(PARAMS.incident_entity_name);
			entity.addAttributes();
            
            var filter = new MobileCRM.FetchXml.Filter();
            filter.where(PARAMS.parent_relationship_field_name, "eq", parentEntityId);
            entity.filter = filter;

            var fetch = new MobileCRM.FetchXml.Fetch(entity);
				fetch.execute(
					"DynamicEntities",
					function (results) {
						if (results.length > 0) {
							resolve(results);
						} else {
							onError("No incidents found for the given parent entity.");
						}
					},
					function (error) {
									onError("Error fetching incidents: " + error);
						},
					null
				);
            });
        }

		//Load the checklist steps from the definition entity

		function setGroupValues() {
			MobileCRM.UI.QuestionnaireForm.requestObject(function (questionnaireForm) {             
                for (let i = 0; i < loadedIncidents.length; i++) {
					try {
                        let currentIncident = loadedIncidents[i];
                        let currentIncidentReference = new MobileCRM.Reference(currentIncident.entityName, currentIncident.id, currentIncident.primaryName);

						let newGroup = questionnaireForm.groups.find(g => g.templateGroup == templateGroupId && g.repeatIndex == i + 1);
						let currentGroupIndex = `#${String(newGroup.repeatIndex).padStart(3, '0')}`;
                        let incidentQuestion = questionnaireForm.questions.find(q => q.groupId == newGroup.id && q.name == PARAMS.incident_lookup_question_name + currentGroupIndex);

                        if (incidentQuestion != null) {
                            incidentQuestion.trySetAnswer(currentIncidentReference, function (error) {
                                onError("Error setting incident question answer: " + error);
							}, null);
                        }									

					} catch (error) {
						onError(error);
						closeWaitDialog();
					}
				}
				closeWaitDialog();
				MobileCRM.bridge.alert("There were " + loadedIncidents.length + " incidents loaded.");

			}, function (err) {
				onError("Error QuestionnaireForm requestObject: " + err);
				closeWaitDialog();
			}, null);
		}