Bulk update

From Resco's Wiki
Jump to navigation Jump to search

Bulk update of records is a feature of Resco mobile apps that allows you to modify selected fields for multiple records at once. App users can select the records on a view, start the bulk update command, enter the desired values, and save them to all selected records.

Prerequisites

Requires release 15.1 or later (15.1.1 for JSBridge support). Both Woodford and the mobile app must be updated.

Configuration in Woodford

Create a new bulk update form.

  1. On the mobile views, forms and charts screen, click New Form.
  2. As Template, select "Bulk Update Form", then click OK.
  3. Add the fields that you want to overwrite in bulk.
  4. Save all changes.

Add the bulk update command to your view.

  1. Edit an entity view in Woodford.
  2. Click Multi Select to display the command editor for multi selection.
  3. Add the BulkUpdate command to the left pane and set its properties:
    • Select a bulk update form, the maximum number of records that the user can process in one go, and the button label.
  4. Save all changes.

Usage in the app

  1. Go to a view where a bulk update command is enabled.
  2. Select multiple records that you want to modify.
  3. Tap the bulk update icon in the top right corner.
  4. On the bulk update form, enter values that you want to overwrite.
  5. Tap Save. The app validates the changes. If the validation succeeds, you can save the changes.

Bulk update in action.png

Result: The bulk update from the example above changes the City to "Chelsea" and clears the content of the other address fields (for all selected records). All visible fields on the bulk update form are saved to the update selected records. You can hide some fields using rules. For example, you can add custom buttons that "Hide empty fields" and "Show all fields". Or, if you need finer control, you can add a "Show/Hide" button for every single field.

Notes:

  • If you are using the old forms UI (i.e., not flexible forms), inactive tabs count as visible.
  • If you are using the new forms UI (i.e., flexible forms), collapsed tabs count as visible.

Form rules

The bulk update form also fully supports rules.

The On Change rule is only executed on the phantom record displayed on the bulk update form. It won't be executed for all the selected records.

The On Save rule has one peculiarity: it is executed n+1 times (if n=number of selected records).

  • The first execution applies to the phantom record displayed on the bulk update form. You can use it for example to validate the content of the fields, i.e., is the email address in the right format?
  • The next n executions apply to every single of the updated records.

You can differentiate between these executions using the Entity.IsNew property. If true, this is the first execution. If false, these are the n executions for the updated records.

In the example below, we use two On Save rules:

  • First one checks if the email address entered on the bulk update form is valid.
  • Second, executed for each updated record, loads the related primary contact record and updates its email address along with the parent account.

On save rule from bulk update form.png

JSBridge

Bulk update form iFrame can use standard MobileCRM.UI.EntityForm APIs to request form object and register events. Method “requestObject” asynchronously returns entity form object which contains an array of “bulkUpdateItems” containing the list of items being bulk-updated. The form entity property will contain the content of virtual template record with properties that have to be changed.

The following event handlers are available:

  • onSave handlers obtain the form with bulkUpdateItems and template record entity.
    This can be used to perform final validation before the save process takes place. Validation can be canceled by setting context.errorMessage property or via suspendSave/resumeSave (see EntityForm reference).
  • onPostSave handlers obtain the form with bulkUpdateItems and template record entity. Entities contain the status and statusMessage properties to identify that the save process was just partially successful and some entities were not saved.
    This can be used to perform additional actions after a successful save, like saving child entities or related records.
  • onChange handlers obtain the form with bulkUpdateItems and template record entity.
    This can be used to fill some fields on the template record automatically or to show/hide some fields according to the current context.

ExecuteJS

In addition to the explicit support, it is possible to use ExecuteJS from the On Save rule.

In the example below, "handleOnSave" is executed via ExecuteJS action from an On Save rule to:

  • validate the phantom Account record user filled on the form
  • for every updated account it loads and caches all its related contacts and updates one of their fields

It uses the form's OnPostSave hook to save all cached contacts in one operation.

