Generic event handler for row operations working with records from Stored Procedure

Contains tips for configurators working with Aware IM
Post Reply
nhofkes
Posts: 128
Joined: Mon Sep 07, 2020 6:03 am
Location: Netherlands

Generic event handler for row operations working with records from Stored Procedure

Post by nhofkes »

Introduction
As described here, you can have a Stored Procedure returning data to a query. If you then would like to have row operations for this data, you will run into the issue that Aware does not have the BOs itself (see this post. As Jaymer pointed out in that post, you can make your own Row Operations via Javascript.

Below an example where I created a row operation "View" with a css class "ep-buttonview"

Code: Select all

widget.bind("dataBound", onDataBound);
function onDataBound(e) {
  grid.tbody.find("a.aw-link.ep-buttonview").click(onViewClick);
}

function onViewClick(e) {
  let currentRow = $(this).parents("tr");
  let record = grid.dataItem(currentRow);
  if (record) {
    AwareApp.startProcess2(
      "AwareProcessName",
      "BOName",
      record["BO_ID"],
      "main"
    );
  }
}
This works but it has a few drawbacks:
- If there are multiple row operations, you need to create an event handler for each row
- The event handler must refer to the correct Aware Process. If the process name is changed, the render script must be updated accordingly. This makes the code harder to maintain and may create future bugs if you would change the configuration but forget to update the render script.

Generic event handler for row operation buttons displayed separately in each row
I developed the following alternative, which is fully generic. This means that the JS code does not need to have any knowledge about how many row operations are defined and what the required AIM process is for each operation.

Code: Select all

// Regular Expression to find the process name for data row operations. The first word after 'process:' in the itemOperand will be matched.
const processRegExp = new RegExp("(?<=process:)\\s*\\w+");
widget.bind("dataBound", onDataBound);

function onDataBound(e) {
  grid.tbody.find("a.aw-link[data-operidx]").click(onDataOperationClick);
}

function onDataOperationClick(e) {
  let currentRow = $(this).parents("tr");
  let record = grid.dataItem(currentRow);
  let itemOperIdx = Number($(this).attr("data-operidx"));
  let processNameMatch =
    parser.m_itemOpers[itemOperIdx].operand.match(processRegExp);
  if (record && processNameMatch) {
    try {
      AwareApp.startProcess2(
        processNameMatch[0].trim(),
        "BOName",
      	record["BO_ID"],
        "main"
      );
    } catch (e) {
      console.log("Error calling process. Matched name: ", processNameMatch," - Error: ", e);
    }
  }
}
This uses the fact that the item operations are described in parser.m_itemOpers - there is an object for each row operation, containing information such as the name of the operation, the type, the css class, and of course the process to run. If you select as type "Execute Javascript", then the javascript for the row operation will also be stored in parser.m_itemOpers. This provides a convenient way to pass the name of the AIM process to the custom event handler. The applicable AIM process name is retrieved from the javascript provided with the row operation, using the RegExp "(?<=process:)\\s*\\w+". This gets the first word following "process:". In the example below, I defined the process name 'AccountHolder_ViewFromJS'. If for any reason I would change the name of the process, or wanted to call another process, the only thing to do is change the name of the applicable process in the settings for the operation and the event handler automatically calls the correct process. There are no changes required to the actual event handler in the render script.

Screenshot 2024-03-15 111656.png
Screenshot 2024-03-15 111656.png (25.43 KiB) Viewed 13177 times
Because the regular Aware handler will also be triggered when clicking on the operation button, I put the information in a JS comment. This way, nothing happens when the Aware handler executes the Javascript. (In principle, it would be possible to attach an event handler to a lower DOM object and then prevent the event triggering at the higher level (no event bubbling), but that is more complicated and error-prone.)
Note that the custom javascript defined for the row operation can also contain other information or actual JS code (i.e. not commented out). This will be ignored by the event handler, but will of course be run by Aware if not commented out. For example, you could add additional clarification comments or even pass additional parameters to the event handler in the render script.

Each row operation button has an attribute "data-operidx", which is the index to the parser.m_itemOpers array. So by getting that index, the requested row operation can be found.

You need to ensure that each row in the grid contains the ID of the applicable BO that you need to pass to the AIM process. But the ID can be in a hidden column, so it isn't visible in the grid but can be easily accessed through the dataItem method for the grid.

Of course, the event handler must know what type of AIM object must be passed to the AIM process. But usually any query, although generated through the Stored Procedure, will be related to one existing AIM object so it is clear that this must be put into context for the row operation process.

Generic event handler for row operation buttons combined behind a single menu button

Screenshot 2024-03-15 114512.png
Screenshot 2024-03-15 114512.png (13.67 KiB) Viewed 13177 times

The above works fine for row operations that are specified as separate buttons on the row. It does not work if the row operations are included in one menu button, i.e. when the option "Create menu button for operations" is ticked (see screenshot above.). The reason is that this creates separate menus that are put in a different place in the DOM, not in the table rows. If row operations are combined behind a menu button, the code below can be used as a generic event handler:

Code: Select all

const processRegExp = new RegExp("(?<=process:)\\s*\\w+");
widget.bind("dataBound", onDataBound);

const menuButtons = {
  menuIDs: [],      // holds unique IDs for the row menus
  operations: {},   // hashtable for the row operations (operation name, AIM process name)
  listItems: "",    // selector for the listitems in the menu, to make sure that only menubuttons that have an operation class are included

  // Store operation names and corresponding process names in a hashtable
  // Create a selector for the css classes of the menu buttons
  // NOTE: each menu button must have a css class, but the class can be identical for all menu buttons
  initialise: function () {  
    parser.m_itemOpers.forEach((operation) => {
      let processNameMatch = operation.operand.match(processRegExp);
      if (processNameMatch) {
        menuButtons.operations[operation.operName] = processNameMatch[0].trim();
        if (operation.cssClass && !(menuButtons.listItems.includes(operation.cssClass))) {
          menuButtons.listItems += ", li." + operation.cssClass;
        }
      }
    });
    if (menuButtons.listItems.length > 2) {
      menuButtons.listItems = menuButtons.listItems.substring(2); // Remove first ", "
    }
   
    // find all popup menus (one for each row) and store their IDs in an array
    if (this.listItems) {
      $("body")
        .find('div.k-menu-popup[data-role="buttonmenu"] ul.k-menu-group')
        .has(this.listItems)
        .each(function () {
          menuButtons.menuIDs.push($(this).attr("id"));
          $(this).click(onDataOperationMenuClick);
        });
    }
  },
};

function onDataBound(e) {
  menuButtons.initialise();
}

function onDataOperationMenuClick(e) {
  let rowIndex = menuButtons.menuIDs.indexOf($(this).attr("id"));
  if (rowIndex >= 0) {
    let currentRow = grid.tbody.find("tr").eq(rowIndex);
    let record = grid.dataItem(currentRow);
    let menuTextElement = $(e.target)
      .closest("li")
      .find("span.k-menu-link-text");
    let operName = menuTextElement ? menuTextElement.text() : null;
    if (record && operName && menuButtons.operations[operName]) {
      try {
        AwareApp.startProcess2(
          menuButtons.operations[operName],
          "BOName",
      	  record["BO_ID"],
          "main"
        );
      } catch (e) {
            console.log("Error calling process. Matched name: ", processNameMatch," - Error: ", e);
      }
    }
  }
}
This is slightly more complex than the first generic event handler, caused by the different structure of the menu buttons, but it works well.
The event handler also takes into account the possibility that for some rows, operations may be excluded as a result of the "Applicable" condition. If in the settings for the row operation an "Applicable" condition is set and for any particular row that condition is not fulfilled, the menu button for that operation will not be included in the generated HTML. This means that the index of the menu button cannot be used to find the relevant row operation. Instead, the event handler creates a hashtable of all row operations, with a key equal to the operation name and value equal to the process defined in the script settings. Note that this works only when the operation name is actually displayed on the button, otherwise it is not included in the HTML. But I think that you would always want to include the operation name on the button if the row operations are behind a separate row menu button. If only an image (icon) and no text would be displayed, it may make more sense to put those icons in the row instead of a separate menu.

Furthermore, note that you need to provide a custom css class for every row operation. This is used by the initialisation code in the script to ensure that the event handler is only bound to the custom menus for the row operations and not to any other menus that may be on the screen (for example top bar menu or left panel menu). But all row operations can have the same css class, they do not have to be different for each operation. If none of the row operation buttons for a particular record have a css class defined, that menu will not be included in the event handler and the menus for the other rows will not work correctly.

A few caveats
  • Be sure to comment out the line 'process:[process name]' in the Javascript for the row operation
For the menu button version:
  • Apply custom css class to each row operation
  • Make sure that 'Display operation name on the button' is ticked
  • Operation names must be unique for the same grid
Last but not least:
Normally, if you change the name of a process then all references to that process are automatically updated by Aware. This does not apply to the reference in the javascript for the row operation (the line "// process: process name"), you will need to change that manually. And unfortunately that reference will not show up when you do a Search Version (Alt-V) for the name. So if you change the name of the process and forget where the reference(s) to that process are, you may have a hard time finding them. It's best to flag/document that the process should not be renamed without care, for example (as in the screen shot above) by adding JS to the process name to alert you that this is called from javascript. What I have done now is put all processes that are called directly from Javascript in a separate category (which becomes a folder under Processes) and added in the description of the process the the name of the query and the row operation that calls the process, to make it easier to find and amend the reference.

Summary
With this code you can create generic event handlers in the render script for queries displaying data output from a Stored Procedure. This enables row operations to be defined from the configurator. The regular AIM process receives the required BO as input, notwithstanding that the records in the query are not persisted in the database. You can even combine both code examples in the same render script, which has the effect that the script can deal with row operations regardless whether they are specified as separate buttons on the row or combined behind a single menu button.
Last edited by nhofkes on Sat Mar 16, 2024 7:32 am, edited 1 time in total.
Niels
(V9.0 build 3272 - MariaDB - Windows)
Jaymer
Posts: 2528
Joined: Tue Jan 13, 2015 10:58 am
Location: Tampa, FL
Contact:

Re: Generic event handler for row operations working with records from Stored Procedure

Post by Jaymer »

Excellent 👍
Click Here to see a collection of my tips & hacks on this forum. Or search for "JaymerTip" in the search bar at the top.

Jaymer
Aware Programming & Consulting - Tampa FL
BLOMASKY
Posts: 1490
Joined: Wed Sep 30, 2015 10:08 pm
Location: Ocala FL

Re: Generic event handler for row operations working with records from Stored Procedure

Post by BLOMASKY »

This is very nice and very useful. (Way above my pay grade, but I am pretty good at copy and paste).

Thanks for sharing.

Bruce
nhofkes
Posts: 128
Joined: Mon Sep 07, 2020 6:03 am
Location: Netherlands

Re: Generic event handler for row operations working with records from Stored Procedure

Post by nhofkes »

The code in the first examples had in the render script some hard-coded parameters for the AwareApp.startProcess2 call (BO name, field containing BO ID and renderOption). I have now made this even more generic by passing the parameters in JSON to the render script. See screenshot for an example:

Screenshot 2024-03-16 230820.png
Screenshot 2024-03-16 230820.png (31.62 KiB) Viewed 13138 times

This enables passing all of the parameters (as described in this documentation) to the render script, as JSON after the keyword "startprocess2:".

The description of the parameters in JSON form reads:
/*
startProcess2: {
"processName": "Name_of_Aware_Process",
"ctxObjectName": "BO_Name",
"ctxObjectId": "Field_Containing_BO_ID",
"renderOption": "main"
}
*/
Note that the parameters must be named exactly as described in the documentation and that you must use double quotes (a JSON requirement, see here for more information).
The parameter ctxObjectId must contain the name of the column (field) containing the ID of the object that you need to be put in context for the row operation.
Because the whole part between the curly brackets is parsed as JSON, there is flexibility in the format. I put the parameters on separate lines, but you could also put everything on the same line or insert any whitespace.

Although the format for the javascript in the row operation is now a bit longer compared to the simple format used in the original version (see first post in this thread), there are some major advantages of this approach:
  • The code for the render script is now fully reusable. This should work for any query regardless of the type of BO to be returned without any changes to the render script.
  • There is the option to use all of the different renderOptions (main, popup, new_tab, #div etc), creating more flexibility where the output of the process will be displayed (except that I am not sure whether "AwareApp.getPanelId(frameType, tabName, panelName)" would work, I didn't test that).
  • More flexibility to specify in the row operation which BO ID must be put in context.
Suppose that you have a query for transactions, showing transferor, transferee and number/amount, and that the transferor ID and transferee ID are also included in the columns (but possibly hidden from view). You could now define two separate row operations, for example "View Transferor Details" and View Transferee Details". The first operation would specify the field "Transferor_ID" as the as the ctxObjectId parameter and the second operation could specify the field "Transferee_ID".

Revised code is below. Note that this is provided 'as is', it works on my computer and browser but I haven't done any cross-browser compatibility tests.

Generic event handler for row operation buttons displayed separately in each row

Code: Select all

// Regular Expression to find the startProcess2 parameters for data row operations, in JSON.
const startProcessRegExp = new RegExp("(?<=startProcess2:?)\\s*\\{.+\\}","gis");
widget.bind("dataBound", onDataBound);

function onDataBound(e) {
  grid.tbody.find("a.aw-link[data-operidx]").click(onDataOperationClick);
}

function onDataOperationClick(e) {
  let currentRow = $(this).parents("tr");
  let record = grid.dataItem(currentRow);
  let itemOperIdx = Number($(this).attr("data-operidx"));
  let processMatch = parser.m_itemOpers[itemOperIdx].operand.match(startProcessRegExp);
  if (record && processMatch) {
    try {
      let parameters = JSON.parse(processMatch[0]);
      AwareApp.startProcess2(
        parameters.processName,
        parameters.ctxObjectName,
        record[parameters.ctxObjectId],
        parameters.renderOption
      );
    } catch (e) {
      console.error("Error calling process. Parameters: ", parameters," - Error: ", e);
    }
  }
}



Generic event handler for row operation buttons combined behind a single menu button

Code: Select all

// Regular Expression to find the startProcess2 parameters for data row operations, in JSON.
const startProcessRegExp = new RegExp("(?<=startProcess2:?)\\s*\\{.+\\}","gis");
widget.bind("dataBound", onDataBound);

const menuButtons = {
  menuIDs: [],      // holds unique IDs for the row menus
  operations: {},   // hashtable for the row operations (operation name, AIM process name)
  listItems: "",    // selector for the listitems in the menu, to make sure that only menubuttons that have an operation class are included

  // Store operation names and corresponding parameters for AwareApp.startProcess2 in a hashtable
  // Create a selector for the css classes of the menu buttons
  // NOTE: each menu button must have a css class, but the class can be identical for all menu buttons
  initialise: function () {  
    parser.m_itemOpers.forEach((operation) => {
      let processMatch = operation.operand.match(startProcessRegExp);
      if (processMatch) {
        try {
          menuButtons.operations[operation.operName] = JSON.parse(processMatch[0]);
        } catch (e) {
          if (e instanceof SyntaxError) {
            console.error(`Syntax error in JSON input. ${e.name}: ${e.message}`);
          } else throw e;
        }
        if (operation.cssClass && !menuButtons.listItems.includes(operation.cssClass)) {
          menuButtons.listItems += ", li." + operation.cssClass;
        }
      }
    });
    if (menuButtons.listItems.length > 2) {
      menuButtons.listItems = menuButtons.listItems.substring(2); // Remove first ", "
    }
   
    // find all popup menus (one for each row) and store their IDs in an array
    if (this.listItems) {
      $("body")
        .find('div.k-menu-popup[data-role="buttonmenu"] ul.k-menu-group')
        .has(this.listItems)
        .each(function () {
          menuButtons.menuIDs.push($(this).attr("id"));
          $(this).click(onDataOperationMenuClick);
        });
    }
  },
};

function onDataBound(e) {
  menuButtons.initialise();
}

function onDataOperationMenuClick(e) {
  let rowIndex = menuButtons.menuIDs.indexOf($(this).attr("id"));
  if (rowIndex >= 0) {
    let currentRow = grid.tbody.find("tr").eq(rowIndex);
    let record = grid.dataItem(currentRow);
    let menuTextElement = $(e.target).closest('li').find('span.k-menu-link-text');
    let operName = menuTextElement ? menuTextElement.text() : null;
    if (record && operName && menuButtons.operations[operName]) {
      let parameters = menuButtons.operations[operName];
      try {
        AwareApp.startProcess2(
          parameters.processName,
          parameters.ctxObjectName,
          record[parameters.ctxObjectId],
          parameters.renderOption
        );
      } catch (e) {
       	console.error("Error calling process. Parameters: ", parameters," - Error: ", e);
      }
    }
  }
}

Niels
(V9.0 build 3272 - MariaDB - Windows)
Jaymer
Posts: 2528
Joined: Tue Jan 13, 2015 10:58 am
Location: Tampa, FL
Contact:

Re: Generic event handler for row operations working with records from Stored Procedure

Post by Jaymer »

Haven’t checked in at least 6 months but last I recall, the Alt-V search function doesn’t include this script.
(That sure would be nice wouldn’t it, to find all the places “RenderOption” is used, for example)

Can u check in V9 ?
If not, sending and Email to Vlad would help get this in an update.
Click Here to see a collection of my tips & hacks on this forum. Or search for "JaymerTip" in the search bar at the top.

Jaymer
Aware Programming & Consulting - Tampa FL
Jaymer
Posts: 2528
Joined: Tue Jan 13, 2015 10:58 am
Location: Tampa, FL
Contact:

Syntax for Margins

Post by Jaymer »

AwareApp.startProcess2('VP_ShowJobWithChat', 'jobs', ObjectID, { newTab: true, id: "JobWithChat", margins: {top: 0, bottom: 0, left: 0, right: 0} });


Since you are deep into this function,
This might come in handy in the future.
I don’t think it’s in the forum. I had this in an email.

The key part here is how you would embed the Margins for the new tab being opened.
Click Here to see a collection of my tips & hacks on this forum. Or search for "JaymerTip" in the search bar at the top.

Jaymer
Aware Programming & Consulting - Tampa FL
nhofkes
Posts: 128
Joined: Mon Sep 07, 2020 6:03 am
Location: Netherlands

Re: Generic event handler for row operations working with records from Stored Procedure

Post by nhofkes »

Jaymer wrote: Sun Mar 17, 2024 4:05 pm Haven’t checked in at least 6 months but last I recall, the Alt-V search function doesn’t include this script.
(That sure would be nice wouldn’t it, to find all the places “RenderOption” is used, for example)

Can u check in V9 ?
If not, sending and Email to Vlad would help get this in an update.
I am working in V9 and indeed ALT-V Search Function does not include custom javascript in the search. Would be good if this could be added in a next update.
Niels
(V9.0 build 3272 - MariaDB - Windows)
nhofkes
Posts: 128
Joined: Mon Sep 07, 2020 6:03 am
Location: Netherlands

Post by nhofkes »

Jaymer wrote: Sun Mar 17, 2024 4:17 pm AwareApp.startProcess2('VP_ShowJobWithChat', 'jobs', ObjectID, { newTab: true, id: "JobWithChat", margins: {top: 0, bottom: 0, left: 0, right: 0} });


Since you are deep into this function,
This might come in handy in the future.
I don’t think it’s in the forum. I had this in an email.

The key part here is how you would embed the Margins for the new tab being opened.
The render option "{ newTab: true, id: "JobWithChat", margins: {top: 0, bottom: 0, left: 0, right: 0} }" contains an object with another nested object. This wouldn't work with the event handler that I have now, because the parameter passed as renderOption is assumed to be a string which is simply passed on to AwareApp.startProcess2. For this to work the renderOption should first be converted into an object. Technically this would be possible through the JS eval() function, but that would require another parameter to signal when the renderOption is not a string but an object that must be evaluated first. I left that complexity out for now.
Niels
(V9.0 build 3272 - MariaDB - Windows)
Post Reply