Microsoft Dynamics/Power Platform admins: Action required for Resco Mobile CRM 18.2

Deep dive: Generate repeatable groups from associated records

From Resco's Wiki
Jump to navigation Jump to search
Warning Work in progress! We are in the process of updating the information on this page. Subject to change.
Deep dives

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.

Execution

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: [script]

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 prepare the questionnaire. To demonstrate how to call custom function from questionnaires, see how it is done in the attached sample template Aggregated Repeated Group v3.zip. Alternatively, you can do the same to reuse the function in your questionnaire: just adjust the values of shared variables to the names of your repeating group and aggregated question.

  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.
    • Go to questionnaire Options and on the Rules tab, set Script Path to "generateRepeatableGroups.html".
      enter script path

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.
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.

Script

const CHECKLIST_STEP_GROUP_NAME = "group";
        const WO_INCIDENT_ENTITY_NAME = "msdyn_workorderincident";
        const WO_INCIDENT_LOOKUP_QUESTION_NAME = "work-order-inlookupcident";
        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
		 */
 
  /**
   * 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.repeatGroupAsync()
				.then(() => {
				repeatNext(count - 1)
				})
				.catch(err => {
				onError("Error QuestionnaireForm requestObject: " + err);
				closeWaitDialog();
				})
				}, 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);
		}