ExecuteJS example
On Save rule
Bulk update example with ExecuteJS.png
Sample script
<!DOCTYPE html>
<html lang="en" xmlns="http://www.w3.org/1999/xhtml">
<head>
	<meta charset="utf-8" />
	<title>Account bulk update</title>
	<meta http-equiv="X-UA-Compatible" content="IE=edge" />
	<meta name="viewport" content="initial-scale=1, user-scalable=no" />
	<script src="JSBridge.js"></script>
</head>
<body>
	<script>

    // The following example shows how Javascript bridge can be used in Bulk Update Form rules for additional processing logic.
    // 1) It uses "handleOnSave" executed via ExecuteJS action from OnSave rule to:
    //    * validate the Template Account Record user filled on form
    //    * for every updated account it loads and caches all its related contacts and update one of their fields
    // 2) It uses form's OnPostSave hook to save all cached contacts in one operation.

    var cachedContactEntities = [];

    /**
     * This function is called from OnSave rule via ExecuteJS action.
     */
    async function handleOnSave(entityForm, updatedAccount) {
      if (!updatedAccount) {
        // updatedRecord is null when called to validate Template Account
        cachedContactEntities = []; // wipe the cached contacts. There may be leftovers after earlier failure.
        return validateTemplateAccount(entityForm.entity);
      } else {
        // We have updated record. Perform additional processing on related contacts.
        var updateResult = await updateRelatedContacts(entityForm, updatedAccount);
        return updateResult;
      }
    }

    function validateTemplateAccount(templateAccount) {
      // just simple demonstration of propagating validation message to rule
      var templateWebSiteUrl = templateAccount.properties.websiteurl;
      if (templateWebSiteUrl && templateWebSiteUrl.indexOf("www.") != 0) {
        return "Warning: Are you sure you want the url without www prefix?";
      } else {
        return "";
      }
    }

    async function updateRelatedContacts(entityForm, updatedAccount) {
      // load all related contacts, update them and store them to be saved later
      var contact = new MobileCRM.FetchXml.Entity("contact");
      contact.addAttributes();
      var linkEntity = contact.addLink(
        "account",
        "id",
        "parentcustomerid",
        "inner"
      );
      linkEntity.addFilter().where("id", "eq", updatedAccount.id);
      var fetch = new MobileCRM.FetchXml.Fetch(contact);      
      try {
        // wait for asynchronous loading so that we can return validation result to calling rule.
        var result = await executeContactFetch(fetch);
      } catch (ex) {
        // let calling rule process the error.
        result = ex;
      }
      return result;
    }

    //** Promise wrapper to allow await on asynchronous fetch */
    async function executeContactFetch(fetchToExecute) {
      var promise = new Promise((resolve, reject) => {
        fetchToExecute.execute(
          "DynamicEntities",
          function (result) {
            if (!result || result.length == 0) {
              resolve("Warning: No contacts found.");
            } else {
              for (var i in result) {
                var contact = result[i];
                contact.properties.donotemail = true;
                cachedContactEntities.push(contact);
              }
              resolve(); // no message means success
            }
          },
          function (err) {
            reject("Error: Failed to fetch contacts. " + err);
          },
          null
        );
      });
      return promise;
    }

    MobileCRM.UI.EntityForm.onPostSave(
      async function (entityForm) {
        if (cachedContactEntities.length > 0) {
          var postSuspend = entityForm.suspendPostSave();
          try {
            // wait for asynchronous saving to ensure it is done before resuming the app.
            await saveUpdatedContacts(cachedContactEntities);
          } catch (ex) {
            // JS Bridge doesn't allow to return values from onPostSave,
            // hence we only let user know using MessageBox.
            MobileCRM.bridge.alert(ex);
          } finally {
            cachedContactEntities = [];
            postSuspend.resumePostSave();
          }
        }
      },
      true,
      MobileCRM.bridge.alert
    );

    //** Promise wrapper to allow await asynchronous save */
    async function saveUpdatedContacts(contacts, entityForm) {
      var promise = new Promise((resolve, reject) => {
        MobileCRM.DynamicEntity.saveMultiple(
          contacts,
          null,
          function () {
            resolve();
          },
          function (err) {
            if (err) {
              reject("Post save failed with exception: " + err);
            } else {
              reject("Post save failed.");
            }
          }
        );
      });
      return promise;
    }
	</script>
</body>
</html>