Deep dive: Create records from repeatable groups

From Resco's Wiki
Jump to navigation Jump to search

In this guide, we will demonstrate how custom JavaScript can create records from repeatable group instances in Questionnaire. This use case showcases the creation of Incidents. Each repeatable group instance includes an Asset lookup and a short-text question. In the newly created Incident, we want to see the parent Account, Questionnaire where it originated, associated Asset, and the short-text question as a title.

Execution

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


Prerequisites

For this script to function properly, the following conditions must be met:

  • Each question from the repeatable group you want to save into a record must exist as a field in the destination table (Asset lookup question --> Asset lookup field in the Incident).
  • Be wary of server logic when creating new records. All required fields must be filled in. Otherwise, the sync will fail, and records won't be uploaded.
Data type

Each question must be stored in a field that corresponds to its specific data type:

  • Lookup question → Lookup field.
  • Date and time question → Date and time field.
  • Option set question → Option set field (values in the option set must match accordingly in order to be saved correctly).

Download sample files

For your convenience, we are providing you sample file:
File:CreateFromRepeatableGroups.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 or create dedicated folder for questionnaire scripts.
  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 createFromRepeatableGroups.html (if the script is directly in root) or file://Questionnaire/createFromRepeatableGroups.html (if in subfolder).
    enter script path in questionnaire

About the script

The whole script can be defined via the CONFIG object. It consists of general properties like childEntity and questionGroupName, field mapping, and trigger config. Trigger config allows record creation to be optional. This can be used when not every repeatable group instance should be translated into a new record.

Script

const CONFIG = {
    childEntity: "incident",
    questionGroupName: "case",

     // trigger configuration for creating records
    trigger: {
        enabled: false, // set to false to disable trigger check
        questionName: "create-case", // question to check within each group
        expectedAnswer: true, // answer that triggers record creation
    },
    
    fieldsMapping: {
        
        questionnaireLookupField: "crbb7_questionnaire", 
        
        staticFields: [ 
        {
            questionName: "parentq", // question name in questionnaire
            questionField: "customerid", // field on child entity
        },
    
        ],
        // Map question names to entity fields
        // Example: { questionName: "question-name", fieldName: "entity_field" }
        dynamicFields: [
                {
                    questionName: "asset",
                    fieldName: "crbb7_asset",
                },
                {
                    questionName: "subject",
                    fieldName: "title",
                },
                            {
                    questionName: "another-question-name",
                    fieldName: "msdyn_field",
                },
                {
                    questionName: "option-set",
                    fieldName: "casetypecode",
                },
                {
                    questionName: "date-and-time",
                    fieldName: "followupby",
                }

            ],
        
    }
};

let templateGroupId = null;
let loadedGroupInstances = [];

function onError(text) {
    MobileCRM.bridge.alert("Error: " + text);
}

window.onload = function () {
    initialize();
}

function initialize() {
    MobileCRM.UI.QuestionnaireForm.onSave(onQuestionnaireSave,true,null);
}

async function onQuestionnaireSave(questionnaireForm) {
    try {
        const questionnaire = questionnaireForm.questionnaire;
        templateGroupId = await loadTemplateGroupId(questionnaire.properties.resco_templateid.id);
        processRepeatableGroups(questionnaireForm);
    } catch (error) {
        onError(error);
    }
}

/**
 * Load the group id of the template group
 * @param {string} templateId the id of the current questionnaire template
 */
async function loadTemplateGroupId(templateId) {
    return new Promise((resolve, reject) => {
        const entity = new MobileCRM.FetchXml.Entity("resco_questiongroup");
        entity.addAttribute("resco_questiongroupid");

        const filter = new MobileCRM.FetchXml.Filter();
        filter.where("resco_name", "eq", CONFIG.questionGroupName);
        filter.where("resco_questionnaireid", "eq", templateId);
        entity.filter = filter;

        const fetch = new MobileCRM.FetchXml.Fetch(entity);
        fetch.execute(
            "Array",
            function (result) {
                if (result.length > 0 && result[0][0] != undefined) {
                    const groupId = result[0][0];
                    resolve(groupId);
                } else {
                    reject("Template group '" + CONFIG.questionGroupName + "' not found.");
                }
            },
            function (error) {
                reject("Error fetching question group: " + error);
            },
            null
        );
    });
}

/**
 * Find all repeatable group instances and extract data from each
 */ 
