Skip to content

Customizing with Advanced Rules

Rules are JavaScript modules that can be used to add application functionality to the Mobile Development Kit client API.

You have the ability to customize the rules you use in your app so that you can change the behavior of the app itself.

Client API Objects

IClientAPI is the base interface used by developers to pass objects to rule functions. The exported rule function will be given a ClientAPI object as an argument to provide access to a context. Through this object, the rule function can query application state and data. The ClientAPI object implements the IClientAPI interface or one of its sub-interface types depending on the context of the rule. An example where the rule is used in the context of a page will receive an IPageProxy instance as its ClientAPI object.

Note

IPageProxy is an interface that will be passed as an argument to a rule when that rule is bound to a page. IControlProxy is an interface that will be passed as an argument to a rule when that rule is bound to a control.

The table below identifies the ClientAPI objects used in rule functions in the Client API.

Term Description
IClientAPI The base interface for all objects passed to rule functions
IControlProxy The interface of the object passed to a rule function when the rule is run in the context of a control
IControlContainerProxy The interface of the object passed to a rule function when the rule is run in the context of a control container
IFormCellProxy The interface of the object passed to a rule function when the rule is run in the context of a form cell
It also provides access to the IControlProxy interface.
IFormCellTargetProxy The interface of the object returned to a rule function when the rule executes the IFormCellProxy::getTargetSpecifier method
IListPickerFormCellTargetProxy The interface for support object cell used in a form cell list-picker
IPageProxy The interface of the object passed to a rule function when the rule is run in the context of a page
ISectionedTableProxy The interface of the object passed to a rule function when the rule is run in the context of a sectioned table
ISectionProxy The interface of the object passed to a rule function when the rule is run in the context of a section
ILinkSpecifierProxy The interface of the object returned to a rule function when the rule executes the IClientAPI::createLinkSpecifierProxy method
IToolbarProxy The interface of the object passed to a rule function when the rule is run in the context of a toolbar
IActionResult The interface of the object passed to a rule function when the rule is run in the context of an action
LoggerManager The interface of the object returned to a rule function when the rule executes the IClientAPI::getLogger method
DataQueryBuilder The API for building OData Query options
FilterBuilder The API for building OData Filter options

Rule Examples