function processRepeatableGroups(questionnaireForm) {
    try {
        const repeatableGroups = questionnaireForm.groups.filter(g => g.templateGroup == templateGroupId);
        
        if (repeatableGroups.length === 0) {
            onError("No repeatable group instances found.");
            return;
        }

        // Extract data from each group instance that passes trigger check
        repeatableGroups.forEach((group) => {
            // Check if group passes trigger condition
            if (!passesTriggerCheck(questionnaireForm, group)) {
                return; // Skip this group
            }

            const groupData = extractGroupData(questionnaireForm, group);
            loadedGroupInstances.push(groupData);
        });

        // Build and save records
        const records = buildRecords(questionnaireForm, loadedGroupInstances);
        saveRecords(records);

    } catch (error) {
        onError("Error processing repeatable groups: " + error);
    }
}

/**
 * Extract field data from a repeatable group instance
 */
function extractGroupData(questionnaireForm, group) {
    const groupData = {
        groupId: group.id,
        repeatIndex: group.repeatIndex,
        fields: {}
    };

    // Extract dynamic fields
    if (CONFIG.fieldsMapping.dynamicFields && CONFIG.fieldsMapping.dynamicFields.length > 0) {
        CONFIG.fieldsMapping.dynamicFields.forEach(({ questionName, fieldName }) => {
            // Construct question name with index suffix (e.g., "question-name#001")
            const indexedQuestionName = questionName + "#" + String(group.repeatIndex).padStart(3, '0');
            const question = questionnaireForm.questions.find(q => 
                q.groupId == group.id && 
                q.name == indexedQuestionName
            );
            
            if (question && question.value !== null) {
                groupData.fields[fieldName] = question.value;
            }
        });
    }

    return groupData;
}

/**
 * Check if a repeatable group instance passes the trigger condition
 */
function passesTriggerCheck(questionnaireForm, group) {
    if (!CONFIG.trigger.enabled) {
        return true; // If trigger is disabled, all groups pass
    }

    // Construct question name with index suffix (e.g., "question-name#001")
    const indexedQuestionName = CONFIG.trigger.questionName + "#" + String(group.repeatIndex).padStart(3, '0');
    
    // Find the question with the trigger question name in this group
    const triggerQuestion = questionnaireForm.questions.find(q => 
        q.groupId == group.id && 
        q.name == indexedQuestionName
    );

    if (!triggerQuestion) {
        return false; // Question not found, fail the check
    }

    // Compare the question value with the expected answer
    return triggerQuestion.value == CONFIG.trigger.expectedAnswer;
}

/**
 * Build DynamicEntity records from extracted group data
 */
function buildRecords(questionnaireForm, groupDataArray) {
    const records = [];
    try {

        const questionnaireId = questionnaireForm.questionnaire.id;
        
        const questionnaireRef = new MobileCRM.Reference(
            "resco_questionnaireanswer",
            questionnaireId
        );

        groupDataArray.forEach((groupData) => {
            const props = {};

            // Set questionnaire lookup field
            props[CONFIG.fieldsMapping.questionnaireLookupField] = questionnaireRef;

            // Set static fields from config
            if (CONFIG.fieldsMapping.staticFields && CONFIG.fieldsMapping.staticFields.length > 0) {
                CONFIG.fieldsMapping.staticFields.forEach(fieldMap => {
                    const question = questionnaireForm.questions.find(q => q.name == fieldMap.questionName);
                    if (question && question.value !== null) { 
                        props[fieldMap.questionField] = question.value;
                    }
                });
            }

            // Set dynamic fields
            Object.entries(groupData.fields).forEach(([fieldName, fieldValue]) => {
                props[fieldName] = fieldValue;  // Just copy the pre-extracted values
            });

            const record = MobileCRM.DynamicEntity.createNew(
                CONFIG.childEntity,
                null,
                null,
                props
            );
            records.push(record);
        });

        return records;
    } catch (error) {
        onError("Error building records: " + error);
    }
}

/**
 * Save all records asynchronously
 */
async function saveRecords(records) {
    if (!records || records.length === 0) {
        onError("No records to save.");
        return Promise.resolve();
    }

    try {
        const savePromises = [];
        for (let i = 0; i < records.length; i++) {
            savePromises.push(records[i].saveAsync());
        }
        
        try {
            await Promise.all(savePromises);
            MobileCRM.bridge.alert("Records created successfully: " + records.length);
        } catch (error_1) {
            onError("Error saving records: " + error_1);
            throw error_1;
        }
    } catch (error) {
        onError("Error saving records: " + error);
        return Promise.reject(error);
    }
}