The following are examples of the IClientAPI objects being used in a rule:

  • Using the IControlProxy.getValue interface in a Control.Type.FormCell.SegmentedControl control

    // Control definition
    {
      "Caption": "Priorities",
      "OnValueChange": "/AssetWorkManager/Rules/SegmentControlChange.js",
      "Segments": [
        "Low",
        "Medium",
        "High"
      ],
      "Value": "Low",
      "_Name": "SegmentedFormCell",
      "_Type": "Control.Type.FormCell.SegmentedControl"
    }
    
    // Whenever a new selection is made
    // "/AssetWorkManager/Rules/FormCell/SegmentControlChange.js" is called:
    
    // SegmentControlChange.js
    export default function FormCellSegmentedChangeHandler(clientAPI) {
      let selections = "";
      // getValue returns an array of Objects in the form:
      // [{SelectedIndex:value, ReturnValue:value}, ...]
      clientAPI.getValue.forEach((selectedItem) => {
        selections += selectedItem.ReturnValue + '\n';
      })
      console.log(selections); // Log all selections to the control
    
      alert(clientAPI.getValue[0].ReturnValue); // Alert with the first selection
    }
    
  • Changing PickerItems using the IFormCellProxy.setTargetSpecifier interface

    // Control definition
    {
      "Caption": "high via rule",
      "Segments": [
        "Low",
        "Medium",
        "High"
      ],
      "Value": "Low",
      "OnValueChange": "/AssetWorkManager/Rules/FormCell/UpdateListPickerItems.js",
      "_Name": "SegmentedFormCell",
      "_Type": "Control.Type.FormCell.SegmentedControl"
    }
    

    js // Whenever the a new segmented button is selected // "/AssetWorkManager/Rules/FormCell/UpdateListPickerItems.js" is called export default function UpdateListPickerItems(clientAPI) { // selection is the selected SegmentedControl value let selection = clientAPI.getPageProxy().getControl('SectionedTable').getSection('FormCellSection0').getControl('SegmentedFormCell').getValue[0].ReturnValue; let containerProxy = clientAPI.getPageProxy().getControl('SectionedTable').getSection('FormCellSection0'); // get a proxy to the Control.Type.FormCell.ListPicker instance let listPickerProxy = containerProxy.getControl('ListPickerFormCellSingle'); let specifier = listPickerProxy.getTargetSpecifier; specifier.setDisplayValue("{OrderDescription}"); specifier.setEntitySet("MyWorkOrderHeaders"); specifier.setReturnValue("{OrderId}"); specifier.setService("/AssetWorkManager/Services/Amw.service"); // set the query options for the list picker based on SegmentedControl Value switch (selection) { case 'Low': specifier.setQueryOptions("$top=5"); break; case 'Medium': specifier.setQueryOptions("$top=10"); break; case 'High': specifier.setQueryOptions("$top=15"); break; } listPickerProxy.setTargetSpecifier(specifier); }

  • Using logging interfaces in a rule

    // Initialize logger by rule
    // Ideally used when the application starts
    export default function InitializeLoggerRule(clientAPI) {
    
      const logFileName = 'LogFile.txt';
      const maxFileSizeInMegaBytes = 8;
      // If the logger has already been initialized, it has no effect.
      // FileName and fileSize are optional values, if not specified, default values will be used.
      clientAPI.initializeLogger(logFileName, maxFileSizeInMegaBytes);
    
      // You can even initialize logger state and level
      let logger = clientAPI.getLogger;
      logger.on;
      logger.setLevel('Info');
    }
    
    // Or you can even use this
    // Initialize logger by rule which calls an action
    export default function InitializeLoggerWithCallingAnAction(clientAPI) {
      return clientAPI.executeAction('/AssetWorkManager/Actions/Logger/SetState.action');
    }
    
    // Use the logger by rule (it should have been already initialized)
    // Available functionality:
    // Log a message, Edit log level, Edit state (turn on/off), Get current logger state,
    // Get current root log level, Upload log file
    export default function UseLoggerRule(clientAPI) {
    
      // Get logger instance
      let logger = clientAPI.getLogger;
    
      // Returns a boolean according to current logger state (on=true, off=false)
      logger.isTurnedOn;
    
      // Turns off the logger
      logger.off;
    
      // Turns on the logger
      logger.on;
    
      // Toggles the logger state (on/off)
      logger.toggle;
    
      // Returns the current logger root level. Possible values: Debug, Info, Warn, Error, Off
      logger.getLevel;
    
      // Sets the logger root log level. Possible values: Debug, Info, Warn, Error, Off
      logger.setLevel('Info');
    
      // Logs a message with the specified log level.
      // If log level is not specified, the root log level will be used.
      const optionalLogMessageLogLevel = 'Debug';
      logger.log('This is a test message', optionalLogMessageLogLevel);
    
      //Log uploading works only after successful authentication
      // Upload the log file according to client settings
      clientAPI.executeAction('/AssetWorkManager/Actions/Logger/Upload.action');
    
      // Upload log file according to own settings
      logger.uploadLogFile(backendURL, applicationID);
    }
    
  • Setting links in a rule using the clientAPI.createLinkSpecifierProxy interface

    // Definition
    {
      "_Type": "Action.Type.ODataService.UpdateEntity",
      "Target": {
        "EntitySet": "MyWorkOrderHeaders",
        "Service": "/AssetWorkManager/Services/Amw.service",
        "ReadLink": "{@odata.readLink}"
      },
      "Properties": {
        "OrderDescription": "#Control:OrderDescription/#Value",
      },
      "UpdateLinks": "/AssetWorkManager/Rules/OData/UpdateLinkRule.js"
    }
    
    export default function UpdateLinkRule(clientAPI) {
      let containerProxy = clientAPI.getControl('PageOneFormCell');
      let listPickerProxy = containerProxy.getControl('EquipmentList');
      let queryOption = '';
      // odata action expects link array
      let links = [];
      if (listPickerProxy.getValue.length > 0) {
          let selectedValue = listPickerProxy.getValue[0].ReturnValue;
          queryOption = `$filter=EquipId eq '${selectedValue}'`
          let link = clientAPI.createLinkSpecifierProxy('EquipmentHeader', 'MyEquipments', queryOption, '');
          links.push(link.getSpecifier);
      }
      return links;
    }
    
  • Setting Toolbar item property value using a rule

    // Definition
    {
      "Caption": "Dynamic toolbar item value based on rule",
      "Controls": [],
      "ToolBar": {
        "Controls": [
          {
            "_Name": "DynamicItem",
            "_Type": "Control.Type.ToolbarItem",
            "Caption": "/AssetWorkManager/Images/off.png",
            "Image": "/AssetWorkManager/Images/off.png",
            "OnPress": "/AssetWorkManager/Rules/Toolbar/SetToolbarItemValue.js"
          },
          ...
    
    // Rule
    export default function SetToolbarItemValue(clientAPI) {
      var toolbarItems = clientAPI.getToolbarControls;
      var imageOn = '/AssetWorkManager/Images/on.png';
      var imageOff = '/AssetWorkManager/Images/off.png';
      var imageOnText = 'AddedAsFavorite';
      var imageOffText = 'RemovedFromFavorite';
      var newValue, newText;
      toolbarItems.forEach(function (element) {
        if (element.name == clientAPI.getName) {
          if (element.name == 'DynamicItem') {
            newText = imageOnText;
            newValue = imageOn;
    
            if (element.caption == imageOnText) {
              newText = imageOffText;
              newValue = imageOff;
            }
            element.setCaption(newText);
            element.setImage(newValue);
    
            /*
            The properties that rule writer is allowed to set and get are:
    
            //SET
            element.setCaption(newValue); // text
            element.setStyle(newValue); // style
            element.setImage(newValue); // image
            element.setSystemItem(newValue); // 0 - 22 (more details on Apple Developer website - https://developer.apple.com/documentation/uikit/uibarbuttonsystemitem)
            element.setWidth(newValue); // number
            element.setEnabled(newValue); // boolean
            element.setVisibility(newValue); // visible/hidden/collapse
    
            //GET
            element.name;
            element.clickable;
            element.caption;
            element.enabled;
            element.visibility;
            element.width;
            element.systemItem;
            */
    
            return clientAPI.executeAction('/AssetWorkManager/Actions/Toast/' + newText + '.action');
          }
        }
      });
    }
    
  • Using feedback indicators in ObjectTable Sections

    // Definition
    {
      "Sections": [
        {
          "ObjectCell": {
            "ProgressIndicator": "/AssetWorkManager/Rules/ProgressIndicator/SetIndicatorState.js",
            "DetailImage": "/AssetWorkManager/Images/compressor.png",
            "Description": "{Description}",
            "Footnote": "{MimeType}",
            "StatusText": "{FileName}",
            "SubstatusText": "/AssetWorkManager/Rules/OData/IsMediaLocal.js",
            "Subhead": "{DocumentID}",
            "Title": "{ObjectKey}",
            "OnPress": "/AssetWorkManager/Rules/ProgressIndicator/DownloadOrOpenDocument.js"
          },
        "Target": {
            "EntitySet": "BDSDocuments",
            "Service": "/AssetWorkManager/Services/Amw.service"
          },
          "_Type": "Section.Type.ObjectTable"
        }
      ],
      "_Type": "Control.Type.SectionedTable",
      "_Name": "SectionedTable"
    }
    
    // Rules
    export default function DownloadOrOpenDocument(clientAPI) {
      const pageProxy = clientAPI.getPageProxy;
      let bindingObject = pageProxy.getActionBinding;
      let readLink = bindingObject["@odata.readLink"];
      let serviceName = '/AssetWorkManager/Services/Amw.service';
      let entitySet = 'BDSDocuments';
    
      // first we need to decide if the media exists locally or needs to be downloaded
      return clientAPI.isMediaLocal(serviceName, entitySet, readLink).then((result) => {
        if (result) {
          // the media has been downloaded, we can open it -> the path needs to be provided in the action definition
          // or it should come from binding
    
          // persist the media data to local sandbox, so we can open it with the document viewer
          let fs = require('file-system');
          const actionBinding = pageProxy.getActionBinding;
          if (typeof actionBinding === 'undefined') {
            return pageProxy.executeAction('/AssetWorkManager/Actions/OData/ODataDownloadFailure.action');
          }
    
          let key = actionBinding['@odata.readLink'];
          let filename = actionBinding.DocFile;
          if (typeof filename === 'undefined' || filename === '') {
            filename = actionBinding.FileName;
          }
          if (typeof filename === 'undefined' || filename === '') {
            filename = actionBinding.SaveDocFile;
          }
          if (typeof filename === 'undefined') {
            // Document filename not found. proceeding with a test filename.
            filename = "res://TestOpenDocument.pdf";
          }
    
          const filepaths = filename.split('/');
          const filelastSegment = filepaths[filepaths.length - 1];
          filename = filelastSegment;
    
          if (key) {
            var tempDir = fs.knownFolders.temp;
            var tempPath = fs.path.join(tempDir.path, filename);
            var tempFile = fs.File.fromPath(tempPath);
            tempFile.writeSync(pageProxy.getClientData[key], err => {
              return pageProxy.executeAction('/AssetWorkManager/Actions/OData/ODataDownloadFailure.action');
            });
    
            pageProxy.setActionBinding({
              'FileName': tempPath,
            });
    
            return pageProxy.executeAction('/AssetWorkManager/Actions/Document/OpenRelatedDocument.action');
          }
        } else {
          // The media is on the server, we can download it
          const pressedItem = pageProxy.getPressedItem;
    
          // get the object table section proxy, which contains the feedback indicators and performs download/open in order to update the indicator state
          const objectTableSection = clientAPI.getSections[0];
    
          // set the indicator's state to in progress and start the download
          objectTableSection.setIndicatorState("inProgress", pressedItem);
          clientAPI.executeAction("/AssetWorkManager/Actions/OData/DownloadDocumentStreams.action").then((result) => {
            // success case
            // the document was successfully downloaded, we can set the indicator's state to open
            objectTableSection.setIndicatorState("open", pressedItem);
          }, (error) => {
            // error case
            objectTableSection.setIndicatorState("toDownload", pressedItem);
            clientAPI.executeAction("/AssetWorkManager/Actions/OData/ODataDownloadFailure.action");
          });
        }
      });
    }
    
    export default function SetIndicatorState(clientAPI) {
      let bindingObject = clientAPI.getBindingObject;
      let readLink = bindingObject["@odata.readLink"];
      let serviceName = '/AssetWorkManager/Services/Amw.service';
      let entitySet = 'BDSDocuments';
    
      // first we need to decide if the media exists locally or needs to be downloaded
      return clientAPI.isMediaLocal(serviceName, entitySet, readLink).then((result) => {
        if (result) {
          // The media is saved locally, we can open it
          return 'open';
        } else {
          // The media is on the server, we can download it
          return 'toDownload';
        }
      });
    }
    
  • Localization and Formatting in a rule

    // Languages related API
    export default function ManageLanguages(clientAPI) {
      // get supported languages based on resource files under i18n directory, returns in key value pair
      var supportedLanguages = clientAPI.getSupportedLanguages;
    
      // set language in appSettings by language key
      clientAPI.setLanguage('en');
    
      // get current language in appSettings
      var language = clientAPI.getLanguage;
    }
    
    // Regions related API
    export default function ManageRegions(clientAPI) {
      // get regions based on system, returns in key value pair
      var regions = clientAPI.getRegions;
    
      // set region in appSettings by language key
      clientAPI.setRegion('SG');
    
      // get current region in appSettings
      var region = clientAPI.getRegion;
    }
    
    // Localize text examples
    export default function Localize(clientAPI) {
      // work_orders="Work Orders"
      var localizedText = clientAPI.localizeText('work_orders');
      // result: Work Orders
    
      // dynamic_localizable_value="{1} and {2} are required for {0} development."
      var dynamicParams = ['MDK', 'NativeScript 3.x', 'XCode 9.x'];
      var localizedTextWithDynamicParams = clientAPI.localizeText('dynamic_localizable_value', dynamicParams);
      // result: NativeScript 3.x and XCode 9.x are required for MDK development.
    
      // order id binding is 4013
      // dynamic_binding_localizable_value_order_id="The order id is: {0}."
      var dynamicBindingParams = ['{OrderId}'];
      var localizedTextWithDynamicBindingParam1 = clientAPI.localizeText('dynamic_binding_localizable_value_order_id', dynamicBindingParams);
      // result: The order id is: 4013
    
      var orderId = clientAPI.binding.OrderId;
      var localizedTextWithDynamicBindingParam2 =  clientAPI.localizeText('dynamic_binding_localizable_value_order_id', [orderId]);
      // result: The order id is: 4013
    }
    
    // Number formatting examples
    export default function NumberFormattingExamples(clientAPI) {
      var valueToBeConverted = 123456.789;
    
      // Format as Number
      var customLocale = 'en-US';  
      var formattedAsNumber = clientAPI.formatNumber(valueToBeConverted, customLocale);
      // result: 123,456.79
    
      // Format as Currency
      var currencyCode = 'USD';
      customLocale = 'en-FR';
      var formattedAsCurrency = clientAPI.formatCurrency(valueToBeConverted, currencyCode, customLocale);
      // result: 123 456,79 $US
    
      // Format as Percentage
      valueToBeConverted = 0.7865;
      customLocale = 'en-US';
      var formattedAsPercentage = clientAPI.formatPercentage(valueToBeConverted, customLocale);
      // result: 78.65%
    
      // Format as Scientific Notation
      var customLocale = 'en-US';  
      var formattedAsNumber = clientAPI.formatScientific(valueToBeConverted, customLocale);
      // result: 1.23456789E5
    
      // Formatting with options.
      // Refer to `https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/NumberFormat`
      let options = {
        maximumFractionDigits: 1,
        useGrouping: false,
      }
    
      // Format as Number
      var customLocale = 'en-US';  
      var formattedAsNumber = clientAPI.formatNumber(valueToBeConverted, customLocale, options);
      // result: 123456.8
    
      // Format as Currency
      var currencyCode = 'USD';
      customLocale = 'en-FR';
      var formattedAsCurrency = clientAPI.formatCurrency(valueToBeConverted, currencyCode, customLocale, options);
      // result: 123456,8 $US
    
      // Format as Percentage
      valueToBeConverted = 0.7865;
      customLocale = 'en-US';
      var formattedAsPercentage = clientAPI.formatPercentage(valueToBeConverted, customLocale, options);
      // result: 78.6%
    
      // Format as Scientific Notation
      var customLocale = 'en-US';  
      var formattedAsNumber = clientAPI.formatScientific(valueToBeConverted, customLocale, options);
      // result: 1E5
    }
    
    // DateTime, Date, Time formatting examples
    export default function DateTimeFormattingExamples(clientAPI) {
      var dateTimeValue = clientAPI.formatDatetime(new Date, "en-GB", "UTC");
      // result: 28 Nov 2017 at 06:16:02
    
      var dateValue = clientAPI.formatDate("2017-11-28T11:40:00Z", "de-DE");
      // result: 28.11.2017
    
      var timeValue = clientAPI.formatTime("2017-11-28T11:40:00Z", "zh-CN", "Asia/Shanghai");
      // result: 下午7:40:00
    }
    
  • Using ActionResult in a target path and in a rule

    // Message Action definition of action run after CreateWorkOrder action result is stored
    {
      "_Type": "Action.Type.Message",
      "Message": "/AssetWorkManager/Rules/OData/CreateEntityMessage.js",
      "Title": "#ActionResults:CreateWorkOrder/#Property:data/#Property:OrderDescription",
      "OKCaption": "Ok",
      "OnSuccess": "/AssetWorkManager/Actions/Navigation/NextChangeSetAction.action"
    }
    
    // CreateEntityMessage.js
    export default function GetCreateEntityMessage(clientAPI) {
      let actionResult = clientAPI.getActionResult('CreateWorkOrder');
      if (actionResult) {
        let entity = actionResult.success;
        return 'Entity created with description \"' + entity.OrderDescription + '\"';
      }
    
      return 'Entity successfully created';
    }
    
  • Call a Function Import in a rule

    // TaskDetail page
    {
        "Caption": "Function Import Example",
        "OnLoaded": "/MDKOnlineFioriTrial/Rules/FunctionImport/GetDecisionOptions.js",
        "Controls": [
            {
                "_Type": "Control.Type.SectionedTable",
                "Sections": [
                    {
                        "Controls": [
                            {
                                "OnPress": "/MDKOnlineFioriTrial/Rules/FunctionImport/ApproveRequest1.js",
                                "_Type": "Control.Type.FormCell.Button",
                                "Title": "Approve",
                                "_Name": "ApproveButton",
                                "IsVisible": false,
                                "ButtonType": "Text",
                                "Semantic": "Tint"
                            },
                            {
                                "OnPress": "/MDKOnlineFioriTrial/Rules/FunctionImport/RejectRequest1.js",
                                "_Type": "Control.Type.FormCell.Button",
                                "Title": "Reject",
                                "_Name": "RejectButton",
                                "IsVisible": false,
                                "ButtonType": "Text",
                                "Semantic": "Tint"
                            }
                        ],
                        "_Type": "Section.Type.FormCell",
                        "_Name": "FormCellSection0"
                    }
                ],
                "_Name": "SectionedTable"
            }
        ],
        "_Name": "TaskDetail",
        "_Type": "Page",
        "PrefersLargeCaption": true
    }
    
    // GetDecisionOptions.js
    export default function GetDecisionOptions(clientAPI) {
      var functionName = 'DecisionOptions';
      var parameters = { 'InstanceID': '001' };
      var oFunction = { Name: functionName, Parameters: parameters };
      var serviceName = '/MDKOnlineFioriTrial/Services/FioriTrial.service';
      var approveBtn = clientAPI.getPageProxy().getControl('SectionedTable').getSection('FormCellSection0').getControl('ApproveButton');
      var RejectButton = clientAPI.getPageProxy().getControl('SectionedTable').getSection('FormCellSection0').getControl('RejectButton');
      clientAPI.callFunction(serviceName, oFunction).then(function (result) {
        if (result && result.length > 0) {
          result.forEach(function (item) {
            if (item.DecisionKey === '0001') {
               approveBtn.setVisible(true);
            }
            if (item.DecisionKey === '0002') {
              RejectButton.setVisible(true);
            }
          });
        } else {
          console.log('No result set');
        }
      }).catch(function (error) {
        return console.log('------ERROR--------' + error);
      });
    }
    

Last update: May 21, 2024